|
|
@@ -0,0 +1,661 @@
|
|
|
+# OA Money Field (金额 + 大写) Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** Deliver FormCreate `money` control so designers can drag「金额」, configure uppercase display and condition participation, and initiators/approvers see numeric input with optional RMB uppercase (DingTalk-aligned).
|
|
|
+
|
|
|
+**Architecture:** Mirror `dept` / `relatedApproval` four-layer FcDesigner extension: `OaMoneyField.vue` + DragRule + `setupOaMoneyField` on both main app and `oa-fc-host`. Persist `number | null` only; uppercase is display-only via `toRmbUpper`. Backend validates non-negative finite numbers on start. Condition eligibility already includes `money`.
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI · pytest · Vue 3 · FormCreate / `@form-create/designer` · Element Plus · existing `money-format` helpers · Node `node:test` + `tsx` for `rmb-upper` unit tests
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-07-30-oa-money-field-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| Path | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| Create: `web-admin/src/utils/rmb-upper.ts` | `number` → 人民币大写纯函数 |
|
|
|
+| Create: `web-admin/src/utils/rmb-upper.test.ts` | Node test runner cases |
|
|
|
+| Create: `web-admin/src/components/oa-approval/OaMoneyField.vue` | Runtime / designer preview 金额控件 |
|
|
|
+| Create: `web-admin/src/utils/oa-money-field-rule.ts` | DragRule(`showUpper` 默认 true) |
|
|
|
+| Create: `web-admin/src/utils/oa-form-create-money.ts` | `setupOaMoneyField` 注册 |
|
|
|
+| Modify: `web-admin/src/main.ts` | 调用 `setupOaMoneyField` |
|
|
|
+| Modify: `web-admin/src/oa-fc-host/main.ts` | 同上 |
|
|
|
+| Modify: `web-admin/src/utils/oa-approval-print.ts` | 打印金额:数字 + 可选大写 |
|
|
|
+| Create: `backend/app/services/oa_money_field.py` | `validate_money_form_values` |
|
|
|
+| Create: `backend/tests/test_oa_money_field.py` | 校验单测 |
|
|
|
+| Modify: `backend/app/services/oa_engine.py` | `start_instance` 挂入校验 |
|
|
|
+| Modify: `backend/app/services/oa_related_approval.py` | 关联摘要格式化金额为千分位数字 |
|
|
|
+| Modify: `docs/superpowers/specs/2026-07-30-oa-money-field-design.md` | 状态更新;去掉文末多余 fence |
|
|
|
+
|
|
|
+**已具备、本计划不改:** `CONDITION_ELIGIBLE_TYPES`(前后端均含 `money`)、`oa_publish.ALLOWED_CONTROL_TYPES`、`ConditionRuleEditor` 对 `money` 的数值 coerce。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: RMB uppercase utility (TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `web-admin/src/utils/rmb-upper.ts`
|
|
|
+- Create: `web-admin/src/utils/rmb-upper.test.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: Write failing tests**
|
|
|
+
|
|
|
+```ts
|
|
|
+// web-admin/src/utils/rmb-upper.test.ts
|
|
|
+import { describe, it } from 'node:test'
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import { toRmbUpper } from './rmb-upper'
|
|
|
+
|
|
|
+describe('toRmbUpper', () => {
|
|
|
+ it('zero', () => {
|
|
|
+ assert.equal(toRmbUpper(0), '零元整')
|
|
|
+ })
|
|
|
+ it('integer', () => {
|
|
|
+ assert.equal(toRmbUpper(1), '壹元整')
|
|
|
+ assert.equal(toRmbUpper(10), '壹拾元整')
|
|
|
+ assert.equal(toRmbUpper(100), '壹佰元整')
|
|
|
+ assert.equal(toRmbUpper(1234), '壹仟贰佰叁拾肆元整')
|
|
|
+ })
|
|
|
+ it('jiao fen', () => {
|
|
|
+ assert.equal(toRmbUpper(1.23), '壹元贰角叁分')
|
|
|
+ assert.equal(toRmbUpper(0.5), '伍角')
|
|
|
+ assert.equal(toRmbUpper(0.05), '伍分')
|
|
|
+ assert.equal(toRmbUpper(1.2), '壹元贰角')
|
|
|
+ assert.equal(toRmbUpper(1.03), '壹元零叁分')
|
|
|
+ })
|
|
|
+ it('nullish and invalid', () => {
|
|
|
+ assert.equal(toRmbUpper(null), '')
|
|
|
+ assert.equal(toRmbUpper(undefined), '')
|
|
|
+ assert.equal(toRmbUpper(Number.NaN), '')
|
|
|
+ assert.equal(toRmbUpper(-1), '')
|
|
|
+ })
|
|
|
+ it('too large', () => {
|
|
|
+ assert.equal(toRmbUpper(1e12), '金额过大')
|
|
|
+ })
|
|
|
+ it('near max has yuan jiao fen', () => {
|
|
|
+ const s = toRmbUpper(999999999999.99)
|
|
|
+ assert.ok(s.includes('元'))
|
|
|
+ assert.ok(s.includes('角') || s.includes('分'))
|
|
|
+ assert.notEqual(s, '金额过大')
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run tests — expect FAIL**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx --test src/utils/rmb-upper.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(模块不存在或 `toRmbUpper` 未定义)
|
|
|
+
|
|
|
+- [ ] **Step 3: Implement `toRmbUpper`**
|
|
|
+
|
|
|
+Create `web-admin/src/utils/rmb-upper.ts` with a complete algorithm (no pseudo-code left in the file):
|
|
|
+
|
|
|
+- Digits: `零壹贰叁肆伍陆柒捌玖`;section units `万/亿`;int units `拾佰仟`.
|
|
|
+- Convert via fen: `Math.round(value * 100)`; reject `value > 999999999999.99` → `金额过大`.
|
|
|
+- `null` / `undefined` / `NaN` / negative / non-finite → `''`.
|
|
|
+- `0` → `零元整`.
|
|
|
+- Integer yuan with no jiao/fen → append `整`.
|
|
|
+- Only jiao/fen (no yuan) → e.g. `伍角` / `伍分` without forcing `零元`.
|
|
|
+- `1.03` → include `零` before `叁分` as in bank-style wording.
|
|
|
+
|
|
|
+Export: `export function toRmbUpper(value: number | null | undefined): string`
|
|
|
+
|
|
|
+- [ ] **Step 4: Run tests — expect PASS**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx --test src/utils/rmb-upper.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/rmb-upper.ts web-admin/src/utils/rmb-upper.test.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add RMB uppercase converter for OA money field
|
|
|
+
|
|
|
+Pure function with node:test coverage for zero, jiao/fen, and range limits.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Backend money form_data validation (TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/app/services/oa_money_field.py`
|
|
|
+- Create: `backend/tests/test_oa_money_field.py`
|
|
|
+- Modify: `backend/app/services/oa_engine.py`(`start_instance` 中 `validate_dept_form_values` 附近)
|
|
|
+
|
|
|
+- [ ] **Step 1: Write failing tests**
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/tests/test_oa_money_field.py
|
|
|
+import unittest
|
|
|
+
|
|
|
+from app.services.oa_money_field import validate_money_form_values
|
|
|
+
|
|
|
+
|
|
|
+class TestValidateMoneyFormValues(unittest.TestCase):
|
|
|
+ def test_ok_number(self):
|
|
|
+ schema = {"rule": [{"type": "money", "field": "amt", "title": "金额", "$required": True}]}
|
|
|
+ self.assertEqual(validate_money_form_values(schema, {"amt": 1234.56}), [])
|
|
|
+
|
|
|
+ def test_ok_int(self):
|
|
|
+ schema = {"rule": [{"type": "money", "field": "amt"}]}
|
|
|
+ self.assertEqual(validate_money_form_values(schema, {"amt": 10}), [])
|
|
|
+
|
|
|
+ def test_ok_null_optional(self):
|
|
|
+ schema = {"rule": [{"type": "money", "field": "amt", "$required": False}]}
|
|
|
+ self.assertEqual(validate_money_form_values(schema, {"amt": None}), [])
|
|
|
+ self.assertEqual(validate_money_form_values(schema, {}), [])
|
|
|
+
|
|
|
+ def test_required_missing(self):
|
|
|
+ schema = {
|
|
|
+ "rule": [{"type": "money", "field": "amt", "title": "报销金额", "$required": True}]
|
|
|
+ }
|
|
|
+ errs = validate_money_form_values(schema, {})
|
|
|
+ self.assertTrue(any("报销金额" in e for e in errs))
|
|
|
+
|
|
|
+ def test_negative_rejected(self):
|
|
|
+ schema = {"rule": [{"type": "money", "field": "amt"}]}
|
|
|
+ errs = validate_money_form_values(schema, {"amt": -1})
|
|
|
+ self.assertTrue(len(errs) >= 1)
|
|
|
+
|
|
|
+ def test_string_number_accepted(self):
|
|
|
+ schema = {"rule": [{"type": "money", "field": "amt"}]}
|
|
|
+ self.assertEqual(validate_money_form_values(schema, {"amt": "12.5"}), [])
|
|
|
+
|
|
|
+ def test_bool_rejected(self):
|
|
|
+ schema = {"rule": [{"type": "money", "field": "amt"}]}
|
|
|
+ errs = validate_money_form_values(schema, {"amt": True})
|
|
|
+ self.assertTrue(len(errs) >= 1)
|
|
|
+
|
|
|
+ def test_ignore_other_types(self):
|
|
|
+ schema = {"rule": [{"type": "input", "field": "x", "$required": True}]}
|
|
|
+ self.assertEqual(validate_money_form_values(schema, {}), [])
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run — expect FAIL**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -m pytest tests/test_oa_money_field.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(`oa_money_field` 不存在)
|
|
|
+
|
|
|
+- [ ] **Step 3: Implement validator**
|
|
|
+
|
|
|
+Create `backend/app/services/oa_money_field.py` following the same private helpers pattern as `oa_dept_select.py` (`_walk_rules`, `_schema_rules`, `_is_required`, `_field_label`).
|
|
|
+
|
|
|
+Rules:
|
|
|
+
|
|
|
+- `MAX_MONEY = 999999999999.99`
|
|
|
+- Normalize: `int`/`float` OK;数字字符串去逗号后 `float`;`bool` 拒绝;其它类型拒绝
|
|
|
+- `None` / `""` → 空;required 时空 → `「{label}」为必填金额字段`
|
|
|
+- `< 0` → `「{label}」金额不能为负数`
|
|
|
+- `> MAX_MONEY` / 非有限 → 对应错误文案
|
|
|
+- 仅处理 `type == "money"`
|
|
|
+
|
|
|
+Export: `validate_money_form_values(schema, form_data) -> List[str]`
|
|
|
+
|
|
|
+- [ ] **Step 4: Wire into `start_instance`**
|
|
|
+
|
|
|
+In `backend/app/services/oa_engine.py` immediately after `validate_dept_form_values`:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.services.oa_money_field import validate_money_form_values
|
|
|
+
|
|
|
+money_errs = validate_money_form_values(schema, data)
|
|
|
+if money_errs:
|
|
|
+ raise OaEngineError(money_errs[0])
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Run tests — expect PASS**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -m pytest tests/test_oa_money_field.py -v
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/oa_money_field.py backend/tests/test_oa_money_field.py backend/app/services/oa_engine.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(backend): validate OA money field values on instance start
|
|
|
+
|
|
|
+Reject negatives, non-finite values, and missing required money controls.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: DragRule + FormCreate registration
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `web-admin/src/utils/oa-money-field-rule.ts`
|
|
|
+- Create: `web-admin/src/utils/oa-form-create-money.ts`
|
|
|
+- Create: `web-admin/src/components/oa-approval/OaMoneyField.vue`(最小 stub,Task 4 充实)
|
|
|
+- Modify: `web-admin/src/main.ts`
|
|
|
+- Modify: `web-admin/src/oa-fc-host/main.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add DragRule**
|
|
|
+
|
|
|
+```ts
|
|
|
+// web-admin/src/utils/oa-money-field-rule.ts
|
|
|
+let _uidSeq = 0
|
|
|
+function uniqueId() {
|
|
|
+ const num = 370 + ++_uidSeq
|
|
|
+ return (
|
|
|
+ 'F' +
|
|
|
+ Math.random().toString(36).slice(3, 6) +
|
|
|
+ Number(`${Date.now()}`).toString(36) +
|
|
|
+ num.toString(36)
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export const moneyFieldDragRule = {
|
|
|
+ menu: 'main' as const,
|
|
|
+ icon: 'icon-number',
|
|
|
+ label: '金额',
|
|
|
+ name: 'money',
|
|
|
+ input: true,
|
|
|
+ mask: true,
|
|
|
+ validate: ['number', 'string'],
|
|
|
+ event: ['change'],
|
|
|
+ rule() {
|
|
|
+ return {
|
|
|
+ type: 'money',
|
|
|
+ field: uniqueId(),
|
|
|
+ title: '金额(元)',
|
|
|
+ info: '',
|
|
|
+ $required: false,
|
|
|
+ props: {
|
|
|
+ showUpper: true,
|
|
|
+ placeholder: '请输入金额'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ props() {
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ type: 'switch',
|
|
|
+ field: 'showUpper',
|
|
|
+ title: '显示大写金额',
|
|
|
+ value: true,
|
|
|
+ props: { activeText: '是', inactiveText: '否' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'input',
|
|
|
+ field: 'placeholder',
|
|
|
+ title: '占位提示',
|
|
|
+ value: '请输入金额'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'input',
|
|
|
+ field: 'formCreateInfo',
|
|
|
+ title: '提示信息'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+若 `icon-number` 在设计器不显示,改为仓库中部门/数字类已用过的 `fc-icon` 名(打开 `@form-create/designer` 图标表核对)。
|
|
|
+
|
|
|
+- [ ] **Step 2: Stub component + setup helper**
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- web-admin/src/components/oa-approval/OaMoneyField.vue -->
|
|
|
+<template>
|
|
|
+ <div class="oa-money-field">金额</div>
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+ defineOptions({ name: 'money' })
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+```ts
|
|
|
+// web-admin/src/utils/oa-form-create-money.ts
|
|
|
+import FcDesigner from '@form-create/designer'
|
|
|
+import OaMoneyField from '@/components/oa-approval/OaMoneyField.vue'
|
|
|
+import { moneyFieldDragRule } from '@/utils/oa-money-field-rule'
|
|
|
+
|
|
|
+const FLAG = '__oaMoneyFieldConfigured'
|
|
|
+
|
|
|
+export function setupOaMoneyField(formCreate?: { component?: (name: string, c: unknown) => void }) {
|
|
|
+ if ((FcDesigner as any)[FLAG]) return
|
|
|
+ FcDesigner.component('money', OaMoneyField)
|
|
|
+ if (typeof formCreate?.component === 'function') {
|
|
|
+ formCreate.component('money', OaMoneyField)
|
|
|
+ }
|
|
|
+ if (typeof FcDesigner.addDragRule === 'function') {
|
|
|
+ FcDesigner.addDragRule(moneyFieldDragRule)
|
|
|
+ }
|
|
|
+ ;(FcDesigner as any)[FLAG] = true
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Register both hosts**
|
|
|
+
|
|
|
+In `web-admin/src/main.ts` after `setupOaDeptSelect`:
|
|
|
+
|
|
|
+```ts
|
|
|
+import { setupOaMoneyField } from '@/utils/oa-form-create-money'
|
|
|
+setupOaMoneyField(FcDesigner.formCreate)
|
|
|
+```
|
|
|
+
|
|
|
+Same pair of lines in `web-admin/src/oa-fc-host/main.ts`.
|
|
|
+
|
|
|
+- [ ] **Step 4: Smoke check(人工)**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm dev
|
|
|
+```
|
|
|
+
|
|
|
+打开审批设计器 →「2 表单设计」→ 左侧「组件」出现「金额」。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/oa-money-field-rule.ts \
|
|
|
+ web-admin/src/utils/oa-form-create-money.ts \
|
|
|
+ web-admin/src/components/oa-approval/OaMoneyField.vue \
|
|
|
+ web-admin/src/main.ts \
|
|
|
+ web-admin/src/oa-fc-host/main.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): register OA money field in FormCreate designer
|
|
|
+
|
|
|
+Add DragRule and dual-host setup so 金额 appears in the component palette.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: Runtime `OaMoneyField.vue`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/oa-approval/OaMoneyField.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Replace stub with full component**
|
|
|
+
|
|
|
+Behavior per spec §5; structure mirrors `OaDeptSelect` (`disabled` / designer host):
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <div class="oa-money-field">
|
|
|
+ <template v-if="isDesignerHost">
|
|
|
+ <el-input model-value="" :placeholder="placeholder || '请输入金额'" disabled />
|
|
|
+ <div v-if="showUpper" class="oa-money-field__upper">大写金额预览</div>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="disabled">
|
|
|
+ <div class="oa-money-field__readonly">
|
|
|
+ <span>{{ displayNumber || '—' }}</span>
|
|
|
+ <div v-if="showUpper && upperText" class="oa-money-field__upper">{{ upperText }}</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <AmountInput
|
|
|
+ :model-value="numericValue"
|
|
|
+ :placeholder="placeholder || '请输入金额'"
|
|
|
+ :min="0"
|
|
|
+ :precision="2"
|
|
|
+ @update:model-value="onInput"
|
|
|
+ />
|
|
|
+ <div v-if="showUpper && upperText" class="oa-money-field__upper">{{ upperText }}</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import { computed } from 'vue'
|
|
|
+ import AmountInput from '@/components/common/amount-input/index.vue'
|
|
|
+ import { formatMoneyDisplay } from '@/utils/money-format'
|
|
|
+ import { toRmbUpper } from '@/utils/rmb-upper'
|
|
|
+
|
|
|
+ defineOptions({ name: 'money' })
|
|
|
+
|
|
|
+ const props = withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ modelValue?: number | string | null
|
|
|
+ disabled?: boolean
|
|
|
+ showUpper?: boolean
|
|
|
+ placeholder?: string
|
|
|
+ formCreateInject?: Record<string, unknown>
|
|
|
+ }>(),
|
|
|
+ { showUpper: true, placeholder: '请输入金额' }
|
|
|
+ )
|
|
|
+
|
|
|
+ const emit = defineEmits<{
|
|
|
+ (e: 'update:modelValue', v: number | null): void
|
|
|
+ (e: 'change', v: number | null): void
|
|
|
+ }>()
|
|
|
+
|
|
|
+ function detectDesignerHost() {
|
|
|
+ try {
|
|
|
+ return /oa-fc-host\.html/i.test(String(window.location?.pathname || ''))
|
|
|
+ } catch {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const isDesignerHost = detectDesignerHost()
|
|
|
+
|
|
|
+ function toNum(v: unknown): number | null {
|
|
|
+ if (v == null || v === '') return null
|
|
|
+ const n = typeof v === 'number' ? v : Number(String(v).replace(/,/g, ''))
|
|
|
+ if (!Number.isFinite(n) || n < 0) return null
|
|
|
+ return Math.round(n * 100) / 100
|
|
|
+ }
|
|
|
+
|
|
|
+ const numericValue = computed(() => toNum(props.modelValue))
|
|
|
+ const displayNumber = computed(() => formatMoneyDisplay(numericValue.value, 2))
|
|
|
+ const upperText = computed(() => {
|
|
|
+ const n = numericValue.value
|
|
|
+ if (n == null) return ''
|
|
|
+ return toRmbUpper(n)
|
|
|
+ })
|
|
|
+
|
|
|
+ function onInput(v: number | null) {
|
|
|
+ const next = v == null || v < 0 ? null : Math.round(Number(v) * 100) / 100
|
|
|
+ emit('update:modelValue', next)
|
|
|
+ emit('change', next)
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .oa-money-field__upper {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.4;
|
|
|
+ color: var(--mint-text-muted, #64748b);
|
|
|
+ }
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+Constraints:
|
|
|
+
|
|
|
+- Persist `number | null` only; never emit uppercase string.
|
|
|
+- `showUpper` comes from rule `props` (FormCreate expands props onto the component).
|
|
|
+- Explicitly import `AmountInput` (same as production-material).
|
|
|
+
|
|
|
+- [ ] **Step 2: Manual verify**
|
|
|
+
|
|
|
+1. Designer: drag 金额;「显示大写金额」default on.
|
|
|
+2. Publish + start: enter `1234.56`, uppercase shows; toggle off → no uppercase line.
|
|
|
+3. Detail readonly: number + uppercase.
|
|
|
+4. Check「参与流程条件分支」→ required locked → field appears in flow condition candidates.
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/oa-approval/OaMoneyField.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): implement OA money field with optional RMB uppercase
|
|
|
+
|
|
|
+Numeric AmountInput plus muted uppercase line; designer host shows preview only.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: Print + related-summary formatting
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/utils/oa-approval-print.ts`
|
|
|
+- Modify: `backend/app/services/oa_related_approval.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: Print path**
|
|
|
+
|
|
|
+In `oa-approval-print.ts`, beside the `dept` branch:
|
|
|
+
|
|
|
+```ts
|
|
|
+import { toRmbUpper } from '@/utils/rmb-upper'
|
|
|
+import { formatMoneyDisplay } from '@/utils/money-format'
|
|
|
+
|
|
|
+if (type === 'money') {
|
|
|
+ const showUpper = rule.props?.showUpper !== false
|
|
|
+ const n =
|
|
|
+ value == null || value === ''
|
|
|
+ ? null
|
|
|
+ : typeof value === 'number'
|
|
|
+ ? value
|
|
|
+ : Number(String(value).replace(/,/g, ''))
|
|
|
+ if (n == null || !Number.isFinite(n)) {
|
|
|
+ out.push({ label, text: '-' })
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const numText = formatMoneyDisplay(n, 2)
|
|
|
+ const upper = showUpper ? toRmbUpper(n) : ''
|
|
|
+ out.push({
|
|
|
+ label,
|
|
|
+ text: upper ? `${numText}(${upper})` : numText
|
|
|
+ })
|
|
|
+ continue
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Use `(大写)` inline so single-line print rows remain compatible with `OaApprovalPrintSheet`.
|
|
|
+
|
|
|
+- [ ] **Step 2: Related approval summary**
|
|
|
+
|
|
|
+In `oa_related_approval.py` flatten display (next to `typ == "dept"`):
|
|
|
+
|
|
|
+```python
|
|
|
+if typ == "money":
|
|
|
+ if raw is None or raw == "":
|
|
|
+ return "", False
|
|
|
+ try:
|
|
|
+ if isinstance(raw, bool):
|
|
|
+ return "", False
|
|
|
+ v = float(str(raw).replace(",", "").strip())
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return str(raw), False
|
|
|
+ text = f"{v:,.2f}"
|
|
|
+ return text, False
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Manual print check** on an instance that includes a money field.
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/oa-approval-print.ts backend/app/services/oa_related_approval.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(oa): format money fields in print and related-approval summary
|
|
|
+
|
|
|
+Show formatted amount and optional uppercase on print; summary uses numeric text.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: Spec status + verification
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `docs/superpowers/specs/2026-07-30-oa-money-field-design.md`
|
|
|
+
|
|
|
+- [ ] **Step 1: Spec polish**
|
|
|
+
|
|
|
+- Set status to `已实现`.
|
|
|
+- Remove the stray trailing ` ``` ` at end of §10 if still present.
|
|
|
+
|
|
|
+- [ ] **Step 2: Regression commands**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec tsx --test src/utils/rmb-upper.test.ts
|
|
|
+cd backend && source venv/bin/activate && python -m pytest tests/test_oa_money_field.py tests/test_oa_publish.py -v --tb=short
|
|
|
+cd web-admin && pnpm build:typecheck
|
|
|
+```
|
|
|
+
|
|
|
+Expected: tests pass; typecheck has no new errors from this change.
|
|
|
+
|
|
|
+- [ ] **Step 3: Acceptance checklist(spec §9)**
|
|
|
+
|
|
|
+1. Designer palette shows「金额」
|
|
|
+2.「显示大写金额」default on; toggle works
|
|
|
+3. Condition participation + required lock + flow candidates
|
|
|
+4. Start form: `1234.56` + uppercase; `form_data` numeric
|
|
|
+5. Uppercase off → no uppercase line
|
|
|
+6. Detail/print consistent
|
|
|
+7. `rmb-upper` tests pass
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-07-30-oa-money-field-design.md
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+docs: mark OA money field design as implemented
|
|
|
+
|
|
|
+Update status after money control delivery and remove stray fence.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Update knowledge graph**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh all
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage self-check
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| 自定义 `money` + 双侧注册 | 3–4 |
|
|
|
+| `showUpper` 默认 true | 3–4 |
|
|
|
+| 只存 number | 2 + 4 |
|
|
|
+| 大写实时换算 / 范围 / 单测 | 1 |
|
|
|
+| 参与条件(现有基础设施) | 4 手工验收 |
|
|
|
+| 后端发起校验 | 2 |
|
|
|
+| 打印 + 大写 | 5 |
|
|
|
+| 非目标(多币种/小程序/落库大写) | 不实现 |
|
|
|
+
|
|
|
+## Placeholder scan
|
|
|
+
|
|
|
+No TBD steps. Icon name may need one-line swap if `icon-number` missing from FcDesigner icon set.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Execution handoff
|
|
|
+
|
|
|
+Plan complete and saved to `docs/superpowers/plans/2026-07-30-oa-money-field.md`.
|
|
|
+
|
|
|
+**Two execution options:**
|
|
|
+
|
|
|
+1. **Subagent-Driven (recommended)** — fresh subagent per task, review between tasks
|
|
|
+2. **Inline Execution** — execute in this session with `executing-plans` and checkpoints
|
|
|
+
|
|
|
+Which approach?
|