Преглед изворни кода

docs: mark OA money field design as implemented

Update status and keep the implementation plan alongside the spec.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu пре 3 месеци
родитељ
комит
221a6fd360

+ 661 - 0
docs/superpowers/plans/2026-07-30-oa-money-field.md

@@ -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?

+ 2 - 3
docs/superpowers/specs/2026-07-30-oa-money-field-design.md

@@ -3,7 +3,7 @@
 **日期:** 2026-07-30  
 **范围:** `web-admin` 审批设计器步骤 2(表单设计)+ 发起/详情/打印运行时;条件分支已预留 `money`;后端发布白名单已含 `money`  
 **关联:** `docs/superpowers/specs/2026-07-14-oa-approval-dingtalk-design.md`;`docs/superpowers/specs/2026-07-15-oa-condition-from-form-design.md`;钉钉开放平台 `MoneyField`  
-**状态:** 待用户评审  
+**状态:** 已实现  
 
 ---
 
@@ -187,5 +187,4 @@ type MoneyFormValue = number | null
 
 - FormCreate 若内置同名 `money` 控件,注册时需确认覆盖策略与 DragRule `name` 不冲突;以本仓库自定义组件为准。  
 - 千分位仅影响输入展示,条件与存值必须是纯 number,避免字符串比较。  
-- Tailwind 不进入 `oa-fc-host`;设计器内预览样式写在宿主局部 CSS。  
-```
+- Tailwind 不进入 `oa-fc-host`;设计器内预览样式写在宿主局部 CSS。