2026-07-30-oa-person-select.md 26 KB

OA Person Select(人员)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 person control (dept-aligned UI) so designers can drag「人员」, configure multi-select / max / condition participation; conditions use contains-only with OR semantics; runtime + designer preview + start-page flow preview all work.

Architecture: Mirror dept / money four-layer FcDesigner extension: OaPersonSelect.vue + DragRule + setupOaPersonSelect on main app and oa-fc-host. Persist { id, name } / array. Engine matches op === 'contains' via id-set intersection (not formula ==). Preview: device canvas ._fc-m-drag.mobile (400px) +「预览」dialog ViewForm must render the registered component and stay full-width / wrap tags.

Tech Stack: FastAPI · pytest · Vue 3 · FormCreate / @form-create/designer · Element Plus · existing fetchUserList

Spec: docs/superpowers/specs/2026-07-30-oa-person-select-design.md


File map

Path Responsibility
Create: web-admin/src/utils/oa-person-value.ts normalize / display helpers
Create: web-admin/src/utils/oa-person-value.test.ts Node unit tests
Create: web-admin/src/utils/oa-person-select-rule.ts Designer DragRule
Create: web-admin/src/utils/oa-form-create-person.ts setupOaPersonSelect
Create: web-admin/src/components/oa-approval/OaPersonSelect.vue Runtime + designer canvas / preview
Modify: web-admin/src/main.ts register person
Modify: web-admin/src/oa-fc-host/main.ts register person (designer + 预览 dialog)
Modify: web-admin/src/assets/styles/core/oa-fc-runtime.scss .oa-person-select full-width
Modify: web-admin/src/components/oa-approval/oa-condition-fields.ts person + contains
Modify: web-admin/src/components/oa-approval/flow-tree/types.ts ConditionOp / value type
Modify: web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue person multi-select RHS
Modify: web-admin/src/utils/oa-approval-print.ts print names
Create: backend/app/services/oa_person_select.py form_data validation + id extract helpers
Create: backend/tests/test_oa_person_select.py validation tests
Modify: backend/app/services/oa_publish.py FILLABLE + CONDITION eligible
Modify: backend/app/services/oa_engine.py contains in _match_condition_group;start_instance validate
Modify: backend/tests/test_oa_engine.py contains match cases
Modify: backend/app/services/oa_related_approval.py summary format
Modify: backend/tests/test_oa_related_approval.py person format assert
Modify: docs/superpowers/specs/2026-07-30-oa-person-select-design.md preview section + status

Preview compatibility (must cover)

FormCreate has two preview surfaces in oa-fc-host:

  1. Device switch(顶部 PC / Pad / Mobile):._fc-m-drag gets class mobile → width 400px(见 @form-create/designer style/index.css)。window.matchMedia('(max-width: 767px)') 不会因 device 切换变 true(iframe 仍很宽)。因此:
    • 不要仅靠 matchMedia 才能在手机画布上可用;PC 的 el-select 必须在 400px 容器内全宽、tag 可换行、不撑破布局。
    • 窄屏抽屉路径仍保留:真实窄 viewport(<768)或后续真机使用。
  2. 「预览」按钮 → el-dialog + ViewForm:必须已 formCreate.component('person', …),否则预览里控件空白/报错。双侧 setupOaPersonSelect 已覆盖。
  3. 发起页流程预览(fetchOaFlowPreview):改 form_data 后走同一套 _match_condition_group;contains 引擎改完后自然生效。手测:人员勾选进入条件名单 → 预览走对应分支。

CSS:在 oa-fc-runtime.scss 把 .oa-person-select 与 .oa-dept-select 并列,保证 width/max-width/min-width:0。


Task 1: Person value helpers (TDD)

Files:

  • Create: web-admin/src/utils/oa-person-value.ts
  • Create: web-admin/src/utils/oa-person-value.test.ts

  • [ ] Step 1: Write failing tests

// web-admin/src/utils/oa-person-value.test.ts
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
  extractPersonIds,
  formatPersonDisplay,
  isPersonValueItem,
  normalizePersonModelValue
} from './oa-person-value'

describe('oa-person-value', () => {
  it('isPersonValueItem', () => {
    assert.equal(isPersonValueItem({ id: 'u1', name: '张三' }), true)
    assert.equal(isPersonValueItem({ id: '', name: 'x' }), false)
    assert.equal(isPersonValueItem(null), false)
  })

  it('normalize single', () => {
    assert.deepEqual(normalizePersonModelValue({ id: 'u1', name: '张三' }, false), {
      id: 'u1',
      name: '张三'
    })
    assert.equal(normalizePersonModelValue(null, false), null)
  })

  it('normalize multiple', () => {
    assert.deepEqual(
      normalizePersonModelValue(
        [
          { id: 'u1', name: '张三' },
          { id: 'u2', name: '李四' }
        ],
        true
      ),
      [
        { id: 'u1', name: '张三' },
        { id: 'u2', name: '李四' }
      ]
    )
    assert.deepEqual(normalizePersonModelValue(null, true), [])
  })

  it('format and extract ids', () => {
    assert.equal(
      formatPersonDisplay([
        { id: 'u1', name: '张三' },
        { id: 'u2', name: '李四' }
      ]),
      '张三、李四'
    )
    assert.deepEqual(
      extractPersonIds({ id: 'u1', name: '张三' }),
      ['u1']
    )
    assert.deepEqual(
      extractPersonIds([
        { id: 'u1', name: '张三' },
        { id: 'u2', name: '李四' }
      ]),
      ['u1', 'u2']
    )
  })
})
  • Step 2: Run tests — expect FAIL
cd web-admin && pnpm exec tsx --test src/utils/oa-person-value.test.ts

Expected: module not found / FAIL

  • Step 3: Implement
// web-admin/src/utils/oa-person-value.ts
export type OaPersonValueItem = { id: string; name: string }

export function isPersonValueItem(v: unknown): v is OaPersonValueItem {
  if (!v || typeof v !== 'object') return false
  const o = v as Record<string, unknown>
  return String(o.id || '').trim() !== '' && String(o.name || '').trim() !== ''
}

export function normalizePersonModelValue(
  raw: unknown,
  multiple: boolean
): OaPersonValueItem | OaPersonValueItem[] | null {
  if (multiple) {
    if (Array.isArray(raw)) {
      return raw.filter(isPersonValueItem).map((x) => ({
        id: String(x.id).trim(),
        name: String(x.name).trim()
      }))
    }
    if (isPersonValueItem(raw)) {
      return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }]
    }
    return []
  }
  if (isPersonValueItem(raw)) {
    return { id: String(raw.id).trim(), name: String(raw.name).trim() }
  }
  if (Array.isArray(raw) && raw.length === 1 && isPersonValueItem(raw[0])) {
    return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() }
  }
  return null
}

export function formatPersonDisplay(raw: unknown): string {
  if (Array.isArray(raw)) {
    return raw
      .filter(isPersonValueItem)
      .map((x) => x.name)
      .filter(Boolean)
      .join('、')
  }
  if (isPersonValueItem(raw)) return raw.name
  return ''
}

export function extractPersonIds(raw: unknown): string[] {
  const n = Array.isArray(raw)
    ? raw.filter(isPersonValueItem)
    : isPersonValueItem(raw)
      ? [raw]
      : []
  const out: string[] = []
  const seen = new Set<string>()
  for (const x of n) {
    const id = String(x.id).trim()
    if (!id || seen.has(id)) continue
    seen.add(id)
    out.push(id)
  }
  return out
}
  • Step 4: Run tests — expect PASS
cd web-admin && pnpm exec tsx --test src/utils/oa-person-value.test.ts
  • Step 5: Commit
git add web-admin/src/utils/oa-person-value.ts web-admin/src/utils/oa-person-value.test.ts
git commit -m "feat(web-admin): add OA person value normalize helpers"

Task 2: Backend person validation + publish whitelist (TDD)

Files:

  • Create: backend/app/services/oa_person_select.py
  • Create: backend/tests/test_oa_person_select.py
  • Modify: backend/app/services/oa_publish.py (FILLABLE_TYPES + CONDITION_ELIGIBLE_TYPES)
  • Modify: backend/app/services/oa_engine.py (start_instance call validate)

  • [ ] Step 1: Write failing tests

# backend/tests/test_oa_person_select.py
import unittest
from app.services.oa_person_select import (
    extract_person_ids,
    person_contains_match,
    validate_person_form_values,
)

class TestOaPersonSelect(unittest.TestCase):
    def test_validate_multi_ok(self):
        schema = {
            "rule": [
                {
                    "type": "person",
                    "field": "F1",
                    "title": "协同人",
                    "props": {"multiple": True},
                }
            ]
        }
        data = {"F1": [{"id": "u1", "name": "张三"}, {"id": "u2", "name": "李四"}]}
        self.assertEqual(validate_person_form_values(schema, data), [])

    def test_validate_required_empty(self):
        schema = {
            "rule": [
                {
                    "type": "person",
                    "field": "F1",
                    "title": "协同人",
                    "$required": True,
                    "props": {"multiple": True},
                }
            ]
        }
        errs = validate_person_form_values(schema, {"F1": []})
        self.assertTrue(any("必填" in e for e in errs))

    def test_validate_max(self):
        schema = {
            "rule": [
                {
                    "type": "person",
                    "field": "F1",
                    "title": "协同人",
                    "props": {"multiple": True, "max": 1},
                }
            ]
        }
        errs = validate_person_form_values(
            schema,
            {"F1": [{"id": "u1", "name": "A"}, {"id": "u2", "name": "B"}]},
        )
        self.assertTrue(any("上限" in e for e in errs))

    def test_extract_and_contains(self):
        self.assertEqual(extract_person_ids({"id": "u1", "name": "A"}), {"u1"})
        self.assertTrue(
            person_contains_match(
                [{"id": "u1", "name": "A"}],
                ["u1", "u2"],
            )
        )
        self.assertFalse(
            person_contains_match(
                [{"id": "u3", "name": "C"}],
                ["u1", "u2"],
            )
        )

if __name__ == "__main__":
    unittest.main()
  • Step 2: Run — expect FAIL
cd backend && source venv/bin/activate && pytest tests/test_oa_person_select.py -v
  • Step 3: Implement oa_person_select.py

Copy structure from oa_dept_select.py, replace type "dept" → "person", labels「人员」, and add:

def extract_person_ids(raw: Any) -> set:
    ids = set()
    items = raw if isinstance(raw, list) else ([raw] if isinstance(raw, dict) else [])
    for item in items:
        if not isinstance(item, dict):
            continue
        pid = str(item.get("id") or "").strip()
        if pid:
            ids.add(pid)
    return ids


def person_contains_match(form_value: Any, cond_ids: Any) -> bool:
    form_ids = extract_person_ids(form_value)
    if isinstance(cond_ids, str):
        want = {cond_ids.strip()} if cond_ids.strip() else set()
    elif isinstance(cond_ids, list):
        want = {str(x).strip() for x in cond_ids if str(x).strip()}
    else:
        want = set()
    return bool(form_ids & want)

Validation rules mirror dept (multiple default True, max, required, {id,name} shape). Do not require live DB user existence in this task (shape-only, same spirit as dept).

  • Step 4: Whitelist + start_instance

In oa_publish.py:

# FILLABLE_TYPES add:
"person",

# CONDITION_ELIGIBLE_TYPES add:
"person",

In oa_engine.py start_instance next to dept/money:

from app.services.oa_person_select import validate_person_form_values
person_errs = validate_person_form_values(schema, data)
if person_errs:
    raise OaEngineError(";".join(person_errs))

(Match exact error-raising pattern already used for money/dept.)

  • Step 5: Run tests PASS + commit
cd backend && source venv/bin/activate && pytest tests/test_oa_person_select.py -v
git add backend/app/services/oa_person_select.py backend/tests/test_oa_person_select.py \
  backend/app/services/oa_publish.py backend/app/services/oa_engine.py
git commit -m "feat(backend): validate OA person field and whitelist type"

Task 3: Engine contains matching (TDD) — also powers 发起流程预览

Files:

  • Modify: backend/app/services/oa_engine.py (_match_condition_group)
  • Modify: backend/tests/test_oa_engine.py

  • [ ] Step 1: Add failing tests

def test_condition_contains_person(self):
    groups = [
        {
            "name": "hit",
            "rules": [
                {"field": "Fperson", "op": "contains", "value": ["u1", "u2"]}
            ],
            "child": {"id": "n1", "type": "end"},
        },
        {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
    ]
    g = _match_condition_group(
        groups,
        {"Fperson": [{"id": "u2", "name": "李四"}]},
    )
    self.assertEqual(g["name"], "hit")
    g2 = _match_condition_group(
        groups,
        {"Fperson": [{"id": "u9", "name": "其他人"}]},
    )
    self.assertEqual(g2["name"], "default")

def test_condition_contains_person_single_object(self):
    groups = [
        {
            "name": "hit",
            "rules": [{"field": "Fperson", "op": "contains", "value": ["u1"]}],
            "child": {"id": "n1", "type": "end"},
        },
        {"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}},
    ]
    g = _match_condition_group(
        groups, {"Fperson": {"id": "u1", "name": "张三"}}
    )
    self.assertEqual(g["name"], "hit")
  • Step 2: Run — expect FAIL(当前会把 contains 塞进公式字符串并失败/走默认)
cd backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_person -v
  • Step 3: Patch _match_condition_group

When building/evaluating list rules, do not stringify contains into formula. Evaluate AND of rules with a helper:

from app.services.oa_person_select import person_contains_match

def _eval_condition_rules(rules: list, form_data: dict) -> bool:
    for r in rules:
        if not isinstance(r, dict):
            return False
        field = r.get("field")
        op = r.get("op") or "=="
        val = r.get("value")
        if field is None:
            return False
        if op == "contains":
            if not person_contains_match(form_data.get(field), val):
                return False
            continue
        # existing formula path for one rule:
        if isinstance(val, str):
            expr = f"{{{field}}} {op} '{val}'"
        else:
            expr = f"{{{field}}} {op} {val}"
        try:
            if not eval_formula(expr, form_data):
                return False
        except FormulaError:
            return False
    return True

In _match_condition_group, when rules is a non-empty list:

try:
    if _eval_condition_rules(rules, form_data):
        return g
except FormulaError:
    continue

Keep when string path unchanged for legacy.

  • Step 4: Run full helper tests PASS + commit
cd backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestOaEngineHelpers -v
git add backend/app/services/oa_engine.py backend/tests/test_oa_engine.py
git commit -m "feat(backend): evaluate person contains rules in condition groups"

Task 4: DragRule + register + OaPersonSelect(含预览布局)

Files:

  • Create: web-admin/src/utils/oa-person-select-rule.ts
  • Create: web-admin/src/utils/oa-form-create-person.ts
  • Create: web-admin/src/components/oa-approval/OaPersonSelect.vue
  • Modify: web-admin/src/main.ts
  • Modify: web-admin/src/oa-fc-host/main.ts
  • Modify: web-admin/src/assets/styles/core/oa-fc-runtime.scss

  • [ ] Step 1: DragRule(对齐 oa-dept-select-rule.ts)

// web-admin/src/utils/oa-person-select-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) +
    'p'
  )
}

export const personSelectDragRule = {
  menu: 'main' as const,
  icon: 'icon-user', // if missing in icon font, use 'icon-input' like other custom fields
  label: '人员',
  name: 'person',
  input: true,
  mask: true,
  validate: ['string', 'array', 'object'],
  event: ['change'],
  rule() {
    return {
      type: 'person',
      field: uniqueId(),
      title: '人员',
      info: '',
      $required: false,
      props: {
        multiple: true,
        max: undefined as number | undefined
      }
    }
  },
  props() {
    return [
      {
        type: 'switch',
        field: 'multiple',
        title: '允许多选',
        value: true,
        props: { activeText: '是', inactiveText: '否' }
      },
      {
        type: 'inputNumber',
        field: 'max',
        title: '最大可选数',
        value: undefined,
        props: { min: 1, precision: 0, placeholder: '不填则不限制' }
      },
      {
        type: 'input',
        field: 'formCreateInfo',
        title: '提示信息'
      }
    ]
  }
}

If icon-user missing in designer icon set, fall back to icon-input (verify in designer left panel).

  • Step 2: setup + main registration
// web-admin/src/utils/oa-form-create-person.ts
import FcDesigner from '@form-create/designer'
import OaPersonSelect from '@/components/oa-approval/OaPersonSelect.vue'
import { personSelectDragRule } from '@/utils/oa-person-select-rule'

const FLAG = '__oaPersonSelectConfigured'

export function setupOaPersonSelect(formCreate?: { component?: (name: string, c: unknown) => void }) {
  if ((FcDesigner as any)[FLAG]) return
  FcDesigner.component('person', OaPersonSelect)
  if (typeof formCreate?.component === 'function') {
    formCreate.component('person', OaPersonSelect)
  }
  if (typeof FcDesigner.addDragRule === 'function') {
    FcDesigner.addDragRule(personSelectDragRule)
  }
  ;(FcDesigner as any)[FLAG] = true
}

In main.ts and oa-fc-host/main.ts after money/dept:

import { setupOaPersonSelect } from '@/utils/oa-form-create-person'
setupOaPersonSelect(FcDesigner.formCreate)
  • Step 3: Implement OaPersonSelect.vue

Mirror OaDeptSelect.vue structure with these differences:

  • Options from fetchUserList({ status: '1', page, pageSize }) paginated to end(同 start/index.vue loadUsers,用 MAX_ADMIN_LIST_PAGE_SIZE)。
  • PC: el-select filterable clearable;:multiple="multiple";options value=id,label = nickName || userNo。
  • Map selection → { id, name } via in-memory Map + already-selected names.
  • Narrow (matchMedia max-width 767px): trigger + drawer + checkbox list / radio click(仿部门,列表非树)。
  • append-to-body on drawer so it works above designer chrome / preview dialog.
  • Styles: root .oa-person-select full width;:deep(.el-select) / tags flex-wrap: wrap;min-height 触控友好(对齐 money 40/44)。

Skeleton(关键部分):

<template>
  <div class="oa-person-select">
    <template v-if="disabled">
      <span class="oa-person-select__readonly">{{ displayText || '—' }}</span>
    </template>
    <template v-else-if="isNarrow">
      <!-- trigger + el-drawer:搜索 + el-checkbox-group / 单选列表 -->
    </template>
    <el-select
      v-else
      :model-value="selectModel"
      class="oa-person-select__select"
      filterable
      clearable
      :multiple="multiple"
      collapse-tags
      collapse-tags-tooltip
      placeholder="请选择人员"
      style="width: 100%"
      @update:model-value="onSelectChange"
    >
      <el-option
        v-for="u in userOptions"
        :key="u.id"
        :label="u.label"
        :value="u.id"
      />
    </el-select>
  </div>
</template>

Notes:

  • collapse-tags 避免 mobile 400px 画布被大量 tag 撑爆;完整名单仍在 model / 抽屉确认里。
  • Preview dialog 与 mobile 画布都走 PC el-select 路径(iframe 宽),故 全宽 + wrap/collapse 是预览兼容关键。

  • [ ] Step 4: oa-fc-runtime.scss

  .oa-dept-select,
  .oa-money-field,
  .oa-person-select,
  .oa-related-approval {
  • Step 5: Browser smoke(预览必测)
  1. 打开设计器「2 表单设计」→ 左侧有「人员」→ 拖入。
  2. 切换顶部 Mobile 设备:控件不横向溢出;可选人。
  3. 点工具栏 预览:表单模式可选人、多选写入。
  4. 保存发布;发起页可选人提交。
  • Step 6: Commit
git add web-admin/src/utils/oa-person-select-rule.ts \
  web-admin/src/utils/oa-form-create-person.ts \
  web-admin/src/components/oa-approval/OaPersonSelect.vue \
  web-admin/src/main.ts web-admin/src/oa-fc-host/main.ts \
  web-admin/src/assets/styles/core/oa-fc-runtime.scss
git commit -m "feat(web-admin): register OA person select for designer and preview"

Task 5: Condition UI(仅「包含」)

Files:

  • Modify: web-admin/src/components/oa-approval/oa-condition-fields.ts
  • Modify: web-admin/src/components/oa-approval/flow-tree/types.ts
  • Modify: web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
  • Modify: web-admin/src/components/oa-approval/flow-tree/FlowBranch.vue(若 formatConditionPreview 需展示 id→名;优先在 formatConditionPreview 内用候选或 raw ids)

  • [ ] Step 1: Types + ops

// types.ts
export type ConditionOp = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains'

export interface ConditionRule {
  field: string
  op: ConditionOp
  value: string | number | string[]
}
// oa-condition-fields.ts — CONDITION_ELIGIBLE_TYPES add 'person'
// ConditionOp type sync
export function opsForFieldType(type: string): Array<{ value: ConditionOp; label: string }> {
  if (type === 'person') {
    return [{ value: 'contains', label: '包含' }]
  }
  if (type === 'radio' || type === 'select') {
    return [
      { value: '==', label: '等于' },
      { value: '!=', label: '不等于' }
    ]
  }
  return [
    { value: '>', label: '大于' },
    { value: '>=', label: '大于等于' },
    { value: '<', label: '小于' },
    { value: '<=', label: '小于等于' },
    { value: '==', label: '等于' },
    { value: '!=', label: '不等于' }
  ]
}

Update formatConditionPreview for array values:

const valueText = Array.isArray(r.value)
  ? r.value.join('、')
  : r.value ?? '?'

(更好:ConditionRuleEditor 存 id,预览时用已加载用户 map 显示姓名——在 Editor 内维护 label。)

  • Step 2: ConditionRuleEditor

When meta(field)?.type === 'person':

  • Op select can be disabled single option「包含」.
  • Value: el-select multiple filterable,options from fetchUserList(组件 mount 时加载一次,与 FlowDesigner 相同)。
  • onValueChange 写入 string[]。
  • On field change to person: op: 'contains', value: [] then user fills.

Also fix onValueChange typing to allow string[].

  • Step 3: Smoke
  1. 人员字段勾选「参与流程条件分支」。
  2. 「3 流程设计」条件可选该字段,运算符仅「包含」,右侧多选人。
  3. 发起页改人员选择 → 底部流程预览分支变化。
  • Step 4: Commit
git add web-admin/src/components/oa-approval/oa-condition-fields.ts \
  web-admin/src/components/oa-approval/flow-tree/types.ts \
  web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
git commit -m "feat(web-admin): person field contains-only condition editor"

Task 6: Print + related-approval summary

Files:

  • Modify: web-admin/src/utils/oa-approval-print.ts
  • Modify: backend/app/services/oa_related_approval.py
  • Modify: backend/tests/test_oa_related_approval.py

  • [ ] Step 1: Print

After dept block:

import { formatPersonDisplay } from '@/utils/oa-person-value'

if (type === 'person') {
  const text = formatPersonDisplay(value)
  out.push({ label, text: text || '-' })
  continue
}
  • Step 2: Backend _format_value

Same as dept branch but typ == "person".

  • Step 3: Test + commit
cd backend && source venv/bin/activate && pytest tests/test_oa_related_approval.py -v
git add web-admin/src/utils/oa-approval-print.ts \
  backend/app/services/oa_related_approval.py \
  backend/tests/test_oa_related_approval.py
git commit -m "feat(oa): format person fields in print and related summary"

Task 7: Spec status + hand-test checklist

Files:

  • Modify: docs/superpowers/specs/2026-07-30-oa-person-select-design.md

  • [ ] Step 1: Mark status「已实现」;确认文档含预览章节(device + 预览按钮 + 发起流程预览)

  • [ ] Step 2: Full hand-test checklist

  • [ ] 设计器拖入人员;单/多选、max

  • [ ] Mobile 设备画布不溢出、可选

  • [ ] 「预览」弹窗表单模式可选、提交校验

  • [ ] useInCondition + 流程 contains

  • [ ] 发起页选人 → 流程预览分支正确

  • [ ] 提交、详情、打印姓名快照

  • [ ] 金额/部门回归

  • [ ] Step 3: Commit

git add docs/superpowers/specs/2026-07-30-oa-person-select-design.md
git commit -m "docs: mark OA person select design implemented"
  • Step 4: Update graphs if code changed
./scripts/update-graph.sh all

(仅 backend/web-admin 有实质代码提交后再跑。)


Spec coverage self-check

Spec 要求 Task
type=person 四层扩展 4
{id,name} 存值 1, 4
对齐部门 UI + max 4
useInCondition 5(eligible types)
仅 contains,OR 3, 5
发布白名单 / 校验 2
打印/摘要 6
设计器 Mobile 画布 + 预览按钮 4(preview section)
发起流程预览 3(engine)+ 5 smoke

Placeholder scan: none intentional.
Type consistency: person / contains / OaPersonValueItem / string[] value aligned across tasks.