From ec628e1b8db03b48bdb25ae7fe56a8408e4afb6f Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 12 Aug 2026 15:48:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/mini_menu.py | 25 +- backend/app/core/config.py | 2 +- backend/graphify-out/.graphify_extract.json | 26 +- backend/graphify-out/GRAPH_REPORT.md | 4 +- backend/graphify-out/graph.json | 26 +- backend/graphify-out/manifest.json | 2 +- docs/oa-start-footer-btn-style-demo.html | 558 +++++++++ docs/oa-start-picker-list-style-demo.html | 1051 +++++++++++++++++ .../2026-08-12-mobile-oa-task-centers.md | 788 ++++++++++++ .../2026-08-12-oa-person-picker-scheme-c.md | 98 ++ ...026-08-12-mobile-oa-task-centers-design.md | 273 +++++ mobile-h5/components/AttachList.vue | 2 +- mobile-h5/components/BizListCard.vue | 2 +- mobile-h5/components/MenuGrid.vue | 19 +- mobile-h5/components/OaAppIcon.vue | 31 +- mobile-h5/components/van-icon/van-icon.vue | 9 +- mobile-h5/components/van-tag/van-tag.vue | 28 +- mobile-h5/pages.json | 18 +- mobile-h5/pages/demo/comps.vue | 18 +- mobile-h5/pages/demo/detail.vue | 4 +- mobile-h5/pages/demo/form.vue | 4 +- mobile-h5/pages/demo/list.vue | 4 + mobile-h5/pages/index/index.vue | 5 +- mobile-h5/pages/oa/start/index.vue | 320 ++--- mobile-h5/pages/oa/start/webview.vue | 7 +- mobile-h5/static/empty-inbox.svg | 2 +- mobile-h5/static/menu-icons/oa-cc.png | Bin 0 -> 3624 bytes mobile-h5/static/menu-icons/oa-done.png | Bin 0 -> 2892 bytes mobile-h5/static/menu-icons/oa-drafts.png | Bin 0 -> 3266 bytes mobile-h5/static/menu-icons/oa-mine.png | Bin 0 -> 4429 bytes mobile-h5/static/menu-icons/oa-pending.png | Bin 0 -> 3865 bytes .../static/menu-icons/start-approval.png | Bin 2736 -> 3663 bytes mobile-h5/styles/app.scss | 5 +- mobile-h5/styles/components/attach.scss | 26 +- .../styles/components/confirm-dialog.scss | 6 +- mobile-h5/styles/components/detail.scss | 32 +- mobile-h5/styles/components/flow.scss | 8 +- mobile-h5/styles/components/form-shell.scss | 29 +- mobile-h5/styles/components/home-banner.scss | 2 +- mobile-h5/styles/components/list.scss | 30 +- mobile-h5/styles/components/menu-grid.scss | 25 +- mobile-h5/styles/components/protocol.scss | 12 +- mobile-h5/styles/components/todo.scss | 30 +- mobile-h5/styles/mixins.scss | 6 +- mobile-h5/styles/tokens.scss | 31 +- mobile-h5/styles/vant-theme.scss | 55 +- mobile-h5/utils/auth-session.js | 98 ++ mobile-h5/utils/auth.js | 75 +- mobile-h5/utils/config.js | 2 +- mobile-h5/utils/menu-icon-colors.js | 22 +- mobile-h5/utils/request.js | 46 +- mobile-oa-h5/src/main.js | 11 +- .../src/oa/components/OaDatePicker.vue | 260 ++++ .../src/oa/components/OaDeptSelect.vue | 182 ++- .../src/oa/components/OaFlowPreview.vue | 120 +- .../src/oa/components/OaOwnerDeptSelect.vue | 18 +- .../src/oa/components/OaOwnerPersonSelect.vue | 18 +- .../src/oa/components/OaPersonSelect.vue | 83 +- .../src/oa/components/OaPostSelect.vue | 46 +- .../src/oa/components/OaPulseLoading.vue | 4 +- .../oa/components/OaReceiveAccountSelect.vue | 47 +- .../src/oa/components/OaRelatedApproval.vue | 226 +++- .../src/oa/components/OaTripItinerary.vue | 326 +++-- mobile-oa-h5/src/oa/kit-runtime.js | 1 + mobile-oa-h5/src/oa/kit/oa-leave-hours.js | 2 +- mobile-oa-h5/src/oa/kit/oa-overtime-hours.js | 2 +- mobile-oa-h5/src/oa/kitchen-sink-rule.js | 32 +- mobile-oa-h5/src/oa/register-components.js | 4 + mobile-oa-h5/src/styles/oa-form.css | 277 ++++- mobile-oa-h5/src/styles/tokens.css | 23 +- mobile-oa-h5/src/views/FormSink.vue | 4 +- mobile-oa-h5/src/views/OaStart.vue | 196 ++- mobile-ui/zkxw-mobile-bright-theme-demos.html | 868 ++++++++++++++ .../zkxw-mobile-control-style-demos.html | 841 +++++++++++++ web-admin/graphify-out/GRAPH_REPORT.md | 4 +- web-admin/graphify-out/manifest.json | 6 +- .../src/components/oa-approval/OaAppIcon.vue | 2 +- .../oa-approval/OaStartFormDialog.vue | 52 +- 待同步到生产的sql脚本 | 27 - 79 files changed, 6749 insertions(+), 799 deletions(-) create mode 100644 docs/oa-start-footer-btn-style-demo.html create mode 100644 docs/oa-start-picker-list-style-demo.html create mode 100644 docs/superpowers/plans/2026-08-12-mobile-oa-task-centers.md create mode 100644 docs/superpowers/plans/2026-08-12-oa-person-picker-scheme-c.md create mode 100644 docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md create mode 100644 mobile-h5/static/menu-icons/oa-cc.png create mode 100644 mobile-h5/static/menu-icons/oa-done.png create mode 100644 mobile-h5/static/menu-icons/oa-drafts.png create mode 100644 mobile-h5/static/menu-icons/oa-mine.png create mode 100644 mobile-h5/static/menu-icons/oa-pending.png create mode 100644 mobile-h5/utils/auth-session.js create mode 100644 mobile-oa-h5/src/oa/components/OaDatePicker.vue create mode 100644 mobile-ui/zkxw-mobile-bright-theme-demos.html create mode 100644 mobile-ui/zkxw-mobile-control-style-demos.html diff --git a/backend/app/api/mini_menu.py b/backend/app/api/mini_menu.py index 67afe6fc..dffbf357 100644 --- a/backend/app/api/mini_menu.py +++ b/backend/app/api/mini_menu.py @@ -93,6 +93,16 @@ def _mobile_menu_icon(path: str) -> str: p = (path or "").lower() if "oa/start" in p: return "approve" + if "oa/drafts" in p: + return "oa-drafts" + if "oa/pending" in p: + return "oa-pending" + if "oa/done" in p: + return "oa-done" + if "oa/mine" in p: + return "oa-mine" + if "oa/cc" in p: + return "oa-cc" if "form-sink" in p: return "orders-o" return "apps-o" @@ -101,8 +111,19 @@ def _mobile_menu_icon(path: str) -> str: def _mobile_menu_icon_colors(path: str) -> dict: """宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。""" p = (path or "").lower() - if "oa/start" in p: - return {"iconBg": "#ffe4e6", "iconFg": "#e11d48"} + # OA 菜单 PNG 自带颜色,宫格不套色底 + if any( + x in p + for x in ( + "oa/start", + "oa/drafts", + "oa/pending", + "oa/done", + "oa/mine", + "oa/cc", + ) + ): + return {"iconBg": "transparent", "iconFg": ""} if "form-sink" in p: return {"iconBg": "#eef2f1", "iconFg": "#3d524a"} return {"iconBg": "#eef2f1", "iconFg": "#3d524a"} diff --git a/backend/app/core/config.py b/backend/app/core/config.py index a5aac3ea..c1d3c541 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -18,7 +18,7 @@ class Settings(BaseSettings): # JWT配置 SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string" ALGORITHM: str = "HS256" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 # 30天 + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30天 # CORS配置 BACKEND_CORS_ORIGINS: list = [ diff --git a/backend/graphify-out/.graphify_extract.json b/backend/graphify-out/.graphify_extract.json index d0311846..4e50031b 100644 --- a/backend/graphify-out/.graphify_extract.json +++ b/backend/graphify-out/.graphify_extract.json @@ -25646,7 +25646,7 @@ "label": "list_my_mobile_menus()", "file_type": "code", "source_file": "api/mini_menu.py", - "source_location": "L112" + "source_location": "L113" }, { "id": "api_mini_menu_rationale_1", @@ -103624,7 +103624,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L112", + "source_location": "L113", "weight": 1.0 }, { @@ -103633,7 +103633,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L114", + "source_location": "L115", "weight": 1.0 }, { @@ -103642,7 +103642,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L122", + "source_location": "L123", "weight": 1.0 }, { @@ -103651,7 +103651,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L126", + "source_location": "L127", "weight": 1.0 }, { @@ -103660,7 +103660,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L128", + "source_location": "L129", "weight": 1.0 }, { @@ -103669,7 +103669,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L139", + "source_location": "L140", "weight": 1.0 }, { @@ -103678,7 +103678,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L145", + "source_location": "L146", "weight": 1.0 }, { @@ -290562,7 +290562,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L116", + "source_location": "L117", "weight": 1.0 }, { @@ -290572,7 +290572,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L118", + "source_location": "L119", "weight": 1.0 }, { @@ -290582,7 +290582,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L118", + "source_location": "L119", "weight": 1.0 }, { @@ -290592,7 +290592,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L124", + "source_location": "L125", "weight": 1.0 }, { @@ -290602,7 +290602,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L127", + "source_location": "L128", "weight": 1.0 }, { diff --git a/backend/graphify-out/GRAPH_REPORT.md b/backend/graphify-out/GRAPH_REPORT.md index be6fe2f6..125d884b 100644 --- a/backend/graphify-out/GRAPH_REPORT.md +++ b/backend/graphify-out/GRAPH_REPORT.md @@ -1,7 +1,7 @@ -# Graph Report - app (2026-08-11) +# Graph Report - app (2026-08-12) ## Corpus Check -- Large corpus: 250 files · ~303,240 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only. +- Large corpus: 250 files · ~303,244 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only. ## Summary - 5238 nodes · 28666 edges · 43 communities detected diff --git a/backend/graphify-out/graph.json b/backend/graphify-out/graph.json index 5909bdca..2e7e3ec1 100644 --- a/backend/graphify-out/graph.json +++ b/backend/graphify-out/graph.json @@ -32533,7 +32533,7 @@ "label": "list_my_mobile_menus()", "file_type": "code", "source_file": "api/mini_menu.py", - "source_location": "L112", + "source_location": "L113", "id": "api_mini_menu_list_my_mobile_menus", "community": 10, "norm_label": "list_my_mobile_menus()" @@ -58348,7 +58348,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L124", + "source_location": "L125", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "core_response_success_response", @@ -67204,7 +67204,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L116", + "source_location": "L117", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "core_response_error_response", @@ -144364,7 +144364,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L118", + "source_location": "L119", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "str", @@ -340912,7 +340912,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L118", + "source_location": "L119", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "crud_base_crudbase_get", @@ -355611,7 +355611,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L112", + "source_location": "L113", "weight": 1.0, "_src": "api_mini_menu_py", "_tgt": "api_mini_menu_list_my_mobile_menus", @@ -355635,7 +355635,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L114", + "source_location": "L115", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "api_mini_menu_current_user", @@ -355647,7 +355647,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L122", + "source_location": "L123", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "api_mini_menu_find_mobile_root", @@ -355659,7 +355659,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L126", + "source_location": "L127", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "api_mini_menu_collect_descendants", @@ -355671,7 +355671,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L128", + "source_location": "L129", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "api_mini_menu_user_menu_ids", @@ -355695,7 +355695,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L145", + "source_location": "L146", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "api_mini_menu_mobile_menu_icon", @@ -355719,7 +355719,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "api/mini_menu.py", - "source_location": "L139", + "source_location": "L140", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "api_mini_menu_mobile_menu_icon_colors", @@ -355744,7 +355744,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "api/mini_menu.py", - "source_location": "L127", + "source_location": "L128", "weight": 1.0, "_src": "api_mini_menu_list_my_mobile_menus", "_tgt": "services_web_auth_roles_resolve_web_role_for_user", diff --git a/backend/graphify-out/manifest.json b/backend/graphify-out/manifest.json index 629828cf..13ca7573 100644 --- a/backend/graphify-out/manifest.json +++ b/backend/graphify-out/manifest.json @@ -184,7 +184,7 @@ "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1786374022.5691514, "/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1782894693.6582327, "/Users/wuzilong/work/00code/zkxw/backend/app/api/production_material.py": 1782404982.2474263, - "/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1786438411.2164767, + "/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1786512705.799277, "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1785207187.402777, "/Users/wuzilong/work/00code/zkxw/backend/app/api/market.py": 1781522283.6823432, "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1782718998.90251, diff --git a/docs/oa-start-footer-btn-style-demo.html b/docs/oa-start-footer-btn-style-demo.html new file mode 100644 index 00000000..d99f7cac --- /dev/null +++ b/docs/oa-start-footer-btn-style-demo.html @@ -0,0 +1,558 @@ + + + + + + 发起审批 · 底栏按钮重设计 Demo + + + +
+

底栏按钮 · 重新设计

+

+ 上一版问题:草稿与提交都抢绿色、圆角/描边千篇一律、缩略图看不出层级。 + 这一版只保留 8 个差异化方向,右侧大屏实时预览;核心原则是 + 草稿弱、提交强,避免双绿对撞。 +

+
+ 点左侧方案切换右侧手机。选好后回复编号,例如「用 S1」。 + 取消是否保留也可一并说明。 +
+ +
+
+ + +
+
+ + + + diff --git a/docs/oa-start-picker-list-style-demo.html b/docs/oa-start-picker-list-style-demo.html new file mode 100644 index 00000000..08dc9a2d --- /dev/null +++ b/docs/oa-start-picker-list-style-demo.html @@ -0,0 +1,1051 @@ + + + + + + 发起审批 · 下拉选择弹窗列表样式 Demo + + + +
+

发起审批 · 下拉选择弹窗列表样式

+

+ 针对 OA 发起页中人员 / 部门 / 岗位 / 收款账户 / 关联审批等底部弹窗列表,提供多套视觉方案。 + 左侧切换方案,右侧手机预览可点选体验;上方可切换「单选 / 多选」与「数据场景」。 +

+
+ Token 对齐 mobile-h5「薄荷微风」:主色 #22c55e、软底 #ecfdf5、正文 #0f291e。 + 当前线上基线为方案 A(透明 cell + 间距,选中态弱)。 +
+ +
+ + + + + + + +
+ +
+
+ +
+
+
+
+ ‹ + 发起审批 +
+
+
+
审批人
+
+ 请选择 + › +
+
+
+
事由
+
请输入
+
+

点击上方字段打开选择弹窗。切换方案时弹窗保持打开,便于对比。

+
+ + +
+
+
+ 当前方案:A · 现状基线 +
+
+
+
+
+ + + + diff --git a/docs/superpowers/plans/2026-08-12-mobile-oa-task-centers.md b/docs/superpowers/plans/2026-08-12-mobile-oa-task-centers.md new file mode 100644 index 00000000..4f68f044 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-mobile-oa-task-centers.md @@ -0,0 +1,788 @@ +# 移动端 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:** 在小程序落地草稿箱 / 待办 / 已办 / 我发起的 / 抄送我的五中心:原生列表 + H5 详情办理(对齐 Web 业务,UI 对齐既有 mobile-h5 / mobile-oa-h5)。 + +**Architecture:** 列表在 `mobile-h5`;表单与办理在 `mobile-oa-h5` 新增 `/oa-detail`(`mode=view|approve`);草稿编辑继续 `/oa-start?draftId=`。API 复用 `/web/admin/oa-approval/*` + JWT。菜单种子挂「移动端菜单」根下。 + +**Tech Stack:** uni-app 微信小程序、`web-view`、Vue3、Vant4、`@form-create/vant`、FastAPI(菜单 SQL) + +**Spec:** `docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md` + +**原则(强制):** 功能语义对齐 `web-admin` OA 对应页与 `instances/detail.vue`;UI 对齐 `mobile-h5`(如 `pages/demo/list`、`pages/oa/start`)与 `mobile-oa-h5`(`OaStart.vue`、footer、picker),禁止照搬 Element 桌面布局。 + +**已完成(勿重复):** 五菜单 PNG → `mobile-h5/static/menu-icons/oa-*.png`;`van-icon` / `menu-icon-colors.js` / `backend/app/api/mini_menu.py` path→icon 映射。 + +**Out of scope:** 时间范围/分组/流程状态等已剔除筛选;Web 设计器改造;小程序内直挂 form-create。 + +--- + +## File map + +| 路径 | 职责 | +|------|------| +| `docs/sql/mobile_menu_oa_task_centers.sql` | 五菜单种子(OA002–OA006) | +| `mobile-h5/utils/oa-api.js` | drafts / tasks / mark-read / counts | +| `mobile-h5/utils/oa-h5.js` | `buildOaDetailUrl`;扩展 start 预填若需要 | +| `mobile-h5/utils/oa-badge.js` | 拉草稿/待办/抄送角标(可新建) | +| `mobile-h5/pages.json` | 注册五列表 + 通用 webview(或扩 start/webview) | +| `mobile-h5/pages/oa/drafts/index.vue` | 草稿箱 | +| `mobile-h5/pages/oa/pending/index.vue` | 待办 | +| `mobile-h5/pages/oa/done/index.vue` | 已办 | +| `mobile-h5/pages/oa/mine/index.vue` | 我发起的 | +| `mobile-h5/pages/oa/cc/index.vue` | 抄送我的 | +| `mobile-h5/pages/oa/webview/index.vue` | 通用 web-view(start + detail)**或** 扩展 `start/webview.vue` | +| `mobile-h5/components/OaTaskCard.vue` | 任务/草稿卡片(移动端样式,非 BizListCard 业务 mock) | +| `mobile-oa-h5/src/api/oa.js` | instance detail + task actions + withdraw + cc read | +| `mobile-oa-h5/src/views/OaDetail.vue` | 详情只读 / 办理 | +| `mobile-oa-h5/src/oa/components/OaProgressTimeline.vue` | 审批进度(对齐 Web progress;可基于现有 Flow 组件) | +| `mobile-oa-h5/src/oa/components/OaActionSheet*.vue` | 意见 / 加签 / 转交弹层(Vant Dialog/Popup) | +| `mobile-oa-h5/src/router.js` | `/oa-detail` | +| `mobile-oa-h5/src/oa/print/`(可选) | PDF 打印移植 | + +**Web 只读对照:** + +- `web-admin/src/views/oa-approval/{drafts,pending,done,mine,cc}/index.vue` +- `web-admin/src/views/oa-approval/instances/detail.vue` +- `web-admin/src/api/oaApproval.ts` +- `web-admin/src/utils/oa-approval-print.ts`(打印) + +--- + +## 目标用户路径 + +``` +首页宫格(授权菜单) + → 五列表之一(原生) + → 草稿编辑 → webview → /oa-start?draftId= + → 待办办理 → webview → /oa-detail?mode=approve&instanceId=&taskId= + → 已办/抄送/我发起查看 → /oa-detail?mode=view&instanceId= + → postMessage → navigateBack → 列表 onShow 刷新 + 角标 +``` + +--- + +### Task 1: 菜单 SQL(S0) + +**Files:** +- Create: `docs/sql/mobile_menu_oa_task_centers.sql` + +- [ ] **Step 1: 按 `mobile_menu_oa_start.sql` 写五条幂等 INSERT** + +完整内容: + +```sql +-- 小程序 OA 五中心菜单种子(挂在「移动端菜单」根下) +-- 前置:docs/sql/mobile_menu_oa_start.sql(发起审批 OA001)可已执行 +-- 角色授权请在 Web「角色管理」勾选本菜单。 + +SET @mini_root = ( + SELECT id FROM sys_menu + WHERE del_flag = 0 AND parent_id = '0' AND menu_name LIKE '%移动端%' + LIMIT 1 +); + +-- 草稿箱 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + 'MINI000000000000000000000000OA002', + '草稿箱', + @mini_root, + 'C', + '/pages/oa/drafts/index', + NULL, + 'mini:oa:drafts', + 1, + '1', + '1', + 'zkxw', + 20, + '小程序草稿箱', + 'admin', + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE @mini_root IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA002' AND del_flag = 0 + ); + +-- 待办中心 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + 'MINI000000000000000000000000OA003', + '待办中心', + @mini_root, + 'C', + '/pages/oa/pending/index', + NULL, + 'mini:oa:pending', + 1, + '1', + '1', + 'zkxw', + 30, + '小程序待办中心', + 'admin', + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE @mini_root IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA003' AND del_flag = 0 + ); + +-- 已办中心 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + 'MINI000000000000000000000000OA004', + '已办中心', + @mini_root, + 'C', + '/pages/oa/done/index', + NULL, + 'mini:oa:done', + 1, + '1', + '1', + 'zkxw', + 40, + '小程序已办中心', + 'admin', + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE @mini_root IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA004' AND del_flag = 0 + ); + +-- 我发起的 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + 'MINI000000000000000000000000OA005', + '我发起的', + @mini_root, + 'C', + '/pages/oa/mine/index', + NULL, + 'mini:oa:mine', + 1, + '1', + '1', + 'zkxw', + 50, + '小程序我发起的', + 'admin', + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE @mini_root IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA005' AND del_flag = 0 + ); + +-- 抄送我的 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + 'MINI000000000000000000000000OA006', + '抄送我的', + @mini_root, + 'C', + '/pages/oa/cc/index', + NULL, + 'mini:oa:cc', + 1, + '1', + '1', + 'zkxw', + 60, + '小程序抄送我的', + 'admin', + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE @mini_root IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM sys_menu WHERE id = 'MINI000000000000000000000000OA006' AND del_flag = 0 + ); +``` + +- [ ] **Step 2: 在测试库执行 SQL,Web 角色管理勾选后调 `GET /mini/menus` 应含 5 条 path** + +Expected: list 中出现 `/pages/oa/drafts|pending|done|mine|cc/index`,icon 分别为 `oa-drafts|oa-pending|oa-done|oa-mine|oa-cc`。 + +- [ ] **Step 3: Commit** + +```bash +git add docs/sql/mobile_menu_oa_task_centers.sql +git commit -m "$(cat <<'EOF' +feat(oa): add mobile menu seeds for five OA task centers + +EOF +)" +``` + +--- + +### Task 2: 扩展双端 OA API(S0/S1 前置) + +**Files:** +- Modify: `mobile-h5/utils/oa-api.js` +- Modify: `mobile-oa-h5/src/api/oa.js` + +- [ ] **Step 1: 在 `mobile-h5/utils/oa-api.js` 追加(保留现有 templates 方法;按项目 `request.js` 支持的 get/post/del 写法实现)** + +对齐 `web-admin/src/api/oaApproval.ts`: + +```js +// drafts +export function fetchOaInstanceDraftList(params) { /* GET /web/admin/oa-approval/drafts */ } +export function fetchOaInstanceDraftCount() { /* GET .../drafts/count */ } +export function fetchDeleteOaInstanceDraft(id) { /* DELETE .../drafts/{id} */ } +export function fetchBatchDeleteOaInstanceDrafts(ids) { /* POST .../drafts/batch-delete { ids } */ } +export function fetchSubmitOaInstanceDraft(id, data) { /* POST .../drafts/{id}/submit */ } + +// tasks +export function fetchOaTasks(kind, params) { + // kind: pending|done|mine|cc + // GET .../tasks/{kind} query: page,pageSize,appName,initiatorName,readStatus(cc) +} +export function fetchOaPendingTaskCount() { /* GET .../tasks/pending/count */ } +export function fetchOaCcUnreadCount() { /* GET .../tasks/cc/unread-count */ } +export function fetchMarkOaCcTaskRead(taskId) { /* POST .../tasks/{id}/read */ } +export function fetchBatchMarkOaCcTasksRead(ids) { /* POST .../tasks/cc/read-batch { ids } */ } +``` + +实现时复用文件内已有 `get` / `authHeader`;若 `request.js` 无 `del`/`post`,按同目录其它 utils 写法调用。每个函数必须带 `Authorization: Bearer`。 + +- [ ] **Step 2: 在 `mobile-oa-h5/src/api/oa.js` 追加** + +```js +export function fetchOaInstanceDetail(id) { + return get(`/web/admin/oa-approval/instances/${encodeURIComponent(id)}`) +} + +export function fetchApproveOaTask(taskId, comment) { + return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/approve`, { comment }) +} + +export function fetchRejectOaTask(taskId, comment) { + return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/reject`, { comment }) +} + +export function fetchReturnOaTask(taskId, comment) { + return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/return`, { comment }) +} + +export function fetchAddSignOaTask(taskId, data) { + // data: { userIds: string[], comment?: string } + return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/add-sign`, data) +} + +export function fetchTransferOaTask(taskId, data) { + // data: { targetUserId: string, comment?: string } + return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/transfer`, data) +} + +export function fetchWithdrawOaInstance(instanceId) { + return post(`/web/admin/oa-approval/instances/${encodeURIComponent(instanceId)}/withdraw`, {}) +} + +export function fetchMarkOaCcTaskRead(taskId) { + return post(`/web/admin/oa-approval/tasks/${encodeURIComponent(taskId)}/read`, {}) +} +``` + +- [ ] **Step 3: 本地用已登录 token 在浏览器/控制台或临时页面各打一条 drafts list / tasks pending 验证 200** + +- [ ] **Step 4: Commit** + +```bash +git add mobile-h5/utils/oa-api.js mobile-oa-h5/src/api/oa.js +git commit -m "$(cat <<'EOF' +feat(oa): extend mobile OA APIs for drafts, tasks, and detail actions + +EOF +)" +``` + +--- + +### Task 3: 通用 web-view + `buildOaDetailUrl`(S0) + +**Files:** +- Modify: `mobile-h5/utils/oa-h5.js` +- Create or Modify: `mobile-h5/pages/oa/webview/index.vue`(推荐新建通用页)**或** 扩展 `pages/oa/start/webview.vue` +- Modify: `mobile-h5/pages.json` + +- [ ] **Step 1: 在 `oa-h5.js` 增加** + +```js +/** 审批详情 / 办理页 */ +export function buildOaDetailUrl({ instanceId, taskId, mode } = {}) { + const token = getToken() + const params = {} + if (instanceId) params.instanceId = instanceId + if (taskId) params.taskId = taskId + if (mode) params.mode = mode // view | approve + if (token) params.token = token + return withQuery('/oa-detail', params) +} + +const DETAIL_TITLE_KEY = 'oa_detail_nav_title' + +export function stashOaDetailTitle(name) { /* 同 stashOaStartTitle 模式 */ } +export function takeOaDetailTitle() { /* 同 takeOaStartTitle */ } +``` + +- [ ] **Step 2: 通用 webview 页逻辑** + +`onLoad(query)`: + +1. 无 token → `forceRelogin` +2. 若 `query.draftId` 或(`query.appId` 且无 `instanceId`)→ `buildOaStartUrl` +3. 若 `query.instanceId` → `buildOaDetailUrl({ instanceId, taskId, mode })` +4. 标题:`takeOaDetailTitle()` / `takeOaStartTitle()` +5. `CLOSE_ACTIONS` 扩展:`oa-detail-close`、`oa-detail-done`、`oa-detail-withdraw`、`oa-detail-restart` 等;H5 iframe `message` 与小程序 `@message` 行为对齐现有 start webview + +- [ ] **Step 3: `pages.json` 注册五列表空壳路径 + webview(标题用中文业务名)** + +示例条目: + +```json +{ + "path": "pages/oa/drafts/index", + "style": { + "navigationBarTitleText": "草稿箱", + "navigationBarBackgroundColor": "#ecfdf5", + "navigationBarTextStyle": "black" + } +} +``` + +同样注册 `pending`/`done`/`mine`/`cc`;webview 标题默认「审批详情」。 + +- [ ] **Step 4: 真机/开发者工具打开任意 webview URL 带 token,确认能加载 H5(detail 可先 404,本 Task 只要求 start 路径不回归)** + +- [ ] **Step 5: Commit** + +```bash +git add mobile-h5/utils/oa-h5.js mobile-h5/pages.json mobile-h5/pages/oa/webview/ +git commit -m "$(cat <<'EOF' +feat(oa): add shared OA webview and detail URL builder + +EOF +)" +``` + +--- + +### Task 4: 共用任务卡片 + 列表壳(UI) + +**Files:** +- Create: `mobile-h5/components/OaTaskCard.vue` +- Create: `mobile-h5/styles/components/oa-task-list.scss`(若项目按组件拆 scss) +- Reference UI: `mobile-h5/pages/demo/list.vue`、`pages/oa/start/index.vue`、`styles/tokens.scss` + +- [ ] **Step 1: 实现 `OaTaskCard`** + +Props 建议: + +```js +{ + title: String, // 流程名 + statusText: String, + statusTone: String, // warn|success|danger|info + metaLines: Array, // [{ label, value }] + unread: Boolean, // 抄送红点 + actions: Array // [{ key, label, primary?, danger? }] +} +``` + +Emits: `action` with `{ key, ... }`;整卡点击可 emit `open`。 + +样式:三墙 Token;卡片圆角/间距对齐 start 列表与 demo list;**不要**复制 Web 表格列。 + +- [ ] **Step 2: 抽可复用 list mixin 或 `utils/oa-list.js`(可选)** + +提供:`page`/`pageSize`/`loading`/`finished`、`loadMore`、`onPullDownRefresh` 重置。若不想抽 mixin,五页内复制最小分页逻辑亦可(YAGNI:先两页共用后再抽)。 + +- [ ] **Step 3: Commit** + +```bash +git add mobile-h5/components/OaTaskCard.vue mobile-h5/styles/components/oa-task-list.scss +git commit -m "$(cat <<'EOF' +feat(oa): add mobile OA task card for center lists + +EOF +)" +``` + +--- + +### Task 5: 草稿箱列表(S1) + +**Files:** +- Create: `mobile-h5/pages/oa/drafts/index.vue` +- Modify: `mobile-h5/pages.json`(若 Task3 已注册可跳过) + +**功能清单(规格 §2.1):** 列表、分页、流程名筛选、编辑、列表提交、单删、批量删、角标(角标可先调 count API,宫格角标 UI 放 Task 11)。 + +- [ ] **Step 1: 实现页面** + +结构参考 `pages/demo/list.vue`:顶区搜索(仅关键词)+ 列表 + 多选模式批量删。 + +关键方法: + +```js +async loadList(reset) { + // fetchOaInstanceDraftList({ keyword, page, pageSize }) +} +openEdit(row) { + stashOaStartTitle(row.appName || row.title) + uni.navigateTo({ + url: `/pages/oa/webview/index?draftId=${row.id}&appId=${row.appId || ''}` + // 若仍用 start/webview:`/pages/oa/start/webview?draftId=...` + }) +} +async submitRow(row) { + // uni.showModal 确认 → fetchSubmitOaInstanceDraft(row.id) → 刷新 + // 失败:可改为 openEdit 让用户改表单(对齐 Web) +} +async deleteRow(row) { /* confirm → fetchDeleteOaInstanceDraft */ } +async batchDelete() { /* selected ids → fetchBatchDeleteOaInstanceDrafts */ } +``` + +- [ ] **Step 2: onShow 刷新列表** + +- [ ] **Step 3: 手工验收** + +1. 发起页暂存 → 草稿箱可见 +2. 编辑打开 H5 回填 → 再暂存/提交 +3. 列表提交成功后草稿消失 +4. 单删、批量删 + +- [ ] **Step 4: Commit** + +```bash +git add mobile-h5/pages/oa/drafts/ +git commit -m "$(cat <<'EOF' +feat(oa): add mobile draft box list with edit submit delete + +EOF +)" +``` + +--- + +### Task 6: H5 `/oa-detail` 只读(S2) + +**Files:** +- Create: `mobile-oa-h5/src/views/OaDetail.vue` +- Create: `mobile-oa-h5/src/oa/components/OaProgressTimeline.vue`(或复用/改造现有 flow 展示) +- Modify: `mobile-oa-h5/src/router.js` +- Reference: `OaStart.vue`(schema-prep、form-create disabled)、Web `detail.vue`(progress 字段) + +- [ ] **Step 1: 注册路由** + +```js +import OaDetail from './views/OaDetail.vue' +// ... +{ path: '/oa-detail', component: OaDetail } +``` + +- [ ] **Step 2: `OaDetail.vue` 最小只读** + +1. `captureTokenFromUrl`(与 main/OaStart 一致) +2. 读 `instanceId`、`mode`(默认 view)、`taskId` +3. `fetchOaInstanceDetail` → `formSchemaJson` + `formData` → `schema-prep` → `` +4. 渲染 `progress` 时间线 +5. 顶栏状态 Tag(文案映射对齐 Web `statusText`) +6. 底栏:`mode=view` 且非发起人特殊操作时仅「关闭」→ `postMessage` / `wx.miniProgram.navigateBack`,action=`oa-detail-close` + +UI:padding、底栏按钮类名对齐 `OaStart` / `oa-form.css` / footer demo。 + +- [ ] **Step 3: 浏览器直接打开** + +`http://127.0.0.1:5178/oa-h5/oa-detail?instanceId=真实ID&token=...&mode=view` +Expected: 表单只读可见、进度可见、关闭可返回。 + +- [ ] **Step 4: Commit** + +```bash +git add mobile-oa-h5/src/views/OaDetail.vue mobile-oa-h5/src/oa/components/OaProgressTimeline.vue mobile-oa-h5/src/router.js +git commit -m "$(cat <<'EOF' +feat(oa): add mobile OA detail page in read-only mode + +EOF +)" +``` + +--- + +### Task 7: 已办 / 我发起 / 抄送列表进 view(S2) + +**Files:** +- Create: `mobile-h5/pages/oa/done/index.vue` +- Create: `mobile-h5/pages/oa/mine/index.vue`(本 Task 仅查看;打印/复制/撤销后续 Task) +- Create: `mobile-h5/pages/oa/cc/index.vue`(本 Task:列表+打开已读+进详情;阅读筛选/批量可本 Task 一并做或见 Task 10) + +- [ ] **Step 1: 已办页** + +筛选:`appName`、`initiatorName`(搜索框 + 可选第二条件,移动端可用一行双字段或展开筛选,**不要** Web 日期/状态)。 +`fetchOaTasks('done', …)` → 卡片 → `stashOaDetailTitle` → navigate webview `instanceId&mode=view`。 + +- [ ] **Step 2: 我发起的页(查看)** + +筛选仅 `appName`。`fetchOaTasks('mine', …)` → view。 + +- [ ] **Step 3: 抄送页(查看 + 打开已读)** + +打开前或打开时:`fetchMarkOaCcTaskRead(taskId)`(task 行上的抄送任务 id,字段名对齐 Web `row.id`)。 +再进 `mode=view`。H5 详情可再兜底 mark-read。 + +- [ ] **Step 4: 验收三页均可进只读详情并返回刷新** + +- [ ] **Step 5: Commit** + +```bash +git add mobile-h5/pages/oa/done/ mobile-h5/pages/oa/mine/ mobile-h5/pages/oa/cc/ +git commit -m "$(cat <<'EOF' +feat(oa): add done mine cc lists opening read-only OA detail + +EOF +)" +``` + +--- + +### Task 8: 待办列表 + 办理动作(S3) + +**Files:** +- Create: `mobile-h5/pages/oa/pending/index.vue` +- Modify: `mobile-oa-h5/src/views/OaDetail.vue` +- Create: 意见弹层 / 加签选人 / 转交选人(复用 `OaPersonSelect` 或 Vant Popup + 已有选人) + +**对照 Web `detail.vue`:** `myPendingTask`、`canApprove`、`allowReturn`、`allowAddSign`、转交排除规则。 + +- [ ] **Step 1: 待办列表** + +筛选:流程名、申请人。进入:`mode=approve&instanceId=&taskId=`(taskId 用行上待办任务 id)。 + +- [ ] **Step 2: 详情底栏(`mode=approve` 且存在当前用户 pending task)** + +| 按钮 | API | +|------|-----| +| 同意 | `fetchApproveOaTask(taskId, comment?)` | +| 拒绝 | `fetchRejectOaTask` | +| 打回 | 意见必填 → `fetchReturnOaTask` | +| 加签 | 选多人 → `fetchAddSignOaTask({ userIds, comment })` | +| 转交 | 选一人(排除本人与本节点已有待办)→ `fetchTransferOaTask({ targetUserId, comment })` | + +成功后 toast + `postMessage({ source:'zkxw-oa-h5', action:'oa-detail-done' })` + navigateBack。 + +意见输入用 Vant Dialog/Popup,样式对齐 mobile-oa-h5,不要 Element MessageBox。 + +- [ ] **Step 3: 五条办理主路径各测至少一次(可用测试租户)** + +- [ ] **Step 4: Commit** + +```bash +git add mobile-h5/pages/oa/pending/ mobile-oa-h5/src/views/OaDetail.vue mobile-oa-h5/src/oa/components/ +git commit -m "$(cat <<'EOF' +feat(oa): add pending list and approve reject return add-sign transfer + +EOF +)" +``` + +--- + +### Task 9: 我发起的 — 撤销与复制重开(S4) + +**Files:** +- Modify: `mobile-h5/pages/oa/mine/index.vue` +- Modify: `mobile-oa-h5/src/views/OaDetail.vue` +- Modify: `mobile-oa-h5/src/views/OaStart.vue`(接收预填) +- Modify: `mobile-h5/utils/oa-h5.js`(可选 storage 传 fromInstance 预填) + +- [ ] **Step 1: 详情撤销** + +条件:当前用户是发起人且实例 `running`(与 Web `canWithdraw` 一致)。 +`fetchWithdrawOaInstance` → done 回列表。 + +- [ ] **Step 2: 复制并重新发起** + +条件:发起人且 status ∈ `returned|cancelled|rejected`。 +实现择一(优先与 Web 一致): + +1. H5:`postMessage` action=`oa-detail-restart` + `appId` + 小程序再开 start webview,并用 `uni.setStorageSync('oa_restart_prefills', { formData, … })` +2. 或 H5 内 `router.replace({ path:'/oa-start', query:{ appId, fromInstanceId } })`,`OaStart` 拉实例详情回填 + +选定一种写清,并在 `OaStart` 实现回填;提交成功后勿留下旧 instanceId。 + +- [ ] **Step 3: 列表行也提供「复制并重新发起」入口(终态时)** + +- [ ] **Step 4: 验收撤销与复制提交新单** + +- [ ] **Step 5: Commit** + +```bash +git add mobile-h5/pages/oa/mine/ mobile-oa-h5/src/views/OaDetail.vue mobile-oa-h5/src/views/OaStart.vue mobile-h5/utils/oa-h5.js +git commit -m "$(cat <<'EOF' +feat(oa): support withdraw and copy-restart on mobile mine flows + +EOF +)" +``` + +--- + +### Task 10: 抄送增强(S5) + +**Files:** +- Modify: `mobile-h5/pages/oa/cc/index.vue` + +- [ ] **Step 1: 阅读状态筛选** — `readStatus`: `unread`|`read`|`all`(默认 unread,对齐 Web) +- [ ] **Step 2: 未读红点样式** — `OaTaskCard` `unread` +- [ ] **Step 3: 单条已读** — 不打开详情也可 mark +- [ ] **Step 4: 批量已读** — 多选 + `fetchBatchMarkOaCcTasksRead` +- [ ] **Step 5: 验收后 Commit** + +```bash +git add mobile-h5/pages/oa/cc/ +git commit -m "$(cat <<'EOF' +feat(oa): add cc read filters batch mark and unread styles + +EOF +)" +``` + +--- + +### Task 11: 角标(S1/S3/S5/S7) + +**Files:** +- Create: `mobile-h5/utils/oa-badge.js` +- Modify: `mobile-h5/components/MenuGrid.vue` 和/或 `pages/index/index.vue` + +- [ ] **Step 1: `refreshOaMenuBadges`** + +并行请求: + +- `fetchOaInstanceDraftCount` +- `fetchOaPendingTaskCount` +- `fetchOaCcUnreadCount` + +返回 map:`path → count`(`/pages/oa/drafts/index` 等)。展示:`>99` 显示 `99+`。 + +- [ ] **Step 2: 首页加载菜单后合并 badge;`onShow` 刷新** + +MenuGrid 在 title 旁或图标右上角显示数字(样式轻量,对齐三墙,勿过重)。 + +- [ ] **Step 3: 从 webview 返回后各列表 `onShow` 也可触发首页角标(通过 storage 事件或全局简单 bus;最小做法:仅首页 onShow 刷新即可) + +- [ ] **Step 4: Commit** + +```bash +git add mobile-h5/utils/oa-badge.js mobile-h5/components/MenuGrid.vue mobile-h5/pages/index/index.vue +git commit -m "$(cat <<'EOF' +feat(oa): show draft pending cc badges on mobile home menu + +EOF +)" +``` + +--- + +### Task 12: 打印(S6) + +**Files:** +- 移植参考:`web-admin/src/utils/oa-approval-print.ts` +- Create: `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js`(或等价) +- Modify: `OaDetail.vue` + `mine`/`cc` 列表入口 + +- [ ] **Step 1: 在 H5 内实现 PDF 生成(优先 jspdf/html2canvas 等与 Web 同类方案;以 Web 工具函数逻辑为准)** + +- [ ] **Step 2: 详情与列表「打印」按钮**:生成后尝试下载 / 新开预览;微信内若失败,`showToast` 明确文案(规格允许兜底,不阻塞其它功能) + +- [ ] **Step 3: 真机验收;记录限制到 `docs/superpowers/specs/` 旁注或本 plan 底部「打印验收笔记」 + +- [ ] **Step 4: Commit** + +```bash +git add mobile-oa-h5/src/oa/print/ mobile-oa-h5/src/views/OaDetail.vue mobile-h5/pages/oa/mine/ mobile-h5/pages/oa/cc/ +git commit -m "$(cat <<'EOF' +feat(oa): add mobile approval PDF print for mine and cc + +EOF +)" +``` + +--- + +### Task 13: 回归与图谱(收尾) + +- [ ] **Step 1: 回归「发起审批」全链路未被破坏**(列表→填写→草稿→提交) +- [ ] **Step 2: 按规格 §8 测试要点过一遍勾选 +- [ ] **Step 3: 若改了 backend `mini_menu.py`(本计划默认已改完),执行 `./scripts/update-graph.sh backend` +- [ ] **Step 4: 最终 commit 若有文档修补** + +--- + +## Spec coverage 自检 + +| 规格项 | Task | +|--------|------| +| 菜单 SQL OA002–006 + 图标映射 | Task 1(图标已完成) | +| 功能对齐 Web / UI 对齐移动端 | 全文原则 + Task 4–8 | +| 草稿 1/2/3/6/7/8/9/10 | Task 5 + Task 11 | +| 待办办理五动作 + 角标 | Task 8 + 11 | +| 已办只读 | Task 6–7 | +| 我发起撤销/复制/打印 | Task 9 + 12 | +| 抄送阅读/批量/打印/角标 | Task 7/10/11/12 | +| `/oa-detail` | Task 6 + 8 + 9 | +| S0–S7 顺序 | Task 1→12 | + +--- + +## 执行说明 + +实现时每完成一个 Task 再开下一个;打印(Task 12)可与角标并行,但不得阻塞 Task 5–10 合并。 diff --git a/docs/superpowers/plans/2026-08-12-oa-person-picker-scheme-c.md b/docs/superpowers/plans/2026-08-12-oa-person-picker-scheme-c.md new file mode 100644 index 00000000..e412723f --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-oa-person-picker-scheme-c.md @@ -0,0 +1,98 @@ +# OA 人员弹窗方案 C 对齐 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:** 让发起审批表单【人员】选择弹窗列表与「选择审批人/抄送人」一致,采用 Demo 方案 C(分割线行表 + 姓名与副信息同行)。 + +**Architecture:** 只改 `OaPersonSelect.vue` 列表项模板与用户映射;在 `oa-form.css` 补充 `.oa-pick__name` / `.oa-pick__sub`。行级方案 C 样式已存在,无需重写。 + +**Tech Stack:** Vue 3、Vant、`mobile-oa-h5` + +**Spec:** `docs/superpowers/specs/2026-08-12-oa-person-picker-scheme-c-design.md` + +--- + +### Task 1: 列表项模板改为同行姓名 + 副信息 + +**Files:** +- Modify: `mobile-oa-h5/src/oa/components/OaPersonSelect.vue` + +- [x] **Step 1: 替换多选 / 单选 `van-cell` 为 `#title` 插槽结构** + +多选与单选均去掉 `:title` / `:label`,改为: + +```vue + + + + +``` + +单选同理,`@click` 设 `pendingSingle`,icon 用 `van-radio`。 + +- [x] **Step 2: 更新 `loadUsers` 副信息与搜索** + +```js +const phone = String(u.phoneNo || '').trim() +const userNo = String(u.userNo || '').trim() +const nick = String(u.nickName || '').trim() +const label = nick || userNo || id +const sub = phone || (userNo && userNo !== label ? userNo : '') +opts.push({ id, label, sub }) +``` + +`filtered` 已用 `u.sub`,保持即可(手机或工号都会进 `sub`)。 + +可选:搜索 placeholder 改为「搜索姓名、工号或手机号」。 + +--- + +### Task 2: 补充同行文字样式 + +**Files:** +- Modify: `mobile-oa-h5/src/styles/oa-form.css` + +- [x] **Step 1: 在 `.oa-pick__check` 规则前增加 name/sub 样式,并强化选中态** + +```css +.oa-pick__name { + font-size: 14px; + color: var(--text-primary, #0f291e); +} + +.oa-pick__sub { + margin-left: 8px; + font-size: 12px; + color: var(--text-muted, #5f7a6d); +} + +.oa-pick__list .van-cell:has(.van-checkbox--checked) .oa-pick__name, +.oa-pick__list .van-cell:has(.van-radio--checked) .oa-pick__name { + color: #15803d; +} +``` + +(保留现有 `.van-cell__title` 选中规则作兜底。) + +--- + +### Task 3: 验收与提交 + +- [ ] **Step 1: 手动核对** — FormSink / 发起页打开【人员】弹窗:分割线行、选中浅绿、姓名副信息同行;与流程选人弹窗观感一致。 + +- [ ] **Step 2: Commit** + +```bash +git add mobile-oa-h5/src/oa/components/OaPersonSelect.vue mobile-oa-h5/src/styles/oa-form.css +git commit -m "fix(mobile-oa-h5): 人员选择弹窗列表对齐方案 C" +``` diff --git a/docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md b/docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md new file mode 100644 index 00000000..950aa148 --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md @@ -0,0 +1,273 @@ +# 移动端 OA 任务中心(草稿 / 待办 / 已办 / 我发起 / 抄送)设计 + +> 日期:2026-08-12 +> 工程:`mobile-h5`(微信小程序)+ `mobile-oa-h5`(OA 表单 H5) +> 对齐参考:`web-admin` 下 OA审批 六菜单(发起已落地移动端;本设计覆盖其余五页) +> 上游设计:`docs/superpowers/specs/2026-08-11-mobile-oa-start-approval-design.md` +> 菜单种子参考:`docs/sql/mobile_menu_oa_start.sql` + +--- + +## 1. 背景与目标 + +### 1.1 现状 + +- Web:`发起审批` / `草稿箱` / `待办中心` / `已办中心` / `我发起的` / `抄送我的` 已齐;详情办理在 `instances/detail.vue`(同意/拒绝/打回/加签/转交/撤销/复制重开/打印)。 +- 移动:`mobile-h5` 已实现「发起审批」列表;填写经 web-view 打开 `mobile-oa-h5` `/oa-start`(含 `draftId` 草稿再编辑)。 +- `mobile-oa-h5` 尚无实例详情 / 审批办理路由;无五中心列表。 + +### 1.2 目标 + +1. 在小程序中一次性补齐五中心功能(可分步开发),能力以本文「已确认范围」为准,办理动作对齐 Web 详情。 +2. 延续既有架构:**列表原生 `mobile-h5`;动态表单与办理在 `mobile-oa-h5`**。 +3. 新增移动端菜单种子 SQL,挂在「移动端菜单」根下,便于角色授权。 + +### 1.3 非目标 + +- 改造 Web 设计器或 Web 六页交互。 +- 在小程序原生内直挂 `@form-create/vant`。 +- 已在核对中明确剔除的筛选(见 §2)。 +- 审批流设计、应用管理、收款账户等非六中心能力。 + +### 1.4 架构选型(已确认) + +**方案 A**:延续发起审批架构——五列表在 `mobile-h5`;详情/办理扩 `mobile-oa-h5` `/oa-detail`;草稿编辑继续走 `/oa-start?draftId=`。 + +不采用:列表全进 H5;或在原生重做表单运行时。 + +### 1.5 功能对齐 Web,UI 对齐移动端(已确认) + +| 维度 | 权威来源 | 说明 | +|------|----------|------| +| **功能 / 业务语义** | `web-admin` OA 六中心 + `instances/detail` | API 字段、操作条件、状态文案、办理规则与 Web 一致;本文 §2 已裁剪的能力除外 | +| **UI / 交互样式** | `mobile-h5` + `mobile-oa-h5` 既有端到端模式 | **禁止**照搬 Web Element 桌面布局(多列表格筛选栏、桌面弹窗栅格等) | + +移动端实现时优先对照: + +- **列表 / 宫格 / 空态 / Token**:`mobile-h5`(如 `pages/oa/start`、`MenuGrid`、`styles/tokens.scss`、`BizListCard` 等已有列表卡片模式) +- **表单 / 底栏 / 选人 / 流预览**:`mobile-oa-h5`(如 `OaStart.vue`、`oa-form.css`、`tokens.css`、既有 picker / footer 按钮样式) +- **视觉规范**:`docs/miniprogram-ui-spec-sanqiang.md`(三墙色;勿混用 Web `#10b981`) + +原则:**Web 决定「做什么」;移动端决定「长什么样、怎么点」。** + +--- + +## 2. 已确认功能范围(逐页核对) + +### 2.1 草稿箱 + +| 要 | 不要 | +|----|------| +| 列表;分页/下拉刷新;流程名筛选;编辑;列表提交审批;单删;批量删;菜单角标 | 保存时间范围;模板分组筛选 | + +编辑 → `mobile-oa-h5` `/oa-start?draftId=`(已有能力)。 + +### 2.2 待办中心 + +| 要 | 不要 | +|----|------| +| 列表;分页;流程名+申请人筛选;进详情;角标 | 申请时间范围;流程状态筛选 | +| 详情:表单只读+流进度;同意/拒绝/打回/加签/转交 | — | + +### 2.3 已办中心 + +| 要 | 不要 | +|----|------| +| 列表;分页;流程名+申请人;只读详情(表单+流) | 时间/状态筛选;办理操作;角标 | + +### 2.4 我发起的 + +| 要 | 不要 | +|----|------| +| 列表;分页;流程名筛选;查看;打印;复制并重新发起;详情撤销;详情内复制重开 | 申请时间;流程状态筛选 | + +### 2.5 抄送我的 + +| 要 | 不要 | +|----|------| +| 列表;分页;流程名+申请人;阅读状态筛选;未读样式;查看(打开标已读);单条已读;批量已读;打印;未读角标;只读详情 | 申请时间;流程状态筛选;办理操作 | + +--- + +## 3. 架构 + +``` +┌─ mobile-h5 ─────────────────────────────────────────────┐ +│ 首页 MenuGrid ← fetchMyMobileMenus + 角色授权 │ +│ /pages/oa/drafts|pending|done|mine|cc/index (列表) │ +│ /pages/oa/webview(或复用 start/webview)整页 web-view │ +│ 角标:drafts/count、pending/count、cc/unread-count │ +└───────────────────────────┬─────────────────────────────┘ + │ token + query + ▼ +┌─ mobile-oa-h5 ──────────────────────────────────────────┐ +│ /oa-start 发起 + 草稿编辑(已有) │ +│ /oa-detail 新增:mode=view|approve │ +│ GET instances/{id};表单 disabled;流进度 │ +│ approve:同意/拒绝/打回/加签/转交 │ +│ 发起人:撤销 / 复制并重新发起 │ +│ 打印:移植 Web PDF 能力 │ +│ 结束:postMessage → 小程序 navigateBack + 刷新 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3.1 工程边界 + +| `mobile-h5` | `mobile-oa-h5` | +|-------------|----------------| +| 五列表、筛选、批量删/已读、角标、菜单 path | `formSchemaJson` 渲染、流进度、办理弹层、打印 | +| 草稿「编辑」→ 已有 start URL | `/oa-detail?instanceId=&taskId=&mode=&token=` | + +### 3.2 API + +继续复用 `/web/admin/oa-approval/*` + 小程序登录 JWT(与发起审批一致),不新开独立 mini OA 前缀(除非后续权限评审要求拆分)。 + +主要端点: + +- 草稿:`/drafts*`、`/drafts/{id}/submit`、`/drafts/count` +- 任务列表:`/tasks/{pending|done|mine|cc}` +- 详情:`GET /instances/{id}` +- 办理:`POST /tasks/{id}/approve|reject|return|add-sign|transfer` +- 抄送已读:`POST /tasks/{id}/read`、`POST /tasks/cc/read-batch` +- 撤销:`POST /instances/{id}/withdraw` +- 角标:`/tasks/pending/count`、`/tasks/cc/unread-count` + +### 3.3 UI Token 与样式权威 + +三墙品牌绿 `#0a9f6d` 等;**禁止**混用 Web Fresh Mint `#10b981`。 + +列表 / 详情视觉对齐 **已有移动端界面**,而非 Web 列表页截图: + +- `mobile-h5`:发起审批列表、首页宫格、现有 list/card/empty 样式 +- `mobile-oa-h5`:`/oa-start` 填写页结构、底栏按钮、Vant 弹层与自研 picker 交互 + +筛选在移动端宜用顶栏搜索 / 简易筛选条 / ActionSheet,避免 Web 多列 `filter-grid` 桌面形态。 + +--- + +## 4. 移动端菜单种子 + +参考 `docs/sql/mobile_menu_oa_start.sql`:解析「移动端菜单」根(`parent_id='0'` 且名称含「移动端」),幂等 `INSERT`。 + +新增脚本建议路径:`docs/sql/mobile_menu_oa_task_centers.sql`。 + +| menu_name | id | path | perms | order_num | +|-----------|-----|------|-------|-----------| +| 草稿箱 | `MINI000000000000000000000000OA002` | `/pages/oa/drafts/index` | `mini:oa:drafts` | 20 | +| 待办中心 | `MINI000000000000000000000000OA003` | `/pages/oa/pending/index` | `mini:oa:pending` | 30 | +| 已办中心 | `MINI000000000000000000000000OA004` | `/pages/oa/done/index` | `mini:oa:done` | 40 | +| 我发起的 | `MINI000000000000000000000000OA005` | `/pages/oa/mine/index` | `mini:oa:mine` | 50 | +| 抄送我的 | `MINI000000000000000000000000OA006` | `/pages/oa/cc/index` | `mini:oa:cc` | 60 | + +约定:`menu_type='C'`、`is_frame=1`、`visible/status='1'`、`tenant_no='zkxw'`、`NOT EXISTS` 防重复;角色在 Web「角色管理」勾选后,首页 `MenuGrid` 经 `fetchMyMobileMenus()` 展示。 + +发起审批既有:`MINI000000000000000000000000OA001` / `mini:oa:start` / order 10,本脚本不改动。 + +### 4.1 宫格图标资源 + +PNG 放在 `mobile-h5/static/menu-icons/`(颜色已烘焙,宫格 `iconBg: transparent`)。后端 `mini_menu._mobile_menu_icon` 按 path 返回 icon 名,与 `van-icon` 的 `PNG_ICONS` 对齐: + +| 菜单 | 文件 | icon 名 | +|------|------|---------| +| 草稿箱 | `oa-drafts.png` | `oa-drafts` | +| 待办中心 | `oa-pending.png` | `oa-pending` | +| 已办中心 | `oa-done.png` | `oa-done` | +| 我发起的 | `oa-mine.png` | `oa-mine` | +| 抄送我的 | `oa-cc.png` | `oa-cc` | + +--- + +## 5. 小程序列表页 + +### 5.1 路由与 web-view + +- 五页:`pages/oa/{drafts,pending,done,mine,cc}/index` +- 通用 web-view:扩展现有 `pages/oa/start/webview` 或抽 `pages/oa/webview/index`,支持 start 与 detail 两类 URL。 +- `utils/oa-h5.js` 增补 `buildOaDetailUrl({ instanceId, taskId, mode })`;标题仍用 storage 暂存,避免中文进 URL。 + +### 5.2 列表行为摘要 + +| 页 | 筛选 | 行/工具栏 | 打开 H5 | +|----|------|-----------|---------| +| 草稿 | 流程名 | 编辑、提交、删;批量删 | `/oa-start?draftId=` | +| 待办 | 流程名、申请人 | 进入办理 | `/oa-detail?mode=approve&instanceId=&taskId=` | +| 已办 | 流程名、申请人 | 查看 | `mode=view` | +| 我发起 | 流程名 | 查看、打印、复制重开(终态) | view;复制 → start(预填策略见 §6) | +| 抄送 | 流程名、申请人、阅读状态 | 查看、已读、批量已读、打印 | view;打开时/前 mark-read | + +共用:卡片列表、下拉刷新、分页;扩展 `mobile-h5/utils/oa-api.js`。 + +--- + +## 6. H5 `/oa-detail` + +### 6.1 Query + +| 参数 | 说明 | +|------|------| +| `instanceId` | 必填;`GET /instances/{id}` | +| `taskId` | 待办办理时定位任务 | +| `mode` | `view` \| `approve` | +| `token` | 与 `/oa-start` 相同,启动时捕获 | + +### 6.2 结构 + +1. 顶:标题 + 状态 Tag +2. 中:form-create **disabled** + 审批流进度(移动端已有/可对齐的 Flow 组件) +3. 底:操作条(全屏 web-view,操作必须在 H5) + +### 6.3 操作可见性(对齐 Web `instances/detail.vue`) + +| 操作 | 条件 | +|------|------| +| 同意 / 拒绝 | `mode=approve` 且存在当前用户可办 task | +| 打回 | 同上且 `allowReturn !== false` | +| 加签 | 同上且 `allowAddSign !== false`;选人 | +| 转交 | 同上;选人排除本人与本节点已有待办;可选说明 | +| 撤销 | 当前用户为发起人且实例进行中 | +| 复制并重新发起 | 发起人且终态(returned / cancelled / rejected);跳转发起填写并预填 | +| 打印 | 我发起的、抄送场景需要;移植 Web `exportOaApprovalPdf` 思路 | + +成功后:`postMessage({ source: 'zkxw-oa-h5', action: 'done'|'back' })` → 小程序返回并刷新列表/角标。 + +### 6.4 复制并重新发起 + +优先与 Web 一致:携带原实例表单数据进入新发起(`/oa-start` + `appId` + 预填来源)。具体 query/storage 约定在实现计划中与现有 `OaStart.vue` 对齐,避免破坏已有草稿链路。 + +### 6.5 打印风险 + +小程序 web-view 内 PDF 能力受限。S6 单独验收;若真机受限,允许「H5 生成文件 + 明确引导」兜底,**不阻塞** S0–S5。 + +--- + +## 7. 分步交付(S0–S7) + +| 步骤 | 内容 | 验收 | +|------|------|------| +| S0 | 菜单 SQL;`pages.json`;通用 web-view / `buildOaDetailUrl` | 授权后菜单可见;未授权不可见 | +| S1 | 草稿箱全量已确认能力 + 草稿角标 | 编辑/提交/删/批量删可用 | +| S2 | `/oa-detail` 只读;已办 / 我发起 / 抄送进 view | 同实例只读正确;抄送打开已读 | +| S3 | 待办列表 + approve 五动作 + 待办角标 | 办理后待办消失;进度正确 | +| S4 | 我发起的撤销、复制重开(列表+详情) | 终态可复制;进行中可撤销 | +| S5 | 抄送阅读筛选、未读样式、单条/批量已读、未读角标 | 与 Web 语义一致 | +| S6 | 打印(我发起的 / 抄送) | 真机可打开或有明确失败提示 | +| S7 | 角标汇总与返回刷新 | 办理/已读后角标更新 | + +--- + +## 8. 测试要点(摘要) + +- 菜单:执行 SQL 后角色勾选/取消,首页宫格增减正确。 +- 草稿:暂存→草稿箱可见→编辑提交→出现在「我发起的」、草稿消失。 +- 待办:同意/拒绝/打回/加签/转交各至少一条主路径;转交人选人约束正确。 +- 已办/抄送:仅只读,无办理按钮。 +- 我发起:撤销后状态与 Web 一致;复制重开可提交新实例。 +- 抄送:未读角标、批量已读、打开详情自动已读。 +- 色值与 web-view 关闭桥接回归(发起审批不被破坏)。 + +--- + +## 9. 文档与实现衔接 + +- 本文为需求/架构权威;实现计划:`docs/superpowers/plans/2026-08-12-mobile-oa-task-centers.md`(Task 1–13,对应 S0–S7)。 +- 变更若调整范围,先改本文再改计划与代码。 diff --git a/mobile-h5/components/AttachList.vue b/mobile-h5/components/AttachList.vue index dead3d27..18a970d5 100644 --- a/mobile-h5/components/AttachList.vue +++ b/mobile-h5/components/AttachList.vue @@ -6,7 +6,7 @@ 添加附件 支持 PDF / 图片 / Office,可多选上传 - + 查看 - + 处理 diff --git a/mobile-h5/components/MenuGrid.vue b/mobile-h5/components/MenuGrid.vue index 75584818..c45d7f00 100644 --- a/mobile-h5/components/MenuGrid.vue +++ b/mobile-h5/components/MenuGrid.vue @@ -6,8 +6,12 @@ class="menu-card" @click="$emit('select', m)" > - - + + {{ m.title }} @@ -32,10 +36,21 @@ iconFg(m) { return this.colors(m).iconFg || '' }, + isPlainIcon(m) { + const bg = String(this.colors(m).iconBg || '').trim().toLowerCase() + return bg === 'transparent' || bg === 'none' + }, + plainClass(m) { + return this.isPlainIcon(m) ? 'menu-icon--plain' : '' + }, + iconSize(m) { + return this.isPlainIcon(m) ? '56rpx' : '52rpx' + }, iconBoxStyle(m) { const { iconBg, iconFg } = this.colors(m) const style = {} if (iconBg) style.background = iconBg + if (this.isPlainIcon(m)) style.boxShadow = 'none' if (iconFg) style.color = iconFg return style }, diff --git a/mobile-h5/components/OaAppIcon.vue b/mobile-h5/components/OaAppIcon.vue index f579d000..4109cf89 100644 --- a/mobile-h5/components/OaAppIcon.vue +++ b/mobile-h5/components/OaAppIcon.vue @@ -22,7 +22,7 @@ export default { name: 'OaAppIcon', props: { iconUrl: { type: String, default: '' }, - /** sm=32, card=36, md=56 */ + /** xs=28, sm=32, card=36, md=40, lg=48 */ size: { type: String, default: 'sm' } }, computed: { @@ -51,35 +51,50 @@ export default { align-items: center; justify-content: center; } +.oa-app-icon--xs { + width: 28px; + height: 28px; + border-radius: 7px; +} .oa-app-icon--sm { width: 32px; height: 32px; border-radius: 8px; - background: #f1f5f9; } .oa-app-icon--card { width: 36px; height: 36px; border-radius: 8px; - background: #f1f5f9; } .oa-app-icon--md { - width: 56px; - height: 56px; + width: 40px; + height: 40px; + border-radius: 10px; +} +.oa-app-icon--lg { + width: 48px; + height: 48px; border-radius: 12px; - background: #f1f5f9; } .oa-app-icon__glyph { width: 16px; height: 16px; } +.oa-app-icon--xs .oa-app-icon__glyph { + width: 14px; + height: 14px; +} .oa-app-icon--card .oa-app-icon__glyph { width: 18px; height: 18px; } .oa-app-icon--md .oa-app-icon__glyph { - width: 26px; - height: 26px; + width: 20px; + height: 20px; +} +.oa-app-icon--lg .oa-app-icon__glyph { + width: 24px; + height: 24px; } .oa-app-icon__photo { width: 100%; diff --git a/mobile-h5/components/van-icon/van-icon.vue b/mobile-h5/components/van-icon/van-icon.vue index 114ec3b1..a5d34d62 100644 --- a/mobile-h5/components/van-icon/van-icon.vue +++ b/mobile-h5/components/van-icon/van-icon.vue @@ -35,6 +35,8 @@ 'eye-o': '◎', down: '↓', checked: '✓', + search: '⌕', + clear: '×', 'manager-o': '☺', 'clock-o': '◷', description: '📄', @@ -47,7 +49,12 @@ /** 本地位图(小程序 真机可靠);颜色已烘焙在 PNG 内 */ const PNG_ICONS = { - approve: '/static/menu-icons/start-approval.png' + approve: '/static/menu-icons/start-approval.png', + 'oa-drafts': '/static/menu-icons/oa-drafts.png', + 'oa-pending': '/static/menu-icons/oa-pending.png', + 'oa-done': '/static/menu-icons/oa-done.png', + 'oa-mine': '/static/menu-icons/oa-mine.png', + 'oa-cc': '/static/menu-icons/oa-cc.png' } export default { diff --git a/mobile-h5/components/van-tag/van-tag.vue b/mobile-h5/components/van-tag/van-tag.vue index 656c67cc..cbc93023 100644 --- a/mobile-h5/components/van-tag/van-tag.vue +++ b/mobile-h5/components/van-tag/van-tag.vue @@ -57,42 +57,42 @@ } .v-tag.is-primary { - background: #f0fdf4; - color: #15803d; + background: #ecfdf5; + color: #16a34a; } .v-tag.is-success { - background: #f0fdf4; - color: #189b65; + background: #ecfdf5; + color: #10b981; } .v-tag.is-warning { - background: #fff7e8; - color: #cf8a19; + background: #fffbeb; + color: #d97706; } .v-tag.is-danger { - background: #fdecec; - color: #d14343; + background: #fef2f2; + color: #ef4444; } .v-tag.is-plain.is-primary, .v-tag.is-plain.is-success { background: transparent; - border-color: #16a34a; - color: #16a34a; + border-color: #22c55e; + color: #22c55e; } .v-tag.is-plain.is-warning { background: transparent; - border-color: #cf8a19; - color: #cf8a19; + border-color: #d97706; + color: #d97706; } .v-tag.is-plain.is-danger { background: transparent; - border-color: #d14343; - color: #d14343; + border-color: #ef4444; + color: #ef4444; } .v-tag.is-plain.is-default { diff --git a/mobile-h5/pages.json b/mobile-h5/pages.json index af8015d6..f039838f 100644 --- a/mobile-h5/pages.json +++ b/mobile-h5/pages.json @@ -36,24 +36,24 @@ "path": "pages/oa/form-sink/index", "style": { "navigationBarTitleText": "表单水槽测试", - "navigationBarBackgroundColor": "#16a34a", - "navigationBarTextStyle": "white" + "navigationBarBackgroundColor": "#ecfdf5", + "navigationBarTextStyle": "black" } }, { "path": "pages/oa/start/index", "style": { "navigationBarTitleText": "发起审批", - "navigationBarBackgroundColor": "#16a34a", - "navigationBarTextStyle": "white" + "navigationBarBackgroundColor": "#ecfdf5", + "navigationBarTextStyle": "black" } }, { "path": "pages/oa/start/webview", "style": { "navigationBarTitleText": "填写审批", - "navigationBarBackgroundColor": "#16a34a", - "navigationBarTextStyle": "white" + "navigationBarBackgroundColor": "#ecfdf5", + "navigationBarTextStyle": "black" } }, { @@ -86,10 +86,10 @@ } ], "globalStyle": { - "navigationBarTextStyle": "white", + "navigationBarTextStyle": "black", "navigationBarTitleText": "中科兴皖", - "navigationBarBackgroundColor": "#16a34a", - "backgroundColor": "#f5f8f7" + "navigationBarBackgroundColor": "#ecfdf5", + "backgroundColor": "#ffffff" }, "uniIdRouter": {} } diff --git a/mobile-h5/pages/demo/comps.vue b/mobile-h5/pages/demo/comps.vue index 5abf2ab7..fbd283fb 100644 --- a/mobile-h5/pages/demo/comps.vue +++ b/mobile-h5/pages/demo/comps.vue @@ -13,14 +13,14 @@ 按钮 Button - 主按钮 - 描边 + 主按钮 + 描边 次要 危险 状态 Tag 待处理 - 进行中 + 进行中 已完成 异常 @@ -45,7 +45,7 @@ left-icon="volume-o" text="移动端菜单在「菜单管理 → 移动端菜单」下配置,按角色授权后展示。" background="#f0fdf4" - color="#15803d" + color="#16a34a" /> - 加载中 + 加载中 反馈 - + Toast - + Dialog @@ -99,7 +99,7 @@ uni.showModal({ title: '确认提交', content: '提交后将进入审批流程,是否继续?', - confirmColor: '#16a34a' + confirmColor: '#22c55e' }) } } diff --git a/mobile-h5/pages/demo/detail.vue b/mobile-h5/pages/demo/detail.vue index 26321de7..8e8726bf 100644 --- a/mobile-h5/pages/demo/detail.vue +++ b/mobile-h5/pages/demo/detail.vue @@ -48,7 +48,7 @@ 业务进度 - + {{ s }} @@ -94,7 +94,7 @@ 返回 - + 编辑处理 diff --git a/mobile-h5/pages/demo/form.vue b/mobile-h5/pages/demo/form.vue index 5e335ff5..10ff4fac 100644 --- a/mobile-h5/pages/demo/form.vue +++ b/mobile-h5/pages/demo/form.vue @@ -32,7 +32,7 @@ /> @@ -62,7 +62,7 @@ 取消 - + 保存 diff --git a/mobile-h5/pages/demo/list.vue b/mobile-h5/pages/demo/list.vue index a9bca192..00d296b8 100644 --- a/mobile-h5/pages/demo/list.vue +++ b/mobile-h5/pages/demo/list.vue @@ -26,6 +26,7 @@ shape="round" background="transparent" placeholder="搜索单号 / 名称 / 负责人" + @update:model-value="onKeywordInput" /> @@ -79,6 +80,9 @@ }, methods: { tabCount, + onKeywordInput(v) { + this.keyword = v == null ? '' : String(v) + }, goHome() { uni.reLaunch({ url: '/pages/index/index' }) }, diff --git a/mobile-h5/pages/index/index.vue b/mobile-h5/pages/index/index.vue index 6e37f016..bbf0cdb1 100644 --- a/mobile-h5/pages/index/index.vue +++ b/mobile-h5/pages/index/index.vue @@ -22,7 +22,7 @@ :circular="banners.length > 1" :indicator-dots="banners.length > 1" indicator-color="rgba(255,255,255,0.45)" - indicator-active-color="#16a34a" + indicator-active-color="#22c55e" :autoplay="false" > @@ -86,8 +86,7 @@ title: '发起审批', icon: 'approve', path: '/pages/oa/start/index', - iconBg: '#ffe4e6', - iconFg: '#e11d48' + iconBg: 'transparent' }, { id: 'oa-form-sink', diff --git a/mobile-h5/pages/oa/start/index.vue b/mobile-h5/pages/oa/start/index.vue index b7b036d9..a6e82e68 100644 --- a/mobile-h5/pages/oa/start/index.vue +++ b/mobile-h5/pages/oa/start/index.vue @@ -1,35 +1,49 @@ - diff --git a/mobile-oa-h5/src/oa/components/OaFlowPreview.vue b/mobile-oa-h5/src/oa/components/OaFlowPreview.vue index 9452c98f..4f0e918a 100644 --- a/mobile-oa-h5/src/oa/components/OaFlowPreview.vue +++ b/mobile-oa-h5/src/oa/components/OaFlowPreview.vue @@ -83,24 +83,33 @@ shape="round" label-align="left" input-align="left" - placeholder="搜索姓名或手机号" + placeholder="搜索姓名、工号或手机号" />
- - - - + +
+ {{ shortName(u.nickName || u.id) }} +
+
+
{{ u.nickName || u.id }}
+
{{ pickerSub(u) }}
+
+
@@ -113,6 +122,8 @@ import { computed, ref } from 'vue' import OaPulseLoading from './OaPulseLoading.vue' +const AVATAR_COLORS = ['#3b82f6', '#22c55e', '#8b5cf6', '#f59e0b', '#06b6d4', '#ef4444'] + const props = defineProps({ steps: { type: Array, default: () => [] }, resolved: { type: Boolean, default: true }, @@ -141,12 +152,37 @@ const filteredUsers = computed(() => { String(u.phoneNo || '') .toLowerCase() .includes(q) || + String(u.userNo || '') + .toLowerCase() + .includes(q) || String(u.id || '') .toLowerCase() .includes(q) ) }) +function pickerSub(u) { + const phone = String(u?.phoneNo || '').trim() + if (phone) return phone + const userNo = String(u?.userNo || '').trim() + const name = String(u?.nickName || '').trim() + if (userNo && userNo !== name) return userNo + return '' +} + +function shortName(name) { + const s = String(name || '').trim() + if (!s) return '?' + return s.length <= 2 ? s : s.slice(-2) +} + +function avatarColor(name) { + const s = String(name || '') + let h = 0 + for (let i = 0; i < s.length; i++) h = (h + s.charCodeAt(i) * 17) % AVATAR_COLORS.length + return AVATAR_COLORS[h] +} + function isInitiatorSelect(s) { const types = Array.isArray(s.assigneeTypes) && s.assigneeTypes.length @@ -226,19 +262,19 @@ function confirmPicker() { padding: 14px 12px 8px; background: #fff; border-radius: 12px; - border: 1px solid var(--border, #dcebe4); + border: 1px solid var(--border, #d8f3e5); } .oa-flow-preview__title { margin-bottom: 12px; font-size: 15px; font-weight: 700; - color: var(--text-primary, #1f2a26); + color: var(--text-primary, #0f291e); } .oa-flow-preview__loading, .oa-flow-preview__pending, .oa-flow-preview__empty { padding: 12px 4px 16px; - color: var(--text-muted, #6b7b74); + color: var(--text-muted, #5f7a6d); font-size: 13px; line-height: 1.5; } @@ -262,7 +298,7 @@ function confirmPicker() { height: 10px; margin-top: 6px; border-radius: 50%; - background: var(--brand-green, #16a34a); + background: var(--brand-green, #22c55e); } .oa-flow-dot.end { background: #94a3b8; @@ -280,44 +316,52 @@ function confirmPicker() { } .oa-flow-card__name-row { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; gap: 6px; align-items: center; + min-width: 0; } .oa-flow-card__name { + flex-shrink: 0; font-size: 14px; font-weight: 600; - color: var(--text-primary, #1f2a26); + color: var(--text-primary, #0f291e); } .oa-flow-badge { + flex-shrink: 0; padding: 1px 6px; border-radius: 4px; font-size: 11px; } .oa-flow-badge.and { - color: #15803d; - background: #f0fdf4; + color: #22c55e; + background: #ecfdf5; } .oa-flow-badge.or { color: #b45309; background: #fff7e8; } .oa-flow-card__meta { - color: var(--text-muted, #6b7b74); + min-width: 0; + overflow: hidden; + color: var(--text-muted, #5f7a6d); font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; } .oa-flow-card__sep { + flex-shrink: 0; color: #cbd5e1; font-size: 12px; } .oa-flow-card__placeholder { - color: var(--text-muted, #6b7b74); + color: var(--text-muted, #5f7a6d); font-size: 12px; line-height: 1.2; } .oa-flow-card__err { margin-top: 4px; - color: #d14343; + color: #ef4444; font-size: 12px; } .oa-flow-card__select { @@ -347,9 +391,9 @@ function confirmPicker() { align-items: center; padding: 4px 8px; border-radius: 999px; - background: #f5f8f7; + background: #ffffff; font-size: 12px; - color: var(--text-primary, #1f2a26); + color: var(--text-primary, #0f291e); } .oa-person-chip__dot { display: inline-flex; @@ -358,7 +402,7 @@ function confirmPicker() { width: 18px; height: 18px; border-radius: 50%; - background: var(--brand-green, #16a34a); + background: var(--brand-green, #22c55e); color: #fff; font-size: 11px; } @@ -382,7 +426,7 @@ function confirmPicker() { border: 1px solid #e2e8f0; border-radius: 6px; background: #fff; - color: var(--text-primary, #1f2a26); + color: var(--text-primary, #0f291e); font-size: 14px; line-height: 1; } @@ -397,32 +441,16 @@ function confirmPicker() { align-items: center; justify-content: space-between; padding: 14px 16px; - border-bottom: 1px solid var(--border, #dcebe4); + border-bottom: 1px solid var(--border, #d8f3e5); } .oa-flow-picker__ok { - color: var(--brand-green, #16a34a); + color: var(--brand-green, #22c55e); font-weight: 600; } .oa-flow-picker__list { flex: 1; - padding: 12px; + padding: 0; overflow: auto; -} -.oa-flow-picker__list .van-cell { - margin-bottom: 10px; - padding: 12px 14px; - border-radius: 10px; -} -.oa-flow-picker__check { - margin-right: 8px; -} -.oa-flow-picker__name { - font-size: 14px; - color: var(--text-primary, #1f2a26); -} -.oa-flow-picker__phone { - margin-left: 8px; - font-size: 13px; - color: var(--text-muted, #6b7b74); + background: #fff; } diff --git a/mobile-oa-h5/src/oa/components/OaOwnerDeptSelect.vue b/mobile-oa-h5/src/oa/components/OaOwnerDeptSelect.vue index a72447a1..10783cd6 100644 --- a/mobile-oa-h5/src/oa/components/OaOwnerDeptSelect.vue +++ b/mobile-oa-h5/src/oa/components/OaOwnerDeptSelect.vue @@ -1,12 +1,14 @@ diff --git a/mobile-oa-h5/src/oa/components/OaTripItinerary.vue b/mobile-oa-h5/src/oa/components/OaTripItinerary.vue index e953f093..7a39c760 100644 --- a/mobile-oa-h5/src/oa/components/OaTripItinerary.vue +++ b/mobile-oa-h5/src/oa/components/OaTripItinerary.vue @@ -1,8 +1,9 @@ + diff --git a/mobile-oa-h5/src/oa/kit-runtime.js b/mobile-oa-h5/src/oa/kit-runtime.js index edfc0589..5975fff7 100644 --- a/mobile-oa-h5/src/oa/kit-runtime.js +++ b/mobile-oa-h5/src/oa/kit-runtime.js @@ -25,6 +25,7 @@ export { export { applyConfirmationKitDateLinkage, applyConfirmationKitDefaults, + clearConfirmationDateManualAndRelink, ensureConfirmationKitVisibility, listConfirmationKitBindings, listManualConfirmationDateFields, diff --git a/mobile-oa-h5/src/oa/kit/oa-leave-hours.js b/mobile-oa-h5/src/oa/kit/oa-leave-hours.js index beeee4c1..8c0c0891 100644 --- a/mobile-oa-h5/src/oa/kit/oa-leave-hours.js +++ b/mobile-oa-h5/src/oa/kit/oa-leave-hours.js @@ -4,7 +4,7 @@ const DEFAULT_LEAVE_WORK_HOURS = { afternoonStart: "13:00", afternoonEnd: "17:30" }; -const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/; +const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::\d{2})?$/; function parseHm(hm) { const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/); if (!m) return null; diff --git a/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js b/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js index 7395efa8..fb7189e9 100644 --- a/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js +++ b/mobile-oa-h5/src/oa/kit/oa-overtime-hours.js @@ -4,7 +4,7 @@ const DEFAULT_OVERTIME_WINDOWS = { restBreakStart: "11:30", restBreakEnd: "13:30" }; -const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/; +const DT_RE = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::\d{2})?$/; function parseHm(hm) { const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/); if (!m) return null; diff --git a/mobile-oa-h5/src/oa/kitchen-sink-rule.js b/mobile-oa-h5/src/oa/kitchen-sink-rule.js index 15da18ab..60e7b0ff 100644 --- a/mobile-oa-h5/src/oa/kitchen-sink-rule.js +++ b/mobile-oa-h5/src/oa/kitchen-sink-rule.js @@ -44,10 +44,32 @@ export const kitchenSinkRule = [ { type: 'datePicker', field: 'bizDate', - title: '日期', + title: '日期(年月日)', value: '', props: { type: 'date' } }, + { + type: 'datePicker', + field: 'bizDateTime', + title: '日期时间(年月日时分秒)', + value: '', + props: { + type: 'datetime', + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss' + } + }, + { + type: 'datePicker', + field: 'bizDateTimeMinute', + title: '日期时间(到分)', + value: '', + props: { + type: 'datetime', + format: 'YYYY-MM-DD HH:mm', + valueFormat: 'YYYY-MM-DD HH:mm' + } + }, { type: 'upload', field: 'files', title: '附件 upload', value: [] }, { type: 'money', field: 'amount', title: '金额 money', value: '' }, { @@ -120,14 +142,14 @@ export const kitchenSinkRule = [ field: 'leaveStart', title: '开始时间', $required: true, - props: { type: 'datetime' } + props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' } }, { type: 'datePicker', field: 'leaveEnd', title: '结束时间', $required: true, - props: { type: 'datetime' } + props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' } }, { type: 'inputNumber', @@ -161,14 +183,14 @@ export const kitchenSinkRule = [ field: 'otStart', title: '开始时间', $required: true, - props: { type: 'datetime' } + props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' } }, { type: 'datePicker', field: 'otEnd', title: '结束时间', $required: true, - props: { type: 'datetime' } + props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' } }, { type: 'inputNumber', diff --git a/mobile-oa-h5/src/oa/register-components.js b/mobile-oa-h5/src/oa/register-components.js index b962f4d2..b052c9d9 100644 --- a/mobile-oa-h5/src/oa/register-components.js +++ b/mobile-oa-h5/src/oa/register-components.js @@ -10,6 +10,7 @@ import OaOwnerPersonSelect from './components/OaOwnerPersonSelect.vue' import OaOwnerDeptSelect from './components/OaOwnerDeptSelect.vue' import OaKitShell from './components/OaKitShell.vue' import OaTripItinerary from './components/OaTripItinerary.vue' +import OaDatePicker from './components/OaDatePicker.vue' /** 与 web-admin type 名对齐 */ const REAL_COMPONENTS = { @@ -37,4 +38,7 @@ export function registerOaComponents(formCreate) { formCreate.component(name, comp) } formCreate.component('fcUpload', OaUploadField) + // 覆盖 @form-create/vant 内置日期:按 props.type 区分 date / datetime + formCreate.component('datePicker', OaDatePicker) + formCreate.component('fcDatePicker', OaDatePicker) } diff --git a/mobile-oa-h5/src/styles/oa-form.css b/mobile-oa-h5/src/styles/oa-form.css index cd850837..3fe95d31 100644 --- a/mobile-oa-h5/src/styles/oa-form.css +++ b/mobile-oa-h5/src/styles/oa-form.css @@ -1,24 +1,33 @@ /* form-create + Vant:上下布局与左对齐兜底 */ .form-create-m .van-field__label--top { width: 100%; - margin-bottom: 6px; + margin-bottom: 4px; text-align: left; - color: var(--text-primary, #1f2a26); - font-size: 13px; + color: var(--text-primary, #0f291e); + font-size: 12px; font-weight: 600; } .form-create-m .van-field__control { text-align: left !important; + font-size: 13px; + min-height: 22px; + line-height: 22px; } .form-create-m .van-field__control::placeholder { text-align: left; - color: var(--text-muted, #6b7b74); + color: var(--text-muted, #5f7a6d); } .form-create-m .fc-form-item { - margin-bottom: 4px; + margin-bottom: 2px; +} + +/* 表单项整体更紧凑 */ +.form-create-m .fc-form-item.van-cell { + padding-top: 8px; + padding-bottom: 8px; } /* 自定义控件 / 内置选择触发条:占满整行,箭头贴右 */ @@ -28,6 +37,7 @@ .form-create-m ._fc-calendar, .form-create-m ._fc-cascader, .form-create-m .oa-pick, +.form-create-m .oa-owner-field, .form-create-m .oa-money, .form-create-m .oa-number, .form-create-m .oa-recv, @@ -58,6 +68,40 @@ align-items: center; width: 100%; box-sizing: border-box; + min-height: 32px; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 0; + padding-right: 0; +} + +.form-create-m ._fc-select > .van-cell .van-field__control, +.form-create-m ._fc-date-picker > .van-cell .van-field__control, +.form-create-m ._fc-time-picker > .van-cell .van-field__control, +.form-create-m ._fc-calendar > .van-cell .van-field__control, +.form-create-m ._fc-cascader > .van-cell .van-field__control, +.form-create-m .oa-pick > .van-cell .van-field__control, +.form-create-m .oa-money > .van-cell .van-field__control, +.form-create-m .oa-number > .van-cell .van-field__control, +.form-create-m .oa-recv > .van-cell .van-field__control { + font-size: 13px; + min-height: 22px; + line-height: 22px; +} + +/* + * 归属人 / 归属部门:不可改选时仍用 disabled 防点开,但展示色用正文色(勿灰字难读)。 + * Vant disabled 会设 -webkit-text-fill-color,须一并覆盖。 + */ +.form-create-m .oa-owner-field.is-locked .van-field__control, +.form-create-m .oa-owner-field.is-locked .van-field__control:disabled { + color: var(--text-primary, #0f291e) !important; + -webkit-text-fill-color: var(--text-primary, #0f291e) !important; + opacity: 1; +} + +.form-create-m .oa-owner-field.is-locked .van-cell__right-icon { + color: #cbd5e1; } .form-create-m ._fc-select > .van-cell > .van-cell__title, @@ -94,8 +138,9 @@ .form-create-m .oa-pick > .van-cell > .van-cell__right-icon, .form-create-m .oa-recv > .van-cell > .van-cell__right-icon { flex: 0 0 auto; - margin-left: 8px; + margin-left: 6px; order: 3; + font-size: 14px; } /* 行程内带自身 label 的 Field:标题在上,箭头仍在内容行右侧 */ @@ -104,22 +149,77 @@ flex-wrap: wrap; align-items: center; width: 100%; + box-sizing: border-box; + background: transparent; + padding-top: 0; + padding-bottom: 0; + padding-left: 0; + padding-right: 0; +} + +.form-create-m .oa-trip .van-field--label-top::after { + display: none; } .form-create-m .oa-trip .van-field--label-top > .van-cell__title { flex: 0 0 100%; width: 100%; + max-width: 100%; + margin-bottom: 4px; } +/* + * 内容行:value 吃剩余宽度,right-icon 固定同行右侧。 + * 窄列(开始/结束时段)也勿让 value 占满导致箭头掉到下一行。 + * value 与箭头强制同高并垂直居中,避免选中后文字相对箭头偏上/偏下。 + */ .form-create-m .oa-trip .van-field--label-top > .van-cell__value { - flex: 1 1 auto; + flex: 1 1 0% !important; + width: 0 !important; + max-width: none !important; min-width: 0; + align-self: center; + display: flex; + align-items: center; text-align: left; } +.form-create-m .oa-trip .van-field--label-top .van-field__body { + display: flex; + align-items: center; + width: 100%; + min-height: 22px; +} + +.form-create-m .oa-trip .van-field--label-top .van-field__control { + height: 22px; + min-height: 22px; + line-height: 22px; + padding: 0; +} + .form-create-m .oa-trip .van-field--label-top > .van-cell__right-icon { flex: 0 0 auto; - margin-left: 8px; + align-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 4px; + order: 3; + height: 22px; + font-size: 14px; + line-height: 22px; +} + +/* 日期+时段同行:列间距,不再额外塞左右 padding(由卡片内边距统一) */ +.form-create-m .oa-trip .oa-trip__row .van-field { + padding-left: 0; + padding-right: 0; +} + +.form-create-m .oa-trip .oa-trip__row .oa-trip__field--half.van-field { + padding-left: 0; + padding-right: 0; } /* 外层表单项:标题独占一行;内容区整行左对齐(箭头在内层控件右侧) */ @@ -130,7 +230,7 @@ .form-create-m .fc-form-item.van-field--label-top > .van-cell__title { flex: 0 0 100%; width: 100%; - margin-bottom: 6px; + margin-bottom: 4px; } .form-create-m .fc-form-item.van-field--label-top > .van-cell__value { @@ -156,7 +256,7 @@ align-items: center; justify-content: space-between; padding: 14px 16px; - border-bottom: 1px solid var(--border, #dcebe4); + border-bottom: 1px solid var(--border, #d8f3e5); } .oa-pick__hd strong, @@ -164,14 +264,14 @@ .oa-rel__hd strong { font-size: 16px; font-weight: 700; - color: var(--text-primary, #1f2a26); + color: var(--text-primary, #0f291e); } .oa-pick__hd > span:first-child, .oa-recv__hd > span:first-child, .oa-rel__hd > span:first-child { min-width: 48px; - color: var(--text-muted, #6b7b74); + color: var(--text-muted, #5f7a6d); font-size: 14px; } @@ -179,81 +279,152 @@ .oa-recv__ok, .oa-rel__ok { min-width: 48px; - color: var(--brand-green, #16a34a); + color: var(--brand-green, #22c55e); font-size: 14px; font-weight: 600; text-align: right; } +/* 方案 C · 分割线行表 */ .oa-pick__list, .oa-recv__list, .oa-rel__picker { flex: 1; - padding: 12px; + padding: 0; overflow: auto; - background: transparent; + background: #fff; box-sizing: border-box; } -.oa-pick__list .van-cell, -.oa-recv__list .van-cell, -.oa-rel__picker .van-cell { - align-items: center; - margin-bottom: 10px; - padding: 12px 14px; - text-align: left; - background: transparent; - border-radius: 10px; +/* + * 弹窗列表行(对齐关联审批单 oa-rel__pick-row) + * 人员 / 部门 / 收款账户 / 关联审批 共用,避免 van-cell 被表单样式污染 + */ +.oa-sheet-row { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 12px 16px; + background: #fff; + border-bottom: 1px solid #f1f5f9; + box-sizing: border-box; } -.oa-pick__list .van-cell:last-child, -.oa-recv__list .van-cell:last-child, -.oa-rel__picker .van-cell:last-child { +.oa-sheet-row:last-child { + border-bottom: none; +} + +.oa-sheet-row.is-selected { + background: var(--brand-green-soft, #ecfdf5); +} + +.oa-sheet-row.is-selected .oa-sheet-row__title { + color: #15803d; +} + +.oa-sheet-row--center { + align-items: center; +} + +.oa-sheet-row__check { + flex-shrink: 0; + margin-top: 10px; + margin-right: 0; +} + +.oa-sheet-row--center .oa-sheet-row__check { + margin-top: 0; +} + +.oa-sheet-row__avatar { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + margin-top: 2px; + border-radius: 8px; + color: #fff; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.3px; +} + +.oa-sheet-row__main { + flex: 1; + min-width: 0; +} + +.oa-sheet-row__title { + margin: 0 0 4px; + color: var(--text-primary, #0f291e); + font-size: 14px; + font-weight: 600; + line-height: 1.4; + word-break: break-all; +} + +.oa-sheet-row__title:last-child { margin-bottom: 0; } -.oa-pick__list .van-cell::after, -.oa-recv__list .van-cell::after, -.oa-rel__picker .van-cell::after { - display: none; +.oa-sheet-row__meta { + overflow: hidden; + color: var(--text-muted, #5f7a6d); + font-size: 12px; + line-height: 1.45; + text-overflow: ellipsis; + white-space: nowrap; } -.oa-pick__list .van-cell__title, -.oa-recv__list .van-cell__title, -.oa-rel__picker .van-cell__title { - text-align: left; +.oa-sheet-row__exp { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: 0; + background: transparent; + color: var(--text-muted, #5f7a6d); + font-size: 12px; + line-height: 1; } -.oa-pick__list .van-cell__label, -.oa-recv__list .van-cell__label, -.oa-rel__picker .van-cell__label { - text-align: left; - color: var(--text-muted, #6b7b74); +.oa-sheet-row__exp--spacer { + visibility: hidden; + pointer-events: none; } .oa-pick__check, .oa-recv__check, .oa-rel__check { - margin-right: 8px; + margin-right: 10px; } /* 弹窗检索:勿继承表单 labelAlign=top,保持单行垂直居中 */ .oa-pick__sheet .van-search, .oa-recv__sheet .van-search, -.oa-rel__sheet .van-search { +.oa-rel__sheet .van-search, +.oa-flow-picker .van-search { padding: 10px 12px; background: #fff; } .oa-pick__sheet .van-search .van-search__content, .oa-recv__sheet .van-search .van-search__content, -.oa-rel__sheet .van-search .van-search__content { +.oa-rel__sheet .van-search .van-search__content, +.oa-flow-picker .van-search .van-search__content { align-items: center; + padding-right: 4px; } .oa-pick__sheet .van-search .van-cell, .oa-recv__sheet .van-search .van-cell, -.oa-rel__sheet .van-search .van-cell { +.oa-rel__sheet .van-search .van-cell, +.oa-flow-picker .van-search .van-cell { align-items: center; padding-top: 0; padding-bottom: 0; @@ -262,14 +433,28 @@ .oa-pick__sheet .van-search .van-field__body, .oa-recv__sheet .van-search .van-field__body, -.oa-rel__sheet .van-search .van-field__body { +.oa-rel__sheet .van-search .van-field__body, +.oa-flow-picker .van-search .van-field__body { align-items: center; } .oa-pick__sheet .van-search .van-field__control, .oa-recv__sheet .van-search .van-field__control, -.oa-rel__sheet .van-search .van-field__control { +.oa-rel__sheet .van-search .van-field__control, +.oa-flow-picker .van-search .van-field__control { height: 36px; line-height: 36px; text-align: left !important; } + +/* + * Vant 默认 .van-field__clear { margin-right: calc(var(--van-padding-xs) * -1) } + * 会把清除图标贴到搜索框右缘;对齐发起审批页检索框内边距(约 12px)。 + */ +.oa-pick__sheet .van-search .van-field__clear, +.oa-recv__sheet .van-search .van-field__clear, +.oa-rel__sheet .van-search .van-field__clear, +.oa-flow-picker .van-search .van-field__clear { + margin-right: 4px; + padding-right: 8px; +} diff --git a/mobile-oa-h5/src/styles/tokens.css b/mobile-oa-h5/src/styles/tokens.css index 05dc394c..2431ad3d 100644 --- a/mobile-oa-h5/src/styles/tokens.css +++ b/mobile-oa-h5/src/styles/tokens.css @@ -1,11 +1,20 @@ :root { - --brand-green: #16a34a; - --brand-green-dark: #15803d; - --brand-green-soft: #f0fdf4; - --bg-page: #f5f8f7; - --text-primary: #1f2a26; - --text-muted: #6b7b74; - --border: #dcebe4; + /* 方案 A · 薄荷微风 — 与 mobile-h5 对齐 */ + --brand-green: #22c55e; + --brand-green-dark: #16a34a; + --brand-green-soft: #ecfdf5; + --header-fg: #065f46; + --bg-page: #ffffff; + --surface: #ffffff; + --text-primary: #0f291e; + --text-muted: #5f7a6d; + --border: #d8f3e5; + --danger: #ef4444; + --warning: #d97706; + --success: #10b981; + --barley-yellow: #fbbf24; + --barley-yellow-soft: #fffbeb; + --shadow-soft: 0 4px 16px rgba(22, 163, 74, 0.07); } body { margin: 0; diff --git a/mobile-oa-h5/src/views/FormSink.vue b/mobile-oa-h5/src/views/FormSink.vue index ae1912b5..f3f6b418 100644 --- a/mobile-oa-h5/src/views/FormSink.vue +++ b/mobile-oa-h5/src/views/FormSink.vue @@ -47,8 +47,8 @@ async function onValidate() { .sink__hd { padding: 14px 16px; font-weight: 700; - color: #fff; - background: linear-gradient(135deg, var(--brand-green), var(--brand-green-dark)); + color: var(--header-fg, #065f46); + background: linear-gradient(160deg, #f0fdf4 0%, #d1fae5 58%, #a7f3d0 100%); } .sink__body { flex: 1; diff --git a/mobile-oa-h5/src/views/OaStart.vue b/mobile-oa-h5/src/views/OaStart.vue index cb4e4578..24adf5c5 100644 --- a/mobile-oa-h5/src/views/OaStart.vue +++ b/mobile-oa-h5/src/views/OaStart.vue @@ -10,6 +10,26 @@ :rule="formRule" :option="formOption" /> +

+ +

+

+ +

+

+ +

+

+ +

+ + +
+

选择偏好方案(可点击下方卡片,也可在上方 Tab 预览):

+
+
A
+

薄荷微风(推荐)

浅绿顶栏 + 品牌绿,最稳妥

+
+
+
B
+

晴空青

天蓝顶栏,更透气

+
+
+
C
+

青瓷医疗

teal 专业、干净

+
+
+
D
+

暖晨光

暖米 + 嫩绿,亲和

+
+
+ + diff --git a/mobile-ui/zkxw-mobile-control-style-demos.html b/mobile-ui/zkxw-mobile-control-style-demos.html new file mode 100644 index 00000000..f34aff66 --- /dev/null +++ b/mobile-ui/zkxw-mobile-control-style-demos.html @@ -0,0 +1,841 @@ + + + + + + mobile-h5 · 控件样式 Demo(输入 / 下拉 / Tag / Tab) + + + +
+

控件样式 Demo · 输入 / 下拉 / Tag / Tab

+

+ 针对「发起审批」顶部检索与分类,以及表单里常见的下拉选择。以下 4 套均为中性清爽风格,主色只作轻量点缀(或完全不用)。 + 点选方案后,右侧手机预览会同步切换。 +

+
+ 文件路径:mobile-ui/zkxw-mobile-control-style-demos.html + · 本地直接打开即可。选定后告诉我方案代号(A/B/C/D),再落到工程。 +
+ +
+ +
+ + +
+
+
+
发起审批
+
+
+
+
假
+
+
请假申请
+
人事 · 通用
+
+
发起
+
+
+
差
+
+
出差申请
+
行政
+
+
发起
+
+
+
报
+
+
费用报销
+
财务
+
+
发起
+
+
+
+
+
+
+ +
+
+ + + + diff --git a/web-admin/graphify-out/GRAPH_REPORT.md b/web-admin/graphify-out/GRAPH_REPORT.md index 80e174de..65ec1fdb 100644 --- a/web-admin/graphify-out/GRAPH_REPORT.md +++ b/web-admin/graphify-out/GRAPH_REPORT.md @@ -1,7 +1,7 @@ -# Graph Report - src (2026-08-11) +# Graph Report - src (2026-08-12) ## Corpus Check -- Large corpus: 967 files · ~631,782 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only. +- Large corpus: 967 files · ~631,821 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only. ## Summary - 3417 nodes · 4865 edges · 71 communities detected diff --git a/web-admin/graphify-out/manifest.json b/web-admin/graphify-out/manifest.json index 4c281e08..cbd240f9 100644 --- a/web-admin/graphify-out/manifest.json +++ b/web-admin/graphify-out/manifest.json @@ -17,8 +17,8 @@ "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1781272556.3597305, "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1781272556.3601494, "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1781272556.3606477, - "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1786416949.0462604, - "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1786416928.7957375, + "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1786506822.9277327, + "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1786506719.2191331, "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1781272556.364766, "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1782394569.8806834, "/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1783921717.4015942, @@ -329,7 +329,7 @@ "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaOvertimeKit.vue": 1786374594.648936, "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaApprovalPrintSheet.vue": 1786413858.4258535, "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue": 1785391747.3254373, - "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaAppIcon.vue": 1785993067.5623589, + "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaAppIcon.vue": 1786504502.4002652, "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaLeaveKit.vue": 1785398262.9313874, "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaPrintRelatedTree.vue": 1786413860.094557, "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaFcRow.vue": 1785145511.6907504, diff --git a/web-admin/src/components/oa-approval/OaAppIcon.vue b/web-admin/src/components/oa-approval/OaAppIcon.vue index 59ebaba4..915e3b9a 100644 --- a/web-admin/src/components/oa-approval/OaAppIcon.vue +++ b/web-admin/src/components/oa-approval/OaAppIcon.vue @@ -77,7 +77,7 @@ if (resolved.value.kind === 'ep') { return { background: resolved.value.bg, color: resolved.value.fg } } - return { background: '#f1f5f9' } + return {} }) diff --git a/web-admin/src/components/oa-approval/OaStartFormDialog.vue b/web-admin/src/components/oa-approval/OaStartFormDialog.vue index 4a4ab5fa..2c514fd0 100644 --- a/web-admin/src/components/oa-approval/OaStartFormDialog.vue +++ b/web-admin/src/components/oa-approval/OaStartFormDialog.vue @@ -25,13 +25,25 @@ 恢复转正日期自动计算

+

+ + + 恢复请假时长自动计算 + +

恢复加班时长自动计算

-

+

+ + + 恢复出差天数自动计算 + +

+

该模板暂无表单字段,仍可直接提交。

{ + const fields: string[] = [] + for (const kit of listLeaveKitBindings({ rule: formRule.value })) { + if (formData.value[LEAVE_MANUAL_PREFIX + kit.durationField]) { + fields.push(kit.durationField) + } + } + return fields + }) + const overtimeDurationManualFields = computed(() => { const fields: string[] = [] for (const kit of listOvertimeKitBindings({ rule: formRule.value })) { @@ -175,6 +197,16 @@ return fields }) + const tripDaysManualFields = computed(() => { + const fields: string[] = [] + for (const kit of listTripKits({ rule: formRule.value })) { + if (formData.value[TRIP_MANUAL_PREFIX + kit.daysField]) { + fields.push(kit.daysField) + } + } + return fields + }) + function flowNeedsInitiatorSelect(node: any): boolean { if (!node || typeof node !== 'object') return false const cfg = node.approverConfig || {} @@ -685,6 +717,24 @@ void syncOvertimeKitDurations() } + function restoreLeaveAuto() { + let data = { ...formData.value } + for (const kit of listLeaveKitBindings({ rule: formRule.value })) { + data = markLeaveDurationManual(data, kit.durationField, false) + } + formData.value = data + syncLeaveKitDurations() + } + + function restoreTripDaysAuto() { + let data = { ...formData.value } + for (const kit of listTripKits({ rule: formRule.value })) { + delete data[TRIP_MANUAL_PREFIX + kit.daysField] + } + formData.value = data + syncTripKitDurations() + } + watch( formData, () => { diff --git a/待同步到生产的sql脚本 b/待同步到生产的sql脚本 index f49c6b72..e69de29b 100644 --- a/待同步到生产的sql脚本 +++ b/待同步到生产的sql脚本 @@ -1,27 +0,0 @@ --- 中国节假日日历缓存 --- MySQL 5.7 / utf8mb4 - -CREATE TABLE IF NOT EXISTS `sys_cn_holiday_day` ( - `id` VARCHAR(64) NOT NULL COMMENT 'UUID', - `day_date` DATE NOT NULL COMMENT '日历日', - `day_type` TINYINT NOT NULL COMMENT '0工作日1周末2节日3调休放假4补班', - `day_name` VARCHAR(64) DEFAULT NULL COMMENT '名称', - `week` TINYINT DEFAULT NULL COMMENT '1-7周一到周日', - `is_rest_day` TINYINT NOT NULL DEFAULT 0 COMMENT '1休息0工作', - `raw_json` TEXT COMMENT '原始JSON片段', - `sync_time` DATETIME DEFAULT NULL COMMENT '同步时间', - `create_time` DATETIME DEFAULT NULL, - `update_time` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_day_date` (`day_date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='中国节假日日历缓存'; - - --- 员工微信小程序 openid 绑定字段(微信授权登录) --- MySQL 5.7:唯一索引允许多个 NULL - -ALTER TABLE `sys_user` - ADD COLUMN `wx_openid` varchar(64) DEFAULT NULL COMMENT '微信小程序 openid' AFTER `phone_no`; - -ALTER TABLE `sys_user` - ADD UNIQUE INDEX `uk_sys_user_wx_openid` (`wx_openid`);