提交
This commit is contained in:
1 parent
8aab018558
commit
1b2896c877
70 files changed
+7721
-3274
No files matched your search
@@ -88,6 +88,26 @@ def _user_menu_ids(db: Session, user_id: str) -> Set[str]:
|
||||
return {str(r[0]).strip() for r in rows if r and r[0]}
|
||||
|
||||
|
||||
def _mobile_menu_icon(path: str) -> str:
|
||||
"""按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。"""
|
||||
p = (path or "").lower()
|
||||
if "oa/start" in p:
|
||||
return "approve"
|
||||
if "form-sink" in p:
|
||||
return "orders-o"
|
||||
return "apps-o"
|
||||
|
||||
|
||||
def _mobile_menu_icon_colors(path: str) -> dict:
|
||||
"""宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
|
||||
p = (path or "").lower()
|
||||
if "oa/start" in p:
|
||||
return {"iconBg": "#ffe4e6", "iconFg": "#e11d48"}
|
||||
if "form-sink" in p:
|
||||
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
||||
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
||||
|
||||
|
||||
@router.get("/menus")
|
||||
async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
|
||||
try:
|
||||
@@ -116,12 +136,15 @@ async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
|
||||
continue
|
||||
if allowed_ids is not None and m.id not in allowed_ids:
|
||||
continue
|
||||
colors = _mobile_menu_icon_colors(path)
|
||||
items.append(
|
||||
{
|
||||
"id": m.id,
|
||||
"title": m.menu_name,
|
||||
"path": path if path.startswith("/") else f"/{path}",
|
||||
"icon": "edit",
|
||||
"icon": _mobile_menu_icon(path),
|
||||
"iconBg": colors["iconBg"],
|
||||
"iconFg": colors["iconFg"],
|
||||
"perms": (m.perms or "").strip() or None,
|
||||
}
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -47,21 +47,21 @@
|
||||
"label": "_mask_secret()",
|
||||
"file_type": "code",
|
||||
"source_file": "main.py",
|
||||
"source_location": "L151"
|
||||
"source_location": "L152"
|
||||
},
|
||||
{
|
||||
"id": "app_main_root",
|
||||
"label": "root()",
|
||||
"file_type": "code",
|
||||
"source_file": "main.py",
|
||||
"source_location": "L173"
|
||||
"source_location": "L174"
|
||||
},
|
||||
{
|
||||
"id": "app_main_health_check",
|
||||
"label": "health_check()",
|
||||
"file_type": "code",
|
||||
"source_file": "main.py",
|
||||
"source_location": "L178"
|
||||
"source_location": "L179"
|
||||
},
|
||||
{
|
||||
"id": "app_main_rationale_93",
|
||||
@@ -25592,6 +25592,83 @@
|
||||
"source_file": "api/production_material.py",
|
||||
"source_location": "L1318"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_py",
|
||||
"label": "mini_menu.py",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L1"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_current_user",
|
||||
"label": "_current_user()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L19"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_find_mobile_root",
|
||||
"label": "_find_mobile_root()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L27"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_collect_descendants",
|
||||
"label": "_collect_descendants()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L45"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_user_menu_ids",
|
||||
"label": "_user_menu_ids()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L74"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_mobile_menu_icon",
|
||||
"label": "_mobile_menu_icon()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L91"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_mobile_menu_icon_colors",
|
||||
"label": "_mobile_menu_icon_colors()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L101"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_list_my_mobile_menus",
|
||||
"label": "list_my_mobile_menus()",
|
||||
"file_type": "code",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L112"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_rationale_1",
|
||||
"label": "\u5c0f\u7a0b\u5e8f\u83dc\u5355\uff1a\u5f53\u524d\u7528\u6237\u5728\u300c\u79fb\u52a8\u7aef\u83dc\u5355\u300d\u6811\u4e0b\u6709\u6743\u9650\u7684\u5165\u53e3\u3002",
|
||||
"file_type": "rationale",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L1"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_rationale_92",
|
||||
"label": "\u6309\u8def\u7531\u5339\u914d\u79fb\u52a8\u7aef\u5bab\u683c\u56fe\u6807\uff08\u4e0e mobile-h5 van-icon \u5b57\u5f62\u540d\u5bf9\u9f50\uff09\u3002",
|
||||
"file_type": "rationale",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L92"
|
||||
},
|
||||
{
|
||||
"id": "api_mini_menu_rationale_102",
|
||||
"label": "\u5bab\u683c\u56fe\u6807\u5e95\u8272/\u56fe\u5f62\u8272\uff08\u53ef\u6309\u5165\u53e3\u81ea\u5b9a\u4e49\uff0c\u4e0d\u5f3a\u5236\u54c1\u724c\u7eff\uff09\u3002",
|
||||
"file_type": "rationale",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L102"
|
||||
},
|
||||
{
|
||||
"id": "api_qc_py",
|
||||
"label": "qc.py",
|
||||
@@ -37289,7 +37366,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "main.py",
|
||||
"source_location": "L151",
|
||||
"source_location": "L152",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
@@ -37298,7 +37375,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "main.py",
|
||||
"source_location": "L173",
|
||||
"source_location": "L174",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
@@ -37307,7 +37384,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "main.py",
|
||||
"source_location": "L178",
|
||||
"source_location": "L179",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
@@ -103406,6 +103483,231 @@
|
||||
"source_location": "L1318",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "typing",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L4",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "fastapi",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L6",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "sqlalchemy_orm",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L7",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "app_core_auth",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L9",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "app_core_config",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L10",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "app_core_database",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L11",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "app_core_response",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L12",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "app_models_models",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "app_services_web_auth_roles",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L14",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_current_user",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L19",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_find_mobile_root",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L27",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_collect_descendants",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L45",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_user_menu_ids",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L74",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_mobile_menu_icon",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L91",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_mobile_menu_icon_colors",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L101",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_py",
|
||||
"target": "api_mini_menu_list_my_mobile_menus",
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L112",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "api_mini_menu_current_user",
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L114",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "api_mini_menu_find_mobile_root",
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L122",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "api_mini_menu_collect_descendants",
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L126",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "api_mini_menu_user_menu_ids",
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L128",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "api_mini_menu_mobile_menu_icon_colors",
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L139",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "api_mini_menu_mobile_menu_icon",
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L145",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_1",
|
||||
"target": "api_mini_menu_py",
|
||||
"relation": "rationale_for",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L1",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_92",
|
||||
"target": "api_mini_menu_mobile_menu_icon",
|
||||
"relation": "rationale_for",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L92",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_102",
|
||||
"target": "api_mini_menu_mobile_menu_icon_colors",
|
||||
"relation": "rationale_for",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L102",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_qc_py",
|
||||
"target": "typing",
|
||||
@@ -109211,6 +109513,15 @@
|
||||
"source_location": "L37",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_init_py",
|
||||
"target": "app_api_mini_menu",
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "api/__init__.py",
|
||||
"source_location": "L38",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_init_rationale_1",
|
||||
"target": "api_init_py",
|
||||
@@ -233429,6 +233740,60 @@
|
||||
"source_location": "L20",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_1",
|
||||
"target": "models_models_sysmenu",
|
||||
"relation": "uses",
|
||||
"confidence": "INFERRED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_92",
|
||||
"target": "models_models_sysmenu",
|
||||
"relation": "uses",
|
||||
"confidence": "INFERRED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_102",
|
||||
"target": "models_models_sysmenu",
|
||||
"relation": "uses",
|
||||
"confidence": "INFERRED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_1",
|
||||
"target": "models_models_sysrolemenu",
|
||||
"relation": "uses",
|
||||
"confidence": "INFERRED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_92",
|
||||
"target": "models_models_sysrolemenu",
|
||||
"relation": "uses",
|
||||
"confidence": "INFERRED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_rationale_102",
|
||||
"target": "models_models_sysrolemenu",
|
||||
"relation": "uses",
|
||||
"confidence": "INFERRED",
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L13",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"source": "api_qc_thirdpartycompanybody",
|
||||
"target": "schemas_qc_work_order_qcworkordercreatebody",
|
||||
@@ -290140,6 +290505,106 @@
|
||||
"source_location": "L1335",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_current_user",
|
||||
"target": "crud_base_crudbase_get",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L20",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_current_user",
|
||||
"target": "valueerror",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L22",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_current_user",
|
||||
"target": "core_auth_get_current_user_by_token",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L24",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_user_menu_ids",
|
||||
"target": "services_web_auth_roles_get_effective_role_ids",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L75",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_user_menu_ids",
|
||||
"target": "str",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L88",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "core_response_error_response",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L116",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "str",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L118",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "crud_base_crudbase_get",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L118",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "core_response_success_response",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L124",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_mini_menu_list_my_mobile_menus",
|
||||
"target": "services_web_auth_roles_resolve_web_role_for_user",
|
||||
"relation": "calls",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": "api/mini_menu.py",
|
||||
"source_location": "L127",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"source": "api_qc_list_qc_third_party_companies",
|
||||
"target": "api_clinical_payment_get_tenant_no",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Graph Report - app (2026-08-11)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 249 files · ~302,805 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,240 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
|
||||
## Summary
|
||||
- 5227 nodes · 28634 edges · 43 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 19594 edges (avg confidence: 0.6)
|
||||
- 5238 nodes · 28666 edges · 43 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 19610 edges (avg confidence: 0.6)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
@@ -54,8 +54,8 @@
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `success_response()` - 735 edges
|
||||
2. `error_response()` - 608 edges
|
||||
1. `success_response()` - 736 edges
|
||||
2. `error_response()` - 609 edges
|
||||
3. `SysUser` - 598 edges
|
||||
4. `BizQcTaskResult` - 346 edges
|
||||
5. `BizProductionMaterial` - 329 edges
|
||||
@@ -70,90 +70,90 @@
|
||||
core/api_request_log_middleware.py → models/models.py
|
||||
- `get_menu_list()` --calls--> `success_response()` [INFERRED]
|
||||
api/menu.py → core/response.py
|
||||
- `中试服务交付管理 API(交付信息、交付物料、交付方式)` --uses--> `SysUser` [INFERRED]
|
||||
api/pilot_delivery.py → models/models.py
|
||||
- `微信小程序登录业务:openid + 手机号一致性校验。` --uses--> `SysUser` [INFERRED]
|
||||
services/wechat_login_service.py → models/models.py
|
||||
- `根据 openid + 本次授权手机号解析可登录员工。 Returns: (user, error_message, error_co` --uses--> `SysUser` [INFERRED]
|
||||
services/wechat_login_service.py → models/models.py
|
||||
- `保留匹配节点及其全部上级,供关键词检索后仍能正确组树。` --uses--> `SysDept` [INFERRED]
|
||||
crud/dept.py → models/models.py
|
||||
|
||||
## Communities
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.01
|
||||
Nodes (991): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+983 more)
|
||||
Nodes (1001): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+993 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.01
|
||||
Nodes (728): generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。 须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, _apply_cell_product_status_change(), create_cell_product() (+720 more)
|
||||
Nodes (706): generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号, 原子分配下一个业务协议号。 须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。, create_cell_product(), create_cell_sample_type() (+698 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.01
|
||||
Nodes (637): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+629 more)
|
||||
Nodes (584): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+576 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.06
|
||||
Nodes (457): 环境检测 — 检测项维护 + 登记管理 API, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody (+449 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (410): 环境检测 — 检测项维护 + 登记管理 API, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 小程序菜单:当前用户在「移动端菜单」树下有权限的入口。, 宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。, 按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。, ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody (+402 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.02
|
||||
Nodes (281): _normalize_date_bound(), _persist_protocol_sub_resources(), _body_to_payload(), _get_current_user_id(), get_production_reception_by_sample_code(), get_production_reception_detail(), _get_tenant_no(), post_production_reception() (+273 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (328): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+320 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.07
|
||||
Nodes (309): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+301 more)
|
||||
Cohesion: 0.02
|
||||
Nodes (270): _normalize_date_bound(), _persist_protocol_sub_resources(), list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), format_date() (+262 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.02
|
||||
Nodes (244): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), build_void_type_label_map(), format_protocol_status_label() (+236 more)
|
||||
Nodes (270): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), build_void_type_label_map(), format_protocol_status_label() (+262 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.02
|
||||
Nodes (212): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+204 more)
|
||||
Nodes (234): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+226 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.02
|
||||
Nodes (195): list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), ApiRequestLogCRUD, delete(), get_multi_with_filters() (+187 more)
|
||||
Nodes (142): get_designer_data(), _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+134 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.02
|
||||
Nodes (144): get_designer_data(), _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+136 more)
|
||||
Cohesion: 0.03
|
||||
Nodes (129): get_next_cell_product_code(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes() (+121 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.02
|
||||
Nodes (130): 小程序鉴权:微信登录 / 绑定 openid。, 微信授权登录:须授权手机号;openid 已绑定也须与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id(), wechat_bind(), wechat_login(), _build_current_user_payload(), cn_holiday_range() (+122 more)
|
||||
Cohesion: 0.03
|
||||
Nodes (110): 小程序鉴权:微信登录 / 绑定 openid。, 微信授权登录:须授权手机号;openid 已绑定也须与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id(), wechat_bind(), wechat_login(), _collect_descendants(), _current_user() (+102 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.04
|
||||
Nodes (93): _fmt_dt(), get_handover_by_schedule(), get_outbound_handover(), get_outbound_handover_view(), _handover_row_to_dict(), _handover_view_dict(), _parse_handover_time(), 供输注计划 detail 聚合的交接只读信息。 (+85 more)
|
||||
Nodes (104): _body_to_payload(), _get_current_user_id(), get_production_reception_by_sample_code(), get_production_reception_detail(), _get_tenant_no(), post_production_reception(), put_production_reception(), _resolve_reception_id() (+96 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.06
|
||||
Nodes (71): add_task_step(), _aggregate_production_tab_step_field(), assert_step_assignee_for_write(), assert_task_assignee(), assert_task_step_assignee_or_legacy(), _batch_load_step_sub_tables(), _clone_route_to_task(), _clone_step_consumables() (+63 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (113): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slot_stats(), OccupancyCreateBody (+105 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.06
|
||||
Nodes (66): aggregate_slot_stats_by_facility(), _apply_slot_select_base_filters(), _apply_slot_select_query_filters(), batch_create_facilities(), batch_create_slots(), _build_node_path_cache(), _build_slot_keyword_sql_filters(), collect_descendant_ids() (+58 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (102): _filter_material_type(), _amount_for_row(), _batch_map(), _deduct_step_usage_legacy_batch(), _display_remark(), _fmt_date_val(), list_available_requisitions_for_prep(), list_linked_step_usages() (+94 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.09
|
||||
Nodes (41): _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_products_by_quantity(), delete_cell_product() (+33 more)
|
||||
Cohesion: 0.45
|
||||
Nodes (71): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+63 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.09
|
||||
Nodes (50): complete_application(), create_basic_experiment_sale_schedules(), create_infusion_plan(), create_schedule(), _create_schedule_products(), _format_yyyy_mm_dd(), _generate_application_no(), get_application() (+42 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total, 申请退款 Args: out_trade_no: 商户订单号 total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API) Args: out_trade_no: 商户订单号 tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.14
|
||||
Nodes (11): apply_formulas_and_linkages(), eval_formula(), _extract_rules(), preprocess_formula(), OA 表单公式与数据联动(禁止任意 eval), 按 schema 中字段 props.formula / props.linkage 回填 form_data。 linkage 示例: { "when, 将 {field} 转为合法标识符引用 field。, _SafeEval (+3 more)
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.19
|
||||
Nodes (21): apply_overtime_kit_hours(), _build_rest_day_segments(), _build_workday_windows(), calc_overtime_hours(), _day_at_minutes(), _day_start_ms(), _extract_rules(), _format_ymd() (+13 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.17
|
||||
Nodes (19): get_by_schedule(), get_method(), _get_tenant_no(), list_by_application(), list_materials(), 中试服务交付管理 API(交付信息、交付物料、交付方式), soft_delete(), create_delivery() (+11 more)
|
||||
Nodes (19): batch_create_checkups(), create_checkup(), delete_actual_item(), delete_reserve_item(), get_checkup_by_application(), get_checkup_by_id(), list_actual_items(), list_checkups_paginated() (+11 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.21
|
||||
@@ -164,12 +164,12 @@ Cohesion: 0.2
|
||||
Nodes (19): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), get_material_entity(), get_my_usages(), _is_enabled_status() (+11 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.27
|
||||
Nodes (12): create_equipment(), _dec(), equipment_code_exists(), equipment_is_referenced(), get_equipment(), list_equipments(), _norm_status(), _parse_date() (+4 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.23
|
||||
Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
|
||||
Cohesion: 0.27
|
||||
Nodes (12): create_equipment(), _dec(), equipment_code_exists(), equipment_is_referenced(), get_equipment(), list_equipments(), _norm_status(), _parse_date() (+4 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.24
|
||||
@@ -284,17 +284,17 @@ Nodes (1): 发送催单通知给配送员 Args: del
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `SysUser` connect `Community 2` to `Community 1`, `Community 3`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 18`, `Community 23`, `Community 24`?**
|
||||
_High betweenness centrality (0.109) - this node is a cross-community bridge._
|
||||
- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 19`, `Community 20`, `Community 21`, `Community 23`, `Community 24`, `Community 25`?**
|
||||
_High betweenness centrality (0.043) - this node is a cross-community bridge._
|
||||
- **Why does `success_response()` connect `Community 0` to `Community 32`, `Community 2`, `Community 4`, `Community 8`, `Community 9`, `Community 10`, `Community 18`?**
|
||||
- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 23`, `Community 24`?**
|
||||
_High betweenness centrality (0.114) - this node is a cross-community bridge._
|
||||
- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`?**
|
||||
_High betweenness centrality (0.042) - this node is a cross-community bridge._
|
||||
- **Are the 877 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
|
||||
_`str` has 877 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 734 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
|
||||
_`success_response()` has 734 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 607 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
|
||||
_`error_response()` has 607 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Why does `success_response()` connect `Community 0` to `Community 32`, `Community 2`, `Community 5`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`?**
|
||||
_High betweenness centrality (0.040) - this node is a cross-community bridge._
|
||||
- **Are the 879 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
|
||||
_`str` has 879 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 735 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
|
||||
_`success_response()` has 735 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 608 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
|
||||
_`error_response()` has 608 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 596 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
|
||||
_`SysUser` has 596 INFERRED edges - model-reasoned connections that need verification._
|
||||
+2283
-1800
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/__init__.py": 1778578257.989357,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/main.py": 1786374025.1465516,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/main.py": 1786426758.368078,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/tasks/__init__.py": 1778579841.437791,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/auth.py": 1786415793.4635684,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/api_crypto.py": 1778578258.039314,
|
||||
@@ -184,6 +184,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1786374022.5691514,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1782894693.6582327,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/production_material.py": 1782404982.2474263,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1786438411.2164767,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1785207187.402777,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market.py": 1781522283.6823432,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_production_reception.py": 1782718998.90251,
|
||||
@@ -192,7 +193,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1782709076.3968658,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_confirm.py": 1779657043.1389203,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_schedule_mgmt.py": 1779940938.18027,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1786350194.7209065,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1786426755.6367717,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_entry_sms_notification.py": 1780325524.9170487,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_application.py": 1781079904.0516074,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pre_transit_sample.py": 1782204426.7965167,
|
||||
|
||||
@@ -0,0 +1,840 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>移动端主题绿 · 色板挑选</title>
|
||||
<style>
|
||||
:root {
|
||||
--page-bg: #f4f6f5;
|
||||
--card: #fff;
|
||||
--ink: #1a2420;
|
||||
--muted: #5c6b64;
|
||||
--line: #e2e8e5;
|
||||
--current: #0a9f6d;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, sans-serif;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 10% -10%, #d8f5e8 0%, transparent 55%),
|
||||
radial-gradient(900px 500px at 100% 0%, #e8f0ff 0%, transparent 50%),
|
||||
var(--page-bg);
|
||||
color: var(--ink);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: 28px 20px 80px; }
|
||||
header { margin-bottom: 28px; }
|
||||
header h1 { margin: 0 0 8px; font-size: 26px; letter-spacing: -0.02em; }
|
||||
header p { margin: 0; color: var(--muted); font-size: 14px; max-width: 720px; }
|
||||
.toolbar {
|
||||
display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
|
||||
margin: 20px 0 8px; padding: 12px 14px; background: rgba(255,255,255,.75);
|
||||
border: 1px solid var(--line); border-radius: 12px; backdrop-filter: blur(8px);
|
||||
}
|
||||
.toolbar label { font-size: 13px; color: var(--muted); }
|
||||
.toolbar select, .toolbar input[type="search"] {
|
||||
height: 36px; border: 1px solid var(--line); border-radius: 8px;
|
||||
padding: 0 10px; background: #fff; font: inherit; color: var(--ink);
|
||||
}
|
||||
.toolbar input[type="search"] { min-width: 200px; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 10px; border-radius: 999px; font-size: 12px;
|
||||
background: #ecfdf5; color: #087f57; border: 1px solid #a7f3d0;
|
||||
}
|
||||
.chip .sw { width: 12px; height: 12px; border-radius: 3px; background: var(--current); }
|
||||
.hint { font-size: 12px; color: var(--muted); margin-top: 8px; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(20, 40, 30, .04);
|
||||
transition: border-color .15s, box-shadow .15s, transform .15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(20,40,30,.08); }
|
||||
.card.is-current { border-color: #94a3b8; outline: 2px dashed #94a3b8; outline-offset: -2px; }
|
||||
.card.is-selected {
|
||||
border-color: var(--p, #0a9f6d);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p) 28%, transparent);
|
||||
}
|
||||
.card-top {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr .8fr .7fr;
|
||||
height: 72px;
|
||||
}
|
||||
.swatch { display: flex; align-items: flex-end; padding: 8px; color: #fff; font-size: 11px; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,.25); }
|
||||
.swatch.soft { color: #1f2a26; text-shadow: none; }
|
||||
.body { padding: 14px 14px 16px; }
|
||||
.title-row { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; margin-bottom: 6px; }
|
||||
.title { font-size: 16px; font-weight: 650; margin: 0; }
|
||||
.tag {
|
||||
font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
||||
background: #f1f5f4; color: var(--muted); white-space: nowrap;
|
||||
}
|
||||
.tag.hot { background: #fff7ed; color: #c2410c; }
|
||||
.tag.cool { background: #eff6ff; color: #1d4ed8; }
|
||||
.tag.fresh { background: #ecfdf5; color: #047857; }
|
||||
.desc { margin: 0 0 12px; font-size: 12.5px; color: var(--muted); min-height: 36px; }
|
||||
.tokens { display: grid; gap: 6px; margin-bottom: 12px; }
|
||||
.token {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||
font-size: 12px; padding: 6px 8px; border-radius: 8px; background: #f8faf9;
|
||||
border: 1px solid #edf2ef;
|
||||
}
|
||||
.token code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px; letter-spacing: .02em;
|
||||
}
|
||||
.token button {
|
||||
border: 0; background: transparent; color: var(--muted); cursor: pointer;
|
||||
font-size: 11px; padding: 2px 6px; border-radius: 4px;
|
||||
}
|
||||
.token button:hover { background: #e8eee9; color: var(--ink); }
|
||||
.preview {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f7faf8;
|
||||
}
|
||||
.nav {
|
||||
height: 40px; display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-size: 13px; font-weight: 600;
|
||||
background: linear-gradient(135deg, var(--p), var(--d));
|
||||
}
|
||||
.preview-body { padding: 12px; display: grid; gap: 8px; }
|
||||
.pill-row { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.pill {
|
||||
font-size: 11px; padding: 4px 8px; border-radius: 999px;
|
||||
background: var(--s); color: var(--d); border: 1px solid color-mix(in srgb, var(--p) 25%, white);
|
||||
}
|
||||
.btn {
|
||||
height: 36px; border: 0; border-radius: 999px; color: #fff;
|
||||
background: var(--p); font-size: 13px; font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.btn.outline {
|
||||
background: transparent; color: var(--p);
|
||||
border: 1.5px solid var(--p);
|
||||
}
|
||||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.soft-block {
|
||||
padding: 10px; border-radius: 10px; background: var(--s);
|
||||
color: var(--d); font-size: 12px;
|
||||
}
|
||||
.footer-bar {
|
||||
position: sticky; bottom: 12px; margin-top: 24px;
|
||||
background: rgba(255,255,255,.92); border: 1px solid var(--line);
|
||||
border-radius: 14px; padding: 12px 14px;
|
||||
display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,.08);
|
||||
}
|
||||
.picked { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
.picked .big {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,.08);
|
||||
background: linear-gradient(135deg, var(--fp, #0a9f6d), var(--fd, #087f57));
|
||||
}
|
||||
.picked strong { display: block; font-size: 14px; }
|
||||
.picked span { color: var(--muted); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
|
||||
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.actions button {
|
||||
height: 36px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--line);
|
||||
background: #fff; cursor: pointer; font: inherit; font-size: 13px;
|
||||
}
|
||||
.actions button.primary {
|
||||
background: var(--fp, #0a9f6d); border-color: transparent; color: #fff; font-weight: 600;
|
||||
}
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 90px; transform: translateX(-50%) translateY(12px);
|
||||
background: #1f2a26; color: #fff; padding: 8px 14px; border-radius: 999px;
|
||||
font-size: 13px; opacity: 0; pointer-events: none; transition: .2s;
|
||||
z-index: 20;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
.section-title {
|
||||
margin: 28px 0 10px; font-size: 14px; color: var(--muted);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.section-title::after {
|
||||
content: ""; flex: 1; height: 1px; background: var(--line);
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.card-top { height: 56px; }
|
||||
.toolbar input[type="search"] { min-width: 0; width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>移动端主题绿 · 色板挑选</h1>
|
||||
<p>
|
||||
当前工程(mobile-h5 / mobile-oa-h5)主色为三墙绿
|
||||
<span class="chip"><i class="sw"></i>#0a9f6d / #087f57</span>。
|
||||
下方每组含主色 / 深绿 / 浅绿底,并有导航、按钮、标签预览。点击卡片选中,点色值可复制。
|
||||
</p>
|
||||
<div class="toolbar">
|
||||
<label>筛选</label>
|
||||
<select id="filter">
|
||||
<option value="all">全部</option>
|
||||
<option value="current">当前</option>
|
||||
<option value="teal">青绿 / Teal</option>
|
||||
<option value="jade">翠绿 / Jade</option>
|
||||
<option value="forest">森绿 / Forest</option>
|
||||
<option value="mint">薄荷绿 / Mint</option>
|
||||
<option value="olive">橄榄 / Olive</option>
|
||||
<option value="deep">更深更沉</option>
|
||||
<option value="bright">更亮更鲜</option>
|
||||
</select>
|
||||
<input id="q" type="search" placeholder="搜名称或色值,如 jade / 0d9488" />
|
||||
<span class="hint">建议:企业 OA 偏稳重选「森绿 / 青绿」;偏年轻活力选「翠绿 / 亮绿」。</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="sections"></div>
|
||||
|
||||
<div class="footer-bar" id="footer">
|
||||
<div class="picked">
|
||||
<div class="big" id="pickedSwatch"></div>
|
||||
<div>
|
||||
<strong id="pickedName">未选择</strong>
|
||||
<span id="pickedHex">点击上方色卡挑选</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" id="copyCss">复制 CSS Token</button>
|
||||
<button type="button" class="primary" id="copyAll">复制主色三件套</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast">已复制</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 每组:id, name, group, vibe(tag), desc, primary, dark, soft, tags[]
|
||||
* soft 为浅底;dark 用于渐变末端与强调字色
|
||||
*/
|
||||
const PALETTES = [
|
||||
{
|
||||
id: 'current',
|
||||
name: '当前 · 三墙绿',
|
||||
group: 'current',
|
||||
vibe: '当前',
|
||||
tagClass: '',
|
||||
desc: '现网 mobile-h5 / mobile-oa-h5 主题。稳、偏深,和三墙小程序一致。',
|
||||
primary: '#0a9f6d',
|
||||
dark: '#087f57',
|
||||
soft: '#e9f9f2',
|
||||
tags: ['current', 'jade', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'admin-mint',
|
||||
name: '管理端 · Fresh Mint',
|
||||
group: 'mint',
|
||||
vibe: '对照',
|
||||
tagClass: 'cool',
|
||||
desc: 'web-admin 主色,偏亮薄荷绿。移动端规范一般不混用,仅作对照。',
|
||||
primary: '#10b981',
|
||||
dark: '#059669',
|
||||
soft: '#ecfdf5',
|
||||
tags: ['mint', 'bright']
|
||||
},
|
||||
|
||||
// —— 青绿 Teal ——
|
||||
{
|
||||
id: 'teal-01',
|
||||
name: '海湾青绿',
|
||||
group: 'teal',
|
||||
vibe: '青绿',
|
||||
tagClass: 'cool',
|
||||
desc: '偏蓝的青绿,科技感强,适合工具型 OA,和「蔬菜绿」拉开距离。',
|
||||
primary: '#0d9488',
|
||||
dark: '#0f766e',
|
||||
soft: '#f0fdfa',
|
||||
tags: ['teal', 'cool']
|
||||
},
|
||||
{
|
||||
id: 'teal-02',
|
||||
name: '深海青',
|
||||
group: 'teal',
|
||||
vibe: '青绿',
|
||||
tagClass: 'cool',
|
||||
desc: '更深更沉的 teal,导航栏会更稳重,按钮对比度高。',
|
||||
primary: '#0f766e',
|
||||
dark: '#115e59',
|
||||
soft: '#ccfbf1',
|
||||
tags: ['teal', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'teal-03',
|
||||
name: '青瓷',
|
||||
group: 'teal',
|
||||
vibe: '青绿',
|
||||
tagClass: 'cool',
|
||||
desc: '略灰的青瓷感,不抢眼,适合长时间办公阅读。',
|
||||
primary: '#14b8a6',
|
||||
dark: '#0d9488',
|
||||
soft: '#f0fdfa',
|
||||
tags: ['teal', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'teal-04',
|
||||
name: '蓝绿松',
|
||||
group: 'teal',
|
||||
vibe: '青绿',
|
||||
tagClass: 'cool',
|
||||
desc: '略偏蓝,接近 Notion/Linear 一类产品的冷静绿。',
|
||||
primary: '#2dd4bf',
|
||||
dark: '#14b8a6',
|
||||
soft: '#f0fdfa',
|
||||
tags: ['teal', 'bright']
|
||||
},
|
||||
|
||||
// —— 翠绿 Jade ——
|
||||
{
|
||||
id: 'jade-01',
|
||||
name: '玉翠',
|
||||
group: 'jade',
|
||||
vibe: '翠绿',
|
||||
tagClass: 'fresh',
|
||||
desc: '比当前色更亮一点,仍是「正绿」,亲和力更好。',
|
||||
primary: '#059669',
|
||||
dark: '#047857',
|
||||
soft: '#ecfdf5',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'jade-02',
|
||||
name: '翡冷翠',
|
||||
group: 'jade',
|
||||
vibe: '翠绿',
|
||||
tagClass: 'fresh',
|
||||
desc: '经典 emerald,比 #0a9f6d 略深、更「正」。',
|
||||
primary: '#047857',
|
||||
dark: '#065f46',
|
||||
soft: '#d1fae5',
|
||||
tags: ['jade', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'jade-03',
|
||||
name: '青葱',
|
||||
group: 'jade',
|
||||
vibe: '翠绿',
|
||||
tagClass: 'fresh',
|
||||
desc: '偏黄的鲜绿,更有「新鲜」感,适合生鲜/农业场景。',
|
||||
primary: '#16a34a',
|
||||
dark: '#15803d',
|
||||
soft: '#f0fdf4',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'jade-04',
|
||||
name: '竹青',
|
||||
group: 'jade',
|
||||
vibe: '翠绿',
|
||||
tagClass: 'fresh',
|
||||
desc: '中等饱和竹青,介于当前色与亮绿之间。',
|
||||
primary: '#22c55e',
|
||||
dark: '#16a34a',
|
||||
soft: '#f0fdf4',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'jade-05',
|
||||
name: '叶绿',
|
||||
group: 'jade',
|
||||
vibe: '翠绿',
|
||||
tagClass: 'fresh',
|
||||
desc: '略偏黄绿,自然、户外感,和「医疗青绿」不同。',
|
||||
primary: '#4ade80',
|
||||
dark: '#22c55e',
|
||||
soft: '#f0fdf4',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'jade-06',
|
||||
name: '苔藓亮',
|
||||
group: 'jade',
|
||||
vibe: '翠绿',
|
||||
tagClass: 'fresh',
|
||||
desc: '饱和度高,主按钮很跳;慎用于大面积顶栏。',
|
||||
primary: '#12b76a',
|
||||
dark: '#039855',
|
||||
soft: '#e8faf0',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
|
||||
// —— 森绿 Forest ——
|
||||
{
|
||||
id: 'forest-01',
|
||||
name: '松针',
|
||||
group: 'forest',
|
||||
vibe: '森绿',
|
||||
tagClass: 'hot',
|
||||
desc: '深森绿,企业感强,顶栏很稳;浅底用浅绿提亮。',
|
||||
primary: '#166534',
|
||||
dark: '#14532d',
|
||||
soft: '#dcfce7',
|
||||
tags: ['forest', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'forest-02',
|
||||
name: '杉木',
|
||||
group: 'forest',
|
||||
vibe: '森绿',
|
||||
tagClass: '',
|
||||
desc: '比松针稍亮,仍属「深绿色」一档,适合严肃审批流。',
|
||||
primary: '#15803d',
|
||||
dark: '#166534',
|
||||
soft: '#dcfce7',
|
||||
tags: ['forest', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'forest-03',
|
||||
name: '墨绿',
|
||||
group: 'forest',
|
||||
vibe: '森绿',
|
||||
tagClass: '',
|
||||
desc: '偏黑的墨绿,极稳重;移动端大面积使用需注意对比度。',
|
||||
primary: '#14532d',
|
||||
dark: '#052e16',
|
||||
soft: '#bbf7d0',
|
||||
tags: ['forest', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'forest-04',
|
||||
name: '柏绿',
|
||||
group: 'forest',
|
||||
vibe: '森绿',
|
||||
tagClass: '',
|
||||
desc: '略灰的森绿,不那么「鲜」,更像线下物料印刷绿。',
|
||||
primary: '#3f6212',
|
||||
dark: '#365314',
|
||||
soft: '#ecfccb',
|
||||
tags: ['forest', 'olive', 'deep']
|
||||
},
|
||||
|
||||
// —— 薄荷绿 Mint ——
|
||||
{
|
||||
id: 'mint-01',
|
||||
name: '霜薄荷',
|
||||
group: 'mint',
|
||||
vibe: '薄荷',
|
||||
tagClass: 'fresh',
|
||||
desc: '浅亮薄荷,年轻清爽;顶栏偏浅时需配深色字。',
|
||||
primary: '#34d399',
|
||||
dark: '#10b981',
|
||||
soft: '#ecfdf5',
|
||||
tags: ['mint', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'mint-02',
|
||||
name: '汽水绿',
|
||||
group: 'mint',
|
||||
vibe: '薄荷',
|
||||
tagClass: 'fresh',
|
||||
desc: '高亮偏青,偏消费级 App;B 端审批可能略跳。',
|
||||
primary: '#2dd4bf',
|
||||
dark: '#14b8a6',
|
||||
soft: '#ccfbf1',
|
||||
tags: ['mint', 'teal', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'mint-03',
|
||||
name: '春芽',
|
||||
group: 'mint',
|
||||
vibe: '薄荷',
|
||||
tagClass: 'fresh',
|
||||
desc: '黄调薄荷,春天感强,和深色字搭配时注意对比。',
|
||||
primary: '#4ade80',
|
||||
dark: '#22c55e',
|
||||
soft: '#f0fdf4',
|
||||
tags: ['mint', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'mint-04',
|
||||
name: '淡青',
|
||||
group: 'mint',
|
||||
vibe: '薄荷',
|
||||
tagClass: 'cool',
|
||||
desc: '低饱和淡青,偏「健康医疗」气质。',
|
||||
primary: '#5eead4',
|
||||
dark: '#2dd4bf',
|
||||
soft: '#f0fdfa',
|
||||
tags: ['mint', 'teal', 'bright']
|
||||
},
|
||||
|
||||
// —— 橄榄 Olive ——
|
||||
{
|
||||
id: 'olive-01',
|
||||
name: '橄榄绿',
|
||||
group: 'olive',
|
||||
vibe: '橄榄',
|
||||
tagClass: 'hot',
|
||||
desc: '军绿感克制,独特且不俗;和常见「翠绿」明显区分。',
|
||||
primary: '#65a30d',
|
||||
dark: '#4d7c0f',
|
||||
soft: '#f7fee7',
|
||||
tags: ['olive', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'olive-02',
|
||||
name: '苔绿',
|
||||
group: 'olive',
|
||||
vibe: '橄榄',
|
||||
tagClass: 'hot',
|
||||
desc: '偏黄灰的苔藓绿,自然、沉静。',
|
||||
primary: '#4d7c0f',
|
||||
dark: '#3f6212',
|
||||
soft: '#ecfccb',
|
||||
tags: ['olive', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'olive-03',
|
||||
name: '卡其绿',
|
||||
group: 'olive',
|
||||
vibe: '橄榄',
|
||||
tagClass: '',
|
||||
desc: '低饱和卡其绿,极克制;主按钮可再略提饱和。',
|
||||
primary: '#84cc16',
|
||||
dark: '#65a30d',
|
||||
soft: '#f7fee7',
|
||||
tags: ['olive', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'olive-04',
|
||||
name: '茶绿',
|
||||
group: 'olive',
|
||||
vibe: '橄榄',
|
||||
tagClass: '',
|
||||
desc: '略灰茶绿,偏传统国风,适合政务/传统企业审美。',
|
||||
primary: '#6b8e23',
|
||||
dark: '#556b2f',
|
||||
soft: '#f2f7e8',
|
||||
tags: ['olive', 'deep']
|
||||
},
|
||||
|
||||
// —— 更多变体 / 品牌向 ——
|
||||
{
|
||||
id: 'brand-01',
|
||||
name: '微信绿邻域',
|
||||
group: 'jade',
|
||||
vibe: '对照',
|
||||
tagClass: 'hot',
|
||||
desc: '接近微信绿 #07c160,用户熟悉;品牌辨识度一般。',
|
||||
primary: '#07c160',
|
||||
dark: '#06ad56',
|
||||
soft: '#e8faf0',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'brand-02',
|
||||
name: '钉钉青邻域',
|
||||
group: 'teal',
|
||||
vibe: '对照',
|
||||
tagClass: 'cool',
|
||||
desc: '偏钉钉青蓝,协作工具感;和生鲜绿差异大。',
|
||||
primary: '#0089ff',
|
||||
dark: '#0066cc',
|
||||
soft: '#e8f3ff',
|
||||
tags: ['teal', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'brand-03',
|
||||
name: '飞书绿邻域',
|
||||
group: 'teal',
|
||||
vibe: '对照',
|
||||
tagClass: 'cool',
|
||||
desc: '偏青绿协作风,冷静专业。',
|
||||
primary: '#00d6b9',
|
||||
dark: '#00b5a0',
|
||||
soft: '#e6fffa',
|
||||
tags: ['teal', 'mint', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'brand-04',
|
||||
name: '深翡翠',
|
||||
group: 'jade',
|
||||
vibe: '推荐',
|
||||
tagClass: 'fresh',
|
||||
desc: '比当前略深、略正,仍是绿系;换主题时改动感适中。',
|
||||
primary: '#0f9f6e',
|
||||
dark: '#0b7f57',
|
||||
soft: '#e6f7f0',
|
||||
tags: ['jade', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'brand-05',
|
||||
name: '青玉',
|
||||
group: 'teal',
|
||||
vibe: '推荐',
|
||||
tagClass: 'cool',
|
||||
desc: '主色略偏青,和当前绿有区分又不跳戏。',
|
||||
primary: '#0b9e8a',
|
||||
dark: '#087f6f',
|
||||
soft: '#e6f8f5',
|
||||
tags: ['teal']
|
||||
},
|
||||
{
|
||||
id: 'brand-06',
|
||||
name: '森氧',
|
||||
group: 'forest',
|
||||
vibe: '推荐',
|
||||
tagClass: 'fresh',
|
||||
desc: '更深一档的品牌绿,顶栏更「重」,按钮仍够绿。',
|
||||
primary: '#1b7a4e',
|
||||
dark: '#145c3b',
|
||||
soft: '#e4f4ec',
|
||||
tags: ['forest', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'brand-07',
|
||||
name: '晴绿',
|
||||
group: 'jade',
|
||||
vibe: '推荐',
|
||||
tagClass: 'fresh',
|
||||
desc: '比当前更亮更干净,适合想「提亮一档」但不换色相。',
|
||||
primary: '#12a875',
|
||||
dark: '#0d8a5f',
|
||||
soft: '#e8f8f1',
|
||||
tags: ['jade', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'brand-08',
|
||||
name: '雾绿',
|
||||
group: 'olive',
|
||||
vibe: '克制',
|
||||
tagClass: '',
|
||||
desc: '降饱和雾绿,高级感;大按钮可再加点饱和。',
|
||||
primary: '#5b8a72',
|
||||
dark: '#476e5a',
|
||||
soft: '#eef4f0',
|
||||
tags: ['olive', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'brand-09',
|
||||
name: '孔雀石',
|
||||
group: 'teal',
|
||||
vibe: '个性',
|
||||
tagClass: 'cool',
|
||||
desc: '偏蓝绿宝石感,辨识度高,适合想和「菜市场绿」拉开。',
|
||||
primary: '#0e9f8c',
|
||||
dark: '#0b7d6e',
|
||||
soft: '#e5f7f4',
|
||||
tags: ['teal']
|
||||
},
|
||||
{
|
||||
id: 'brand-10',
|
||||
name: '荧光绿(慎用)',
|
||||
group: 'mint',
|
||||
vibe: '个性',
|
||||
tagClass: 'hot',
|
||||
desc: '极亮,仅适合强调态;不建议做整站主色。',
|
||||
primary: '#39ff14',
|
||||
dark: '#22c55e',
|
||||
soft: '#f0ffe8',
|
||||
tags: ['mint', 'bright']
|
||||
},
|
||||
{
|
||||
id: 'brand-11',
|
||||
name: '北欧松',
|
||||
group: 'forest',
|
||||
vibe: '克制',
|
||||
tagClass: '',
|
||||
desc: '灰调森绿,北欧家具感,B 端偏设计感。',
|
||||
primary: '#2f6b4f',
|
||||
dark: '#24543e',
|
||||
soft: '#e8f1ec',
|
||||
tags: ['forest', 'deep']
|
||||
},
|
||||
{
|
||||
id: 'brand-12',
|
||||
name: '青柠',
|
||||
group: 'olive',
|
||||
vibe: '活力',
|
||||
tagClass: 'hot',
|
||||
desc: '黄绿偏青柠,活泼;审批严肃页可能略轻松。',
|
||||
primary: '#84cc16',
|
||||
dark: '#4d7c0f',
|
||||
soft: '#f7fee7',
|
||||
tags: ['olive', 'bright']
|
||||
}
|
||||
];
|
||||
|
||||
const GROUP_LABEL = {
|
||||
current: '当前与对照',
|
||||
teal: '青绿 / Teal',
|
||||
jade: '翠绿 / Jade',
|
||||
forest: '森绿 / Forest',
|
||||
mint: '薄荷绿 / Mint',
|
||||
olive: '橄榄 / Olive'
|
||||
};
|
||||
|
||||
let selected = null;
|
||||
|
||||
function toast(msg) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
clearTimeout(toast._t);
|
||||
toast._t = setTimeout(() => el.classList.remove('show'), 1400);
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast('已复制:' + text);
|
||||
} catch {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
toast('已复制:' + text);
|
||||
}
|
||||
}
|
||||
|
||||
function matchesFilter(p, filter, q) {
|
||||
if (filter !== 'all') {
|
||||
if (filter === 'current') {
|
||||
if (p.id !== 'current' && p.id !== 'admin-mint') return false;
|
||||
} else if (!p.tags.includes(filter) && p.group !== filter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!q) return true;
|
||||
const hay = (p.name + p.desc + p.primary + p.dark + p.soft + p.id + p.vibe).toLowerCase();
|
||||
return hay.includes(q);
|
||||
}
|
||||
|
||||
function render() {
|
||||
const filter = document.getElementById('filter').value;
|
||||
const q = document.getElementById('q').value.trim().toLowerCase();
|
||||
const root = document.getElementById('sections');
|
||||
root.innerHTML = '';
|
||||
|
||||
const list = PALETTES.filter(p => matchesFilter(p, filter, q));
|
||||
const byGroup = {};
|
||||
for (const p of list) {
|
||||
(byGroup[p.group] ||= []).push(p);
|
||||
}
|
||||
|
||||
const order = ['current', 'jade', 'teal', 'forest', 'mint', 'olive'];
|
||||
for (const g of order) {
|
||||
const items = byGroup[g];
|
||||
if (!items || !items.length) continue;
|
||||
const title = document.createElement('div');
|
||||
title.className = 'section-title';
|
||||
title.textContent = GROUP_LABEL[g] || g;
|
||||
root.appendChild(title);
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'grid';
|
||||
for (const p of items) {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card' + (p.id === 'current' ? ' is-current' : '') + (selected?.id === p.id ? ' is-selected' : '');
|
||||
card.style.setProperty('--p', p.primary);
|
||||
card.style.setProperty('--d', p.dark);
|
||||
card.style.setProperty('--s', p.soft);
|
||||
card.innerHTML = `
|
||||
<div class="card-top">
|
||||
<div class="swatch" style="background:${p.primary}">主色</div>
|
||||
<div class="swatch" style="background:${p.dark}">深绿</div>
|
||||
<div class="swatch soft" style="background:${p.soft}">浅绿底</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title-row">
|
||||
<h3 class="title">${p.name}</h3>
|
||||
<span class="tag ${p.tagClass || ''}">${p.vibe}</span>
|
||||
</div>
|
||||
<p class="desc">${p.desc}</p>
|
||||
<div class="tokens">
|
||||
<div class="token"><span>primary</span><span><code data-hex="${p.primary}">${p.primary}</code> <button type="button" data-copy="${p.primary}">复制</button></span></div>
|
||||
<div class="token"><span>dark</span><span><code data-hex="${p.dark}">${p.dark}</code> <button type="button" data-copy="${p.dark}">复制</button></span></div>
|
||||
<div class="token"><span>soft</span><span><code data-hex="${p.soft}">${p.soft}</code> <button type="button" data-copy="${p.soft}">复制</button></span></div>
|
||||
</div>
|
||||
<div class="preview">
|
||||
<div class="nav">发起审批</div>
|
||||
<div class="preview-body">
|
||||
<div class="pill-row">
|
||||
<span class="pill">进行中</span>
|
||||
<span class="pill">已通过</span>
|
||||
</div>
|
||||
<div class="soft-block">浅绿底提示条 · 待办 3 项</div>
|
||||
<div class="btn-row">
|
||||
<div class="btn">提交</div>
|
||||
<div class="btn outline">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('[data-copy]')) return;
|
||||
selectPalette(p);
|
||||
render();
|
||||
});
|
||||
card.querySelectorAll('[data-copy]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
copyText(btn.getAttribute('data-copy'));
|
||||
});
|
||||
});
|
||||
grid.appendChild(card);
|
||||
}
|
||||
root.appendChild(grid);
|
||||
}
|
||||
|
||||
if (!list.length) {
|
||||
root.innerHTML = '<p class="hint">没有匹配的色板,试试换个关键词。</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function selectPalette(p) {
|
||||
selected = p;
|
||||
document.getElementById('pickedName').textContent = p.name;
|
||||
document.getElementById('pickedHex').textContent =
|
||||
`${p.primary} / ${p.dark} / ${p.soft}`;
|
||||
const bar = document.getElementById('footer');
|
||||
bar.style.setProperty('--fp', p.primary);
|
||||
bar.style.setProperty('--fd', p.dark);
|
||||
document.getElementById('pickedSwatch').style.background =
|
||||
`linear-gradient(135deg, ${p.primary}, ${p.dark})`;
|
||||
}
|
||||
|
||||
document.getElementById('filter').addEventListener('change', render);
|
||||
document.getElementById('q').addEventListener('input', render);
|
||||
|
||||
document.getElementById('copyAll').addEventListener('click', () => {
|
||||
if (!selected) { toast('请先点选一张色卡'); return; }
|
||||
copyText(`${selected.primary}, ${selected.dark}, ${selected.soft}`);
|
||||
});
|
||||
|
||||
document.getElementById('copyCss').addEventListener('click', () => {
|
||||
if (!selected) { toast('请先点选一张色卡'); return; }
|
||||
const css = `/* ${selected.name} */
|
||||
--brand-green: ${selected.primary};
|
||||
--brand-green-dark: ${selected.dark};
|
||||
--brand-green-soft: ${selected.soft};
|
||||
--van-primary-color: ${selected.primary};`;
|
||||
copyText(css);
|
||||
});
|
||||
|
||||
// 默认选中当前
|
||||
selectPalette(PALETTES[0]);
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,398 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>主页菜单图标 · 配色挑选</title>
|
||||
<style>
|
||||
:root {
|
||||
--ink: #1f2a26;
|
||||
--muted: #6b7b74;
|
||||
--line: #e2e8e5;
|
||||
--page: #f5f8f7;
|
||||
--brand: #16a34a;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(900px 480px at 0% -10%, #dcfce7 0%, transparent 50%),
|
||||
radial-gradient(800px 420px at 100% 0%, #e0e7ff 0%, transparent 45%),
|
||||
var(--page);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.wrap { max-width: 1100px; margin: 0 auto; padding: 24px 18px 100px; }
|
||||
h1 { margin: 0 0 8px; font-size: 24px; letter-spacing: -0.02em; }
|
||||
.sub { margin: 0; color: var(--muted); font-size: 14px; max-width: 720px; }
|
||||
.note {
|
||||
margin: 14px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.note code {
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
background: #f1f5f4;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.phone {
|
||||
margin: 20px 0 8px;
|
||||
max-width: 390px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 16px 40px rgba(20,40,30,.08);
|
||||
}
|
||||
.phone-hd {
|
||||
padding: 18px 16px 14px;
|
||||
background: linear-gradient(155deg, #14532d 0%, #16a34a 48%, #22c55e 100%);
|
||||
color: #fff;
|
||||
}
|
||||
.phone-hd .brand { font-size: 12px; opacity: .85; }
|
||||
.phone-hd .hello { font-size: 18px; font-weight: 650; margin-top: 6px; }
|
||||
.phone-body { padding: 14px; background: #f5f8f7; }
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--line);
|
||||
padding: 12px;
|
||||
}
|
||||
.panel-hd {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.panel-title { font-size: 14px; font-weight: 650; }
|
||||
.hint { font-size: 12px; color: var(--muted); }
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px 6px;
|
||||
}
|
||||
.menu-card {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||
padding: 8px 4px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.menu-icon {
|
||||
width: 48px; height: 48px; border-radius: 14px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: inset 0 0 0 1px rgba(0,0,0,.04);
|
||||
}
|
||||
.menu-icon svg { width: 26px; height: 26px; display: block; }
|
||||
.menu-title {
|
||||
font-size: 12px; text-align: center; color: var(--ink);
|
||||
max-width: 72px; line-height: 1.2;
|
||||
}
|
||||
|
||||
.sec {
|
||||
margin: 28px 0 10px;
|
||||
font-size: 14px; color: var(--muted);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.sec::after { content: ""; flex: 1; height: 1px; background: var(--line); }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 14px 12px 12px;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
text-align: center;
|
||||
}
|
||||
.card:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(0,0,0,.06); }
|
||||
.card.is-current { outline: 2px dashed #94a3b8; outline-offset: -2px; }
|
||||
.card.is-selected {
|
||||
border-color: var(--fg, #2563eb);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fg) 25%, transparent);
|
||||
}
|
||||
.card .icon-box {
|
||||
width: 56px; height: 56px; border-radius: 16px;
|
||||
margin: 0 auto 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: inset 0 0 0 1px rgba(0,0,0,.05);
|
||||
}
|
||||
.card .icon-box svg { width: 30px; height: 30px; }
|
||||
.card .name { font-size: 13px; font-weight: 650; margin: 0 0 4px; }
|
||||
.card .meta {
|
||||
font-size: 11px; color: var(--muted);
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.tag {
|
||||
display: inline-block; margin-top: 8px;
|
||||
font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
||||
background: #f1f5f4; color: var(--muted);
|
||||
}
|
||||
.tag.rec { background: #eff6ff; color: #1d4ed8; }
|
||||
.tag.hot { background: #fff7ed; color: #c2410c; }
|
||||
|
||||
.footer {
|
||||
position: sticky; bottom: 12px; margin-top: 22px;
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
display: flex; flex-wrap: wrap; gap: 12px;
|
||||
align-items: center; justify-content: space-between;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,.08);
|
||||
}
|
||||
.picked { display: flex; align-items: center; gap: 10px; font-size: 13px; }
|
||||
.picked .sw {
|
||||
width: 44px; height: 44px; border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: inset 0 0 0 1px rgba(0,0,0,.06);
|
||||
}
|
||||
.picked .sw svg { width: 24px; height: 24px; }
|
||||
.picked strong { display: block; font-size: 14px; }
|
||||
.picked span { color: var(--muted); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
|
||||
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.actions button {
|
||||
height: 36px; padding: 0 14px; border-radius: 8px;
|
||||
border: 1px solid var(--line); background: #fff;
|
||||
font: inherit; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.actions button.primary {
|
||||
background: var(--brand); border-color: transparent; color: #fff; font-weight: 600;
|
||||
}
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 88px; transform: translateX(-50%) translateY(10px);
|
||||
background: #1f2a26; color: #fff; padding: 8px 14px; border-radius: 999px;
|
||||
font-size: 13px; opacity: 0; transition: .2s; pointer-events: none; z-index: 20;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 印章/发起审批图标:用 currentColor 便于换色 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||
<symbol id="icon-approve" viewBox="0 0 1024 1024">
|
||||
<path fill="currentColor" d="M645.51 854.33c15.49-57.33 86.49-42 86.49-42s-43.54-44.53-23-112.67c11.22-37.2 39.58-67.99 79.81-76.43 20.41-4.28 56.37-4.23 69.42 0.55 3.66-18.04 7.33-36.14 10.85-54.39l23.84-117.26c7.41-37.35 14.81-74.7 22.78-112.05 6.27-32.55 13.19-65.1 19.53-97.81 4.72-24.74 10.09-49.48 14.81-74.71 2.6-14.16 5.86-28.31 8.46-42.07 0.61-2.04 0.96-4.14 1.06-6.26-1.06-24.66-1.06-41.01-16.93-48.83-16.43-7.89-32.38-7.89-40.85 0-21.65 10.98-43.45 21.48-65.1 32.55-54.04 26.85-107.41 53.7-161.45 80.48L468.71 285.97c-59.16 29.46-118.4 60.46-178.21 88.78-45.57 21.56-90 45.81-135.08 67.38-19.53 9.44-40.2 15.79-59.73 25.71-15.79 7.97-31.25 20.02-31.25 39.55 0 8.87 1.63 21.48 8.95 27.83 2.68 2.03 5.94 3.58 8.46 5.69 40.28 22.14 78.44 48.42 117.58 72.67l76.17 47.2h0.57c16.44 7.98 32.31 7.98 48.74-7.81l0.49-0.57c0.49-0.57 0.98-0.57 0.98-1.05 0.65-0.49 1.71-1.06 2.2-2.04 1.14-1.14 2.11-1.63 3.25-2.68 0.98-1.06 2.61-2.04 3.66-3.17 1.63-1.55 3.17-2.61 4.23-4.15 1.62-1.73 3.39-3.31 5.29-4.72 2.11-1.63 3.66-3.66 5.77-5.29 2.2-2.11 4.31-3.66 6.35-5.7 2.11-2.19 4.8-4.23 6.91-6.43 2.68-2.03 4.8-4.72 7.49-6.83 2.61-2.61 5.21-4.72 7.81-7.32 2.77-2.61 5.86-5.29 8.46-7.89 3.25-2.61 5.86-5.21 9.03-7.89 3.25-2.61 6.35-5.86 9.52-8.38 3.25-3.26 6.43-5.86 10.09-8.95 3.17-3.17 6.91-6.35 10.09-8.95 3.67-3.17 6.84-6.34 10.58-9.44 3.76-3.08 7.3-6.43 10.58-10.01 3.66-3.17 7.32-6.92 11.15-10.09 3.66-3.1 7.33-6.76 11.07-9.93 3.66-3.66 7.97-6.83 11.64-10.5 3.74-3.66 7.98-6.84 11.72-10.58 3.66-3.66 7.81-6.83 11.64-10.41 4.07-3.74 7.89-7.32 12.13-10.58 4.23-3.67 7.97-7.33 12.21-10.58 4.15-3.58 7.89-7.32 12.21-10.98 4.15-3.66 7.89-7.32 12.12-10.98 4.15-3.74 7.98-7.4 12.13-10.58 3.74-3.58 7.65-7.08 11.72-10.41 3.66-3.74 7.89-6.83 11.64-10.58 3.66-3.67 7.89-6.84 11.64-10.5 3.66-3.74 7.32-6.83 11.64-10.58 3.66-3.09 7.32-6.75 11.15-9.93 3.66-3.25 7.32-6.83 11.07-10.01 3.67-3.01 7.25-6.19 10.58-9.44 3.74-3.17 6.83-6.35 10.58-9.52 3.25-3.1 6.83-6.27 10.09-8.95 3.25-3.17 6.34-5.7 9.52-8.87 3.17-2.68 6.35-5.78 9.52-8.46 3.25-2.6 5.86-5.21 8.95-7.89 2.68-2.6 5.86-5.21 8.54-7.89 2.6-2.6 5.21-4.72 7.89-7.32 2.64-2.11 5.14-4.39 7.49-6.83 2.03-2.12 4.72-4.23 6.83-6.35 2.11-2.11 4.23-3.66 6.34-5.69 2.12-1.63 3.66-3.26 5.86-5.29l4.72-4.72c1.62-1.14 2.6-2.68 4.23-3.74 1.05-0.98 2.19-2.11 3.66-3.09 1.13-1.14 2.19-1.63 2.68-2.69l3.17-3.09h0.57c2.68 0.98 5.29 2.53 7.89 3.58 1.63 0.65 2.69 1.63 4.23 2.2 0.57 0.49 1.14 0.49 2.12 0.98 0.56 0 2.11 0.57 2.11 1.14v0.49c-0.57 0.49-1.06 0.49-1.06 0.98l-1.62 1.63-6.84 6.83a76.647 76.647 0 0 0-8.95 10.01c-2.12 2.12-4.31 4.72-6.35 6.84a22.64 22.64 0 0 0-3.25 3.66l-1.55 1.62c-0.57 0.49-1.05 1.47-1.62 1.47-9.44 10.09-19.04 20.59-28.48 30.59l-74.7 78.94c-28.56 29.86-56.64 59.89-85.28 89.83-26.93 28.4-53.95 56.8-80.4 85.2-20.67 21.56-40.69 43.13-61.36 64.69-8.95 9.44-18.06 18.88-27.02 28.88l-0.57 0.57c-7.89 7.81-7.89 16.28-7.89 24.17v177.81c0 16.28 7.89 31.98 24.33 40.36 16.36 7.97 32.31 0 40.69-7.89l62.98-62.5c10.66-10.58 20.75-21.07 30.76-31.57 1.13-1.06 2.11-2.11 3.25-2.11 1.46-0.57 3.58 0.57 4.72 1.62 22.7 14.9 97.27 71.69 97.27 71.69s3.59-31.79 6.31-41.84z"/>
|
||||
<path fill="currentColor" d="M699.65 908.97H948.6c7.12 0 13.06-7.66 11.87-15.33l-4.75-28.07c-2.37-11.5-11.87-20.41-22.52-20.41h-76.72l-4.75-31.9c26.08-11.5 43.89-38.29 43.89-70.19 0-42.12-32.02-76.58-71.16-76.58-39.14 0-71.2 34.42-71.2 76.55 0 31.9 17.78 58.7 43.89 70.19l-3.56 31.9h-77.33c-10.68 0-21.34 8.94-22.52 20.41L689 893.61c-2.4 7.69 3.53 15.36 10.65 15.36z m248.89 19.12M944.08 960.22h-239.4c-9.34 0-16.92-7.58-16.92-16.92v-0.91c0-9.34 7.58-16.92 16.92-16.92h239.4c9.34 0 16.92 7.58 16.92 16.92v0.91c0 9.35-7.57 16.92-16.92 16.92z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<div class="wrap">
|
||||
<h1>主页菜单图标 · 配色挑选</h1>
|
||||
<p class="sub">
|
||||
针对「发起审批」图标底色 + 图形色。未来多个入口应各自配色,避免整屏同一种绿。
|
||||
点选下方色卡,顶部手机预览会同步;底部可复制 <code>iconBg / iconFg</code>。
|
||||
</p>
|
||||
<p class="note">
|
||||
当前实现:底色靠 CSS <code>.tone-approve</code>(浅绿),SVG 内写死 <code>fill="#16a34a"</code>。
|
||||
目标:菜单项可配置 <code>iconBg</code> + <code>iconFg</code>(或复用已有 OA 色对)。
|
||||
</p>
|
||||
|
||||
<div class="phone">
|
||||
<div class="phone-hd">
|
||||
<div class="brand">中科兴皖 · 移动工作台</div>
|
||||
<div class="hello">您好,示例用户</div>
|
||||
</div>
|
||||
<div class="phone-body">
|
||||
<div class="panel">
|
||||
<div class="panel-hd">
|
||||
<span class="panel-title">功能菜单</span>
|
||||
<span class="hint">配色预览</span>
|
||||
</div>
|
||||
<div class="menu-grid" id="liveGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sec">当前(对照)</div>
|
||||
<div class="grid" id="g-current"></div>
|
||||
|
||||
<div class="sec">OA 审批色对(与发起页 ep 图标一致,推荐复用)</div>
|
||||
<div class="grid" id="g-oa"></div>
|
||||
|
||||
<div class="sec">扩展色对(非绿,适合多入口区分)</div>
|
||||
<div class="grid" id="g-extra"></div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="picked">
|
||||
<div class="sw" id="pickedSw"></div>
|
||||
<div>
|
||||
<strong id="pickedName">未选择</strong>
|
||||
<span id="pickedHex">点击色卡挑选</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" id="copyPair">复制 bg, fg</button>
|
||||
<button type="button" class="primary" id="copyJson">复制菜单字段 JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast">已复制</div>
|
||||
|
||||
<script>
|
||||
const ICON = `<svg viewBox="0 0 1024 1024" aria-hidden="true"><use href="#icon-approve"/></svg>`;
|
||||
|
||||
/** 与 mobile-h5/utils/oa-app-icons.js 对齐 */
|
||||
const OA_PAIRS = [
|
||||
{ id: 'oa-0', name: '翠绿', bg: '#d1fae5', fg: '#059669', tag: 'OA', tagClass: '' },
|
||||
{ id: 'oa-1', name: '青蓝', bg: '#cffafe', fg: '#0891b2', tag: 'OA · 推荐', tagClass: 'rec' },
|
||||
{ id: 'oa-2', name: '天蓝', bg: '#dbeafe', fg: '#2563eb', tag: 'OA · 推荐', tagClass: 'rec' },
|
||||
{ id: 'oa-3', name: '靛蓝', bg: '#e0e7ff', fg: '#4f46e5', tag: 'OA', tagClass: '' },
|
||||
{ id: 'oa-4', name: '紫罗兰', bg: '#ede9fe', fg: '#7c3aed', tag: 'OA', tagClass: '' },
|
||||
{ id: 'oa-5', name: '玫粉', bg: '#fce7f3', fg: '#db2777', tag: 'OA', tagClass: '' },
|
||||
{ id: 'oa-6', name: '橙', bg: '#ffedd5', fg: '#ea580c', tag: 'OA · 推荐', tagClass: 'rec' },
|
||||
{ id: 'oa-7', name: '琥珀', bg: '#fef3c7', fg: '#d97706', tag: 'OA', tagClass: '' }
|
||||
];
|
||||
|
||||
const CURRENT = [
|
||||
{
|
||||
id: 'current',
|
||||
name: '当前 tone-approve',
|
||||
bg: '#c8f5de',
|
||||
fg: '#16a34a',
|
||||
tag: '当前',
|
||||
tagClass: 'hot',
|
||||
current: true
|
||||
}
|
||||
];
|
||||
|
||||
const EXTRA = [
|
||||
{ id: 'ex-slate', name: '石板灰', bg: '#eef2f1', fg: '#3d524a', tag: '克制', tagClass: '' },
|
||||
{ id: 'ex-sky', name: '晴空蓝', bg: '#e0f2fe', fg: '#0284c7', tag: '清爽', tagClass: 'rec' },
|
||||
{ id: 'ex-royal', name: '宝蓝', bg: '#dbeafe', fg: '#1d4ed8', tag: '正式', tagClass: 'rec' },
|
||||
{ id: 'ex-indigo', name: '深靛', bg: '#e0e7ff', fg: '#3730a3', tag: '稳重', tagClass: '' },
|
||||
{ id: 'ex-violet', name: '葡萄紫', bg: '#f3e8ff', fg: '#7e22ce', tag: '个性', tagClass: '' },
|
||||
{ id: 'ex-rose', name: '玫瑰红', bg: '#ffe4e6', fg: '#e11d48', tag: '醒目', tagClass: '' },
|
||||
{ id: 'ex-coral', name: '珊瑚', bg: '#ffedd5', fg: '#c2410c', tag: '温暖', tagClass: '' },
|
||||
{ id: 'ex-amber', name: '大麦黄邻', bg: '#fff7e8', fg: '#a67b20', tag: '已有 tone', tagClass: '' },
|
||||
{ id: 'ex-teal', name: '孔雀石', bg: '#ccfbf1', fg: '#0f766e', tag: '青绿', tagClass: '' },
|
||||
{ id: 'ex-lime', name: '青柠', bg: '#ecfccb', fg: '#4d7c0f', tag: '活力', tagClass: '' },
|
||||
{ id: 'ex-brown', name: '咖啡', bg: '#f5e6d3', fg: '#92400e', tag: '沉稳', tagClass: '' },
|
||||
{ id: 'ex-ink', name: '墨蓝', bg: '#e2e8f0', fg: '#334155', tag: '极简', tagClass: '' }
|
||||
];
|
||||
|
||||
/** 预览宫格:第 1 个跟随选中的「发起审批」配色,其余用固定多色演示可定制 */
|
||||
const PREVIEW_OTHERS = [
|
||||
{ title: '待办', bg: '#dbeafe', fg: '#2563eb', glyph: '☰' },
|
||||
{ title: '已办', bg: '#ffedd5', fg: '#ea580c', glyph: '▤' },
|
||||
{ title: '抄送', bg: '#ede9fe', fg: '#7c3aed', glyph: '↗' },
|
||||
{ title: '统计', bg: '#fef3c7', fg: '#d97706', glyph: '¥' },
|
||||
{ title: '通讯录', bg: '#fce7f3', fg: '#db2777', glyph: '☺' },
|
||||
{ title: '设置', bg: '#eef2f1', fg: '#3d524a', glyph: '⚙' },
|
||||
{ title: '更多', bg: '#cffafe', fg: '#0891b2', glyph: '▦' }
|
||||
];
|
||||
|
||||
let selected = CURRENT[0];
|
||||
|
||||
function toast(msg) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
clearTimeout(toast._t);
|
||||
toast._t = setTimeout(() => el.classList.remove('show'), 1400);
|
||||
}
|
||||
|
||||
async function copyText(t) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(t);
|
||||
} catch {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = t;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
ta.remove();
|
||||
}
|
||||
toast('已复制');
|
||||
}
|
||||
|
||||
function renderCard(p, host) {
|
||||
const el = document.createElement('article');
|
||||
el.className = 'card' + (p.current ? ' is-current' : '') + (selected?.id === p.id ? ' is-selected' : '');
|
||||
el.style.setProperty('--fg', p.fg);
|
||||
el.innerHTML = `
|
||||
<div class="icon-box" style="background:${p.bg};color:${p.fg}">${ICON}</div>
|
||||
<div class="name">${p.name}</div>
|
||||
<div class="meta">${p.bg}<br/>${p.fg}</div>
|
||||
<span class="tag ${p.tagClass || ''}">${p.tag || ''}</span>
|
||||
`;
|
||||
el.addEventListener('click', () => {
|
||||
selected = p;
|
||||
sync();
|
||||
});
|
||||
host.appendChild(el);
|
||||
}
|
||||
|
||||
function renderLiveGrid() {
|
||||
const grid = document.getElementById('liveGrid');
|
||||
const first = `
|
||||
<div class="menu-card">
|
||||
<div class="menu-icon" style="background:${selected.bg};color:${selected.fg}">${ICON}</div>
|
||||
<div class="menu-title">发起审批</div>
|
||||
</div>`;
|
||||
const rest = PREVIEW_OTHERS.map(
|
||||
(o) => `
|
||||
<div class="menu-card">
|
||||
<div class="menu-icon" style="background:${o.bg};color:${o.fg}">
|
||||
<span style="font-size:20px;line-height:1;color:${o.fg}">${o.glyph}</span>
|
||||
</div>
|
||||
<div class="menu-title">${o.title}</div>
|
||||
</div>`
|
||||
).join('');
|
||||
grid.innerHTML = first + rest;
|
||||
}
|
||||
|
||||
function syncFooter() {
|
||||
const sw = document.getElementById('pickedSw');
|
||||
sw.style.background = selected.bg;
|
||||
sw.style.color = selected.fg;
|
||||
sw.innerHTML = ICON;
|
||||
document.getElementById('pickedName').textContent = selected.name;
|
||||
document.getElementById('pickedHex').textContent = `${selected.bg} / ${selected.fg}`;
|
||||
}
|
||||
|
||||
function sync() {
|
||||
document.getElementById('g-current').innerHTML = '';
|
||||
document.getElementById('g-oa').innerHTML = '';
|
||||
document.getElementById('g-extra').innerHTML = '';
|
||||
CURRENT.forEach((p) => renderCard(p, document.getElementById('g-current')));
|
||||
OA_PAIRS.forEach((p) => renderCard(p, document.getElementById('g-oa')));
|
||||
EXTRA.forEach((p) => renderCard(p, document.getElementById('g-extra')));
|
||||
renderLiveGrid();
|
||||
syncFooter();
|
||||
}
|
||||
|
||||
document.getElementById('copyPair').addEventListener('click', () => {
|
||||
copyText(`${selected.bg}, ${selected.fg}`);
|
||||
});
|
||||
document.getElementById('copyJson').addEventListener('click', () => {
|
||||
const json = JSON.stringify(
|
||||
{
|
||||
title: '发起审批',
|
||||
icon: 'approve',
|
||||
iconBg: selected.bg,
|
||||
iconFg: selected.fg
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
copyText(json);
|
||||
});
|
||||
|
||||
// 默认推荐:天蓝(正式、非绿)
|
||||
selected = OA_PAIRS[2];
|
||||
sync();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,551 @@
|
||||
<!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 {
|
||||
--brand: #16a34a;
|
||||
--brand-dark: #15803d;
|
||||
--brand-soft: #f0fdf4;
|
||||
--bg: #f5f8f7;
|
||||
--ink: #1f2a26;
|
||||
--muted: #6b7b74;
|
||||
--border: #dcebe4;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(900px 480px at 0% -10%, #dcfce7 0%, transparent 50%),
|
||||
radial-gradient(700px 400px at 100% 0%, #e8f5ee 0%, transparent 45%),
|
||||
var(--bg);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.wrap { max-width: 1100px; margin: 0 auto; padding: 24px 16px 96px; }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; letter-spacing: -0.02em; }
|
||||
.sub { margin: 0; color: var(--muted); font-size: 14px; max-width: 720px; }
|
||||
.note {
|
||||
margin: 14px 0 22px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.note strong { color: var(--brand-dark); font-weight: 650; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.card.is-selected {
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 28%, transparent);
|
||||
}
|
||||
.card.is-rec {
|
||||
outline: 2px dashed color-mix(in srgb, var(--brand) 45%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.letter {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 8px;
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-dark);
|
||||
display: grid; place-items: center;
|
||||
font-weight: 700; font-size: 13px;
|
||||
}
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f3;
|
||||
color: var(--muted);
|
||||
}
|
||||
.tag.warn { background: #fff7ed; color: #c2410c; }
|
||||
.tag.rec { background: var(--brand-soft); color: var(--brand-dark); font-weight: 650; }
|
||||
.card h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.card p {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12.5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.phone {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.phone-body {
|
||||
height: 88px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ft {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--border);
|
||||
box-shadow: 0 -4px 16px rgba(31, 42, 38, 0.04);
|
||||
}
|
||||
.btn {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 12px;
|
||||
transition: opacity .15s, filter .15s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.btn:disabled { cursor: not-allowed; }
|
||||
.btn-cancel {
|
||||
flex: 0 0 64px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 0;
|
||||
}
|
||||
.btn-draft {
|
||||
flex: 1;
|
||||
border: 1.5px solid var(--brand);
|
||||
background: #fff;
|
||||
color: var(--brand-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-submit {
|
||||
flex: 1.35;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--brand), var(--brand-dark));
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.28);
|
||||
}
|
||||
.btn.is-loading { pointer-events: none; }
|
||||
|
||||
/* spinner */
|
||||
.spin {
|
||||
width: 16px; height: 16px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin .7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.spin--lg { width: 18px; height: 18px; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* A: 现状 — 文字消失,只剩转圈 */
|
||||
.a .btn.is-loading .label { display: none; }
|
||||
.a .btn.is-loading .spin-only { display: block; }
|
||||
.a .spin-only { display: none; }
|
||||
|
||||
/* B: 左侧小转圈 + 文案保留/改为「…中」 */
|
||||
/* default markup handles it */
|
||||
|
||||
/* C: 仅改文案,无转圈 */
|
||||
/* handled in JS text swap */
|
||||
|
||||
/* D: 文案 + 跳动三点 */
|
||||
.dots span {
|
||||
display: inline-block;
|
||||
animation: bounce 1.1s infinite ease-in-out;
|
||||
opacity: .35;
|
||||
}
|
||||
.dots span:nth-child(2) { animation-delay: .15s; }
|
||||
.dots span:nth-child(3) { animation-delay: .3s; }
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { opacity: .25; transform: translateY(0); }
|
||||
40% { opacity: 1; transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
/* E: 保留文案 + 按钮内浅色 shimmer */
|
||||
.e .btn.is-loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 35%,
|
||||
rgba(255,255,255,.35) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.1s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
.e .btn-draft.is-loading::after {
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
transparent 35%,
|
||||
rgba(22,163,74,.12) 50%,
|
||||
transparent 65%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
/* F: 保留文案 + 整体脉冲透明度 */
|
||||
.f .btn.is-loading {
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .55; }
|
||||
}
|
||||
|
||||
/* G: 转圈叠在文字上(半透明底)— 接近现状但更差,作对比 */
|
||||
.g .btn.is-loading .label { opacity: .25; }
|
||||
.g .btn.is-loading .overlay-spin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0,0,0,.06);
|
||||
}
|
||||
.g .overlay-spin { display: none; }
|
||||
.g .btn.is-loading .overlay-spin { display: grid; }
|
||||
|
||||
.actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.pick {
|
||||
flex: 1;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pick:hover { border-color: var(--brand); color: var(--brand-dark); }
|
||||
.card.is-selected .pick {
|
||||
background: var(--brand);
|
||||
border-color: var(--brand);
|
||||
color: #fff;
|
||||
font-weight: 650;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
.bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-top: 28px;
|
||||
padding: 12px 14px;
|
||||
background: rgba(255,255,255,.92);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid #e2e8e5;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bar strong { color: var(--brand-dark); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>发起审批 · 底栏按钮加载态对比</h1>
|
||||
<p class="sub">
|
||||
模拟「暂存草稿 / 提交」点击后的等待反馈。点任意主按钮可预览约 2 秒加载态;选中喜欢的方案后告诉我编号即可落地。
|
||||
</p>
|
||||
<div class="note">
|
||||
<strong>现状问题:</strong>Vant <code>:loading</code> 默认会把按钮文字藏掉,中间只剩一个转圈,看起来像「按钮坏了 / 内容丢失」,而不是「正在处理」。
|
||||
更好的做法一般是:<strong>保留语义文案</strong>(如「提交中…」),转圈可有可无、且应偏小、靠左。
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- A -->
|
||||
<article class="card a" data-id="A" data-name="现状:只剩转圈">
|
||||
<div class="badge-row">
|
||||
<span class="letter">A</span>
|
||||
<span class="tag warn">现状 / 不推荐</span>
|
||||
</div>
|
||||
<h2>文字消失 · 中间转圈</h2>
|
||||
<p>Vant 默认 loading:文案隐藏,只留 spinner。容易误以为按钮空白或出错。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft">
|
||||
<span class="spin spin-only" aria-hidden="true"></span>
|
||||
<span class="label">暂存草稿</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit">
|
||||
<span class="spin spin-only spin--lg" aria-hidden="true"></span>
|
||||
<span class="label">提交</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
|
||||
<!-- B -->
|
||||
<article class="card b is-rec" data-id="B" data-name="小转圈 + 文案">
|
||||
<div class="badge-row">
|
||||
<span class="letter">B</span>
|
||||
<span class="tag rec">推荐</span>
|
||||
</div>
|
||||
<h2>左侧小转圈 +「…中」文案</h2>
|
||||
<p>保留可读语义:暂存中… / 提交中…。转圈提示进度,文案说明在做什么。行业常见做法。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft"
|
||||
data-idle="暂存草稿" data-busy="暂存中…">
|
||||
<span class="spin" hidden aria-hidden="true"></span>
|
||||
<span class="label">暂存草稿</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit"
|
||||
data-idle="提交" data-busy="提交中…">
|
||||
<span class="spin" hidden aria-hidden="true"></span>
|
||||
<span class="label">提交</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
|
||||
<!-- C -->
|
||||
<article class="card c" data-id="C" data-name="仅改文案">
|
||||
<div class="badge-row">
|
||||
<span class="letter">C</span>
|
||||
<span class="tag">轻量</span>
|
||||
</div>
|
||||
<h2>只改文案 · 无转圈</h2>
|
||||
<p>「提交」→「提交中…」,按钮禁用。最干净,但进度感稍弱;短请求时够用。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft"
|
||||
data-idle="暂存草稿" data-busy="暂存中…">
|
||||
<span class="label">暂存草稿</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit"
|
||||
data-idle="提交" data-busy="提交中…">
|
||||
<span class="label">提交</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
|
||||
<!-- D -->
|
||||
<article class="card d" data-id="D" data-name="跳动三点">
|
||||
<div class="badge-row">
|
||||
<span class="letter">D</span>
|
||||
<span class="tag">活泼</span>
|
||||
</div>
|
||||
<h2>文案 + 跳动三点</h2>
|
||||
<p>「提交中···」三点依次闪动。无转圈、有动效,适合品牌感稍强的场景。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft"
|
||||
data-idle="暂存草稿" data-busy-html='暂存中<span class="dots"><span>·</span><span>·</span><span>·</span></span>'>
|
||||
<span class="label">暂存草稿</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit"
|
||||
data-idle="提交" data-busy-html='提交中<span class="dots"><span>·</span><span>·</span><span>·</span></span>'>
|
||||
<span class="label">提交</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
|
||||
<!-- E -->
|
||||
<article class="card e" data-id="E" data-name="光带扫描">
|
||||
<div class="badge-row">
|
||||
<span class="letter">E</span>
|
||||
<span class="tag">动感</span>
|
||||
</div>
|
||||
<h2>保留文案 · 光带扫描</h2>
|
||||
<p>文字不变(或改为「…中」),按钮表面扫光。进度感强,实现稍重一点。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft"
|
||||
data-idle="暂存草稿" data-busy="暂存中…">
|
||||
<span class="label">暂存草稿</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit"
|
||||
data-idle="提交" data-busy="提交中…">
|
||||
<span class="label">提交</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
|
||||
<!-- F -->
|
||||
<article class="card f" data-id="F" data-name="透明度脉冲">
|
||||
<div class="badge-row">
|
||||
<span class="letter">F</span>
|
||||
<span class="tag">极简</span>
|
||||
</div>
|
||||
<h2>保留文案 · 整体脉冲</h2>
|
||||
<p>文字改为「…中」,按钮透明度呼吸。实现最简单,但不够「明确在请求」。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft"
|
||||
data-idle="暂存草稿" data-busy="暂存中…">
|
||||
<span class="label">暂存草稿</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit"
|
||||
data-idle="提交" data-busy="提交中…">
|
||||
<span class="label">提交</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
|
||||
<!-- G -->
|
||||
<article class="card g" data-id="G" data-name="转圈压字">
|
||||
<div class="badge-row">
|
||||
<span class="letter">G</span>
|
||||
<span class="tag warn">对比用</span>
|
||||
</div>
|
||||
<h2>转圈压在字上</h2>
|
||||
<p>文字半透明仍在,转圈居中覆盖。比 A 稍好但仍挤、难读,一般不建议。</p>
|
||||
<div class="phone">
|
||||
<div class="phone-body">表单区占位…</div>
|
||||
<div class="ft">
|
||||
<button type="button" class="btn btn-cancel">取消</button>
|
||||
<button type="button" class="btn btn-draft" data-role="draft">
|
||||
<span class="label">暂存草稿</span>
|
||||
<span class="overlay-spin"><span class="spin"></span></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-submit" data-role="submit">
|
||||
<span class="label">提交</span>
|
||||
<span class="overlay-spin"><span class="spin" style="border-color:#fff;border-right-color:transparent"></span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">点「暂存草稿」或「提交」试效果</p>
|
||||
<div class="actions"><button type="button" class="pick">选这个</button></div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="bar">
|
||||
<span>当前选择:<strong id="choice">未选</strong></span>
|
||||
<span>建议优先试 <strong>B</strong>(小转圈 + 文案),短接口也可考虑 <strong>C</strong>。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const timers = new WeakMap()
|
||||
|
||||
function setLoading(card, btn, on) {
|
||||
const id = card.dataset.id
|
||||
const label = btn.querySelector('.label')
|
||||
const spin = btn.querySelector('.spin:not(.spin-only)')
|
||||
|
||||
btn.classList.toggle('is-loading', on)
|
||||
btn.disabled = on
|
||||
|
||||
if (id === 'A') {
|
||||
// CSS handles hide label / show spin-only
|
||||
return
|
||||
}
|
||||
if (id === 'G') {
|
||||
return
|
||||
}
|
||||
if (spin) spin.hidden = !on
|
||||
|
||||
if (btn.dataset.busyHtml) {
|
||||
if (on) label.innerHTML = btn.dataset.busyHtml
|
||||
else label.textContent = btn.dataset.idle || label.textContent
|
||||
return
|
||||
}
|
||||
if (btn.dataset.busy) {
|
||||
if (on) label.textContent = btn.dataset.busy
|
||||
else label.textContent = btn.dataset.idle || label.textContent
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.card').forEach((card) => {
|
||||
card.querySelectorAll('[data-role]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.classList.contains('is-loading')) return
|
||||
// clear sibling loading in same footer
|
||||
card.querySelectorAll('[data-role]').forEach((b) => {
|
||||
clearTimeout(timers.get(b))
|
||||
setLoading(card, b, false)
|
||||
})
|
||||
setLoading(card, btn, true)
|
||||
const t = setTimeout(() => setLoading(card, btn, false), 2000)
|
||||
timers.set(btn, t)
|
||||
})
|
||||
})
|
||||
|
||||
card.querySelector('.pick').addEventListener('click', () => {
|
||||
document.querySelectorAll('.card').forEach((c) => c.classList.remove('is-selected'))
|
||||
card.classList.add('is-selected')
|
||||
document.getElementById('choice').textContent =
|
||||
card.dataset.id + ' · ' + card.dataset.name
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,514 @@
|
||||
<!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 {
|
||||
--brand: #16a34a;
|
||||
--brand-dark: #15803d;
|
||||
--brand-soft: #f0fdf4;
|
||||
--bg: #f5f8f7;
|
||||
--surface: #fff;
|
||||
--ink: #1f2a26;
|
||||
--muted: #6b7b74;
|
||||
--border: #dcebe4;
|
||||
--slate: #334155;
|
||||
--slate-soft: #94a3b8;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(900px 480px at 0% -10%, #dcfce7 0%, transparent 50%),
|
||||
radial-gradient(700px 400px at 100% 0%, #e8f5ee 0%, transparent 45%),
|
||||
var(--bg);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 24px 18px 80px; }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; letter-spacing: -0.02em; }
|
||||
.sub { margin: 0; color: var(--muted); font-size: 14px; max-width: 720px; }
|
||||
.note {
|
||||
margin: 14px 0 22px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.note strong { color: var(--brand-dark); font-weight: 650; }
|
||||
.phones {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
.phone-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s, box-shadow .15s, transform .15s;
|
||||
}
|
||||
.phone-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 28px rgba(20, 40, 30, .08);
|
||||
}
|
||||
.phone-card.is-selected {
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 28%, transparent);
|
||||
}
|
||||
.phone-card.is-rec {
|
||||
outline: 2px dashed color-mix(in srgb, var(--brand) 45%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.letter {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 8px;
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-dark);
|
||||
display: grid; place-items: center;
|
||||
font-weight: 700; font-size: 13px;
|
||||
}
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #ecfdf5;
|
||||
color: #15803d;
|
||||
border: 1px solid #a7f3d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge.muted {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
.title { margin: 0 0 4px; font-size: 15px; font-weight: 700; }
|
||||
.desc { margin: 0 0 12px; font-size: 12px; color: var(--muted); min-height: 36px; }
|
||||
|
||||
/* phone chrome */
|
||||
.phone {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
border-radius: 28px;
|
||||
border: 1px solid #cfd8d3;
|
||||
background: #111;
|
||||
padding: 10px;
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, .12);
|
||||
}
|
||||
.screen {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
min-height: 520px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #eef2f0;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
color: var(--ink);
|
||||
position: relative;
|
||||
}
|
||||
.nav::before {
|
||||
content: "‹";
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
font-size: 22px;
|
||||
color: var(--ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.search-wrap {
|
||||
padding: 10px 12px 4px;
|
||||
background: #fff;
|
||||
}
|
||||
.search {
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg);
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tab {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.tab.on {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
.hint {
|
||||
padding: 8px 12px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
background: var(--bg);
|
||||
}
|
||||
.body-area {
|
||||
flex: 1;
|
||||
background: var(--bg);
|
||||
padding: 8px 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* A: current plain text */
|
||||
.state-plain {
|
||||
padding: 48px 12px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* B: brand spinner */
|
||||
.state-spin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 56px 12px 40px;
|
||||
}
|
||||
.spin-ring {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #dcfce7;
|
||||
border-top-color: var(--brand);
|
||||
animation: spin 0.85s linear infinite;
|
||||
}
|
||||
.spin-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 28px 32px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 8px 24px rgba(21, 128, 61, 0.08);
|
||||
}
|
||||
.spin-text {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.spin-sub {
|
||||
font-size: 11px;
|
||||
color: var(--slate-soft);
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
/* C: skeleton cards */
|
||||
.sec-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 4px 0 10px;
|
||||
}
|
||||
.sk {
|
||||
background: linear-gradient(90deg, #e8eeeB 25%, #f4f7f5 50%, #e8eeeB 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.2s ease-in-out infinite;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.sk-title { width: 72px; height: 14px; }
|
||||
.sk-meta { width: 48px; height: 12px; margin-left: auto; }
|
||||
.sk-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: #fca5a5;
|
||||
flex-shrink: 0;
|
||||
opacity: .7;
|
||||
}
|
||||
.card-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.sk-card {
|
||||
min-height: 96px;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #d8e3de;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.sk-main { display: flex; gap: 10px; }
|
||||
.sk-icon {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sk-lines { flex: 1; display: flex; flex-direction: column; gap: 6px; padding-top: 2px; }
|
||||
.sk-name { width: 78%; height: 12px; }
|
||||
.sk-desc { width: 55%; height: 10px; }
|
||||
.sk-ops {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.sk-btn { width: 40px; height: 22px; border-radius: 6px; }
|
||||
|
||||
/* D: soft pulse dots */
|
||||
.state-pulse {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 64px 12px;
|
||||
}
|
||||
.pulse-dots {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.pulse-dots span {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand);
|
||||
animation: bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
.pulse-dots span:nth-child(2) { animation-delay: .15s; opacity: .75; }
|
||||
.pulse-dots span:nth-child(3) { animation-delay: .3s; opacity: .55; }
|
||||
.pulse-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
.pick-hint {
|
||||
margin-top: 22px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>发起审批 · 加载态方案</h1>
|
||||
<p class="sub">
|
||||
当前页打开时只有一行「加载中…」,观感偏空。下面四种方案都保留搜索栏与分组 Tab(已渲染的壳),仅替换列表区域的加载表现。色值对齐 mobile-h5 Token(品牌绿 <code>#16a34a</code>)。
|
||||
</p>
|
||||
<p class="note">
|
||||
点击卡片可选中;推荐 <strong>C · 骨架屏</strong>(布局不跳动、等待时也有结构感)。选好后在对话里告诉我字母即可。
|
||||
</p>
|
||||
|
||||
<div class="phones">
|
||||
<!-- 0 current -->
|
||||
<div class="phone-card" data-choice="0" onclick="pick(this)">
|
||||
<div class="badge-row">
|
||||
<div class="letter">0</div>
|
||||
<span class="badge muted">现状</span>
|
||||
</div>
|
||||
<h3 class="title">纯文字</h3>
|
||||
<p class="desc">仅居中「加载中…」,无动效、无结构提示。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav">发起审批</div>
|
||||
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
|
||||
<div class="tabs">
|
||||
<div class="tab on">全部</div>
|
||||
<div class="tab">人事</div>
|
||||
<div class="tab">行政</div>
|
||||
</div>
|
||||
<div class="hint">选择一个审批单即可发起</div>
|
||||
<div class="body-area">
|
||||
<div class="state-plain">加载中…</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- A spinner -->
|
||||
<div class="phone-card" data-choice="a" onclick="pick(this)">
|
||||
<div class="badge-row">
|
||||
<div class="letter">A</div>
|
||||
<span class="badge muted">轻量</span>
|
||||
</div>
|
||||
<h3 class="title">品牌旋转环</h3>
|
||||
<p class="desc">白卡片 + 绿色 spinner + 文案,改动最小,接近 Vant Loading。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav">发起审批</div>
|
||||
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
|
||||
<div class="tabs">
|
||||
<div class="tab on">全部</div>
|
||||
<div class="tab">人事</div>
|
||||
<div class="tab">行政</div>
|
||||
</div>
|
||||
<div class="hint">选择一个审批单即可发起</div>
|
||||
<div class="body-area">
|
||||
<div class="state-spin">
|
||||
<div class="spin-card">
|
||||
<div class="spin-ring"></div>
|
||||
<div class="spin-text">正在加载审批模板</div>
|
||||
<div class="spin-sub">请稍候</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- B pulse -->
|
||||
<div class="phone-card" data-choice="b" onclick="pick(this)">
|
||||
<div class="badge-row">
|
||||
<div class="letter">B</div>
|
||||
<span class="badge muted">克制</span>
|
||||
</div>
|
||||
<h3 class="title">三点弹跳</h3>
|
||||
<p class="desc">无卡片、更轻;适合短等待,但信息感偏弱。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav">发起审批</div>
|
||||
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
|
||||
<div class="tabs">
|
||||
<div class="tab on">全部</div>
|
||||
<div class="tab">人事</div>
|
||||
<div class="tab">行政</div>
|
||||
</div>
|
||||
<div class="hint">选择一个审批单即可发起</div>
|
||||
<div class="body-area">
|
||||
<div class="state-pulse">
|
||||
<div class="pulse-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="pulse-label">加载审批模板…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- C skeleton -->
|
||||
<div class="phone-card is-rec" data-choice="c" onclick="pick(this)">
|
||||
<div class="badge-row">
|
||||
<div class="letter">C</div>
|
||||
<span class="badge">推荐</span>
|
||||
</div>
|
||||
<h3 class="title">双列骨架屏</h3>
|
||||
<p class="desc">模拟分组标题 + 双列卡片占位,加载完成后几乎无布局跳动。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav">发起审批</div>
|
||||
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
|
||||
<div class="tabs">
|
||||
<div class="tab on">全部</div>
|
||||
<div class="tab">人事</div>
|
||||
<div class="tab">行政</div>
|
||||
</div>
|
||||
<div class="hint">选择一个审批单即可发起</div>
|
||||
<div class="body-area">
|
||||
<div class="sec-hd">
|
||||
<div class="sk sk-title"></div>
|
||||
<div class="sk-dot"></div>
|
||||
<div class="sk sk-meta"></div>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<div class="sk-card">
|
||||
<div class="sk-main">
|
||||
<div class="sk sk-icon"></div>
|
||||
<div class="sk-lines">
|
||||
<div class="sk sk-name"></div>
|
||||
<div class="sk sk-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-ops"><div class="sk sk-btn"></div></div>
|
||||
</div>
|
||||
<div class="sk-card">
|
||||
<div class="sk-main">
|
||||
<div class="sk sk-icon"></div>
|
||||
<div class="sk-lines">
|
||||
<div class="sk sk-name"></div>
|
||||
<div class="sk sk-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-ops"><div class="sk sk-btn"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sec-hd">
|
||||
<div class="sk sk-title" style="width:56px"></div>
|
||||
<div class="sk-dot"></div>
|
||||
<div class="sk sk-meta" style="width:40px"></div>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<div class="sk-card">
|
||||
<div class="sk-main">
|
||||
<div class="sk sk-icon"></div>
|
||||
<div class="sk-lines">
|
||||
<div class="sk sk-name"></div>
|
||||
<div class="sk sk-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-ops"><div class="sk sk-btn"></div></div>
|
||||
</div>
|
||||
<div class="sk-card">
|
||||
<div class="sk-main">
|
||||
<div class="sk sk-icon"></div>
|
||||
<div class="sk-lines">
|
||||
<div class="sk sk-name"></div>
|
||||
<div class="sk sk-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-ops"><div class="sk sk-btn"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="pick-hint">选中结果会记在下方;也可直接回复 A / B / C。</p>
|
||||
<p id="picked" class="pick-hint" style="color:var(--brand-dark);font-weight:650;"></p>
|
||||
</div>
|
||||
<script>
|
||||
function pick(el) {
|
||||
document.querySelectorAll('.phone-card').forEach((c) => c.classList.remove('is-selected'))
|
||||
el.classList.add('is-selected')
|
||||
const map = { '0': '现状(纯文字)', a: 'A · 品牌旋转环', b: 'B · 三点弹跳', c: 'C · 双列骨架屏' }
|
||||
const choice = el.getAttribute('data-choice')
|
||||
document.getElementById('picked').textContent = '当前选择:' + (map[choice] || choice)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,422 @@
|
||||
<!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 {
|
||||
--brand: #16a34a;
|
||||
--brand-dark: #15803d;
|
||||
--brand-soft: #f0fdf4;
|
||||
--bg: #f5f8f7;
|
||||
--surface: #fff;
|
||||
--ink: #1f2a26;
|
||||
--muted: #6b7b74;
|
||||
--border: #dcebe4;
|
||||
--danger: #d14343;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(900px 480px at 0% -10%, #dcfce7 0%, transparent 50%),
|
||||
radial-gradient(700px 400px at 100% 0%, #e8f5ee 0%, transparent 45%),
|
||||
var(--bg);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: 24px 18px 80px; }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; letter-spacing: -0.02em; }
|
||||
.sub { margin: 0; color: var(--muted); font-size: 14px; max-width: 720px; }
|
||||
.note {
|
||||
margin: 14px 0 22px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
h2 {
|
||||
margin: 28px 0 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hint { margin: 0 0 14px; font-size: 13px; color: var(--muted); }
|
||||
|
||||
.phones {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.phone-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.phone-card:hover {
|
||||
border-color: #86efac;
|
||||
box-shadow: 0 8px 28px rgba(21, 128, 61, 0.1);
|
||||
}
|
||||
.phone-card.selected {
|
||||
border-color: var(--brand);
|
||||
box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.25);
|
||||
}
|
||||
.phone-card .label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-dark);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.phone-card .desc {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.phone {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #cbd5d1;
|
||||
background: var(--bg);
|
||||
box-shadow: 0 10px 30px rgba(31, 42, 38, 0.08);
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
padding: 0 10px;
|
||||
color: #fff;
|
||||
background: var(--brand);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav .back { opacity: 0.9; font-weight: 400; font-size: 18px; width: 36px; }
|
||||
.nav .title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.nav .right { width: 36px; }
|
||||
|
||||
.inner-hd {
|
||||
padding: 12px 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--brand), var(--brand-dark));
|
||||
font-size: 15px;
|
||||
}
|
||||
.inner-hd.gone {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 12px;
|
||||
min-height: 220px;
|
||||
}
|
||||
.field {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.field .k { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
||||
.field .v { font-size: 14px; color: #94a3b8; }
|
||||
|
||||
.ft {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 14px;
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--brand);
|
||||
border-color: var(--brand);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* A: 等宽三键,主色提交,描边次要 */
|
||||
.style-a .btn-ghost {
|
||||
background: #fff;
|
||||
border-color: var(--border);
|
||||
color: var(--muted);
|
||||
}
|
||||
.style-a .btn-soft {
|
||||
background: var(--brand-soft);
|
||||
border-color: #bbf7d0;
|
||||
color: var(--brand-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* B: 取消弱化,暂存描边,提交加宽主按钮 */
|
||||
.style-b .ft { gap: 10px; }
|
||||
.style-b .btn-cancel {
|
||||
flex: 0 0 64px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
.style-b .btn-draft {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1.5px solid var(--brand);
|
||||
color: var(--brand-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.style-b .btn-submit {
|
||||
flex: 1.35;
|
||||
background: linear-gradient(135deg, var(--brand), var(--brand-dark));
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.28);
|
||||
}
|
||||
|
||||
/* C: 圆角胶囊 + 图标感文字层次 */
|
||||
.style-c .ft {
|
||||
gap: 8px;
|
||||
padding: 12px 12px 16px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.92), #fff);
|
||||
}
|
||||
.style-c .btn {
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.style-c .btn-cancel {
|
||||
background: #f5f8f7;
|
||||
border-color: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
.style-c .btn-draft {
|
||||
background: #fff;
|
||||
border: 1.5px solid #86efac;
|
||||
color: var(--brand-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
.style-c .btn-submit {
|
||||
background: linear-gradient(135deg, #22c55e, var(--brand-dark));
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 18px rgba(22, 163, 74, 0.32);
|
||||
}
|
||||
|
||||
.legend {
|
||||
margin-top: 18px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8e5;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.legend strong { color: var(--ink); }
|
||||
.pick {
|
||||
position: sticky;
|
||||
bottom: 12px;
|
||||
margin-top: 20px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--brand);
|
||||
box-shadow: 0 8px 24px rgba(21, 128, 61, 0.12);
|
||||
font-size: 14px;
|
||||
}
|
||||
.pick em { font-style: normal; color: var(--brand-dark); font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>填写审批 · 标题与底栏按钮</h1>
|
||||
<p class="sub">顶部栏显示流程名称(如「入库单」),去掉页面内重复绿条标题;底部「取消 / 暂存草稿 / 提交」三套样式可选。</p>
|
||||
<p class="note">示例流程:入库单。点击下方某一方案卡片即可选中(也可在对话里直接回复 A / B / C)。</p>
|
||||
|
||||
<h2>1. 标题结构(已定方向)</h2>
|
||||
<p class="hint">左侧为现状,右侧为调整后:导航栏标题 = 流程名;去掉界面内「入库单」绿条。</p>
|
||||
<div class="phones">
|
||||
<div class="phone-card" style="cursor: default;">
|
||||
<div class="label"><span class="badge">旧</span> 现状</div>
|
||||
<p class="desc">导航「填写审批」+ 页内再显示「入库单」,标题重复。</p>
|
||||
<div class="phone">
|
||||
<div class="nav">
|
||||
<span class="back">‹</span>
|
||||
<span class="title">填写审批</span>
|
||||
<span class="right"></span>
|
||||
</div>
|
||||
<div class="inner-hd">入库单</div>
|
||||
<div class="body">
|
||||
<div class="field"><div class="k">仓库</div><div class="v">请选择</div></div>
|
||||
<div class="field"><div class="k">备注</div><div class="v">选填</div></div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="btn">取消</div>
|
||||
<div class="btn">暂存草稿</div>
|
||||
<div class="btn btn-primary">提交</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-card" style="cursor: default;">
|
||||
<div class="label"><span class="badge">新</span> 调整后</div>
|
||||
<p class="desc">导航直接显示「入库单」;页内绿条标题移除,表单上移。</p>
|
||||
<div class="phone">
|
||||
<div class="nav">
|
||||
<span class="back">‹</span>
|
||||
<span class="title">入库单</span>
|
||||
<span class="right"></span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field"><div class="k">仓库</div><div class="v">请选择</div></div>
|
||||
<div class="field"><div class="k">备注</div><div class="v">选填</div></div>
|
||||
</div>
|
||||
<div class="ft style-b">
|
||||
<div class="btn btn-cancel">取消</div>
|
||||
<div class="btn btn-draft">暂存草稿</div>
|
||||
<div class="btn btn-submit">提交</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>2. 底部按钮样式(请选一)</h2>
|
||||
<p class="hint">三套均基于品牌绿 #16a34a;主行动「提交」视觉权重最高。</p>
|
||||
|
||||
<div class="phones" id="choices">
|
||||
<div class="phone-card" data-choice="a" onclick="pick(this)">
|
||||
<div class="label"><span class="badge">A</span> 等宽三键 · 浅绿暂存</div>
|
||||
<p class="desc">取消中性描边;暂存浅绿底;提交实心主色。三键等宽,最稳妥。</p>
|
||||
<div class="phone style-a">
|
||||
<div class="nav">
|
||||
<span class="back">‹</span>
|
||||
<span class="title">入库单</span>
|
||||
<span class="right"></span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field"><div class="k">仓库</div><div class="v">请选择</div></div>
|
||||
<div class="field"><div class="k">备注</div><div class="v">选填</div></div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="btn btn-ghost">取消</div>
|
||||
<div class="btn btn-soft">暂存草稿</div>
|
||||
<div class="btn btn-primary">提交</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-card" data-choice="b" onclick="pick(this)">
|
||||
<div class="label"><span class="badge">B</span> 取消弱化 · 提交加宽(推荐)</div>
|
||||
<p class="desc">取消缩成文字按钮;暂存品牌描边;提交加宽渐变,主次更清晰。</p>
|
||||
<div class="phone style-b">
|
||||
<div class="nav">
|
||||
<span class="back">‹</span>
|
||||
<span class="title">入库单</span>
|
||||
<span class="right"></span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field"><div class="k">仓库</div><div class="v">请选择</div></div>
|
||||
<div class="field"><div class="k">备注</div><div class="v">选填</div></div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="btn btn-cancel">取消</div>
|
||||
<div class="btn btn-draft">暂存草稿</div>
|
||||
<div class="btn btn-submit">提交</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-card" data-choice="c" onclick="pick(this)">
|
||||
<div class="label"><span class="badge">C</span> 胶囊圆角 · 更高底栏</div>
|
||||
<p class="desc">全圆角胶囊;底栏略高、触达更友好;风格更「C 端」一些。</p>
|
||||
<div class="phone style-c">
|
||||
<div class="nav">
|
||||
<span class="back">‹</span>
|
||||
<span class="title">入库单</span>
|
||||
<span class="right"></span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="field"><div class="k">仓库</div><div class="v">请选择</div></div>
|
||||
<div class="field"><div class="k">备注</div><div class="v">选填</div></div>
|
||||
</div>
|
||||
<div class="ft">
|
||||
<div class="btn btn-cancel">取消</div>
|
||||
<div class="btn btn-draft">暂存草稿</div>
|
||||
<div class="btn btn-submit">提交</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<strong>实现要点(选定后):</strong>
|
||||
uni-app 顶栏用流程名(query 传 <code>appName</code> 或 webview 内加载后同步);
|
||||
<code>OaStart.vue</code> 去掉 <code>.oa-start__hd</code>;
|
||||
底栏按所选样式改 Vant 按钮 class / SCSS。
|
||||
</div>
|
||||
|
||||
<div class="pick" id="pickBar">当前未选择 — 请点击上方 A / B / C</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function pick(el) {
|
||||
document.querySelectorAll('.phone-card[data-choice]').forEach(function (n) {
|
||||
n.classList.remove('selected')
|
||||
})
|
||||
el.classList.add('selected')
|
||||
var map = { a: 'A · 等宽三键', b: 'B · 取消弱化 + 提交加宽(推荐)', c: 'C · 胶囊圆角' }
|
||||
var c = el.getAttribute('data-choice')
|
||||
document.getElementById('pickBar').innerHTML =
|
||||
'已选:<em>' + map[c] + '</em> — 在对话里回复该字母即可继续'
|
||||
if (typeof toggleSelect === 'function') {
|
||||
try { toggleSelect(el) } catch (e) {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -715,7 +715,7 @@ git commit -m "feat(mobile-oa-h5): align OA kit runtime helpers with web-admin"
|
||||
- Modify: `docs/superpowers/specs/2026-08-11-mobile-oa-form-sink-spike-notes.md`(可追加阶段二验收记录)
|
||||
- 可选:更新设计文档 §9 实施顺序勾选状态
|
||||
|
||||
- [ ] **Step 1: 验收清单(全部勾选才算阶段二完成)** — 代码侧就绪;**真机 1~8 项见 spike 笔记「Task 9 验收记录」待勾**
|
||||
- [ ] **Step 1: 验收清单(全部勾选才算阶段二完成)** — 代码侧就绪;**真机 1~8 项见 spike 笔记「阶段二验收清单(Task 9)」待勾**
|
||||
|
||||
| # | 项 | 通过标准 |
|
||||
|---|----|----------|
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
- Create: `mobile-h5/utils/oa-app-icons.js`
|
||||
- Create: `mobile-h5/scripts/check-oa-app-icons.js`
|
||||
|
||||
- [ ] **Step 1: Write the check script (failing until util exists)**
|
||||
- [x] **Step 1: Write the check script (failing until util exists)**
|
||||
|
||||
Create `mobile-h5/scripts/check-oa-app-icons.js`:
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 发起审批页加载态(三点弹跳)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:** 将发起审批页加载态从纯文字改为品牌绿三点弹跳 +「加载审批模板…」。
|
||||
|
||||
**Architecture:** 仅改 `pages/oa/start/index.vue` 的 loading 模板与 scoped CSS;逻辑不变。
|
||||
|
||||
**Tech Stack:** uni-app Vue SFC、scoped SCSS/CSS、mobile-h5 Token `#16a34a`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 替换加载态 UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/pages/oa/start/index.vue`
|
||||
|
||||
- [x] **Step 1:** 将 `v-if="loading"` 分支改为三点弹跳结构 + 文案
|
||||
- [x] **Step 2:** 增加 `.state-loading`、`.pulse-dots` 与 `@keyframes bounce` 样式
|
||||
- [x] **Step 3:** 浏览器打开 `/#/pages/oa/start/index` 确认加载动效与加载完成后列表/空态正常
|
||||
|
||||
---
|
||||
@@ -0,0 +1,37 @@
|
||||
# 发起审批页 · 加载态美化(三点弹跳)
|
||||
|
||||
**日期:** 2026-08-11
|
||||
**状态:** 已确认
|
||||
**Demo:** `docs/oa-start-loading-demo.html`(方案 B)
|
||||
|
||||
## 目标
|
||||
|
||||
改善 `mobile-h5`「发起审批」页(`/#/pages/oa/start/index`)打开时的加载表现:由纯文字「加载中…」改为更轻、更克制的三点弹跳动效。
|
||||
|
||||
## 方案
|
||||
|
||||
- **位置**:列表区域居中(搜索栏、分组 Tab、提示文案在加载时仍可见)。
|
||||
- **动效**:三个品牌绿圆点(`#16a34a`)依次上下弹跳;下方文案 `加载审批模板…`。
|
||||
- **实现**:仅改 `mobile-h5/pages/oa/start/index.vue` 的 loading 分支模板与 scoped CSS;不引入新组件、不依赖 Vant Loading。
|
||||
- **空态**:仍用现有 `.state` 居中文案「暂无可发起的审批模板」,不改动。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不做骨架屏、全页遮罩、旋转环方案。
|
||||
- 不改选择器弹层、按钮 `:loading` 等交互态。
|
||||
- 不改加载逻辑(仍为原有 `loading` / `detailLoading` 开关与接口请求)。
|
||||
|
||||
## 范围补充(mobile-oa-h5)
|
||||
|
||||
同一套三点弹跳复用到填写审批 H5:
|
||||
|
||||
- 新增 `mobile-oa-h5/src/oa/components/OaPulseLoading.vue`
|
||||
- `OaStart.vue` 模板加载:`加载审批模板…`
|
||||
- `OaFlowPreview.vue` 流程预览加载:`加载中…`
|
||||
|
||||
## 验收
|
||||
|
||||
1. 打开发起审批页(mobile-h5),列表区出现三点弹跳 +「加载审批模板…」。
|
||||
2. 打开填写审批(mobile-oa-h5),整页加载与流程预览加载均为同一套三点弹跳。
|
||||
3. 加载完成后动效消失,正常展示内容或空态。
|
||||
4. 色值与品牌绿 `#16a34a` 一致。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 填写审批页:流程标题顶栏化 + 底栏按钮 B
|
||||
|
||||
**日期:** 2026-08-11
|
||||
**状态:** 已确认,落地中
|
||||
|
||||
## 目标
|
||||
|
||||
1. uni-app 顶栏标题由固定「填写审批」改为当前流程名称(如「入库单」)。
|
||||
2. 去掉 OA H5 页内顶部绿条流程名(`.oa-start__hd`),避免与顶栏重复。
|
||||
3. 底栏采用方案 B:取消弱化 + 暂存品牌描边 + 提交加宽渐变。
|
||||
|
||||
## 方案
|
||||
|
||||
- **标题**:跳转前 `stashOaStartTitle(appName)`,webview `onLoad` 读取后 `setNavigationBarTitle`(**不要**把中文 `appName` 放进 `navigateTo` / `web-view` URL,微信易白屏)。无缓存时按 `appId` 拉模板详情补标题;仍失败则保留「填写审批」。
|
||||
- **页内标题**:删除 `mobile-oa-h5` `OaStart.vue` 的 header 与对应样式;加载后同步 `document.title`(直连 H5 时有用)。
|
||||
- **底栏**:仅改 `OaStart.vue` footer 结构与 scoped 样式,对齐 demo 方案 B。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改其它审批详情/待办页。
|
||||
- 不改为 custom navigationStyle。
|
||||
@@ -0,0 +1 @@
|
||||
b5bd60077e8e9ca31f71775e2ca2fe08
|
||||
@@ -6,7 +6,7 @@
|
||||
<view class="up-title">添加附件</view>
|
||||
<view class="up-sub">支持 PDF / 图片 / Office,可多选上传</view>
|
||||
</view>
|
||||
<van-icon name="arrow" color="#0a9f6d" />
|
||||
<van-icon name="arrow" color="#16a34a" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<van-button size="small" plain hairline type="default" @click.stop="$emit('view', item)">
|
||||
查看
|
||||
</van-button>
|
||||
<van-button size="small" type="primary" color="#0a9f6d" @click.stop="$emit('action', item)">
|
||||
<van-button size="small" type="primary" color="#16a34a" @click.stop="$emit('action', item)">
|
||||
处理
|
||||
</van-button>
|
||||
</view>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
class="menu-card"
|
||||
@click="$emit('select', m)"
|
||||
>
|
||||
<view class="menu-icon" :class="m.tone || ''">
|
||||
<VanIcon :name="m.icon" />
|
||||
<view class="menu-icon" :class="toneClass(m)" :style="iconBoxStyle(m)">
|
||||
<VanIcon :name="m.icon" :color="iconFg(m)" size="52rpx" />
|
||||
</view>
|
||||
<text class="menu-title">{{ m.title }}</text>
|
||||
</view>
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<script>
|
||||
import VanIcon from './van-icon/van-icon.vue'
|
||||
import { resolveMenuIconColors } from '../utils/menu-icon-colors.js'
|
||||
|
||||
export default {
|
||||
name: 'MenuGrid',
|
||||
@@ -23,6 +24,27 @@
|
||||
props: {
|
||||
menus: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: ['select']
|
||||
emits: ['select'],
|
||||
methods: {
|
||||
colors(m) {
|
||||
return resolveMenuIconColors(m)
|
||||
},
|
||||
iconFg(m) {
|
||||
return this.colors(m).iconFg || ''
|
||||
},
|
||||
iconBoxStyle(m) {
|
||||
const { iconBg, iconFg } = this.colors(m)
|
||||
const style = {}
|
||||
if (iconBg) style.background = iconBg
|
||||
if (iconFg) style.color = iconFg
|
||||
return style
|
||||
},
|
||||
/** 有自定义色时不再套 tone-*,避免绿底覆盖 */
|
||||
toneClass(m) {
|
||||
const { iconBg, iconFg } = this.colors(m)
|
||||
if (iconBg || iconFg) return ''
|
||||
return m.tone || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
v-else
|
||||
class="oa-app-icon__photo"
|
||||
:src="resolved.src"
|
||||
mode="aspectFill"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
name: 'OaAppIcon',
|
||||
props: {
|
||||
iconUrl: { type: String, default: '' },
|
||||
/** sm=32 card, md=56 */
|
||||
/** sm=32, card=36, md=56 */
|
||||
size: { type: String, default: 'sm' }
|
||||
},
|
||||
computed: {
|
||||
@@ -55,16 +55,28 @@ export default {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.oa-app-icon--card {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.oa-app-icon--md {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.oa-app-icon__glyph {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.oa-app-icon--card .oa-app-icon__glyph {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.oa-app-icon--md .oa-app-icon__glyph {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@@ -72,5 +84,6 @@ export default {
|
||||
.oa-app-icon__photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,18 @@
|
||||
<template>
|
||||
<text class="v-icon" :style="iconStyle">{{ glyph }}</text>
|
||||
<!--
|
||||
微信小程序真机:
|
||||
- CSS mask-image + 本地 SVG 常失效
|
||||
- <image> 对 SVG / svg+xml data-URI 也常失效
|
||||
故精致图标用本地 PNG;尺寸必须写死 px/rpx(勿用 em,自定义组件内 em 易为 0)
|
||||
-->
|
||||
<image
|
||||
v-if="src"
|
||||
class="v-icon v-icon--img"
|
||||
:src="src"
|
||||
:style="imgStyle"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text v-else class="v-icon" :style="iconStyle">{{ glyph }}</text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -16,18 +29,25 @@
|
||||
passed: '✓',
|
||||
'warning-o': '!',
|
||||
plus: '+',
|
||||
'add-o': '+',
|
||||
edit: '✎',
|
||||
arrow: '›',
|
||||
'eye-o': '◎',
|
||||
down: '↓',
|
||||
checked: '✓',
|
||||
'manager-o': '☺',
|
||||
'clock-o': '◷',
|
||||
'description': '📄',
|
||||
description: '📄',
|
||||
'photo-o': '▣',
|
||||
'video-o': '▶',
|
||||
'music-o': '♪',
|
||||
'bag-o': '▣',
|
||||
'records': '☰'
|
||||
records: '☰'
|
||||
}
|
||||
|
||||
/** 本地位图(小程序 <image> 真机可靠);颜色已烘焙在 PNG 内 */
|
||||
const PNG_ICONS = {
|
||||
approve: '/static/menu-icons/start-approval.png'
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -38,17 +58,27 @@
|
||||
color: { type: String, default: '' }
|
||||
},
|
||||
computed: {
|
||||
src() {
|
||||
return PNG_ICONS[this.name] || ''
|
||||
},
|
||||
glyph() {
|
||||
return GLYPHS[this.name] || '●'
|
||||
},
|
||||
dim() {
|
||||
if (!this.size) return ''
|
||||
const s = String(this.size)
|
||||
return /px|rpx|em|rem|%$/.test(s) ? s : `${s}px`
|
||||
},
|
||||
iconStyle() {
|
||||
const style = {}
|
||||
if (this.size) {
|
||||
const s = String(this.size)
|
||||
style.fontSize = /px|rpx|em|rem|%$/.test(s) ? s : `${s}px`
|
||||
}
|
||||
if (this.dim) style.fontSize = this.dim
|
||||
if (this.color) style.color = this.color
|
||||
return style
|
||||
},
|
||||
imgStyle() {
|
||||
// 默认 52rpx:对齐首页宫格;勿回退到 em
|
||||
const d = this.dim || '52rpx'
|
||||
return { width: d, height: d }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,4 +92,11 @@
|
||||
line-height: 1;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.v-icon--img {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -57,12 +57,12 @@
|
||||
}
|
||||
|
||||
.v-tag.is-primary {
|
||||
background: #e9f9f2;
|
||||
color: #087f57;
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.v-tag.is-success {
|
||||
background: #e9f9f2;
|
||||
background: #f0fdf4;
|
||||
color: #189b65;
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@
|
||||
.v-tag.is-plain.is-primary,
|
||||
.v-tag.is-plain.is-success {
|
||||
background: transparent;
|
||||
border-color: #0a9f6d;
|
||||
color: #0a9f6d;
|
||||
border-color: #16a34a;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.v-tag.is-plain.is-warning {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"path": "pages/oa/form-sink/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "表单水槽测试",
|
||||
"navigationBarBackgroundColor": "#0a9f6d",
|
||||
"navigationBarBackgroundColor": "#16a34a",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
@@ -44,7 +44,7 @@
|
||||
"path": "pages/oa/start/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发起审批",
|
||||
"navigationBarBackgroundColor": "#0a9f6d",
|
||||
"navigationBarBackgroundColor": "#16a34a",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
@@ -52,7 +52,7 @@
|
||||
"path": "pages/oa/start/webview",
|
||||
"style": {
|
||||
"navigationBarTitleText": "填写审批",
|
||||
"navigationBarBackgroundColor": "#0a9f6d",
|
||||
"navigationBarBackgroundColor": "#16a34a",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
@@ -88,7 +88,7 @@
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "中科兴皖",
|
||||
"navigationBarBackgroundColor": "#0a9f6d",
|
||||
"navigationBarBackgroundColor": "#16a34a",
|
||||
"backgroundColor": "#f5f8f7"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">按钮 Button</text></view>
|
||||
<view class="comp-row">
|
||||
<van-button type="primary" size="small" color="#0a9f6d" round>主按钮</van-button>
|
||||
<van-button type="primary" size="small" plain hairline color="#0a9f6d" round>描边</van-button>
|
||||
<van-button type="primary" size="small" color="#16a34a" round>主按钮</van-button>
|
||||
<van-button type="primary" size="small" plain hairline color="#16a34a" round>描边</van-button>
|
||||
<van-button size="small" plain hairline round>次要</van-button>
|
||||
<van-button type="danger" size="small" plain hairline round>危险</van-button>
|
||||
</view>
|
||||
<van-button
|
||||
type="primary"
|
||||
color="#0a9f6d"
|
||||
color="#16a34a"
|
||||
block
|
||||
round
|
||||
style="margin-top: 16rpx; height: 88rpx;"
|
||||
@@ -33,7 +33,7 @@
|
||||
<view class="panel-hd"><text class="panel-title">状态 Tag</text></view>
|
||||
<view class="comp-row">
|
||||
<van-tag type="warning" plain round>待处理</van-tag>
|
||||
<van-tag type="primary" color="#0a9f6d" round>进行中</van-tag>
|
||||
<van-tag type="primary" color="#16a34a" round>进行中</van-tag>
|
||||
<van-tag type="success" plain round>已完成</van-tag>
|
||||
<van-tag type="danger" plain round>异常</van-tag>
|
||||
</view>
|
||||
@@ -44,8 +44,8 @@
|
||||
<van-notice-bar
|
||||
left-icon="volume-o"
|
||||
text="移动端菜单在「菜单管理 → 移动端菜单」下配置,按角色授权后展示。"
|
||||
background="#e9f9f2"
|
||||
color="#087f57"
|
||||
background="#f0fdf4"
|
||||
color="#15803d"
|
||||
/>
|
||||
<view style="min-height: 360rpx; margin-top: 24rpx">
|
||||
<van-empty
|
||||
@@ -54,17 +54,17 @@
|
||||
/>
|
||||
</view>
|
||||
<view style="display: flex; justify-content: center; padding: 32rpx 0">
|
||||
<van-loading size="24px" color="#0a9f6d">加载中</van-loading>
|
||||
<van-loading size="24px" color="#16a34a">加载中</van-loading>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel-hd"><text class="panel-title">反馈</text></view>
|
||||
<view class="comp-row">
|
||||
<van-button size="small" plain hairline color="#0a9f6d" @click="toast('操作成功')">
|
||||
<van-button size="small" plain hairline color="#16a34a" @click="toast('操作成功')">
|
||||
Toast
|
||||
</van-button>
|
||||
<van-button size="small" plain hairline color="#0a9f6d" @click="showDialog">
|
||||
<van-button size="small" plain hairline color="#16a34a" @click="showDialog">
|
||||
Dialog
|
||||
</van-button>
|
||||
</view>
|
||||
@@ -99,7 +99,7 @@
|
||||
uni.showModal({
|
||||
title: '确认提交',
|
||||
content: '提交后将进入审批流程,是否继续?',
|
||||
confirmColor: '#0a9f6d'
|
||||
confirmColor: '#16a34a'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<view class="detail-section">
|
||||
<view class="sec-hd"><text class="sec-title">业务进度</text></view>
|
||||
<van-steps :active="detail.stepActive" active-color="#0a9f6d">
|
||||
<van-steps :active="detail.stepActive" active-color="#16a34a">
|
||||
<van-step v-for="s in detail.steps" :key="s">{{ s }}</van-step>
|
||||
</van-steps>
|
||||
</view>
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<view class="detail-footer">
|
||||
<van-button class="btn-flex" plain hairline type="default" @click="goBack">返回</van-button>
|
||||
<van-button class="btn-flex" type="primary" color="#0a9f6d" round @click="goEdit">
|
||||
<van-button class="btn-flex" type="primary" color="#16a34a" round @click="goEdit">
|
||||
编辑处理
|
||||
</van-button>
|
||||
</view>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
<van-cell title="加急">
|
||||
<template #right-icon>
|
||||
<van-switch v-model="form.urgent" size="20px" active-color="#0a9f6d" />
|
||||
<van-switch v-model="form.urgent" size="20px" active-color="#16a34a" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<view class="form-footer">
|
||||
<van-button class="btn-flex" plain hairline type="default" @click="goBack">取消</van-button>
|
||||
<van-button class="btn-flex" type="primary" color="#0a9f6d" round @click="onSave">
|
||||
<van-button class="btn-flex" type="primary" color="#16a34a" round @click="onSave">
|
||||
保存
|
||||
</van-button>
|
||||
</view>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
:circular="banners.length > 1"
|
||||
:indicator-dots="banners.length > 1"
|
||||
indicator-color="rgba(255,255,255,0.45)"
|
||||
indicator-active-color="#0a9f6d"
|
||||
indicator-active-color="#16a34a"
|
||||
:autoplay="false"
|
||||
>
|
||||
<swiper-item v-for="item in banners" :key="item.id">
|
||||
@@ -84,16 +84,18 @@
|
||||
{
|
||||
id: 'oa-start',
|
||||
title: '发起审批',
|
||||
icon: 'edit',
|
||||
icon: 'approve',
|
||||
path: '/pages/oa/start/index',
|
||||
tone: ''
|
||||
iconBg: '#ffe4e6',
|
||||
iconFg: '#e11d48'
|
||||
},
|
||||
{
|
||||
id: 'oa-form-sink',
|
||||
title: '表单水槽测试',
|
||||
icon: 'orders-o',
|
||||
path: '/pages/oa/form-sink/index',
|
||||
tone: ''
|
||||
iconBg: '#eef2f1',
|
||||
iconFg: '#3d524a'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@
|
||||
</view>
|
||||
|
||||
<view class="login-agree" @click="toggleAgree">
|
||||
<view class="login-agree__box" :class="{ 'is-on': agreed }" />
|
||||
<view class="login-agree__box" :class="{ 'is-on': agreed }">
|
||||
<text v-if="agreed" class="login-agree__check">✓</text>
|
||||
</view>
|
||||
<view class="login-agree__text">
|
||||
已阅读并同意
|
||||
<text class="login-agree__link" @click.stop="openProtocol('user')">《用户协议》</text>
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
return { src: '' }
|
||||
},
|
||||
onLoad() {
|
||||
this.src = buildFormSinkUrl()
|
||||
const src = buildFormSinkUrl()
|
||||
if (!/\btoken=/.test(src)) {
|
||||
uni.showToast({ title: '未登录,无法打开 OA 表单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
console.log('[oa-form-sink] src=', src)
|
||||
this.src = src
|
||||
},
|
||||
methods: {
|
||||
onMessage(e) {
|
||||
|
||||
@@ -30,7 +30,14 @@
|
||||
|
||||
<view class="hint">选择一个审批单即可发起</view>
|
||||
|
||||
<view v-if="loading" class="state">加载中…</view>
|
||||
<view v-if="loading" class="state state-loading">
|
||||
<view class="pulse-dots" aria-hidden="true">
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
</view>
|
||||
<text class="state-loading__text">加载审批模板…</text>
|
||||
</view>
|
||||
<view v-else-if="!displayGroups.length" class="state">
|
||||
暂无可发起的审批模板
|
||||
</view>
|
||||
@@ -49,11 +56,11 @@
|
||||
@click="onPick(row)"
|
||||
>
|
||||
<view class="oa-app-card__main">
|
||||
<OaAppIcon :icon-url="row.iconUrl" size="sm" />
|
||||
<OaAppIcon :icon-url="row.iconUrl" size="card" />
|
||||
<view class="oa-app-card__body">
|
||||
<text class="oa-app-card__name">{{ row.appName }}</text>
|
||||
<text class="oa-app-card__desc">{{
|
||||
row.description || '点击发起审批'
|
||||
<text v-if="row.description" class="oa-app-card__desc">{{
|
||||
row.description
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -71,6 +78,7 @@
|
||||
import OaAppIcon from '../../../components/OaAppIcon.vue'
|
||||
import { fetchOaGroupList, fetchOaTemplates } from '../../../utils/oa-api.js'
|
||||
import { getToken } from '../../../utils/auth.js'
|
||||
import { stashOaStartTitle } from '../../../utils/oa-h5.js'
|
||||
|
||||
export default {
|
||||
components: { OaAppIcon },
|
||||
@@ -152,6 +160,9 @@ export default {
|
||||
},
|
||||
onPick(row) {
|
||||
if (!row || !row.id) return
|
||||
// 与「表单水槽」一致:navigateTo 只带 ASCII 参数。
|
||||
// 流程名经 stash 传递,避免微信对含中文链接白屏(见上一步标题改动)。
|
||||
stashOaStartTitle(row.appName)
|
||||
uni.navigateTo({
|
||||
url: '/pages/oa/start/webview?appId=' + encodeURIComponent(row.id)
|
||||
})
|
||||
@@ -193,7 +204,7 @@ export default {
|
||||
}
|
||||
.tab.active {
|
||||
color: #fff;
|
||||
background: #0a9f6d;
|
||||
background: #16a34a;
|
||||
}
|
||||
.hint {
|
||||
padding: 16rpx 24rpx;
|
||||
@@ -206,6 +217,49 @@ export default {
|
||||
color: #6b7b74;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.state-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28rpx;
|
||||
padding: 120rpx 24rpx 80rpx;
|
||||
}
|
||||
.pulse-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.pulse-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #16a34a;
|
||||
animation: oa-start-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
.pulse-dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.pulse-dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.state-loading__text {
|
||||
font-size: 26rpx;
|
||||
color: #6b7b74;
|
||||
font-weight: 500;
|
||||
}
|
||||
@keyframes oa-start-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-12rpx);
|
||||
}
|
||||
}
|
||||
.body {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
@@ -259,7 +313,7 @@ export default {
|
||||
}
|
||||
.oa-app-card__main {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -268,15 +322,16 @@ export default {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
gap: 6rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
.oa-app-card__name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
.oa-app-card__desc {
|
||||
font-size: 22rpx;
|
||||
@@ -296,7 +351,7 @@ export default {
|
||||
line-height: 44rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #0a9f6d;
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<!-- 与表单水槽页保持同一结构:仅整页 web-view,避免额外节点干扰 -->
|
||||
<web-view v-if="src" :src="src" @message="onMessage" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildOaStartUrl } from '../../../utils/oa-h5.js'
|
||||
import {
|
||||
buildOaStartUrl,
|
||||
takeOaStartTitle
|
||||
} from '../../../utils/oa-h5.js'
|
||||
import { getToken } from '../../../utils/auth.js'
|
||||
import { fetchOaTemplateDetail } from '../../../utils/oa-api.js'
|
||||
|
||||
const CLOSE_ACTIONS = new Set([
|
||||
'oa-start-close',
|
||||
'oa-start-cancel',
|
||||
'oa-start-draft',
|
||||
'oa-start-submitted'
|
||||
])
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { src: '' }
|
||||
return { src: '', _closing: false }
|
||||
},
|
||||
onLoad(query) {
|
||||
if (!getToken()) {
|
||||
@@ -16,14 +28,77 @@ export default {
|
||||
setTimeout(() => uni.navigateBack(), 500)
|
||||
return
|
||||
}
|
||||
this.src = buildOaStartUrl({
|
||||
appId: query.appId,
|
||||
draftId: query.draftId
|
||||
})
|
||||
const appId = String((query && query.appId) || '')
|
||||
const draftId = String((query && query.draftId) || '')
|
||||
|
||||
// 标题:优先读跳转前缓存(无中文 query);否则按 appId 拉详情
|
||||
const cachedName = takeOaStartTitle()
|
||||
if (cachedName) {
|
||||
uni.setNavigationBarTitle({ title: cachedName })
|
||||
} else if (appId) {
|
||||
this.resolveTitle(appId)
|
||||
}
|
||||
|
||||
const src = buildOaStartUrl({ appId, draftId })
|
||||
if (!/\btoken=/.test(src)) {
|
||||
uni.showToast({ title: '未登录,无法打开 OA 表单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 对齐水槽:onLoad 内同步赋值,与 pages/oa/form-sink/index 一致
|
||||
console.log('[oa-start webview] src=', src)
|
||||
this.src = src
|
||||
|
||||
// H5:OA 页在 iframe 内,靠 postMessage 通知本页 uni.navigateBack
|
||||
// (微信小程序由 OA 内 wx.miniProgram.navigateBack,勿在此再退一层)
|
||||
// #ifdef H5
|
||||
this._onParentMessage = (e) => {
|
||||
const d = e && e.data
|
||||
if (!d || d.source !== 'zkxw-oa-h5') return
|
||||
this.navigateBackFromOa(d)
|
||||
}
|
||||
window.addEventListener('message', this._onParentMessage)
|
||||
// #endif
|
||||
},
|
||||
onUnload() {
|
||||
// #ifdef H5
|
||||
if (this._onParentMessage) {
|
||||
window.removeEventListener('message', this._onParentMessage)
|
||||
this._onParentMessage = null
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
onMessage() {
|
||||
/* 微信多在后退时触发;列表页 onShow 会刷新 */
|
||||
async resolveTitle(appId) {
|
||||
try {
|
||||
const detail = await fetchOaTemplateDetail(appId)
|
||||
const name = String(
|
||||
(detail && (detail.appName || detail.name)) || ''
|
||||
).trim()
|
||||
if (name) uni.setNavigationBarTitle({ title: name })
|
||||
} catch (_) {
|
||||
/* 保留 pages.json 兜底「填写审批」 */
|
||||
}
|
||||
},
|
||||
navigateBackFromOa(payload) {
|
||||
if (this._closing) return
|
||||
const action = String((payload && payload.action) || '')
|
||||
if (action && !CLOSE_ACTIONS.has(action)) return
|
||||
this._closing = true
|
||||
const msg = String((payload && payload.message) || '').trim()
|
||||
if (msg) {
|
||||
uni.showToast({ title: msg, icon: 'none', duration: 1200 })
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
this._closing = false
|
||||
}
|
||||
})
|
||||
}, msg ? 400 : 0)
|
||||
},
|
||||
onMessage(e) {
|
||||
/* 微信:多在后退/销毁时触发;列表页 onShow 可刷新。勿再 navigateBack */
|
||||
console.log('[oa-start webview] message', e && e.detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,46 @@ import assert from 'node:assert/strict'
|
||||
assert.strictEqual(legacy.kind, 'image')
|
||||
assert.strictEqual(legacy.src, '/static/oa-app-icons/doc-orange.svg')
|
||||
|
||||
const uploaded = resolveOaAppIcon('202608/11/c73acbdede3749b1b81cd7349c7c03fa.png')
|
||||
assert.strictEqual(uploaded.kind, 'image')
|
||||
assert.ok(
|
||||
String(uploaded.src).endsWith('/202608/11/c73acbdede3749b1b81cd7349c7c03fa.png'),
|
||||
'uploaded MinIO key should keep object path'
|
||||
)
|
||||
assert.ok(
|
||||
/^https?:\/\//i.test(uploaded.src),
|
||||
'uploaded MinIO key should resolve to absolute URL'
|
||||
)
|
||||
|
||||
const built = buildEpIconUrl('Wallet', '#fef3c7')
|
||||
assert.strictEqual(built, 'ep:Wallet|#fef3c7')
|
||||
const r = resolveOaAppIcon(built)
|
||||
assert.strictEqual(r.kind, 'ep')
|
||||
assert.strictEqual(r.fg, '#d97706')
|
||||
|
||||
assert.strictEqual(epIconDataUri('Calendar', '#2563eb').startsWith('data:image/svg+xml'), true)
|
||||
const withBtoa = epIconDataUri('Calendar', '#2563eb')
|
||||
assert.strictEqual(withBtoa.startsWith('data:image/svg+xml;base64,'), true)
|
||||
assert.ok(!withBtoa.includes('undefined'), 'data uri must not contain undefined')
|
||||
|
||||
// 模拟微信 appservice:无 btoa、无 Buffer
|
||||
const savedBtoa = globalThis.btoa
|
||||
const savedBuffer = globalThis.Buffer
|
||||
try {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
globalThis.btoa = undefined
|
||||
// eslint-disable-next-line no-global-assign
|
||||
globalThis.Buffer = undefined
|
||||
const noBtoa = epIconDataUri('Money', '#db2777')
|
||||
assert.strictEqual(noBtoa.startsWith('data:image/svg+xml;base64,'), true)
|
||||
assert.ok(noBtoa.length > 40, 'fallback base64 should produce payload')
|
||||
const office = epIconDataUri('OfficeBuilding', '#0891b2')
|
||||
assert.strictEqual(office.startsWith('data:image/svg+xml;base64,'), true)
|
||||
} finally {
|
||||
if (savedBtoa !== undefined) globalThis.btoa = savedBtoa
|
||||
else delete globalThis.btoa
|
||||
if (savedBuffer !== undefined) globalThis.Buffer = savedBuffer
|
||||
else delete globalThis.Buffer
|
||||
}
|
||||
|
||||
console.log('ok: oa-app-icons checks passed')
|
||||
})().catch((e) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#0a9f6d" 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="#16a34a" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 341 B After Width: | Height: | Size: 341 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
|
||||
<path
|
||||
d="M645.51 854.33c15.49-57.33 86.49-42 86.49-42s-43.54-44.53-23-112.67c11.22-37.2 39.58-67.99 79.81-76.43 20.41-4.28 56.37-4.23 69.42 0.55 3.66-18.04 7.33-36.14 10.85-54.39l23.84-117.26c7.41-37.35 14.81-74.7 22.78-112.05 6.27-32.55 13.19-65.1 19.53-97.81 4.72-24.74 10.09-49.48 14.81-74.71 2.6-14.16 5.86-28.31 8.46-42.07 0.61-2.04 0.96-4.14 1.06-6.26-1.06-24.66-1.06-41.01-16.93-48.83-16.43-7.89-32.38-7.89-40.85 0-21.65 10.98-43.45 21.48-65.1 32.55-54.04 26.85-107.41 53.7-161.45 80.48L468.71 285.97c-59.16 29.46-118.4 60.46-178.21 88.78-45.57 21.56-90 45.81-135.08 67.38-19.53 9.44-40.2 15.79-59.73 25.71-15.79 7.97-31.25 20.02-31.25 39.55 0 8.87 1.63 21.48 8.95 27.83 2.68 2.03 5.94 3.58 8.46 5.69 40.28 22.14 78.44 48.42 117.58 72.67l76.17 47.2h0.57c16.44 7.98 32.31 7.98 48.74-7.81l0.49-0.57c0.49-0.57 0.98-0.57 0.98-1.05 0.65-0.49 1.71-1.06 2.2-2.04 1.14-1.14 2.11-1.63 3.25-2.68 0.98-1.06 2.61-2.04 3.66-3.17 1.63-1.55 3.17-2.61 4.23-4.15 1.62-1.73 3.39-3.31 5.29-4.72 2.11-1.63 3.66-3.66 5.77-5.29 2.2-2.11 4.31-3.66 6.35-5.7 2.11-2.19 4.8-4.23 6.91-6.43 2.68-2.03 4.8-4.72 7.49-6.83 2.61-2.61 5.21-4.72 7.81-7.32 2.77-2.61 5.86-5.29 8.46-7.89 3.25-2.61 5.86-5.21 9.03-7.89 3.25-2.61 6.35-5.86 9.52-8.38 3.25-3.26 6.43-5.86 10.09-8.95 3.17-3.17 6.91-6.35 10.09-8.95 3.67-3.17 6.84-6.34 10.58-9.44 3.76-3.08 7.3-6.43 10.58-10.01 3.66-3.17 7.32-6.92 11.15-10.09 3.66-3.1 7.33-6.76 11.07-9.93 3.66-3.66 7.97-6.83 11.64-10.5 3.74-3.66 7.98-6.84 11.72-10.58 3.66-3.66 7.81-6.83 11.64-10.41 4.07-3.74 7.89-7.32 12.13-10.58 4.23-3.67 7.97-7.33 12.21-10.58 4.15-3.58 7.89-7.32 12.21-10.98 4.15-3.66 7.89-7.32 12.12-10.98 4.15-3.74 7.98-7.4 12.13-10.58 3.74-3.58 7.65-7.08 11.72-10.41 3.66-3.74 7.89-6.83 11.64-10.58 3.66-3.67 7.89-6.84 11.64-10.5 3.66-3.74 7.32-6.83 11.64-10.58 3.66-3.09 7.32-6.75 11.15-9.93 3.66-3.25 7.32-6.83 11.07-10.01 3.67-3.01 7.25-6.19 10.58-9.44 3.74-3.17 6.83-6.35 10.58-9.52 3.25-3.1 6.83-6.27 10.09-8.95 3.25-3.17 6.34-5.7 9.52-8.87 3.17-2.68 6.35-5.78 9.52-8.46 3.25-2.6 5.86-5.21 8.95-7.89 2.68-2.6 5.86-5.21 8.54-7.89 2.6-2.6 5.21-4.72 7.89-7.32 2.64-2.11 5.14-4.39 7.49-6.83 2.03-2.12 4.72-4.23 6.83-6.35 2.11-2.11 4.23-3.66 6.34-5.69 2.12-1.63 3.66-3.26 5.86-5.29l4.72-4.72c1.62-1.14 2.6-2.68 4.23-3.74 1.05-0.98 2.19-2.11 3.66-3.09 1.13-1.14 2.19-1.63 2.68-2.69l3.17-3.09h0.57c2.68 0.98 5.29 2.53 7.89 3.58 1.63 0.65 2.69 1.63 4.23 2.2 0.57 0.49 1.14 0.49 2.12 0.98 0.56 0 2.11 0.57 2.11 1.14v0.49c-0.57 0.49-1.06 0.49-1.06 0.98l-1.62 1.63-6.84 6.83a76.647 76.647 0 0 0-8.95 10.01c-2.12 2.12-4.31 4.72-6.35 6.84a22.64 22.64 0 0 0-3.25 3.66l-1.55 1.62c-0.57 0.49-1.05 1.47-1.62 1.47-9.44 10.09-19.04 20.59-28.48 30.59l-74.7 78.94c-28.56 29.86-56.64 59.89-85.28 89.83-26.93 28.4-53.95 56.8-80.4 85.2-20.67 21.56-40.69 43.13-61.36 64.69-8.95 9.44-18.06 18.88-27.02 28.88l-0.57 0.57c-7.89 7.81-7.89 16.28-7.89 24.17v177.81c0 16.28 7.89 31.98 24.33 40.36 16.36 7.97 32.31 0 40.69-7.89l62.98-62.5c10.66-10.58 20.75-21.07 30.76-31.57 1.13-1.06 2.11-2.11 3.25-2.11 1.46-0.57 3.58 0.57 4.72 1.62 22.7 14.9 97.27 71.69 97.27 71.69s3.59-31.79 6.31-41.84z"
|
||||
fill="#e11d48"
|
||||
/>
|
||||
<path
|
||||
d="M699.65 908.97H948.6c7.12 0 13.06-7.66 11.87-15.33l-4.75-28.07c-2.37-11.5-11.87-20.41-22.52-20.41h-76.72l-4.75-31.9c26.08-11.5 43.89-38.29 43.89-70.19 0-42.12-32.02-76.58-71.16-76.58-39.14 0-71.2 34.42-71.2 76.55 0 31.9 17.78 58.7 43.89 70.19l-3.56 31.9h-77.33c-10.68 0-21.34 8.94-22.52 20.41L689 893.61c-2.4 7.69 3.53 15.36 10.65 15.36z m248.89 19.12M944.08 960.22h-239.4c-9.34 0-16.92-7.58-16.92-16.92v-0.91c0-9.34 7.58-16.92 16.92-16.92h239.4c9.34 0 16.92 7.58 16.92 16.92v0.91c0 9.35-7.57 16.92-16.92 16.92z"
|
||||
fill="#e11d48"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -18,7 +18,7 @@ view, text {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(840rpx 440rpx at 100% 0%, rgba(214, 163, 67, 0.14), transparent 55%),
|
||||
radial-gradient(760rpx 400rpx at 0% 20%, rgba(10, 159, 109, 0.1), transparent 50%),
|
||||
radial-gradient(760rpx 400rpx at 0% 20%, rgba(22, 163, 74, 0.1), transparent 50%),
|
||||
linear-gradient(180deg, #e8f5ef 0%, #f4f7f6 40%, #f7f9f8 100%);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
}
|
||||
|
||||
.d-attach-upload:active {
|
||||
border-color: #0a9f6d;
|
||||
border-color: #16a34a;
|
||||
}
|
||||
|
||||
.d-attach-upload .up-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
|
||||
background: linear-gradient(145deg, #4ade80, #16a34a);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 44rpx;
|
||||
box-shadow: 0 12rpx 28rpx rgba(10, 159, 109, 0.28);
|
||||
box-shadow: 0 12rpx 28rpx rgba(22, 163, 74, 0.28);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
.d-attach-upload .up-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #087f57;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.d-attach-upload .up-sub {
|
||||
@@ -71,14 +71,14 @@
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6rpx;
|
||||
background: var(--file-accent, #0a9f6d);
|
||||
background: var(--file-accent, #16a34a);
|
||||
}
|
||||
|
||||
.d-attach-card.ft-pdf {
|
||||
--file-accent: #d14343;
|
||||
}
|
||||
.d-attach-card.ft-img {
|
||||
--file-accent: #0a9f6d;
|
||||
--file-accent: #16a34a;
|
||||
}
|
||||
.d-attach-card.ft-xls {
|
||||
--file-accent: #189b65;
|
||||
@@ -108,7 +108,7 @@
|
||||
background: linear-gradient(145deg, #f07171, #d14343);
|
||||
}
|
||||
.d-attach-card.ft-img .file-icon {
|
||||
background: linear-gradient(145deg, #3dca96, #0a9f6d);
|
||||
background: linear-gradient(145deg, #4ade80, #16a34a);
|
||||
}
|
||||
.d-attach-card.ft-xls .file-icon {
|
||||
background: linear-gradient(145deg, #4fbf8a, #189b65);
|
||||
@@ -169,8 +169,8 @@
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops .btn-preview {
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
color: var(--brand-green-dark, #087f57);
|
||||
background: var(--brand-green-soft, #f0fdf4);
|
||||
color: var(--brand-green-dark, #15803d);
|
||||
}
|
||||
|
||||
.d-attach-card .file-ops .btn-download {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
.confirm-dialog__link {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
color: var(--brand-green, #16a34a);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
}
|
||||
|
||||
.confirm-dialog__btn--confirm {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
color: var(--brand-green, #16a34a);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,12 +146,12 @@
|
||||
width: 8rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #12b37a, #087f57);
|
||||
background: linear-gradient(180deg, #22c55e, #15803d);
|
||||
}
|
||||
|
||||
.detail-section .sec-hd .sec-more {
|
||||
font-size: 24rpx;
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
color: var(--brand-green, #16a34a);
|
||||
}
|
||||
|
||||
.kv-list {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
bottom: -28rpx;
|
||||
width: 4rpx;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(180deg, rgba(10, 159, 109, 0.45), rgba(220, 235, 228, 0.9));
|
||||
background: linear-gradient(180deg, rgba(22, 163, 74, 0.45), rgba(220, 235, 228, 0.9));
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 28rpx rgba(8, 127, 87, 0.2);
|
||||
box-shadow: 0 12rpx 28rpx rgba(21, 128, 61, 0.2);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.flow-item.done .flow-dot {
|
||||
background: linear-gradient(145deg, #2ecf8f, #0a9f6d);
|
||||
background: linear-gradient(145deg, #4ade80, #16a34a);
|
||||
}
|
||||
.flow-item.warn .flow-dot {
|
||||
background: linear-gradient(145deg, #ecc06a, #d6a343);
|
||||
@@ -103,8 +103,8 @@
|
||||
}
|
||||
|
||||
.flow-item.done .flow-badge {
|
||||
background: rgba(10, 159, 109, 0.12);
|
||||
color: #087f57;
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
color: #15803d;
|
||||
}
|
||||
.flow-item.warn .flow-badge {
|
||||
background: rgba(214, 163, 67, 0.16);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
.home-banner__item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e9f9f2;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.home-banner__video,
|
||||
|
||||
@@ -98,13 +98,13 @@
|
||||
|
||||
.seg-tab.active {
|
||||
background: #fff;
|
||||
color: var(--brand-green-dark, #087f57);
|
||||
color: var(--brand-green-dark, #15803d);
|
||||
font-weight: 650;
|
||||
box-shadow: 0 8rpx 24rpx rgba(8, 127, 87, 0.14);
|
||||
box-shadow: 0 8rpx 24rpx rgba(21, 128, 61, 0.14);
|
||||
}
|
||||
|
||||
.seg-tab.active .count {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
color: var(--brand-green, #16a34a);
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
@@ -163,7 +163,7 @@
|
||||
color: #7a5610;
|
||||
}
|
||||
.biz-card.status-primary .type-icon {
|
||||
background: linear-gradient(145deg, #8fe0bf, #0a9f6d);
|
||||
background: linear-gradient(145deg, #86efac, #16a34a);
|
||||
}
|
||||
.biz-card.status-danger .type-icon {
|
||||
background: linear-gradient(145deg, #ffb4b4, #e85a5a);
|
||||
@@ -192,8 +192,8 @@
|
||||
color: #8f6718;
|
||||
}
|
||||
.biz-card.status-primary .type-chip {
|
||||
background: rgba(10, 159, 109, 0.12);
|
||||
color: #087f57;
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
color: #15803d;
|
||||
}
|
||||
.biz-card.status-danger .type-chip {
|
||||
background: rgba(209, 67, 67, 0.12);
|
||||
@@ -225,7 +225,7 @@
|
||||
box-shadow: 0 4rpx 16rpx rgba(214, 163, 67, 0.22);
|
||||
}
|
||||
.biz-card.status-primary .status-pill {
|
||||
background: #0a9f6d;
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
.biz-card.status-danger .status-pill {
|
||||
@@ -286,7 +286,7 @@
|
||||
color: #9a7424;
|
||||
}
|
||||
.biz-card.status-primary .side-metric .amt {
|
||||
color: #087f57;
|
||||
color: #15803d;
|
||||
}
|
||||
.biz-card.status-danger .side-metric .amt {
|
||||
color: #b13535;
|
||||
@@ -351,7 +351,7 @@
|
||||
color: #a67b20;
|
||||
}
|
||||
.biz-card.status-primary .footer .left-hint {
|
||||
color: #0a9f6d;
|
||||
color: #16a34a;
|
||||
}
|
||||
.biz-card.status-danger .footer .left-hint {
|
||||
color: #d14343;
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 32rpx;
|
||||
padding: 36rpx 32rpx 32rpx;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.login-field {
|
||||
@@ -55,7 +54,7 @@
|
||||
.login-field__box.is-focus {
|
||||
border-color: var(--brand-green);
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 6rpx rgba(10, 159, 109, 0.12);
|
||||
box-shadow: 0 0 0 6rpx rgba(22, 163, 74, 0.12);
|
||||
}
|
||||
|
||||
.login-field__box.is-error {
|
||||
@@ -112,10 +111,21 @@
|
||||
border: 3rpx solid var(--brand-green);
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.login-agree__box.is-on {
|
||||
background: var(--brand-green);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.login-agree__check {
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
color: var(--brand-green);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-agree__text {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
.menu-card:active {
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
background: var(--brand-green-soft, #f0fdf4);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
@@ -28,10 +28,21 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
color: var(--brand-green-dark, #087f57);
|
||||
background: #eef2f1;
|
||||
color: #3d524a;
|
||||
font-size: 48rpx;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(10, 159, 109, 0.08);
|
||||
box-shadow: inset 0 0 0 1rpx rgba(31, 42, 38, 0.06);
|
||||
}
|
||||
|
||||
.menu-icon .v-icon--img {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
}
|
||||
|
||||
.menu-icon.tone-approve {
|
||||
background: #ffe4e6;
|
||||
color: #e11d48;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(225, 29, 72, 0.12);
|
||||
}
|
||||
|
||||
.menu-icon.tone-yellow {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
padding: 24rpx 20rpx 20rpx;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 20rpx rgba(8, 127, 87, 0.06);
|
||||
box-shadow: 0 4rpx 20rpx rgba(21, 128, 61, 0.06);
|
||||
border: 1rpx solid rgba(220, 235, 228, 0.9);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
|
||||
.todo-item:active {
|
||||
background: var(--brand-green-soft, #e9f9f2);
|
||||
background: var(--brand-green-soft, #f0fdf4);
|
||||
border-color: #b7e5d0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@mixin hero-green {
|
||||
background: linear-gradient(155deg, #066647 0%, #0a9f6d 48%, #12b37a 100%);
|
||||
background: linear-gradient(155deg, #14532d 0%, #16a34a 48%, #22c55e 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 0 0 56rpx 56rpx;
|
||||
position: relative;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 唯一 Token 源 — 对齐三墙 / Demo,禁止 #10b981
|
||||
$brand-green: #0a9f6d;
|
||||
$brand-green-dark: #087f57;
|
||||
$brand-green-soft: #e9f9f2;
|
||||
// 唯一 Token 源 — 品牌绿(青葱),禁止 web-admin #10b981
|
||||
$brand-green: #16a34a;
|
||||
$brand-green-dark: #15803d;
|
||||
$brand-green-soft: #f0fdf4;
|
||||
$barley-yellow: #d6a343;
|
||||
$barley-yellow-soft: #fff7e8;
|
||||
$bg-page: #f5f8f7;
|
||||
@@ -31,5 +31,5 @@ $radius-lg: 40rpx;
|
||||
--success: #{$success};
|
||||
--radius-card: #{$radius-card};
|
||||
--radius-lg: #{$radius-lg};
|
||||
--shadow-soft: 0 8rpx 32rpx rgba(8, 127, 87, 0.08);
|
||||
--shadow-soft: 0 8rpx 32rpx rgba(21, 128, 61, 0.08);
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
/* Vant 4 CSS 变量 — 对齐三墙 Token,禁止 #10b981 */
|
||||
/* Vant 4 CSS 变量 — 对齐品牌绿 Token,禁止 #10b981 */
|
||||
page {
|
||||
--van-primary-color: #0a9f6d;
|
||||
--van-primary-color: #16a34a;
|
||||
--van-success-color: #189b65;
|
||||
--van-danger-color: #d14343;
|
||||
--van-warning-color: #cf8a19;
|
||||
|
||||
--van-button-primary-background: #0a9f6d;
|
||||
--van-button-primary-border-color: #0a9f6d;
|
||||
--van-button-primary-background: #16a34a;
|
||||
--van-button-primary-border-color: #16a34a;
|
||||
|
||||
--van-nav-bar-background: transparent;
|
||||
--van-nav-bar-title-text-color: #ffffff;
|
||||
--van-nav-bar-icon-color: #ffffff;
|
||||
|
||||
--van-tabs-bottom-bar-color: #0a9f6d;
|
||||
--van-tag-primary-color: #0a9f6d;
|
||||
--van-steps-active-color: #0a9f6d;
|
||||
--van-tabs-bottom-bar-color: #16a34a;
|
||||
--van-tag-primary-color: #16a34a;
|
||||
--van-steps-active-color: #16a34a;
|
||||
|
||||
--van-notice-bar-background: #e9f9f2;
|
||||
--van-notice-bar-text-color: #087f57;
|
||||
--van-notice-bar-background: #f0fdf4;
|
||||
--van-notice-bar-text-color: #15803d;
|
||||
}
|
||||
+27
-11
@@ -1,17 +1,33 @@
|
||||
/**
|
||||
* 后端 API 根路径(含 /api,不含末尾斜杠)。
|
||||
* 测试环境(微信体验版 / 真机):https://www.chengyantek.cn/api
|
||||
* 本地联调可改回:http://127.0.0.1:8000/api
|
||||
* true = 测试机域名(微信体验版 / 真机)
|
||||
* false = 本机联调(模拟器 / 同网手机)
|
||||
*
|
||||
* 微信公众平台须配置 request 合法域名:www.chengyantek.cn(需 HTTPS,且云安全组放行 443)
|
||||
* 微信公众平台须配置 request / 业务域名:www.chengyantek.cn(HTTPS,安全组放行 443)
|
||||
*/
|
||||
export const API_BASE_URL = 'http://127.0.0.1:8000/api'
|
||||
const USE_REMOTE_TEST = false
|
||||
|
||||
/** 本机联调 */
|
||||
const LOCAL_API_BASE_URL = 'http://127.0.0.1:8000/api'
|
||||
/** 本机 OA H5(Vite);真机同网时把 IP 改成电脑局域网地址 */
|
||||
const LOCAL_OA_H5_ORIGIN = 'http://127.0.0.1:5178/oa-h5'
|
||||
/** 与 web-admin `.env.development` 的 VITE_MINIO_BASE_URL 对齐 */
|
||||
const LOCAL_MINIO_BASE_URL = 'http://47.99.164.66:20004/zkxw'
|
||||
|
||||
/** 测试环境 */
|
||||
const REMOTE_API_BASE_URL = 'https://www.chengyantek.cn/api'
|
||||
const REMOTE_OA_H5_ORIGIN = 'https://www.chengyantek.cn/oa-h5'
|
||||
/** 与测试 Nginx `/minio/zkxw` 同域反代对齐 */
|
||||
const REMOTE_MINIO_BASE_URL = 'https://www.chengyantek.cn/minio/zkxw'
|
||||
|
||||
/** 后端 API 根路径(含 /api,不含末尾斜杠) */
|
||||
export const API_BASE_URL = USE_REMOTE_TEST ? REMOTE_API_BASE_URL : LOCAL_API_BASE_URL
|
||||
|
||||
export const REQUEST_TIMEOUT_MS = 15000
|
||||
|
||||
/**
|
||||
* OA 表单 H5 源(web-view)。
|
||||
* 本地:电脑局域网 IP + Vite 端口,须与手机同网;真机/体验版改为已配置业务域名。
|
||||
* 例:http://192.168.1.8:5178/oa-h5/
|
||||
*/
|
||||
export const OA_H5_ORIGIN = 'http://127.0.0.1:5178/oa-h5'
|
||||
/** OA 表单 H5 源(web-view) */
|
||||
export const OA_H5_ORIGIN = USE_REMOTE_TEST ? REMOTE_OA_H5_ORIGIN : LOCAL_OA_H5_ORIGIN
|
||||
|
||||
/** MinIO 公网/反代基址(不含末尾斜杠);附件相对 key 需拼此前缀 */
|
||||
export const MINIO_BASE_URL = USE_REMOTE_TEST
|
||||
? REMOTE_MINIO_BASE_URL
|
||||
: LOCAL_MINIO_BASE_URL
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 主页宫格图标配色:每项可自定义 iconBg / iconFg。
|
||||
* 未配置时按 icon 名回退到此处默认色对。
|
||||
*/
|
||||
export const MENU_ICON_COLOR_DEFAULTS = {
|
||||
approve: { iconBg: '#ffe4e6', iconFg: '#e11d48' },
|
||||
'orders-o': { iconBg: '#eef2f1', iconFg: '#3d524a' },
|
||||
'apps-o': { iconBg: '#eef2f1', iconFg: '#3d524a' }
|
||||
}
|
||||
|
||||
export function resolveMenuIconColors(menu) {
|
||||
const icon = String(menu?.icon || '').trim()
|
||||
const fallback = MENU_ICON_COLOR_DEFAULTS[icon] || {}
|
||||
const iconBg = String(menu?.iconBg || menu?.icon_bg || fallback.iconBg || '').trim()
|
||||
const iconFg = String(menu?.iconFg || menu?.icon_fg || fallback.iconFg || '').trim()
|
||||
return { iconBg, iconFg }
|
||||
}
|
||||
+15
-4
@@ -1,5 +1,6 @@
|
||||
import { get } from './request.js'
|
||||
import { getToken } from './auth.js'
|
||||
import { resolveMenuIconColors } from './menu-icon-colors.js'
|
||||
|
||||
function authHeader() {
|
||||
const token = getToken()
|
||||
@@ -11,12 +12,22 @@ export async function fetchMyMobileMenus() {
|
||||
const data = await get('/mini/menus', authHeader())
|
||||
const list = data?.list || data || []
|
||||
return list
|
||||
.map((m) => ({
|
||||
.map((m) => {
|
||||
const icon = m.icon || 'approve'
|
||||
const colors = resolveMenuIconColors({
|
||||
icon,
|
||||
iconBg: m.iconBg || m.icon_bg,
|
||||
iconFg: m.iconFg || m.icon_fg
|
||||
})
|
||||
return {
|
||||
id: m.id,
|
||||
title: m.title || m.menuName || m.menu_name,
|
||||
icon: m.icon || 'edit',
|
||||
icon,
|
||||
path: m.path,
|
||||
tone: ''
|
||||
}))
|
||||
iconBg: colors.iconBg,
|
||||
iconFg: colors.iconFg,
|
||||
tone: m.tone || ''
|
||||
}
|
||||
})
|
||||
.filter((m) => m.path)
|
||||
}
|
||||
@@ -13,3 +13,10 @@ export function fetchOaGroupList() {
|
||||
export function fetchOaTemplates() {
|
||||
return get('/web/admin/oa-approval/templates', authHeader())
|
||||
}
|
||||
|
||||
export function fetchOaTemplateDetail(appId) {
|
||||
return get(
|
||||
'/web/admin/oa-approval/templates/' + encodeURIComponent(appId),
|
||||
authHeader()
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** OA 审批应用图标:与 web-admin/src/utils/oa-app-icons.ts 色对/解析对齐 */
|
||||
|
||||
import { MINIO_BASE_URL } from './config.js'
|
||||
|
||||
export const OA_APP_ICON_COLOR_PAIRS = [
|
||||
{ bg: '#d1fae5', fg: '#059669' },
|
||||
{ bg: '#cffafe', fg: '#0891b2' },
|
||||
@@ -82,6 +84,7 @@ export function parseEpIconUrl(iconUrl) {
|
||||
/**
|
||||
* 旧版静态图标存 `/oa-app-icons/*.svg`(web-admin/public)。
|
||||
* 小程序/H5 需走 `static/oa-app-icons/`,否则 5173 下 404。
|
||||
* 上传图存 MinIO 相对 key(如 `202608/11/xxx.png`),需拼 MINIO_BASE_URL。
|
||||
*/
|
||||
export function resolveOaAppIconImageSrc(iconUrl) {
|
||||
const u = String(iconUrl || '').trim()
|
||||
@@ -91,10 +94,17 @@ export function resolveOaAppIconImageSrc(iconUrl) {
|
||||
return '/static' + u
|
||||
}
|
||||
if (u.startsWith('/static/')) return u
|
||||
if (u.startsWith('/')) return u
|
||||
// MinIO 相对 key:由调用方或 config 拼完整 URL;此处原样返回
|
||||
const minioBase = String(MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
// 同域反代路径:本机 H5 无 /minio 代理时,改写为绝对 MinIO 基址
|
||||
if (u.startsWith('/minio/zkxw/')) {
|
||||
if (minioBase && /^https?:\/\//i.test(minioBase)) {
|
||||
return `${minioBase}/${u.slice('/minio/zkxw/'.length)}`
|
||||
}
|
||||
return u
|
||||
}
|
||||
if (u.startsWith('/')) return u
|
||||
return minioBase ? `${minioBase}/${u.replace(/^\//, '')}` : u
|
||||
}
|
||||
|
||||
export function resolveOaAppIcon(iconUrl) {
|
||||
const u = String(iconUrl || '').trim()
|
||||
@@ -152,6 +162,29 @@ const EP_ICON_SVG_INNER = {
|
||||
'<path d="M12 21s7-4.5 7-11a7 7 0 1 0-14 0c0 6.5 7 11 7 11z"/><circle cx="12" cy="10" r="2"/>'
|
||||
}
|
||||
|
||||
const BASE64_CHARS =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
/** UTF-8 字符串 → base64;不依赖 btoa / Buffer(微信 appservice 两者皆无) */
|
||||
function utf8ToBase64(str) {
|
||||
const utf8 = unescape(encodeURIComponent(String(str || '')))
|
||||
if (typeof btoa === 'function') {
|
||||
return btoa(utf8)
|
||||
}
|
||||
let out = ''
|
||||
for (let i = 0; i < utf8.length; i += 3) {
|
||||
const a = utf8.charCodeAt(i)
|
||||
const b = i + 1 < utf8.length ? utf8.charCodeAt(i + 1) : 0
|
||||
const c = i + 2 < utf8.length ? utf8.charCodeAt(i + 2) : 0
|
||||
const n = (a << 16) | (b << 8) | c
|
||||
out += BASE64_CHARS[(n >> 18) & 63]
|
||||
out += BASE64_CHARS[(n >> 12) & 63]
|
||||
out += i + 1 < utf8.length ? BASE64_CHARS[(n >> 6) & 63] : '='
|
||||
out += i + 2 < utf8.length ? BASE64_CHARS[n & 63] : '='
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 生成小程序 <image> 可用的 SVG data-URI(base64,兼容微信) */
|
||||
export function epIconDataUri(icon, fg) {
|
||||
const name = ICON_SET.has(icon) ? icon : OA_APP_DEFAULT_EP_ICON
|
||||
@@ -160,9 +193,5 @@ export function epIconDataUri(icon, fg) {
|
||||
const svg =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${stroke}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`
|
||||
// WeChat <image> 对 charset=utf-8 + encodeURIComponent 偶发不兼容,统一 base64
|
||||
const b64 =
|
||||
typeof btoa === 'function'
|
||||
? btoa(unescape(encodeURIComponent(svg)))
|
||||
: Buffer.from(svg, 'utf8').toString('base64')
|
||||
return 'data:image/svg+xml;base64,' + b64
|
||||
return 'data:image/svg+xml;base64,' + utf8ToBase64(svg)
|
||||
}
|
||||
+51
-13
@@ -5,20 +5,58 @@ function trimSlash(s) {
|
||||
return String(s || '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
/** 水槽页 URL */
|
||||
export function buildFormSinkUrl() {
|
||||
const base = trimSlash(OA_H5_ORIGIN)
|
||||
const token = encodeURIComponent(getToken() || '')
|
||||
return `${base}/oa-form-sink?token=${token}`
|
||||
/** 小程序基础库未必有 URLSearchParams,用手写 query 更稳 */
|
||||
function buildQuery(params) {
|
||||
const parts = []
|
||||
Object.keys(params || {}).forEach((k) => {
|
||||
const v = params[k]
|
||||
if (v == null || v === '') return
|
||||
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v)))
|
||||
})
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
/** 正式发起页 URL(阶段二) */
|
||||
export function buildOaStartUrl({ appId, draftId }) {
|
||||
const base = trimSlash(OA_H5_ORIGIN)
|
||||
const q = new URLSearchParams()
|
||||
if (appId) q.set('appId', appId)
|
||||
if (draftId) q.set('draftId', draftId)
|
||||
function withQuery(path, params) {
|
||||
const base = trimSlash(OA_H5_ORIGIN) + path
|
||||
const qs = buildQuery(params)
|
||||
return qs ? base + '?' + qs : base
|
||||
}
|
||||
|
||||
/** 水槽页 URL(须带小程序登录 token,供 OA H5 调 /api) */
|
||||
export function buildFormSinkUrl() {
|
||||
const token = getToken()
|
||||
if (token) q.set('token', token)
|
||||
return `${base}/oa-start?${q.toString()}`
|
||||
return withQuery('/oa-form-sink', token ? { token } : {})
|
||||
}
|
||||
|
||||
/** 正式发起页 URL(与水槽同一套拼法,仅多 appId/draftId) */
|
||||
export function buildOaStartUrl({ appId, draftId } = {}) {
|
||||
const token = getToken()
|
||||
const params = {}
|
||||
if (appId) params.appId = appId
|
||||
if (draftId) params.draftId = draftId
|
||||
if (token) params.token = token
|
||||
return withQuery('/oa-start', params)
|
||||
}
|
||||
|
||||
const TITLE_KEY = 'oa_start_nav_title'
|
||||
|
||||
/** 跳转前缓存流程名(勿把中文放进 navigateTo / web-view URL,微信易白屏) */
|
||||
export function stashOaStartTitle(name) {
|
||||
const t = String(name || '').trim()
|
||||
try {
|
||||
if (t) uni.setStorageSync(TITLE_KEY, t)
|
||||
else uni.removeStorageSync(TITLE_KEY)
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function takeOaStartTitle() {
|
||||
try {
|
||||
const t = String(uni.getStorageSync(TITLE_KEY) || '').trim()
|
||||
uni.removeStorageSync(TITLE_KEY)
|
||||
return t
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
VITE_API_BASE=/api
|
||||
# 与 web-admin `.env.development` / mobile-h5 LOCAL_MINIO 对齐
|
||||
VITE_MINIO_BASE_URL=http://47.99.164.66:20004/zkxw
|
||||
@@ -9,8 +9,8 @@ import './styles/oa-form.css'
|
||||
import { registerOaComponents } from './oa/register-components'
|
||||
import { captureTokenFromUrl } from './utils/auth'
|
||||
|
||||
// 三墙主色(勿用 web-admin #10b981)
|
||||
document.documentElement.style.setProperty('--van-primary-color', '#0a9f6d')
|
||||
// 品牌绿主色(勿用 web-admin #10b981)
|
||||
document.documentElement.style.setProperty('--van-primary-color', '#16a34a')
|
||||
|
||||
// 须在路由/组件发请求前捕获小程序传入的 ?token=
|
||||
captureTokenFromUrl()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="oa-flow-preview">
|
||||
<div class="oa-flow-preview__title">审批流程</div>
|
||||
<van-loading v-if="loading" vertical class="oa-flow-preview__loading">加载中…</van-loading>
|
||||
<OaPulseLoading
|
||||
v-if="loading"
|
||||
class="oa-flow-preview__loading"
|
||||
text="加载中…"
|
||||
/>
|
||||
<div v-else-if="!resolved" class="oa-flow-preview__pending">
|
||||
{{ pendingMessage || '必填信息填写后,流程将自动显示' }}
|
||||
</div>
|
||||
@@ -18,11 +22,14 @@
|
||||
<span v-if="signBadge(s)" class="oa-flow-badge" :class="signBadge(s)">
|
||||
{{ signBadgeText(s) }}
|
||||
</span>
|
||||
<template v-if="s.meta">
|
||||
<span class="oa-flow-card__sep">·</span>
|
||||
<span class="oa-flow-card__meta">{{ s.meta }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="s.meta" class="oa-flow-card__meta">{{ s.meta }}</div>
|
||||
<div v-if="s.resolveError" class="oa-flow-card__err">{{ s.resolveError }}</div>
|
||||
|
||||
<template v-if="isInitiatorSelect(s)">
|
||||
<div class="oa-flow-card__select">
|
||||
<div class="oa-flow-card__people">
|
||||
<span
|
||||
v-for="p in displayPeople(s)"
|
||||
@@ -43,9 +50,16 @@
|
||||
{{ s.type === 'cc' ? '请选择抄送人' : '请选择审批人' }}
|
||||
</span>
|
||||
</div>
|
||||
<van-button size="small" plain type="primary" class="oa-flow-add" @click="openPicker(s)">
|
||||
添加
|
||||
</van-button>
|
||||
<button
|
||||
type="button"
|
||||
class="oa-flow-add-btn"
|
||||
title="添加"
|
||||
aria-label="添加"
|
||||
@click="openPicker(s)"
|
||||
>
|
||||
<van-icon name="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="(s.people || []).length" class="oa-flow-card__people">
|
||||
<span v-for="p in s.people" :key="p.id" class="oa-person-chip">
|
||||
@@ -69,7 +83,7 @@
|
||||
shape="round"
|
||||
label-align="left"
|
||||
input-align="left"
|
||||
placeholder="搜索姓名或工号"
|
||||
placeholder="搜索姓名或手机号"
|
||||
/>
|
||||
<div class="oa-flow-picker__list">
|
||||
<van-checkbox-group v-model="pickerIds">
|
||||
@@ -77,13 +91,15 @@
|
||||
v-for="u in filteredUsers"
|
||||
:key="u.id"
|
||||
clickable
|
||||
:title="u.nickName || u.id"
|
||||
:label="u.userNo || undefined"
|
||||
@click="togglePicker(u.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<van-checkbox class="oa-flow-picker__check" :name="u.id" @click.stop />
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="oa-flow-picker__name">{{ u.nickName || u.id }}</span>
|
||||
<span v-if="u.phoneNo" class="oa-flow-picker__phone">{{ u.phoneNo }}</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-checkbox-group>
|
||||
<van-empty v-if="!filteredUsers.length" description="暂无人员" />
|
||||
@@ -95,6 +111,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import OaPulseLoading from './OaPulseLoading.vue'
|
||||
|
||||
const props = defineProps({
|
||||
steps: { type: Array, default: () => [] },
|
||||
@@ -121,7 +138,7 @@ const filteredUsers = computed(() => {
|
||||
String(u.nickName || '')
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(u.userNo || '')
|
||||
String(u.phoneNo || '')
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(u.id || '')
|
||||
@@ -225,6 +242,10 @@ function confirmPicker() {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.oa-flow-preview__loading {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
.oa-flow-step {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -241,7 +262,7 @@ function confirmPicker() {
|
||||
height: 10px;
|
||||
margin-top: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #0a9f6d);
|
||||
background: var(--brand-green, #16a34a);
|
||||
}
|
||||
.oa-flow-dot.end {
|
||||
background: #94a3b8;
|
||||
@@ -274,30 +295,52 @@ function confirmPicker() {
|
||||
font-size: 11px;
|
||||
}
|
||||
.oa-flow-badge.and {
|
||||
color: #087f57;
|
||||
background: #e9f9f2;
|
||||
color: #15803d;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
.oa-flow-badge.or {
|
||||
color: #b45309;
|
||||
background: #fff7e8;
|
||||
}
|
||||
.oa-flow-card__meta,
|
||||
.oa-flow-card__placeholder {
|
||||
margin-top: 4px;
|
||||
.oa-flow-card__meta {
|
||||
color: var(--text-muted, #6b7b74);
|
||||
font-size: 12px;
|
||||
}
|
||||
.oa-flow-card__sep {
|
||||
color: #cbd5e1;
|
||||
font-size: 12px;
|
||||
}
|
||||
.oa-flow-card__placeholder {
|
||||
color: var(--text-muted, #6b7b74);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.oa-flow-card__err {
|
||||
margin-top: 4px;
|
||||
color: #d14343;
|
||||
font-size: 12px;
|
||||
}
|
||||
.oa-flow-card__select {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.oa-flow-card__people {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.oa-flow-card__select .oa-flow-card__people {
|
||||
flex: 1 1 auto;
|
||||
margin-top: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.oa-person-chip {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
@@ -315,7 +358,7 @@ function confirmPicker() {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #0a9f6d);
|
||||
background: var(--brand-green, #16a34a);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -328,8 +371,20 @@ function confirmPicker() {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.oa-flow-add {
|
||||
margin-top: 8px;
|
||||
.oa-flow-add-btn {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: var(--text-primary, #1f2a26);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
.oa-flow-picker {
|
||||
display: flex;
|
||||
@@ -345,7 +400,7 @@ function confirmPicker() {
|
||||
border-bottom: 1px solid var(--border, #dcebe4);
|
||||
}
|
||||
.oa-flow-picker__ok {
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
color: var(--brand-green, #16a34a);
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-flow-picker__list {
|
||||
@@ -361,4 +416,13 @@ function confirmPicker() {
|
||||
.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>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="oa-pulse-loading">
|
||||
<div class="oa-pulse-loading__dots" aria-hidden="true">
|
||||
<span class="oa-pulse-loading__dot" />
|
||||
<span class="oa-pulse-loading__dot" />
|
||||
<span class="oa-pulse-loading__dot" />
|
||||
</div>
|
||||
<div v-if="text" class="oa-pulse-loading__text">{{ text }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
text: { type: String, default: '' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-pulse-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.oa-pulse-loading__dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.oa-pulse-loading__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #16a34a);
|
||||
animation: oa-pulse-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
.oa-pulse-loading__dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.oa-pulse-loading__dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.oa-pulse-loading__text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
}
|
||||
@keyframes oa-pulse-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
:before-delete="onBeforeDelete"
|
||||
multiple
|
||||
/>
|
||||
<div class="oa-upload__tip">
|
||||
<div v-if="tipText" class="oa-upload__tip">
|
||||
{{ tipText }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,6 +20,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { uploadAttachment } from '../../api/oa.js'
|
||||
import { attachmentFullUrl } from '../../utils/attachment.js'
|
||||
import { getToken } from '../../utils/auth.js'
|
||||
|
||||
defineOptions({ name: 'upload' })
|
||||
@@ -42,8 +43,7 @@ const emit = defineEmits(['update:modelValue', 'change'])
|
||||
const captureAttr = computed(() => (props.capture === 'camera' ? 'camera' : undefined))
|
||||
const tipText = computed(() => {
|
||||
if (!getToken()) return '未携带登录 token,上传可能失败'
|
||||
if (props.capture === 'camera') return '将直接调起相机拍照上传'
|
||||
return '图片可选择拍照或从相册选取(微信内 web-view 支持)'
|
||||
return ''
|
||||
})
|
||||
|
||||
function toUrlList(raw) {
|
||||
@@ -51,15 +51,20 @@ function toUrlList(raw) {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.map((x) => {
|
||||
if (typeof x === 'string') return x
|
||||
if (x && typeof x === 'object') return String(x.url || x.fileUrl || '')
|
||||
if (typeof x === 'string') return attachmentFullUrl(x) || x
|
||||
if (x && typeof x === 'object') {
|
||||
return attachmentFullUrl(String(x.url || x.fileUrl || '')) || ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
if (typeof raw === 'string') return [raw]
|
||||
if (typeof raw === 'string') {
|
||||
const u = attachmentFullUrl(raw) || raw
|
||||
return u ? [u] : []
|
||||
}
|
||||
if (typeof raw === 'object') {
|
||||
const u = String(raw.url || raw.fileUrl || '')
|
||||
const u = attachmentFullUrl(String(raw.url || raw.fileUrl || '')) || ''
|
||||
return u ? [u] : []
|
||||
}
|
||||
return []
|
||||
@@ -109,7 +114,8 @@ async function onAfterRead(item) {
|
||||
const res = await uploadAttachment(f.file)
|
||||
const rawUrl = String(res?.fileUrl || res?.data?.fileUrl || '').trim()
|
||||
if (!rawUrl) throw new Error('未返回文件地址')
|
||||
f.url = rawUrl
|
||||
// 与 web-admin oa-form-create-upload 一致:表单存完整 URL,便于两端回显
|
||||
f.url = attachmentFullUrl(rawUrl) || rawUrl
|
||||
f.status = 'done'
|
||||
f.isString = true
|
||||
} catch (e) {
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
.oa-recv__ok,
|
||||
.oa-rel__ok {
|
||||
min-width: 48px;
|
||||
color: var(--brand-green, #0a9f6d);
|
||||
color: var(--brand-green, #16a34a);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
:root {
|
||||
--brand-green: #0a9f6d;
|
||||
--brand-green-dark: #087f57;
|
||||
--brand-green-soft: #e9f9f2;
|
||||
--brand-green: #16a34a;
|
||||
--brand-green-dark: #15803d;
|
||||
--brand-green-soft: #f0fdf4;
|
||||
--bg-page: #f5f8f7;
|
||||
--text-primary: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** 与 web-admin `attachmentFullUrl` 对齐:上传接口返回相对 key,表单需存可访问的完整 URL */
|
||||
|
||||
const minioBaseUrl = String(import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
export function attachmentFullUrl(url) {
|
||||
const u = String(url || '').trim()
|
||||
if (!u) return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="oa-start">
|
||||
<header class="oa-start__hd">{{ appMeta.appName || '发起审批' }}</header>
|
||||
<div v-if="detailLoading" class="oa-start__loading">
|
||||
<van-loading vertical>加载模板…</van-loading>
|
||||
<OaPulseLoading text="加载审批模板…" />
|
||||
</div>
|
||||
<div v-else class="oa-start__body">
|
||||
<form-create-mobile
|
||||
@@ -21,17 +20,26 @@
|
||||
/>
|
||||
</div>
|
||||
<footer class="oa-start__ft">
|
||||
<van-button class="oa-start__btn" @click="onCancel">取消</van-button>
|
||||
<van-button class="oa-start__btn" :loading="savingDraft" @click="saveDraft">
|
||||
暂存草稿
|
||||
<van-button class="oa-start__btn oa-start__btn--cancel" @click="onCancel">
|
||||
取消
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-start__btn"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
class="oa-start__btn oa-start__btn--draft"
|
||||
:class="{ 'is-loading': savingDraft }"
|
||||
:disabled="savingDraft"
|
||||
@click="saveDraft"
|
||||
>
|
||||
<span class="oa-start__btn-label">暂存草稿</span>
|
||||
<span v-if="savingDraft" class="oa-start__btn-spin" aria-hidden="true" />
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-start__btn oa-start__btn--submit"
|
||||
:class="{ 'is-loading': submitting }"
|
||||
:disabled="submitting"
|
||||
@click="submit"
|
||||
>
|
||||
提交
|
||||
<span class="oa-start__btn-label">提交</span>
|
||||
<span v-if="submitting" class="oa-start__btn-spin" aria-hidden="true" />
|
||||
</van-button>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -74,6 +82,7 @@ import {
|
||||
applyOwnerFieldDefaults
|
||||
} from '../oa/kit-runtime.js'
|
||||
import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
|
||||
import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const detailLoading = ref(true)
|
||||
@@ -105,17 +114,42 @@ let overtimeKitSyncing = false
|
||||
let overtimeKitPrevSnapshot = {}
|
||||
const overtimeRestMap = {}
|
||||
|
||||
/** 通知宿主(小程序 web-view / uni-app H5 iframe)关闭并返回发起列表 */
|
||||
function closeToMini(payload) {
|
||||
const data = payload || { action: 'oa-start-close' }
|
||||
try {
|
||||
// 微信小程序 web-view
|
||||
if (typeof wx !== 'undefined' && wx.miniProgram?.navigateBack) {
|
||||
wx.miniProgram.postMessage({ data: payload || { action: 'oa-start-close' } })
|
||||
wx.miniProgram.postMessage({ data })
|
||||
wx.miniProgram.navigateBack()
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
showToast((payload && payload.message) || '已完成(非小程序环境)')
|
||||
try {
|
||||
// uni-app App / 部分 web-view 容器
|
||||
const uniWv = typeof uni !== 'undefined' ? uni.webView : null
|
||||
if (uniWv?.postMessage) {
|
||||
uniWv.postMessage({ data })
|
||||
}
|
||||
if (uniWv?.navigateBack) {
|
||||
uniWv.navigateBack()
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
// mobile-h5 H5:web-view 为 iframe,由父页 listen 后 uni.navigateBack
|
||||
if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ source: 'zkxw-oa-h5', ...data }, '*')
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
showToast((data && data.message) || '已完成(非小程序环境)')
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
@@ -144,7 +178,8 @@ async function loadUsers() {
|
||||
.map((u) => ({
|
||||
id: String(u.id || ''),
|
||||
nickName: u.nickName || '',
|
||||
userNo: u.userNo || ''
|
||||
userNo: u.userNo || '',
|
||||
phoneNo: u.phoneNo || ''
|
||||
}))
|
||||
.filter((u) => u.id)
|
||||
} catch {
|
||||
@@ -422,6 +457,11 @@ async function loadByAppId(appId, { seedData = null, applyDefaults = true } = {}
|
||||
id: appId,
|
||||
appName: detail?.appName || detail?.name || '发起审批'
|
||||
}
|
||||
try {
|
||||
document.title = appMeta.value.appName || '填写审批'
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
|
||||
formRule.value = prepared.rule
|
||||
formOption.value = prepared.option
|
||||
@@ -598,12 +638,6 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
background: var(--bg-page, #f5f8f7);
|
||||
}
|
||||
.oa-start__hd {
|
||||
padding: 14px 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--brand-green, #0a9f6d), var(--brand-green-dark, #087f57));
|
||||
}
|
||||
.oa-start__loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -620,13 +654,77 @@ onUnmounted(() => {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--border, #dcebe4);
|
||||
box-shadow: 0 -4px 16px rgba(31, 42, 38, 0.04);
|
||||
}
|
||||
.oa-start__btn {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.oa-start__btn--cancel {
|
||||
flex: 0 0 64px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
color: var(--text-muted, #6b7b74) !important;
|
||||
}
|
||||
.oa-start__btn--cancel:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.oa-start__btn--draft {
|
||||
flex: 1;
|
||||
border: 1.5px solid var(--brand-green, #16a34a) !important;
|
||||
background: #fff !important;
|
||||
color: var(--brand-green-dark, #15803d) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-start__btn--submit {
|
||||
flex: 1.35;
|
||||
border: none !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--brand-green, #16a34a),
|
||||
var(--brand-green-dark, #15803d)
|
||||
) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.28);
|
||||
}
|
||||
/* G:文字半透明保留,转圈居中叠层(禁用时勿再整钮降透明) */
|
||||
.oa-start__btn.is-loading.van-button--disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.oa-start__btn.is-loading .oa-start__btn-label {
|
||||
opacity: 0.25;
|
||||
}
|
||||
.oa-start__btn-spin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
.oa-start__btn-spin::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: oa-start-btn-spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes oa-start-btn-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
# Graph Report - src (2026-08-11)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 967 files · ~631,785 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,782 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
|
||||
## Summary
|
||||
- 3417 nodes · 4865 edges · 71 communities detected
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1781272556.3597305,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1781272556.3601494,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1781272556.3606477,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1786415187.7799106,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1786413529.8487704,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1786416949.0462604,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1786416928.7957375,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1781272556.364766,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1782394569.8806834,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1783921717.4015942,
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
if (resolved.value.kind === 'ep') {
|
||||
return { background: resolved.value.bg, color: resolved.value.fg }
|
||||
}
|
||||
return {}
|
||||
return { background: '#f1f5f9' }
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1786436536718" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1712" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M645.51 854.33c15.49-57.33 86.49-42 86.49-42s-43.54-44.53-23-112.67c11.22-37.2 39.58-67.99 79.81-76.43 20.41-4.28 56.37-4.23 69.42 0.55 3.66-18.04 7.33-36.14 10.85-54.39l23.84-117.26c7.41-37.35 14.81-74.7 22.78-112.05 6.27-32.55 13.19-65.1 19.53-97.81 4.72-24.74 10.09-49.48 14.81-74.71 2.6-14.16 5.86-28.31 8.46-42.07 0.61-2.04 0.96-4.14 1.06-6.26-1.06-24.66-1.06-41.01-16.93-48.83-16.43-7.89-32.38-7.89-40.85 0-21.65 10.98-43.45 21.48-65.1 32.55-54.04 26.85-107.41 53.7-161.45 80.48L468.71 285.97c-59.16 29.46-118.4 60.46-178.21 88.78-45.57 21.56-90 45.81-135.08 67.38-19.53 9.44-40.2 15.79-59.73 25.71-15.79 7.97-31.25 20.02-31.25 39.55 0 8.87 1.63 21.48 8.95 27.83 2.68 2.03 5.94 3.58 8.46 5.69 40.28 22.14 78.44 48.42 117.58 72.67l76.17 47.2h0.57c16.44 7.98 32.31 7.98 48.74-7.81l0.49-0.57c0.49-0.57 0.98-0.57 0.98-1.05 0.65-0.49 1.71-1.06 2.2-2.04 1.14-1.14 2.11-1.63 3.25-2.68 0.98-1.06 2.61-2.04 3.66-3.17 1.63-1.55 3.17-2.61 4.23-4.15 1.62-1.73 3.39-3.31 5.29-4.72 2.11-1.63 3.66-3.66 5.77-5.29 2.2-2.11 4.31-3.66 6.35-5.7 2.11-2.19 4.8-4.23 6.91-6.43 2.68-2.03 4.8-4.72 7.49-6.83 2.61-2.61 5.21-4.72 7.81-7.32 2.77-2.61 5.86-5.29 8.46-7.89 3.25-2.61 5.86-5.21 9.03-7.89 3.25-2.61 6.35-5.86 9.52-8.38 3.25-3.26 6.43-5.86 10.09-8.95 3.17-3.17 6.91-6.35 10.09-8.95 3.67-3.17 6.84-6.34 10.58-9.44 3.76-3.08 7.3-6.43 10.58-10.01 3.66-3.17 7.32-6.92 11.15-10.09 3.66-3.1 7.33-6.76 11.07-9.93 3.66-3.66 7.97-6.83 11.64-10.5 3.74-3.66 7.98-6.84 11.72-10.58 3.66-3.66 7.81-6.83 11.64-10.41 4.07-3.74 7.89-7.32 12.13-10.58 4.23-3.67 7.97-7.33 12.21-10.58 4.15-3.58 7.89-7.32 12.21-10.98 4.15-3.66 7.89-7.32 12.12-10.98 4.15-3.74 7.98-7.4 12.13-10.58 3.74-3.58 7.65-7.08 11.72-10.41 3.66-3.74 7.89-6.83 11.64-10.58 3.66-3.67 7.89-6.84 11.64-10.5 3.66-3.74 7.32-6.83 11.64-10.58 3.66-3.09 7.32-6.75 11.15-9.93 3.66-3.25 7.32-6.83 11.07-10.01 3.67-3.01 7.25-6.19 10.58-9.44 3.74-3.17 6.83-6.35 10.58-9.52 3.25-3.1 6.83-6.27 10.09-8.95 3.25-3.17 6.34-5.7 9.52-8.87 3.17-2.68 6.35-5.78 9.52-8.46 3.25-2.6 5.86-5.21 8.95-7.89 2.68-2.6 5.86-5.21 8.54-7.89 2.6-2.6 5.21-4.72 7.89-7.32 2.64-2.11 5.14-4.39 7.49-6.83 2.03-2.12 4.72-4.23 6.83-6.35 2.11-2.11 4.23-3.66 6.34-5.69 2.12-1.63 3.66-3.26 5.86-5.29l4.72-4.72c1.62-1.14 2.6-2.68 4.23-3.74 1.05-0.98 2.19-2.11 3.66-3.09 1.13-1.14 2.19-1.63 2.68-2.69l3.17-3.09h0.57c2.68 0.98 5.29 2.53 7.89 3.58 1.63 0.65 2.69 1.63 4.23 2.2 0.57 0.49 1.14 0.49 2.12 0.98 0.56 0 2.11 0.57 2.11 1.14v0.49c-0.57 0.49-1.06 0.49-1.06 0.98l-1.62 1.63-6.84 6.83a76.647 76.647 0 0 0-8.95 10.01c-2.12 2.12-4.31 4.72-6.35 6.84a22.64 22.64 0 0 0-3.25 3.66l-1.55 1.62c-0.57 0.49-1.05 1.47-1.62 1.47-9.44 10.09-19.04 20.59-28.48 30.59l-74.7 78.94c-28.56 29.86-56.64 59.89-85.28 89.83-26.93 28.4-53.95 56.8-80.4 85.2-20.67 21.56-40.69 43.13-61.36 64.69-8.95 9.44-18.06 18.88-27.02 28.88l-0.57 0.57c-7.89 7.81-7.89 16.28-7.89 24.17v177.81c0 16.28 7.89 31.98 24.33 40.36 16.36 7.97 32.31 0 40.69-7.89l62.98-62.5c10.66-10.58 20.75-21.07 30.76-31.57 1.13-1.06 2.11-2.11 3.25-2.11 1.46-0.57 3.58 0.57 4.72 1.62 22.7 14.9 97.27 71.69 97.27 71.69s3.59-31.79 6.31-41.84z" p-id="1713" fill="#0a9f6d"></path><path d="M699.65 908.97H948.6c7.12 0 13.06-7.66 11.87-15.33l-4.75-28.07c-2.37-11.5-11.87-20.41-22.52-20.41h-76.72l-4.75-31.9c26.08-11.5 43.89-38.29 43.89-70.19 0-42.12-32.02-76.58-71.16-76.58-39.14 0-71.2 34.42-71.2 76.55 0 31.9 17.78 58.7 43.89 70.19l-3.56 31.9h-77.33c-10.68 0-21.34 8.94-22.52 20.41L689 893.61c-2.4 7.69 3.53 15.36 10.65 15.36z m248.89 19.12M944.08 960.22h-239.4c-9.34 0-16.92-7.58-16.92-16.92v-0.91c0-9.34 7.58-16.92 16.92-16.92h239.4c9.34 0 16.92 7.58 16.92 16.92v0.91c0 9.35-7.57 16.92-16.92 16.92z" p-id="1714" fill="#0a9f6d"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
+6
-2
@@ -13,6 +13,7 @@
|
||||
|------|------|
|
||||
| 入口 | `https://www.chengyantek.cn`(兼容 `http://101.35.245.122:10001`) |
|
||||
| Nginx 静态 | `/root/zkxw/web-admin/dist` |
|
||||
| OA H5 静态 | `/root/zkxw/static/oa-h5`(路径前缀 `/oa-h5/`) |
|
||||
| 后端 | `127.0.0.1:10002`(仅本机) |
|
||||
| 中间件 | 与本机 `backend/.env` 相同(`47.99.164.66`) |
|
||||
| MinIO | 同域 `/minio/zkxw` → `47.99.164.66:20004/zkxw` |
|
||||
@@ -21,12 +22,14 @@
|
||||
流量路径:
|
||||
|
||||
```text
|
||||
浏览器 → http://101.35.245.122:10001
|
||||
浏览器 → https://www.chengyantek.cn (或 http://IP:10001)
|
||||
├─ / → Nginx 静态(web-admin/dist)
|
||||
├─ /oa-h5/ → Nginx 静态(mobile-oa-h5 → /root/zkxw/static/oa-h5)
|
||||
├─ /api/* → 127.0.0.1:10002(本机后端)
|
||||
└─ MinIO 附件 → 浏览器直连 47.99.164.66:20004/zkxw
|
||||
└─ /minio/zkxw/ → 47.99.164.66:20004/zkxw/
|
||||
```
|
||||
|
||||
`mobile-h5` 配置示例:`OA_H5_ORIGIN = 'https://www.chengyantek.cn/oa-h5'`(微信 web-view 业务域名须含 `www.chengyantek.cn`)。
|
||||
云安全组 / 防火墙仅放行 **TCP 10001**;后端绑定 `127.0.0.1`,不对公网暴露 10002。
|
||||
|
||||
---
|
||||
@@ -127,6 +130,7 @@ Nginx 关键配置摘要:
|
||||
- `https://www.chengyantek.cn`(443 SSL)+ `http` 80/apex → 301 到 https://www
|
||||
- 兼容 `http://IP:10001`
|
||||
- `root /root/zkxw/web-admin/dist`;`/api`、`/health` → `127.0.0.1:10002`
|
||||
- `/oa-h5/` → `/root/zkxw/static/oa-h5`(`mobile-oa-h5`,Vite `base=/oa-h5/`)
|
||||
- `/minio/zkxw/` → `47.99.164.66:20004/zkxw/`(HTTPS 同域,避免混合内容)
|
||||
- 证书:`/root/zkxw/cert/www.chengyantek.cn.{pem,key}`(来源仓库旁证书包,私钥勿提交 git;`*.pem`/`*.key` 已在 `.gitignore`)
|
||||
|
||||
|
||||
@@ -111,6 +111,16 @@ server {
|
||||
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
|
||||
}
|
||||
|
||||
# mobile-oa-h5(Vite base=/oa-h5/;静态目录 /root/zkxw/static/oa-h5)
|
||||
# mobile-h5 配置 OA_H5_ORIGIN=https://www.chengyantek.cn/oa-h5
|
||||
location = /oa-h5 {
|
||||
return 301 /oa-h5/$is_args$args;
|
||||
}
|
||||
location ^~ /oa-h5/ {
|
||||
root /root/zkxw/static;
|
||||
try_files $uri $uri/ /oa-h5/index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
@@ -180,6 +190,14 @@ server {
|
||||
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
|
||||
}
|
||||
|
||||
location = /oa-h5 {
|
||||
return 301 /oa-h5/$is_args$args;
|
||||
}
|
||||
location ^~ /oa-h5/ {
|
||||
root /root/zkxw/static;
|
||||
try_files $uri $uri/ /oa-h5/index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user