提交
No files matched your search
@@ -93,6 +93,16 @@ def _mobile_menu_icon(path: str) -> str:
|
|||||||
p = (path or "").lower()
|
p = (path or "").lower()
|
||||||
if "oa/start" in p:
|
if "oa/start" in p:
|
||||||
return "approve"
|
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:
|
if "form-sink" in p:
|
||||||
return "orders-o"
|
return "orders-o"
|
||||||
return "apps-o"
|
return "apps-o"
|
||||||
@@ -101,8 +111,19 @@ def _mobile_menu_icon(path: str) -> str:
|
|||||||
def _mobile_menu_icon_colors(path: str) -> dict:
|
def _mobile_menu_icon_colors(path: str) -> dict:
|
||||||
"""宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
|
"""宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
|
||||||
p = (path or "").lower()
|
p = (path or "").lower()
|
||||||
if "oa/start" in p:
|
# OA 菜单 PNG 自带颜色,宫格不套色底
|
||||||
return {"iconBg": "#ffe4e6", "iconFg": "#e11d48"}
|
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:
|
if "form-sink" in p:
|
||||||
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
||||||
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Settings(BaseSettings):
|
|||||||
# JWT配置
|
# JWT配置
|
||||||
SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string"
|
SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string"
|
||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 # 30天
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30天
|
||||||
|
|
||||||
# CORS配置
|
# CORS配置
|
||||||
BACKEND_CORS_ORIGINS: list = [
|
BACKEND_CORS_ORIGINS: list = [
|
||||||
|
|||||||
@@ -25646,7 +25646,7 @@
|
|||||||
"label": "list_my_mobile_menus()",
|
"label": "list_my_mobile_menus()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L112"
|
"source_location": "L113"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api_mini_menu_rationale_1",
|
"id": "api_mini_menu_rationale_1",
|
||||||
@@ -103624,7 +103624,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L112",
|
"source_location": "L113",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103633,7 +103633,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L114",
|
"source_location": "L115",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103642,7 +103642,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L122",
|
"source_location": "L123",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103651,7 +103651,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L126",
|
"source_location": "L127",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103660,7 +103660,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L128",
|
"source_location": "L129",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103669,7 +103669,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L139",
|
"source_location": "L140",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103678,7 +103678,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L145",
|
"source_location": "L146",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290562,7 +290562,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L116",
|
"source_location": "L117",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290572,7 +290572,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L118",
|
"source_location": "L119",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290582,7 +290582,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L118",
|
"source_location": "L119",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290592,7 +290592,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L124",
|
"source_location": "L125",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -290602,7 +290602,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L127",
|
"source_location": "L128",
|
||||||
"weight": 1.0
|
"weight": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Graph Report - app (2026-08-11)
|
# Graph Report - app (2026-08-12)
|
||||||
|
|
||||||
## Corpus Check
|
## 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
|
## Summary
|
||||||
- 5238 nodes · 28666 edges · 43 communities detected
|
- 5238 nodes · 28666 edges · 43 communities detected
|
||||||
|
|||||||
@@ -32533,7 +32533,7 @@
|
|||||||
"label": "list_my_mobile_menus()",
|
"label": "list_my_mobile_menus()",
|
||||||
"file_type": "code",
|
"file_type": "code",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L112",
|
"source_location": "L113",
|
||||||
"id": "api_mini_menu_list_my_mobile_menus",
|
"id": "api_mini_menu_list_my_mobile_menus",
|
||||||
"community": 10,
|
"community": 10,
|
||||||
"norm_label": "list_my_mobile_menus()"
|
"norm_label": "list_my_mobile_menus()"
|
||||||
@@ -58348,7 +58348,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L124",
|
"source_location": "L125",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "core_response_success_response",
|
"_tgt": "core_response_success_response",
|
||||||
@@ -67204,7 +67204,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L116",
|
"source_location": "L117",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "core_response_error_response",
|
"_tgt": "core_response_error_response",
|
||||||
@@ -144364,7 +144364,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L118",
|
"source_location": "L119",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "str",
|
"_tgt": "str",
|
||||||
@@ -340912,7 +340912,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L118",
|
"source_location": "L119",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "crud_base_crudbase_get",
|
"_tgt": "crud_base_crudbase_get",
|
||||||
@@ -355611,7 +355611,7 @@
|
|||||||
"relation": "contains",
|
"relation": "contains",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L112",
|
"source_location": "L113",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_py",
|
"_src": "api_mini_menu_py",
|
||||||
"_tgt": "api_mini_menu_list_my_mobile_menus",
|
"_tgt": "api_mini_menu_list_my_mobile_menus",
|
||||||
@@ -355635,7 +355635,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L114",
|
"source_location": "L115",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "api_mini_menu_current_user",
|
"_tgt": "api_mini_menu_current_user",
|
||||||
@@ -355647,7 +355647,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L122",
|
"source_location": "L123",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "api_mini_menu_find_mobile_root",
|
"_tgt": "api_mini_menu_find_mobile_root",
|
||||||
@@ -355659,7 +355659,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L126",
|
"source_location": "L127",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "api_mini_menu_collect_descendants",
|
"_tgt": "api_mini_menu_collect_descendants",
|
||||||
@@ -355671,7 +355671,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L128",
|
"source_location": "L129",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "api_mini_menu_user_menu_ids",
|
"_tgt": "api_mini_menu_user_menu_ids",
|
||||||
@@ -355695,7 +355695,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L145",
|
"source_location": "L146",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "api_mini_menu_mobile_menu_icon",
|
"_tgt": "api_mini_menu_mobile_menu_icon",
|
||||||
@@ -355719,7 +355719,7 @@
|
|||||||
"relation": "calls",
|
"relation": "calls",
|
||||||
"confidence": "EXTRACTED",
|
"confidence": "EXTRACTED",
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L139",
|
"source_location": "L140",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "api_mini_menu_mobile_menu_icon_colors",
|
"_tgt": "api_mini_menu_mobile_menu_icon_colors",
|
||||||
@@ -355744,7 +355744,7 @@
|
|||||||
"confidence": "INFERRED",
|
"confidence": "INFERRED",
|
||||||
"confidence_score": 0.8,
|
"confidence_score": 0.8,
|
||||||
"source_file": "api/mini_menu.py",
|
"source_file": "api/mini_menu.py",
|
||||||
"source_location": "L127",
|
"source_location": "L128",
|
||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"_src": "api_mini_menu_list_my_mobile_menus",
|
"_src": "api_mini_menu_list_my_mobile_menus",
|
||||||
"_tgt": "services_web_auth_roles_resolve_web_role_for_user",
|
"_tgt": "services_web_auth_roles_resolve_web_role_for_user",
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1786374022.5691514,
|
"/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/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/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/qc.py": 1785207187.402777,
|
||||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market.py": 1781522283.6823432,
|
"/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,
|
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1782718998.90251,
|
||||||
|
|||||||
@@ -0,0 +1,558 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>发起审批 · 底栏按钮重设计 Demo</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ink: #0f172a;
|
||||||
|
--muted: #64748b;
|
||||||
|
--line: #e2e8f0;
|
||||||
|
--brand: #22c55e;
|
||||||
|
--brand-dark: #16a34a;
|
||||||
|
--brand-deep: #15803d;
|
||||||
|
--brand-soft: #ecfdf5;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; min-height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 420px at 8% -8%, rgba(52, 211, 153, 0.16), transparent 55%),
|
||||||
|
radial-gradient(700px 380px at 92% 0%, rgba(148, 163, 184, 0.14), transparent 50%),
|
||||||
|
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 18px 96px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 720px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
margin: 14px 0 22px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.note strong { color: var(--brand-deep); }
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr minmax(300px, 360px);
|
||||||
|
gap: 22px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— 方案列表 —— */
|
||||||
|
.schemes { display: grid; gap: 12px; }
|
||||||
|
.scheme {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, box-shadow .15s, background .15s;
|
||||||
|
font-family: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.scheme:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
.scheme.is-on {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: linear-gradient(180deg, #f0fdf4, #fff);
|
||||||
|
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
|
||||||
|
}
|
||||||
|
.scheme-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.letter {
|
||||||
|
width: 28px; height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
font-size: 13px; font-weight: 750;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #334155;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.scheme.is-on .letter {
|
||||||
|
background: var(--brand-soft);
|
||||||
|
color: var(--brand-deep);
|
||||||
|
}
|
||||||
|
.scheme h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 680;
|
||||||
|
}
|
||||||
|
.scheme p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--muted);
|
||||||
|
padding-left: 38px;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.tag.rec {
|
||||||
|
background: var(--brand-soft);
|
||||||
|
color: var(--brand-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— 右侧手机 —— */
|
||||||
|
.stage {
|
||||||
|
position: sticky;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
.phone {
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 36px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 24px 50px rgba(15, 23, 42, 0.18);
|
||||||
|
}
|
||||||
|
.phone-screen {
|
||||||
|
border-radius: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8fafc;
|
||||||
|
min-height: 640px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.phone-nav {
|
||||||
|
height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #0f172a;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.phone-nav .back {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.form-mock {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 14px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #eef2f6;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.field .lab {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.field .val {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.field .val.ph { color: #cbd5e1; }
|
||||||
|
.stage-meta {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(255,255,255,.92);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.stage-meta strong { color: var(--brand-deep); }
|
||||||
|
.stage-meta .why {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 按钮原子 ========== */
|
||||||
|
.ft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #eef2f6;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
margin: 0;
|
||||||
|
appearance: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform .12s, opacity .12s, background .15s, box-shadow .15s;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn:active { transform: scale(0.98); opacity: 0.92; }
|
||||||
|
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* 方案样式仅作用于右侧大屏,避免污染左侧迷你条 */
|
||||||
|
.phone-screen.s1 .ft { gap: 10px; padding: 12px 14px; }
|
||||||
|
.phone-screen.s1 .btn-cancel {
|
||||||
|
flex: 0 0 auto; height: 44px; padding: 0 10px;
|
||||||
|
background: transparent; color: #94a3b8;
|
||||||
|
font-weight: 500; font-size: 14px; border-radius: 0;
|
||||||
|
}
|
||||||
|
.phone-screen.s1 .btn-draft {
|
||||||
|
flex: 1; background: #f1f5f9; color: #334155; font-weight: 600;
|
||||||
|
}
|
||||||
|
.phone-screen.s1 .btn-submit {
|
||||||
|
flex: 1.35; background: var(--brand); color: #fff; font-weight: 700;
|
||||||
|
box-shadow: 0 8px 18px rgba(34, 197, 94, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s2 .ft { gap: 12px; padding: 12px 16px; }
|
||||||
|
.phone-screen.s2 .btn-cancel { display: none; }
|
||||||
|
.phone-screen.s2 .btn-draft {
|
||||||
|
flex: 0 0 auto; background: transparent; color: #64748b;
|
||||||
|
font-weight: 500; font-size: 15px; padding: 0 8px; height: 44px; border-radius: 0;
|
||||||
|
}
|
||||||
|
.phone-screen.s2 .btn-submit {
|
||||||
|
flex: 1; height: 46px; border-radius: 12px;
|
||||||
|
background: var(--brand-dark); color: #fff; font-weight: 680; letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s3 .ft { gap: 12px; padding: 14px 16px; border-top-color: #f1f5f9; }
|
||||||
|
.phone-screen.s3 .btn-cancel { display: none; }
|
||||||
|
.phone-screen.s3 .btn-draft {
|
||||||
|
flex: 1; height: 46px; border-radius: 10px; background: #fff; color: #475569;
|
||||||
|
font-weight: 550; box-shadow: inset 0 0 0 1px #d0d5dd;
|
||||||
|
}
|
||||||
|
.phone-screen.s3 .btn-submit {
|
||||||
|
flex: 1; height: 46px; border-radius: 10px; background: #0f172a; color: #fff; font-weight: 650;
|
||||||
|
}
|
||||||
|
.phone-screen.s3 .btn-submit span.dot {
|
||||||
|
width: 6px; height: 6px; border-radius: 50%; background: var(--brand); margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s4 { background: #f1f5f9; }
|
||||||
|
.phone-screen.s4 .ft {
|
||||||
|
margin: 0 12px 12px; padding: 8px; gap: 8px; border: none; border-radius: 18px;
|
||||||
|
background: rgba(255,255,255,0.92);
|
||||||
|
box-shadow: 0 1px 0 rgba(255,255,255,0.8) inset, 0 10px 28px rgba(15, 23, 42, 0.1);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
.phone-screen.s4 .btn-cancel {
|
||||||
|
flex: 0 0 44px; width: 44px; height: 44px; padding: 0; border-radius: 14px;
|
||||||
|
background: #f1f5f9; color: #64748b; font-size: 13px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.phone-screen.s4 .btn-draft {
|
||||||
|
flex: 1; height: 44px; border-radius: 14px; background: #f8fafc; color: #334155;
|
||||||
|
font-weight: 600; box-shadow: inset 0 0 0 1px #e2e8f0;
|
||||||
|
}
|
||||||
|
.phone-screen.s4 .btn-submit {
|
||||||
|
flex: 1.2; height: 44px; border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #4ade80, #16a34a); color: #fff; font-weight: 720;
|
||||||
|
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s5 .ft { gap: 0; padding: 0; border-top: 1px solid #e5e7eb; }
|
||||||
|
.phone-screen.s5 .btn { height: 52px; border-radius: 0; font-size: 15px; }
|
||||||
|
.phone-screen.s5 .btn-cancel {
|
||||||
|
flex: 0 0 76px; background: #fff; color: #64748b; font-weight: 500;
|
||||||
|
border-right: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.phone-screen.s5 .btn-draft {
|
||||||
|
flex: 1; background: #fff; color: #1e293b; font-weight: 550;
|
||||||
|
border-right: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.phone-screen.s5 .btn-submit {
|
||||||
|
flex: 1.25; background: var(--brand-dark); color: #fff; font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s6 .ft { gap: 10px; padding: 12px 14px; }
|
||||||
|
.phone-screen.s6 .btn-cancel {
|
||||||
|
flex: 0 0 auto; background: transparent; color: #94a3b8; font-weight: 500;
|
||||||
|
font-size: 14px; padding: 0 8px; height: 44px; border-radius: 0;
|
||||||
|
}
|
||||||
|
.phone-screen.s6 .btn-draft {
|
||||||
|
flex: 1; height: 44px; border-radius: 999px; background: #fff; color: var(--brand-deep);
|
||||||
|
font-weight: 620; box-shadow: inset 0 0 0 1.5px #86efac;
|
||||||
|
}
|
||||||
|
.phone-screen.s6 .btn-submit {
|
||||||
|
flex: 1.35; height: 44px; border-radius: 999px; background: var(--brand-deep);
|
||||||
|
color: #fff; font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s7 .ft { gap: 10px; padding: 12px 14px; }
|
||||||
|
.phone-screen.s7 .btn-cancel { display: none; }
|
||||||
|
.phone-screen.s7 .btn-draft {
|
||||||
|
flex: 0 0 auto; min-width: 108px; height: 46px; border-radius: 12px;
|
||||||
|
background: #fff; color: #475569; font-weight: 550; font-size: 14px; gap: 6px;
|
||||||
|
box-shadow: inset 0 0 0 1px #e2e8f0;
|
||||||
|
}
|
||||||
|
.phone-screen.s7 .btn-submit {
|
||||||
|
flex: 1; height: 46px; border-radius: 12px; background: var(--brand); color: #fff;
|
||||||
|
font-weight: 720; font-size: 16px; box-shadow: 0 10px 22px rgba(34, 197, 94, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen.s8 .ft {
|
||||||
|
gap: 10px; padding: 12px 14px; background: #fafbfc; border-top-color: #eef2f6;
|
||||||
|
}
|
||||||
|
.phone-screen.s8 .btn-cancel {
|
||||||
|
flex: 0 0 52px; height: 44px; padding: 0; border-radius: 12px;
|
||||||
|
background: #fff; color: #64748b; font-weight: 500; font-size: 14px;
|
||||||
|
box-shadow: inset 0 0 0 1px #e5e7eb;
|
||||||
|
}
|
||||||
|
.phone-screen.s8 .btn-draft {
|
||||||
|
flex: 1; height: 44px; border-radius: 12px; background: #eff6ff; color: #1d4ed8; font-weight: 620;
|
||||||
|
}
|
||||||
|
.phone-screen.s8 .btn-submit {
|
||||||
|
flex: 1.3; height: 44px; border-radius: 12px; background: var(--brand-dark); color: #fff; font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧迷你色条:只示意配色,不套完整底栏布局 */
|
||||||
|
.mini-ft {
|
||||||
|
display: flex; gap: 6px; align-items: center;
|
||||||
|
margin-left: 38px; pointer-events: none;
|
||||||
|
}
|
||||||
|
.mini-ft .ft { display: contents; }
|
||||||
|
.mini-ft .btn {
|
||||||
|
height: 28px; font-size: 11px; padding: 0 10px; border-radius: 8px;
|
||||||
|
box-shadow: none !important; min-width: 0;
|
||||||
|
}
|
||||||
|
.mini-ft .btn-cancel { display: none; }
|
||||||
|
.scheme.s1 .mini-ft .btn-draft { background: #f1f5f9; color: #334155; }
|
||||||
|
.scheme.s1 .mini-ft .btn-submit { background: #22c55e; color: #fff; }
|
||||||
|
.scheme.s2 .mini-ft .btn-draft { background: transparent; color: #64748b; padding: 0 4px; }
|
||||||
|
.scheme.s2 .mini-ft .btn-submit { flex: 1; background: #16a34a; color: #fff; }
|
||||||
|
.scheme.s3 .mini-ft .btn-draft { background: #fff; color: #475569; box-shadow: inset 0 0 0 1px #d0d5dd !important; }
|
||||||
|
.scheme.s3 .mini-ft .btn-submit { background: #0f172a; color: #fff; }
|
||||||
|
.scheme.s3 .mini-ft .dot { display: none; }
|
||||||
|
.scheme.s4 .mini-ft .btn-draft { background: #f8fafc; color: #334155; box-shadow: inset 0 0 0 1px #e2e8f0 !important; }
|
||||||
|
.scheme.s4 .mini-ft .btn-submit { background: linear-gradient(180deg, #4ade80, #16a34a); color: #fff; }
|
||||||
|
.scheme.s5 .mini-ft .btn { border-radius: 0; height: 30px; }
|
||||||
|
.scheme.s5 .mini-ft .btn-draft { background: #fff; color: #1e293b; box-shadow: inset 0 0 0 1px #e5e7eb !important; }
|
||||||
|
.scheme.s5 .mini-ft .btn-submit { background: #16a34a; color: #fff; }
|
||||||
|
.scheme.s6 .mini-ft .btn { border-radius: 999px; }
|
||||||
|
.scheme.s6 .mini-ft .btn-draft { background: #fff; color: #15803d; box-shadow: inset 0 0 0 1.5px #86efac !important; }
|
||||||
|
.scheme.s6 .mini-ft .btn-submit { background: #15803d; color: #fff; }
|
||||||
|
.scheme.s7 .mini-ft .btn-draft { background: #fff; color: #475569; box-shadow: inset 0 0 0 1px #e2e8f0 !important; }
|
||||||
|
.scheme.s7 .mini-ft .btn-submit { flex: 1; background: #22c55e; color: #fff; }
|
||||||
|
.scheme.s8 .mini-ft .btn-draft { background: #eff6ff; color: #1d4ed8; }
|
||||||
|
.scheme.s8 .mini-ft .btn-submit { background: #16a34a; color: #fff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>底栏按钮 · 重新设计</h1>
|
||||||
|
<p class="sub">
|
||||||
|
上一版问题:草稿与提交都抢绿色、圆角/描边千篇一律、缩略图看不出层级。
|
||||||
|
这一版只保留 <strong>8 个差异化方向</strong>,右侧大屏实时预览;核心原则是
|
||||||
|
<strong>草稿弱、提交强</strong>,避免双绿对撞。
|
||||||
|
</p>
|
||||||
|
<div class="note">
|
||||||
|
点左侧方案切换右侧手机。选好后回复编号,例如「用 <strong>S1</strong>」。
|
||||||
|
取消是否保留也可一并说明。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<div class="schemes" id="schemes"></div>
|
||||||
|
|
||||||
|
<aside class="stage">
|
||||||
|
<div class="phone">
|
||||||
|
<div class="phone-screen" id="phoneScreen">
|
||||||
|
<div class="phone-nav">
|
||||||
|
<span class="back">‹ 返回</span>
|
||||||
|
请假申请
|
||||||
|
</div>
|
||||||
|
<div class="form-mock">
|
||||||
|
<div class="field">
|
||||||
|
<div class="lab">请假类型</div>
|
||||||
|
<div class="val">年假</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="lab">开始时间</div>
|
||||||
|
<div class="val">2026-08-12 09:00</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="lab">结束时间</div>
|
||||||
|
<div class="val">2026-08-13 18:00</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="lab">请假事由</div>
|
||||||
|
<div class="val ph">请填写事由…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ft" id="liveFt"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stage-meta">
|
||||||
|
当前:<strong id="liveName">—</strong>
|
||||||
|
<div class="why" id="liveWhy"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ICO_SAVE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>`
|
||||||
|
|
||||||
|
const schemes = [
|
||||||
|
{
|
||||||
|
id: 'S1',
|
||||||
|
title: '石板次钮 + 薄荷主钮',
|
||||||
|
rec: true,
|
||||||
|
why: '草稿用中性灰底,不再抢绿;提交纯色+轻阴影,层级一眼可读。保留取消文字链。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">暂存草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S2',
|
||||||
|
title: '飞书式 · 文字草稿 + 宽提交',
|
||||||
|
why: '草稿降为次级文字操作,主视线落在提交。信息密度低、决策成本低。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">暂存草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交审批</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S3',
|
||||||
|
title: '等宽双钮 · 白描边 + 墨黑提交',
|
||||||
|
why: '提交用近黑而非绿,品牌绿只做小圆点点缀,克制、偏工具产品。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">暂存草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button"><span class="dot"></span>提交</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S4',
|
||||||
|
title: '悬浮胶囊底栏',
|
||||||
|
why: '底栏浮起成卡片;取消为方块、草稿线框、提交亮绿渐变,现代感强。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S5',
|
||||||
|
title: '通栏三段 · 右主色块',
|
||||||
|
why: '无圆角贴底通栏,右段深绿实心。偏企业后台/工具效率感。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">暂存草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S6',
|
||||||
|
title: '双胶囊 · 线框绿字 + 实心深绿',
|
||||||
|
why: '同圆角语言,但草稿只留线框墨绿字,提交实心深绿,绿不打架。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">暂存草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S7',
|
||||||
|
title: '紧凑草稿 + 超宽提交',
|
||||||
|
why: '草稿缩成带图标的次钮,提交占满剩余宽度,强调「主行动只有一个」。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">${ICO_SAVE}草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'S8',
|
||||||
|
title: '冷暖分层 · 蓝草稿 / 绿提交',
|
||||||
|
why: '用色相区分动作:草稿偏冷蓝、提交偏暖绿,语义分离最强(略跳脱品牌单色)。',
|
||||||
|
markup: `
|
||||||
|
<button class="btn btn-cancel" type="button">取消</button>
|
||||||
|
<button class="btn btn-draft" type="button">暂存草稿</button>
|
||||||
|
<button class="btn btn-submit" type="button">提交</button>`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const list = document.getElementById('schemes')
|
||||||
|
const phone = document.getElementById('phoneScreen')
|
||||||
|
const liveFt = document.getElementById('liveFt')
|
||||||
|
const liveName = document.getElementById('liveName')
|
||||||
|
const liveWhy = document.getElementById('liveWhy')
|
||||||
|
|
||||||
|
function apply(id) {
|
||||||
|
const s = schemes.find((x) => x.id === id)
|
||||||
|
phone.className = 'phone-screen ' + id.toLowerCase()
|
||||||
|
liveFt.innerHTML = s.markup
|
||||||
|
liveName.textContent = id + ' · ' + s.title
|
||||||
|
liveWhy.textContent = s.why
|
||||||
|
list.querySelectorAll('.scheme').forEach((el) => {
|
||||||
|
el.classList.toggle('is-on', el.dataset.id === id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
schemes.forEach((s) => {
|
||||||
|
const btn = document.createElement('button')
|
||||||
|
btn.type = 'button'
|
||||||
|
btn.className = 'scheme ' + s.id.toLowerCase()
|
||||||
|
btn.dataset.id = s.id
|
||||||
|
btn.innerHTML = `
|
||||||
|
<div class="scheme-top">
|
||||||
|
<span class="letter">${s.id.replace('S', '')}</span>
|
||||||
|
<h2>${s.title}</h2>
|
||||||
|
${s.rec ? '<span class="tag rec">推荐</span>' : '<span class="tag">' + s.id + '</span>'}
|
||||||
|
</div>
|
||||||
|
<p>${s.why}</p>
|
||||||
|
<div class="mini-ft"><div class="ft">${s.markup}</div></div>`
|
||||||
|
btn.addEventListener('click', () => apply(s.id))
|
||||||
|
list.appendChild(btn)
|
||||||
|
})
|
||||||
|
|
||||||
|
apply('S1')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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` → `<form-create-mobile disabled>`
|
||||||
|
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 合并。
|
||||||
@@ -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
|
||||||
|
<van-cell
|
||||||
|
v-for="u in filtered"
|
||||||
|
:key="u.id"
|
||||||
|
clickable
|
||||||
|
@click="toggle(u.id)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<van-checkbox class="oa-pick__check" :name="u.id" @click.stop />
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<span class="oa-pick__name">{{ u.label }}</span>
|
||||||
|
<span v-if="u.sub" class="oa-pick__sub">{{ u.sub }}</span>
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
```
|
||||||
|
|
||||||
|
单选同理,`@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"
|
||||||
|
```
|
||||||
@@ -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)。
|
||||||
|
- 变更若调整范围,先改本文再改计划与代码。
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<view class="up-title">添加附件</view>
|
<view class="up-title">添加附件</view>
|
||||||
<view class="up-sub">支持 PDF / 图片 / Office,可多选上传</view>
|
<view class="up-sub">支持 PDF / 图片 / Office,可多选上传</view>
|
||||||
</view>
|
</view>
|
||||||
<van-icon name="arrow" color="#16a34a" />
|
<van-icon name="arrow" color="#22c55e" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<van-button size="small" plain hairline type="default" @click.stop="$emit('view', item)">
|
<van-button size="small" plain hairline type="default" @click.stop="$emit('view', item)">
|
||||||
查看
|
查看
|
||||||
</van-button>
|
</van-button>
|
||||||
<van-button size="small" type="primary" color="#16a34a" @click.stop="$emit('action', item)">
|
<van-button size="small" type="primary" color="#22c55e" @click.stop="$emit('action', item)">
|
||||||
处理
|
处理
|
||||||
</van-button>
|
</van-button>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -6,8 +6,12 @@
|
|||||||
class="menu-card"
|
class="menu-card"
|
||||||
@click="$emit('select', m)"
|
@click="$emit('select', m)"
|
||||||
>
|
>
|
||||||
<view class="menu-icon" :class="toneClass(m)" :style="iconBoxStyle(m)">
|
<view
|
||||||
<VanIcon :name="m.icon" :color="iconFg(m)" size="52rpx" />
|
class="menu-icon"
|
||||||
|
:class="[toneClass(m), plainClass(m)]"
|
||||||
|
:style="iconBoxStyle(m)"
|
||||||
|
>
|
||||||
|
<VanIcon :name="m.icon" :color="iconFg(m)" :size="iconSize(m)" />
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-title">{{ m.title }}</text>
|
<text class="menu-title">{{ m.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -32,10 +36,21 @@
|
|||||||
iconFg(m) {
|
iconFg(m) {
|
||||||
return this.colors(m).iconFg || ''
|
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) {
|
iconBoxStyle(m) {
|
||||||
const { iconBg, iconFg } = this.colors(m)
|
const { iconBg, iconFg } = this.colors(m)
|
||||||
const style = {}
|
const style = {}
|
||||||
if (iconBg) style.background = iconBg
|
if (iconBg) style.background = iconBg
|
||||||
|
if (this.isPlainIcon(m)) style.boxShadow = 'none'
|
||||||
if (iconFg) style.color = iconFg
|
if (iconFg) style.color = iconFg
|
||||||
return style
|
return style
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default {
|
|||||||
name: 'OaAppIcon',
|
name: 'OaAppIcon',
|
||||||
props: {
|
props: {
|
||||||
iconUrl: { type: String, default: '' },
|
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' }
|
size: { type: String, default: 'sm' }
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -51,35 +51,50 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.oa-app-icon--xs {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
.oa-app-icon--sm {
|
.oa-app-icon--sm {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
}
|
||||||
.oa-app-icon--card {
|
.oa-app-icon--card {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
}
|
||||||
.oa-app-icon--md {
|
.oa-app-icon--md {
|
||||||
width: 56px;
|
width: 40px;
|
||||||
height: 56px;
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.oa-app-icon--lg {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
}
|
||||||
.oa-app-icon__glyph {
|
.oa-app-icon__glyph {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
.oa-app-icon--xs .oa-app-icon__glyph {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
.oa-app-icon--card .oa-app-icon__glyph {
|
.oa-app-icon--card .oa-app-icon__glyph {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
.oa-app-icon--md .oa-app-icon__glyph {
|
.oa-app-icon--md .oa-app-icon__glyph {
|
||||||
width: 26px;
|
width: 20px;
|
||||||
height: 26px;
|
height: 20px;
|
||||||
|
}
|
||||||
|
.oa-app-icon--lg .oa-app-icon__glyph {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
}
|
}
|
||||||
.oa-app-icon__photo {
|
.oa-app-icon__photo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
'eye-o': '◎',
|
'eye-o': '◎',
|
||||||
down: '↓',
|
down: '↓',
|
||||||
checked: '✓',
|
checked: '✓',
|
||||||
|
search: '⌕',
|
||||||
|
clear: '×',
|
||||||
'manager-o': '☺',
|
'manager-o': '☺',
|
||||||
'clock-o': '◷',
|
'clock-o': '◷',
|
||||||
description: '📄',
|
description: '📄',
|
||||||
@@ -47,7 +49,12 @@
|
|||||||
|
|
||||||
/** 本地位图(小程序 <image> 真机可靠);颜色已烘焙在 PNG 内 */
|
/** 本地位图(小程序 <image> 真机可靠);颜色已烘焙在 PNG 内 */
|
||||||
const PNG_ICONS = {
|
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 {
|
export default {
|
||||||
|
|||||||
@@ -57,42 +57,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-primary {
|
.v-tag.is-primary {
|
||||||
background: #f0fdf4;
|
background: #ecfdf5;
|
||||||
color: #15803d;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-success {
|
.v-tag.is-success {
|
||||||
background: #f0fdf4;
|
background: #ecfdf5;
|
||||||
color: #189b65;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-warning {
|
.v-tag.is-warning {
|
||||||
background: #fff7e8;
|
background: #fffbeb;
|
||||||
color: #cf8a19;
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-danger {
|
.v-tag.is-danger {
|
||||||
background: #fdecec;
|
background: #fef2f2;
|
||||||
color: #d14343;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-plain.is-primary,
|
.v-tag.is-plain.is-primary,
|
||||||
.v-tag.is-plain.is-success {
|
.v-tag.is-plain.is-success {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: #16a34a;
|
border-color: #22c55e;
|
||||||
color: #16a34a;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-plain.is-warning {
|
.v-tag.is-plain.is-warning {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: #cf8a19;
|
border-color: #d97706;
|
||||||
color: #cf8a19;
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-plain.is-danger {
|
.v-tag.is-plain.is-danger {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: #d14343;
|
border-color: #ef4444;
|
||||||
color: #d14343;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-tag.is-plain.is-default {
|
.v-tag.is-plain.is-default {
|
||||||
|
|||||||
@@ -36,24 +36,24 @@
|
|||||||
"path": "pages/oa/form-sink/index",
|
"path": "pages/oa/form-sink/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "表单水槽测试",
|
"navigationBarTitleText": "表单水槽测试",
|
||||||
"navigationBarBackgroundColor": "#16a34a",
|
"navigationBarBackgroundColor": "#ecfdf5",
|
||||||
"navigationBarTextStyle": "white"
|
"navigationBarTextStyle": "black"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/oa/start/index",
|
"path": "pages/oa/start/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "发起审批",
|
"navigationBarTitleText": "发起审批",
|
||||||
"navigationBarBackgroundColor": "#16a34a",
|
"navigationBarBackgroundColor": "#ecfdf5",
|
||||||
"navigationBarTextStyle": "white"
|
"navigationBarTextStyle": "black"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/oa/start/webview",
|
"path": "pages/oa/start/webview",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "填写审批",
|
"navigationBarTitleText": "填写审批",
|
||||||
"navigationBarBackgroundColor": "#16a34a",
|
"navigationBarBackgroundColor": "#ecfdf5",
|
||||||
"navigationBarTextStyle": "white"
|
"navigationBarTextStyle": "black"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarTitleText": "中科兴皖",
|
"navigationBarTitleText": "中科兴皖",
|
||||||
"navigationBarBackgroundColor": "#16a34a",
|
"navigationBarBackgroundColor": "#ecfdf5",
|
||||||
"backgroundColor": "#f5f8f7"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"uniIdRouter": {}
|
"uniIdRouter": {}
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
<view class="panel">
|
<view class="panel">
|
||||||
<view class="panel-hd"><text class="panel-title">按钮 Button</text></view>
|
<view class="panel-hd"><text class="panel-title">按钮 Button</text></view>
|
||||||
<view class="comp-row">
|
<view class="comp-row">
|
||||||
<van-button type="primary" size="small" color="#16a34a" round>主按钮</van-button>
|
<van-button type="primary" size="small" color="#22c55e" round>主按钮</van-button>
|
||||||
<van-button type="primary" size="small" plain hairline color="#16a34a" round>描边</van-button>
|
<van-button type="primary" size="small" plain hairline color="#22c55e" round>描边</van-button>
|
||||||
<van-button size="small" plain hairline round>次要</van-button>
|
<van-button size="small" plain hairline round>次要</van-button>
|
||||||
<van-button type="danger" size="small" plain hairline round>危险</van-button>
|
<van-button type="danger" size="small" plain hairline round>危险</van-button>
|
||||||
</view>
|
</view>
|
||||||
<van-button
|
<van-button
|
||||||
type="primary"
|
type="primary"
|
||||||
color="#16a34a"
|
color="#22c55e"
|
||||||
block
|
block
|
||||||
round
|
round
|
||||||
style="margin-top: 16rpx; height: 88rpx;"
|
style="margin-top: 16rpx; height: 88rpx;"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<view class="panel-hd"><text class="panel-title">状态 Tag</text></view>
|
<view class="panel-hd"><text class="panel-title">状态 Tag</text></view>
|
||||||
<view class="comp-row">
|
<view class="comp-row">
|
||||||
<van-tag type="warning" plain round>待处理</van-tag>
|
<van-tag type="warning" plain round>待处理</van-tag>
|
||||||
<van-tag type="primary" color="#16a34a" round>进行中</van-tag>
|
<van-tag type="primary" color="#22c55e" round>进行中</van-tag>
|
||||||
<van-tag type="success" plain round>已完成</van-tag>
|
<van-tag type="success" plain round>已完成</van-tag>
|
||||||
<van-tag type="danger" plain round>异常</van-tag>
|
<van-tag type="danger" plain round>异常</van-tag>
|
||||||
</view>
|
</view>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
left-icon="volume-o"
|
left-icon="volume-o"
|
||||||
text="移动端菜单在「菜单管理 → 移动端菜单」下配置,按角色授权后展示。"
|
text="移动端菜单在「菜单管理 → 移动端菜单」下配置,按角色授权后展示。"
|
||||||
background="#f0fdf4"
|
background="#f0fdf4"
|
||||||
color="#15803d"
|
color="#16a34a"
|
||||||
/>
|
/>
|
||||||
<view style="min-height: 360rpx; margin-top: 24rpx">
|
<view style="min-height: 360rpx; margin-top: 24rpx">
|
||||||
<van-empty
|
<van-empty
|
||||||
@@ -54,17 +54,17 @@
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view style="display: flex; justify-content: center; padding: 32rpx 0">
|
<view style="display: flex; justify-content: center; padding: 32rpx 0">
|
||||||
<van-loading size="24px" color="#16a34a">加载中</van-loading>
|
<van-loading size="24px" color="#22c55e">加载中</van-loading>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="panel">
|
<view class="panel">
|
||||||
<view class="panel-hd"><text class="panel-title">反馈</text></view>
|
<view class="panel-hd"><text class="panel-title">反馈</text></view>
|
||||||
<view class="comp-row">
|
<view class="comp-row">
|
||||||
<van-button size="small" plain hairline color="#16a34a" @click="toast('操作成功')">
|
<van-button size="small" plain hairline color="#22c55e" @click="toast('操作成功')">
|
||||||
Toast
|
Toast
|
||||||
</van-button>
|
</van-button>
|
||||||
<van-button size="small" plain hairline color="#16a34a" @click="showDialog">
|
<van-button size="small" plain hairline color="#22c55e" @click="showDialog">
|
||||||
Dialog
|
Dialog
|
||||||
</van-button>
|
</van-button>
|
||||||
</view>
|
</view>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认提交',
|
title: '确认提交',
|
||||||
content: '提交后将进入审批流程,是否继续?',
|
content: '提交后将进入审批流程,是否继续?',
|
||||||
confirmColor: '#16a34a'
|
confirmColor: '#22c55e'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<view class="detail-section">
|
<view class="detail-section">
|
||||||
<view class="sec-hd"><text class="sec-title">业务进度</text></view>
|
<view class="sec-hd"><text class="sec-title">业务进度</text></view>
|
||||||
<van-steps :active="detail.stepActive" active-color="#16a34a">
|
<van-steps :active="detail.stepActive" active-color="#22c55e">
|
||||||
<van-step v-for="s in detail.steps" :key="s">{{ s }}</van-step>
|
<van-step v-for="s in detail.steps" :key="s">{{ s }}</van-step>
|
||||||
</van-steps>
|
</van-steps>
|
||||||
</view>
|
</view>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<view class="detail-footer">
|
<view class="detail-footer">
|
||||||
<van-button class="btn-flex" plain hairline type="default" @click="goBack">返回</van-button>
|
<van-button class="btn-flex" plain hairline type="default" @click="goBack">返回</van-button>
|
||||||
<van-button class="btn-flex" type="primary" color="#16a34a" round @click="goEdit">
|
<van-button class="btn-flex" type="primary" color="#22c55e" round @click="goEdit">
|
||||||
编辑处理
|
编辑处理
|
||||||
</van-button>
|
</van-button>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
/>
|
/>
|
||||||
<van-cell title="加急">
|
<van-cell title="加急">
|
||||||
<template #right-icon>
|
<template #right-icon>
|
||||||
<van-switch v-model="form.urgent" size="20px" active-color="#16a34a" />
|
<van-switch v-model="form.urgent" size="20px" active-color="#22c55e" />
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<view class="form-footer">
|
<view class="form-footer">
|
||||||
<van-button class="btn-flex" plain hairline type="default" @click="goBack">取消</van-button>
|
<van-button class="btn-flex" plain hairline type="default" @click="goBack">取消</van-button>
|
||||||
<van-button class="btn-flex" type="primary" color="#16a34a" round @click="onSave">
|
<van-button class="btn-flex" type="primary" color="#22c55e" round @click="onSave">
|
||||||
保存
|
保存
|
||||||
</van-button>
|
</van-button>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
shape="round"
|
shape="round"
|
||||||
background="transparent"
|
background="transparent"
|
||||||
placeholder="搜索单号 / 名称 / 负责人"
|
placeholder="搜索单号 / 名称 / 负责人"
|
||||||
|
@update:model-value="onKeywordInput"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -79,6 +80,9 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
tabCount,
|
tabCount,
|
||||||
|
onKeywordInput(v) {
|
||||||
|
this.keyword = v == null ? '' : String(v)
|
||||||
|
},
|
||||||
goHome() {
|
goHome() {
|
||||||
uni.reLaunch({ url: '/pages/index/index' })
|
uni.reLaunch({ url: '/pages/index/index' })
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
:circular="banners.length > 1"
|
:circular="banners.length > 1"
|
||||||
:indicator-dots="banners.length > 1"
|
:indicator-dots="banners.length > 1"
|
||||||
indicator-color="rgba(255,255,255,0.45)"
|
indicator-color="rgba(255,255,255,0.45)"
|
||||||
indicator-active-color="#16a34a"
|
indicator-active-color="#22c55e"
|
||||||
:autoplay="false"
|
:autoplay="false"
|
||||||
>
|
>
|
||||||
<swiper-item v-for="item in banners" :key="item.id">
|
<swiper-item v-for="item in banners" :key="item.id">
|
||||||
@@ -86,8 +86,7 @@
|
|||||||
title: '发起审批',
|
title: '发起审批',
|
||||||
icon: 'approve',
|
icon: 'approve',
|
||||||
path: '/pages/oa/start/index',
|
path: '/pages/oa/start/index',
|
||||||
iconBg: '#ffe4e6',
|
iconBg: 'transparent'
|
||||||
iconFg: '#e11d48'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'oa-form-sink',
|
id: 'oa-form-sink',
|
||||||
|
|||||||
@@ -1,35 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<view class="search-wrap">
|
<view class="toolbar">
|
||||||
<input
|
<view class="search-box" :class="{ 'is-focus': searchFocused }">
|
||||||
class="search"
|
<view class="search-ico" aria-hidden="true" />
|
||||||
v-model="searchKeyword"
|
<input
|
||||||
confirm-type="search"
|
class="search-input"
|
||||||
placeholder="请输入审批单名称"
|
:value="searchKeyword"
|
||||||
/>
|
confirm-type="search"
|
||||||
|
placeholder="搜索审批单"
|
||||||
|
placeholder-class="search-ph"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@focus="searchFocused = true"
|
||||||
|
@blur="searchFocused = false"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-if="searchKeyword"
|
||||||
|
class="search-clear"
|
||||||
|
@click="searchKeyword = ''"
|
||||||
|
>
|
||||||
|
<text class="search-clear__x">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-x class="tabs" :show-scrollbar="false">
|
||||||
|
<view class="tabs-inner">
|
||||||
|
<view
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: activeGroupId === '' }"
|
||||||
|
@click="activeGroupId = ''"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-for="g in groupOptions"
|
||||||
|
:key="g.id"
|
||||||
|
class="tab"
|
||||||
|
:class="{ active: activeGroupId === g.id }"
|
||||||
|
@click="activeGroupId = g.id"
|
||||||
|
>
|
||||||
|
{{ g.groupName }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view scroll-x class="tabs" :show-scrollbar="false">
|
|
||||||
<view
|
|
||||||
class="tab"
|
|
||||||
:class="{ active: activeGroupId === '' }"
|
|
||||||
@click="activeGroupId = ''"
|
|
||||||
>
|
|
||||||
全部
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
v-for="g in groupOptions"
|
|
||||||
:key="g.id"
|
|
||||||
class="tab"
|
|
||||||
:class="{ active: activeGroupId === g.id }"
|
|
||||||
@click="activeGroupId = g.id"
|
|
||||||
>
|
|
||||||
{{ g.groupName }}
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<view class="hint">选择一个审批单即可发起</view>
|
|
||||||
|
|
||||||
<view v-if="loading" class="state state-loading">
|
<view v-if="loading" class="state state-loading">
|
||||||
<view class="pulse-dots" aria-hidden="true">
|
<view class="pulse-dots" aria-hidden="true">
|
||||||
<view class="pulse-dot" />
|
<view class="pulse-dot" />
|
||||||
@@ -45,7 +59,6 @@
|
|||||||
<view v-for="sec in displayGroups" :key="sec.id" class="section">
|
<view v-for="sec in displayGroups" :key="sec.id" class="section">
|
||||||
<view class="section-hd">
|
<view class="section-hd">
|
||||||
<text class="section-name">{{ sec.name }}</text>
|
<text class="section-name">{{ sec.name }}</text>
|
||||||
<view class="section-dot" />
|
|
||||||
<text class="section-meta">{{ sec.apps.length }} 个表单</text>
|
<text class="section-meta">{{ sec.apps.length }} 个表单</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="card-row">
|
<view class="card-row">
|
||||||
@@ -55,18 +68,8 @@
|
|||||||
class="oa-app-card"
|
class="oa-app-card"
|
||||||
@click="onPick(row)"
|
@click="onPick(row)"
|
||||||
>
|
>
|
||||||
<view class="oa-app-card__main">
|
<OaAppIcon :icon-url="row.iconUrl" size="xs" />
|
||||||
<OaAppIcon :icon-url="row.iconUrl" size="card" />
|
<text class="oa-app-card__name">{{ row.appName }}</text>
|
||||||
<view class="oa-app-card__body">
|
|
||||||
<text class="oa-app-card__name">{{ row.appName }}</text>
|
|
||||||
<text v-if="row.description" class="oa-app-card__desc">{{
|
|
||||||
row.description
|
|
||||||
}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="oa-app-card__ops">
|
|
||||||
<view class="oa-app-card__btn" @click.stop="onPick(row)">发起</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -77,7 +80,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import OaAppIcon from '../../../components/OaAppIcon.vue'
|
import OaAppIcon from '../../../components/OaAppIcon.vue'
|
||||||
import { fetchOaGroupList, fetchOaTemplates } from '../../../utils/oa-api.js'
|
import { fetchOaGroupList, fetchOaTemplates } from '../../../utils/oa-api.js'
|
||||||
import { getToken } from '../../../utils/auth.js'
|
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||||
import { stashOaStartTitle } from '../../../utils/oa-h5.js'
|
import { stashOaStartTitle } from '../../../utils/oa-h5.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -88,6 +91,7 @@ export default {
|
|||||||
templates: [],
|
templates: [],
|
||||||
groupOptions: [],
|
groupOptions: [],
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
|
searchFocused: false,
|
||||||
activeGroupId: ''
|
activeGroupId: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -132,12 +136,21 @@ export default {
|
|||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
forceRelogin('请先登录')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.load()
|
this.load()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onSearchInput(e) {
|
||||||
|
const v =
|
||||||
|
e && e.detail && e.detail.value != null
|
||||||
|
? String(e.detail.value)
|
||||||
|
: e && e.target
|
||||||
|
? String(e.target.value || '')
|
||||||
|
: ''
|
||||||
|
this.searchKeyword = v
|
||||||
|
},
|
||||||
async load() {
|
async load() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
@@ -175,55 +188,132 @@ export default {
|
|||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-bottom: 24rpx;
|
padding-bottom: 24rpx;
|
||||||
background: #f5f8f7;
|
background: #f5f6f8;
|
||||||
}
|
}
|
||||||
.search-wrap {
|
.toolbar {
|
||||||
padding: 20rpx 24rpx 8rpx;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.search {
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin: 20rpx 24rpx 0;
|
||||||
height: 72rpx;
|
height: 72rpx;
|
||||||
padding: 0 24rpx;
|
padding: 0 24rpx;
|
||||||
border-radius: 16rpx;
|
border-radius: 20rpx;
|
||||||
background: #f5f8f7;
|
border: 1rpx solid #e8eaed;
|
||||||
|
background: #fafafa;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
.search-box.is-focus {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 6rpx rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
.search-ico {
|
||||||
|
position: relative;
|
||||||
|
width: 26rpx;
|
||||||
|
height: 26rpx;
|
||||||
|
border: 3rpx solid #94a3b8;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.search-ico::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 12rpx;
|
||||||
|
height: 3rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
bottom: -5rpx;
|
||||||
|
background: #94a3b8;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 72rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.search-ph {
|
||||||
|
color: #b0b7c3;
|
||||||
|
}
|
||||||
|
.search-clear {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e5e7eb;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.search-clear__x {
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: -2rpx;
|
||||||
}
|
}
|
||||||
.tabs {
|
.tabs {
|
||||||
|
width: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 16rpx 16rpx 8rpx;
|
margin-top: 8rpx;
|
||||||
background: #fff;
|
padding: 0 8rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 1rpx solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.tabs-inner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 0 8rpx;
|
||||||
}
|
}
|
||||||
.tab {
|
.tab {
|
||||||
display: inline-block;
|
position: relative;
|
||||||
margin-right: 16rpx;
|
display: inline-flex;
|
||||||
padding: 12rpx 24rpx;
|
align-items: center;
|
||||||
border-radius: 999rpx;
|
justify-content: center;
|
||||||
background: #f5f8f7;
|
height: 80rpx;
|
||||||
color: #6b7b74;
|
padding: 0 28rpx;
|
||||||
font-size: 26rpx;
|
font-size: 28rpx;
|
||||||
|
color: #64748b;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: #fff;
|
color: #166534;
|
||||||
background: #16a34a;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.hint {
|
.tab.active::after {
|
||||||
padding: 16rpx 24rpx;
|
content: '';
|
||||||
color: #6b7b74;
|
position: absolute;
|
||||||
font-size: 24rpx;
|
left: 28rpx;
|
||||||
|
right: 28rpx;
|
||||||
|
bottom: 0;
|
||||||
|
height: 4rpx;
|
||||||
|
border-radius: 4rpx 4rpx 0 0;
|
||||||
|
background: #22c55e;
|
||||||
}
|
}
|
||||||
.state {
|
.state {
|
||||||
padding: 80rpx 24rpx;
|
padding: 80rpx 24rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6b7b74;
|
color: #999;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
|
/* 对齐 mobile-oa-h5 OaPulseLoading:品牌绿三点跳动 + muted 文案 */
|
||||||
.state-loading {
|
.state-loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 28rpx;
|
gap: 28rpx;
|
||||||
padding: 120rpx 24rpx 80rpx;
|
min-height: 50vh;
|
||||||
|
padding: 96rpx 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.pulse-dots {
|
.pulse-dots {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -234,8 +324,8 @@ export default {
|
|||||||
width: 16rpx;
|
width: 16rpx;
|
||||||
height: 16rpx;
|
height: 16rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #16a34a;
|
background: var(--brand-green, #22c55e);
|
||||||
animation: oa-start-bounce 1.2s ease-in-out infinite;
|
animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.pulse-dot:nth-child(2) {
|
.pulse-dot:nth-child(2) {
|
||||||
animation-delay: 0.15s;
|
animation-delay: 0.15s;
|
||||||
@@ -247,10 +337,11 @@ export default {
|
|||||||
}
|
}
|
||||||
.state-loading__text {
|
.state-loading__text {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #6b7b74;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: var(--text-muted, #5f7a6d);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
@keyframes oa-start-bounce {
|
@keyframes oa-pulse-bounce {
|
||||||
0%,
|
0%,
|
||||||
80%,
|
80%,
|
||||||
100% {
|
100% {
|
||||||
@@ -261,30 +352,32 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.body {
|
.body {
|
||||||
padding: 0 24rpx;
|
padding: 24rpx 24rpx 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
}
|
}
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 28rpx;
|
background: #fff;
|
||||||
|
border: 1rpx solid #e5e7eb;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8rpx 8rpx 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.section-hd {
|
.section-hd {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
margin-bottom: 16rpx;
|
min-height: 64rpx;
|
||||||
min-height: 40rpx;
|
padding: 0 16rpx;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
}
|
}
|
||||||
.section-name {
|
.section-name {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
.section-dot {
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #f87171;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.section-meta {
|
.section-meta {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
@@ -293,67 +386,30 @@ export default {
|
|||||||
.card-row {
|
.card-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16rpx;
|
|
||||||
}
|
}
|
||||||
.oa-app-card {
|
.oa-app-card {
|
||||||
width: calc((100% - 16rpx) / 2);
|
width: 25%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16rpx;
|
align-items: center;
|
||||||
min-height: 180rpx;
|
gap: 12rpx;
|
||||||
padding: 20rpx 20rpx 14rpx;
|
padding: 16rpx 8rpx 20rpx;
|
||||||
border-radius: 20rpx;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #d8e3de;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.oa-app-card:active {
|
.oa-app-card:active {
|
||||||
opacity: 0.94;
|
opacity: 0.72;
|
||||||
}
|
|
||||||
.oa-app-card__main {
|
|
||||||
display: flex;
|
|
||||||
gap: 20rpx;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.oa-app-card__body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6rpx;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.oa-app-card__name {
|
.oa-app-card__name {
|
||||||
font-size: 26rpx;
|
width: 100%;
|
||||||
font-weight: 600;
|
font-size: 24rpx;
|
||||||
color: #0f172a;
|
font-weight: 400;
|
||||||
|
color: #334155;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
word-break: break-word;
|
text-align: center;
|
||||||
white-space: normal;
|
word-break: break-all;
|
||||||
}
|
display: -webkit-box;
|
||||||
.oa-app-card__desc {
|
-webkit-box-orient: vertical;
|
||||||
font-size: 22rpx;
|
-webkit-line-clamp: 2;
|
||||||
color: #94a3b8;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.oa-app-card__ops {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-top: 12rpx;
|
|
||||||
border-top: 1px solid #f1f5f9;
|
|
||||||
}
|
|
||||||
.oa-app-card__btn {
|
|
||||||
height: 44rpx;
|
|
||||||
line-height: 44rpx;
|
|
||||||
padding: 0 16rpx;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
background: #16a34a;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 22rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
buildOaStartUrl,
|
buildOaStartUrl,
|
||||||
takeOaStartTitle
|
takeOaStartTitle
|
||||||
} from '../../../utils/oa-h5.js'
|
} from '../../../utils/oa-h5.js'
|
||||||
import { getToken } from '../../../utils/auth.js'
|
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||||
import { fetchOaTemplateDetail } from '../../../utils/oa-api.js'
|
import { fetchOaTemplateDetail } from '../../../utils/oa-api.js'
|
||||||
|
|
||||||
const CLOSE_ACTIONS = new Set([
|
const CLOSE_ACTIONS = new Set([
|
||||||
@@ -24,8 +24,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(query) {
|
onLoad(query) {
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
forceRelogin('请先登录')
|
||||||
setTimeout(() => uni.navigateBack(), 500)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const appId = String((query && query.appId) || '')
|
const appId = String((query && query.appId) || '')
|
||||||
@@ -41,7 +40,7 @@ export default {
|
|||||||
|
|
||||||
const src = buildOaStartUrl({ appId, draftId })
|
const src = buildOaStartUrl({ appId, draftId })
|
||||||
if (!/\btoken=/.test(src)) {
|
if (!/\btoken=/.test(src)) {
|
||||||
uni.showToast({ title: '未登录,无法打开 OA 表单', icon: 'none' })
|
forceRelogin('请先登录')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 对齐水槽:onLoad 内同步赋值,与 pages/oa/form-sink/index 一致
|
// 对齐水槽:onLoad 内同步赋值,与 pages/oa/form-sink/index 一致
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
|
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
|
||||||
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
|
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -16,10 +16,7 @@ view, text {
|
|||||||
|
|
||||||
.page-shell {
|
.page-shell {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background: var(--bg-page);
|
||||||
radial-gradient(840rpx 440rpx at 100% 0%, rgba(214, 163, 67, 0.14), transparent 55%),
|
|
||||||
radial-gradient(760rpx 400rpx at 0% 20%, rgba(22, 163, 74, 0.1), transparent 50%),
|
|
||||||
linear-gradient(180deg, #e8f5ef 0%, #f4f7f6 40%, #f7f9f8 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.safe-bottom {
|
.safe-bottom {
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-upload:active {
|
.d-attach-upload:active {
|
||||||
border-color: #16a34a;
|
border-color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-upload .up-icon {
|
.d-attach-upload .up-icon {
|
||||||
width: 88rpx;
|
width: 88rpx;
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
background: linear-gradient(145deg, #4ade80, #16a34a);
|
background: linear-gradient(145deg, #4ade80, #22c55e);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -41,13 +41,13 @@
|
|||||||
.d-attach-upload .up-title {
|
.d-attach-upload .up-title {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #15803d;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-upload .up-sub {
|
.d-attach-upload .up-sub {
|
||||||
margin-top: 4rpx;
|
margin-top: 4rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #6b7b74;
|
color: #5f7a6d;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,17 +71,17 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 6rpx;
|
width: 6rpx;
|
||||||
background: var(--file-accent, #16a34a);
|
background: var(--file-accent, #22c55e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-card.ft-pdf {
|
.d-attach-card.ft-pdf {
|
||||||
--file-accent: #d14343;
|
--file-accent: #ef4444;
|
||||||
}
|
}
|
||||||
.d-attach-card.ft-img {
|
.d-attach-card.ft-img {
|
||||||
--file-accent: #16a34a;
|
--file-accent: #22c55e;
|
||||||
}
|
}
|
||||||
.d-attach-card.ft-xls {
|
.d-attach-card.ft-xls {
|
||||||
--file-accent: #189b65;
|
--file-accent: #10b981;
|
||||||
}
|
}
|
||||||
.d-attach-card.ft-doc {
|
.d-attach-card.ft-doc {
|
||||||
--file-accent: #3b82f6;
|
--file-accent: #3b82f6;
|
||||||
@@ -105,13 +105,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-card.ft-pdf .file-icon {
|
.d-attach-card.ft-pdf .file-icon {
|
||||||
background: linear-gradient(145deg, #f07171, #d14343);
|
background: linear-gradient(145deg, #f07171, #ef4444);
|
||||||
}
|
}
|
||||||
.d-attach-card.ft-img .file-icon {
|
.d-attach-card.ft-img .file-icon {
|
||||||
background: linear-gradient(145deg, #4ade80, #16a34a);
|
background: linear-gradient(145deg, #4ade80, #22c55e);
|
||||||
}
|
}
|
||||||
.d-attach-card.ft-xls .file-icon {
|
.d-attach-card.ft-xls .file-icon {
|
||||||
background: linear-gradient(145deg, #4fbf8a, #189b65);
|
background: linear-gradient(145deg, #4fbf8a, #10b981);
|
||||||
}
|
}
|
||||||
.d-attach-card.ft-doc .file-icon {
|
.d-attach-card.ft-doc .file-icon {
|
||||||
background: linear-gradient(145deg, #6ea8ff, #3b82f6);
|
background: linear-gradient(145deg, #6ea8ff, #3b82f6);
|
||||||
@@ -169,8 +169,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-card .file-ops .btn-preview {
|
.d-attach-card .file-ops .btn-preview {
|
||||||
background: var(--brand-green-soft, #f0fdf4);
|
background: var(--brand-green-soft, #ecfdf5);
|
||||||
color: var(--brand-green-dark, #15803d);
|
color: var(--brand-green-dark, #16a34a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-attach-card .file-ops .btn-download {
|
.d-attach-card .file-ops .btn-download {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
margin: 0 0 16rpx;
|
margin: 0 0 16rpx;
|
||||||
font-size: 34rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__link {
|
.confirm-dialog__link {
|
||||||
color: var(--brand-green, #16a34a);
|
color: var(--brand-green, #22c55e);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__btn--confirm {
|
.confirm-dialog__btn--confirm {
|
||||||
color: var(--brand-green, #16a34a);
|
color: var(--brand-green, #22c55e);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
.detail-page {
|
.detail-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background: var(--bg-page, #ffffff);
|
||||||
radial-gradient(760rpx 400rpx at 100% 8%, rgba(214, 163, 67, 0.14), transparent 55%),
|
|
||||||
linear-gradient(180deg, #e8f5ef 0%, #f4f7f6 42%, #f7f9f8 100%);
|
|
||||||
padding-bottom: 176rpx;
|
padding-bottom: 176rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,17 +15,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-hero.status-warning {
|
.detail-hero.status-warning {
|
||||||
background: linear-gradient(155deg, #a67b20 0%, #d6a343 45%, #e8c06a 100%);
|
background: linear-gradient(160deg, #fffbeb 0%, #fde68a 55%, #fbbf24 100%);
|
||||||
|
color: #92400e;
|
||||||
}
|
}
|
||||||
.detail-hero.status-primary {
|
.detail-hero.status-primary {
|
||||||
@include hero-green;
|
@include hero-green;
|
||||||
border-radius: 0 0 56rpx 56rpx;
|
border-radius: 0 0 56rpx 56rpx;
|
||||||
}
|
}
|
||||||
.detail-hero.status-danger {
|
.detail-hero.status-danger {
|
||||||
background: linear-gradient(155deg, #9c2f2f 0%, #d14343 50%, #e36a6a 100%);
|
background: linear-gradient(160deg, #fef2f2 0%, #fecaca 55%, #f87171 100%);
|
||||||
|
color: #991b1b;
|
||||||
}
|
}
|
||||||
.detail-hero.status-success {
|
.detail-hero.status-success {
|
||||||
background: linear-gradient(155deg, #0f6b48 0%, #189b65 52%, #3bb883 100%);
|
background: linear-gradient(160deg, #ecfdf5 0%, #a7f3d0 55%, #6ee7b7 100%);
|
||||||
|
color: #065f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-hero-body {
|
.detail-hero-body {
|
||||||
@@ -52,13 +53,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-hero .type-chip {
|
.detail-hero .type-chip {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.35);
|
||||||
color: #fff;
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-hero.status-primary .type-chip,
|
||||||
|
.detail-hero.status-success .type-chip,
|
||||||
|
.detail-hero.status-warning .type-chip,
|
||||||
|
.detail-hero.status-danger .type-chip {
|
||||||
|
background: rgba(15, 41, 30, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-hero .status-chip {
|
.detail-hero .status-chip {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #173128;
|
color: #0f291e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-hero .hero-title {
|
.detail-hero .hero-title {
|
||||||
@@ -146,12 +154,12 @@
|
|||||||
width: 8rpx;
|
width: 8rpx;
|
||||||
height: 28rpx;
|
height: 28rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: linear-gradient(180deg, #22c55e, #15803d);
|
background: linear-gradient(180deg, #4ade80, #22c55e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section .sec-hd .sec-more {
|
.detail-section .sec-hd .sec-more {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: var(--brand-green, #16a34a);
|
color: var(--brand-green, #22c55e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.kv-list {
|
.kv-list {
|
||||||
@@ -232,7 +240,7 @@
|
|||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
background: rgba(255, 255, 255, 0.96);
|
background: rgba(255, 255, 255, 0.96);
|
||||||
border-top: 1rpx solid var(--border, #dcebe4);
|
border-top: 1rpx solid var(--border, #d8f3e5);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flow-item.done .flow-dot {
|
.flow-item.done .flow-dot {
|
||||||
background: linear-gradient(145deg, #4ade80, #16a34a);
|
background: linear-gradient(145deg, #4ade80, #22c55e);
|
||||||
}
|
}
|
||||||
.flow-item.warn .flow-dot {
|
.flow-item.warn .flow-dot {
|
||||||
background: linear-gradient(145deg, #ecc06a, #d6a343);
|
background: linear-gradient(145deg, #ecc06a, #d6a343);
|
||||||
box-shadow: 0 12rpx 28rpx rgba(214, 163, 67, 0.28);
|
box-shadow: 0 12rpx 28rpx rgba(214, 163, 67, 0.28);
|
||||||
}
|
}
|
||||||
.flow-item.danger .flow-dot {
|
.flow-item.danger .flow-dot {
|
||||||
background: linear-gradient(145deg, #ef7a7a, #d14343);
|
background: linear-gradient(145deg, #ef7a7a, #ef4444);
|
||||||
box-shadow: 0 12rpx 28rpx rgba(209, 67, 67, 0.25);
|
box-shadow: 0 12rpx 28rpx rgba(209, 67, 67, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
|
|
||||||
.flow-item.done .flow-badge {
|
.flow-item.done .flow-badge {
|
||||||
background: rgba(22, 163, 74, 0.12);
|
background: rgba(22, 163, 74, 0.12);
|
||||||
color: #15803d;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
.flow-item.warn .flow-badge {
|
.flow-item.warn .flow-badge {
|
||||||
background: rgba(214, 163, 67, 0.16);
|
background: rgba(214, 163, 67, 0.16);
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8rpx;
|
gap: 8rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #6b7b74;
|
color: #5f7a6d;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border: 1rpx solid #e8f0ec;
|
border: 1rpx solid #e8f0ec;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.form-page {
|
.form-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-page, #f5f8f7);
|
background: var(--bg-page, #ffffff);
|
||||||
padding-bottom: 176rpx;
|
padding-bottom: 176rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
background: rgba(255, 255, 255, 0.96);
|
background: rgba(255, 255, 255, 0.96);
|
||||||
border-top: 1rpx solid var(--border, #dcebe4);
|
border-top: 1rpx solid var(--border, #d8f3e5);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,15 +53,19 @@
|
|||||||
|
|
||||||
.home-header .brand {
|
.home-header .brand {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
opacity: 0.88;
|
opacity: 0.78;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-logout {
|
.home-logout {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(255, 255, 255, 0.92);
|
color: var(--header-fg, #065f46);
|
||||||
padding: 8rpx 4rpx;
|
opacity: 0.85;
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border: 1rpx solid rgba(6, 95, 70, 0.35);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-header .hello {
|
.home-header .hello {
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
opacity: 0.92;
|
opacity: 0.82;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-header .meta__left {
|
.home-header .meta__left {
|
||||||
@@ -90,7 +94,8 @@
|
|||||||
width: 8rpx;
|
width: 8rpx;
|
||||||
height: 8rpx;
|
height: 8rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: currentColor;
|
||||||
|
opacity: 0.45;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,21 +120,21 @@
|
|||||||
.panel-hd .panel-title {
|
.panel-hd .panel-title {
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-hd .hint {
|
.panel-hd .hint {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.perm-banner {
|
.perm-banner {
|
||||||
padding: 20rpx 24rpx;
|
padding: 20rpx 24rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
background: var(--barley-yellow-soft, #fff7e8);
|
background: var(--barley-yellow-soft, #fffbeb);
|
||||||
border: 1rpx solid #f0e0b8;
|
border: 1rpx solid #fde68a;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #8a6520;
|
color: #a16207;
|
||||||
margin-bottom: 24rpx;
|
margin-bottom: 24rpx;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
.home-banner__item {
|
.home-banner__item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #f0fdf4;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-banner__video,
|
.home-banner__video,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
right: -100rpx;
|
right: -100rpx;
|
||||||
bottom: -140rpx;
|
bottom: -140rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(6, 95, 70, 0.06);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin: 8rpx 4rpx 28rpx;
|
margin: 8rpx 4rpx 28rpx;
|
||||||
color: #fff;
|
color: var(--header-fg, #065f46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-hero-copy .eyebrow {
|
.list-hero-copy .eyebrow {
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
.list-hero-copy .subline .em {
|
.list-hero-copy .subline .em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: rgba(255, 255, 255, 0.18);
|
background: rgba(6, 95, 70, 0.1);
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
padding: 2rpx 16rpx;
|
padding: 2rpx 16rpx;
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
.seg-tab {
|
.seg-tab {
|
||||||
border-radius: 22rpx;
|
border-radius: 22rpx;
|
||||||
padding: 16rpx 8rpx;
|
padding: 16rpx 8rpx;
|
||||||
color: #6b7b74;
|
color: #5f7a6d;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -98,13 +98,13 @@
|
|||||||
|
|
||||||
.seg-tab.active {
|
.seg-tab.active {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--brand-green-dark, #15803d);
|
color: var(--brand-green-dark, #16a34a);
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
box-shadow: 0 8rpx 24rpx rgba(21, 128, 61, 0.14);
|
box-shadow: 0 8rpx 24rpx rgba(21, 128, 61, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seg-tab.active .count {
|
.seg-tab.active .count {
|
||||||
color: var(--brand-green, #16a34a);
|
color: var(--brand-green, #22c55e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-wrap {
|
.list-wrap {
|
||||||
@@ -163,13 +163,13 @@
|
|||||||
color: #7a5610;
|
color: #7a5610;
|
||||||
}
|
}
|
||||||
.biz-card.status-primary .type-icon {
|
.biz-card.status-primary .type-icon {
|
||||||
background: linear-gradient(145deg, #86efac, #16a34a);
|
background: linear-gradient(145deg, #86efac, #22c55e);
|
||||||
}
|
}
|
||||||
.biz-card.status-danger .type-icon {
|
.biz-card.status-danger .type-icon {
|
||||||
background: linear-gradient(145deg, #ffb4b4, #e85a5a);
|
background: linear-gradient(145deg, #ffb4b4, #e85a5a);
|
||||||
}
|
}
|
||||||
.biz-card.status-success .type-icon {
|
.biz-card.status-success .type-icon {
|
||||||
background: linear-gradient(145deg, #b8efd4, #189b65);
|
background: linear-gradient(145deg, #b8efd4, #10b981);
|
||||||
}
|
}
|
||||||
|
|
||||||
.biz-card .title-wrap {
|
.biz-card .title-wrap {
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
}
|
}
|
||||||
.biz-card.status-primary .type-chip {
|
.biz-card.status-primary .type-chip {
|
||||||
background: rgba(22, 163, 74, 0.12);
|
background: rgba(22, 163, 74, 0.12);
|
||||||
color: #15803d;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
.biz-card.status-danger .type-chip {
|
.biz-card.status-danger .type-chip {
|
||||||
background: rgba(209, 67, 67, 0.12);
|
background: rgba(209, 67, 67, 0.12);
|
||||||
@@ -225,11 +225,11 @@
|
|||||||
box-shadow: 0 4rpx 16rpx rgba(214, 163, 67, 0.22);
|
box-shadow: 0 4rpx 16rpx rgba(214, 163, 67, 0.22);
|
||||||
}
|
}
|
||||||
.biz-card.status-primary .status-pill {
|
.biz-card.status-primary .status-pill {
|
||||||
background: #16a34a;
|
background: #22c55e;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.biz-card.status-danger .status-pill {
|
.biz-card.status-danger .status-pill {
|
||||||
background: #d14343;
|
background: #ef4444;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.biz-card.status-success .status-pill {
|
.biz-card.status-success .status-pill {
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
color: #9a7424;
|
color: #9a7424;
|
||||||
}
|
}
|
||||||
.biz-card.status-primary .side-metric .amt {
|
.biz-card.status-primary .side-metric .amt {
|
||||||
color: #15803d;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
.biz-card.status-danger .side-metric .amt {
|
.biz-card.status-danger .side-metric .amt {
|
||||||
color: #b13535;
|
color: #b13535;
|
||||||
@@ -351,13 +351,13 @@
|
|||||||
color: #a67b20;
|
color: #a67b20;
|
||||||
}
|
}
|
||||||
.biz-card.status-primary .footer .left-hint {
|
.biz-card.status-primary .footer .left-hint {
|
||||||
color: #16a34a;
|
color: #22c55e;
|
||||||
}
|
}
|
||||||
.biz-card.status-danger .footer .left-hint {
|
.biz-card.status-danger .footer .left-hint {
|
||||||
color: #d14343;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
.biz-card.status-success .footer .left-hint {
|
.biz-card.status-success .footer .left-hint {
|
||||||
color: #189b65;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.biz-card .footer .ops {
|
.biz-card .footer .ops {
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 16rpx;
|
gap: 8rpx;
|
||||||
min-height: 168rpx;
|
min-height: 168rpx;
|
||||||
padding: 20rpx 8rpx 12rpx;
|
padding: 20rpx 8rpx 12rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-card:active {
|
.menu-card:active {
|
||||||
background: var(--brand-green-soft, #f0fdf4);
|
background: var(--brand-green-soft, #ecfdf5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
background: #eef2f1;
|
background: #eef2f1;
|
||||||
color: #3d524a;
|
color: #3d524a;
|
||||||
font-size: 48rpx;
|
font-size: 48rpx;
|
||||||
box-shadow: inset 0 0 0 1rpx rgba(31, 42, 38, 0.06);
|
box-shadow: inset 0 0 0 1rpx rgba(15, 41, 30, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon .v-icon--img {
|
.menu-icon .v-icon--img {
|
||||||
@@ -39,6 +39,19 @@
|
|||||||
height: 52rpx;
|
height: 52rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 无色底:仅展示位图本身(如发起审批纸飞机),收紧容器避免与标题脱节 */
|
||||||
|
.menu-icon.menu-icon--plain {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon.menu-icon--plain .v-icon--img {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-icon.tone-approve {
|
.menu-icon.tone-approve {
|
||||||
background: #ffe4e6;
|
background: #ffe4e6;
|
||||||
color: #e11d48;
|
color: #e11d48;
|
||||||
@@ -46,8 +59,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon.tone-yellow {
|
.menu-icon.tone-yellow {
|
||||||
background: var(--barley-yellow-soft, #fff7e8);
|
background: var(--barley-yellow-soft, #fffbeb);
|
||||||
color: #a67b20;
|
color: #a16207;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon.tone-slate {
|
.menu-icon.tone-slate {
|
||||||
@@ -58,7 +71,7 @@
|
|||||||
.menu-title {
|
.menu-title {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 144rpx;
|
max-width: 144rpx;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding-top: env(safe-area-inset-top);
|
padding-top: env(safe-area-inset-top);
|
||||||
border-bottom: 1rpx solid var(--border, #dcebe4);
|
border-bottom: 1rpx solid var(--border, #d8f3e5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.protocol-nav__bar {
|
.protocol-nav__bar {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
.protocol-nav__back-icon {
|
.protocol-nav__back-icon {
|
||||||
font-size: 56rpx;
|
font-size: 56rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #1f2a26;
|
color: #0f291e;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
margin-top: -4rpx;
|
margin-top: -4rpx;
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2a26;
|
color: #0f291e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.protocol-body {
|
.protocol-body {
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
.protocol-meta {
|
.protocol-meta {
|
||||||
margin-bottom: 28rpx;
|
margin-bottom: 28rpx;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.protocol-section {
|
.protocol-section {
|
||||||
@@ -72,14 +72,14 @@
|
|||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.protocol-section__p {
|
.protocol-section__p {
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 12rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
@@ -32,13 +32,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.todo-stat.tone-warn::before {
|
.todo-stat.tone-warn::before {
|
||||||
background: var(--warning, #cf8a19);
|
background: var(--warning, #d97706);
|
||||||
}
|
}
|
||||||
.todo-stat.tone-pay::before {
|
.todo-stat.tone-pay::before {
|
||||||
background: var(--barley-yellow, #d6a343);
|
background: var(--barley-yellow, #fbbf24);
|
||||||
}
|
}
|
||||||
.todo-stat.tone-danger::before {
|
.todo-stat.tone-danger::before {
|
||||||
background: var(--danger, #d14343);
|
background: var(--danger, #ef4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-stat.tone-warn {
|
.todo-stat.tone-warn {
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
font-size: 44rpx;
|
font-size: 44rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,14 +67,14 @@
|
|||||||
color: #9a7424;
|
color: #9a7424;
|
||||||
}
|
}
|
||||||
.todo-stat.tone-danger .num {
|
.todo-stat.tone-danger .num {
|
||||||
color: var(--danger, #d14343);
|
color: var(--danger, #ef4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-stat .label {
|
.todo-stat .label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-stat .stat-icon {
|
.todo-stat .stat-icon {
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
.todo-stat.tone-warn .stat-icon {
|
.todo-stat.tone-warn .stat-icon {
|
||||||
background: rgba(207, 138, 25, 0.12);
|
background: rgba(207, 138, 25, 0.12);
|
||||||
color: var(--warning, #cf8a19);
|
color: var(--warning, #d97706);
|
||||||
}
|
}
|
||||||
.todo-stat.tone-pay .stat-icon {
|
.todo-stat.tone-pay .stat-icon {
|
||||||
background: rgba(214, 163, 67, 0.16);
|
background: rgba(214, 163, 67, 0.16);
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
}
|
}
|
||||||
.todo-stat.tone-danger .stat-icon {
|
.todo-stat.tone-danger .stat-icon {
|
||||||
background: rgba(209, 67, 67, 0.12);
|
background: rgba(209, 67, 67, 0.12);
|
||||||
color: var(--danger, #d14343);
|
color: var(--danger, #ef4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-list {
|
.todo-list {
|
||||||
@@ -115,13 +115,13 @@
|
|||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
border: 1rpx solid var(--border, #dcebe4);
|
border: 1rpx solid var(--border, #d8f3e5);
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-item:active {
|
.todo-item:active {
|
||||||
background: var(--brand-green-soft, #f0fdf4);
|
background: var(--brand-green-soft, #ecfdf5);
|
||||||
border-color: #b7e5d0;
|
border-color: #b7e5d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +138,15 @@
|
|||||||
|
|
||||||
.todo-item .ti-icon.i-approve {
|
.todo-item .ti-icon.i-approve {
|
||||||
background: #fff3e0;
|
background: #fff3e0;
|
||||||
color: var(--warning, #cf8a19);
|
color: var(--warning, #d97706);
|
||||||
}
|
}
|
||||||
.todo-item .ti-icon.i-pay {
|
.todo-item .ti-icon.i-pay {
|
||||||
background: var(--barley-yellow-soft, #fff7e8);
|
background: var(--barley-yellow-soft, #fffbeb);
|
||||||
color: #a67b20;
|
color: #a67b20;
|
||||||
}
|
}
|
||||||
.todo-item .ti-icon.i-alert {
|
.todo-item .ti-icon.i-alert {
|
||||||
background: #fdecec;
|
background: #fdecec;
|
||||||
color: var(--danger, #d14343);
|
color: var(--danger, #ef4444);
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-item .ti-body {
|
.todo-item .ti-body {
|
||||||
@@ -157,14 +157,14 @@
|
|||||||
.todo-item .ti-title {
|
.todo-item .ti-title {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-item .ti-desc {
|
.todo-item .ti-desc {
|
||||||
margin-top: 4rpx;
|
margin-top: 4rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@mixin hero-green {
|
@mixin hero-green {
|
||||||
background: linear-gradient(155deg, #14532d 0%, #16a34a 48%, #22c55e 100%);
|
background: linear-gradient(160deg, #f0fdf4 0%, #d1fae5 58%, #a7f3d0 100%);
|
||||||
color: #ffffff;
|
color: var(--header-fg, #065f46);
|
||||||
border-radius: 0 0 56rpx 56rpx;
|
border-radius: 0 0 56rpx 56rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
@mixin surface-card {
|
@mixin surface-card {
|
||||||
background: var(--surface, #ffffff);
|
background: var(--surface, #ffffff);
|
||||||
border: 1rpx solid var(--border, #dcebe4);
|
border: 1rpx solid var(--border, #d8f3e5);
|
||||||
border-radius: var(--radius-card, 28rpx);
|
border-radius: var(--radius-card, 28rpx);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
// 唯一 Token 源 — 品牌绿(青葱),禁止 web-admin #10b981
|
// 唯一 Token 源 — 方案 A「薄荷微风」:明亮清爽,勿用深墨绿顶栏
|
||||||
$brand-green: #16a34a;
|
// 主色 #22c55e;成功语义可用 #10b981,但主按钮勿单独改成 web-admin 主色链
|
||||||
$brand-green-dark: #15803d;
|
$brand-green: #22c55e;
|
||||||
$brand-green-soft: #f0fdf4;
|
$brand-green-dark: #16a34a;
|
||||||
$barley-yellow: #d6a343;
|
$brand-green-soft: #ecfdf5;
|
||||||
$barley-yellow-soft: #fff7e8;
|
$header-fg: #065f46;
|
||||||
$bg-page: #f5f8f7;
|
$barley-yellow: #fbbf24;
|
||||||
|
$barley-yellow-soft: #fffbeb;
|
||||||
|
$bg-page: #ffffff;
|
||||||
$surface: #ffffff;
|
$surface: #ffffff;
|
||||||
$text-primary: #1f2a26;
|
$text-primary: #0f291e;
|
||||||
$text-muted: #6b7b74;
|
$text-muted: #5f7a6d;
|
||||||
$border: #dcebe4;
|
$border: #d8f3e5;
|
||||||
$danger: #d14343;
|
$danger: #ef4444;
|
||||||
$warning: #cf8a19;
|
$warning: #d97706;
|
||||||
$success: #189b65;
|
$success: #10b981;
|
||||||
$radius-card: 28rpx;
|
$radius-card: 28rpx;
|
||||||
$radius-lg: 40rpx;
|
$radius-lg: 40rpx;
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ $radius-lg: 40rpx;
|
|||||||
--brand-green: #{$brand-green};
|
--brand-green: #{$brand-green};
|
||||||
--brand-green-dark: #{$brand-green-dark};
|
--brand-green-dark: #{$brand-green-dark};
|
||||||
--brand-green-soft: #{$brand-green-soft};
|
--brand-green-soft: #{$brand-green-soft};
|
||||||
|
--header-fg: #{$header-fg};
|
||||||
--barley-yellow: #{$barley-yellow};
|
--barley-yellow: #{$barley-yellow};
|
||||||
--barley-yellow-soft: #{$barley-yellow-soft};
|
--barley-yellow-soft: #{$barley-yellow-soft};
|
||||||
--bg-page: #{$bg-page};
|
--bg-page: #{$bg-page};
|
||||||
@@ -31,5 +34,5 @@ $radius-lg: 40rpx;
|
|||||||
--success: #{$success};
|
--success: #{$success};
|
||||||
--radius-card: #{$radius-card};
|
--radius-card: #{$radius-card};
|
||||||
--radius-lg: #{$radius-lg};
|
--radius-lg: #{$radius-lg};
|
||||||
--shadow-soft: 0 8rpx 32rpx rgba(21, 128, 61, 0.08);
|
--shadow-soft: 0 8rpx 32rpx rgba(22, 163, 74, 0.07);
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,48 @@
|
|||||||
/* Vant 4 CSS 变量 — 对齐品牌绿 Token,禁止 #10b981 */
|
/* Vant 4 CSS 变量 — 方案 A 薄荷微风 */
|
||||||
page {
|
page {
|
||||||
--van-primary-color: #16a34a;
|
--van-primary-color: #22c55e;
|
||||||
--van-success-color: #189b65;
|
--van-success-color: #10b981;
|
||||||
--van-danger-color: #d14343;
|
--van-danger-color: #ef4444;
|
||||||
--van-warning-color: #cf8a19;
|
--van-warning-color: #d97706;
|
||||||
|
|
||||||
--van-button-primary-background: #16a34a;
|
--van-button-primary-background: #22c55e;
|
||||||
--van-button-primary-border-color: #16a34a;
|
--van-button-primary-border-color: #22c55e;
|
||||||
|
|
||||||
--van-nav-bar-background: transparent;
|
--van-nav-bar-background: transparent;
|
||||||
--van-nav-bar-title-text-color: #ffffff;
|
--van-nav-bar-title-text-color: #065f46;
|
||||||
--van-nav-bar-icon-color: #ffffff;
|
--van-nav-bar-icon-color: #065f46;
|
||||||
|
|
||||||
--van-tabs-bottom-bar-color: #16a34a;
|
--van-tabs-bottom-bar-color: #22c55e;
|
||||||
--van-tag-primary-color: #16a34a;
|
--van-tag-primary-color: #22c55e;
|
||||||
--van-steps-active-color: #16a34a;
|
--van-steps-active-color: #22c55e;
|
||||||
|
|
||||||
--van-notice-bar-background: #f0fdf4;
|
--van-notice-bar-background: #ecfdf5;
|
||||||
--van-notice-bar-text-color: #15803d;
|
--van-notice-bar-text-color: #16a34a;
|
||||||
|
|
||||||
|
/* 表单 Cell / Field 更紧凑(选择框与输入行) */
|
||||||
|
--van-cell-vertical-padding: 8px;
|
||||||
|
--van-cell-horizontal-padding: 16px;
|
||||||
|
--van-cell-font-size: 14px;
|
||||||
|
--van-cell-line-height: 22px;
|
||||||
|
--van-field-input-text-color: #0f291e;
|
||||||
|
--van-field-placeholder-text-color: #5f7a6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选择类触发条(is-link / readonly):再压矮一点 */
|
||||||
|
.van-field.van-cell--clickable,
|
||||||
|
.van-field--readonly.van-cell--clickable {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-field.van-cell--clickable .van-field__control,
|
||||||
|
.van-field--readonly.van-cell--clickable .van-field__control {
|
||||||
|
font-size: 13px;
|
||||||
|
min-height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-field.van-cell--clickable .van-cell__right-icon,
|
||||||
|
.van-field--readonly.van-cell--clickable .van-cell__right-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/** 本地登录会话(无网络依赖,供 request / auth 共用,避免循环引用) */
|
||||||
|
|
||||||
|
const TOKEN_KEY = 'zkxw_mobile_token'
|
||||||
|
const REFRESH_TOKEN_KEY = 'zkxw_mobile_refresh_token'
|
||||||
|
const USER_KEY = 'zkxw_mobile_user'
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
try {
|
||||||
|
return uni.getStorageSync(TOKEN_KEY) || ''
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRefreshToken() {
|
||||||
|
try {
|
||||||
|
return uni.getStorageSync(REFRESH_TOKEN_KEY) || ''
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLoggedIn() {
|
||||||
|
return !!getToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUser() {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(USER_KEY)
|
||||||
|
return raw ? JSON.parse(raw) : null
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSession() {
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(TOKEN_KEY)
|
||||||
|
uni.removeStorageSync(REFRESH_TOKEN_KEY)
|
||||||
|
uni.removeStorageSync(USER_KEY)
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSession({ token, refreshToken, userInfo }) {
|
||||||
|
uni.setStorageSync(TOKEN_KEY, token || '')
|
||||||
|
uni.setStorageSync(REFRESH_TOKEN_KEY, refreshToken || '')
|
||||||
|
uni.setStorageSync(USER_KEY, JSON.stringify(userInfo || null))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新本地缓存的用户信息(保留 token)。 */
|
||||||
|
export function updateStoredUser(userInfo) {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(USER_KEY, JSON.stringify(userInfo || null))
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _reloginPending = false
|
||||||
|
|
||||||
|
function isOnLoginPage() {
|
||||||
|
try {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const cur = pages && pages[pages.length - 1]
|
||||||
|
const route = (cur && cur.route) || ''
|
||||||
|
return String(route).indexOf('pages/login') !== -1
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 失效:清本地会话并回到登录页(防抖,避免并发 401 连跳)。
|
||||||
|
* @param {string} [message]
|
||||||
|
*/
|
||||||
|
export function forceRelogin(message) {
|
||||||
|
if (_reloginPending) return
|
||||||
|
clearSession()
|
||||||
|
if (isOnLoginPage()) return
|
||||||
|
_reloginPending = true
|
||||||
|
const title =
|
||||||
|
(typeof message === 'string' && message.trim()) ||
|
||||||
|
'登录已失效,请重新登录'
|
||||||
|
uni.showToast({ title, icon: 'none', duration: 2000 })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/index',
|
||||||
|
complete() {
|
||||||
|
_reloginPending = false
|
||||||
|
},
|
||||||
|
fail() {
|
||||||
|
_reloginPending = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
@@ -1,65 +1,26 @@
|
|||||||
import { validateCredentials } from './validate-credentials.js'
|
import { validateCredentials } from './validate-credentials.js'
|
||||||
import { buildEncryptedLoginBody } from './login-rsa.js'
|
import { buildEncryptedLoginBody } from './login-rsa.js'
|
||||||
import { get, post } from './request.js'
|
import { get, post } from './request.js'
|
||||||
|
import {
|
||||||
|
getToken,
|
||||||
|
getRefreshToken,
|
||||||
|
isLoggedIn,
|
||||||
|
getUser,
|
||||||
|
clearSession,
|
||||||
|
saveSession,
|
||||||
|
updateStoredUser,
|
||||||
|
forceRelogin
|
||||||
|
} from './auth-session.js'
|
||||||
|
|
||||||
export { validateCredentials }
|
export { validateCredentials }
|
||||||
|
export {
|
||||||
const TOKEN_KEY = 'zkxw_mobile_token'
|
getToken,
|
||||||
const REFRESH_TOKEN_KEY = 'zkxw_mobile_refresh_token'
|
getRefreshToken,
|
||||||
const USER_KEY = 'zkxw_mobile_user'
|
isLoggedIn,
|
||||||
|
getUser,
|
||||||
export function getToken() {
|
clearSession,
|
||||||
try {
|
updateStoredUser,
|
||||||
return uni.getStorageSync(TOKEN_KEY) || ''
|
forceRelogin
|
||||||
} catch (e) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRefreshToken() {
|
|
||||||
try {
|
|
||||||
return uni.getStorageSync(REFRESH_TOKEN_KEY) || ''
|
|
||||||
} catch (e) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLoggedIn() {
|
|
||||||
return !!getToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUser() {
|
|
||||||
try {
|
|
||||||
const raw = uni.getStorageSync(USER_KEY)
|
|
||||||
return raw ? JSON.parse(raw) : null
|
|
||||||
} catch (e) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSession() {
|
|
||||||
try {
|
|
||||||
uni.removeStorageSync(TOKEN_KEY)
|
|
||||||
uni.removeStorageSync(REFRESH_TOKEN_KEY)
|
|
||||||
uni.removeStorageSync(USER_KEY)
|
|
||||||
} catch (e) {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSession({ token, refreshToken, userInfo }) {
|
|
||||||
uni.setStorageSync(TOKEN_KEY, token || '')
|
|
||||||
uni.setStorageSync(REFRESH_TOKEN_KEY, refreshToken || '')
|
|
||||||
uni.setStorageSync(USER_KEY, JSON.stringify(userInfo || null))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 更新本地缓存的用户信息(保留 token)。 */
|
|
||||||
export function updateStoredUser(userInfo) {
|
|
||||||
try {
|
|
||||||
uni.setStorageSync(USER_KEY, JSON.stringify(userInfo || null))
|
|
||||||
} catch (e) {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* 微信公众平台须配置 request / 业务域名:www.chengyantek.cn(HTTPS,安全组放行 443)
|
* 微信公众平台须配置 request / 业务域名:www.chengyantek.cn(HTTPS,安全组放行 443)
|
||||||
*/
|
*/
|
||||||
const USE_REMOTE_TEST = false
|
const USE_REMOTE_TEST = true
|
||||||
|
|
||||||
/** 本机联调 */
|
/** 本机联调 */
|
||||||
const LOCAL_API_BASE_URL = 'http://127.0.0.1:8000/api'
|
const LOCAL_API_BASE_URL = 'http://127.0.0.1:8000/api'
|
||||||
|
|||||||
@@ -2,16 +2,36 @@
|
|||||||
* 主页宫格图标配色:每项可自定义 iconBg / iconFg。
|
* 主页宫格图标配色:每项可自定义 iconBg / iconFg。
|
||||||
* 未配置时按 icon 名回退到此处默认色对。
|
* 未配置时按 icon 名回退到此处默认色对。
|
||||||
*/
|
*/
|
||||||
|
/** OA 宫格 PNG 自带颜色,不套色底 */
|
||||||
|
const OA_PNG_ICONS = new Set([
|
||||||
|
'approve',
|
||||||
|
'oa-drafts',
|
||||||
|
'oa-pending',
|
||||||
|
'oa-done',
|
||||||
|
'oa-mine',
|
||||||
|
'oa-cc'
|
||||||
|
])
|
||||||
|
|
||||||
export const MENU_ICON_COLOR_DEFAULTS = {
|
export const MENU_ICON_COLOR_DEFAULTS = {
|
||||||
approve: { iconBg: '#ffe4e6', iconFg: '#e11d48' },
|
approve: { iconBg: 'transparent', iconFg: '' },
|
||||||
|
'oa-drafts': { iconBg: 'transparent', iconFg: '' },
|
||||||
|
'oa-pending': { iconBg: 'transparent', iconFg: '' },
|
||||||
|
'oa-done': { iconBg: 'transparent', iconFg: '' },
|
||||||
|
'oa-mine': { iconBg: 'transparent', iconFg: '' },
|
||||||
|
'oa-cc': { iconBg: 'transparent', iconFg: '' },
|
||||||
'orders-o': { iconBg: '#eef2f1', iconFg: '#3d524a' },
|
'orders-o': { iconBg: '#eef2f1', iconFg: '#3d524a' },
|
||||||
'apps-o': { iconBg: '#eef2f1', iconFg: '#3d524a' }
|
'apps-o': { iconBg: '#eef2f1', iconFg: '#3d524a' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMenuIconColors(menu) {
|
export function resolveMenuIconColors(menu) {
|
||||||
const icon = String(menu?.icon || '').trim()
|
const icon = String(menu?.icon || '').trim()
|
||||||
|
// OA PNG 自带色:忽略接口下发的色底,避免灰底盖住图标
|
||||||
|
if (OA_PNG_ICONS.has(icon)) {
|
||||||
|
return { iconBg: 'transparent', iconFg: '' }
|
||||||
|
}
|
||||||
const fallback = MENU_ICON_COLOR_DEFAULTS[icon] || {}
|
const fallback = MENU_ICON_COLOR_DEFAULTS[icon] || {}
|
||||||
const iconBg = String(menu?.iconBg || menu?.icon_bg || fallback.iconBg || '').trim()
|
const iconBg = String(menu?.iconBg || menu?.icon_bg || fallback.iconBg || '').trim()
|
||||||
const iconFg = String(menu?.iconFg || menu?.icon_fg || fallback.iconFg || '').trim()
|
const iconFg = String(menu?.iconFg || menu?.icon_fg || fallback.iconFg || '').trim()
|
||||||
return { iconBg, iconFg }
|
return { iconBg, iconFg }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,42 @@
|
|||||||
import { API_BASE_URL, REQUEST_TIMEOUT_MS } from './config.js'
|
import { API_BASE_URL, REQUEST_TIMEOUT_MS } from './config.js'
|
||||||
|
import { forceRelogin } from './auth-session.js'
|
||||||
|
|
||||||
|
/** 这些接口的业务 code=401 表示凭据错误等,不是会话过期 */
|
||||||
|
const AUTH_EXEMPT_PATHS = [
|
||||||
|
'/web/auth/login',
|
||||||
|
'/web/auth/login-rsa-public-key',
|
||||||
|
'/web/auth/refresh-token',
|
||||||
|
'/mini/auth/wechat-login'
|
||||||
|
]
|
||||||
|
|
||||||
|
function isAuthExemptPath(path) {
|
||||||
|
const p = String(path || '')
|
||||||
|
for (let i = 0; i < AUTH_EXEMPT_PATHS.length; i++) {
|
||||||
|
if (p.indexOf(AUTH_EXEMPT_PATHS[i]) !== -1) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBearerToken(header) {
|
||||||
|
const auth = (header && header.Authorization) || ''
|
||||||
|
return /^Bearer\s+\S+/i.test(String(auth))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 401,或已带 token 的业务 code=401 → 强制重新登录。
|
||||||
|
* 登录接口豁免,避免「账号密码错误」被当成会话失效。
|
||||||
|
*/
|
||||||
|
function maybeForceRelogin(options, httpStatus, body) {
|
||||||
|
if (isAuthExemptPath(options && options.url)) return
|
||||||
|
const bizCode = body && body.code
|
||||||
|
const msg =
|
||||||
|
(body && typeof body.message === 'string' && body.message.trim()) ||
|
||||||
|
(body && typeof body.detail === 'string' && body.detail.trim()) ||
|
||||||
|
''
|
||||||
|
if (httpStatus === 401 || (bizCode === 401 && hasBearerToken(options && options.header))) {
|
||||||
|
forceRelogin(msg || '登录已失效,请重新登录')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析后端统一响应。成功返回 data;失败抛 Error(message 为业务文案)。
|
* 解析后端统一响应。成功返回 data;失败抛 Error(message 为业务文案)。
|
||||||
@@ -41,6 +79,7 @@ export function request(options) {
|
|||||||
{ 'Content-Type': 'application/json' },
|
{ 'Content-Type': 'application/json' },
|
||||||
options.header || {}
|
options.header || {}
|
||||||
)
|
)
|
||||||
|
const reqOpts = { url: options.url, header }
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.request({
|
uni.request({
|
||||||
@@ -51,13 +90,16 @@ export function request(options) {
|
|||||||
timeout: REQUEST_TIMEOUT_MS,
|
timeout: REQUEST_TIMEOUT_MS,
|
||||||
success(res) {
|
success(res) {
|
||||||
try {
|
try {
|
||||||
|
const body = res.data
|
||||||
|
maybeForceRelogin(reqOpts, res.statusCode, body)
|
||||||
|
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
resolve(unwrapResponse(res.data, res.statusCode))
|
resolve(unwrapResponse(body, res.statusCode))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// HTTP 非 2xx:若 body 仍是业务包,优先展示 message
|
// HTTP 非 2xx:若 body 仍是业务包,优先展示 message
|
||||||
try {
|
try {
|
||||||
resolve(unwrapResponse(res.data, res.statusCode))
|
resolve(unwrapResponse(body, res.statusCode))
|
||||||
} catch (bizErr) {
|
} catch (bizErr) {
|
||||||
reject(bizErr)
|
reject(bizErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ import './styles/oa-form.css'
|
|||||||
import { registerOaComponents } from './oa/register-components'
|
import { registerOaComponents } from './oa/register-components'
|
||||||
import { captureTokenFromUrl } from './utils/auth'
|
import { captureTokenFromUrl } from './utils/auth'
|
||||||
|
|
||||||
// 品牌绿主色(勿用 web-admin #10b981)
|
// 品牌绿主色 — 方案 A 薄荷微风
|
||||||
document.documentElement.style.setProperty('--van-primary-color', '#16a34a')
|
document.documentElement.style.setProperty('--van-primary-color', '#22c55e')
|
||||||
|
document.documentElement.style.setProperty('--van-success-color', '#10b981')
|
||||||
|
document.documentElement.style.setProperty('--van-danger-color', '#ef4444')
|
||||||
|
document.documentElement.style.setProperty('--van-warning-color', '#d97706')
|
||||||
|
// 表单 Cell / Field 更紧凑
|
||||||
|
document.documentElement.style.setProperty('--van-cell-vertical-padding', '8px')
|
||||||
|
document.documentElement.style.setProperty('--van-cell-line-height', '22px')
|
||||||
|
document.documentElement.style.setProperty('--van-cell-font-size', '14px')
|
||||||
|
|
||||||
// 须在路由/组件发请求前捕获小程序传入的 ?token=
|
// 须在路由/组件发请求前捕获小程序传入的 ?token=
|
||||||
captureTokenFromUrl()
|
captureTokenFromUrl()
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<div class="_fc-date-picker">
|
||||||
|
<van-field
|
||||||
|
ref="fieldRef"
|
||||||
|
:model-value="displayValue"
|
||||||
|
:placeholder="placeholderText"
|
||||||
|
:disabled="disabled"
|
||||||
|
readonly
|
||||||
|
:border="false"
|
||||||
|
is-link
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
|
<template v-if="clearable && displayValue" #right-icon>
|
||||||
|
<i class="van-badge__wrapper van-icon van-icon-clear van-field__clear" @click.stop="clear" />
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
<van-popup v-model:show="show" round position="bottom">
|
||||||
|
<!-- 仅日期 / 年 / 月 -->
|
||||||
|
<van-date-picker
|
||||||
|
v-if="!isDateTime"
|
||||||
|
v-model="dateParts"
|
||||||
|
:title="pickerTitle"
|
||||||
|
:columns-type="dateColumnsType"
|
||||||
|
:min-date="minDateObj"
|
||||||
|
:max-date="maxDateObj"
|
||||||
|
@confirm="onDateConfirm"
|
||||||
|
@cancel="show = false"
|
||||||
|
/>
|
||||||
|
<!-- 日期时间:日期 Tab + 时间 Tab -->
|
||||||
|
<van-picker-group
|
||||||
|
v-else
|
||||||
|
:title="pickerTitle"
|
||||||
|
:tabs="['选择日期', '选择时间']"
|
||||||
|
next-step-text="下一步"
|
||||||
|
@confirm="onDateTimeConfirm"
|
||||||
|
@cancel="show = false"
|
||||||
|
>
|
||||||
|
<van-date-picker
|
||||||
|
v-model="dateParts"
|
||||||
|
:columns-type="['year', 'month', 'day']"
|
||||||
|
:min-date="minDateObj"
|
||||||
|
:max-date="maxDateObj"
|
||||||
|
/>
|
||||||
|
<van-time-picker v-model="timeParts" :columns-type="timeColumnsType" />
|
||||||
|
</van-picker-group>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'fcDatePicker' })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: [String, Number], default: '' },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
clearable: { type: Boolean, default: true },
|
||||||
|
placeholder: { type: String, default: '' },
|
||||||
|
/** Element Plus / 设计器:date | datetime | year | month | … */
|
||||||
|
type: { type: String, default: 'date' },
|
||||||
|
format: { type: String, default: '' },
|
||||||
|
valueFormat: { type: String, default: '' },
|
||||||
|
minDate: { type: [String, Date], default: undefined },
|
||||||
|
maxDate: { type: [String, Date], default: undefined },
|
||||||
|
formCreateInject: { type: Object, default: null }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change', 'fc.el'])
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
const fieldRef = ref(null)
|
||||||
|
const dateParts = ref(['2026', '01', '01'])
|
||||||
|
const timeParts = ref(['00', '00'])
|
||||||
|
|
||||||
|
const pickerType = computed(() => String(props.type || 'date').toLowerCase())
|
||||||
|
|
||||||
|
const isDateTime = computed(() => {
|
||||||
|
const t = pickerType.value
|
||||||
|
return t === 'datetime' || t === 'datetimerange'
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 展示/落库用的格式串:优先 valueFormat,其次 format,再按 type 默认 */
|
||||||
|
const effectiveFormat = computed(() => {
|
||||||
|
const explicit = String(props.valueFormat || props.format || '').trim()
|
||||||
|
if (explicit) return explicit
|
||||||
|
switch (pickerType.value) {
|
||||||
|
case 'year':
|
||||||
|
return 'YYYY'
|
||||||
|
case 'month':
|
||||||
|
return 'YYYY-MM'
|
||||||
|
case 'datetime':
|
||||||
|
case 'datetimerange':
|
||||||
|
// 与请假/加班套件及 Web 常用 valueFormat 对齐;需到秒时在 schema 中显式配置 ss
|
||||||
|
return 'YYYY-MM-DD HH:mm'
|
||||||
|
default:
|
||||||
|
return 'YYYY-MM-DD'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const includeSeconds = computed(() => {
|
||||||
|
const f = effectiveFormat.value
|
||||||
|
// Element / dayjs:ss;未写秒时(如 YYYY-MM-DD HH:mm)仅到分
|
||||||
|
return f.includes('ss') || f.includes('SS')
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeColumnsType = computed(() =>
|
||||||
|
includeSeconds.value ? ['hour', 'minute', 'second'] : ['hour', 'minute']
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateColumnsType = computed(() => {
|
||||||
|
switch (pickerType.value) {
|
||||||
|
case 'year':
|
||||||
|
return ['year']
|
||||||
|
case 'month':
|
||||||
|
return ['year', 'month']
|
||||||
|
default:
|
||||||
|
return ['year', 'month', 'day']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const placeholderText = computed(() => {
|
||||||
|
if (props.placeholder) return props.placeholder
|
||||||
|
return isDateTime.value ? '请选择日期时间' : '请选择日期'
|
||||||
|
})
|
||||||
|
|
||||||
|
const pickerTitle = computed(() => (isDateTime.value ? '选择日期时间' : '选择日期'))
|
||||||
|
|
||||||
|
const displayValue = computed(() => {
|
||||||
|
const v = props.modelValue
|
||||||
|
if (v == null || v === '') return ''
|
||||||
|
return String(v)
|
||||||
|
})
|
||||||
|
|
||||||
|
function toDateObj(v) {
|
||||||
|
if (v == null || v === '') return undefined
|
||||||
|
if (v instanceof Date && !Number.isNaN(v.getTime())) return v
|
||||||
|
const s = String(v).trim().replace(/-/g, '/')
|
||||||
|
const d = new Date(s)
|
||||||
|
return Number.isNaN(d.getTime()) ? undefined : d
|
||||||
|
}
|
||||||
|
|
||||||
|
const minDateObj = computed(() => toDateObj(props.minDate))
|
||||||
|
const maxDateObj = computed(() => toDateObj(props.maxDate))
|
||||||
|
|
||||||
|
function pad2(n) {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowParts() {
|
||||||
|
const d = new Date()
|
||||||
|
return {
|
||||||
|
date: [String(d.getFullYear()), pad2(d.getMonth() + 1), pad2(d.getDate())],
|
||||||
|
time: includeSeconds.value
|
||||||
|
? [pad2(d.getHours()), pad2(d.getMinutes()), pad2(d.getSeconds())]
|
||||||
|
: [pad2(d.getHours()), pad2(d.getMinutes())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从已有值解析出 dateParts / timeParts */
|
||||||
|
function parseModelValue(raw) {
|
||||||
|
const fallback = nowParts()
|
||||||
|
if (raw == null || raw === '') return fallback
|
||||||
|
const s = String(raw).trim()
|
||||||
|
// 兼容 ISO:2026-08-12T14:30:00
|
||||||
|
const normalized = s.replace('T', ' ')
|
||||||
|
const m = normalized.match(
|
||||||
|
/^(\d{4})(?:[-/](\d{1,2}))?(?:[-/](\d{1,2}))?(?:\s+(\d{1,2})(?::(\d{1,2})(?::(\d{1,2}))?)?)?/
|
||||||
|
)
|
||||||
|
if (!m) return fallback
|
||||||
|
const y = m[1]
|
||||||
|
const mo = m[2] != null ? pad2(m[2]) : '01'
|
||||||
|
const day = m[3] != null ? pad2(m[3]) : '01'
|
||||||
|
const hh = m[4] != null ? pad2(m[4]) : '00'
|
||||||
|
const mm = m[5] != null ? pad2(m[5]) : '00'
|
||||||
|
const ss = m[6] != null ? pad2(m[6]) : '00'
|
||||||
|
return {
|
||||||
|
date: [y, mo, day],
|
||||||
|
time: includeSeconds.value ? [hh, mm, ss] : [hh, mm]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOutput(dateArr, timeArr) {
|
||||||
|
const t = pickerType.value
|
||||||
|
const y = dateArr[0] || '1970'
|
||||||
|
const mo = dateArr[1] || '01'
|
||||||
|
const day = dateArr[2] || '01'
|
||||||
|
const hh = timeArr?.[0] || '00'
|
||||||
|
const mm = timeArr?.[1] || '00'
|
||||||
|
const ss = timeArr?.[2] || '00'
|
||||||
|
|
||||||
|
if (t === 'year') return y
|
||||||
|
if (t === 'month') return `${y}-${mo}`
|
||||||
|
|
||||||
|
if (isDateTime.value) {
|
||||||
|
const f = effectiveFormat.value
|
||||||
|
// 常见 valueFormat 直出,避免引入 dayjs
|
||||||
|
if (f === 'YYYY-MM-DD HH:mm' || (!includeSeconds.value && /HH:mm/.test(f) && !/[sS]/.test(f))) {
|
||||||
|
return `${y}-${mo}-${day} ${hh}:${mm}`
|
||||||
|
}
|
||||||
|
if (f === 'YYYY-MM-DD HH:mm:ss' || includeSeconds.value) {
|
||||||
|
return `${y}-${mo}-${day} ${hh}:${mm}:${ss}`
|
||||||
|
}
|
||||||
|
// 其它自定义:尽量按是否含时间拼装
|
||||||
|
if (/HH|hh|mm|ss/.test(f)) {
|
||||||
|
return includeSeconds.value
|
||||||
|
? `${y}-${mo}-${day} ${hh}:${mm}:${ss}`
|
||||||
|
: `${y}-${mo}-${day} ${hh}:${mm}`
|
||||||
|
}
|
||||||
|
return `${y}-${mo}-${day} ${hh}:${mm}:${ss}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${y}-${mo}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitValue(val) {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
emit('change', val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
if (props.disabled) return
|
||||||
|
const parsed = parseModelValue(props.modelValue)
|
||||||
|
dateParts.value = parsed.date.slice(0, dateColumnsType.value.length)
|
||||||
|
timeParts.value = parsed.time.slice(0, timeColumnsType.value.length)
|
||||||
|
show.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDateConfirm({ selectedValues }) {
|
||||||
|
const parts = selectedValues || dateParts.value
|
||||||
|
emitValue(formatOutput(parts, ['00', '00', '00']))
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDateTimeConfirm() {
|
||||||
|
// 依赖 v-model 已同步的 dateParts / timeParts(滚动过程中已更新)
|
||||||
|
emitValue(formatOutput(dateParts.value, timeParts.value))
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
emitValue('')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.modelValue, includeSeconds.value],
|
||||||
|
() => {
|
||||||
|
if (!show.value) return
|
||||||
|
const parsed = parseModelValue(props.modelValue)
|
||||||
|
dateParts.value = parsed.date
|
||||||
|
timeParts.value = parsed.time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
emit('fc.el', fieldRef.value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -23,38 +23,70 @@
|
|||||||
input-align="left"
|
input-align="left"
|
||||||
placeholder="搜索部门名称"
|
placeholder="搜索部门名称"
|
||||||
/>
|
/>
|
||||||
<div class="oa-pick__list">
|
<div class="oa-pick__list oa-dept-tree">
|
||||||
<template v-if="multiple">
|
<template v-if="multiple">
|
||||||
<van-checkbox-group v-model="pendingIds">
|
<van-checkbox-group v-model="pendingIds">
|
||||||
<van-cell
|
<div
|
||||||
v-for="d in filtered"
|
v-for="row in visibleRows"
|
||||||
:key="d.id"
|
:key="row.id"
|
||||||
clickable
|
class="oa-sheet-row oa-sheet-row--center"
|
||||||
:title="d.label"
|
:class="{ 'is-selected': pendingIds.includes(row.id) }"
|
||||||
@click="toggle(d.id)"
|
:style="{ paddingLeft: `${12 + row.depth * 16}px` }"
|
||||||
|
@click="toggle(row.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<button
|
||||||
<van-checkbox class="oa-pick__check" :name="d.id" @click.stop />
|
v-if="row.hasChildren"
|
||||||
</template>
|
type="button"
|
||||||
</van-cell>
|
class="oa-sheet-row__exp"
|
||||||
|
aria-label="展开或收起"
|
||||||
|
@click.stop="toggleExpand(row.id)"
|
||||||
|
>
|
||||||
|
<van-icon :name="isExpanded(row.id) ? 'arrow-down' : 'arrow'" />
|
||||||
|
</button>
|
||||||
|
<span v-else class="oa-sheet-row__exp oa-sheet-row__exp--spacer" />
|
||||||
|
<van-checkbox
|
||||||
|
class="oa-sheet-row__check"
|
||||||
|
:name="row.id"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ row.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<van-radio-group v-model="pendingSingle">
|
<van-radio-group v-model="pendingSingle">
|
||||||
<van-cell
|
<div
|
||||||
v-for="d in filtered"
|
v-for="row in visibleRows"
|
||||||
:key="d.id"
|
:key="row.id"
|
||||||
clickable
|
class="oa-sheet-row oa-sheet-row--center"
|
||||||
:title="d.label"
|
:class="{ 'is-selected': pendingSingle === row.id }"
|
||||||
@click="pendingSingle = d.id"
|
:style="{ paddingLeft: `${12 + row.depth * 16}px` }"
|
||||||
|
@click="pendingSingle = row.id"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<button
|
||||||
<van-radio class="oa-pick__check" :name="d.id" @click.stop />
|
v-if="row.hasChildren"
|
||||||
</template>
|
type="button"
|
||||||
</van-cell>
|
class="oa-sheet-row__exp"
|
||||||
|
aria-label="展开或收起"
|
||||||
|
@click.stop="toggleExpand(row.id)"
|
||||||
|
>
|
||||||
|
<van-icon :name="isExpanded(row.id) ? 'arrow-down' : 'arrow'" />
|
||||||
|
</button>
|
||||||
|
<span v-else class="oa-sheet-row__exp oa-sheet-row__exp--spacer" />
|
||||||
|
<van-radio
|
||||||
|
class="oa-sheet-row__check"
|
||||||
|
:name="row.id"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ row.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
<van-empty v-if="!loading && !filtered.length" description="暂无部门" />
|
<van-empty v-if="!loading && !visibleRows.length" description="暂无部门" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
@@ -62,9 +94,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import { fetchDeptTree } from '../../api/oa.js'
|
import { fetchDeptTree } from '../../api/oa.js'
|
||||||
|
import { buildPathLabelMapFromTreeRoots } from '../kit/tree-path.js'
|
||||||
import {
|
import {
|
||||||
formatIdNameDisplay,
|
formatIdNameDisplay,
|
||||||
normalizeIdNameModelValue,
|
normalizeIdNameModelValue,
|
||||||
@@ -73,8 +106,6 @@ import {
|
|||||||
|
|
||||||
defineOptions({ name: 'dept' })
|
defineOptions({ name: 'dept' })
|
||||||
|
|
||||||
const SEP = ' / '
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: [Array, Object], default: null },
|
modelValue: { type: [Array, Object], default: null },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
@@ -90,32 +121,86 @@ const keyword = ref('')
|
|||||||
const pendingIds = ref([])
|
const pendingIds = ref([])
|
||||||
const pendingSingle = ref('')
|
const pendingSingle = ref('')
|
||||||
const nameMap = ref(new Map())
|
const nameMap = ref(new Map())
|
||||||
const flatOptions = ref([])
|
const treeData = ref([])
|
||||||
|
const expandedIds = ref(new Set())
|
||||||
|
|
||||||
const multiple = computed(() => !!props.multiple)
|
const multiple = computed(() => !!props.multiple)
|
||||||
const normalized = computed(() =>
|
const normalized = computed(() =>
|
||||||
normalizeIdNameModelValue(props.modelValue, multiple.value)
|
normalizeIdNameModelValue(props.modelValue, multiple.value)
|
||||||
)
|
)
|
||||||
const displayText = computed(() => formatIdNameDisplay(normalized.value))
|
const displayText = computed(() => formatIdNameDisplay(normalized.value))
|
||||||
const filtered = computed(() => {
|
|
||||||
const q = keyword.value.trim().toLowerCase()
|
const filteredTree = computed(() => filterTree(treeData.value, keyword.value.trim().toLowerCase()))
|
||||||
if (!q) return flatOptions.value
|
|
||||||
return flatOptions.value.filter((d) => d.label.toLowerCase().includes(q))
|
const visibleRows = computed(() => {
|
||||||
|
const out = []
|
||||||
|
const walk = (nodes, depth) => {
|
||||||
|
for (const n of nodes || []) {
|
||||||
|
const kids = n.children || []
|
||||||
|
const hasChildren = kids.length > 0
|
||||||
|
out.push({
|
||||||
|
id: n.id,
|
||||||
|
label: n.label,
|
||||||
|
depth,
|
||||||
|
hasChildren
|
||||||
|
})
|
||||||
|
if (hasChildren && expandedIds.value.has(n.id)) walk(kids, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(filteredTree.value, 0)
|
||||||
|
return out
|
||||||
})
|
})
|
||||||
|
|
||||||
function flattenTree(nodes, ancestors = [], out = []) {
|
function mapTree(nodes) {
|
||||||
|
return (nodes || [])
|
||||||
|
.map((n) => {
|
||||||
|
const id = String(n.id || '').trim()
|
||||||
|
if (!id) return null
|
||||||
|
const label = String(n.deptName || n.name || id).trim() || id
|
||||||
|
const children = n.children?.length ? mapTree(n.children).filter(Boolean) : []
|
||||||
|
return { id, label, children }
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectExpandableIds(nodes, out = []) {
|
||||||
for (const n of nodes || []) {
|
for (const n of nodes || []) {
|
||||||
const id = String(n.id || '').trim()
|
if (n.children?.length) {
|
||||||
const name = String(n.deptName || n.name || id).trim()
|
out.push(n.id)
|
||||||
if (!id) continue
|
collectExpandableIds(n.children, out)
|
||||||
const path = [...ancestors, name]
|
}
|
||||||
const label = path.join(SEP)
|
|
||||||
out.push({ id, label, name: label })
|
|
||||||
if (n.children?.length) flattenTree(n.children, path, out)
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterTree(nodes, q) {
|
||||||
|
if (!q) return nodes
|
||||||
|
const out = []
|
||||||
|
for (const n of nodes || []) {
|
||||||
|
const kids = filterTree(n.children || [], q)
|
||||||
|
const selfHit = n.label.toLowerCase().includes(q)
|
||||||
|
if (selfHit || kids.length) {
|
||||||
|
out.push({ ...n, children: kids })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpanded(id) {
|
||||||
|
return expandedIds.value.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpand(id) {
|
||||||
|
const next = new Set(expandedIds.value)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
expandedIds.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandAllIn(nodes) {
|
||||||
|
expandedIds.value = new Set(collectExpandableIds(nodes))
|
||||||
|
}
|
||||||
|
|
||||||
function emitFromIds(ids) {
|
function emitFromIds(ids) {
|
||||||
const items = []
|
const items = []
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
@@ -146,8 +231,9 @@ function openPicker() {
|
|||||||
if (multiple.value) pendingIds.value = ids
|
if (multiple.value) pendingIds.value = ids
|
||||||
else pendingSingle.value = ids[0] || ''
|
else pendingSingle.value = ids[0] || ''
|
||||||
keyword.value = ''
|
keyword.value = ''
|
||||||
|
expandAllIn(treeData.value)
|
||||||
visible.value = true
|
visible.value = true
|
||||||
if (!flatOptions.value.length) loadDepts()
|
if (!treeData.value.length) loadDepts()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(id) {
|
function toggle(id) {
|
||||||
@@ -167,9 +253,8 @@ async function loadDepts() {
|
|||||||
try {
|
try {
|
||||||
const tree = await fetchDeptTree()
|
const tree = await fetchDeptTree()
|
||||||
const list = Array.isArray(tree) ? tree : tree?.list || []
|
const list = Array.isArray(tree) ? tree : tree?.list || []
|
||||||
const flat = flattenTree(list)
|
const mapped = mapTree(list)
|
||||||
const map = new Map()
|
const map = buildPathLabelMapFromTreeRoots(list, { nameKey: 'deptName' })
|
||||||
for (const d of flat) map.set(d.id, d.label)
|
|
||||||
const kept = Array.isArray(normalized.value)
|
const kept = Array.isArray(normalized.value)
|
||||||
? normalized.value
|
? normalized.value
|
||||||
: normalized.value
|
: normalized.value
|
||||||
@@ -179,18 +264,25 @@ async function loadDepts() {
|
|||||||
if (!map.has(x.id) && x.name) map.set(x.id, x.name)
|
if (!map.has(x.id) && x.name) map.set(x.id, x.name)
|
||||||
}
|
}
|
||||||
nameMap.value = map
|
nameMap.value = map
|
||||||
flatOptions.value = flat
|
treeData.value = mapped
|
||||||
|
expandAllIn(mapped)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e?.message || '加载部门失败')
|
showToast(e?.message || '加载部门失败')
|
||||||
nameMap.value = new Map()
|
nameMap.value = new Map()
|
||||||
flatOptions.value = []
|
treeData.value = []
|
||||||
|
expandedIds.value = new Set()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(keyword, (q) => {
|
||||||
|
const t = q.trim()
|
||||||
|
if (t) expandAllIn(filteredTree.value)
|
||||||
|
else expandAllIn(treeData.value)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDepts()
|
loadDepts()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -83,24 +83,33 @@
|
|||||||
shape="round"
|
shape="round"
|
||||||
label-align="left"
|
label-align="left"
|
||||||
input-align="left"
|
input-align="left"
|
||||||
placeholder="搜索姓名或手机号"
|
placeholder="搜索姓名、工号或手机号"
|
||||||
/>
|
/>
|
||||||
<div class="oa-flow-picker__list">
|
<div class="oa-flow-picker__list">
|
||||||
<van-checkbox-group v-model="pickerIds">
|
<van-checkbox-group v-model="pickerIds">
|
||||||
<van-cell
|
<div
|
||||||
v-for="u in filteredUsers"
|
v-for="u in filteredUsers"
|
||||||
:key="u.id"
|
:key="u.id"
|
||||||
clickable
|
class="oa-sheet-row"
|
||||||
|
:class="{ 'is-selected': pickerIds.includes(u.id) }"
|
||||||
@click="togglePicker(u.id)"
|
@click="togglePicker(u.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-checkbox
|
||||||
<van-checkbox class="oa-flow-picker__check" :name="u.id" @click.stop />
|
class="oa-sheet-row__check"
|
||||||
</template>
|
:name="u.id"
|
||||||
<template #title>
|
@click.stop
|
||||||
<span class="oa-flow-picker__name">{{ u.nickName || u.id }}</span>
|
/>
|
||||||
<span v-if="u.phoneNo" class="oa-flow-picker__phone">{{ u.phoneNo }}</span>
|
<div
|
||||||
</template>
|
class="oa-sheet-row__avatar"
|
||||||
</van-cell>
|
:style="{ background: avatarColor(u.nickName || u.id) }"
|
||||||
|
>
|
||||||
|
{{ shortName(u.nickName || u.id) }}
|
||||||
|
</div>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ u.nickName || u.id }}</div>
|
||||||
|
<div v-if="pickerSub(u)" class="oa-sheet-row__meta">{{ pickerSub(u) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
<van-empty v-if="!filteredUsers.length" description="暂无人员" />
|
<van-empty v-if="!filteredUsers.length" description="暂无人员" />
|
||||||
</div>
|
</div>
|
||||||
@@ -113,6 +122,8 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import OaPulseLoading from './OaPulseLoading.vue'
|
import OaPulseLoading from './OaPulseLoading.vue'
|
||||||
|
|
||||||
|
const AVATAR_COLORS = ['#3b82f6', '#22c55e', '#8b5cf6', '#f59e0b', '#06b6d4', '#ef4444']
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
steps: { type: Array, default: () => [] },
|
steps: { type: Array, default: () => [] },
|
||||||
resolved: { type: Boolean, default: true },
|
resolved: { type: Boolean, default: true },
|
||||||
@@ -141,12 +152,37 @@ const filteredUsers = computed(() => {
|
|||||||
String(u.phoneNo || '')
|
String(u.phoneNo || '')
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(q) ||
|
.includes(q) ||
|
||||||
|
String(u.userNo || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(q) ||
|
||||||
String(u.id || '')
|
String(u.id || '')
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(q)
|
.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) {
|
function isInitiatorSelect(s) {
|
||||||
const types =
|
const types =
|
||||||
Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
|
Array.isArray(s.assigneeTypes) && s.assigneeTypes.length
|
||||||
@@ -226,19 +262,19 @@ function confirmPicker() {
|
|||||||
padding: 14px 12px 8px;
|
padding: 14px 12px 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border, #dcebe4);
|
border: 1px solid var(--border, #d8f3e5);
|
||||||
}
|
}
|
||||||
.oa-flow-preview__title {
|
.oa-flow-preview__title {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
}
|
}
|
||||||
.oa-flow-preview__loading,
|
.oa-flow-preview__loading,
|
||||||
.oa-flow-preview__pending,
|
.oa-flow-preview__pending,
|
||||||
.oa-flow-preview__empty {
|
.oa-flow-preview__empty {
|
||||||
padding: 12px 4px 16px;
|
padding: 12px 4px 16px;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@@ -262,7 +298,7 @@ function confirmPicker() {
|
|||||||
height: 10px;
|
height: 10px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--brand-green, #16a34a);
|
background: var(--brand-green, #22c55e);
|
||||||
}
|
}
|
||||||
.oa-flow-dot.end {
|
.oa-flow-dot.end {
|
||||||
background: #94a3b8;
|
background: #94a3b8;
|
||||||
@@ -280,44 +316,52 @@ function confirmPicker() {
|
|||||||
}
|
}
|
||||||
.oa-flow-card__name-row {
|
.oa-flow-card__name-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.oa-flow-card__name {
|
.oa-flow-card__name {
|
||||||
|
flex-shrink: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
}
|
}
|
||||||
.oa-flow-badge {
|
.oa-flow-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
.oa-flow-badge.and {
|
.oa-flow-badge.and {
|
||||||
color: #15803d;
|
color: #22c55e;
|
||||||
background: #f0fdf4;
|
background: #ecfdf5;
|
||||||
}
|
}
|
||||||
.oa-flow-badge.or {
|
.oa-flow-badge.or {
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
background: #fff7e8;
|
background: #fff7e8;
|
||||||
}
|
}
|
||||||
.oa-flow-card__meta {
|
.oa-flow-card__meta {
|
||||||
color: var(--text-muted, #6b7b74);
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.oa-flow-card__sep {
|
.oa-flow-card__sep {
|
||||||
|
flex-shrink: 0;
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.oa-flow-card__placeholder {
|
.oa-flow-card__placeholder {
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.oa-flow-card__err {
|
.oa-flow-card__err {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #d14343;
|
color: #ef4444;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.oa-flow-card__select {
|
.oa-flow-card__select {
|
||||||
@@ -347,9 +391,9 @@ function confirmPicker() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #f5f8f7;
|
background: #ffffff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
}
|
}
|
||||||
.oa-person-chip__dot {
|
.oa-person-chip__dot {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -358,7 +402,7 @@ function confirmPicker() {
|
|||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--brand-green, #16a34a);
|
background: var(--brand-green, #22c55e);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
@@ -382,7 +426,7 @@ function confirmPicker() {
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -397,32 +441,16 @@ function confirmPicker() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid var(--border, #dcebe4);
|
border-bottom: 1px solid var(--border, #d8f3e5);
|
||||||
}
|
}
|
||||||
.oa-flow-picker__ok {
|
.oa-flow-picker__ok {
|
||||||
color: var(--brand-green, #16a34a);
|
color: var(--brand-green, #22c55e);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.oa-flow-picker__list {
|
.oa-flow-picker__list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
background: #fff;
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<OaDeptSelect
|
<div class="oa-owner-field" :class="{ 'is-locked': effectiveDisabled }">
|
||||||
:model-value="modelValue"
|
<OaDeptSelect
|
||||||
:disabled="effectiveDisabled"
|
:model-value="modelValue"
|
||||||
:multiple="false"
|
:disabled="effectiveDisabled"
|
||||||
:form-create-inject="formCreateInject"
|
:multiple="false"
|
||||||
@update:model-value="onUpdate"
|
:form-create-inject="formCreateInject"
|
||||||
@change="onChange"
|
@update:model-value="onUpdate"
|
||||||
/>
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<OaPersonSelect
|
<div class="oa-owner-field" :class="{ 'is-locked': effectiveDisabled }">
|
||||||
:model-value="modelValue"
|
<OaPersonSelect
|
||||||
:disabled="effectiveDisabled"
|
:model-value="modelValue"
|
||||||
:multiple="false"
|
:disabled="effectiveDisabled"
|
||||||
:form-create-inject="formCreateInject"
|
:multiple="false"
|
||||||
@update:model-value="onUpdate"
|
:form-create-inject="formCreateInject"
|
||||||
@change="onChange"
|
@update:model-value="onUpdate"
|
||||||
/>
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -21,39 +21,61 @@
|
|||||||
shape="round"
|
shape="round"
|
||||||
label-align="left"
|
label-align="left"
|
||||||
input-align="left"
|
input-align="left"
|
||||||
placeholder="搜索姓名或工号"
|
placeholder="搜索姓名、工号或手机号"
|
||||||
/>
|
/>
|
||||||
<div class="oa-pick__list">
|
<div class="oa-pick__list">
|
||||||
<template v-if="multiple">
|
<template v-if="multiple">
|
||||||
<van-checkbox-group v-model="pendingIds">
|
<van-checkbox-group v-model="pendingIds">
|
||||||
<van-cell
|
<div
|
||||||
v-for="u in filtered"
|
v-for="u in filtered"
|
||||||
:key="u.id"
|
:key="u.id"
|
||||||
clickable
|
class="oa-sheet-row"
|
||||||
:title="u.label"
|
:class="{ 'is-selected': pendingIds.includes(u.id) }"
|
||||||
:label="u.sub || undefined"
|
|
||||||
@click="toggle(u.id)"
|
@click="toggle(u.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-checkbox
|
||||||
<van-checkbox class="oa-pick__check" :name="u.id" @click.stop />
|
class="oa-sheet-row__check"
|
||||||
</template>
|
:name="u.id"
|
||||||
</van-cell>
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="oa-sheet-row__avatar"
|
||||||
|
:style="{ background: avatarColor(u.label) }"
|
||||||
|
>
|
||||||
|
{{ shortName(u.label) }}
|
||||||
|
</div>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ u.label }}</div>
|
||||||
|
<div v-if="u.sub" class="oa-sheet-row__meta">{{ u.sub }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<van-radio-group v-model="pendingSingle">
|
<van-radio-group v-model="pendingSingle">
|
||||||
<van-cell
|
<div
|
||||||
v-for="u in filtered"
|
v-for="u in filtered"
|
||||||
:key="u.id"
|
:key="u.id"
|
||||||
clickable
|
class="oa-sheet-row"
|
||||||
:title="u.label"
|
:class="{ 'is-selected': pendingSingle === u.id }"
|
||||||
:label="u.sub || undefined"
|
|
||||||
@click="pendingSingle = u.id"
|
@click="pendingSingle = u.id"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-radio
|
||||||
<van-radio class="oa-pick__check" :name="u.id" @click.stop />
|
class="oa-sheet-row__check"
|
||||||
</template>
|
:name="u.id"
|
||||||
</van-cell>
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="oa-sheet-row__avatar"
|
||||||
|
:style="{ background: avatarColor(u.label) }"
|
||||||
|
>
|
||||||
|
{{ shortName(u.label) }}
|
||||||
|
</div>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ u.label }}</div>
|
||||||
|
<div v-if="u.sub" class="oa-sheet-row__meta">{{ u.sub }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
<van-empty v-if="!loading && !filtered.length" description="暂无人员" />
|
<van-empty v-if="!loading && !filtered.length" description="暂无人员" />
|
||||||
@@ -75,6 +97,8 @@ import {
|
|||||||
|
|
||||||
defineOptions({ name: 'person' })
|
defineOptions({ name: 'person' })
|
||||||
|
|
||||||
|
const AVATAR_COLORS = ['#3b82f6', '#22c55e', '#8b5cf6', '#f59e0b', '#06b6d4', '#ef4444']
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: [Array, Object], default: null },
|
modelValue: { type: [Array, Object], default: null },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
@@ -103,11 +127,26 @@ const filtered = computed(() => {
|
|||||||
return userOptions.value.filter(
|
return userOptions.value.filter(
|
||||||
(u) =>
|
(u) =>
|
||||||
u.label.toLowerCase().includes(q) ||
|
u.label.toLowerCase().includes(q) ||
|
||||||
u.sub.toLowerCase().includes(q) ||
|
String(u.sub || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(q) ||
|
||||||
u.id.toLowerCase().includes(q)
|
u.id.toLowerCase().includes(q)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 emitFromIds(ids) {
|
function emitFromIds(ids) {
|
||||||
const items = []
|
const items = []
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
@@ -176,13 +215,11 @@ async function loadUsers() {
|
|||||||
if (!id) continue
|
if (!id) continue
|
||||||
const nick = String(u.nickName || '').trim()
|
const nick = String(u.nickName || '').trim()
|
||||||
const userNo = String(u.userNo || '').trim()
|
const userNo = String(u.userNo || '').trim()
|
||||||
|
const phone = String(u.phoneNo || '').trim()
|
||||||
const label = nick || userNo || id
|
const label = nick || userNo || id
|
||||||
|
const sub = phone || (userNo && userNo !== label ? userNo : '')
|
||||||
map.set(id, label)
|
map.set(id, label)
|
||||||
opts.push({
|
opts.push({ id, label, sub })
|
||||||
id,
|
|
||||||
label,
|
|
||||||
sub: nick && userNo && nick !== userNo ? userNo : ''
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const kept = Array.isArray(normalized.value)
|
const kept = Array.isArray(normalized.value)
|
||||||
? normalized.value
|
? normalized.value
|
||||||
|
|||||||
@@ -26,34 +26,44 @@
|
|||||||
<div class="oa-pick__list">
|
<div class="oa-pick__list">
|
||||||
<template v-if="multiple">
|
<template v-if="multiple">
|
||||||
<van-checkbox-group v-model="pendingIds">
|
<van-checkbox-group v-model="pendingIds">
|
||||||
<van-cell
|
<div
|
||||||
v-for="p in filtered"
|
v-for="p in filtered"
|
||||||
:key="p.id"
|
:key="p.id"
|
||||||
clickable
|
class="oa-sheet-row"
|
||||||
:title="p.label"
|
:class="{ 'is-selected': pendingIds.includes(p.id) }"
|
||||||
:label="p.sub || undefined"
|
|
||||||
@click="toggle(p.id)"
|
@click="toggle(p.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-checkbox
|
||||||
<van-checkbox class="oa-pick__check" :name="p.id" @click.stop />
|
class="oa-sheet-row__check"
|
||||||
</template>
|
:name="p.id"
|
||||||
</van-cell>
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ p.label }}</div>
|
||||||
|
<div v-if="p.sub" class="oa-sheet-row__meta">{{ p.sub }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<van-radio-group v-model="pendingSingle">
|
<van-radio-group v-model="pendingSingle">
|
||||||
<van-cell
|
<div
|
||||||
v-for="p in filtered"
|
v-for="p in filtered"
|
||||||
:key="p.id"
|
:key="p.id"
|
||||||
clickable
|
class="oa-sheet-row"
|
||||||
:title="p.label"
|
:class="{ 'is-selected': pendingSingle === p.id }"
|
||||||
:label="p.sub || undefined"
|
|
||||||
@click="pendingSingle = p.id"
|
@click="pendingSingle = p.id"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-radio
|
||||||
<van-radio class="oa-pick__check" :name="p.id" @click.stop />
|
class="oa-sheet-row__check"
|
||||||
</template>
|
:name="p.id"
|
||||||
</van-cell>
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ p.label }}</div>
|
||||||
|
<div v-if="p.sub" class="oa-sheet-row__meta">{{ p.sub }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
<van-empty v-if="!loading && !filtered.length" description="暂无职位" />
|
<van-empty v-if="!loading && !filtered.length" description="暂无职位" />
|
||||||
@@ -103,7 +113,9 @@ const filtered = computed(() => {
|
|||||||
return postOptions.value.filter(
|
return postOptions.value.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.label.toLowerCase().includes(q) ||
|
p.label.toLowerCase().includes(q) ||
|
||||||
p.sub.toLowerCase().includes(q) ||
|
String(p.sub || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(q) ||
|
||||||
p.id.toLowerCase().includes(q)
|
p.id.toLowerCase().includes(q)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ defineProps({
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--brand-green, #16a34a);
|
background: var(--brand-green, #22c55e);
|
||||||
animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.oa-pulse-loading__dot:nth-child(2) {
|
.oa-pulse-loading__dot:nth-child(2) {
|
||||||
@@ -46,7 +46,7 @@ defineProps({
|
|||||||
.oa-pulse-loading__text {
|
.oa-pulse-loading__text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
}
|
}
|
||||||
@keyframes oa-pulse-bounce {
|
@keyframes oa-pulse-bounce {
|
||||||
0%,
|
0%,
|
||||||
|
|||||||
@@ -31,17 +31,23 @@
|
|||||||
/>
|
/>
|
||||||
<div class="oa-recv__list">
|
<div class="oa-recv__list">
|
||||||
<van-checkbox-group v-model="pendingIds">
|
<van-checkbox-group v-model="pendingIds">
|
||||||
<van-cell
|
<div
|
||||||
v-for="a in filtered"
|
v-for="a in filtered"
|
||||||
:key="a.id"
|
:key="a.id"
|
||||||
clickable
|
class="oa-sheet-row"
|
||||||
:title="a.label"
|
:class="{ 'is-selected': pendingIds.includes(a.id) }"
|
||||||
@click="toggle(a.id)"
|
@click="toggle(a.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-checkbox
|
||||||
<van-checkbox class="oa-recv__check" :name="a.id" @click.stop />
|
class="oa-sheet-row__check"
|
||||||
</template>
|
:name="a.id"
|
||||||
</van-cell>
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div class="oa-sheet-row__main">
|
||||||
|
<div class="oa-sheet-row__title">{{ a.label }}</div>
|
||||||
|
<div v-if="a.sub" class="oa-sheet-row__meta">{{ a.sub }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
<van-empty v-if="!loading && !filtered.length" description="暂无收款账户" />
|
<van-empty v-if="!loading && !filtered.length" description="暂无收款账户" />
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +61,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import { fetchReceiveAccountOptions } from '../../api/oa.js'
|
import { fetchReceiveAccountOptions } from '../../api/oa.js'
|
||||||
import {
|
import {
|
||||||
|
ACCOUNT_TYPE_LABEL,
|
||||||
formatReceiveAccountLine,
|
formatReceiveAccountLine,
|
||||||
normalizeReceiveAccountModelValue,
|
normalizeReceiveAccountModelValue,
|
||||||
optionLabelFromRow,
|
optionLabelFromRow,
|
||||||
@@ -87,7 +94,13 @@ const summary = computed(() => {
|
|||||||
const filtered = computed(() => {
|
const filtered = computed(() => {
|
||||||
const kw = keyword.value.trim().toLowerCase()
|
const kw = keyword.value.trim().toLowerCase()
|
||||||
if (!kw) return accountOptions.value
|
if (!kw) return accountOptions.value
|
||||||
return accountOptions.value.filter((a) => a.label.toLowerCase().includes(kw))
|
return accountOptions.value.filter(
|
||||||
|
(a) =>
|
||||||
|
a.label.toLowerCase().includes(kw) ||
|
||||||
|
String(a.sub || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(kw)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function emitItems(items) {
|
function emitItems(items) {
|
||||||
@@ -128,7 +141,21 @@ async function loadOptions() {
|
|||||||
const snap = snapshotFromRow(row)
|
const snap = snapshotFromRow(row)
|
||||||
if (!snap.id) continue
|
if (!snap.id) continue
|
||||||
map.set(snap.id, snap)
|
map.set(snap.id, snap)
|
||||||
opts.push({ id: snap.id, label: optionLabelFromRow(snap) })
|
const typeLabel = ACCOUNT_TYPE_LABEL[snap.accountType] || snap.accountType
|
||||||
|
const label = [typeLabel, snap.accountName].filter(Boolean).join(' · ')
|
||||||
|
const subParts = [snap.accountNo]
|
||||||
|
if (
|
||||||
|
(snap.accountType === 'personal' || snap.accountType === 'corporate') &&
|
||||||
|
snap.bankName
|
||||||
|
) {
|
||||||
|
subParts.push(snap.bankName)
|
||||||
|
}
|
||||||
|
const sub = subParts.filter(Boolean).join(' · ')
|
||||||
|
opts.push({
|
||||||
|
id: snap.id,
|
||||||
|
label: label || optionLabelFromRow(snap),
|
||||||
|
sub
|
||||||
|
})
|
||||||
}
|
}
|
||||||
for (const x of normalized.value) {
|
for (const x of normalized.value) {
|
||||||
if (!map.has(x.id)) map.set(x.id, x)
|
if (!map.has(x.id)) map.set(x.id, x)
|
||||||
@@ -150,7 +177,7 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.oa-recv__detail {
|
.oa-recv__detail {
|
||||||
padding: 0 12px 8px;
|
padding: 0 12px 8px;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -66,21 +66,60 @@
|
|||||||
input-align="left"
|
input-align="left"
|
||||||
placeholder="搜索流程名 / 标题"
|
placeholder="搜索流程名 / 标题"
|
||||||
@search="loadCandidates"
|
@search="loadCandidates"
|
||||||
|
@clear="loadCandidates"
|
||||||
/>
|
/>
|
||||||
<div class="oa-rel__picker">
|
<div class="oa-rel__picker">
|
||||||
<van-checkbox-group v-model="pendingIds">
|
<van-checkbox-group v-model="pendingIds">
|
||||||
<van-cell
|
<div
|
||||||
v-for="row in candidates"
|
v-for="row in candidates"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
clickable
|
class="oa-rel__pick-row"
|
||||||
:title="row.displayTitle || row.appName || '审批单'"
|
:class="{
|
||||||
:label="pickerLabel(row)"
|
'is-selected': pendingIds.includes(row.id),
|
||||||
@click="toggle(row.id)"
|
'is-disabled': !rowSelectable(row)
|
||||||
|
}"
|
||||||
|
@click="toggle(row)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<van-checkbox
|
||||||
<van-checkbox class="oa-rel__check" :name="row.id" @click.stop />
|
class="oa-rel__check"
|
||||||
</template>
|
:name="row.id"
|
||||||
</van-cell>
|
:disabled="!rowSelectable(row)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="oa-rel__avatar"
|
||||||
|
:style="{ background: avatarColor(row.initiatorUserName) }"
|
||||||
|
>
|
||||||
|
{{ shortName(row.initiatorUserName) }}
|
||||||
|
</div>
|
||||||
|
<div class="oa-rel__pick-main">
|
||||||
|
<div class="oa-rel__pick-title">
|
||||||
|
{{ row.displayTitle || row.appName || '审批单' }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.initiatorDeptName" class="oa-rel__pick-meta">
|
||||||
|
部门:{{ row.initiatorDeptName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(f, i) in (row.previewFields || []).slice(0, 2)"
|
||||||
|
:key="i"
|
||||||
|
class="oa-rel__pick-meta"
|
||||||
|
>
|
||||||
|
{{ f.label }}:{{ f.value }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.applyTime" class="oa-rel__pick-time">
|
||||||
|
{{ row.applyTime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oa-rel__pick-status" :class="statusClass(row.status)">
|
||||||
|
<template v-if="row.status === 'running'">
|
||||||
|
<div v-if="row.currentApproverNames">
|
||||||
|
{{ row.currentApproverNames }}(审批人)
|
||||||
|
</div>
|
||||||
|
<div>{{ row.statusText || '处理中' }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ row.statusText || row.status }}</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</van-checkbox-group>
|
</van-checkbox-group>
|
||||||
<van-empty
|
<van-empty
|
||||||
v-if="!pickerLoading && !candidates.length"
|
v-if="!pickerLoading && !candidates.length"
|
||||||
@@ -99,6 +138,8 @@ import { fetchRelatedCandidates, fetchRelatedSummaries } from '../../api/oa.js'
|
|||||||
|
|
||||||
defineOptions({ name: 'relatedApproval' })
|
defineOptions({ name: 'relatedApproval' })
|
||||||
|
|
||||||
|
const AVATAR_COLORS = ['#3b82f6', '#22c55e', '#8b5cf6', '#f59e0b', '#06b6d4', '#ef4444']
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: [Array, String], default: () => [] },
|
modelValue: { type: [Array, String], default: () => [] },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
@@ -156,34 +197,64 @@ function removeId(id) {
|
|||||||
function openPicker() {
|
function openPicker() {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
pendingIds.value = []
|
pendingIds.value = []
|
||||||
|
if (keywordTimer) {
|
||||||
|
clearTimeout(keywordTimer)
|
||||||
|
keywordTimer = null
|
||||||
|
}
|
||||||
|
keyword.value = ''
|
||||||
visible.value = true
|
visible.value = true
|
||||||
loadCandidates()
|
loadCandidates()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(id) {
|
function toggle(row) {
|
||||||
|
if (!rowSelectable(row)) return
|
||||||
|
const id = String(row?.id || '')
|
||||||
|
if (!id) return
|
||||||
const set = new Set(pendingIds.value)
|
const set = new Set(pendingIds.value)
|
||||||
if (set.has(id)) set.delete(id)
|
if (set.has(id)) set.delete(id)
|
||||||
else set.add(id)
|
else set.add(id)
|
||||||
pendingIds.value = [...set]
|
pendingIds.value = [...set]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowSelectable(row) {
|
||||||
|
return !ids.value.includes(String(row?.id || ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 statusClass(status) {
|
||||||
|
const s = String(status || '').trim()
|
||||||
|
if (s === 'completed') return 'is-success'
|
||||||
|
if (s === 'rejected') return 'is-danger'
|
||||||
|
if (s === 'cancelled') return 'is-muted'
|
||||||
|
return 'is-warning'
|
||||||
|
}
|
||||||
|
|
||||||
function confirm() {
|
function confirm() {
|
||||||
emitValue([...ids.value, ...pendingIds.value])
|
emitValue([...ids.value, ...pendingIds.value])
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTabChange() {
|
function onTabChange() {
|
||||||
|
pendingIds.value = []
|
||||||
|
if (keywordTimer) {
|
||||||
|
clearTimeout(keywordTimer)
|
||||||
|
keywordTimer = null
|
||||||
|
}
|
||||||
loadCandidates()
|
loadCandidates()
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickerLabel(row) {
|
|
||||||
const bits = []
|
|
||||||
if (row.initiatorUserName) bits.push(row.initiatorUserName)
|
|
||||||
if (row.statusText || row.status) bits.push(row.statusText || row.status)
|
|
||||||
if (row.applyTime) bits.push(row.applyTime)
|
|
||||||
return bits.join(' · ')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCandidates() {
|
async function loadCandidates() {
|
||||||
pickerLoading.value = true
|
pickerLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -223,6 +294,29 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 输入即搜(防抖);打开弹窗 / 切 Tab / 回车 / 清除 走立即请求 */
|
||||||
|
let keywordTimer = null
|
||||||
|
let keywordWatchReady = false
|
||||||
|
watch(keyword, () => {
|
||||||
|
if (!visible.value || !keywordWatchReady) return
|
||||||
|
if (keywordTimer) clearTimeout(keywordTimer)
|
||||||
|
keywordTimer = setTimeout(() => {
|
||||||
|
keywordTimer = null
|
||||||
|
loadCandidates()
|
||||||
|
}, 280)
|
||||||
|
})
|
||||||
|
watch(visible, (v) => {
|
||||||
|
if (v) {
|
||||||
|
keywordWatchReady = true
|
||||||
|
} else {
|
||||||
|
keywordWatchReady = false
|
||||||
|
if (keywordTimer) {
|
||||||
|
clearTimeout(keywordTimer)
|
||||||
|
keywordTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -235,14 +329,14 @@ watch(
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.oa-rel__card {
|
.oa-rel__card {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--border, #dcebe4);
|
border: 1px solid var(--border, #d8f3e5);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.oa-rel__head {
|
.oa-rel__head {
|
||||||
@@ -256,17 +350,105 @@ watch(
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
}
|
}
|
||||||
.oa-rel__field {
|
.oa-rel__field {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.oa-rel__label {
|
.oa-rel__label {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 方案 C · 分割线行;内容对齐 Web 选单卡片字段 */
|
||||||
|
.oa-rel__pick-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-row.is-selected {
|
||||||
|
background: var(--brand-green-soft, #ecfdf5);
|
||||||
|
}
|
||||||
|
.oa-rel__pick-row.is-selected .oa-rel__pick-title {
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-row.is-disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.oa-rel__check {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.oa-rel__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-rel__pick-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-title {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--text-primary, #0f291e);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-muted, #5f7a6d);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-time {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 88px;
|
||||||
|
padding-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-status.is-warning {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-status.is-success {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-status.is-danger {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.oa-rel__pick-status.is-muted {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
<!-- 对照 Web OaTripItinerary:多行程卡片 · 日期+时段同行 · 半日制时长自动计算 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="oa-trip">
|
<div class="oa-trip">
|
||||||
<div v-for="(leg, idx) in inner" :key="idx" class="oa-trip__leg">
|
<div v-for="(leg, idx) in inner" :key="idx" class="oa-trip__leg">
|
||||||
<div class="oa-trip__head">
|
<div class="oa-trip__head">
|
||||||
<strong>行程 {{ idx + 1 }}</strong>
|
<strong class="oa-trip__title">行程 {{ idx + 1 }}</strong>
|
||||||
<van-button
|
<van-button
|
||||||
v-if="!disabled && inner.length > 1"
|
v-if="!disabled && inner.length > 1"
|
||||||
size="mini"
|
size="mini"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
删除
|
删除
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
:model-value="leg.transport"
|
:model-value="leg.transport"
|
||||||
label="交通工具"
|
label="交通工具"
|
||||||
@@ -41,8 +43,8 @@
|
|||||||
label-align="top"
|
label-align="top"
|
||||||
input-align="left"
|
input-align="left"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
placeholder="请输入"
|
placeholder="请输入出发城市"
|
||||||
@update:model-value="(v) => patch(idx, { fromCity: v })"
|
@update:model-value="(v) => patchLeg(idx, { fromCity: String(v || '') })"
|
||||||
/>
|
/>
|
||||||
<van-field
|
<van-field
|
||||||
:model-value="leg.toCity"
|
:model-value="leg.toCity"
|
||||||
@@ -50,61 +52,87 @@
|
|||||||
label-align="top"
|
label-align="top"
|
||||||
input-align="left"
|
input-align="left"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
placeholder="请输入"
|
placeholder="请输入目的城市"
|
||||||
@update:model-value="(v) => patch(idx, { toCity: v })"
|
@update:model-value="(v) => patchLeg(idx, { toCity: String(v || '') })"
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="leg.startDate"
|
|
||||||
label="开始日期"
|
|
||||||
label-align="top"
|
|
||||||
input-align="left"
|
|
||||||
readonly
|
|
||||||
is-link
|
|
||||||
:disabled="disabled"
|
|
||||||
placeholder="选择日期"
|
|
||||||
@click="!disabled && openDate(idx, 'startDate')"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="leg.startHalf"
|
|
||||||
label="开始时段"
|
|
||||||
label-align="top"
|
|
||||||
input-align="left"
|
|
||||||
readonly
|
|
||||||
is-link
|
|
||||||
:disabled="disabled"
|
|
||||||
@click="!disabled && openSelect(idx, 'startHalf', halfOpts)"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="leg.endDate"
|
|
||||||
label="结束日期"
|
|
||||||
label-align="top"
|
|
||||||
input-align="left"
|
|
||||||
readonly
|
|
||||||
is-link
|
|
||||||
:disabled="disabled"
|
|
||||||
placeholder="选择日期"
|
|
||||||
@click="!disabled && openDate(idx, 'endDate')"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="leg.endHalf"
|
|
||||||
label="结束时段"
|
|
||||||
label-align="top"
|
|
||||||
input-align="left"
|
|
||||||
readonly
|
|
||||||
is-link
|
|
||||||
:disabled="disabled"
|
|
||||||
@click="!disabled && openSelect(idx, 'endHalf', halfOpts)"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
:model-value="leg.duration == null ? '' : String(leg.duration)"
|
|
||||||
label="时长(天)"
|
|
||||||
label-align="top"
|
|
||||||
input-align="left"
|
|
||||||
type="number"
|
|
||||||
:disabled="disabled"
|
|
||||||
@update:model-value="(v) => patch(idx, { duration: v === '' ? null : Number(v) })"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 开始日期 + 开始时段:一行两列(对齐 Web) -->
|
||||||
|
<div class="oa-trip__row">
|
||||||
|
<van-field
|
||||||
|
class="oa-trip__field oa-trip__field--date"
|
||||||
|
:model-value="leg.startDate"
|
||||||
|
label="开始日期"
|
||||||
|
label-align="top"
|
||||||
|
input-align="left"
|
||||||
|
readonly
|
||||||
|
is-link
|
||||||
|
:disabled="disabled"
|
||||||
|
placeholder="选择日期"
|
||||||
|
@click="!disabled && openDate(idx, 'startDate')"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
class="oa-trip__field oa-trip__field--half"
|
||||||
|
:model-value="leg.startHalf"
|
||||||
|
label="开始时段"
|
||||||
|
label-align="top"
|
||||||
|
input-align="left"
|
||||||
|
readonly
|
||||||
|
is-link
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="!disabled && openSelect(idx, 'startHalf', halfOpts)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 结束日期 + 结束时段:一行两列 -->
|
||||||
|
<div class="oa-trip__row">
|
||||||
|
<van-field
|
||||||
|
class="oa-trip__field oa-trip__field--date"
|
||||||
|
:model-value="leg.endDate"
|
||||||
|
label="结束日期"
|
||||||
|
label-align="top"
|
||||||
|
input-align="left"
|
||||||
|
readonly
|
||||||
|
is-link
|
||||||
|
:disabled="disabled"
|
||||||
|
placeholder="选择日期"
|
||||||
|
@click="!disabled && openDate(idx, 'endDate')"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
class="oa-trip__field oa-trip__field--half"
|
||||||
|
:model-value="leg.endHalf"
|
||||||
|
label="结束时段"
|
||||||
|
label-align="top"
|
||||||
|
input-align="left"
|
||||||
|
readonly
|
||||||
|
is-link
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="!disabled && openSelect(idx, 'endHalf', halfOpts)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oa-trip__duration">
|
||||||
|
<van-field
|
||||||
|
class="oa-trip__duration-input"
|
||||||
|
:model-value="leg.duration == null ? '' : String(leg.duration)"
|
||||||
|
label="时长(天)"
|
||||||
|
label-align="top"
|
||||||
|
input-align="left"
|
||||||
|
type="number"
|
||||||
|
:disabled="disabled"
|
||||||
|
placeholder="自动计算"
|
||||||
|
@update:model-value="(v) => onDurationChange(idx, v)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="!disabled && leg.durationManual"
|
||||||
|
type="button"
|
||||||
|
class="oa-trip__restore"
|
||||||
|
@click="restoreAuto(idx)"
|
||||||
|
>
|
||||||
|
恢复自动计算
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<van-button v-if="!disabled" block type="primary" plain size="small" @click="add">
|
<van-button v-if="!disabled" block type="primary" plain size="small" @click="add">
|
||||||
添加行程
|
添加行程
|
||||||
</van-button>
|
</van-button>
|
||||||
@@ -128,7 +156,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
import { calcTripLegDays, emptyTripLeg } from '../kit/oa-trip-days.js'
|
||||||
|
|
||||||
defineOptions({ name: 'tripItinerary' })
|
defineOptions({ name: 'tripItinerary' })
|
||||||
|
|
||||||
@@ -147,21 +176,35 @@ const halfOpts = [
|
|||||||
{ text: '下午', value: '下午' }
|
{ text: '下午', value: '下午' }
|
||||||
]
|
]
|
||||||
|
|
||||||
function emptyLeg() {
|
function normalizeLeg(raw) {
|
||||||
|
const base = emptyTripLeg()
|
||||||
|
const src = raw && typeof raw === 'object' ? raw : {}
|
||||||
|
const durationRaw = src.duration
|
||||||
|
const duration =
|
||||||
|
durationRaw == null || durationRaw === ''
|
||||||
|
? null
|
||||||
|
: Number.isFinite(Number(durationRaw))
|
||||||
|
? Number(durationRaw)
|
||||||
|
: null
|
||||||
return {
|
return {
|
||||||
transport: '',
|
transport: String(src.transport ?? base.transport ?? ''),
|
||||||
tripType: '',
|
tripType: String(src.tripType ?? base.tripType ?? ''),
|
||||||
fromCity: '',
|
fromCity: String(src.fromCity ?? base.fromCity ?? ''),
|
||||||
toCity: '',
|
toCity: String(src.toCity ?? base.toCity ?? ''),
|
||||||
startDate: '',
|
startDate: String(src.startDate ?? base.startDate ?? ''),
|
||||||
startHalf: '上午',
|
startHalf: String(src.startHalf ?? base.startHalf ?? '上午'),
|
||||||
endDate: '',
|
endDate: String(src.endDate ?? base.endDate ?? ''),
|
||||||
endHalf: '下午',
|
endHalf: String(src.endHalf ?? base.endHalf ?? '下午'),
|
||||||
duration: null,
|
duration,
|
||||||
durationManual: false
|
durationManual: src.durationManual === true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initLegs(v) {
|
||||||
|
if (!Array.isArray(v) || v.length === 0) return [normalizeLeg(emptyTripLeg())]
|
||||||
|
return v.map((leg) => normalizeLeg(leg))
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Array, default: () => [] },
|
modelValue: { type: Array, default: () => [] },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
@@ -171,19 +214,16 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
const inner = ref(
|
const inner = ref(initLegs(props.modelValue))
|
||||||
Array.isArray(props.modelValue) && props.modelValue.length
|
let syncing = false
|
||||||
? props.modelValue.map((x) => ({ ...emptyLeg(), ...x }))
|
|
||||||
: [emptyLeg()]
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => {
|
(v) => {
|
||||||
if (!Array.isArray(v)) return
|
if (syncing) return
|
||||||
const next = v.length ? v.map((x) => ({ ...emptyLeg(), ...x })) : [emptyLeg()]
|
inner.value = initLegs(v)
|
||||||
if (JSON.stringify(next) !== JSON.stringify(inner.value)) inner.value = next
|
},
|
||||||
}
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const transportOpts = computed(() => {
|
const transportOpts = computed(() => {
|
||||||
@@ -199,27 +239,55 @@ const tripTypeOpts = computed(() => {
|
|||||||
return raw.map((o) => ({ text: o.label || o.text, value: o.value }))
|
return raw.map((o) => ({ text: o.label || o.text, value: o.value }))
|
||||||
})
|
})
|
||||||
|
|
||||||
function sync() {
|
function commit() {
|
||||||
emit('update:modelValue', inner.value.map((x) => ({ ...x })))
|
syncing = true
|
||||||
emit('change', inner.value.map((x) => ({ ...x })))
|
const out = inner.value.map((leg) => ({ ...leg }))
|
||||||
|
emit('update:modelValue', out)
|
||||||
|
emit('change', out)
|
||||||
|
nextTick(() => {
|
||||||
|
syncing = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function patch(idx, patchObj) {
|
function recalcLeg(idx) {
|
||||||
const next = inner.value.slice()
|
const leg = inner.value[idx]
|
||||||
next[idx] = { ...next[idx], ...patchObj }
|
if (!leg || leg.durationManual) return
|
||||||
inner.value = next
|
leg.duration = calcTripLegDays(leg.startDate, leg.startHalf, leg.endDate, leg.endHalf)
|
||||||
sync()
|
}
|
||||||
|
|
||||||
|
function patchLeg(idx, patchObj) {
|
||||||
|
Object.assign(inner.value[idx], patchObj)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScheduleChange(idx, field, val) {
|
||||||
|
inner.value[idx][field] = val
|
||||||
|
if (!inner.value[idx].durationManual) recalcLeg(idx)
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDurationChange(idx, raw) {
|
||||||
|
const n = raw === '' || raw == null ? null : Number(raw)
|
||||||
|
inner.value[idx].duration = n == null || !Number.isFinite(n) ? null : n
|
||||||
|
inner.value[idx].durationManual = true
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreAuto(idx) {
|
||||||
|
inner.value[idx].durationManual = false
|
||||||
|
recalcLeg(idx)
|
||||||
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
inner.value = [...inner.value, emptyLeg()]
|
inner.value.push(normalizeLeg(emptyTripLeg()))
|
||||||
sync()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(idx) {
|
function remove(idx) {
|
||||||
if (inner.value.length <= 1) return
|
if (inner.value.length <= 1) return
|
||||||
inner.value = inner.value.filter((_, i) => i !== idx)
|
inner.value.splice(idx, 1)
|
||||||
sync()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickerShow = ref(false)
|
const pickerShow = ref(false)
|
||||||
@@ -238,7 +306,12 @@ function openSelect(idx, key, opts) {
|
|||||||
|
|
||||||
function onPickerConfirm({ selectedOptions }) {
|
function onPickerConfirm({ selectedOptions }) {
|
||||||
const v = selectedOptions?.[0]?.value ?? ''
|
const v = selectedOptions?.[0]?.value ?? ''
|
||||||
patch(editIdx.value, { [editKey.value]: v })
|
const key = editKey.value
|
||||||
|
if (key === 'startHalf' || key === 'endHalf') {
|
||||||
|
onScheduleChange(editIdx.value, key, v)
|
||||||
|
} else {
|
||||||
|
patchLeg(editIdx.value, { [key]: v })
|
||||||
|
}
|
||||||
pickerShow.value = false
|
pickerShow.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,25 +334,90 @@ function openDate(idx, key) {
|
|||||||
|
|
||||||
function onDateConfirm({ selectedValues }) {
|
function onDateConfirm({ selectedValues }) {
|
||||||
const v = (selectedValues || dateValue.value).join('-')
|
const v = (selectedValues || dateValue.value).join('-')
|
||||||
patch(editIdx.value, { [editKey.value]: v })
|
onScheduleChange(editIdx.value, editKey.value, v)
|
||||||
dateShow.value = false
|
dateShow.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.oa-trip {
|
.oa-trip {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
padding: 4px 0 8px;
|
padding: 4px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-trip__leg {
|
.oa-trip__leg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 8px;
|
padding: 10px 12px 12px;
|
||||||
border-bottom: 1px dashed var(--border, #dcebe4);
|
border: 1px solid var(--border, #d8f3e5);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-soft, #f0fdf4);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-trip__head {
|
.oa-trip__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 4px 12px 8px;
|
padding: 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__title {
|
||||||
|
color: var(--brand-dark, #087f57);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__field {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__field--date {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__field--half {
|
||||||
|
flex: 0 0 36%;
|
||||||
|
max-width: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__duration {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__duration-input {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__restore {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--brand, #0a9f6d);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-trip__restore:active {
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -25,6 +25,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
applyConfirmationKitDateLinkage,
|
applyConfirmationKitDateLinkage,
|
||||||
applyConfirmationKitDefaults,
|
applyConfirmationKitDefaults,
|
||||||
|
clearConfirmationDateManualAndRelink,
|
||||||
ensureConfirmationKitVisibility,
|
ensureConfirmationKitVisibility,
|
||||||
listConfirmationKitBindings,
|
listConfirmationKitBindings,
|
||||||
listManualConfirmationDateFields,
|
listManualConfirmationDateFields,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const DEFAULT_LEAVE_WORK_HOURS = {
|
|||||||
afternoonStart: "13:00",
|
afternoonStart: "13:00",
|
||||||
afternoonEnd: "17:30"
|
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) {
|
function parseHm(hm) {
|
||||||
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
|
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const DEFAULT_OVERTIME_WINDOWS = {
|
|||||||
restBreakStart: "11:30",
|
restBreakStart: "11:30",
|
||||||
restBreakEnd: "13: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) {
|
function parseHm(hm) {
|
||||||
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
|
const m = String(hm || "").trim().match(/^(\d{1,2}):(\d{2})$/);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
|
|||||||
@@ -44,10 +44,32 @@ export const kitchenSinkRule = [
|
|||||||
{
|
{
|
||||||
type: 'datePicker',
|
type: 'datePicker',
|
||||||
field: 'bizDate',
|
field: 'bizDate',
|
||||||
title: '日期',
|
title: '日期(年月日)',
|
||||||
value: '',
|
value: '',
|
||||||
props: { type: 'date' }
|
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: 'upload', field: 'files', title: '附件 upload', value: [] },
|
||||||
{ type: 'money', field: 'amount', title: '金额 money', value: '' },
|
{ type: 'money', field: 'amount', title: '金额 money', value: '' },
|
||||||
{
|
{
|
||||||
@@ -120,14 +142,14 @@ export const kitchenSinkRule = [
|
|||||||
field: 'leaveStart',
|
field: 'leaveStart',
|
||||||
title: '开始时间',
|
title: '开始时间',
|
||||||
$required: true,
|
$required: true,
|
||||||
props: { type: 'datetime' }
|
props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'datePicker',
|
type: 'datePicker',
|
||||||
field: 'leaveEnd',
|
field: 'leaveEnd',
|
||||||
title: '结束时间',
|
title: '结束时间',
|
||||||
$required: true,
|
$required: true,
|
||||||
props: { type: 'datetime' }
|
props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'inputNumber',
|
type: 'inputNumber',
|
||||||
@@ -161,14 +183,14 @@ export const kitchenSinkRule = [
|
|||||||
field: 'otStart',
|
field: 'otStart',
|
||||||
title: '开始时间',
|
title: '开始时间',
|
||||||
$required: true,
|
$required: true,
|
||||||
props: { type: 'datetime' }
|
props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'datePicker',
|
type: 'datePicker',
|
||||||
field: 'otEnd',
|
field: 'otEnd',
|
||||||
title: '结束时间',
|
title: '结束时间',
|
||||||
$required: true,
|
$required: true,
|
||||||
props: { type: 'datetime' }
|
props: { type: 'datetime', format: 'YYYY-MM-DD HH:mm', valueFormat: 'YYYY-MM-DD HH:mm' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'inputNumber',
|
type: 'inputNumber',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import OaOwnerPersonSelect from './components/OaOwnerPersonSelect.vue'
|
|||||||
import OaOwnerDeptSelect from './components/OaOwnerDeptSelect.vue'
|
import OaOwnerDeptSelect from './components/OaOwnerDeptSelect.vue'
|
||||||
import OaKitShell from './components/OaKitShell.vue'
|
import OaKitShell from './components/OaKitShell.vue'
|
||||||
import OaTripItinerary from './components/OaTripItinerary.vue'
|
import OaTripItinerary from './components/OaTripItinerary.vue'
|
||||||
|
import OaDatePicker from './components/OaDatePicker.vue'
|
||||||
|
|
||||||
/** 与 web-admin type 名对齐 */
|
/** 与 web-admin type 名对齐 */
|
||||||
const REAL_COMPONENTS = {
|
const REAL_COMPONENTS = {
|
||||||
@@ -37,4 +38,7 @@ export function registerOaComponents(formCreate) {
|
|||||||
formCreate.component(name, comp)
|
formCreate.component(name, comp)
|
||||||
}
|
}
|
||||||
formCreate.component('fcUpload', OaUploadField)
|
formCreate.component('fcUpload', OaUploadField)
|
||||||
|
// 覆盖 @form-create/vant 内置日期:按 props.type 区分 date / datetime
|
||||||
|
formCreate.component('datePicker', OaDatePicker)
|
||||||
|
formCreate.component('fcDatePicker', OaDatePicker)
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,33 @@
|
|||||||
/* form-create + Vant:上下布局与左对齐兜底 */
|
/* form-create + Vant:上下布局与左对齐兜底 */
|
||||||
.form-create-m .van-field__label--top {
|
.form-create-m .van-field__label--top {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-create-m .van-field__control {
|
.form-create-m .van-field__control {
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
|
font-size: 13px;
|
||||||
|
min-height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-create-m .van-field__control::placeholder {
|
.form-create-m .van-field__control::placeholder {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-create-m .fc-form-item {
|
.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-calendar,
|
||||||
.form-create-m ._fc-cascader,
|
.form-create-m ._fc-cascader,
|
||||||
.form-create-m .oa-pick,
|
.form-create-m .oa-pick,
|
||||||
|
.form-create-m .oa-owner-field,
|
||||||
.form-create-m .oa-money,
|
.form-create-m .oa-money,
|
||||||
.form-create-m .oa-number,
|
.form-create-m .oa-number,
|
||||||
.form-create-m .oa-recv,
|
.form-create-m .oa-recv,
|
||||||
@@ -58,6 +68,40 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
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,
|
.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-pick > .van-cell > .van-cell__right-icon,
|
||||||
.form-create-m .oa-recv > .van-cell > .van-cell__right-icon {
|
.form-create-m .oa-recv > .van-cell > .van-cell__right-icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-left: 8px;
|
margin-left: 6px;
|
||||||
order: 3;
|
order: 3;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 行程内带自身 label 的 Field:标题在上,箭头仍在内容行右侧 */
|
/* 行程内带自身 label 的 Field:标题在上,箭头仍在内容行右侧 */
|
||||||
@@ -104,22 +149,77 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
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 {
|
.form-create-m .oa-trip .van-field--label-top > .van-cell__title {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
width: 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 {
|
.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;
|
min-width: 0;
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
text-align: left;
|
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 {
|
.form-create-m .oa-trip .van-field--label-top > .van-cell__right-icon {
|
||||||
flex: 0 0 auto;
|
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 {
|
.form-create-m .fc-form-item.van-field--label-top > .van-cell__title {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-create-m .fc-form-item.van-field--label-top > .van-cell__value {
|
.form-create-m .fc-form-item.van-field--label-top > .van-cell__value {
|
||||||
@@ -156,7 +256,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid var(--border, #dcebe4);
|
border-bottom: 1px solid var(--border, #d8f3e5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__hd strong,
|
.oa-pick__hd strong,
|
||||||
@@ -164,14 +264,14 @@
|
|||||||
.oa-rel__hd strong {
|
.oa-rel__hd strong {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary, #1f2a26);
|
color: var(--text-primary, #0f291e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__hd > span:first-child,
|
.oa-pick__hd > span:first-child,
|
||||||
.oa-recv__hd > span:first-child,
|
.oa-recv__hd > span:first-child,
|
||||||
.oa-rel__hd > span:first-child {
|
.oa-rel__hd > span:first-child {
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
color: var(--text-muted, #6b7b74);
|
color: var(--text-muted, #5f7a6d);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,81 +279,152 @@
|
|||||||
.oa-recv__ok,
|
.oa-recv__ok,
|
||||||
.oa-rel__ok {
|
.oa-rel__ok {
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
color: var(--brand-green, #16a34a);
|
color: var(--brand-green, #22c55e);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 方案 C · 分割线行表 */
|
||||||
.oa-pick__list,
|
.oa-pick__list,
|
||||||
.oa-recv__list,
|
.oa-recv__list,
|
||||||
.oa-rel__picker {
|
.oa-rel__picker {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: transparent;
|
background: #fff;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__list .van-cell,
|
/*
|
||||||
.oa-recv__list .van-cell,
|
* 弹窗列表行(对齐关联审批单 oa-rel__pick-row)
|
||||||
.oa-rel__picker .van-cell {
|
* 人员 / 部门 / 收款账户 / 关联审批 共用,避免 van-cell 被表单样式污染
|
||||||
align-items: center;
|
*/
|
||||||
margin-bottom: 10px;
|
.oa-sheet-row {
|
||||||
padding: 12px 14px;
|
display: flex;
|
||||||
text-align: left;
|
gap: 10px;
|
||||||
background: transparent;
|
align-items: flex-start;
|
||||||
border-radius: 10px;
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__list .van-cell:last-child,
|
.oa-sheet-row:last-child {
|
||||||
.oa-recv__list .van-cell:last-child,
|
border-bottom: none;
|
||||||
.oa-rel__picker .van-cell:last-child {
|
}
|
||||||
|
|
||||||
|
.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;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__list .van-cell::after,
|
.oa-sheet-row__meta {
|
||||||
.oa-recv__list .van-cell::after,
|
overflow: hidden;
|
||||||
.oa-rel__picker .van-cell::after {
|
color: var(--text-muted, #5f7a6d);
|
||||||
display: none;
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__list .van-cell__title,
|
.oa-sheet-row__exp {
|
||||||
.oa-recv__list .van-cell__title,
|
display: inline-flex;
|
||||||
.oa-rel__picker .van-cell__title {
|
flex-shrink: 0;
|
||||||
text-align: left;
|
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-sheet-row__exp--spacer {
|
||||||
.oa-recv__list .van-cell__label,
|
visibility: hidden;
|
||||||
.oa-rel__picker .van-cell__label {
|
pointer-events: none;
|
||||||
text-align: left;
|
|
||||||
color: var(--text-muted, #6b7b74);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__check,
|
.oa-pick__check,
|
||||||
.oa-recv__check,
|
.oa-recv__check,
|
||||||
.oa-rel__check {
|
.oa-rel__check {
|
||||||
margin-right: 8px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹窗检索:勿继承表单 labelAlign=top,保持单行垂直居中 */
|
/* 弹窗检索:勿继承表单 labelAlign=top,保持单行垂直居中 */
|
||||||
.oa-pick__sheet .van-search,
|
.oa-pick__sheet .van-search,
|
||||||
.oa-recv__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;
|
padding: 10px 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__sheet .van-search .van-search__content,
|
.oa-pick__sheet .van-search .van-search__content,
|
||||||
.oa-recv__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;
|
align-items: center;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__sheet .van-search .van-cell,
|
.oa-pick__sheet .van-search .van-cell,
|
||||||
.oa-recv__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;
|
align-items: center;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
@@ -262,14 +433,28 @@
|
|||||||
|
|
||||||
.oa-pick__sheet .van-search .van-field__body,
|
.oa-pick__sheet .van-search .van-field__body,
|
||||||
.oa-recv__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;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-pick__sheet .van-search .van-field__control,
|
.oa-pick__sheet .van-search .van-field__control,
|
||||||
.oa-recv__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;
|
height: 36px;
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
text-align: left !important;
|
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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
:root {
|
:root {
|
||||||
--brand-green: #16a34a;
|
/* 方案 A · 薄荷微风 — 与 mobile-h5 对齐 */
|
||||||
--brand-green-dark: #15803d;
|
--brand-green: #22c55e;
|
||||||
--brand-green-soft: #f0fdf4;
|
--brand-green-dark: #16a34a;
|
||||||
--bg-page: #f5f8f7;
|
--brand-green-soft: #ecfdf5;
|
||||||
--text-primary: #1f2a26;
|
--header-fg: #065f46;
|
||||||
--text-muted: #6b7b74;
|
--bg-page: #ffffff;
|
||||||
--border: #dcebe4;
|
--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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ async function onValidate() {
|
|||||||
.sink__hd {
|
.sink__hd {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: var(--header-fg, #065f46);
|
||||||
background: linear-gradient(135deg, var(--brand-green), var(--brand-green-dark));
|
background: linear-gradient(160deg, #f0fdf4 0%, #d1fae5 58%, #a7f3d0 100%);
|
||||||
}
|
}
|
||||||
.sink__body {
|
.sink__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -10,6 +10,26 @@
|
|||||||
:rule="formRule"
|
:rule="formRule"
|
||||||
:option="formOption"
|
:option="formOption"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="confirmationDateManualFields.length" class="oa-start__tip">
|
||||||
|
<button type="button" class="oa-start__relink" @click="restoreConfirmationDateAuto">
|
||||||
|
恢复转正日期自动计算
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p v-if="leaveDurationManualFields.length" class="oa-start__tip">
|
||||||
|
<button type="button" class="oa-start__relink" @click="restoreLeaveAuto">
|
||||||
|
恢复请假时长自动计算
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p v-if="overtimeDurationManualFields.length" class="oa-start__tip">
|
||||||
|
<button type="button" class="oa-start__relink" @click="restoreOvertimeAuto">
|
||||||
|
恢复加班时长自动计算
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p v-if="tripDaysManualFields.length" class="oa-start__tip">
|
||||||
|
<button type="button" class="oa-start__relink" @click="restoreTripDaysAuto">
|
||||||
|
恢复出差天数自动计算
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
<OaFlowPreview
|
<OaFlowPreview
|
||||||
v-model:initiator-select-by-node="initiatorSelectByNode"
|
v-model:initiator-select-by-node="initiatorSelectByNode"
|
||||||
:steps="flowSteps"
|
:steps="flowSteps"
|
||||||
@@ -46,7 +66,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import {
|
import {
|
||||||
@@ -77,7 +97,11 @@ import {
|
|||||||
TRIP_MANUAL_PREFIX,
|
TRIP_MANUAL_PREFIX,
|
||||||
applyConfirmationKitDateLinkage,
|
applyConfirmationKitDateLinkage,
|
||||||
applyConfirmationKitDefaults,
|
applyConfirmationKitDefaults,
|
||||||
|
clearConfirmationDateManualAndRelink,
|
||||||
listConfirmationKitBindings,
|
listConfirmationKitBindings,
|
||||||
|
listManualConfirmationDateFields,
|
||||||
|
markConfirmationDateManual,
|
||||||
|
CONFIRMATION_MANUAL_PREFIX,
|
||||||
applyResignationKitDefaults,
|
applyResignationKitDefaults,
|
||||||
applyOwnerFieldDefaults
|
applyOwnerFieldDefaults
|
||||||
} from '../oa/kit-runtime.js'
|
} from '../oa/kit-runtime.js'
|
||||||
@@ -114,6 +138,40 @@ let overtimeKitSyncing = false
|
|||||||
let overtimeKitPrevSnapshot = {}
|
let overtimeKitPrevSnapshot = {}
|
||||||
const overtimeRestMap = {}
|
const overtimeRestMap = {}
|
||||||
|
|
||||||
|
const confirmationDateManualFields = computed(() =>
|
||||||
|
listManualConfirmationDateFields({ rule: formRule.value }, formData.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const leaveDurationManualFields = computed(() => {
|
||||||
|
const fields = []
|
||||||
|
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 = []
|
||||||
|
for (const kit of listOvertimeKitBindings({ rule: formRule.value })) {
|
||||||
|
if (formData.value[OVERTIME_MANUAL_PREFIX + kit.durationField]) {
|
||||||
|
fields.push(kit.durationField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
})
|
||||||
|
|
||||||
|
const tripDaysManualFields = computed(() => {
|
||||||
|
const fields = []
|
||||||
|
for (const kit of listTripKits({ rule: formRule.value })) {
|
||||||
|
if (formData.value[TRIP_MANUAL_PREFIX + kit.daysField]) {
|
||||||
|
fields.push(kit.daysField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
})
|
||||||
|
|
||||||
/** 通知宿主(小程序 web-view / uni-app H5 iframe)关闭并返回发起列表 */
|
/** 通知宿主(小程序 web-view / uni-app H5 iframe)关闭并返回发起列表 */
|
||||||
function closeToMini(payload) {
|
function closeToMini(payload) {
|
||||||
const data = payload || { action: 'oa-start-close' }
|
const data = payload || { action: 'oa-start-close' }
|
||||||
@@ -296,7 +354,10 @@ function syncTripKitDurations() {
|
|||||||
!!formData.value[TRIP_MANUAL_PREFIX + k.daysField]
|
!!formData.value[TRIP_MANUAL_PREFIX + k.daysField]
|
||||||
)
|
)
|
||||||
const daysFieldChanged = kits.some((k) => data[k.daysField] !== formData.value[k.daysField])
|
const daysFieldChanged = kits.some((k) => data[k.daysField] !== formData.value[k.daysField])
|
||||||
if (changed || manualKeysChanged || daysFieldChanged) {
|
const listFieldChanged = kits.some(
|
||||||
|
(k) => JSON.stringify(data[k.listField]) !== JSON.stringify(formData.value[k.listField])
|
||||||
|
)
|
||||||
|
if (changed || manualKeysChanged || daysFieldChanged || listFieldChanged) {
|
||||||
tripKitSyncing = true
|
tripKitSyncing = true
|
||||||
formData.value = data
|
formData.value = data
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -309,11 +370,43 @@ function syncConfirmationKitDates() {
|
|||||||
if (confirmationKitSyncing) return
|
if (confirmationKitSyncing) return
|
||||||
if (!formRule.value.length) return
|
if (!formRule.value.length) return
|
||||||
const schema = { rule: formRule.value }
|
const schema = { rule: formRule.value }
|
||||||
if (!listConfirmationKitBindings(schema).length) return
|
const kits = listConfirmationKitBindings(schema)
|
||||||
const curr = { ...formData.value }
|
if (!kits.length) return
|
||||||
|
|
||||||
|
let curr = { ...formData.value }
|
||||||
|
for (const kit of kits) {
|
||||||
|
const confField = kit.confirmationDateField
|
||||||
|
const hireField = kit.hireDateField
|
||||||
|
const probField = kit.probationMonthsField
|
||||||
|
if (!confField || !hireField || !probField) continue
|
||||||
|
const confChanged = confirmationKitPrevSnapshot[confField] !== curr[confField]
|
||||||
|
const hireChanged = confirmationKitPrevSnapshot[hireField] !== curr[hireField]
|
||||||
|
const probChanged = confirmationKitPrevSnapshot[probField] !== curr[probField]
|
||||||
|
if (
|
||||||
|
confChanged &&
|
||||||
|
!hireChanged &&
|
||||||
|
!probChanged &&
|
||||||
|
curr[confField] !== undefined &&
|
||||||
|
curr[confField] !== ''
|
||||||
|
) {
|
||||||
|
curr = markConfirmationDateManual(curr, confField, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { data, changed } = applyConfirmationKitDateLinkage(schema, curr)
|
const { data, changed } = applyConfirmationKitDateLinkage(schema, curr)
|
||||||
confirmationKitPrevSnapshot = { ...data }
|
confirmationKitPrevSnapshot = { ...data }
|
||||||
if (changed) {
|
const anyDiff =
|
||||||
|
changed ||
|
||||||
|
kits.some((k) => {
|
||||||
|
const f = k.confirmationDateField
|
||||||
|
if (!f) return false
|
||||||
|
return (
|
||||||
|
data[f] !== formData.value[f] ||
|
||||||
|
!!data[CONFIRMATION_MANUAL_PREFIX + f] !==
|
||||||
|
!!formData.value[CONFIRMATION_MANUAL_PREFIX + f]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (anyDiff) {
|
||||||
confirmationKitSyncing = true
|
confirmationKitSyncing = true
|
||||||
formData.value = data
|
formData.value = data
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -322,6 +415,47 @@ function syncConfirmationKitDates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreConfirmationDateAuto() {
|
||||||
|
const schema = { rule: formRule.value }
|
||||||
|
let data = { ...formData.value }
|
||||||
|
for (const field of listManualConfirmationDateFields(schema, data)) {
|
||||||
|
data = clearConfirmationDateManualAndRelink(schema, data, field)
|
||||||
|
}
|
||||||
|
confirmationKitSyncing = true
|
||||||
|
formData.value = data
|
||||||
|
confirmationKitPrevSnapshot = { ...data }
|
||||||
|
queueMicrotask(() => {
|
||||||
|
confirmationKitSyncing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 restoreOvertimeAuto() {
|
||||||
|
let data = { ...formData.value }
|
||||||
|
for (const kit of listOvertimeKitBindings({ rule: formRule.value })) {
|
||||||
|
data = markOvertimeDurationManual(data, kit.durationField, false)
|
||||||
|
}
|
||||||
|
formData.value = data
|
||||||
|
void syncOvertimeKitDurations()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureOvertimeHolidayRange(startYmd, endYmd) {
|
async function ensureOvertimeHolidayRange(startYmd, endYmd) {
|
||||||
const start = String(startYmd || '').trim().slice(0, 10)
|
const start = String(startYmd || '').trim().slice(0, 10)
|
||||||
const end = String(endYmd || '').trim().slice(0, 10)
|
const end = String(endYmd || '').trim().slice(0, 10)
|
||||||
@@ -636,7 +770,7 @@ onUnmounted(() => {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-page, #f5f8f7);
|
background: var(--bg-page, #ffffff);
|
||||||
}
|
}
|
||||||
.oa-start__loading {
|
.oa-start__loading {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -649,6 +783,22 @@ onUnmounted(() => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px 12px 96px;
|
padding: 12px 12px 96px;
|
||||||
}
|
}
|
||||||
|
.oa-start__tip {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.oa-start__relink {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--brand, #0a9f6d);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.oa-start__relink:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
.oa-start__ft {
|
.oa-start__ft {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -658,45 +808,47 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-top: 1px solid var(--border, #dcebe4);
|
border-top: 1px solid #eef2f6;
|
||||||
box-shadow: 0 -4px 16px rgba(31, 42, 38, 0.04);
|
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
.oa-start__btn {
|
.oa-start__btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 40px;
|
height: 44px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.oa-start__btn--cancel {
|
.oa-start__btn--cancel {
|
||||||
flex: 0 0 64px;
|
flex: 0 0 auto;
|
||||||
padding: 0;
|
height: 44px;
|
||||||
|
padding: 0 10px !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: var(--text-muted, #6b7b74) !important;
|
color: #94a3b8 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
.oa-start__btn--cancel:active {
|
.oa-start__btn--cancel:active {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
.oa-start__btn--draft {
|
.oa-start__btn--draft {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 1.5px solid var(--brand-green, #16a34a) !important;
|
border: none !important;
|
||||||
background: #fff !important;
|
background: #f1f5f9 !important;
|
||||||
color: var(--brand-green-dark, #15803d) !important;
|
color: #334155 !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.oa-start__btn--submit {
|
.oa-start__btn--submit {
|
||||||
flex: 1.35;
|
flex: 1.35;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
background: linear-gradient(
|
background: var(--brand-green, #22c55e) !important;
|
||||||
135deg,
|
|
||||||
var(--brand-green, #16a34a),
|
|
||||||
var(--brand-green-dark, #15803d)
|
|
||||||
) !important;
|
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.28);
|
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.14);
|
||||||
}
|
}
|
||||||
/* G:文字半透明保留,转圈居中叠层(禁用时勿再整钮降透明) */
|
/* G:文字半透明保留,转圈居中叠层(禁用时勿再整钮降透明) */
|
||||||
.oa-start__btn.is-loading.van-button--disabled {
|
.oa-start__btn.is-loading.van-button--disabled {
|
||||||
|
|||||||
@@ -0,0 +1,868 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>mobile-h5 · 明亮清爽配色 Demo</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; min-height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||||
|
color: #1e293b;
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 420px at 8% -8%, rgba(52, 211, 153, 0.18), transparent 55%),
|
||||||
|
radial-gradient(700px 380px at 92% 0%, rgba(125, 211, 252, 0.16), transparent 50%),
|
||||||
|
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 45%, #f7f8fa 100%);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 18px 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 720px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.hero .note {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.hero .note strong { color: #0f766e; }
|
||||||
|
|
||||||
|
.theme-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 22px 0 20px;
|
||||||
|
}
|
||||||
|
.theme-tab {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: border-color .15s, box-shadow .15s, color .15s;
|
||||||
|
}
|
||||||
|
.theme-tab:hover { border-color: #94a3b8; }
|
||||||
|
.theme-tab.is-active {
|
||||||
|
color: var(--t-brand-dark);
|
||||||
|
border-color: var(--t-brand);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--t-brand) 22%, transparent);
|
||||||
|
background: var(--t-brand-soft);
|
||||||
|
}
|
||||||
|
.theme-tab .dot {
|
||||||
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background: var(--swatch, #22c55e);
|
||||||
|
}
|
||||||
|
.theme-tab .badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
.theme-tab.is-current .badge {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(280px, 360px) 1fr;
|
||||||
|
gap: 22px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: rgba(255,255,255,.92);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
.panel h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.panel .sub {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.token {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 36px 1fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
.token i {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(15,23,42,.06);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.token .name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.token .desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.token code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #0f766e;
|
||||||
|
background: #f0fdfa;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.stage-caption {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 390px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stage-caption strong { color: #0f172a; }
|
||||||
|
|
||||||
|
.phone {
|
||||||
|
width: 390px;
|
||||||
|
height: 780px;
|
||||||
|
background: #0f172a;
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow:
|
||||||
|
0 28px 60px rgba(15, 23, 42, 0.18),
|
||||||
|
inset 0 0 0 1px rgba(255,255,255,.08);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.phone::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 16px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 108px; height: 24px;
|
||||||
|
background: #020617;
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
.phone-screen {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--t-bg);
|
||||||
|
color: var(--t-text);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.screen-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— 完整工作台界面 —— */
|
||||||
|
.home-header {
|
||||||
|
padding: 52px 16px 22px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0 0 22px 22px;
|
||||||
|
background: var(--t-header-bg);
|
||||||
|
color: var(--t-header-fg);
|
||||||
|
}
|
||||||
|
.home-header::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -36px; top: -28px;
|
||||||
|
width: 140px; height: 140px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--t-header-glow);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.home-header .brand {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .78;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.home-header .row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.home-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 720;
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
}
|
||||||
|
.home-header .logout {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .85;
|
||||||
|
padding: 4px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.home-header .meta {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .82;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.home-header .meta .dot {
|
||||||
|
width: 4px; height: 4px; border-radius: 50%;
|
||||||
|
background: currentColor; opacity: .55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-body { padding: 12px 12px 20px; }
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
height: 132px;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid var(--t-border);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, var(--t-banner-a), var(--t-banner-b)),
|
||||||
|
var(--t-brand-soft);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 14px;
|
||||||
|
color: var(--t-banner-fg);
|
||||||
|
}
|
||||||
|
.banner::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(180px 100px at 85% 20%, rgba(255,255,255,.45), transparent 60%),
|
||||||
|
radial-gradient(120px 80px at 10% 80%, rgba(255,255,255,.2), transparent 55%);
|
||||||
|
}
|
||||||
|
.banner .txt { position: relative; z-index: 1; }
|
||||||
|
.banner .kicker { font-size: 11px; opacity: .85; margin-bottom: 4px; }
|
||||||
|
.banner .title { font-size: 16px; font-weight: 700; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--t-surface);
|
||||||
|
border: 1px solid var(--t-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: var(--t-shadow);
|
||||||
|
padding: 14px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.card-hd {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.card-hd h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 680;
|
||||||
|
color: var(--t-text);
|
||||||
|
}
|
||||||
|
.card-hd .hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px 4px;
|
||||||
|
}
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 4px 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.menu-icon {
|
||||||
|
width: 44px; height: 44px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.menu-item span {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t-text);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--t-border);
|
||||||
|
background: var(--t-todo-bg);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.todo:last-child { margin-bottom: 0; }
|
||||||
|
.todo .bar {
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--todo-accent, var(--t-brand));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.todo .body { flex: 1; min-width: 0; }
|
||||||
|
.todo .top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.todo .name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--t-text);
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 650;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tag.warn {
|
||||||
|
background: color-mix(in srgb, var(--t-warning) 14%, #fff);
|
||||||
|
color: var(--t-warning);
|
||||||
|
}
|
||||||
|
.tag.ok {
|
||||||
|
background: color-mix(in srgb, var(--t-success) 14%, #fff);
|
||||||
|
color: var(--t-success);
|
||||||
|
}
|
||||||
|
.todo .meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--t-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--t-border);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--t-text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.btn.primary {
|
||||||
|
background: var(--t-brand);
|
||||||
|
border-color: var(--t-brand);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 6px 16px color-mix(in srgb, var(--t-brand) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare {
|
||||||
|
margin-top: 28px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.compare { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.compare .mini {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.compare .mini-hd {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
border-bottom: 1px solid #eef2f7;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
.compare .mini-body { height: 88px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="hero">
|
||||||
|
<h1>mobile-h5 · 明亮清爽配色方案</h1>
|
||||||
|
<p>
|
||||||
|
针对「整体偏暗、顶栏沉重」的问题:去掉深绿顶栏(#14532d),改用浅色头图 + 通透背景。
|
||||||
|
下方可切换方案,左侧是完整 Token,右侧是工作台完整界面预览。
|
||||||
|
</p>
|
||||||
|
<div class="note">点选方案后,在对话里告诉我选哪套(或组合喜好)。文件也可本地打开:<strong>mobile-ui/zkxw-mobile-bright-theme-demos.html</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-tabs" id="tabs"></div>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<aside class="panel">
|
||||||
|
<h2 id="themeTitle">—</h2>
|
||||||
|
<p class="sub" id="themeDesc">—</p>
|
||||||
|
<div class="token-grid" id="tokens"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="stage">
|
||||||
|
<div class="stage-caption">完整界面预览 · <strong id="previewName">—</strong></div>
|
||||||
|
<div class="phone" id="phone">
|
||||||
|
<div class="phone-screen">
|
||||||
|
<div class="screen-scroll">
|
||||||
|
<header class="home-header">
|
||||||
|
<div class="brand">中科兴皖 · 移动工作台</div>
|
||||||
|
<div class="row">
|
||||||
|
<h3>您好,张明</h3>
|
||||||
|
<div class="logout">退出登录</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span>产品经理</span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span>细胞存储事业部</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="home-body">
|
||||||
|
<div class="banner">
|
||||||
|
<div class="txt">
|
||||||
|
<div class="kicker">今日提醒</div>
|
||||||
|
<div class="title">有 3 条待办审批待处理</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-hd">
|
||||||
|
<h4>功能菜单</h4>
|
||||||
|
<span class="hint">共 8 项</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-grid" id="menus"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-hd">
|
||||||
|
<h4>我的待办</h4>
|
||||||
|
<span class="hint">最近更新</span>
|
||||||
|
</div>
|
||||||
|
<div class="todo" style="--todo-accent: var(--t-warning)">
|
||||||
|
<div class="bar"></div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="name">采集协议审批</div>
|
||||||
|
<span class="tag warn">待处理</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta">申请人 李华 · 今天 09:18</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="todo" style="--todo-accent: var(--t-brand)">
|
||||||
|
<div class="bar"></div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="name">样本入库确认</div>
|
||||||
|
<span class="tag ok">进行中</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta">库位 A-12 · 今天 08:42</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cta-row">
|
||||||
|
<div class="btn">查看全部</div>
|
||||||
|
<div class="btn primary">发起审批</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="compare">
|
||||||
|
<div class="mini">
|
||||||
|
<div class="mini-hd">现状顶栏(偏沉)</div>
|
||||||
|
<div class="mini-body" style="background:linear-gradient(155deg,#14532d,#16a34a,#22c55e)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="mini-hd">目标顶栏(明亮)</div>
|
||||||
|
<div class="mini-body" id="targetHeader" style="background:linear-gradient(160deg,#ecfdf5,#d1fae5)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const THEMES = {
|
||||||
|
mint: {
|
||||||
|
id: 'mint',
|
||||||
|
name: 'A · 薄荷微风',
|
||||||
|
badge: '推荐',
|
||||||
|
swatch: '#22c55e',
|
||||||
|
desc: '保留绿色品牌,但顶栏改为浅绿渐变 + 深色字,页面通透。改造成本最低,最贴近现有业务。',
|
||||||
|
tokens: [
|
||||||
|
['brand', '#22c55e', '主色 / 按钮'],
|
||||||
|
['brandDark', '#16a34a', '主色深 / 强调文字'],
|
||||||
|
['brandSoft', '#ecfdf5', '主色浅底'],
|
||||||
|
['headerBg', 'linear-gradient(160deg, #f0fdf4 0%, #d1fae5 58%, #a7f3d0 100%)', '顶栏背景'],
|
||||||
|
['headerFg', '#065f46', '顶栏文字'],
|
||||||
|
['bg', '#f7fffb', '页面背景'],
|
||||||
|
['surface', '#ffffff', '卡片表面'],
|
||||||
|
['text', '#0f291e', '主文字'],
|
||||||
|
['muted', '#5f7a6d', '次要文字'],
|
||||||
|
['border', '#d8f3e5', '边框'],
|
||||||
|
['accent', '#fbbf24', '辅助强调(大麦黄提亮)'],
|
||||||
|
['warning', '#d97706', '警告'],
|
||||||
|
['danger', '#ef4444', '危险'],
|
||||||
|
['success', '#10b981', '成功']
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
'--t-brand': '#22c55e',
|
||||||
|
'--t-brand-dark': '#16a34a',
|
||||||
|
'--t-brand-soft': '#ecfdf5',
|
||||||
|
'--t-header-bg': 'linear-gradient(160deg, #f0fdf4 0%, #d1fae5 58%, #a7f3d0 100%)',
|
||||||
|
'--t-header-fg': '#065f46',
|
||||||
|
'--t-header-glow': 'rgba(255,255,255,.55)',
|
||||||
|
'--t-bg': '#f7fffb',
|
||||||
|
'--t-surface': '#ffffff',
|
||||||
|
'--t-text': '#0f291e',
|
||||||
|
'--t-muted': '#5f7a6d',
|
||||||
|
'--t-border': '#d8f3e5',
|
||||||
|
'--t-shadow': '0 8px 24px rgba(22, 163, 74, 0.07)',
|
||||||
|
'--t-banner-a': '#bbf7d0',
|
||||||
|
'--t-banner-b': '#86efac',
|
||||||
|
'--t-banner-fg': '#14532d',
|
||||||
|
'--t-todo-bg': 'linear-gradient(180deg, #f6fffa 0%, #ffffff 70%)',
|
||||||
|
'--t-warning': '#d97706',
|
||||||
|
'--t-success': '#059669'
|
||||||
|
},
|
||||||
|
menus: [
|
||||||
|
['审', '#ffe4e6', '#e11d48', '发起审批'],
|
||||||
|
['待', '#ecfdf5', '#16a34a', '我的待办'],
|
||||||
|
['协', '#e0f2fe', '#0284c7', '协议管理'],
|
||||||
|
['样', '#fef3c7', '#d97706', '样本查询'],
|
||||||
|
['质', '#ede9fe', '#7c3aed', '质检工单'],
|
||||||
|
['库', '#dcfce7', '#15803d', '入库登记'],
|
||||||
|
['报', '#f1f5f9', '#475569', '统计报表'],
|
||||||
|
['设', '#f8fafc', '#64748b', '个人设置']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sky: {
|
||||||
|
id: 'sky',
|
||||||
|
name: 'B · 晴空青',
|
||||||
|
swatch: '#0ea5e9',
|
||||||
|
desc: '顶栏用极浅天蓝,主操作偏青蓝,绿色作成功/进行态点缀。整体更「透气、白天感」。',
|
||||||
|
tokens: [
|
||||||
|
['brand', '#0ea5e9', '主色 / 按钮'],
|
||||||
|
['brandDark', '#0284c7', '主色深'],
|
||||||
|
['brandSoft', '#e0f2fe', '主色浅底'],
|
||||||
|
['headerBg', 'linear-gradient(165deg, #f0f9ff 0%, #e0f2fe 55%, #bae6fd 100%)', '顶栏背景'],
|
||||||
|
['headerFg', '#0c4a6e', '顶栏文字'],
|
||||||
|
['bg', '#f8fbff', '页面背景'],
|
||||||
|
['surface', '#ffffff', '卡片表面'],
|
||||||
|
['text', '#0f172a', '主文字'],
|
||||||
|
['muted', '#64748b', '次要文字'],
|
||||||
|
['border', '#dbeafe', '边框'],
|
||||||
|
['accent', '#22c55e', '绿色点缀'],
|
||||||
|
['warning', '#d97706', '警告'],
|
||||||
|
['danger', '#ef4444', '危险'],
|
||||||
|
['success', '#22c55e', '成功']
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
'--t-brand': '#0ea5e9',
|
||||||
|
'--t-brand-dark': '#0284c7',
|
||||||
|
'--t-brand-soft': '#e0f2fe',
|
||||||
|
'--t-header-bg': 'linear-gradient(165deg, #f0f9ff 0%, #e0f2fe 55%, #bae6fd 100%)',
|
||||||
|
'--t-header-fg': '#0c4a6e',
|
||||||
|
'--t-header-glow': 'rgba(255,255,255,.6)',
|
||||||
|
'--t-bg': '#f8fbff',
|
||||||
|
'--t-surface': '#ffffff',
|
||||||
|
'--t-text': '#0f172a',
|
||||||
|
'--t-muted': '#64748b',
|
||||||
|
'--t-border': '#dbeafe',
|
||||||
|
'--t-shadow': '0 8px 24px rgba(14, 165, 233, 0.08)',
|
||||||
|
'--t-banner-a': '#bae6fd',
|
||||||
|
'--t-banner-b': '#7dd3fc',
|
||||||
|
'--t-banner-fg': '#0c4a6e',
|
||||||
|
'--t-todo-bg': 'linear-gradient(180deg, #f0f9ff 0%, #ffffff 70%)',
|
||||||
|
'--t-warning': '#d97706',
|
||||||
|
'--t-success': '#16a34a'
|
||||||
|
},
|
||||||
|
menus: [
|
||||||
|
['审', '#ffe4e6', '#e11d48', '发起审批'],
|
||||||
|
['待', '#e0f2fe', '#0284c7', '我的待办'],
|
||||||
|
['协', '#ecfdf5', '#16a34a', '协议管理'],
|
||||||
|
['样', '#fef3c7', '#d97706', '样本查询'],
|
||||||
|
['质', '#ede9fe', '#7c3aed', '质检工单'],
|
||||||
|
['库', '#cffafe', '#0891b2', '入库登记'],
|
||||||
|
['报', '#f1f5f9', '#475569', '统计报表'],
|
||||||
|
['设', '#f8fafc', '#64748b', '个人设置']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
teal: {
|
||||||
|
id: 'teal',
|
||||||
|
name: 'C · 青瓷医疗',
|
||||||
|
swatch: '#14b8a6',
|
||||||
|
desc: '偏医疗 / 实验室气质的青绿(teal),白瓷感背景,干净专业,不压抑。',
|
||||||
|
tokens: [
|
||||||
|
['brand', '#14b8a6', '主色 / 按钮'],
|
||||||
|
['brandDark', '#0f766e', '主色深'],
|
||||||
|
['brandSoft', '#f0fdfa', '主色浅底'],
|
||||||
|
['headerBg', 'linear-gradient(160deg, #f0fdfa 0%, #ccfbf1 60%, #99f6e4 100%)', '顶栏背景'],
|
||||||
|
['headerFg', '#115e59', '顶栏文字'],
|
||||||
|
['bg', '#f7fffe', '页面背景'],
|
||||||
|
['surface', '#ffffff', '卡片表面'],
|
||||||
|
['text', '#134e4a', '主文字'],
|
||||||
|
['muted', '#5f7c78', '次要文字'],
|
||||||
|
['border', '#ccfbf1', '边框'],
|
||||||
|
['accent', '#f59e0b', '辅助强调'],
|
||||||
|
['warning', '#d97706', '警告'],
|
||||||
|
['danger', '#ef4444', '危险'],
|
||||||
|
['success', '#0d9488', '成功']
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
'--t-brand': '#14b8a6',
|
||||||
|
'--t-brand-dark': '#0f766e',
|
||||||
|
'--t-brand-soft': '#f0fdfa',
|
||||||
|
'--t-header-bg': 'linear-gradient(160deg, #f0fdfa 0%, #ccfbf1 60%, #99f6e4 100%)',
|
||||||
|
'--t-header-fg': '#115e59',
|
||||||
|
'--t-header-glow': 'rgba(255,255,255,.55)',
|
||||||
|
'--t-bg': '#f7fffe',
|
||||||
|
'--t-surface': '#ffffff',
|
||||||
|
'--t-text': '#134e4a',
|
||||||
|
'--t-muted': '#5f7c78',
|
||||||
|
'--t-border': '#ccfbf1',
|
||||||
|
'--t-shadow': '0 8px 24px rgba(13, 148, 136, 0.08)',
|
||||||
|
'--t-banner-a': '#99f6e4',
|
||||||
|
'--t-banner-b': '#5eead4',
|
||||||
|
'--t-banner-fg': '#115e59',
|
||||||
|
'--t-todo-bg': 'linear-gradient(180deg, #f0fdfa 0%, #ffffff 70%)',
|
||||||
|
'--t-warning': '#d97706',
|
||||||
|
'--t-success': '#0d9488'
|
||||||
|
},
|
||||||
|
menus: [
|
||||||
|
['审', '#ffe4e6', '#e11d48', '发起审批'],
|
||||||
|
['待', '#f0fdfa', '#0f766e', '我的待办'],
|
||||||
|
['协', '#e0f2fe', '#0284c7', '协议管理'],
|
||||||
|
['样', '#fef3c7', '#d97706', '样本查询'],
|
||||||
|
['质', '#ede9fe', '#7c3aed', '质检工单'],
|
||||||
|
['库', '#ccfbf1', '#0f766e', '入库登记'],
|
||||||
|
['报', '#f1f5f9', '#475569', '统计报表'],
|
||||||
|
['设', '#f8fafc', '#64748b', '个人设置']
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dawn: {
|
||||||
|
id: 'dawn',
|
||||||
|
name: 'D · 暖晨光',
|
||||||
|
swatch: '#84cc16',
|
||||||
|
desc: '浅暖米背景 + 柔和嫩绿顶栏,带一点大麦黄,像早晨阳光,亲和不沉重。',
|
||||||
|
tokens: [
|
||||||
|
['brand', '#65a30d', '主色 / 按钮'],
|
||||||
|
['brandDark', '#4d7c0f', '主色深'],
|
||||||
|
['brandSoft', '#f7fee7', '主色浅底'],
|
||||||
|
['headerBg', 'linear-gradient(160deg, #fffbeb 0%, #fef9c3 45%, #d9f99d 100%)', '顶栏背景'],
|
||||||
|
['headerFg', '#3f6212', '顶栏文字'],
|
||||||
|
['bg', '#fffcf5', '页面背景'],
|
||||||
|
['surface', '#ffffff', '卡片表面'],
|
||||||
|
['text', '#1c1917', '主文字'],
|
||||||
|
['muted', '#78716c', '次要文字'],
|
||||||
|
['border', '#f5e6c8', '边框'],
|
||||||
|
['accent', '#eab308', '大麦黄'],
|
||||||
|
['warning', '#d97706', '警告'],
|
||||||
|
['danger', '#ef4444', '危险'],
|
||||||
|
['success', '#65a30d', '成功']
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
'--t-brand': '#65a30d',
|
||||||
|
'--t-brand-dark': '#4d7c0f',
|
||||||
|
'--t-brand-soft': '#f7fee7',
|
||||||
|
'--t-header-bg': 'linear-gradient(160deg, #fffbeb 0%, #fef9c3 45%, #d9f99d 100%)',
|
||||||
|
'--t-header-fg': '#3f6212',
|
||||||
|
'--t-header-glow': 'rgba(255,255,255,.5)',
|
||||||
|
'--t-bg': '#fffcf5',
|
||||||
|
'--t-surface': '#ffffff',
|
||||||
|
'--t-text': '#1c1917',
|
||||||
|
'--t-muted': '#78716c',
|
||||||
|
'--t-border': '#f5e6c8',
|
||||||
|
'--t-shadow': '0 8px 24px rgba(101, 163, 13, 0.08)',
|
||||||
|
'--t-banner-a': '#fde68a',
|
||||||
|
'--t-banner-b': '#bef264',
|
||||||
|
'--t-banner-fg': '#3f6212',
|
||||||
|
'--t-todo-bg': 'linear-gradient(180deg, #fffbeb 0%, #ffffff 70%)',
|
||||||
|
'--t-warning': '#d97706',
|
||||||
|
'--t-success': '#65a30d'
|
||||||
|
},
|
||||||
|
menus: [
|
||||||
|
['审', '#ffe4e6', '#e11d48', '发起审批'],
|
||||||
|
['待', '#f7fee7', '#4d7c0f', '我的待办'],
|
||||||
|
['协', '#e0f2fe', '#0284c7', '协议管理'],
|
||||||
|
['样', '#fef3c7', '#d97706', '样本查询'],
|
||||||
|
['质', '#ede9fe', '#7c3aed', '质检工单'],
|
||||||
|
['库', '#ecfccb', '#4d7c0f', '入库登记'],
|
||||||
|
['报', '#f5f5f4', '#57534e', '统计报表'],
|
||||||
|
['设', '#fafaf9', '#78716c', '个人设置']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = ['mint', 'sky', 'teal', 'dawn']
|
||||||
|
let active = 'mint'
|
||||||
|
|
||||||
|
const tabsEl = document.getElementById('tabs')
|
||||||
|
const tokensEl = document.getElementById('tokens')
|
||||||
|
const menusEl = document.getElementById('menus')
|
||||||
|
const phoneEl = document.getElementById('phone')
|
||||||
|
const root = document.documentElement
|
||||||
|
|
||||||
|
function renderTabs() {
|
||||||
|
tabsEl.innerHTML = order.map((id) => {
|
||||||
|
const t = THEMES[id]
|
||||||
|
const rec = id === 'mint' ? '<span class="badge">推荐</span>' : ''
|
||||||
|
return `<button class="theme-tab ${id === active ? 'is-active' : ''}" data-id="${id}" style="--swatch:${t.swatch};--t-brand:${t.css['--t-brand']};--t-brand-dark:${t.css['--t-brand-dark']};--t-brand-soft:${t.css['--t-brand-soft']}">
|
||||||
|
<span class="dot"></span>${t.name}${rec}
|
||||||
|
</button>`
|
||||||
|
}).join('')
|
||||||
|
tabsEl.querySelectorAll('.theme-tab').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
active = btn.dataset.id
|
||||||
|
applyTheme()
|
||||||
|
// for visual companion selection
|
||||||
|
if (typeof toggleSelect === 'function') {
|
||||||
|
const fake = { dataset: { choice: active }, classList: { add() {}, remove() {}, contains() { return false } } }
|
||||||
|
}
|
||||||
|
window.parent && window.parent.postMessage
|
||||||
|
document.dispatchEvent(new CustomEvent('theme-selected', { detail: active }))
|
||||||
|
try {
|
||||||
|
const payload = { type: 'click', choice: active, text: THEMES[active].name, timestamp: Math.floor(Date.now() / 1000) }
|
||||||
|
// companion helper may listen to option clicks; also expose for agent
|
||||||
|
window.__selectedTheme = active
|
||||||
|
} catch (e) {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme() {
|
||||||
|
const t = THEMES[active]
|
||||||
|
Object.entries(t.css).forEach(([k, v]) => phoneEl.style.setProperty(k, v))
|
||||||
|
// also set on page for tab active colors
|
||||||
|
Object.entries(t.css).forEach(([k, v]) => root.style.setProperty(k, v))
|
||||||
|
|
||||||
|
document.getElementById('themeTitle').textContent = t.name
|
||||||
|
document.getElementById('themeDesc').textContent = t.desc
|
||||||
|
document.getElementById('previewName').textContent = t.name
|
||||||
|
document.getElementById('targetHeader').style.background = t.css['--t-header-bg']
|
||||||
|
|
||||||
|
tokensEl.innerHTML = t.tokens.map(([name, hex, desc]) => {
|
||||||
|
const isGrad = String(hex).includes('gradient')
|
||||||
|
const swatch = isGrad
|
||||||
|
? `<i style="background:${hex}"></i>`
|
||||||
|
: `<i style="background:${hex}"></i>`
|
||||||
|
return `<div class="token">${swatch}<div><div class="name">${name}</div><div class="desc">${desc}</div></div><code>${hex}</code></div>`
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
menusEl.innerHTML = t.menus.map(([ch, bg, fg, label]) =>
|
||||||
|
`<div class="menu-item"><div class="menu-icon" style="background:${bg};color:${fg}">${ch}</div><span>${label}</span></div>`
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
renderTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// companion-friendly selectable cards at bottom via data-choice hooks
|
||||||
|
function injectCompanionHooks() {
|
||||||
|
// Add invisible option markers for companion selection if wrapped
|
||||||
|
order.forEach((id) => {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.className = 'option'
|
||||||
|
el.dataset.choice = id
|
||||||
|
el.style.display = 'none'
|
||||||
|
el.onclick = () => {}
|
||||||
|
document.body.appendChild(el)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme()
|
||||||
|
injectCompanionHooks()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- companion selectable options -->
|
||||||
|
<div class="options" style="max-width:1180px;margin:0 auto;padding:0 18px 40px;display:grid;gap:10px">
|
||||||
|
<p style="margin:0 0 4px;font-size:13px;color:#64748b">选择偏好方案(可点击下方卡片,也可在上方 Tab 预览):</p>
|
||||||
|
<div class="option" data-choice="mint" onclick="toggleSelect && toggleSelect(this); document.querySelector('[data-id=mint]')?.click()" style="display:flex;gap:12px;align-items:center;padding:12px 14px;border:1px solid #e2e8f0;border-radius:14px;background:#fff;cursor:pointer">
|
||||||
|
<div class="letter" style="width:28px;height:28px;border-radius:8px;background:#ecfdf5;color:#065f46;display:grid;place-items:center;font-weight:700">A</div>
|
||||||
|
<div class="content"><h3 style="margin:0;font-size:14px">薄荷微风(推荐)</h3><p style="margin:2px 0 0;font-size:12px;color:#64748b">浅绿顶栏 + 品牌绿,最稳妥</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="option" data-choice="sky" onclick="toggleSelect && toggleSelect(this); document.querySelector('[data-id=sky]')?.click()" style="display:flex;gap:12px;align-items:center;padding:12px 14px;border:1px solid #e2e8f0;border-radius:14px;background:#fff;cursor:pointer">
|
||||||
|
<div class="letter" style="width:28px;height:28px;border-radius:8px;background:#e0f2fe;color:#0c4a6e;display:grid;place-items:center;font-weight:700">B</div>
|
||||||
|
<div class="content"><h3 style="margin:0;font-size:14px">晴空青</h3><p style="margin:2px 0 0;font-size:12px;color:#64748b">天蓝顶栏,更透气</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="option" data-choice="teal" onclick="toggleSelect && toggleSelect(this); document.querySelector('[data-id=teal]')?.click()" style="display:flex;gap:12px;align-items:center;padding:12px 14px;border:1px solid #e2e8f0;border-radius:14px;background:#fff;cursor:pointer">
|
||||||
|
<div class="letter" style="width:28px;height:28px;border-radius:8px;background:#f0fdfa;color:#115e59;display:grid;place-items:center;font-weight:700">C</div>
|
||||||
|
<div class="content"><h3 style="margin:0;font-size:14px">青瓷医疗</h3><p style="margin:2px 0 0;font-size:12px;color:#64748b">teal 专业、干净</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="option" data-choice="dawn" onclick="toggleSelect && toggleSelect(this); document.querySelector('[data-id=dawn]')?.click()" style="display:flex;gap:12px;align-items:center;padding:12px 14px;border:1px solid #e2e8f0;border-radius:14px;background:#fff;cursor:pointer">
|
||||||
|
<div class="letter" style="width:28px;height:28px;border-radius:8px;background:#f7fee7;color:#3f6212;display:grid;place-items:center;font-weight:700">D</div>
|
||||||
|
<div class="content"><h3 style="margin:0;font-size:14px">暖晨光</h3><p style="margin:2px 0 0;font-size:12px;color:#64748b">暖米 + 嫩绿,亲和</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,841 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>mobile-h5 · 控件样式 Demo(输入 / 下拉 / Tag / Tab)</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ink: #1f2937;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--line: #e5e7eb;
|
||||||
|
--page: #f3f4f6;
|
||||||
|
--brand: #22c55e;
|
||||||
|
--brand-ink: #166534;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; min-height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1240px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 16px 80px;
|
||||||
|
}
|
||||||
|
h1 { margin: 0 0 8px; font-size: 24px; letter-spacing: -0.02em; }
|
||||||
|
.sub { margin: 0; color: var(--muted); font-size: 14px; max-width: 760px; }
|
||||||
|
.note {
|
||||||
|
margin: 14px 0 22px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.note strong { color: var(--brand-ink); }
|
||||||
|
|
||||||
|
.scheme-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.scheme-tab {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 9px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #475569;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.scheme-tab.is-on {
|
||||||
|
border-color: #94a3b8;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.scheme-tab .rec {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
.scheme-tab.is-on .rec {
|
||||||
|
background: rgba(255,255,255,.18);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 390px;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
.panel h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.panel .desc {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block { margin-bottom: 22px; }
|
||||||
|
.block:last-child { margin-bottom: 0; }
|
||||||
|
.block-label {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 手机预览壳 ========== */
|
||||||
|
.phone {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 390px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 36px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 24px 50px rgba(15, 23, 42, 0.18);
|
||||||
|
}
|
||||||
|
.phone-screen {
|
||||||
|
border-radius: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
min-height: 720px;
|
||||||
|
}
|
||||||
|
.phone-nav {
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 方案公共预览区 ========== */
|
||||||
|
.preview-pad { padding: 12px 14px 20px; background: #fff; }
|
||||||
|
.preview-stack { display: grid; gap: 16px; }
|
||||||
|
|
||||||
|
/* —— 控件原子(由 scheme class 覆盖) —— */
|
||||||
|
.ui-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ui-search input {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink);
|
||||||
|
min-width: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-search input::placeholder { color: #b0b7c3; }
|
||||||
|
.ui-ico { color: #9ca3af; font-size: 14px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.ui-select {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ui-select-trigger {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.ui-select-trigger .ph { color: #b0b7c3; }
|
||||||
|
.ui-select-trigger .chev {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform .15s;
|
||||||
|
}
|
||||||
|
.ui-select.open .ui-select-trigger .chev { transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.ui-dropdown {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
z-index: 20;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
.ui-select.open .ui-dropdown { display: block; }
|
||||||
|
.ui-dropdown .dd-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
.ui-dropdown .dd-search input {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-dropdown .dd-list {
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 4px 0 8px;
|
||||||
|
}
|
||||||
|
.ui-dropdown .dd-item {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.ui-dropdown .dd-item:hover { background: #f8fafc; }
|
||||||
|
.ui-dropdown .dd-item.is-on { font-weight: 650; }
|
||||||
|
|
||||||
|
.ui-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ui-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-tag.is-on { font-weight: 650; }
|
||||||
|
|
||||||
|
.ui-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.ui-tab {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-tab.is-on {
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== A 极简线框 ========== */
|
||||||
|
.scheme-a .ui-search {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.scheme-a .ui-select-trigger {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.scheme-a .ui-dropdown {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 28px rgba(15,23,42,.08);
|
||||||
|
}
|
||||||
|
.scheme-a .ui-dropdown .dd-search {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.scheme-a .ui-dropdown .dd-item.is-on { color: #111827; background: #f3f4f6; }
|
||||||
|
.scheme-a .ui-tag {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.scheme-a .ui-tag.is-on {
|
||||||
|
border-color: #111827;
|
||||||
|
color: #111827;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.scheme-a .ui-tabs {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.scheme-a .ui-tab.is-on::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 14px; right: 14px; bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== B 浅灰填充 ========== */
|
||||||
|
.scheme-b .ui-search {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-select-trigger {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-dropdown {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 12px 32px rgba(15,23,42,.12);
|
||||||
|
}
|
||||||
|
.scheme-b .ui-dropdown .dd-search {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-dropdown .dd-item.is-on { background: #eef2ff; color: #3730a3; }
|
||||||
|
.scheme-b .ui-tag {
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-tag.is-on {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-tabs {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-tab {
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.scheme-b .ui-tab.is-on {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== C 分段控件 ========== */
|
||||||
|
.scheme-c .ui-search {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f5f5f7;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-select-trigger {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f5f5f7;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-dropdown {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 16px 40px rgba(15,23,42,.14);
|
||||||
|
border: 1px solid rgba(15,23,42,.06);
|
||||||
|
}
|
||||||
|
.scheme-c .ui-dropdown .dd-search {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f5f5f7;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-dropdown .dd-item.is-on { color: #007aff; background: #f0f7ff; }
|
||||||
|
.scheme-c .ui-tag {
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5f5f7;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-tag.is-on {
|
||||||
|
background: #111827;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-tabs {
|
||||||
|
background: #f5f5f7;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-tab {
|
||||||
|
flex: 1;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.scheme-c .ui-tab.is-on {
|
||||||
|
background: #fff;
|
||||||
|
color: #111827;
|
||||||
|
box-shadow: 0 1px 3px rgba(15,23,42,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== D 清爽描边 + 轻主色(推荐) ========== */
|
||||||
|
.scheme-d .ui-search {
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-search:focus-within {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
.scheme-d .ui-select-trigger {
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-select.open .ui-select-trigger {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-dropdown {
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 28px rgba(15,23,42,.1);
|
||||||
|
}
|
||||||
|
.scheme-d .ui-dropdown .dd-search {
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-dropdown .dd-item.is-on {
|
||||||
|
color: var(--brand-ink);
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-tag {
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-tag.is-on {
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: var(--brand-ink);
|
||||||
|
}
|
||||||
|
.scheme-d .ui-tabs {
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.scheme-d .ui-tab.is-on {
|
||||||
|
color: var(--brand-ink);
|
||||||
|
}
|
||||||
|
.scheme-d .ui-tab.is-on::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 14px; right: 14px; bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--brand);
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-list {
|
||||||
|
padding: 12px 14px 20px;
|
||||||
|
}
|
||||||
|
.phone-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e8eaed;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.phone-card .ico {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
font-size: 12px; font-weight: 700; color: #64748b;
|
||||||
|
}
|
||||||
|
.phone-card .name { font-size: 14px; font-weight: 650; }
|
||||||
|
.phone-card .meta { font-size: 12px; color: #94a3b8; margin-top: 2px; }
|
||||||
|
.phone-card .go {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--brand);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.token-list code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #eef2f7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare {
|
||||||
|
margin-top: 22px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.compare { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.compare { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
.compare .mini {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.compare .mini:hover,
|
||||||
|
.compare .mini.is-on {
|
||||||
|
border-color: #94a3b8;
|
||||||
|
box-shadow: 0 0 0 3px rgba(148,163,184,.2);
|
||||||
|
}
|
||||||
|
.compare .mini h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>控件样式 Demo · 输入 / 下拉 / Tag / Tab</h1>
|
||||||
|
<p class="sub">
|
||||||
|
针对「发起审批」顶部检索与分类,以及表单里常见的下拉选择。以下 4 套均为中性清爽风格,主色只作轻量点缀(或完全不用)。
|
||||||
|
点选方案后,右侧手机预览会同步切换。
|
||||||
|
</p>
|
||||||
|
<div class="note">
|
||||||
|
文件路径:<strong>mobile-ui/zkxw-mobile-control-style-demos.html</strong>
|
||||||
|
· 本地直接打开即可。选定后告诉我方案代号(A/B/C/D),再落到工程。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scheme-tabs" id="schemeTabs"></div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<aside class="panel" id="specPanel">
|
||||||
|
<h2 id="schemeTitle">—</h2>
|
||||||
|
<p class="desc" id="schemeDesc">—</p>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-label">1. 搜索输入框</div>
|
||||||
|
<div id="demoSearch"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-label">2. 下拉框</div>
|
||||||
|
<div id="demoSelect"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-label">3. 下拉框(内嵌检索)</div>
|
||||||
|
<div id="demoSelectSearch"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-label">4. Tag 标签</div>
|
||||||
|
<div id="demoTags"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-label">5. Tab 栏</div>
|
||||||
|
<div id="demoTabs"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="token-list" id="tokenList"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="phone">
|
||||||
|
<div class="phone-screen" id="phoneRoot">
|
||||||
|
<div class="phone-nav">发起审批</div>
|
||||||
|
<div class="preview-pad" id="phoneToolbar"></div>
|
||||||
|
<div class="phone-list">
|
||||||
|
<div class="phone-card">
|
||||||
|
<div class="ico">假</div>
|
||||||
|
<div>
|
||||||
|
<div class="name">请假申请</div>
|
||||||
|
<div class="meta">人事 · 通用</div>
|
||||||
|
</div>
|
||||||
|
<div class="go">发起</div>
|
||||||
|
</div>
|
||||||
|
<div class="phone-card">
|
||||||
|
<div class="ico">差</div>
|
||||||
|
<div>
|
||||||
|
<div class="name">出差申请</div>
|
||||||
|
<div class="meta">行政</div>
|
||||||
|
</div>
|
||||||
|
<div class="go">发起</div>
|
||||||
|
</div>
|
||||||
|
<div class="phone-card">
|
||||||
|
<div class="ico">报</div>
|
||||||
|
<div>
|
||||||
|
<div class="name">费用报销</div>
|
||||||
|
<div class="meta">财务</div>
|
||||||
|
</div>
|
||||||
|
<div class="go">发起</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="compare" id="compare"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const SCHEMES = {
|
||||||
|
a: {
|
||||||
|
id: 'a',
|
||||||
|
name: 'A · 极简线框',
|
||||||
|
desc: '白底细线框,Tab 用下划线。最克制,适合信息密度高的列表页。完全不用主题绿。',
|
||||||
|
tokens: [
|
||||||
|
'搜索:白底 + #e5e7eb 描边,圆角 10',
|
||||||
|
'下拉:同搜索线框;展开层白底阴影',
|
||||||
|
'Tag:线框方圆角;选中加深描边',
|
||||||
|
'Tab:文字 + 黑色下划线'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
id: 'b',
|
||||||
|
name: 'B · 浅灰填充',
|
||||||
|
desc: '控件用浅灰底、无描边。Tag / Tab 都是胶囊。柔和,但选中对比偏弱。',
|
||||||
|
tokens: [
|
||||||
|
'搜索 / 下拉:#f3f4f6 填充,圆角 12',
|
||||||
|
'内嵌检索:同浅灰底',
|
||||||
|
'Tag / Tab:灰色胶囊,选中更深灰'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
c: {
|
||||||
|
id: 'c',
|
||||||
|
name: 'C · 分段控件',
|
||||||
|
desc: '搜索做成胶囊;分类用 iOS 分段控件。适合分类少(≤5)的场景。',
|
||||||
|
tokens: [
|
||||||
|
'搜索:圆角胶囊 #f5f5f7',
|
||||||
|
'Tag:选中黑底白字',
|
||||||
|
'Tab:整条灰底,选中白块浮起'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
d: {
|
||||||
|
id: 'd',
|
||||||
|
name: 'D · 清爽描边',
|
||||||
|
badge: '推荐',
|
||||||
|
desc: '浅灰底 + 细描边,聚焦时微阴影。Tag/Tab 选中才用极浅绿点缀,主色不抢戏。最适合当前「明亮但不花」目标。',
|
||||||
|
tokens: [
|
||||||
|
'搜索:#fafafa + #e8eaed 边,聚焦提亮',
|
||||||
|
'下拉检索:同风格;选中项浅绿底',
|
||||||
|
'Tag:白底描边胶囊;选中浅绿',
|
||||||
|
'Tab:下划线用主色,文字深绿'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUPS = ['全部', '人事', '行政', '财务', '其他']
|
||||||
|
const OPTIONS = ['请假申请', '出差申请', '加班申请', '费用报销', '用章申请', '采购申请']
|
||||||
|
|
||||||
|
let active = 'd'
|
||||||
|
let tabOn = 0
|
||||||
|
let tagOn = 0
|
||||||
|
let selectVal = ''
|
||||||
|
let selectSearchVal = ''
|
||||||
|
let openSelect = '' // 'plain' | 'search' | ''
|
||||||
|
|
||||||
|
function el(html) {
|
||||||
|
const t = document.createElement('template')
|
||||||
|
t.innerHTML = html.trim()
|
||||||
|
return t.content.firstElementChild
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSchemeTabs() {
|
||||||
|
const root = document.getElementById('schemeTabs')
|
||||||
|
root.innerHTML = ''
|
||||||
|
Object.values(SCHEMES).forEach((s) => {
|
||||||
|
const b = el(`<button class="scheme-tab ${s.id === active ? 'is-on' : ''}" type="button">
|
||||||
|
${s.name}${s.badge ? `<span class="rec">${s.badge}</span>` : ''}
|
||||||
|
</button>`)
|
||||||
|
b.onclick = () => { active = s.id; openSelect = ''; renderAll() }
|
||||||
|
root.appendChild(b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCompare() {
|
||||||
|
const root = document.getElementById('compare')
|
||||||
|
root.innerHTML = ''
|
||||||
|
Object.values(SCHEMES).forEach((s) => {
|
||||||
|
const card = el(`<div class="mini ${s.id === active ? 'is-on' : ''} scheme-${s.id}" data-choice="${s.id}">
|
||||||
|
<h3>${s.name}</h3>
|
||||||
|
<div class="ui-search" style="margin-bottom:8px"><span class="ui-ico">⌕</span><input placeholder="搜索审批单" readonly /></div>
|
||||||
|
<div class="ui-tags" style="margin-bottom:8px">
|
||||||
|
<button class="ui-tag is-on" type="button">全部</button>
|
||||||
|
<button class="ui-tag" type="button">人事</button>
|
||||||
|
</div>
|
||||||
|
<div class="ui-tabs">
|
||||||
|
<button class="ui-tab is-on" type="button">全部</button>
|
||||||
|
<button class="ui-tab" type="button">人事</button>
|
||||||
|
<button class="ui-tab" type="button">行政</button>
|
||||||
|
</div>
|
||||||
|
</div>`)
|
||||||
|
card.onclick = () => { active = s.id; openSelect = ''; renderAll() }
|
||||||
|
root.appendChild(card)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchHtml() {
|
||||||
|
return `<div class="ui-search"><span class="ui-ico">⌕</span><input placeholder="搜索审批单" /><span class="ui-ico">×</span></div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectHtml(kind) {
|
||||||
|
const isOpen = openSelect === kind
|
||||||
|
const val = kind === 'plain' ? selectVal : selectSearchVal
|
||||||
|
const label = val || '<span class="ph">请选择审批类型</span>'
|
||||||
|
const items = OPTIONS.map((o) =>
|
||||||
|
`<div class="dd-item ${o === val ? 'is-on' : ''}" data-v="${o}">${o}</div>`
|
||||||
|
).join('')
|
||||||
|
const searchBar = kind === 'search'
|
||||||
|
? `<div class="dd-search"><span class="ui-ico">⌕</span><input placeholder="输入关键词筛选" /></div>`
|
||||||
|
: ''
|
||||||
|
return `<div class="ui-select ${isOpen ? 'open' : ''}" data-kind="${kind}">
|
||||||
|
<button type="button" class="ui-select-trigger"><span>${label}</span><span class="chev">▾</span></button>
|
||||||
|
<div class="ui-dropdown">${searchBar}<div class="dd-list">${items}</div></div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagsHtml() {
|
||||||
|
return `<div class="ui-tags">${GROUPS.map((g, i) =>
|
||||||
|
`<button type="button" class="ui-tag ${i === tagOn ? 'is-on' : ''}" data-i="${i}">${g}</button>`
|
||||||
|
).join('')}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsHtml() {
|
||||||
|
return `<div class="ui-tabs">${GROUPS.map((g, i) =>
|
||||||
|
`<button type="button" class="ui-tab ${i === tabOn ? 'is-on' : ''}" data-i="${i}">${g}</button>`
|
||||||
|
).join('')}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindSelect(root) {
|
||||||
|
root.querySelectorAll('.ui-select').forEach((box) => {
|
||||||
|
const kind = box.dataset.kind
|
||||||
|
box.querySelector('.ui-select-trigger').onclick = (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
openSelect = openSelect === kind ? '' : kind
|
||||||
|
renderAll()
|
||||||
|
}
|
||||||
|
box.querySelectorAll('.dd-item').forEach((item) => {
|
||||||
|
item.onclick = (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (kind === 'plain') selectVal = item.dataset.v
|
||||||
|
else selectSearchVal = item.dataset.v
|
||||||
|
openSelect = ''
|
||||||
|
renderAll()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindTagsTabs(root) {
|
||||||
|
root.querySelectorAll('.ui-tag').forEach((t) => {
|
||||||
|
t.onclick = () => { tagOn = Number(t.dataset.i); renderAll() }
|
||||||
|
})
|
||||||
|
root.querySelectorAll('.ui-tab').forEach((t) => {
|
||||||
|
t.onclick = () => { tabOn = Number(t.dataset.i); renderAll() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAll() {
|
||||||
|
const s = SCHEMES[active]
|
||||||
|
document.getElementById('schemeTitle').textContent = s.name
|
||||||
|
document.getElementById('schemeDesc').textContent = s.desc
|
||||||
|
document.getElementById('tokenList').innerHTML = s.tokens.map((t) => `<div>· ${t}</div>`).join('')
|
||||||
|
|
||||||
|
const panel = document.getElementById('specPanel')
|
||||||
|
panel.className = `panel scheme-${active}`
|
||||||
|
|
||||||
|
document.getElementById('demoSearch').innerHTML = searchHtml()
|
||||||
|
document.getElementById('demoSelect').innerHTML = selectHtml('plain')
|
||||||
|
document.getElementById('demoSelectSearch').innerHTML = selectHtml('search')
|
||||||
|
document.getElementById('demoTags').innerHTML = tagsHtml()
|
||||||
|
document.getElementById('demoTabs').innerHTML = tabsHtml()
|
||||||
|
bindSelect(panel)
|
||||||
|
bindTagsTabs(panel)
|
||||||
|
|
||||||
|
const phone = document.getElementById('phoneRoot')
|
||||||
|
phone.className = `phone-screen scheme-${active}`
|
||||||
|
document.getElementById('phoneToolbar').innerHTML =
|
||||||
|
`<div class="preview-stack">${searchHtml()}${tabsHtml()}</div>`
|
||||||
|
bindTagsTabs(document.getElementById('phoneToolbar'))
|
||||||
|
|
||||||
|
renderSchemeTabs()
|
||||||
|
renderCompare()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
if (!openSelect) return
|
||||||
|
openSelect = ''
|
||||||
|
renderAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
renderAll()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Graph Report - src (2026-08-11)
|
# Graph Report - src (2026-08-12)
|
||||||
|
|
||||||
## Corpus Check
|
## 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
|
## Summary
|
||||||
- 3417 nodes · 4865 edges · 71 communities detected
|
- 3417 nodes · 4865 edges · 71 communities detected
|
||||||
|
|||||||
@@ -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/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/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/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/components.d.ts": 1786506822.9277327,
|
||||||
"/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/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/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/types/router/index.ts": 1782394569.8806834,
|
||||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1783921717.4015942,
|
"/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/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/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/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/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/OaPrintRelatedTree.vue": 1786413860.094557,
|
||||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaFcRow.vue": 1785145511.6907504,
|
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaFcRow.vue": 1785145511.6907504,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
if (resolved.value.kind === 'ep') {
|
if (resolved.value.kind === 'ep') {
|
||||||
return { background: resolved.value.bg, color: resolved.value.fg }
|
return { background: resolved.value.bg, color: resolved.value.fg }
|
||||||
}
|
}
|
||||||
return { background: '#f1f5f9' }
|
return {}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,25 @@
|
|||||||
恢复转正日期自动计算
|
恢复转正日期自动计算
|
||||||
</el-button>
|
</el-button>
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="leaveDurationManualFields.length" class="oa-start__tip">
|
||||||
|
<el-button link type="primary" @click="restoreLeaveAuto">
|
||||||
|
<el-icon><RefreshRight /></el-icon>
|
||||||
|
恢复请假时长自动计算
|
||||||
|
</el-button>
|
||||||
|
</p>
|
||||||
<p v-if="overtimeDurationManualFields.length" class="oa-start__tip">
|
<p v-if="overtimeDurationManualFields.length" class="oa-start__tip">
|
||||||
<el-button link type="primary" @click="restoreOvertimeAuto">
|
<el-button link type="primary" @click="restoreOvertimeAuto">
|
||||||
<el-icon><RefreshRight /></el-icon>
|
<el-icon><RefreshRight /></el-icon>
|
||||||
恢复加班时长自动计算
|
恢复加班时长自动计算
|
||||||
</el-button>
|
</el-button>
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="!formRule.length && !detailLoading" class="oa-start__tip">
|
<p v-if="tripDaysManualFields.length" class="oa-start__tip">
|
||||||
|
<el-button link type="primary" @click="restoreTripDaysAuto">
|
||||||
|
<el-icon><RefreshRight /></el-icon>
|
||||||
|
恢复出差天数自动计算
|
||||||
|
</el-button>
|
||||||
|
</p>
|
||||||
|
<p v-if="!formRule.length && !detailLoading" class="oa-start__tip">
|
||||||
该模板暂无表单字段,仍可直接提交。
|
该模板暂无表单字段,仍可直接提交。
|
||||||
</p>
|
</p>
|
||||||
<OaFlowPreview
|
<OaFlowPreview
|
||||||
@@ -165,6 +177,16 @@
|
|||||||
listManualConfirmationDateFields({ rule: formRule.value }, formData.value)
|
listManualConfirmationDateFields({ rule: formRule.value }, formData.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const leaveDurationManualFields = computed(() => {
|
||||||
|
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 overtimeDurationManualFields = computed(() => {
|
||||||
const fields: string[] = []
|
const fields: string[] = []
|
||||||
for (const kit of listOvertimeKitBindings({ rule: formRule.value })) {
|
for (const kit of listOvertimeKitBindings({ rule: formRule.value })) {
|
||||||
@@ -175,6 +197,16 @@
|
|||||||
return fields
|
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 {
|
function flowNeedsInitiatorSelect(node: any): boolean {
|
||||||
if (!node || typeof node !== 'object') return false
|
if (!node || typeof node !== 'object') return false
|
||||||
const cfg = node.approverConfig || {}
|
const cfg = node.approverConfig || {}
|
||||||
@@ -685,6 +717,24 @@
|
|||||||
void syncOvertimeKitDurations()
|
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(
|
watch(
|
||||||
formData,
|
formData,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -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`);
|
|
||||||