2026-08-11-oa-approval-task-filter.md 34 KB

OA 审批任务中心检索区 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: 为待办/已办/我发起的/抄送我的四中心增加服务端筛选(流程名称、申请人、申请时间、流程状态)与即时检索 UI。

Architecture: 扩展 list_tasks 在分页前按 Query 过滤(join OaInstance/OaApp/SysUser);API 透传参数;四页 Vue 使用 Fresh Mint filter-block + watchDebounced(300),保留搜索/重置。

Tech Stack: FastAPI · SQLAlchemy · unittest;Vue 3 · Element Plus · @vueuse/core watchDebounced(auto-import)

Spec: docs/superpowers/specs/2026-08-11-oa-approval-task-filter-design.md
UI Demo: docs/demos/oa-approval-task-filter-demo.html


File map

Path Responsibility
Create: backend/tests/test_oa_tasks_filters.py 筛选单测(TDD)
Modify: backend/app/services/oa_engine.py list_tasks 过滤参数与 join
Modify: backend/app/api/oa_approval.py list_oa_tasks Query 透传
Modify: web-admin/src/api/oaApproval.ts fetchOaTasks params
Modify: web-admin/src/views/oa-approval/pending/index.vue 筛选区 + 即时检索
Modify: web-admin/src/views/oa-approval/done/index.vue 同上
Modify: web-admin/src/views/oa-approval/mine/index.vue 同上(无申请人)
Modify: web-admin/src/views/oa-approval/cc/index.vue 扩展筛选 + 即时检索
Run: ./scripts/update-graph.sh backend 图谱(签名变更)

Task 1: 后端筛选失败测试(TDD)

Files:

  • Create: backend/tests/test_oa_tasks_filters.py

  • [ ] Step 1: 写入测试文件

"""OA list_tasks 检索过滤。"""
from __future__ import annotations

import unittest
from datetime import datetime

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from app.models.base import Base
from app.models.models import OaApp, OaAppVersion, OaInstance, OaTask, SysUser


class TestOaTasksFilters(unittest.TestCase):
    def setUp(self):
        self.engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(
            self.engine,
            tables=[
                OaApp.__table__,
                OaAppVersion.__table__,
                SysUser.__table__,
                OaInstance.__table__,
                OaTask.__table__,
            ],
        )
        Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
        self.db = Session()
        t1 = datetime(2026, 7, 10, 9, 0, 0)
        t2 = datetime(2026, 7, 20, 15, 30, 0)
        self.db.add_all(
            [
                OaApp(
                    id="app-leave",
                    app_name="请假审批",
                    initiator_scope="all",
                    admin_scope="all_oa_admin",
                    status="1",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaApp(
                    id="app-ot",
                    app_name="加班申请",
                    initiator_scope="all",
                    admin_scope="all_oa_admin",
                    status="1",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaAppVersion(
                    id="v-leave",
                    app_id="app-leave",
                    version_no=1,
                    status="published",
                    is_latest_published="1",
                    form_schema_json="{}",
                    flow_tree_json="{}",
                    advanced_settings_json="{}",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaAppVersion(
                    id="v-ot",
                    app_id="app-ot",
                    version_no=1,
                    status="published",
                    is_latest_published="1",
                    form_schema_json="{}",
                    flow_tree_json="{}",
                    advanced_settings_json="{}",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                SysUser(
                    id="u-a",
                    user_no="a",
                    nick_name="张三",
                    password="x",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                SysUser(
                    id="u-b",
                    user_no="b",
                    nick_name="李四",
                    password="x",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                SysUser(
                    id="u-approver",
                    user_no="ap",
                    nick_name="审批人",
                    password="x",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaInstance(
                    id="i-leave",
                    app_id="app-leave",
                    app_version_id="v-leave",
                    title="请假-忽略标题",
                    initiator_user_id="u-a",
                    form_data_json="{}",
                    status="running",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaInstance(
                    id="i-ot",
                    app_id="app-ot",
                    app_version_id="v-ot",
                    title="加班标题含报销",
                    initiator_user_id="u-b",
                    form_data_json="{}",
                    status="completed",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t2,
                    update_time=t2,
                    del_flag=0,
                ),
                OaTask(
                    id="t-leave-pending",
                    instance_id="i-leave",
                    node_key="n1",
                    node_name="审批",
                    task_type="approval",
                    assignee_user_id="u-approver",
                    status="pending",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaTask(
                    id="t-ot-done",
                    instance_id="i-ot",
                    node_key="n1",
                    node_name="审批",
                    task_type="approval",
                    assignee_user_id="u-approver",
                    status="approved",
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t2,
                    update_time=t2,
                    del_flag=0,
                ),
                OaTask(
                    id="t-leave-cc-unread",
                    instance_id="i-leave",
                    node_key="cc1",
                    node_name="抄送",
                    task_type="cc",
                    assignee_user_id="u-approver",
                    status="done",
                    is_read=0,
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t1,
                    update_time=t1,
                    del_flag=0,
                ),
                OaTask(
                    id="t-ot-cc-read",
                    instance_id="i-ot",
                    node_key="cc1",
                    node_name="抄送",
                    task_type="cc",
                    assignee_user_id="u-approver",
                    status="done",
                    is_read=1,
                    tenant_no="zkxw",
                    create_dept_id="",
                    create_time=t2,
                    update_time=t2,
                    del_flag=0,
                ),
            ]
        )
        self.db.commit()

    def tearDown(self):
        self.db.close()
        self.engine.dispose()

    def test_mine_filter_by_app_name(self):
        from app.services.oa_engine import list_tasks

        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-a",
            kind="mine",
            app_name="请假",
        )
        self.assertEqual(res["total"], 1)
        self.assertEqual(res["list"][0]["id"], "i-leave")

    def test_mine_filter_by_app_name_matches_title(self):
        from app.services.oa_engine import list_tasks

        # u-b 发起的加班实例 title 含「报销」
        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-b",
            kind="mine",
            app_name="报销",
        )
        self.assertEqual(res["total"], 1)
        self.assertEqual(res["list"][0]["id"], "i-ot")

    def test_pending_filter_by_initiator_name(self):
        from app.services.oa_engine import list_tasks

        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-approver",
            kind="pending",
            initiator_name="张",
        )
        self.assertEqual(res["total"], 1)
        self.assertEqual(res["list"][0]["instanceId"], "i-leave")

    def test_done_filter_by_apply_time_range(self):
        from app.services.oa_engine import list_tasks

        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-approver",
            kind="done",
            apply_time_from="2026-07-15",
            apply_time_to="2026-07-25",
        )
        self.assertEqual(res["total"], 1)
        self.assertEqual(res["list"][0]["instanceId"], "i-ot")

    def test_mine_filter_by_instance_status(self):
        from app.services.oa_engine import list_tasks

        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-b",
            kind="mine",
            instance_status="completed",
        )
        self.assertEqual(res["total"], 1)
        self.assertEqual(res["list"][0]["instanceStatus"], "completed")

    def test_invalid_instance_status_ignored(self):
        from app.services.oa_engine import list_tasks

        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-a",
            kind="mine",
            instance_status="not-a-real-status",
        )
        self.assertEqual(res["total"], 1)

    def test_cc_combine_read_status_and_app_name(self):
        from app.services.oa_engine import list_tasks

        res = list_tasks(
            self.db,
            tenant_no="zkxw",
            user_id="u-approver",
            kind="cc",
            read_status="unread",
            app_name="请假",
        )
        self.assertEqual(res["total"], 1)
        self.assertEqual(res["list"][0]["id"], "t-leave-cc-unread")


if __name__ == "__main__":
    unittest.main()
  • Step 2: 跑测确认失败
cd backend && source venv/bin/activate && pytest tests/test_oa_tasks_filters.py -v

Expected: FAIL(list_tasks() 不接受 app_name 等关键字参数,或过滤未生效)

  • Step 3: Commit 测试
git add backend/tests/test_oa_tasks_filters.py
git commit -m "test: add failing OA task list filter coverage"

Task 2: 实现 list_tasks 过滤

Files:

  • Modify: backend/app/services/oa_engine.py(list_tasks 及其上方辅助函数)

  • [ ] Step 1: 在 list_tasks 上方增加常量与辅助函数

在 list_tasks 定义之前加入:

_INSTANCE_STATUS_FILTER_VALUES = frozenset(
    {"running", "completed", "rejected", "returned", "cancelled"}
)


def _parse_day_start(s: Optional[str]) -> Optional[datetime]:
    raw = (s or "").strip()
    if not raw:
        return None
    try:
        d = datetime.strptime(raw[:10], "%Y-%m-%d")
    except ValueError:
        return None
    return d.replace(hour=0, minute=0, second=0, microsecond=0)


def _parse_day_end(s: Optional[str]) -> Optional[datetime]:
    raw = (s or "").strip()
    if not raw:
        return None
    try:
        d = datetime.strptime(raw[:10], "%Y-%m-%d")
    except ValueError:
        return None
    return d.replace(hour=23, minute=59, second=59, microsecond=999999)


def _apply_instance_list_filters(
    q,
    *,
    app_name: Optional[str] = None,
    initiator_name: Optional[str] = None,
    apply_time_from: Optional[str] = None,
    apply_time_to: Optional[str] = None,
    instance_status: Optional[str] = None,
    joined_app: bool = False,
    joined_initiator: bool = False,
):
    """在已包含 OaInstance 的 query 上叠加列表筛选。返回 (query, joined_app, joined_initiator)。"""
    from sqlalchemy import or_

    kw = (app_name or "").strip()
    if kw:
        if not joined_app:
            q = q.outerjoin(
                OaApp,
                (OaApp.id == OaInstance.app_id) & (OaApp.del_flag == 0),
            )
            joined_app = True
        like = f"%{kw}%"
        q = q.filter(or_(OaApp.app_name.like(like), OaInstance.title.like(like)))

    iname = (initiator_name or "").strip()
    if iname:
        if not joined_initiator:
            q = q.outerjoin(
                SysUser,
                (SysUser.id == OaInstance.initiator_user_id) & (SysUser.del_flag == 0),
            )
            joined_initiator = True
        q = q.filter(SysUser.nick_name.like(f"%{iname}%"))

    t0 = _parse_day_start(apply_time_from)
    if t0 is not None:
        q = q.filter(OaInstance.create_time >= t0)
    t1 = _parse_day_end(apply_time_to)
    if t1 is not None:
        q = q.filter(OaInstance.create_time <= t1)

    st = (instance_status or "").strip()
    if st in _INSTANCE_STATUS_FILTER_VALUES:
        q = q.filter(OaInstance.status == st)

    return q, joined_app, joined_initiator

确认文件顶部已 import:OaApp, OaInstance, SysUser, Optional, datetime(已有则不重复)。

  • Step 2: 扩展 list_tasks 签名并在分页前调用过滤

将签名改为:

def list_tasks(
    db: Session,
    *,
    tenant_no: str,
    user_id: str,
    kind: str,
    read_status: Optional[str] = None,
    app_name: Optional[str] = None,
    initiator_name: Optional[str] = None,
    apply_time_from: Optional[str] = None,
    apply_time_to: Optional[str] = None,
    instance_status: Optional[str] = None,
    page: int = 1,
    page_size: int = 20,
) -> Dict[str, Any]:

在 kind == "mine" 分支中,基础 q 建好后、count 之前:

        q, _, _ = _apply_instance_list_filters(
            q,
            app_name=app_name,
            initiator_name=initiator_name,
            apply_time_from=apply_time_from,
            apply_time_to=apply_time_to,
            instance_status=instance_status,
        )

在 pending / done / cc 分支中,在各自 q 与 read_status 条件之后、count 之前:

    need_inst_join = any(
        [
            (app_name or "").strip(),
            (initiator_name or "").strip(),
            (apply_time_from or "").strip(),
            (apply_time_to or "").strip(),
            (instance_status or "").strip() in _INSTANCE_STATUS_FILTER_VALUES,
        ]
    )
    if need_inst_join:
        q = q.join(
            OaInstance,
            (OaInstance.id == OaTask.instance_id)
            & (OaInstance.tenant_no == tenant_no)
            & (OaInstance.del_flag == 0),
        )
        q, _, _ = _apply_instance_list_filters(
            q,
            app_name=app_name,
            initiator_name=initiator_name,
            apply_time_from=apply_time_from,
            apply_time_to=apply_time_to,
            instance_status=instance_status,
        )

注意:若 instance_status 非法,need_inst_join 中不要因非法 status 单独 join;上面写法已用 in _INSTANCE_STATUS_FILTER_VALUES。

  • Step 3: 跑测通过
cd backend && source venv/bin/activate && pytest tests/test_oa_tasks_filters.py tests/test_oa_engine.py -q

Expected: PASS

  • Step 4: Commit
git add backend/app/services/oa_engine.py backend/tests/test_oa_tasks_filters.py
git commit -m "feat: filter OA task lists by app name, initiator, time, status"

Task 3: API 透传 Query

Files:

  • Modify: backend/app/api/oa_approval.py(list_oa_tasks,约 1040 行)

  • [ ] Step 1: 扩展端点参数并传入 list_tasks

@router.get("/tasks/{kind}")
async def list_oa_tasks(
    kind: str,
    readStatus: Optional[str] = Query(None, alias="readStatus"),
    appName: Optional[str] = Query(None, alias="appName"),
    initiatorName: Optional[str] = Query(None, alias="initiatorName"),
    applyTimeFrom: Optional[str] = Query(None, alias="applyTimeFrom"),
    applyTimeTo: Optional[str] = Query(None, alias="applyTimeTo"),
    instanceStatus: Optional[str] = Query(None, alias="instanceStatus"),
    page: int = Query(1, ge=1),
    pageSize: int = Query(20, ge=1, le=100),
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    from app.services.oa_engine import OaEngineError, list_tasks

    try:
        tenant_no = _get_tenant_no(db, user)
        data = list_tasks(
            db,
            tenant_no=tenant_no,
            user_id=user.get("user_id") or "",
            kind=kind,
            read_status=readStatus,
            app_name=appName,
            initiator_name=initiatorName,
            apply_time_from=applyTimeFrom,
            apply_time_to=applyTimeTo,
            instance_status=instanceStatus,
            page=page,
            page_size=pageSize,
        )
        return success_response(
            {"list": data.get("list") or [], "total": int(data.get("total") or 0)}
        )
    except OaEngineError as e:
        return error_response(400, str(e))
  • Step 2: Commit
git add backend/app/api/oa_approval.py
git commit -m "feat: expose OA task list filter query params on API"

Task 4: 前端 fetchOaTasks 参数

Files:

  • Modify: web-admin/src/api/oaApproval.ts

  • [ ] Step 1: 扩展 params 并透传

将 fetchOaTasks 替换为:

export function fetchOaTasks(
  kind: 'pending' | 'done' | 'mine' | 'cc',
  params?: {
    readStatus?: 'unread' | 'read' | 'all'
    appName?: string
    initiatorName?: string
    applyTimeFrom?: string
    applyTimeTo?: string
    instanceStatus?: string
    page?: number
    pageSize?: number
  }
) {
  const appName = (params?.appName || '').trim()
  const initiatorName = (params?.initiatorName || '').trim()
  const applyTimeFrom = (params?.applyTimeFrom || '').trim()
  const applyTimeTo = (params?.applyTimeTo || '').trim()
  const instanceStatus = (params?.instanceStatus || '').trim()
  return request.get<{ list: any[]; total: number }>({
    url: `${BASE}/tasks/${kind}`,
    params: {
      page: params?.page ?? 1,
      pageSize: params?.pageSize ?? 20,
      ...(kind === 'cc' ? { readStatus: params?.readStatus ?? 'unread' } : {}),
      ...(appName ? { appName } : {}),
      ...(initiatorName ? { initiatorName } : {}),
      ...(applyTimeFrom ? { applyTimeFrom } : {}),
      ...(applyTimeTo ? { applyTimeTo } : {}),
      ...(instanceStatus ? { instanceStatus } : {})
    }
  })
}
  • Step 2: Commit
git add web-admin/src/api/oaApproval.ts
git commit -m "feat: pass OA task list filter params from web-admin API"

Task 5: 待办中心筛选 UI + 即时检索

Files:

  • Modify: web-admin/src/views/oa-approval/pending/index.vue

  • [ ] Step 1: 在 panel-top 与 list-body 之间插入筛选区

      <div class="filter-block">
        <el-form :model="searchForm" class="filter-form" @submit.prevent>
          <div class="filter-grid">
            <el-form-item label="流程名称">
              <el-input
                v-model="searchForm.appName"
                placeholder="请输入流程名称"
                clearable
              />
            </el-form-item>
            <el-form-item label="申请人">
              <el-input
                v-model="searchForm.initiatorName"
                placeholder="请输入申请人"
                clearable
              />
            </el-form-item>
            <el-form-item label="申请时间">
              <el-date-picker
                v-model="applyTimeRange"
                type="daterange"
                range-separator="至"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
            <el-form-item label="流程状态">
              <el-select v-model="searchForm.instanceStatus" placeholder="全部" clearable>
                <el-option label="全部" value="" />
                <el-option label="进行中" value="running" />
                <el-option label="已完成" value="completed" />
                <el-option label="已拒绝" value="rejected" />
                <el-option label="已打回" value="returned" />
                <el-option label="已撤销" value="cancelled" />
              </el-select>
            </el-form-item>
          </div>
          <div class="filter-actions-row">
            <div class="filter-actions-end">
              <el-button class="btn-ghost" @click="handleReset">
                <el-icon><RefreshLeft /></el-icon>
                重置
              </el-button>
              <el-button class="btn-main" type="primary" @click="handleSearch">
                <el-icon><Search /></el-icon>
                搜索
              </el-button>
            </div>
          </div>
        </el-form>
      </div>
  • Step 2: 更新 script

将 import 与状态逻辑改为(保留原有 statusType / statusText / openDetail / indexMethod):

  import { onActivated, onMounted, reactive, ref } from 'vue'
  import { useRouter } from 'vue-router'
  import { RefreshLeft, Search, View } from '@element-plus/icons-vue'
  import { fetchOaTasks } from '@/api/oaApproval'
  import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'

  defineOptions({ name: 'OaApprovalPending' })

  const router = useRouter()
  const loading = ref(false)
  const rows = ref<any[]>([])
  const page = ref(1)
  const pageSize = ref(20)
  const total = ref(0)
  const applyTimeRange = ref<string[] | null>(null)
  const searchForm = reactive({
    appName: '',
    initiatorName: '',
    instanceStatus: ''
  })

  function buildFilterParams() {
    const range = applyTimeRange.value
    return {
      appName: searchForm.appName,
      initiatorName: searchForm.initiatorName,
      instanceStatus: searchForm.instanceStatus,
      applyTimeFrom: range?.[0] || '',
      applyTimeTo: range?.[1] || '',
      page: page.value,
      pageSize: pageSize.value
    }
  }

  function handleSearch() {
    page.value = 1
    void load()
  }

  function handleReset() {
    searchForm.appName = ''
    searchForm.initiatorName = ''
    searchForm.instanceStatus = ''
    applyTimeRange.value = null
    page.value = 1
    void load()
  }

  async function load() {
    loading.value = true
    try {
      const res = await fetchOaTasks('pending', buildFilterParams())
      rows.value = res?.list || []
      total.value = res?.total || 0
    } catch {
      rows.value = []
      total.value = 0
    } finally {
      loading.value = false
    }
  }

  watchDebounced(
    [searchForm, applyTimeRange],
    () => {
      page.value = 1
      void load()
    },
    { debounce: 300, deep: true }
  )

  onMounted(() => {
    load()
    void refreshOaMenuBadges()
  })
  onActivated(() => {
    load()
    void refreshOaMenuBadges()
  })

说明:watchDebounced 由 unplugin auto-import,无需手动 import(与 basic-info/customer 一致)。分页 @size-change / @current-change 仍调用 load。

注意:onMounted 的 load 与 watchDebounced 首次可能双请求——与展馆预约等页一致可接受。若需避免,可加 skipWatch 标志;本任务不强制。

  • Step 3: Commit
git add web-admin/src/views/oa-approval/pending/index.vue
git commit -m "feat: add instant filters to OA pending task center"

Task 6: 已办中心筛选 UI + 即时检索

Files:

  • Modify: web-admin/src/views/oa-approval/done/index.vue

  • [ ] Step 1: 插入与待办相同的 filter-block 模板(字段:流程名称、申请人、申请时间、流程状态;重置+搜索)

  • [ ] Step 2: script 对齐待办,差异仅:

  • defineOptions({ name: 'OaApprovalDone' })

  • fetchOaTasks('done', buildFilterParams())

  • 无 refreshOaMenuBadges(保持现有 done 页行为)

  • onMounted(load) / onActivated(load)

  • icons:RefreshLeft, Search, View

  • watchDebounced([searchForm, applyTimeRange], ...) 同待办

  • [ ] Step 3: Commit

git add web-admin/src/views/oa-approval/done/index.vue
git commit -m "feat: add instant filters to OA done task center"

Task 7: 我发起的筛选 UI + 即时检索

Files:

  • Modify: web-admin/src/views/oa-approval/mine/index.vue

  • [ ] Step 1: 插入 filter-block,字段仅三项(无申请人)

      <div class="filter-block">
        <el-form :model="searchForm" class="filter-form" @submit.prevent>
          <div class="filter-grid">
            <el-form-item label="流程名称">
              <el-input
                v-model="searchForm.appName"
                placeholder="请输入流程名称"
                clearable
              />
            </el-form-item>
            <el-form-item label="申请时间">
              <el-date-picker
                v-model="applyTimeRange"
                type="daterange"
                range-separator="至"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
            <el-form-item label="流程状态">
              <el-select v-model="searchForm.instanceStatus" placeholder="全部" clearable>
                <el-option label="全部" value="" />
                <el-option label="进行中" value="running" />
                <el-option label="已完成" value="completed" />
                <el-option label="已拒绝" value="rejected" />
                <el-option label="已打回" value="returned" />
                <el-option label="已撤销" value="cancelled" />
              </el-select>
            </el-form-item>
          </div>
          <div class="filter-actions-row">
            <div class="filter-actions-end">
              <el-button class="btn-ghost" @click="handleReset">
                <el-icon><RefreshLeft /></el-icon>
                重置
              </el-button>
              <el-button class="btn-main" type="primary" @click="handleSearch">
                <el-icon><Search /></el-icon>
                搜索
              </el-button>
            </div>
          </div>
        </el-form>
      </div>
  • Step 2: script 增加筛选状态与即时检索

在现有 imports 中增加 reactive、RefreshLeft、Search(保留 Printer/RefreshRight/View 等)。

  const applyTimeRange = ref<string[] | null>(null)
  const searchForm = reactive({
    appName: '',
    instanceStatus: ''
  })

  function buildFilterParams() {
    const range = applyTimeRange.value
    return {
      appName: searchForm.appName,
      instanceStatus: searchForm.instanceStatus,
      applyTimeFrom: range?.[0] || '',
      applyTimeTo: range?.[1] || '',
      page: page.value,
      pageSize: pageSize.value
    }
  }

  function handleSearch() {
    page.value = 1
    void load()
  }

  function handleReset() {
    searchForm.appName = ''
    searchForm.instanceStatus = ''
    applyTimeRange.value = null
    page.value = 1
    void load()
  }

load() 内改为 fetchOaTasks('mine', buildFilterParams())。

  watchDebounced(
    [searchForm, applyTimeRange],
    () => {
      page.value = 1
      void load()
    },
    { debounce: 300, deep: true }
  )

保留打印 / 复制并重新发起逻辑不变。

  • Step 3: Commit
git add web-admin/src/views/oa-approval/mine/index.vue
git commit -m "feat: add instant filters to OA mine instances list"

Task 8: 抄送我的扩展筛选 + 即时检索

Files:

  • Modify: web-admin/src/views/oa-approval/cc/index.vue

  • [ ] Step 1: 扩展现有 filter-grid

在「阅读状态」前后加入流程名称、申请人、申请时间、流程状态(阅读状态保留):

          <div class="filter-grid">
            <el-form-item label="流程名称">
              <el-input
                v-model="searchForm.appName"
                placeholder="请输入流程名称"
                clearable
              />
            </el-form-item>
            <el-form-item label="申请人">
              <el-input
                v-model="searchForm.initiatorName"
                placeholder="请输入申请人"
                clearable
              />
            </el-form-item>
            <el-form-item label="申请时间">
              <el-date-picker
                v-model="applyTimeRange"
                type="daterange"
                range-separator="至"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
            <el-form-item label="流程状态">
              <el-select v-model="searchForm.instanceStatus" placeholder="全部" clearable>
                <el-option label="全部" value="" />
                <el-option label="进行中" value="running" />
                <el-option label="已完成" value="completed" />
                <el-option label="已拒绝" value="rejected" />
                <el-option label="已打回" value="returned" />
                <el-option label="已撤销" value="cancelled" />
              </el-select>
            </el-form-item>
            <el-form-item label="阅读状态">
              <el-select v-model="searchForm.readStatus" placeholder="请选择">
                <el-option label="未读" value="unread" />
                <el-option label="已读" value="read" />
                <el-option label="全部" value="all" />
              </el-select>
            </el-form-item>
          </div>
  • Step 2: 扩展 searchForm 与 load / 重置 / 防抖
  const applyTimeRange = ref<string[] | null>(null)
  const searchForm = reactive({
    appName: '',
    initiatorName: '',
    instanceStatus: '',
    readStatus: 'unread' as 'unread' | 'read' | 'all'
  })

  function buildFilterParams() {
    const range = applyTimeRange.value
    return {
      appName: searchForm.appName,
      initiatorName: searchForm.initiatorName,
      instanceStatus: searchForm.instanceStatus,
      applyTimeFrom: range?.[0] || '',
      applyTimeTo: range?.[1] || '',
      readStatus: searchForm.readStatus,
      page: page.value,
      pageSize: pageSize.value
    }
  }

  function handleSearch() {
    page.value = 1
    void load()
  }

  function handleReset() {
    searchForm.appName = ''
    searchForm.initiatorName = ''
    searchForm.instanceStatus = ''
    searchForm.readStatus = 'unread'
    applyTimeRange.value = null
    page.value = 1
    void load()
  }

  async function load() {
    loading.value = true
    try {
      const res = await fetchOaTasks('cc', buildFilterParams())
      rows.value = res?.list || []
      total.value = res?.total || 0
    } catch {
      rows.value = []
      total.value = 0
    } finally {
      loading.value = false
      selectedRows.value = []
      void refreshOaMenuBadges()
    }
  }

  watchDebounced(
    [searchForm, applyTimeRange],
    () => {
      page.value = 1
      void load()
    },
    { debounce: 300, deep: true }
  )
  • Step 3: Commit
git add web-admin/src/views/oa-approval/cc/index.vue
git commit -m "feat: extend OA cc list filters with instant search"

Task 9: 回归验证与图谱

  • Step 1: 后端测试
cd backend && source venv/bin/activate && pytest tests/test_oa_tasks_filters.py tests/test_oa_engine.py tests/test_oa_tasks_pagination.py -q

Expected: PASS(若无 test_oa_tasks_pagination.py 则省略该文件)

  • Step 2: 更新知识图谱
./scripts/update-graph.sh backend
  • Step 3: 手测清单(对照 spec §9)
  1. 四页出现 filter-block,重置+搜索可见
  2. 输入流程名约 300ms 后列表刷新;点搜索立即刷新
  3. 重置清空;抄送默认回到「未读」
  4. 改条件后面页码回到 1
  5. 无筛选时列表与改前一致
  • Step 4: 将设计文档状态改为已实现(可选同 commit)

在 docs/superpowers/specs/2026-08-11-oa-approval-task-filter-design.md 将 **状态**: 待审核 改为 **状态**: 已实现。

git add docs/superpowers/specs/2026-08-11-oa-approval-task-filter-design.md backend/graphify-out
git commit -m "docs: mark OA task filter design implemented and refresh graph"

Spec coverage checklist(自审)

Spec 要求 Task
四页 filter-block 5–8
流程名称 / 申请时间 / 流程状态 2 + 5–8
申请人(待办/已办/抄送) 2 + 5,6,8
我发起的无申请人 7
抄送阅读状态保留 8
即时检索 300ms + 搜索 + 重置 5–8
API Query 参数 3–4
服务端分页前过滤 2
非法 status 忽略 1 + 2
applyTimeTo 含当日结束 2 _parse_day_end
nick_name 同源 2
后端单测 1
角标接口不改 非目标,未改 count API
图谱更新 9