|
|
@@ -0,0 +1,656 @@
|
|
|
+# OA Dept / OwnerDept Condition Contains 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:** Let form controls `dept` and `ownerDept` participate in workflow condition branches with person-parity `contains` matching (exact department id intersection) and a tree multi-select value picker.
|
|
|
+
|
|
|
+**Architecture:** Mirror the existing person/post path: whitelist types in front/back `CONDITION_ELIGIBLE_TYPES`; add `extract_dept_ids` / `dept_contains_match` in `oa_dept_select.py`; OR them into `oa_engine._eval_condition_rules` contains branch; extend `ConditionRuleEditor` with `el-tree-select` fed by `fetchDeptTree`. Matching is exact id only — tree UI is for picking, not hierarchical match.
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI · pytest · Vue 3 · Element Plus · existing ConditionRuleEditor / oa-condition-fields / oa_engine contains path
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| Path | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| Modify: `backend/app/services/oa_dept_select.py` | `extract_dept_ids` + `dept_contains_match` |
|
|
|
+| Modify: `backend/tests/test_oa_dept_select.py` | Unit tests for extract/contains |
|
|
|
+| Modify: `backend/app/services/oa_engine.py` | OR `dept_contains_match` into contains eval |
|
|
|
+| Modify: `backend/tests/test_oa_engine.py` | Engine condition tests for dept |
|
|
|
+| Modify: `backend/app/services/oa_publish.py` | Add `dept` / `ownerDept` to `CONDITION_ELIGIBLE_TYPES` |
|
|
|
+| Modify: `backend/tests/test_oa_publish.py` | Publish allows flagged dept field in rules |
|
|
|
+| Modify: `web-admin/src/components/oa-approval/oa-condition-fields.ts` | Frontend whitelist + ops |
|
|
|
+| Modify: `web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue` | Dept-like RHS tree multi-select |
|
|
|
+| Optional: `docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md` | Strike “ownerDept 暂不进条件” note |
|
|
|
+| Modify: `docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md` | Mark status implemented when done |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: `dept_contains_match` helpers (TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/oa_dept_select.py`
|
|
|
+- Modify: `backend/tests/test_oa_dept_select.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: Write failing tests** — append to `TestOaDeptSelect` in `backend/tests/test_oa_dept_select.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.services.oa_dept_select import (
|
|
|
+ extract_dept_ids,
|
|
|
+ dept_contains_match,
|
|
|
+ validate_dept_form_values,
|
|
|
+)
|
|
|
+
|
|
|
+def test_extract_and_contains(self):
|
|
|
+ self.assertEqual(extract_dept_ids({"id": "d1", "name": "研发"}), {"d1"})
|
|
|
+ self.assertEqual(
|
|
|
+ extract_dept_ids([{"id": "d1", "name": "A"}, {"id": "d2", "name": "B"}]),
|
|
|
+ {"d1", "d2"},
|
|
|
+ )
|
|
|
+ self.assertTrue(
|
|
|
+ dept_contains_match(
|
|
|
+ [{"id": "d1", "name": "A"}],
|
|
|
+ ["d1", "d9"],
|
|
|
+ )
|
|
|
+ )
|
|
|
+ self.assertFalse(
|
|
|
+ dept_contains_match(
|
|
|
+ [{"id": "d3", "name": "C"}],
|
|
|
+ ["d1", "d2"],
|
|
|
+ )
|
|
|
+ )
|
|
|
+ # 精确匹配:下级 id 不等于上级 id
|
|
|
+ self.assertFalse(
|
|
|
+ dept_contains_match(
|
|
|
+ {"id": "d1-child", "name": "前端组"},
|
|
|
+ ["d1"],
|
|
|
+ )
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+Update the import at the top of the test file to include `extract_dept_ids` and `dept_contains_match` (keep existing `validate_dept_form_values` import).
|
|
|
+
|
|
|
+- [ ] **Step 2: Run tests to verify they fail**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_dept_select.py::TestOaDeptSelect::test_extract_and_contains -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL (import error or `AttributeError`: `extract_dept_ids` / `dept_contains_match` missing).
|
|
|
+
|
|
|
+- [ ] **Step 3: Implement helpers** — append to `backend/app/services/oa_dept_select.py` (after `validate_dept_form_values`, mirror `oa_person_select.py`):
|
|
|
+
|
|
|
+```python
|
|
|
+def extract_dept_ids(raw: Any) -> Set[str]:
|
|
|
+ ids: Set[str] = set()
|
|
|
+ items = raw if isinstance(raw, list) else ([raw] if isinstance(raw, dict) else [])
|
|
|
+ for item in items:
|
|
|
+ if not isinstance(item, dict):
|
|
|
+ continue
|
|
|
+ did = str(item.get("id") or "").strip()
|
|
|
+ if did:
|
|
|
+ ids.add(did)
|
|
|
+ return ids
|
|
|
+
|
|
|
+
|
|
|
+def dept_contains_match(form_value: Any, cond_ids: Any) -> bool:
|
|
|
+ form_ids = extract_dept_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)
|
|
|
+```
|
|
|
+
|
|
|
+Add to the module imports at top:
|
|
|
+
|
|
|
+```python
|
|
|
+from typing import Any, Dict, List, Optional, Set
|
|
|
+```
|
|
|
+
|
|
|
+(`Set` is new; keep existing imports.)
|
|
|
+
|
|
|
+- [ ] **Step 4: Run tests to verify they pass**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_dept_select.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS (all tests in file).
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/oa_dept_select.py backend/tests/test_oa_dept_select.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(oa): add dept_contains_match for condition id intersection
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Engine wires dept contains (TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/oa_engine.py` (`_eval_condition_rules`)
|
|
|
+- Modify: `backend/tests/test_oa_engine.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: Write failing engine tests** — append after `test_condition_contains_person_single_object` in `TestOaEngineHelpers`:
|
|
|
+
|
|
|
+```python
|
|
|
+def test_condition_contains_dept(self):
|
|
|
+ groups = [
|
|
|
+ {
|
|
|
+ "name": "hit",
|
|
|
+ "rules": [
|
|
|
+ {"field": "Fdept", "op": "contains", "value": ["d1", "d2"]}
|
|
|
+ ],
|
|
|
+ "child": {"id": "n1", "type": "end"},
|
|
|
+ },
|
|
|
+ {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
|
|
|
+ ]
|
|
|
+ g = _match_condition_group(
|
|
|
+ groups,
|
|
|
+ {"Fdept": [{"id": "d2", "name": "研发"}]},
|
|
|
+ )
|
|
|
+ self.assertEqual(g["name"], "hit")
|
|
|
+ g2 = _match_condition_group(
|
|
|
+ groups,
|
|
|
+ {"Fdept": [{"id": "d9", "name": "其他"}]},
|
|
|
+ )
|
|
|
+ self.assertEqual(g2["name"], "default")
|
|
|
+
|
|
|
+def test_condition_contains_owner_dept_single_object(self):
|
|
|
+ groups = [
|
|
|
+ {
|
|
|
+ "name": "hit",
|
|
|
+ "rules": [{"field": "Fod", "op": "contains", "value": ["d1"]}],
|
|
|
+ "child": {"id": "n1", "type": "end"},
|
|
|
+ },
|
|
|
+ {"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}},
|
|
|
+ ]
|
|
|
+ g = _match_condition_group(
|
|
|
+ groups, {"Fod": {"id": "d1", "name": "研发"}}
|
|
|
+ )
|
|
|
+ self.assertEqual(g["name"], "hit")
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run tests to verify they fail**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_dept tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_owner_dept_single_object -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL — dept form values do not match (person/post helpers return False → group falls through to default).
|
|
|
+
|
|
|
+- [ ] **Step 3: Wire engine** — in `backend/app/services/oa_engine.py` function `_eval_condition_rules`, change the contains branch imports and OR:
|
|
|
+
|
|
|
+Find:
|
|
|
+
|
|
|
+```python
|
|
|
+ from app.services.oa_person_select import person_contains_match
|
|
|
+ from app.services.oa_post_select import post_contains_match
|
|
|
+```
|
|
|
+
|
|
|
+Replace with:
|
|
|
+
|
|
|
+```python
|
|
|
+ from app.services.oa_person_select import person_contains_match
|
|
|
+ from app.services.oa_post_select import post_contains_match
|
|
|
+ from app.services.oa_dept_select import dept_contains_match
|
|
|
+```
|
|
|
+
|
|
|
+Find:
|
|
|
+
|
|
|
+```python
|
|
|
+ form_val = form_data.get(field)
|
|
|
+ if not (person_contains_match(form_val, val) or post_contains_match(form_val, val)):
|
|
|
+ return False
|
|
|
+```
|
|
|
+
|
|
|
+Replace with:
|
|
|
+
|
|
|
+```python
|
|
|
+ form_val = form_data.get(field)
|
|
|
+ if not (
|
|
|
+ person_contains_match(form_val, val)
|
|
|
+ or post_contains_match(form_val, val)
|
|
|
+ or dept_contains_match(form_val, val)
|
|
|
+ ):
|
|
|
+ return False
|
|
|
+```
|
|
|
+
|
|
|
+Also update the docstring of `_eval_condition_rules` from 「人员/职位」to 「人员/职位/部门」.
|
|
|
+
|
|
|
+- [ ] **Step 4: Run tests to verify they pass**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_person tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_person_single_object tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_dept tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_owner_dept_single_object tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_initiator -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS (dept new + person/initiator regression).
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/oa_engine.py backend/tests/test_oa_engine.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(oa): evaluate dept contains in condition rules
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: Publish whitelist (TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/oa_publish.py`
|
|
|
+- Modify: `backend/tests/test_oa_publish.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: Write failing publish test** — append near other condition field tests in `TestConditionPublish` (`backend/tests/test_oa_publish.py`):
|
|
|
+
|
|
|
+```python
|
|
|
+def test_dept_condition_field_allowed(self):
|
|
|
+ errs = validate_condition_rules_against_form(
|
|
|
+ {
|
|
|
+ "rule": [
|
|
|
+ {
|
|
|
+ "type": "dept",
|
|
|
+ "field": "Fdept",
|
|
|
+ "title": "部门",
|
|
|
+ "useInCondition": True,
|
|
|
+ "$required": True,
|
|
|
+ "props": {"multiple": True},
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ _condition_flow(
|
|
|
+ rules=[
|
|
|
+ {"field": "Fdept", "op": "contains", "value": ["d1", "d2"]}
|
|
|
+ ]
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ self.assertEqual(errs, [])
|
|
|
+
|
|
|
+def test_owner_dept_condition_field_allowed(self):
|
|
|
+ errs = validate_condition_rules_against_form(
|
|
|
+ {
|
|
|
+ "rule": [
|
|
|
+ {
|
|
|
+ "type": "ownerDept",
|
|
|
+ "field": "Fod",
|
|
|
+ "title": "归属部门",
|
|
|
+ "useInCondition": True,
|
|
|
+ "$required": True,
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ _condition_flow(
|
|
|
+ rules=[{"field": "Fod", "op": "contains", "value": ["d1"]}]
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ self.assertEqual(errs, [])
|
|
|
+```
|
|
|
+
|
|
|
+Ensure `validate_condition_rules_against_form` and `_condition_flow` are already imported/defined in that file (they are).
|
|
|
+
|
|
|
+- [ ] **Step 2: Run tests to verify they fail**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_publish.py::TestConditionPublish::test_dept_condition_field_allowed tests/test_oa_publish.py::TestConditionPublish::test_owner_dept_condition_field_allowed -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL — `条件引用了无效字段:Fdept` (or Fod) because type not in `CONDITION_ELIGIBLE_TYPES`.
|
|
|
+
|
|
|
+- [ ] **Step 3: Update whitelist** — in `backend/app/services/oa_publish.py`, extend:
|
|
|
+
|
|
|
+```python
|
|
|
+CONDITION_ELIGIBLE_TYPES = frozenset(
|
|
|
+ {
|
|
|
+ "input",
|
|
|
+ "inputNumber",
|
|
|
+ "number",
|
|
|
+ "money",
|
|
|
+ "radio",
|
|
|
+ "select",
|
|
|
+ "datePicker",
|
|
|
+ "date",
|
|
|
+ "person",
|
|
|
+ "ownerPerson",
|
|
|
+ "post",
|
|
|
+ "dept",
|
|
|
+ "ownerDept",
|
|
|
+ }
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Run tests to verify they pass**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_publish.py -k "dept_condition or initiator_condition or field_not_flagged" -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/oa_publish.py backend/tests/test_oa_publish.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(oa): allow dept and ownerDept in condition publish whitelist
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: Frontend condition field whitelist
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/oa-approval/oa-condition-fields.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: Extend `CONDITION_ELIGIBLE_TYPES`**
|
|
|
+
|
|
|
+In `web-admin/src/components/oa-approval/oa-condition-fields.ts`, change the Set to include dept types:
|
|
|
+
|
|
|
+```typescript
|
|
|
+export const CONDITION_ELIGIBLE_TYPES = new Set([
|
|
|
+ 'input',
|
|
|
+ 'inputNumber',
|
|
|
+ 'number',
|
|
|
+ 'money',
|
|
|
+ 'person',
|
|
|
+ 'ownerPerson',
|
|
|
+ 'dept',
|
|
|
+ 'ownerDept',
|
|
|
+ 'post',
|
|
|
+ 'radio',
|
|
|
+ 'select',
|
|
|
+ 'datePicker',
|
|
|
+ 'date'
|
|
|
+])
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Extend `opsForFieldType`**
|
|
|
+
|
|
|
+Find:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') {
|
|
|
+ return [{ value: 'contains', label: '包含' }]
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+Replace with:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ if (
|
|
|
+ type === 'person' ||
|
|
|
+ type === 'ownerPerson' ||
|
|
|
+ type === 'dept' ||
|
|
|
+ type === 'ownerDept' ||
|
|
|
+ type === 'post' ||
|
|
|
+ type === 'initiator'
|
|
|
+ ) {
|
|
|
+ return [{ value: 'contains', label: '包含' }]
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+(`oa-fc-host/App.vue` already gates the switch on `CONDITION_ELIGIBLE_TYPES` — no host change required.)
|
|
|
+
|
|
|
+- [ ] **Step 3: Smoke-check TypeScript of the module** (optional quick grep)
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/web-admin && rg "CONDITION_ELIGIBLE_TYPES|opsForFieldType" src/components/oa-approval/oa-condition-fields.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: both `dept` and `ownerDept` appear in the Set and ops branch.
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/oa-approval/oa-condition-fields.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(oa): whitelist dept and ownerDept for condition participation
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: ConditionRuleEditor tree multi-select
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Import `fetchDeptTree` and add dept tree state**
|
|
|
+
|
|
|
+In script imports, change:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ import { fetchPostList, fetchUserList } from '@/api/admin'
|
|
|
+```
|
|
|
+
|
|
|
+to:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ import { fetchDeptTree, fetchPostList, fetchUserList } from '@/api/admin'
|
|
|
+```
|
|
|
+
|
|
|
+After `postOptions` ref, add:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ type DeptTreeNode = { value: string; label: string; children?: DeptTreeNode[] }
|
|
|
+
|
|
|
+ const deptTreeData = ref<DeptTreeNode[]>([])
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Add helpers** next to `isPostField`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ function isDeptLikeField(field: string) {
|
|
|
+ const t = meta(field)?.type
|
|
|
+ return t === 'dept' || t === 'ownerDept'
|
|
|
+ }
|
|
|
+
|
|
|
+ function mapDeptTree(nodes: Api.Dept.DeptItem[]): DeptTreeNode[] {
|
|
|
+ return (nodes || []).map((n) => ({
|
|
|
+ value: String(n.id),
|
|
|
+ label: String(n.deptName || n.id),
|
|
|
+ children: n.children?.length ? mapDeptTree(n.children) : undefined
|
|
|
+ }))
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+Update `defaultValueForType`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ function defaultValueForType(type: string, opts?: ConditionCandidate['options']) {
|
|
|
+ if (
|
|
|
+ type === 'person' ||
|
|
|
+ type === 'ownerPerson' ||
|
|
|
+ type === 'dept' ||
|
|
|
+ type === 'ownerDept' ||
|
|
|
+ type === 'post' ||
|
|
|
+ type === 'initiator'
|
|
|
+ ) {
|
|
|
+ return [] as string[]
|
|
|
+ }
|
|
|
+ return opts?.[0]?.value ?? ''
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Add `loadDepts` + mount/watch**
|
|
|
+
|
|
|
+```typescript
|
|
|
+ async function loadDepts() {
|
|
|
+ try {
|
|
|
+ const tree = (await fetchDeptTree()) || []
|
|
|
+ deptTreeData.value = mapDeptTree(tree as Api.Dept.DeptItem[])
|
|
|
+ } catch {
|
|
|
+ deptTreeData.value = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+In `onMounted`, after the post block:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ if (props.candidates.some((c) => c.type === 'dept' || c.type === 'ownerDept')) {
|
|
|
+ loadDepts()
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+Add watch:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ watch(
|
|
|
+ () => props.candidates.some((c) => c.type === 'dept' || c.type === 'ownerDept'),
|
|
|
+ (need) => {
|
|
|
+ if (need && !deptTreeData.value.length) loadDepts()
|
|
|
+ }
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Template RHS for dept** — insert **after** the person `el-select` block and **before** the post `el-select` (or after post; order among non-person is free as long as `v-else-if` chain is correct). Recommended chain:
|
|
|
+
|
|
|
+1. `isPersonLikeField` → user select (unchanged)
|
|
|
+2. `isPostField` → post select (unchanged)
|
|
|
+3. `isDeptLikeField` → tree select (new)
|
|
|
+4. options / input (unchanged)
|
|
|
+
|
|
|
+New block:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <el-tree-select
|
|
|
+ v-else-if="isDeptLikeField(rule.field)"
|
|
|
+ :model-value="personValueIds(rule.value)"
|
|
|
+ multiple
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ check-strictly
|
|
|
+ collapse-tags
|
|
|
+ collapse-tags-tooltip
|
|
|
+ default-expand-all
|
|
|
+ :data="deptTreeData"
|
|
|
+ :props="{ value: 'value', label: 'label', children: 'children' }"
|
|
|
+ placeholder="选择部门(任一命中)"
|
|
|
+ :disabled="disabled"
|
|
|
+ class="cond-rule-editor__dept-select"
|
|
|
+ @update:model-value="(v) => onValueChange(idx, Array.isArray(v) ? v.map(String) : [])"
|
|
|
+ />
|
|
|
+```
|
|
|
+
|
|
|
+Reuse `personValueIds` for normalizing `value` to `string[]` (same shape).
|
|
|
+
|
|
|
+Optional style (mirror post):
|
|
|
+
|
|
|
+```scss
|
|
|
+ .cond-rule-editor__dept-select :deep(.el-select__selection) {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Manual UI check**
|
|
|
+
|
|
|
+1. Open designer →「2 表单设计」→ 拖入「部门」与「归属部门」→ 右侧应出现「参与流程条件分支」→ 勾选。
|
|
|
+2. 「3 流程设计」→ 条件分支 → 字段可选这两项;运算符仅「包含」;值侧为部门树多选。
|
|
|
+3. 保存/发布草稿(若环境可用)应通过。
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(oa): tree multi-select for dept condition values
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: Spec footnotes + mark implemented
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md` (one line)
|
|
|
+- Modify: `docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md`
|
|
|
+- Optional: checkboxes in this plan file as tasks complete
|
|
|
+
|
|
|
+- [ ] **Step 1: Update owner-fields historical note**
|
|
|
+
|
|
|
+In `docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md` §4.2, replace:
|
|
|
+
|
|
|
+```markdown
|
|
|
+- 条件分支:`ownerPerson` 与 `person` 同属人员类(`contains`);`ownerDept` 暂不进条件候选(与现有 `dept` 一致)。
|
|
|
+```
|
|
|
+
|
|
|
+with:
|
|
|
+
|
|
|
+```markdown
|
|
|
+- 条件分支:`ownerPerson` 与 `person` 同属人员类(`contains`);`ownerDept` 与 `dept` 同属部门类(`contains`,精确 id)。详见 `2026-08-06-oa-dept-condition-design.md`。
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Mark dept condition spec status**
|
|
|
+
|
|
|
+In `docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md` header, set:
|
|
|
+
|
|
|
+```markdown
|
|
|
+**状态:** 已实现
|
|
|
+```
|
|
|
+
|
|
|
+Check off §8 验收标准 items that were verified.
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+docs(oa): mark dept condition spec implemented
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Final regression**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_dept_select.py tests/test_oa_engine.py tests/test_oa_publish.py tests/test_oa_person_select.py tests/test_oa_post_select.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+After frontend changes, if graph update is required by repo rules:
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw && ./scripts/update-graph.sh frontend
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Plan self-review
|
|
|
+
|
|
|
+| Spec requirement | Task |
|
|
|
+|------------------|------|
|
|
|
+| Whitelist `dept` / `ownerDept` (form switch) | Task 4 (+ host already gated) |
|
|
|
+| Exact id `contains` match | Task 1–2 |
|
|
|
+| Condition value tree multi-select | Task 5 |
|
|
|
+| Publish eligible types | Task 3 |
|
|
|
+| Tests extract/engine/publish | Task 1–3 |
|
|
|
+| Non-goals (subtree, initiator dept, ==) | Not implemented (intentional) |
|
|
|
+| Update owner-fields note | Task 6 |
|
|
|
+
|
|
|
+No TBD placeholders. Function names consistent: `extract_dept_ids`, `dept_contains_match` across tasks.
|