提交
No files matched your search
@@ -155,6 +155,8 @@ async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
|
||||
path = (m.path or "").strip()
|
||||
if not path:
|
||||
continue
|
||||
if "form-sink" in path.lower() or "水槽" in str(m.menu_name or ""):
|
||||
continue
|
||||
if allowed_ids is not None and m.id not in allowed_ids:
|
||||
continue
|
||||
colors = _mobile_menu_icon_colors(path)
|
||||
|
||||
@@ -1043,6 +1043,7 @@ async def list_oa_tasks(
|
||||
readStatus: Optional[str] = Query(None, alias="readStatus"),
|
||||
appName: Optional[str] = Query(None, alias="appName"),
|
||||
initiatorName: Optional[str] = Query(None, alias="initiatorName"),
|
||||
keyword: Optional[str] = Query(None, alias="keyword"),
|
||||
applyTimeFrom: Optional[str] = Query(None, alias="applyTimeFrom"),
|
||||
applyTimeTo: Optional[str] = Query(None, alias="applyTimeTo"),
|
||||
instanceStatus: Optional[str] = Query(None, alias="instanceStatus"),
|
||||
@@ -1063,6 +1064,7 @@ async def list_oa_tasks(
|
||||
read_status=readStatus,
|
||||
app_name=appName,
|
||||
initiator_name=initiatorName,
|
||||
keyword=keyword,
|
||||
apply_time_from=applyTimeFrom,
|
||||
apply_time_to=applyTimeTo,
|
||||
instance_status=instanceStatus,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import os
|
||||
import uuid
|
||||
import base64
|
||||
import mimetypes
|
||||
from io import BytesIO
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote
|
||||
@@ -33,6 +34,25 @@ from app.core.config import settings
|
||||
# HEIC 格式的 MIME 类型
|
||||
HEIC_TYPES = {"image/heic", "image/heif"}
|
||||
|
||||
# 扩展名 → MIME(补齐部分系统 mimetypes 缺失,尤其 SVG)
|
||||
_EXTRA_CONTENT_TYPES = {
|
||||
".svg": "image/svg+xml",
|
||||
".svgz": "image/svg+xml",
|
||||
".webp": "image/webp",
|
||||
".heic": "image/heic",
|
||||
".heif": "image/heif",
|
||||
}
|
||||
|
||||
|
||||
def guess_content_type(filename: str) -> str:
|
||||
"""按文件名推断 Content-Type,供对象存储写入元数据(SVG 回显依赖正确 MIME)。"""
|
||||
name = (filename or "").strip()
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
if ext in _EXTRA_CONTENT_TYPES:
|
||||
return _EXTRA_CONTENT_TYPES[ext]
|
||||
guessed, _ = mimetypes.guess_type(name)
|
||||
return guessed or "application/octet-stream"
|
||||
|
||||
|
||||
def convert_heic_to_jpeg(content: bytes) -> bytes:
|
||||
"""将 HEIC/HEIF 格式转换为 JPEG 格式"""
|
||||
@@ -176,8 +196,9 @@ class QiniuStorage(BaseStorage):
|
||||
|
||||
key = self._generate_filename(filename)
|
||||
token = self.qiniu.upload_token(self.bucket, key, 3600)
|
||||
mime = guess_content_type(filename)
|
||||
|
||||
ret, info = put_data(token, key, file_content)
|
||||
ret, info = put_data(token, key, file_content, mime_type=mime)
|
||||
if ret is not None:
|
||||
return self.get_url(ret["key"])
|
||||
raise Exception(f"七牛云上传失败: {info}")
|
||||
@@ -231,7 +252,8 @@ class OSSStorage(BaseStorage):
|
||||
file_content, filename = process_image_content(file_content, filename)
|
||||
|
||||
key = self._generate_filename(filename)
|
||||
result = self.bucket.put_object(key, file_content)
|
||||
headers = {"Content-Type": guess_content_type(filename)}
|
||||
result = self.bucket.put_object(key, file_content, headers=headers)
|
||||
if result.status == 200:
|
||||
return self.get_url(key)
|
||||
raise Exception(f"OSS上传失败: {result.status}")
|
||||
@@ -294,11 +316,13 @@ class MinIOStorage(BaseStorage):
|
||||
if folder:
|
||||
key = f"{folder.strip('/')}/{key}"
|
||||
data = BytesIO(file_content)
|
||||
# 必须写入正确 MIME:SVG 若为 octet-stream,浏览器 <img> 会拒绝回显
|
||||
self.client.put_object(
|
||||
bucket_name=self.bucket_name,
|
||||
object_name=key,
|
||||
data=data,
|
||||
length=len(file_content),
|
||||
content_type=guess_content_type(filename),
|
||||
)
|
||||
return self.get_url(key)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
||||
from app.core.utils import generate_uuid
|
||||
from app.crud import oa_approval as oa_crud
|
||||
from app.models.models import OaApp, OaAppGroup, OaAppVersion, OaInstanceDraft
|
||||
from app.services.oa_related_approval import flatten_form_fields_for_summary
|
||||
|
||||
|
||||
def _loads(raw: Any, default: Any) -> Any:
|
||||
@@ -187,15 +188,21 @@ def list_drafts(
|
||||
)
|
||||
items: List[Dict[str, Any]] = []
|
||||
for d, app, ver, group in rows:
|
||||
items.append(
|
||||
row_to_dict(
|
||||
d,
|
||||
app_name=(app.app_name if app else "") or "",
|
||||
version_no=ver.version_no if ver else None,
|
||||
group_id=(app.group_id if app else "") or "",
|
||||
group_name=(group.group_name if group else "") or "",
|
||||
)
|
||||
item = row_to_dict(
|
||||
d,
|
||||
app_name=(app.app_name if app else "") or "",
|
||||
version_no=ver.version_no if ver else None,
|
||||
group_id=(app.group_id if app else "") or "",
|
||||
group_name=(group.group_name if group else "") or "",
|
||||
)
|
||||
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
|
||||
form_data = _loads(d.form_data_json, {}) or {}
|
||||
preview = [
|
||||
{"label": f.get("label") or "", "value": f.get("value") or ""}
|
||||
for f in flatten_form_fields_for_summary(schema, form_data)[:3]
|
||||
]
|
||||
item["previewFields"] = preview
|
||||
items.append(item)
|
||||
return items, int(total)
|
||||
|
||||
|
||||
|
||||
@@ -238,16 +238,30 @@ def _fmt_dt(dt: Any) -> str:
|
||||
return str(dt)
|
||||
|
||||
|
||||
def _terminal_item(status: str, title: str, inst: Any) -> Dict[str, Any]:
|
||||
def _terminal_item(
|
||||
status: str,
|
||||
title: str,
|
||||
inst: Any,
|
||||
name_map: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
name_map = name_map or {}
|
||||
uid = ""
|
||||
uname = ""
|
||||
comment = ""
|
||||
# 撤销终态:归因发起人,勿让任务节点承担「已撤销」语义
|
||||
if status == "cancelled":
|
||||
uid = (getattr(inst, "initiator_user_id", None) or "").strip()
|
||||
uname = name_map.get(uid, "") or uid
|
||||
comment = "发起人撤销"
|
||||
return {
|
||||
"kind": "terminal",
|
||||
"roundNo": getattr(inst, "current_round", None) or 1,
|
||||
"title": title,
|
||||
"status": status,
|
||||
"assigneeUserId": "",
|
||||
"assigneeUserName": "",
|
||||
"assigneeUserId": uid,
|
||||
"assigneeUserName": uname,
|
||||
"timestamp": _fmt_dt(getattr(inst, "update_time", None)),
|
||||
"commentText": "",
|
||||
"commentText": comment,
|
||||
"nodeKey": "",
|
||||
"taskType": "",
|
||||
}
|
||||
@@ -682,6 +696,9 @@ def build_progress_items(
|
||||
for p in ts_src:
|
||||
if p.get("timestamp") and (not timestamp or p["timestamp"] > timestamp):
|
||||
timestamp = p["timestamp"]
|
||||
# 整节点仅系统清待办(cancelled)时勿带时间,避免像审批人操作过
|
||||
if node_status == "cancelled":
|
||||
timestamp = ""
|
||||
comments = [
|
||||
f"{p['nickName']}:{p['commentText']}"
|
||||
if len(people) > 1 and p.get("nickName")
|
||||
@@ -751,11 +768,11 @@ def build_progress_items(
|
||||
)
|
||||
|
||||
if inst.status == "completed":
|
||||
items.append(_terminal_item("done", "流程结束", inst))
|
||||
items.append(_terminal_item("done", "流程结束", inst, name_map))
|
||||
elif inst.status == "rejected":
|
||||
items.append(_terminal_item("rejected", "流程结束(已拒绝)", inst))
|
||||
items.append(_terminal_item("rejected", "流程结束(已拒绝)", inst, name_map))
|
||||
elif inst.status == "cancelled":
|
||||
items.append(_terminal_item("cancelled", "已撤销", inst))
|
||||
items.append(_terminal_item("cancelled", "已撤销", inst, name_map))
|
||||
# returned:打回结果只落在审批任务节点上,不再追加独立终态「已打回」行
|
||||
return items
|
||||
|
||||
@@ -913,6 +930,8 @@ def _enrich_list_rows(
|
||||
) -> List[Dict[str, Any]]:
|
||||
if not rows:
|
||||
return []
|
||||
from app.services.oa_related_approval import flatten_form_fields_for_summary
|
||||
|
||||
inst_by_id = {i.id: i for i in instances}
|
||||
app_ids = list({i.app_id for i in instances if i.app_id})
|
||||
apps = (
|
||||
@@ -928,6 +947,16 @@ def _enrich_list_rows(
|
||||
)
|
||||
app_name_by_id = {a.id: (a.app_name or "") for a in apps}
|
||||
|
||||
ver_ids = list({i.app_version_id for i in instances if i.app_version_id})
|
||||
ver_by_id: Dict[str, OaAppVersion] = {}
|
||||
if ver_ids:
|
||||
for v in (
|
||||
db.query(OaAppVersion)
|
||||
.filter(OaAppVersion.id.in_(ver_ids), OaAppVersion.del_flag == 0)
|
||||
.all()
|
||||
):
|
||||
ver_by_id[v.id] = v
|
||||
|
||||
inst_ids = list(inst_by_id.keys())
|
||||
pending_tasks = (
|
||||
db.query(OaTask)
|
||||
@@ -970,8 +999,16 @@ def _enrich_list_rows(
|
||||
pending_approval_tasks=pending_by_inst.get(inst.id, []),
|
||||
name_map=name_map,
|
||||
)
|
||||
ver = ver_by_id.get(inst.app_version_id or "")
|
||||
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
|
||||
form_data = _loads(inst.form_data_json, {}) or {}
|
||||
preview = [
|
||||
{"label": f.get("label") or "", "value": f.get("value") or ""}
|
||||
for f in flatten_form_fields_for_summary(schema, form_data)[:3]
|
||||
]
|
||||
merged = dict(row)
|
||||
merged.update(summary)
|
||||
merged["previewFields"] = preview
|
||||
out.append(merged)
|
||||
return out
|
||||
|
||||
@@ -2121,6 +2158,7 @@ def _apply_instance_list_filters(
|
||||
*,
|
||||
app_name: Optional[str] = None,
|
||||
initiator_name: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
apply_time_from: Optional[str] = None,
|
||||
apply_time_to: Optional[str] = None,
|
||||
instance_status: Optional[str] = None,
|
||||
@@ -2128,26 +2166,49 @@ def _apply_instance_list_filters(
|
||||
joined_initiator: bool = False,
|
||||
):
|
||||
"""在已包含 OaInstance 的 query 上叠加列表筛选。返回 (query, joined_app, joined_initiator)。"""
|
||||
kw = (app_name or "").strip()
|
||||
if kw:
|
||||
combined = (keyword or "").strip()
|
||||
if combined:
|
||||
if not joined_app:
|
||||
q = q.outerjoin(
|
||||
OaApp,
|
||||
(OaApp.id == OaInstance.app_id) & (OaApp.del_flag == 0),
|
||||
)
|
||||
joined_app = True
|
||||
like = f"%{kw}%"
|
||||
q = q.filter(or_(OaApp.app_name.like(like), OaInstance.title.like(like)))
|
||||
|
||||
iname = (initiator_name or "").strip()
|
||||
if iname:
|
||||
if not joined_initiator:
|
||||
q = q.outerjoin(
|
||||
SysUser,
|
||||
(SysUser.id == OaInstance.initiator_user_id) & (SysUser.del_flag == 0),
|
||||
)
|
||||
joined_initiator = True
|
||||
q = q.filter(SysUser.nick_name.like(f"%{iname}%"))
|
||||
like = f"%{combined}%"
|
||||
q = q.filter(
|
||||
or_(
|
||||
OaApp.app_name.like(like),
|
||||
OaInstance.title.like(like),
|
||||
SysUser.nick_name.like(like),
|
||||
)
|
||||
)
|
||||
else:
|
||||
kw = (app_name or "").strip()
|
||||
if kw:
|
||||
if not joined_app:
|
||||
q = q.outerjoin(
|
||||
OaApp,
|
||||
(OaApp.id == OaInstance.app_id) & (OaApp.del_flag == 0),
|
||||
)
|
||||
joined_app = True
|
||||
like = f"%{kw}%"
|
||||
q = q.filter(or_(OaApp.app_name.like(like), OaInstance.title.like(like)))
|
||||
|
||||
iname = (initiator_name or "").strip()
|
||||
if iname:
|
||||
if not joined_initiator:
|
||||
q = q.outerjoin(
|
||||
SysUser,
|
||||
(SysUser.id == OaInstance.initiator_user_id) & (SysUser.del_flag == 0),
|
||||
)
|
||||
joined_initiator = True
|
||||
q = q.filter(SysUser.nick_name.like(f"%{iname}%"))
|
||||
|
||||
t0 = _parse_day_start(apply_time_from)
|
||||
if t0 is not None:
|
||||
@@ -2172,6 +2233,7 @@ def list_tasks(
|
||||
read_status: Optional[str] = None,
|
||||
app_name: Optional[str] = None,
|
||||
initiator_name: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
apply_time_from: Optional[str] = None,
|
||||
apply_time_to: Optional[str] = None,
|
||||
instance_status: Optional[str] = None,
|
||||
@@ -2193,6 +2255,7 @@ def list_tasks(
|
||||
q,
|
||||
app_name=app_name,
|
||||
initiator_name=initiator_name,
|
||||
keyword=keyword,
|
||||
apply_time_from=apply_time_from,
|
||||
apply_time_to=apply_time_to,
|
||||
instance_status=instance_status,
|
||||
@@ -2244,6 +2307,7 @@ def list_tasks(
|
||||
|
||||
need_inst_join = any(
|
||||
[
|
||||
(keyword or "").strip(),
|
||||
(app_name or "").strip(),
|
||||
(initiator_name or "").strip(),
|
||||
(apply_time_from or "").strip(),
|
||||
@@ -2262,6 +2326,7 @@ def list_tasks(
|
||||
q,
|
||||
app_name=app_name,
|
||||
initiator_name=initiator_name,
|
||||
keyword=keyword,
|
||||
apply_time_from=apply_time_from,
|
||||
apply_time_to=apply_time_to,
|
||||
instance_status=instance_status,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Graph Report - app (2026-08-12)
|
||||
# Graph Report - app (2026-08-13)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 250 files · ~303,290 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,609 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
|
||||
## Summary
|
||||
- 5238 nodes · 28666 edges · 43 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 19610 edges (avg confidence: 0.6)
|
||||
- 5240 nodes · 28677 edges · 46 communities detected
|
||||
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 19614 edges (avg confidence: 0.6)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
@@ -46,12 +46,15 @@
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 40|Community 40]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
- [[_COMMUNITY_Community 46|Community 46]]
|
||||
- [[_COMMUNITY_Community 47|Community 47]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `success_response()` - 736 edges
|
||||
@@ -70,90 +73,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_service.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 (1001): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+993 more)
|
||||
Nodes (1032): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+1024 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.01
|
||||
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)
|
||||
Nodes (746): _ensure_customer_for_mother(), api_save_application(), set_current_process_route(), generate_uuid(), allocate_next_biz_no(), _format_biz_no(), _prefix_for(), 业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号 (+738 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.01
|
||||
Nodes (584): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+576 more)
|
||||
Nodes (599): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+591 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (410): 环境检测 — 检测项维护 + 登记管理 API, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), 小程序菜单:当前用户在「移动端菜单」树下有权限的入口。, 宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。, 按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。, ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody (+402 more)
|
||||
Nodes (476): 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), Base, 按输注计划 ID 批量索引缴费记录(列表页展示用,不做关联 enrich), 输注计划创建时自动调用,生成 dfj 缴费记录, 关联查询申请单、患者、输注计划信息,附加到 dict, 将 BizMaterialUsage 行转换为字典(用于我的领用记录)。, 当前登录人的手动出库领用记录:领用人字段存昵称或用户 ID(与生产/质检一致)。, 获取当前用户的物料领用记录(手动出库记录) 筛选条件: - tenant_no = 当前用户租户 - source_type = 'm (+468 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.06
|
||||
Nodes (328): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+320 more)
|
||||
Cohesion: 0.01
|
||||
Nodes (320): _normalize_date_bound(), _persist_protocol_sub_resources(), batch_delete_research_donations(), change_donation_status(), convert_from_storage(), convert_to_storage(), create_research_donation(), delete_research_donation() (+312 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
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)
|
||||
Cohesion: 0.03
|
||||
Nodes (238): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _copied_app_name(), copy_app(), create_app(), create_group(), _create_version() (+230 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.02
|
||||
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)
|
||||
Nodes (244): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), batch_review(), _dec() (+236 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.02
|
||||
Nodes (234): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+226 more)
|
||||
Nodes (146): get_designer_data(), _error_json(), lifespan(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware (+138 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.02
|
||||
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)
|
||||
Cohesion: 0.08
|
||||
Nodes (185): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+177 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
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)
|
||||
Cohesion: 0.02
|
||||
Nodes (163): get_next_cell_product_code(), build_void_type_label_map(), format_protocol_status_label(), resolve_void_type_label(), _ancestor_link_from_cell_product_input(), _ancestor_sample_chain_link(), _apply_cell_product_status_change(), _build_ancestor_chain() (+155 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.03
|
||||
Nodes (110): 小程序鉴权:微信登录 / 绑定 openid。, 微信授权登录:须授权手机号;openid 已绑定也须与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id(), wechat_bind(), wechat_login(), _collect_descendants(), _current_user() (+102 more)
|
||||
Nodes (117): 小程序鉴权:微信登录 / 绑定 openid。, 微信授权登录:须授权手机号;openid 已绑定也须与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, _resolve_user_id(), wechat_bind(), wechat_login(), _collect_descendants(), _current_user() (+109 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.04
|
||||
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)
|
||||
Cohesion: 0.05
|
||||
Nodes (109): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, OccupancyCreateBody, 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+101 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (113): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slot_stats(), OccupancyCreateBody (+105 more)
|
||||
Cohesion: 0.38
|
||||
Nodes (78): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+70 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
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)
|
||||
Cohesion: 0.07
|
||||
Nodes (54): create_leave(), get_leave(), list_leaves(), _row_to_dict(), soft_delete_leave(), _dec(), _fmt_dt(), list_stock_logs() (+46 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.45
|
||||
Nodes (71): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+63 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (53): batch_stock_in(), batch_stock_out(), code_exists(), create_material(), _dec(), _filter_material_type(), _fmt_dt(), get_material() (+45 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"
|
||||
Cohesion: 0.13
|
||||
Nodes (20): 微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import Wech, 发送模板消息 Args: openid: 用户openid templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification() (+12 more)
|
||||
|
||||
### 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)
|
||||
Nodes (22): save_patient(), batch_create_checkups(), create_checkup(), delete_actual_item(), delete_reserve_item(), ensure_checkup_for_patient(), get_checkup_by_application(), get_checkup_by_id() (+14 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.17
|
||||
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)
|
||||
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 19 - "Community 19"
|
||||
Cohesion: 0.21
|
||||
@@ -164,132 +167,144 @@ 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.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)
|
||||
Cohesion: 0.25
|
||||
Nodes (18): _amount_for_row(), _batch_map(), _display_remark(), _fmt_date_val(), list_stock_check_requisitions(), list_usages(), _list_usages_query(), mark_requisition_fully_used() (+10 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
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.21
|
||||
Nodes (16): _field_label(), _is_non_empty_str(), _is_required(), _is_valid_receive_account_item(), normalize_account_fields(), _pick(), _pick_order_num(), _pick_str() (+8 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.22
|
||||
Nodes (15): apply_trip_kit_days(), calc_trip_leg_days(), _day_index(), _end_offset(), _extract_rules(), is_half_day_multiple(), 出差套件:半日制行程时长(与前端 oa-trip-days.ts 对齐), 遍历 tripKit:行程未手改则覆盖 duration;出差天数未手改则覆盖为各行程时长之和。 (+7 more)
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.26
|
||||
Nodes (14): apply_leave_kit_hours(), calc_leave_hours(), _extract_rules(), _kit_child_fields(), _overlap_minutes_on_day(), _parse_dt(), _parse_hm(), 请假套件:双段工时重叠小时(与前端 oa-leave-hours.ts 对齐) (+6 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.24
|
||||
Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 试剂变动记录 CRUD(biz_reagent_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减试剂库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.24
|
||||
Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 耗材变动记录 CRUD(biz_consumable_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减耗材库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.27
|
||||
Nodes (11): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。 (+3 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.31
|
||||
Nodes (10): create_account(), _filtered_query(), get_account(), list_accounts(), list_options(), OA 收款账户 CRUD(oa_receive_account), row_to_dict(), soft_delete_account() (+2 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.31
|
||||
Nodes (10): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。, _row_to_dict() (+2 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.33
|
||||
Nodes (8): _build_tree(), _cascade_soft_delete(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu(), update_menu()
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.29
|
||||
Nodes (2): BaseModel, SoftDeleteModel
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 0.4
|
||||
Nodes (4): list_ethnics_for_select(), list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict()
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 0.4
|
||||
Nodes (3): ApiRequestLogCRUD, delete(), get_multi_with_filters()
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.5
|
||||
Nodes (3): BaseSettings, Config, Settings
|
||||
|
||||
### Community 31 - "Community 31"
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.67
|
||||
Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.67
|
||||
Nodes (1): 转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 质检物料金额查看授权:专用角色与菜单权限常量。
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送订单状态通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 40 - "Community 40"
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送支付成功通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送配送通知 Args: openid: 配送员openid task
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送配送时效预警 Args: openid: 配送员openid ta
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
### Community 46 - "Community 46"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送退款结果通知 Args: openid: 用户openid ord
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
### Community 47 - "Community 47"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): 发送催单通知给配送员 Args: deliver_openid: 配送员openid
|
||||
|
||||
## Knowledge Gaps
|
||||
- **170 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `加载 RSA 密钥对:优先环境变量,否则使用内置固定默认密钥。` (+165 more)
|
||||
- **171 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `加载 RSA 密钥对:优先环境变量,否则使用内置固定默认密钥。` (+166 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **Thin community `Community 28`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
|
||||
- **Thin community `Community 29`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 31`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
|
||||
- **Thin community `Community 34`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 32`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
|
||||
- **Thin community `Community 35`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,5 个一级菜单分组)`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 33`** (3 nodes): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
|
||||
- **Thin community `Community 36`** (3 nodes): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 34`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
|
||||
- **Thin community `Community 37`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 35`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
|
||||
- **Thin community `Community 38`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 36`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
|
||||
- **Thin community `Community 39`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 39`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
|
||||
- **Thin community `Community 42`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 40`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
|
||||
- **Thin community `Community 43`** (1 nodes): `发送支付成功通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 41`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
|
||||
- **Thin community `Community 44`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 42`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
|
||||
- **Thin community `Community 45`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 43`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
|
||||
- **Thin community `Community 46`** (1 nodes): `发送退款结果通知 Args: openid: 用户openid ord`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 44`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
|
||||
- **Thin community `Community 47`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **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._
|
||||
- **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._
|
||||
- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 21`, `Community 25`, `Community 26`?**
|
||||
_High betweenness centrality (0.109) - this node is a cross-community bridge._
|
||||
- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 2`, `Community 35`, `Community 4`, `Community 7`, `Community 9`, `Community 10`, `Community 31`?**
|
||||
_High betweenness centrality (0.043) - 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 13`, `Community 14`, `Community 16`, `Community 19`, `Community 20`, `Community 25`, `Community 26`, `Community 27`, `Community 28`?**
|
||||
_High betweenness centrality (0.030) - 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?**
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/context.py": 1779242263.6923044,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/utils.py": 1780305790.0009735,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/protocol_status_display.py": 1783303208.7618413,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/storage.py": 1782437555.6682527,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/storage.py": 1786527936.515132,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/order_status_compat.py": 1778578258.0426462,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/exceptions.py": 1778578258.0413945,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/core/data_scope.py": 1785663691.2518938,
|
||||
@@ -165,7 +165,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol_no.py": 1779882001.753651,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_handover.py": 1780455574.0993364,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_facility.py": 1785832360.7565567,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_instance_draft.py": 1785990690.2179956,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_instance_draft.py": 1786527123.364954,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_lab_code.py": 1783308340.5854337,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_sample_type.py": 1781747645.1762104,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_outbound_status_sync.py": 1781595840.6955194,
|
||||
@@ -184,7 +184,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1786374022.5691514,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1782894693.6582327,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/production_material.py": 1782404982.2474263,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1786520649.8233767,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_menu.py": 1786587944.1214068,
|
||||
"/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,
|
||||
@@ -199,7 +199,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pre_transit_sample.py": 1782204426.7965167,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/qc_sms_notification.py": 1780326875.7886882,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation_sample_collection.py": 1780457788.2998126,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/oa_approval.py": 1786416766.968504,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/oa_approval.py": 1786527707.997609,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/operations.py": 1785213144.2881503,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1785836159.1492305,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_delivery.py": 1779655584.820777,
|
||||
@@ -218,7 +218,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_sample_collection.py": 1780457788.2093296,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1779266168.8693042,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/api/clinical_payment.py": 1781750841.074568,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1786416725.0574527,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1786600784.1269102,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_overtime_hours.py": 1786373766.5113294,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1778727966.7277644,
|
||||
"/Users/wuzilong/work/00code/zkxw/backend/app/services/qc_work_order_fee_summary.py": 1783329884.6125586,
|
||||
|
||||
@@ -812,6 +812,64 @@ class TestBuildProgress(unittest.TestCase):
|
||||
len(terminal), expect_n, f"status={status} terminal count"
|
||||
)
|
||||
|
||||
def test_cancelled_terminal_attributes_to_initiator(self):
|
||||
"""撤销终态归因发起人;任务 cancelled 仍投影为节点,由前端映射「无需处理」。"""
|
||||
from app.services.oa_engine import build_progress_items
|
||||
|
||||
root = {
|
||||
"id": "s",
|
||||
"type": "start",
|
||||
"child": {
|
||||
"id": "a1",
|
||||
"type": "approval",
|
||||
"name": "直接主管",
|
||||
"child": {"id": "e1", "type": "end"},
|
||||
},
|
||||
}
|
||||
ts = datetime(2026, 8, 13, 12, 47, 46)
|
||||
t_pending_cancelled = MagicMock(
|
||||
id="tA",
|
||||
node_key="a1",
|
||||
node_name="直接主管",
|
||||
task_type="approval",
|
||||
status="cancelled",
|
||||
assignee_user_id="u2",
|
||||
order_num=0,
|
||||
round_no=1,
|
||||
create_time=datetime(2026, 8, 13, 11, 17, 38),
|
||||
update_time=ts,
|
||||
comment_text=None,
|
||||
resolve_source="direct_supervisor",
|
||||
sign_mode="and",
|
||||
)
|
||||
inst = MagicMock(
|
||||
status="cancelled",
|
||||
initiator_user_id="u0",
|
||||
current_round=1,
|
||||
create_time=datetime(2026, 8, 13, 11, 17, 38),
|
||||
update_time=ts,
|
||||
)
|
||||
items = build_progress_items(
|
||||
inst=inst,
|
||||
flow_root=root,
|
||||
tasks=[t_pending_cancelled],
|
||||
name_map={"u0": "吴子龙", "u2": "张主管"},
|
||||
events=[],
|
||||
)
|
||||
task_card = next(x for x in items if x["kind"] == "task")
|
||||
self.assertEqual(task_card["title"], "直接主管")
|
||||
self.assertEqual(task_card["status"], "cancelled")
|
||||
self.assertEqual(task_card["people"][0]["status"], "cancelled")
|
||||
self.assertEqual(task_card["timestamp"], "", "cancelled 节点勿带撤销时间")
|
||||
|
||||
terminal = next(x for x in items if x["kind"] == "terminal")
|
||||
self.assertEqual(terminal["status"], "cancelled")
|
||||
self.assertEqual(terminal["title"], "已撤销")
|
||||
self.assertEqual(terminal["assigneeUserId"], "u0")
|
||||
self.assertEqual(terminal["assigneeUserName"], "吴子龙")
|
||||
self.assertEqual(terminal["commentText"], "发起人撤销")
|
||||
self.assertTrue(terminal["timestamp"])
|
||||
|
||||
def test_progress_returned_has_no_kind_return_node(self):
|
||||
from app.services.oa_engine import build_progress_items
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.crud.oa_instance_draft import DraftAccessError, row_to_dict
|
||||
from app.services.oa_related_approval import flatten_form_fields_for_summary
|
||||
|
||||
|
||||
class TestOaInstanceDraftRowToDict(unittest.TestCase):
|
||||
@@ -49,6 +50,25 @@ class TestOaInstanceDraftRowToDict(unittest.TestCase):
|
||||
err = DraftAccessError("草稿不存在或无权访问")
|
||||
self.assertEqual(str(err), "草稿不存在或无权访问")
|
||||
|
||||
def test_preview_fields_first_three(self):
|
||||
schema = {
|
||||
"rule": [
|
||||
{"type": "input", "field": "a", "title": "数字输入框"},
|
||||
{"type": "input", "field": "b", "title": "出差事由"},
|
||||
{"type": "input", "field": "c", "title": "出差天数"},
|
||||
{"type": "input", "field": "d", "title": "备注"},
|
||||
]
|
||||
}
|
||||
data = {"a": "123", "b": "开会", "c": "4", "d": "忽略"}
|
||||
preview = [
|
||||
{"label": f.get("label") or "", "value": f.get("value") or ""}
|
||||
for f in flatten_form_fields_for_summary(schema, data)[:3]
|
||||
]
|
||||
self.assertEqual(len(preview), 3)
|
||||
self.assertEqual(preview[0], {"label": "数字输入框", "value": "123"})
|
||||
self.assertEqual(preview[1], {"label": "出差事由", "value": "开会"})
|
||||
self.assertEqual(preview[2], {"label": "出差天数", "value": "4"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,20 @@
|
||||
"""storage Content-Type 推断(SVG 回显依赖正确 MIME)"""
|
||||
import unittest
|
||||
|
||||
from app.core.storage import guess_content_type
|
||||
|
||||
|
||||
class TestGuessContentType(unittest.TestCase):
|
||||
def test_svg(self):
|
||||
self.assertEqual(guess_content_type("icon.svg"), "image/svg+xml")
|
||||
self.assertEqual(guess_content_type("a/b/ICON.SVG"), "image/svg+xml")
|
||||
|
||||
def test_png(self):
|
||||
self.assertEqual(guess_content_type("a.png"), "image/png")
|
||||
|
||||
def test_unknown(self):
|
||||
self.assertEqual(guess_content_type("noext"), "application/octet-stream")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,41 @@
|
||||
# OA Detail Header Stamp 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:** Remove duplicate title on mobile OA detail and show a 72px dingding-style status stamp at the form top-right.
|
||||
|
||||
**Architecture:** Extract `resolveStamp` to a shared module; add `OaStatusStamp.vue`; overlay it in `OaDetail.vue` over the form host.
|
||||
|
||||
**Tech Stack:** Vue 3, mobile-oa-h5, Node assert tests
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-13-oa-detail-header-stamp-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Extract `resolveStamp`
|
||||
|
||||
**Files:**
|
||||
- Create: `mobile-oa-h5/src/oa/oa-status-stamp.js`
|
||||
- Create: `mobile-oa-h5/src/oa/oa-status-stamp.test.js`
|
||||
- Modify: `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js` (re-export / import)
|
||||
|
||||
- [x] Write failing tests for stamp mapping
|
||||
- [x] Implement `resolveStamp`
|
||||
- [x] Point print module at shared export
|
||||
|
||||
### Task 2: `OaStatusStamp` component
|
||||
|
||||
**Files:**
|
||||
- Create: `mobile-oa-h5/src/oa/components/OaStatusStamp.vue`
|
||||
|
||||
- [x] Dingding stamp DOM + 72px CSS (demo `.sm`)
|
||||
- [x] Props: `status` → uses `resolveStamp`
|
||||
|
||||
### Task 3: Wire `OaDetail`
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-oa-h5/src/views/OaDetail.vue`
|
||||
|
||||
- [x] Remove header title + status tag
|
||||
- [x] Wrap form in relative host; render stamp when `detail.status`
|
||||
- [x] Drop unused `statusText` / `statusTone` imports if unused
|
||||
@@ -32,7 +32,7 @@
|
||||
- Create: `mobile-h5/utils/oa-corner-index.js`
|
||||
- Create: `mobile-h5/utils/oa-corner-index.test.js`
|
||||
|
||||
- [ ] **Step 1: 写失败单测**
|
||||
- [x] **Step 1: 写失败单测**
|
||||
|
||||
```js
|
||||
// mobile-h5/utils/oa-corner-index.test.js
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 待办中心批量通过/打回 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:** 在 mobile-h5 待办中心支持标题栏管理多选,并批量通过 / 打回。
|
||||
|
||||
**Architecture:** 自定义顶栏对齐草稿箱;多选与批量栏对齐抄送/草稿;意见弹层后串行调用现有单条 approve/return API,不新增后端批量接口。
|
||||
|
||||
**Tech Stack:** uni-app (mobile-h5)、现有 oa-api / request 封装
|
||||
|
||||
---
|
||||
|
||||
### Task 1: API 封装
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/utils/oa-api.js`
|
||||
|
||||
- [x] 增加 `fetchApproveOaTask(taskId, comment)`、`fetchReturnOaTask(taskId, comment)`
|
||||
|
||||
### Task 2: 路由自定义导航
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/pages.json`(pending)
|
||||
|
||||
- [x] `navigationStyle: custom`,保留标题文案与下拉刷新配置
|
||||
|
||||
### Task 3: 待办页 UI + 批量逻辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-h5/pages/oa/pending/index.vue`
|
||||
|
||||
- [x] 自定义顶栏(返回 / 标题 / 管理)
|
||||
- [x] 管理态勾选 + 批量栏
|
||||
- [x] 意见弹层(通过可空、打回必填)
|
||||
- [x] 串行提交与结果 toast、刷新列表
|
||||
|
||||
### Task 4: 手工验收
|
||||
|
||||
- [ ] 对照 spec §5 清单自测关键路径
|
||||
@@ -0,0 +1,21 @@
|
||||
# OA 附件:Vant 缩略图美化
|
||||
|
||||
> 状态:已确认(2026-08-13)
|
||||
|
||||
## 决策
|
||||
|
||||
保留 `van-uploader` 交互(格子预览、删除、非图点按下载、详情 disabled 无删除),仅美化样式:
|
||||
|
||||
1. 预览格 84px、14px 圆角、浅绿描边、轻阴影
|
||||
2. 删除钮改为圆形半透明
|
||||
3. 非图封面:类型色小图标 + 短文件名 + 品牌绿「点击下载」条(不展示「图片/PDF」标签)
|
||||
4. 添加格:虚线框 + 绿色渐变 + 号
|
||||
5. **详情只读**(`disabled` / form-create 禁用):不展示添加格、不展示右上角删除;附件仅可预览/下载
|
||||
|
||||
## Demo
|
||||
|
||||
`mobile-ui/zkxw-mobile-oa-attachment-actions-demo.html`
|
||||
|
||||
## 落点
|
||||
|
||||
`mobile-oa-h5/src/oa/components/OaUploadField.vue`
|
||||
@@ -0,0 +1,24 @@
|
||||
# OA 详情头区:去掉重复标题 + 状态印章
|
||||
|
||||
> 状态:已确认(2026-08-13)
|
||||
|
||||
## 背景
|
||||
|
||||
移动端 OA 详情(`mobile-oa-h5` `OaDetail`,经 `mobile-h5` web-view 打开)内容区顶部重复展示流程标题(顶栏已有),状态为胶囊 Tag,与 web 打印钉钉风印章不一致。
|
||||
|
||||
## 决策
|
||||
|
||||
1. **去掉**内容区顶部流程标题 `h1`(顶栏已展示)。
|
||||
2. **状态 Tag 改为** web 打印同款钉钉风圆形印章(结构:外圈 / 上下弧 / `OA审批` / 主文案 / `approval`),`absolute` 叠在表单区域右上角,旋转约 `-18°`。
|
||||
3. **尺寸 72px**(相对 web 打印 118px 明显缩小,遮挡更少)。
|
||||
4. **文案/色类**复用 `resolveStamp(status)`(如 `completed` →「已通过」/`ok`)。
|
||||
5. **范围**:`mobile-oa-h5` 详情页;列表页、关联审批弹层不在本次范围。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改 web-admin 打印印章尺寸
|
||||
- 不改移动端 PDF/系统打印 HTML 里的简化印章(仍为另一套实现)
|
||||
|
||||
## Demo
|
||||
|
||||
`mobile-ui/zkxw-mobile-oa-detail-header-stamp-demo.html`
|
||||
@@ -0,0 +1,70 @@
|
||||
# 待办中心 · 批量通过 / 打回
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 状态 | 已确认 |
|
||||
| 日期 | 2026-08-13 |
|
||||
| 范围 | `mobile-h5` 待办中心列表页批量审批 |
|
||||
| Demo | `mobile-ui/zkxw-mobile-oa-pending-batch-actions-demo.html` |
|
||||
| 对齐 | 草稿箱自定义顶栏「管理」;抄送多选 / 批量栏交互 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在移动端「待办中心」支持勾选多条待办,**批量通过**与**批量打回**,减少逐条进入详情审批的操作成本。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 不做批量拒绝(降低误操作风险)
|
||||
- 不新增后端批量 approve/return API(本期前端串行调用现有单条接口)
|
||||
- 不做跨页 / 全库全选
|
||||
- 不改造 web-admin 待办页
|
||||
|
||||
## 3. 交互(SHALL)
|
||||
|
||||
### 3.1 顶栏
|
||||
|
||||
- 待办页使用 **自定义导航栏**(`navigationStyle: custom`),结构对齐草稿箱:
|
||||
- 左侧返回、居中标题「待办中心」、右侧「管理」/「完成」
|
||||
- 搜索区仅保留关键词检索,**不**再放管理按钮
|
||||
|
||||
### 3.2 管理态
|
||||
|
||||
- 点「管理」进入多选;点「完成」退出并清空勾选
|
||||
- 管理态卡片左侧圆形勾选;选中卡片描边高亮
|
||||
- 管理态点卡片 = 切换勾选;非管理态点卡片 = 进入审批详情(现有 webview)
|
||||
- 已勾选时展示批量栏:「已选 N 项」+「批量打回」+「批量通过」
|
||||
- 未勾选时批量按钮禁用或隐藏批量栏(与 Demo 一致:无勾选则不展示栏)
|
||||
|
||||
### 3.3 意见弹层
|
||||
|
||||
| 动作 | 标题 | 默认意见 | 校验 |
|
||||
|------|------|----------|------|
|
||||
| 批量通过 | 批量通过 | 「同意」 | 可改、可空(与单条同意一致) |
|
||||
| 批量打回 | 批量打回 | 空 | **必填**,否则 toast「打回意见必填」 |
|
||||
|
||||
弹层提示:将对 N 条待办执行操作。同一意见应用于本次选中的全部任务。
|
||||
|
||||
### 3.4 提交与结果
|
||||
|
||||
- 对选中 `taskId` **串行**调用:
|
||||
- 通过:`POST /web/admin/oa-approval/tasks/{id}/approve`
|
||||
- 打回:`POST /web/admin/oa-approval/tasks/{id}/return`
|
||||
- 结束后 toast 汇总:`成功 n,失败 m`(若全部成功可用更短文案如「已通过 n 条」)
|
||||
- 成功后退出管理态、清空勾选、刷新列表
|
||||
|
||||
## 4. 实现要点
|
||||
|
||||
- API 封装:`mobile-h5/utils/oa-api.js` 增加 `fetchApproveOaTask` / `fetchReturnOaTask`
|
||||
- 页面:`mobile-h5/pages/oa/pending/index.vue`(顶栏、多选、批量栏、意见弹层)
|
||||
- 路由:`mobile-h5/pages.json` 待办页 `navigationStyle: custom`
|
||||
- 视觉 Token:三墙绿 `#0a9f6d` 等,对齐现有草稿箱 / 抄送页
|
||||
|
||||
## 5. 验收
|
||||
|
||||
1. 标题栏有「管理」,搜索区无管理按钮
|
||||
2. 管理态可勾选多条,批量通过默认「同意」且可空提交
|
||||
3. 批量打回空意见被拦截
|
||||
4. 部分失败时提示成功/失败数,列表刷新
|
||||
5. 非管理态点卡片仍进审批详情
|
||||
@@ -0,0 +1,26 @@
|
||||
# OA 撤销后进度展示(方案 B)
|
||||
|
||||
> 日期:2026-08-13
|
||||
> Demo:`mobile-ui/zkxw-mobile-oa-progress-withdraw-cancelled-demo.html` 方案 B
|
||||
> 关联:`docs/superpowers/specs/2026-07-15-oa-approval-progress-timeline-design.md`
|
||||
> 打印:`web-admin` `buildFlowItems` / 移动端 `exportOaApprovalPdf.buildFlowItems` 须同映射
|
||||
|
||||
## 问题
|
||||
|
||||
发起人撤销后,引擎将待办任务置为 `cancelled`。进度轴把该状态直接展示为「已撤销」,误挂在「直接主管」等未审批节点上,语义像「主管撤销」。打印 PDF「审批流程」区同样存在该问题。
|
||||
|
||||
## 决策(方案 B)
|
||||
|
||||
| 位置 | 展示 |
|
||||
|------|------|
|
||||
| 任务 `status=cancelled`(清待办) | 节点徽章/打印头「无需处理」、人员行「无需处理」、mute、**不展示时间**(对齐或签 `done`) |
|
||||
| 实例终态 `kind=terminal` + `cancelled` | 标题/徽章「已撤销」;人员为发起人 + 文案「发起人撤销」+ 撤销时间 |
|
||||
| 库表 | **不改**:任务仍存 `cancelled`,仅投影/前端映射调整 |
|
||||
|
||||
打回时其它人的 `cancelled` 任务同样映射为「无需处理」;打回结论仍落在操作人任务节点。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不隐藏已到达的主管节点(方案 C)
|
||||
- 不改撤回/打回写库逻辑
|
||||
- 实例印章「已撤销」、列表「已撤销」不变
|
||||
@@ -75,6 +75,7 @@
|
||||
{
|
||||
"path": "pages/oa/drafts/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "草稿箱",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black",
|
||||
@@ -86,6 +87,7 @@
|
||||
{
|
||||
"path": "pages/oa/pending/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "待办中心",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black",
|
||||
@@ -119,9 +121,8 @@
|
||||
{
|
||||
"path": "pages/oa/cc/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "抄送我的",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 80,
|
||||
"backgroundTextStyle": "dark"
|
||||
|
||||
@@ -58,9 +58,21 @@
|
||||
<view class="panel">
|
||||
<view class="panel-hd">
|
||||
<text class="panel-title">功能菜单</text>
|
||||
<text class="hint">共 {{ menus.length }} 项</text>
|
||||
<text class="hint">{{ menusHint }}</text>
|
||||
</view>
|
||||
<MenuGrid v-if="menus.length" :menus="menus" @select="onMenuSelect" />
|
||||
<view v-if="menusLoading" class="home-menus-loading">
|
||||
<view class="pulse-dots" aria-hidden="true">
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
<view class="pulse-dot" />
|
||||
</view>
|
||||
<text class="home-menus-loading__text">加载菜单…</text>
|
||||
</view>
|
||||
<MenuGrid
|
||||
v-else-if="menus.length"
|
||||
:menus="menus"
|
||||
@select="onMenuSelect"
|
||||
/>
|
||||
<VanEmpty
|
||||
v-else
|
||||
description="暂无可用菜单权限"
|
||||
@@ -91,14 +103,6 @@
|
||||
icon: 'approve',
|
||||
path: '/pages/oa/start/index',
|
||||
iconBg: 'transparent'
|
||||
},
|
||||
{
|
||||
id: 'oa-form-sink',
|
||||
title: '表单水槽测试',
|
||||
icon: 'orders-o',
|
||||
path: '/pages/oa/form-sink/index',
|
||||
iconBg: '#eef2f1',
|
||||
iconFg: '#3d524a'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -118,7 +122,8 @@
|
||||
userName: '',
|
||||
postName: '',
|
||||
deptName: '',
|
||||
menus: [...FALLBACK_MENUS],
|
||||
menus: [],
|
||||
menusLoading: true,
|
||||
banners: [
|
||||
{
|
||||
id: 'banner-video-1',
|
||||
@@ -128,6 +133,12 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
menusHint() {
|
||||
if (this.menusLoading) return '加载中'
|
||||
return `共 ${this.menus.length} 项`
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
applyUserToState(this, getUser())
|
||||
fetchCurrentUser()
|
||||
@@ -164,18 +175,38 @@
|
||||
}
|
||||
},
|
||||
async loadMenus() {
|
||||
// 仅首次(尚无菜单)显示加载态,避免二次 onShow 闪一下空宫格
|
||||
if (!this.menus.length) {
|
||||
this.menusLoading = true
|
||||
}
|
||||
try {
|
||||
const list = await fetchMyMobileMenus()
|
||||
if (list && list.length) {
|
||||
this.menus = list
|
||||
this.menus = list.filter((m) => {
|
||||
const path = String((m && m.path) || '')
|
||||
const id = String((m && m.id) || '')
|
||||
return (
|
||||
path.indexOf('form-sink') < 0 &&
|
||||
id.indexOf('form-sink') < 0 &&
|
||||
String((m && m.title) || '').indexOf('水槽') < 0
|
||||
)
|
||||
})
|
||||
await this.refreshMenuBadges()
|
||||
return
|
||||
}
|
||||
if (!this.menus.length) {
|
||||
this.menus = [...FALLBACK_MENUS]
|
||||
}
|
||||
await this.refreshMenuBadges()
|
||||
} catch (e) {
|
||||
/* 接口未就绪或无权限时用过渡菜单 */
|
||||
/* 接口未就绪时用过渡菜单,避免首页完全空白 */
|
||||
if (!this.menus.length) {
|
||||
this.menus = [...FALLBACK_MENUS]
|
||||
}
|
||||
await this.refreshMenuBadges()
|
||||
} finally {
|
||||
this.menusLoading = false
|
||||
}
|
||||
this.menus = [...FALLBACK_MENUS]
|
||||
await this.refreshMenuBadges()
|
||||
},
|
||||
onMenuSelect(m) {
|
||||
if (m.path) {
|
||||
@@ -189,3 +220,58 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-menus-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28rpx;
|
||||
min-height: 240rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pulse-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--brand-green, #22c55e);
|
||||
animation: home-menu-pulse 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;
|
||||
}
|
||||
|
||||
.home-menus-loading__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes home-menu-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -54,11 +54,12 @@
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="login-submit"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
class="login-submit oa-btn-loading"
|
||||
:class="{ 'is-loading': loading }"
|
||||
@click="onPasswordLogin"
|
||||
>
|
||||
{{ loading ? '登录中…' : '登录' }}
|
||||
<text class="oa-btn-loading__label">登录</text>
|
||||
<view v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
|
||||
</view>
|
||||
|
||||
<view class="login-agree" @click="toggleAgree">
|
||||
@@ -78,23 +79,29 @@
|
||||
|
||||
<button
|
||||
v-if="agreed"
|
||||
class="login-wechat"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
class="login-wechat oa-btn-loading"
|
||||
:class="{ 'is-loading': loading }"
|
||||
open-type="getPhoneNumber"
|
||||
:disabled="loading"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
>
|
||||
<image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
||||
<text>微信授权登录</text>
|
||||
<view class="oa-btn-loading__label login-wechat__inner">
|
||||
<image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
||||
<text>微信授权登录</text>
|
||||
</view>
|
||||
<view v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
|
||||
</button>
|
||||
<view
|
||||
v-else
|
||||
class="login-wechat"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
class="login-wechat oa-btn-loading"
|
||||
:class="{ 'is-loading': loading }"
|
||||
@click="onWechatGate"
|
||||
>
|
||||
<image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
||||
<text>微信授权登录</text>
|
||||
<view class="oa-btn-loading__label login-wechat__inner">
|
||||
<image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
||||
<text>微信授权登录</text>
|
||||
</view>
|
||||
<view v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
|
||||
</view>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -51,11 +51,19 @@
|
||||
<view v-if="manageMode && selectedIds.length" class="batch-bar">
|
||||
<text class="batch-bar__hint">已选 {{ selectedIds.length }} 项</text>
|
||||
<view
|
||||
class="batch-bar__btn"
|
||||
:class="{ 'is-disabled': batchReadDisabled }"
|
||||
class="batch-bar__btn oa-btn-loading"
|
||||
:class="{
|
||||
'is-disabled': batchReadDisabled && !batchMarking,
|
||||
'is-loading': batchMarking
|
||||
}"
|
||||
@click="batchMarkRead"
|
||||
>
|
||||
批量已读
|
||||
<text class="oa-btn-loading__label">批量已读</text>
|
||||
<view
|
||||
v-if="batchMarking"
|
||||
class="oa-btn-loading__spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -37,7 +37,18 @@
|
||||
|
||||
<view v-if="manageMode && selectedIds.length" class="batch-bar">
|
||||
<text class="batch-bar__hint">已选 {{ selectedIds.length }} 项</text>
|
||||
<view class="batch-bar__btn" @click="batchDelete">批量删除</view>
|
||||
<view
|
||||
class="batch-bar__btn oa-btn-loading"
|
||||
:class="{ 'is-loading': batchDeleting }"
|
||||
@click="batchDelete"
|
||||
>
|
||||
<text class="oa-btn-loading__label">批量删除</text>
|
||||
<view
|
||||
v-if="batchDeleting"
|
||||
class="oa-btn-loading__spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !list.length" class="state state-loading">
|
||||
@@ -139,6 +150,7 @@
|
||||
hasMore: true,
|
||||
manageMode: false,
|
||||
selectedIds: [],
|
||||
batchDeleting: false,
|
||||
_searchTimer: null
|
||||
}
|
||||
},
|
||||
@@ -355,13 +367,14 @@
|
||||
},
|
||||
async batchDelete() {
|
||||
const ids = (this.selectedIds || []).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
if (!ids.length || this.batchDeleting) return
|
||||
const ok = await this.confirm(
|
||||
'批量删除',
|
||||
'确认删除选中的 ' + ids.length + ' 条草稿吗?',
|
||||
'删除'
|
||||
)
|
||||
if (!ok) return
|
||||
this.batchDeleting = true
|
||||
try {
|
||||
await fetchBatchDeleteOaInstanceDrafts(ids)
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
@@ -374,6 +387,8 @@
|
||||
title: (e && e.message) || '批量删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.batchDeleting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +176,15 @@
|
||||
return COPY_RESTART_STATUSES.includes(status) && !!appId
|
||||
},
|
||||
submitterText(row) {
|
||||
const status = this.rowStatus(row)
|
||||
const names = String((row && row.currentApproverNames) || '').trim()
|
||||
if (names) return '待' + names + '处理'
|
||||
// 已结束时后端 currentApproverNames 常为 "-",勿拼成「待-处理」
|
||||
const inProgress = status === 'pending' || status === 'running'
|
||||
if (inProgress && names && names !== '-') {
|
||||
return '待' + names + '处理'
|
||||
}
|
||||
const node = String((row && row.currentNodeName) || '').trim()
|
||||
if (node) return node
|
||||
if (node && node !== '-') return node
|
||||
return '我发起的'
|
||||
},
|
||||
statusText(s) {
|
||||
|
||||
@@ -147,11 +147,19 @@
|
||||
<view class="sheet__ft">
|
||||
<view class="sheet__btn is-cancel" @click="closeBatchSheet">取消</view>
|
||||
<view
|
||||
class="sheet__btn"
|
||||
:class="sheetKind === 'return' ? 'is-return' : 'is-ok'"
|
||||
class="sheet__btn oa-btn-loading"
|
||||
:class="[
|
||||
sheetKind === 'return' ? 'is-return' : 'is-ok',
|
||||
{ 'is-loading': batchActing }
|
||||
]"
|
||||
@click="confirmBatch"
|
||||
>
|
||||
{{ batchActing ? '处理中…' : '确定' }}
|
||||
<text class="oa-btn-loading__label">确定</text>
|
||||
<view
|
||||
v-if="batchActing"
|
||||
class="oa-btn-loading__spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
buildOaStartUrl,
|
||||
takeOaStartTitle
|
||||
} from '../../../utils/oa-h5.js'
|
||||
import { navigateBackFromOaHost } from '../../../utils/oa-navigate-back.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { fetchOaTemplateDetail } from '../../../utils/oa-api.js'
|
||||
|
||||
@@ -88,8 +89,9 @@ export default {
|
||||
uni.showToast({ title: msg, icon: 'none', duration: 1200 })
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
navigateBackFromOaHost({
|
||||
fallbackUrl: '/pages/oa/start/index',
|
||||
onStuck: () => {
|
||||
this._closing = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
takeOaDetailTitle,
|
||||
takeOaStartTitle
|
||||
} from '../../../utils/oa-h5.js'
|
||||
import { navigateBackFromOaHost } from '../../../utils/oa-navigate-back.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { fetchOaTemplateDetail } from '../../../utils/oa-api.js'
|
||||
|
||||
@@ -146,8 +147,8 @@ export default {
|
||||
uni.showToast({ title: msg, icon: 'none', duration: 1200 })
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
navigateBackFromOaHost({
|
||||
onStuck: () => {
|
||||
this._closing = false
|
||||
}
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -1,4 +1,5 @@
|
||||
@import './tokens.scss';
|
||||
@import './components/btn-loading.scss';
|
||||
|
||||
page {
|
||||
@include token-css-vars;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* 按钮点击加载:文案淡化 + 居中自绘转圈(对齐 mobile-oa-h5 发起页) */
|
||||
.oa-btn-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.oa-btn-loading.is-loading {
|
||||
opacity: 1 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.oa-btn-loading.is-loading .oa-btn-loading__label {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.oa-btn-loading__spin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.oa-btn-loading__spin::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: oa-btn-loading-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes oa-btn-loading-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,6 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-submit.is-disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.login-agree {
|
||||
display: flex;
|
||||
@@ -179,9 +176,11 @@ button.login-wechat::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-wechat.is-disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
.login-wechat__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.login-wechat__icon {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* 微信公众平台须配置 request / 业务域名:www.chengyantek.cn(HTTPS,安全组放行 443)
|
||||
*/
|
||||
const USE_REMOTE_TEST = true
|
||||
const USE_REMOTE_TEST = false
|
||||
|
||||
/** 本机联调 */
|
||||
const LOCAL_API_BASE_URL = 'http://127.0.0.1:8000/api'
|
||||
|
||||
@@ -86,6 +86,7 @@ export function fetchSubmitOaInstanceDraft(id, data) {
|
||||
|
||||
export function fetchOaTasks(kind, params) {
|
||||
const p = params || {}
|
||||
const keyword = (p.keyword || '').trim()
|
||||
const appName = (p.appName || '').trim()
|
||||
const initiatorName = (p.initiatorName || '').trim()
|
||||
const query = {
|
||||
@@ -95,6 +96,7 @@ export function fetchOaTasks(kind, params) {
|
||||
if (kind === 'cc') {
|
||||
query.readStatus = p.readStatus != null && p.readStatus !== '' ? p.readStatus : 'unread'
|
||||
}
|
||||
if (keyword) query.keyword = keyword
|
||||
if (appName) query.appName = appName
|
||||
if (initiatorName) query.initiatorName = initiatorName
|
||||
return get(
|
||||
@@ -126,3 +128,19 @@ export function fetchBatchMarkOaCcTasksRead(ids) {
|
||||
authHeader()
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchApproveOaTask(taskId, comment) {
|
||||
return post(
|
||||
'/web/admin/oa-approval/tasks/' + encodeURIComponent(taskId) + '/approve',
|
||||
{ comment: comment || undefined },
|
||||
authHeader()
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchReturnOaTask(taskId, comment) {
|
||||
return post(
|
||||
'/web/admin/oa-approval/tasks/' + encodeURIComponent(taskId) + '/return',
|
||||
{ comment: comment || undefined },
|
||||
authHeader()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* H5 上 uni.navigateBack 偶发既不成功也不进 fail;离开 OA web-view 时做兜底。
|
||||
* @param {{ fallbackUrl?: string, onStuck?: () => void }} [opts]
|
||||
*/
|
||||
export function navigateBackFromOaHost(opts) {
|
||||
const onStuck = opts && opts.onStuck
|
||||
|
||||
const currentRoute = () => {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const cur = pages && pages[pages.length - 1]
|
||||
return String((cur && (cur.route || (cur.$page && cur.$page.fullPath))) || '')
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const stillOnWebview = () => {
|
||||
const route = currentRoute()
|
||||
return (
|
||||
route.indexOf('oa/start/webview') !== -1 ||
|
||||
route.indexOf('oa/webview/') !== -1
|
||||
)
|
||||
}
|
||||
|
||||
const resolveFallbackUrl = () => {
|
||||
const explicit = String((opts && opts.fallbackUrl) || '').trim()
|
||||
if (explicit) return explicit
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
if (pages && pages.length >= 2) {
|
||||
const prev = pages[pages.length - 2]
|
||||
const route = prev && prev.route
|
||||
if (route) return '/' + String(route).replace(/^\//, '')
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
return '/pages/oa/start/index'
|
||||
}
|
||||
|
||||
const fallback = () => {
|
||||
const fallbackUrl = resolveFallbackUrl()
|
||||
if (!fallbackUrl) {
|
||||
if (onStuck) onStuck()
|
||||
return
|
||||
}
|
||||
uni.redirectTo({
|
||||
url: fallbackUrl,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: fallbackUrl,
|
||||
fail: () => {
|
||||
if (onStuck) onStuck()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
if (pages && pages.length > 1) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => fallback()
|
||||
})
|
||||
// H5:navigateBack 偶发静默失败
|
||||
setTimeout(() => {
|
||||
if (stillOnWebview()) fallback()
|
||||
}, 280)
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* fall through */
|
||||
}
|
||||
fallback()
|
||||
}
|
||||
@@ -38,6 +38,14 @@ export function uploadAttachment(file) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取带 Content-Disposition 的下载地址(保留原始文件名) */
|
||||
export function fetchAttachmentDownloadUrl({ fileUrl, fileName } = {}) {
|
||||
return post('/web/admin/attachment/download-url', {
|
||||
fileUrl,
|
||||
fileName: fileName || undefined
|
||||
})
|
||||
}
|
||||
|
||||
// —— 发起审批运行时(对齐 web-admin oaApproval.ts)——
|
||||
|
||||
export function fetchOaGroupList() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/tokens.css'
|
||||
import './styles/oa-form.css'
|
||||
import './styles/oa-btn-loading.css'
|
||||
import { registerOaComponents } from './oa/register-components'
|
||||
import { captureTokenFromUrl } from './utils/auth'
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 通知宿主(小程序 web-view / uni-app H5 iframe)关闭并返回上一页。
|
||||
* @param {object} payload
|
||||
* @param {{ wx?: any, uni?: any, window?: any, showToast?: (msg: string) => void, fallbackMessage?: string }} [deps]
|
||||
* @returns {'wx' | 'iframe' | 'uni' | 'toast' | 'none'}
|
||||
*/
|
||||
export function closeToHost(payload, deps = {}) {
|
||||
const data = payload || { action: 'oa-close' }
|
||||
const wxApi = deps.wx !== undefined ? deps.wx : typeof wx !== 'undefined' ? wx : undefined
|
||||
const uniApi = deps.uni !== undefined ? deps.uni : typeof uni !== 'undefined' ? uni : undefined
|
||||
const win = deps.window !== undefined ? deps.window : typeof window !== 'undefined' ? window : undefined
|
||||
const toast =
|
||||
deps.showToast ||
|
||||
((msg) => {
|
||||
/* no-op without toast */
|
||||
void msg
|
||||
})
|
||||
const fallbackMessage = deps.fallbackMessage || '已完成(非小程序环境)'
|
||||
|
||||
try {
|
||||
if (wxApi?.miniProgram?.navigateBack) {
|
||||
// 小程序宿主不展示 payload.message,须在 web-view 内提示
|
||||
if (data && data.message) toast(String(data.message))
|
||||
wxApi.miniProgram.postMessage?.({ data })
|
||||
wxApi.miniProgram.navigateBack()
|
||||
return 'wx'
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
// H5 嵌入:优先 parent postMessage,避免 uni.webView.navigateBack 空操作后提前 return
|
||||
// 成功文案由宿主 uni.showToast 展示,此处不再 toast
|
||||
const inIframe = !!(win && win.parent && win.parent !== win)
|
||||
if (inIframe) {
|
||||
try {
|
||||
win.parent.postMessage({ source: 'zkxw-oa-h5', ...data }, '*')
|
||||
return 'iframe'
|
||||
} catch (_) {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const uniWv = uniApi?.webView || null
|
||||
if (uniWv?.postMessage) {
|
||||
uniWv.postMessage({ data })
|
||||
}
|
||||
if (uniWv?.navigateBack) {
|
||||
if (data && data.message) toast(String(data.message))
|
||||
uniWv.navigateBack()
|
||||
return 'uni'
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
toast((data && data.message) || fallbackMessage)
|
||||
return 'toast'
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { closeToHost } from './close-to-host.js'
|
||||
|
||||
// 微信小程序:走 wx,不碰 iframe / uni
|
||||
{
|
||||
const calls = []
|
||||
const result = closeToHost(
|
||||
{ action: 'oa-start-submitted', message: '提交成功' },
|
||||
{
|
||||
wx: {
|
||||
miniProgram: {
|
||||
postMessage: (p) => calls.push(['post', p]),
|
||||
navigateBack: () => calls.push(['back'])
|
||||
}
|
||||
},
|
||||
window: { parent: {} },
|
||||
uni: { webView: { navigateBack: () => calls.push(['uni-back']) } },
|
||||
showToast: (m) => calls.push(['toast', m])
|
||||
}
|
||||
)
|
||||
assert.equal(result, 'wx')
|
||||
assert.deepEqual(
|
||||
calls.map((c) => c[0]),
|
||||
['toast', 'post', 'back']
|
||||
)
|
||||
assert.equal(calls[0][1], '提交成功')
|
||||
}
|
||||
|
||||
// H5 iframe:即使存在 uni.webView.navigateBack,也必须走 parent postMessage(修复空操作提前 return)
|
||||
{
|
||||
const calls = []
|
||||
const parent = {
|
||||
postMessage: (payload, origin) => calls.push(['parent', payload, origin])
|
||||
}
|
||||
const win = { parent }
|
||||
const result = closeToHost(
|
||||
{ action: 'oa-start-submitted', message: '提交成功' },
|
||||
{
|
||||
wx: undefined,
|
||||
window: win,
|
||||
uni: {
|
||||
webView: {
|
||||
postMessage: () => calls.push(['uni-post']),
|
||||
navigateBack: () => calls.push(['uni-back'])
|
||||
}
|
||||
},
|
||||
showToast: (m) => calls.push(['toast', m])
|
||||
}
|
||||
)
|
||||
assert.equal(result, 'iframe')
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0][0], 'parent')
|
||||
assert.equal(calls[0][1].source, 'zkxw-oa-h5')
|
||||
assert.equal(calls[0][1].action, 'oa-start-submitted')
|
||||
assert.equal(calls[0][1].message, '提交成功')
|
||||
assert.equal(calls[0][2], '*')
|
||||
}
|
||||
|
||||
// 非 iframe:走 uni.webView
|
||||
{
|
||||
const calls = []
|
||||
const win = {}
|
||||
win.parent = win
|
||||
const result = closeToHost(
|
||||
{ action: 'oa-start-draft', message: '草稿已保存' },
|
||||
{
|
||||
wx: undefined,
|
||||
window: win,
|
||||
uni: {
|
||||
webView: {
|
||||
postMessage: (p) => calls.push(['uni-post', p]),
|
||||
navigateBack: () => calls.push(['uni-back'])
|
||||
}
|
||||
},
|
||||
showToast: (m) => calls.push(['toast', m])
|
||||
}
|
||||
)
|
||||
assert.equal(result, 'uni')
|
||||
assert.deepEqual(
|
||||
calls.map((c) => c[0]),
|
||||
['uni-post', 'toast', 'uni-back']
|
||||
)
|
||||
assert.equal(calls[1][1], '草稿已保存')
|
||||
}
|
||||
|
||||
// 无宿主:toast 兜底,优先用 payload.message
|
||||
{
|
||||
const calls = []
|
||||
const win = {}
|
||||
win.parent = win
|
||||
const result = closeToHost(
|
||||
{ action: 'oa-start-submitted', message: '提交成功' },
|
||||
{
|
||||
wx: undefined,
|
||||
window: win,
|
||||
uni: undefined,
|
||||
showToast: (m) => calls.push(m)
|
||||
}
|
||||
)
|
||||
assert.equal(result, 'toast')
|
||||
assert.deepEqual(calls, ['提交成功'])
|
||||
}
|
||||
|
||||
console.log('close-to-host.test.js: ok')
|
||||
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<span class="oa-btn-loading__label"><slot /></span>
|
||||
<span v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
loading: { type: Boolean, default: false }
|
||||
})
|
||||
</script>
|
||||
@@ -4,13 +4,13 @@
|
||||
ref="fieldRef"
|
||||
:model-value="displayValue"
|
||||
:placeholder="placeholderText"
|
||||
:disabled="disabled"
|
||||
:disabled="isReadonly"
|
||||
readonly
|
||||
:border="false"
|
||||
is-link
|
||||
:is-link="!isReadonly"
|
||||
@click="open"
|
||||
>
|
||||
<template v-if="clearable && displayValue" #right-icon>
|
||||
<template v-if="showClear" #right-icon>
|
||||
<i class="van-badge__wrapper van-icon van-icon-clear van-field__clear" @click.stop="clear" />
|
||||
</template>
|
||||
</van-field>
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, useAttrs, watch } from 'vue'
|
||||
|
||||
defineOptions({ name: 'fcDatePicker' })
|
||||
|
||||
@@ -67,6 +67,17 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'fc.el'])
|
||||
const attrs = useAttrs()
|
||||
|
||||
const isReadonly = computed(() => {
|
||||
if (props.disabled) return true
|
||||
const a = attrs.disabled
|
||||
if (a === true || a === '' || a === 'true' || a === 1 || a === '1') return true
|
||||
const inj = props.formCreateInject
|
||||
if (inj?.api?.options?.form?.disabled) return true
|
||||
if (inj?.rule?.props?.disabled) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const show = ref(false)
|
||||
const fieldRef = ref(null)
|
||||
@@ -132,6 +143,11 @@ const displayValue = computed(() => {
|
||||
return String(v)
|
||||
})
|
||||
|
||||
/** 详情只读不展示清除图标 */
|
||||
const showClear = computed(
|
||||
() => props.clearable && !!displayValue.value && !isReadonly.value
|
||||
)
|
||||
|
||||
function toDateObj(v) {
|
||||
if (v == null || v === '') return undefined
|
||||
if (v instanceof Date && !Number.isNaN(v.getTime())) return v
|
||||
@@ -219,7 +235,7 @@ function emitValue(val) {
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (props.disabled) return
|
||||
if (isReadonly.value) return
|
||||
const parsed = parseModelValue(props.modelValue)
|
||||
dateParts.value = parsed.date.slice(0, dateColumnsType.value.length)
|
||||
timeParts.value = parsed.time.slice(0, timeColumnsType.value.length)
|
||||
@@ -239,6 +255,7 @@ function onDateTimeConfirm() {
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (isReadonly.value) return
|
||||
emitValue('')
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
<!-- 关联审批单 · 只读详情底部弹层(对齐 web-admin OaInstanceDetailDialog) -->
|
||||
<template>
|
||||
<van-popup
|
||||
:show="modelValue"
|
||||
position="bottom"
|
||||
round
|
||||
:style="{ height: '88%' }"
|
||||
teleport="body"
|
||||
@update:show="onShowUpdate"
|
||||
>
|
||||
<div class="oa-detail-sheet">
|
||||
<div class="oa-detail-sheet__hd">
|
||||
<span class="oa-detail-sheet__close" @click="close">关闭</span>
|
||||
<div class="oa-detail-sheet__hd-main">
|
||||
<strong class="oa-detail-sheet__title">{{ sheetTitle }}</strong>
|
||||
<span
|
||||
v-if="detail?.status"
|
||||
class="oa-detail-sheet__status"
|
||||
:class="statusTone(detail.status)"
|
||||
>
|
||||
{{ statusText(detail.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="oa-detail-sheet__hd-spacer" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div class="oa-detail-sheet__body">
|
||||
<div v-if="loading" class="oa-detail-sheet__loading">
|
||||
<OaPulseLoading text="加载审批详情…" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<form-create-mobile
|
||||
v-if="formRule.length"
|
||||
v-model="formData"
|
||||
:rule="formRule"
|
||||
:option="formOption"
|
||||
:disabled="true"
|
||||
/>
|
||||
<p v-else class="oa-detail-sheet__empty">暂无表单内容</p>
|
||||
<OaProgressTimeline :items="timelineItems" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { fetchOaInstanceDetail } from '../../api/oa.js'
|
||||
import { prepareMobileFormSchema } from '../schema-prep.js'
|
||||
import { statusText, statusTone } from '../status-label.js'
|
||||
import OaProgressTimeline from './OaProgressTimeline.vue'
|
||||
import OaPulseLoading from './OaPulseLoading.vue'
|
||||
|
||||
defineOptions({ name: 'OaInstanceDetailSheet' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
instanceId: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const formData = ref({})
|
||||
const formRule = ref([])
|
||||
const formOption = ref({
|
||||
form: { disabled: true, labelAlign: 'top' },
|
||||
submitBtn: false,
|
||||
resetBtn: false
|
||||
})
|
||||
|
||||
const sheetTitle = computed(() => detail.value?.title || '审批详情')
|
||||
|
||||
const timelineItems = computed(() => {
|
||||
const list = detail.value?.progress
|
||||
if (!Array.isArray(list) || !list.length) return []
|
||||
return list
|
||||
.filter((p) => {
|
||||
const kind = String(p?.kind || '')
|
||||
const status = String(p?.status || '')
|
||||
const title = String(p?.title || '').trim()
|
||||
if (kind === 'terminal' && (status === 'returned' || title === '已打回')) return false
|
||||
return true
|
||||
})
|
||||
.map((p, idx) => ({
|
||||
key: `${p.kind}-${p.nodeKey || p.taskId || idx}-${p.roundNo || 1}`,
|
||||
title: p.title || '-',
|
||||
status: p.status || '',
|
||||
timestamp: p.timestamp || '',
|
||||
assignee: p.assigneeUserName || p.assigneeUserId || '',
|
||||
commentText: p.commentText || '',
|
||||
taskType: p.taskType || '',
|
||||
kind: p.kind || '',
|
||||
signMode: p.signMode || '',
|
||||
people: Array.isArray(p.people)
|
||||
? p.people.map((x) => ({
|
||||
id: x.id || '',
|
||||
nickName: x.nickName || x.id || '',
|
||||
status: x.status || '',
|
||||
timestamp: x.timestamp || '',
|
||||
commentText: x.commentText || '',
|
||||
taskId: x.taskId || '',
|
||||
transferToUserId: x.transferToUserId || '',
|
||||
transferToUserName: x.transferToUserName || ''
|
||||
}))
|
||||
: []
|
||||
}))
|
||||
})
|
||||
|
||||
function parseFormData(d) {
|
||||
if (d?.formData && typeof d.formData === 'object') return { ...d.formData }
|
||||
if (d?.formDataJson && typeof d.formDataJson === 'object') return { ...d.formDataJson }
|
||||
return {}
|
||||
}
|
||||
|
||||
function onShowUpdate(v) {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
async function loadDetail(id) {
|
||||
const iid = String(id || '').trim()
|
||||
if (!iid) return
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
formRule.value = []
|
||||
formData.value = {}
|
||||
try {
|
||||
const d = await fetchOaInstanceDetail(iid)
|
||||
detail.value = d
|
||||
const prepared = prepareMobileFormSchema(d?.formSchemaJson)
|
||||
formRule.value = prepared.rule
|
||||
formOption.value = {
|
||||
...prepared.option,
|
||||
form: {
|
||||
...(prepared.option?.form || {}),
|
||||
disabled: true
|
||||
},
|
||||
submitBtn: false,
|
||||
resetBtn: false
|
||||
}
|
||||
formData.value = parseFormData(d)
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加载失败')
|
||||
close()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.instanceId],
|
||||
([open, id]) => {
|
||||
if (open && id) loadDetail(id)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-detail-sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-page, #ffffff);
|
||||
}
|
||||
.oa-detail-sheet__hd {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.oa-detail-sheet__close {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
}
|
||||
.oa-detail-sheet__hd-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.oa-detail-sheet__title {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.oa-detail-sheet__hd-spacer {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
}
|
||||
.oa-detail-sheet__status {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-detail-sheet__status.ok {
|
||||
color: var(--success, #10b981);
|
||||
background: var(--brand-green-soft, #ecfdf5);
|
||||
}
|
||||
.oa-detail-sheet__status.warn {
|
||||
color: var(--warning, #d97706);
|
||||
background: var(--barley-yellow-soft, #fffbeb);
|
||||
}
|
||||
.oa-detail-sheet__status.danger {
|
||||
color: var(--danger, #ef4444);
|
||||
background: #fef2f2;
|
||||
}
|
||||
.oa-detail-sheet__status.mute {
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.oa-detail-sheet__body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px 12px 24px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.oa-detail-sheet__loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
.oa-detail-sheet__empty {
|
||||
margin: 12px 0;
|
||||
padding: 16px 8px;
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -14,18 +14,99 @@
|
||||
<span
|
||||
v-if="item.status"
|
||||
class="oa-progress-badge"
|
||||
:class="statusTone(item.status)"
|
||||
:class="nodeStatusTone(item)"
|
||||
>
|
||||
{{ statusText(item.status) }}
|
||||
{{ nodeStatusText(item) }}
|
||||
</span>
|
||||
<span v-if="signBadge(item)" class="oa-progress-badge" :class="signBadge(item)">
|
||||
{{ signBadgeText(item) }}
|
||||
</span>
|
||||
<span v-else-if="item.taskType === 'cc'" class="oa-progress-badge cc">抄送</span>
|
||||
<span v-else-if="item.taskType === 'end'" class="oa-progress-badge mute">结束</span>
|
||||
</div>
|
||||
<div v-if="item.assignee" class="oa-progress-card__meta">
|
||||
{{ item.assignee }}
|
||||
|
||||
<!-- 会签/或签等:按人分行 -->
|
||||
<div v-if="personLines(item).length" class="oa-person-lines">
|
||||
<div
|
||||
v-for="(line, li) in personLines(item)"
|
||||
:key="`${item.key}-pl-${li}`"
|
||||
class="oa-person-block"
|
||||
:class="{
|
||||
'is-ok': line.tone === 'ok',
|
||||
'is-pending': line.tone === 'pending',
|
||||
'is-danger': line.tone === 'danger',
|
||||
'is-mute': line.tone === 'mute'
|
||||
}"
|
||||
>
|
||||
<div class="oa-person-line">
|
||||
<span class="oa-person-line__body">
|
||||
<template v-if="line.timestamp || line.isOpinion">{{ line.name }}</template>
|
||||
<template v-else>{{ line.body }}</template>
|
||||
</span>
|
||||
<span v-if="line.timestamp" class="oa-person-line__time">{{ line.timestamp }}</span>
|
||||
</div>
|
||||
<div v-if="line.detail && (line.timestamp || line.isOpinion)" class="oa-person-line__opinion">
|
||||
<template v-if="line.isOpinion">意见:{{ line.detail }}</template>
|
||||
<template v-else>{{ line.detail }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.timestamp" class="oa-progress-card__meta">
|
||||
{{ item.timestamp }}
|
||||
|
||||
<!-- 抄送:姓名字号/颜色与审批人已处理态一致 -->
|
||||
<div v-else-if="ccPersonLines(item).length" class="oa-person-lines">
|
||||
<div
|
||||
v-for="(line, li) in ccPersonLines(item)"
|
||||
:key="`${item.key}-cc-${li}`"
|
||||
class="oa-person-block is-ok"
|
||||
>
|
||||
<div class="oa-person-line">
|
||||
<span class="oa-person-line__body">{{ line.name }}</span>
|
||||
<span v-if="line.timestamp" class="oa-person-line__time">{{ line.timestamp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.commentText" class="oa-progress-card__comment">
|
||||
|
||||
<!-- 发起人:姓名颜色与审批人已处理态一致 -->
|
||||
<div
|
||||
v-else-if="item.kind === 'submit' && (item.assignee || item.timestamp)"
|
||||
class="oa-person-lines"
|
||||
>
|
||||
<div class="oa-person-block is-ok">
|
||||
<div class="oa-person-line">
|
||||
<span v-if="item.assignee" class="oa-person-line__body">{{ item.assignee }}</span>
|
||||
<span v-if="item.timestamp" class="oa-person-line__time">{{ item.timestamp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 终态撤销:发起人 +「发起人撤销」+ 时间(勿用「意见:」前缀) -->
|
||||
<div
|
||||
v-else-if="item.kind === 'terminal' && item.status === 'cancelled' && (item.assignee || item.commentText || item.timestamp)"
|
||||
class="oa-person-lines"
|
||||
>
|
||||
<div class="oa-person-block is-danger">
|
||||
<div class="oa-person-line">
|
||||
<span class="oa-person-line__body">
|
||||
<template v-if="item.assignee">{{ item.assignee }}</template>
|
||||
<template v-if="item.commentText">
|
||||
<template v-if="item.assignee"> </template>{{ item.commentText }}
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="item.timestamp" class="oa-person-line__time">{{ item.timestamp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无 people:姓名与时间同一行,时间右对齐 -->
|
||||
<div
|
||||
v-else-if="item.assignee || item.timestamp"
|
||||
class="oa-person-line oa-person-line--fallback"
|
||||
>
|
||||
<span v-if="item.assignee" class="oa-person-line__body">{{ item.assignee }}</span>
|
||||
<span v-if="item.timestamp" class="oa-person-line__time">{{ item.timestamp }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showNodeComment(item)" class="oa-progress-card__comment">
|
||||
意见:{{ item.commentText }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,13 +117,76 @@
|
||||
|
||||
<script setup>
|
||||
import { statusText, statusTone } from '../status-label.js'
|
||||
import {
|
||||
formatCcPersonLines,
|
||||
formatProgressPersonLines
|
||||
} from '../oa-progress-person-lines.js'
|
||||
|
||||
defineProps({
|
||||
items: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
function displayPeople(item) {
|
||||
const list = Array.isArray(item?.people)
|
||||
? item.people.filter((p) => p?.nickName || p?.id)
|
||||
: []
|
||||
if (!list.length) return []
|
||||
return list.map((p) => ({
|
||||
...p,
|
||||
nickName: String(p.nickName || p.id || '').trim()
|
||||
}))
|
||||
}
|
||||
|
||||
function personLines(item) {
|
||||
if (item?.kind === 'terminal' || item?.kind === 'submit') return []
|
||||
if (item?.taskType === 'cc' || item?.signMode === 'cc') return []
|
||||
const list = displayPeople(item)
|
||||
if (!list.length) return []
|
||||
return formatProgressPersonLines(list)
|
||||
}
|
||||
|
||||
function ccPersonLines(item) {
|
||||
return formatCcPersonLines(item)
|
||||
}
|
||||
|
||||
function showNodeComment(item) {
|
||||
// 有按人行时不再展示节点级拼接意见,避免双份
|
||||
if (personLines(item).length) return false
|
||||
// 撤销终态已在人员区展示「发起人撤销」,勿再套「意见:」
|
||||
if (item?.kind === 'terminal' && item?.status === 'cancelled') return false
|
||||
return Boolean(item?.commentText)
|
||||
}
|
||||
|
||||
function signBadge(item) {
|
||||
if (item?.taskType === 'cc' || item?.signMode === 'cc') return 'cc'
|
||||
if (item?.signMode === 'and') return 'and'
|
||||
if (item?.signMode === 'or') return 'or'
|
||||
return ''
|
||||
}
|
||||
|
||||
function signBadgeText(item) {
|
||||
const k = signBadge(item)
|
||||
if (k === 'cc') return '抄送'
|
||||
if (k === 'and') return '会签'
|
||||
if (k === 'or') return '或签'
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 任务节点 cancelled=系统清待办 →「无需处理」;终态 cancelled 仍为「已撤销」 */
|
||||
function nodeStatusText(item) {
|
||||
const s = String(item?.status || '').trim()
|
||||
if (item?.kind !== 'terminal' && s === 'cancelled') return '无需处理'
|
||||
return statusText(s)
|
||||
}
|
||||
|
||||
function nodeStatusTone(item) {
|
||||
const s = String(item?.status || '').trim()
|
||||
if (item?.kind !== 'terminal' && s === 'cancelled') return 'mute'
|
||||
return statusTone(s)
|
||||
}
|
||||
|
||||
function dotClass(item) {
|
||||
return statusTone(item?.status)
|
||||
return nodeStatusTone(item)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -82,6 +226,9 @@ function dotClass(item) {
|
||||
height: 10px;
|
||||
margin-top: 6px;
|
||||
border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
}
|
||||
.oa-progress-dot.ok {
|
||||
background: var(--brand-green, #22c55e);
|
||||
}
|
||||
.oa-progress-dot.warn {
|
||||
@@ -137,11 +284,81 @@ function dotClass(item) {
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.oa-progress-card__meta {
|
||||
margin-top: 4px;
|
||||
.oa-progress-badge.and {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.oa-progress-badge.or {
|
||||
color: #d97706;
|
||||
background: #fff7ed;
|
||||
}
|
||||
.oa-progress-badge.cc {
|
||||
color: #0891b2;
|
||||
background: #ecfeff;
|
||||
}
|
||||
.oa-person-lines {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.oa-person-block {
|
||||
min-width: 0;
|
||||
color: var(--text-primary, #0f291e);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.oa-person-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
.oa-person-line--fallback {
|
||||
margin-top: 6px;
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.oa-person-line__body {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.oa-person-line__time {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
.oa-person-line__opinion {
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
color: var(--text-primary, #0f291e);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
.oa-person-block.is-ok .oa-person-line__body {
|
||||
color: #059669;
|
||||
}
|
||||
.oa-person-block.is-pending .oa-person-line__body {
|
||||
color: #d97706;
|
||||
}
|
||||
.oa-person-block.is-danger .oa-person-line__body {
|
||||
color: #dc2626;
|
||||
}
|
||||
.oa-person-block.is-mute .oa-person-line__body {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.oa-progress-card__comment {
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
<van-field
|
||||
:model-value="summary"
|
||||
readonly
|
||||
is-link
|
||||
:is-link="!disabled"
|
||||
input-align="left"
|
||||
:disabled="disabled"
|
||||
placeholder="请选择收款账户"
|
||||
@click="openPicker"
|
||||
/>
|
||||
<div v-if="normalized.length" class="oa-recv__detail">
|
||||
<!-- 单账户已在 summary 展示完整行,避免详情/发起页重复两遍 -->
|
||||
<div v-if="normalized.length > 1" class="oa-recv__detail">
|
||||
<div v-for="item in normalized" :key="item.id" class="oa-recv__line">
|
||||
{{ formatReceiveAccountLine(item) }}
|
||||
</div>
|
||||
|
||||
@@ -12,19 +12,34 @@
|
||||
<div class="oa-rel__head">
|
||||
<div class="oa-rel__title">
|
||||
{{ item.unavailable ? '无法查看' : item.appName || '审批单' }}
|
||||
<template v-if="!item.unavailable && item.applyTime">
|
||||
· {{ item.applyTime }}
|
||||
</template>
|
||||
</div>
|
||||
<van-button
|
||||
v-if="!disabled"
|
||||
size="mini"
|
||||
type="danger"
|
||||
plain
|
||||
@click="removeId(item.id)"
|
||||
>
|
||||
删除
|
||||
</van-button>
|
||||
<div class="oa-rel__actions">
|
||||
<button
|
||||
v-if="!item.unavailable"
|
||||
type="button"
|
||||
class="oa-rel__icon-btn"
|
||||
aria-label="查看详情"
|
||||
@click.stop="goDetail(item)"
|
||||
>
|
||||
<van-icon name="eye-o" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!disabled"
|
||||
type="button"
|
||||
class="oa-rel__icon-btn is-danger"
|
||||
aria-label="删除"
|
||||
@click.stop="removeId(item.id)"
|
||||
>
|
||||
<van-icon name="delete-o" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!item.unavailable && item.applyTime"
|
||||
class="oa-rel__field"
|
||||
>
|
||||
<span class="oa-rel__label">申请时间</span>
|
||||
<span>{{ item.applyTime }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(f, idx) in (item.fields || []).slice(0, 3)"
|
||||
@@ -128,6 +143,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<OaInstanceDetailSheet v-model="detailVisible" :instance-id="detailInstanceId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -135,6 +152,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { fetchRelatedCandidates, fetchRelatedSummaries } from '../../api/oa.js'
|
||||
import OaInstanceDetailSheet from './OaInstanceDetailSheet.vue'
|
||||
|
||||
defineOptions({ name: 'relatedApproval' })
|
||||
|
||||
@@ -148,6 +166,8 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const visible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const detailInstanceId = ref('')
|
||||
const pickerLoading = ref(false)
|
||||
const keyword = ref('')
|
||||
const activeTab = ref('mine')
|
||||
@@ -194,6 +214,16 @@ function removeId(id) {
|
||||
emitValue(ids.value.filter((x) => x !== id))
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
const id = String(item?.id || '').trim()
|
||||
if (!id || item?.unavailable) {
|
||||
showToast('无法查看该审批单')
|
||||
return
|
||||
}
|
||||
detailInstanceId.value = id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (props.disabled) return
|
||||
pendingIds.value = []
|
||||
@@ -321,7 +351,8 @@ watch(visible, (v) => {
|
||||
|
||||
<style scoped>
|
||||
.oa-rel {
|
||||
padding: 4px 12px 8px;
|
||||
/* 与行程明细等块级控件一致:左右贴齐表单项内容区,卡片更宽 */
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
.oa-rel__empty {
|
||||
display: flex;
|
||||
@@ -348,10 +379,43 @@ watch(visible, (v) => {
|
||||
}
|
||||
.oa-rel__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #0f291e);
|
||||
}
|
||||
.oa-rel__actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.oa-rel__icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.oa-rel__icon-btn:active {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.oa-rel__icon-btn.is-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
.oa-rel__icon-btn.is-danger:active {
|
||||
background: #fef2f2;
|
||||
}
|
||||
.oa-rel__field {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<!-- OA 详情状态印章 · 对齐 web 打印钉钉风,移动端 72px -->
|
||||
<template>
|
||||
<div
|
||||
v-if="stampText"
|
||||
class="oa-stamp"
|
||||
:class="[stampClass, { 'is-chars3': stampText.length >= 3 }]"
|
||||
:aria-label="stampText"
|
||||
>
|
||||
<div class="oa-stamp__outer" />
|
||||
<div class="oa-stamp__arc oa-stamp__arc--top" />
|
||||
<div class="oa-stamp__arc oa-stamp__arc--bottom" />
|
||||
<div class="oa-stamp__label-top">OA审批</div>
|
||||
<div class="oa-stamp__main" :class="{ 'is-chars3': stampText.length >= 3 }">
|
||||
{{ stampText }}
|
||||
</div>
|
||||
<div class="oa-stamp__label-bottom">approval</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { resolveStamp } from '../oa-status-stamp.js'
|
||||
|
||||
const props = defineProps({
|
||||
status: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const resolved = computed(() => resolveStamp(props.status))
|
||||
const stampClass = computed(() => resolved.value.stampClass)
|
||||
const stampText = computed(() => resolved.value.stampText)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oa-stamp {
|
||||
--stamp: #3ec9a8;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
transform: rotate(-18deg);
|
||||
opacity: 0.88;
|
||||
color: var(--stamp);
|
||||
}
|
||||
.oa-stamp.ok {
|
||||
--stamp: #3ec9a8;
|
||||
}
|
||||
.oa-stamp.run {
|
||||
--stamp: #f0a020;
|
||||
}
|
||||
.oa-stamp.rej {
|
||||
--stamp: #e85d5d;
|
||||
}
|
||||
.oa-stamp.ret {
|
||||
--stamp: #e07070;
|
||||
}
|
||||
.oa-stamp.can {
|
||||
--stamp: #9aa0a6;
|
||||
}
|
||||
.oa-stamp__outer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 3px solid currentColor;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.oa-stamp__arc {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 20px;
|
||||
border: 2px solid currentColor;
|
||||
opacity: 0.95;
|
||||
pointer-events: none;
|
||||
}
|
||||
.oa-stamp__arc--top {
|
||||
top: 7px;
|
||||
border-bottom: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
|
||||
}
|
||||
.oa-stamp__arc--bottom {
|
||||
bottom: 7px;
|
||||
border-top: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-radius: 0 0 50% 50% / 0 0 100% 100%;
|
||||
}
|
||||
.oa-stamp__label-top {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 7px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1;
|
||||
color: currentColor;
|
||||
opacity: 0.95;
|
||||
}
|
||||
.oa-stamp__label-bottom {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 6px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1;
|
||||
color: currentColor;
|
||||
opacity: 0.9;
|
||||
font-family: Arial, 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
.oa-stamp__main {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1;
|
||||
color: currentColor;
|
||||
font-family:
|
||||
'PingFang SC',
|
||||
'Microsoft YaHei',
|
||||
'Helvetica Neue',
|
||||
Arial,
|
||||
sans-serif;
|
||||
-webkit-text-stroke: 0.3px currentColor;
|
||||
}
|
||||
.oa-stamp__main.is-chars3 {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,15 +4,15 @@
|
||||
<div v-for="(leg, idx) in inner" :key="idx" class="oa-trip__leg">
|
||||
<div class="oa-trip__head">
|
||||
<strong class="oa-trip__title">行程 {{ idx + 1 }}</strong>
|
||||
<van-button
|
||||
<button
|
||||
v-if="!disabled && inner.length > 1"
|
||||
size="mini"
|
||||
type="danger"
|
||||
plain
|
||||
type="button"
|
||||
class="oa-trip__icon-btn is-danger"
|
||||
aria-label="删除"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
删除
|
||||
</van-button>
|
||||
<van-icon name="delete-o" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
@@ -372,6 +372,36 @@ function onDateConfirm({ selectedValues }) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oa-trip__icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.oa-trip__icon-btn:active {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.oa-trip__icon-btn.is-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.oa-trip__icon-btn.is-danger:active {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.oa-trip__row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
@@ -1,15 +1,50 @@
|
||||
<template>
|
||||
<div class="oa-upload">
|
||||
<div class="oa-upload" :class="{ 'is-readonly': isReadonly }">
|
||||
<van-uploader
|
||||
v-model="fileList"
|
||||
:disabled="disabled"
|
||||
:disabled="false"
|
||||
:readonly="isReadonly"
|
||||
:deletable="!isReadonly"
|
||||
:max-count="limit"
|
||||
:accept="accept"
|
||||
:capture="captureAttr"
|
||||
:preview-size="84"
|
||||
:show-upload="!isReadonly"
|
||||
:after-read="onAfterRead"
|
||||
:before-delete="onBeforeDelete"
|
||||
multiple
|
||||
/>
|
||||
@click-preview="onClickPreview"
|
||||
>
|
||||
<div class="oa-upload__add">
|
||||
<div class="oa-upload__add-plus">+</div>
|
||||
<div class="oa-upload__add-txt">添加</div>
|
||||
</div>
|
||||
<template #preview-cover="slotProps">
|
||||
<div
|
||||
v-if="!isImageFileItem(slotProps)"
|
||||
class="oa-upload__file-cover"
|
||||
>
|
||||
<div
|
||||
class="oa-upload__file-ico"
|
||||
:class="'ft-' + fileKind(slotProps)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||||
<path d="M14 2v6h6" />
|
||||
<path v-if="fileKind(slotProps) === 'xls'" d="M8 13h8M8 17h5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="oa-upload__file-name">{{ shortDisplayName(slotProps) }}</div>
|
||||
<div class="oa-upload__dl-hint">点击下载</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-uploader>
|
||||
<div v-if="tipText" class="oa-upload__tip">
|
||||
{{ tipText }}
|
||||
</div>
|
||||
@@ -17,14 +52,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, useAttrs, watch } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { uploadAttachment } from '../../api/oa.js'
|
||||
import { attachmentFullUrl } from '../../utils/attachment.js'
|
||||
import { downloadAttachmentFile } from '../../utils/download-attachment.js'
|
||||
import { getToken } from '../../utils/auth.js'
|
||||
|
||||
defineOptions({ name: 'upload' })
|
||||
|
||||
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg|avif)(\?|$)/i
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, String, Object], default: () => [] },
|
||||
disabled: { type: Boolean, default: false },
|
||||
@@ -39,13 +77,97 @@ const props = defineProps({
|
||||
formCreateInject: { type: Object, default: null }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
const attrs = useAttrs()
|
||||
|
||||
/** 详情/只读:隐藏添加与删除,仅保留预览/下载 */
|
||||
const isReadonly = computed(() => {
|
||||
if (props.disabled) return true
|
||||
const a = attrs.disabled
|
||||
if (a === true || a === '' || a === 'true' || a === 1 || a === '1') return true
|
||||
const inj = props.formCreateInject
|
||||
if (inj?.api?.options?.form?.disabled) return true
|
||||
if (inj?.rule?.props?.disabled) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const captureAttr = computed(() => (props.capture === 'camera' ? 'camera' : undefined))
|
||||
const tipText = computed(() => {
|
||||
if (isReadonly.value) return ''
|
||||
if (!getToken()) return '未携带登录 token,上传可能失败'
|
||||
return ''
|
||||
})
|
||||
|
||||
/** url → 原始文件名(表单只存 URL,回显时靠此映射) */
|
||||
const nameByUrl = ref({})
|
||||
|
||||
function displayNameFromUrl(url) {
|
||||
const raw = String(url || '').trim()
|
||||
if (!raw) return ''
|
||||
try {
|
||||
const path = raw.split('?')[0] || raw
|
||||
const last = path.split('/').pop() || ''
|
||||
return decodeURIComponent(last) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function rememberName(url, name) {
|
||||
const u = String(url || '').trim()
|
||||
const n = String(name || '').trim()
|
||||
if (!u || !n) return
|
||||
nameByUrl.value = { ...nameByUrl.value, [u]: n }
|
||||
}
|
||||
|
||||
function resolveDisplayName(url, fallbackName) {
|
||||
const u = String(url || '').trim()
|
||||
return (
|
||||
(u && nameByUrl.value[u]) ||
|
||||
String(fallbackName || '').trim() ||
|
||||
displayNameFromUrl(u) ||
|
||||
'未命名文件'
|
||||
)
|
||||
}
|
||||
|
||||
function isImageFileItem(item) {
|
||||
if (!item) return false
|
||||
if (item.isImage === true) return true
|
||||
if (item.file && item.file.type && String(item.file.type).startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
const name = String((item.file && item.file.name) || item.name || '')
|
||||
if (IMAGE_REGEXP.test(name)) return true
|
||||
return IMAGE_REGEXP.test(String(item.url || item.content || ''))
|
||||
}
|
||||
|
||||
function itemName(item) {
|
||||
const url = String(item?.url || '').trim()
|
||||
return resolveDisplayName(url, (item?.file && item.file.name) || item?.name)
|
||||
}
|
||||
|
||||
function shortDisplayName(item) {
|
||||
const name = itemName(item)
|
||||
if (name.length <= 12) return name
|
||||
const i = name.lastIndexOf('.')
|
||||
if (i > 0 && name.length - i <= 5) {
|
||||
const ext = name.slice(i)
|
||||
const base = name.slice(0, i)
|
||||
const keep = Math.max(4, 10 - ext.length)
|
||||
return base.slice(0, keep) + '…' + ext
|
||||
}
|
||||
return name.slice(0, 10) + '…'
|
||||
}
|
||||
|
||||
/** 仅用于类型色图标,不展示「PDF」等文案 */
|
||||
function fileKind(item) {
|
||||
const n = String(itemName(item) || item?.url || '').toLowerCase()
|
||||
if (n.includes('.pdf')) return 'pdf'
|
||||
if (/\.(xls|xlsx|csv)(\?|$)/.test(n) || n.includes('.xls')) return 'xls'
|
||||
if (/\.(doc|docx)(\?|$)/.test(n) || n.includes('.doc')) return 'doc'
|
||||
if (/\.(zip|rar|7z)(\?|$)/.test(n)) return 'zip'
|
||||
return 'other'
|
||||
}
|
||||
|
||||
function toUrlList(raw) {
|
||||
if (raw == null || raw === '') return []
|
||||
if (Array.isArray(raw)) {
|
||||
@@ -53,7 +175,10 @@ function toUrlList(raw) {
|
||||
.map((x) => {
|
||||
if (typeof x === 'string') return attachmentFullUrl(x) || x
|
||||
if (x && typeof x === 'object') {
|
||||
return attachmentFullUrl(String(x.url || x.fileUrl || '')) || ''
|
||||
const u = attachmentFullUrl(String(x.url || x.fileUrl || '')) || ''
|
||||
const n = String(x.name || x.fileName || '').trim()
|
||||
if (u && n) rememberName(u, n)
|
||||
return u
|
||||
}
|
||||
return ''
|
||||
})
|
||||
@@ -65,18 +190,28 @@ function toUrlList(raw) {
|
||||
}
|
||||
if (typeof raw === 'object') {
|
||||
const u = attachmentFullUrl(String(raw.url || raw.fileUrl || '')) || ''
|
||||
const n = String(raw.name || raw.fileName || '').trim()
|
||||
if (u && n) rememberName(u, n)
|
||||
return u ? [u] : []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Vant 非图片预览文案:item.file ? item.file.name : item.url
|
||||
* 回显只有 url 时必须补 file.name,否则会露出整段 http 地址。
|
||||
*/
|
||||
function urlsToFileList(urls) {
|
||||
return urls.map((url, i) => ({
|
||||
url,
|
||||
name: url.split('/').pop() || `file-${i}`,
|
||||
status: 'done',
|
||||
isString: true
|
||||
}))
|
||||
return urls.map((url, i) => {
|
||||
const name = resolveDisplayName(url, '') || `file-${i}`
|
||||
return {
|
||||
url,
|
||||
name,
|
||||
file: { name },
|
||||
status: 'done',
|
||||
isString: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fileList = ref(urlsToFileList(toUrlList(props.modelValue)))
|
||||
@@ -114,8 +249,18 @@ async function onAfterRead(item) {
|
||||
const res = await uploadAttachment(f.file)
|
||||
const rawUrl = String(res?.fileUrl || res?.data?.fileUrl || '').trim()
|
||||
if (!rawUrl) throw new Error('未返回文件地址')
|
||||
// 与 web-admin oa-form-create-upload 一致:表单存完整 URL,便于两端回显
|
||||
f.url = attachmentFullUrl(rawUrl) || rawUrl
|
||||
const fullUrl = attachmentFullUrl(rawUrl) || rawUrl
|
||||
const originalName = String(
|
||||
(f.file && f.file.name) ||
|
||||
res?.fileName ||
|
||||
res?.data?.fileName ||
|
||||
''
|
||||
).trim()
|
||||
f.url = fullUrl
|
||||
const showName = originalName || displayNameFromUrl(fullUrl) || '未命名文件'
|
||||
rememberName(fullUrl, showName)
|
||||
rememberName(rawUrl, showName)
|
||||
f.name = showName
|
||||
f.status = 'done'
|
||||
f.isString = true
|
||||
} catch (e) {
|
||||
@@ -131,6 +276,25 @@ function onBeforeDelete() {
|
||||
setTimeout(() => emitUrls(), 0)
|
||||
return true
|
||||
}
|
||||
|
||||
async function onClickPreview(item) {
|
||||
if (isImageFileItem(item)) return
|
||||
const url = String(item?.url || '').trim()
|
||||
if (!url) {
|
||||
showToast('文件地址无效')
|
||||
return
|
||||
}
|
||||
const fileName = resolveDisplayName(url, (item.file && item.file.name) || item.name)
|
||||
try {
|
||||
showToast({ message: '开始下载…', duration: 800 })
|
||||
await downloadAttachmentFile({
|
||||
fileUrl: url,
|
||||
fileName
|
||||
})
|
||||
} catch (e) {
|
||||
showToast((e && e.message) || '下载失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -138,9 +302,196 @@ function onBeforeDelete() {
|
||||
padding: 4px 12px 8px;
|
||||
}
|
||||
.oa-upload__tip {
|
||||
margin-top: 4px;
|
||||
margin-top: 6px;
|
||||
color: var(--text-muted, #6b7b74);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 预览格 */
|
||||
.oa-upload :deep(.van-uploader__wrapper) {
|
||||
gap: 0;
|
||||
}
|
||||
.oa-upload :deep(.van-uploader__preview) {
|
||||
margin: 0 10px 10px 0;
|
||||
}
|
||||
.oa-upload :deep(.van-uploader__preview-image),
|
||||
.oa-upload :deep(.van-uploader__file) {
|
||||
width: 84px !important;
|
||||
height: 84px !important;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e4eee9;
|
||||
box-shadow: 0 4px 12px rgba(20, 55, 40, 0.06);
|
||||
background: #fff;
|
||||
}
|
||||
.oa-upload :deep(.van-uploader__file) {
|
||||
padding: 0;
|
||||
}
|
||||
.oa-upload :deep(.van-uploader__file-icon),
|
||||
.oa-upload :deep(.van-uploader__file-name) {
|
||||
/* 由 preview-cover 自定义展示 */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 删除:圆形半透明 */
|
||||
.oa-upload :deep(.van-uploader__preview-delete) {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.oa-upload :deep(.van-uploader__preview-delete-icon) {
|
||||
position: static;
|
||||
transform: none;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 非图:自定义封面(图标 + 文件名 + 下载条) */
|
||||
.oa-upload__file-cover {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 6px 20px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
.oa-upload__file-ico {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
box-shadow: 0 3px 8px rgba(15, 40, 30, 0.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.oa-upload__file-ico svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
.oa-upload__file-ico.ft-pdf {
|
||||
background: linear-gradient(145deg, #f07171, #ef4444);
|
||||
}
|
||||
.oa-upload__file-ico.ft-xls {
|
||||
background: linear-gradient(145deg, #4fbf8a, #10b981);
|
||||
}
|
||||
.oa-upload__file-ico.ft-doc {
|
||||
background: linear-gradient(145deg, #6ea8ff, #3b82f6);
|
||||
}
|
||||
.oa-upload__file-ico.ft-zip {
|
||||
background: linear-gradient(145deg, #e4bc6a, #d6a343);
|
||||
}
|
||||
.oa-upload__file-ico.ft-other {
|
||||
background: linear-gradient(145deg, #4ade80, #0a9f6d);
|
||||
}
|
||||
.oa-upload__file-name {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #4d635a;
|
||||
text-align: center;
|
||||
line-height: 1.25;
|
||||
max-height: 26px;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.oa-upload__dl-hint {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 4px 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, rgba(8, 127, 87, 0), rgba(8, 127, 87, 0.92));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 添加格 */
|
||||
.oa-upload__add {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 14px;
|
||||
border: 1.5px dashed #9ad9be;
|
||||
background: linear-gradient(145deg, #f7fdf9, #e9f9f2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.oa-upload__add:active {
|
||||
border-color: #0a9f6d;
|
||||
}
|
||||
.oa-upload__add-plus {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(145deg, #4ade80, #0a9f6d);
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
box-shadow: 0 4px 10px rgba(10, 159, 109, 0.28);
|
||||
}
|
||||
.oa-upload__add-txt {
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
color: #087f57;
|
||||
}
|
||||
|
||||
/* 默认上传按钮兜底样式(编辑态) */
|
||||
.oa-upload :deep(.van-uploader__upload) {
|
||||
margin: 0 10px 10px 0;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(145deg, #f7fdf9, #e9f9f2);
|
||||
border: 1.5px dashed #9ad9be;
|
||||
}
|
||||
.oa-upload :deep(.van-uploader__upload-icon) {
|
||||
color: #0a9f6d;
|
||||
}
|
||||
|
||||
/* 详情只读:不展示添加/删除,且取消 disabled 灰蒙层 */
|
||||
.oa-upload.is-readonly {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.oa-upload.is-readonly :deep(.van-uploader__wrapper--disabled),
|
||||
.oa-upload.is-readonly :deep(.van-uploader__wrapper) {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.oa-upload.is-readonly :deep(.van-uploader__upload),
|
||||
.oa-upload.is-readonly :deep(.oa-upload__add) {
|
||||
display: none !important;
|
||||
}
|
||||
.oa-upload.is-readonly :deep(.van-uploader__preview-delete) {
|
||||
display: none !important;
|
||||
}
|
||||
.oa-upload.is-readonly :deep(.van-uploader__mask) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
/** 与后端 oa_engine.INITIATOR_CONDITION_FIELD 对齐:不依赖 formData */
|
||||
const INITIATOR_CONDITION_FIELD = '__initiator__'
|
||||
const FORMULA_FIELD_RE = /\{([^{}]+)\}/g
|
||||
|
||||
function nodeType(node) {
|
||||
return String((node && (node.type || node.nodeType)) || '').trim()
|
||||
}
|
||||
|
||||
function addField(name, fields, seen) {
|
||||
const key = String(name || '').trim()
|
||||
if (!key || seen.has(key)) return
|
||||
if (key === INITIATOR_CONDITION_FIELD) return
|
||||
seen.add(key)
|
||||
fields.push(key)
|
||||
}
|
||||
|
||||
function addFormulaFields(text, fields, seen) {
|
||||
if (!text) return
|
||||
FORMULA_FIELD_RE.lastIndex = 0
|
||||
let m
|
||||
while ((m = FORMULA_FIELD_RE.exec(String(text)))) {
|
||||
addField(m[1], fields, seen)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集流程树中非默认条件分支引用的表单字段(去重保序)。
|
||||
* 语义对齐 backend `oa_engine._collect_condition_fields`。
|
||||
*/
|
||||
export function collectConditionFields(node) {
|
||||
const fields = []
|
||||
const seen = new Set()
|
||||
|
||||
function walk(n) {
|
||||
if (!n || typeof n !== 'object') return
|
||||
if (nodeType(n) === 'condition') {
|
||||
for (const g of n.conditionGroups || []) {
|
||||
if (!g || typeof g !== 'object') continue
|
||||
const rules = g.rules
|
||||
const when = g.when
|
||||
if (g.isDefault || (!rules && !when)) {
|
||||
if (g.child && typeof g.child === 'object') walk(g.child)
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(rules) && rules.length) {
|
||||
for (const r of rules) {
|
||||
if (r && typeof r === 'object' && r.field != null) {
|
||||
addField(r.field, fields, seen)
|
||||
}
|
||||
}
|
||||
} else if (typeof rules === 'string' && rules.trim()) {
|
||||
addFormulaFields(rules, fields, seen)
|
||||
} else if (when) {
|
||||
addFormulaFields(when, fields, seen)
|
||||
}
|
||||
if (g.child && typeof g.child === 'object') walk(g.child)
|
||||
}
|
||||
if (n.child && typeof n.child === 'object') walk(n.child)
|
||||
return
|
||||
}
|
||||
if (n.child && typeof n.child === 'object') walk(n.child)
|
||||
for (const g of n.conditionGroups || []) {
|
||||
if (g && typeof g === 'object' && g.child && typeof g.child === 'object') {
|
||||
walk(g.child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(node)
|
||||
return fields
|
||||
}
|
||||
|
||||
function sortKeysDeep(value) {
|
||||
if (Array.isArray(value)) return value.map(sortKeysDeep)
|
||||
if (value && typeof value === 'object') {
|
||||
const out = {}
|
||||
for (const k of Object.keys(value).sort()) {
|
||||
out[k] = sortKeysDeep(value[k])
|
||||
}
|
||||
return out
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function pickRelevantFormData(formData, conditionFields) {
|
||||
const data = formData && typeof formData === 'object' ? formData : {}
|
||||
const fields = Array.isArray(conditionFields) ? conditionFields : []
|
||||
const out = {}
|
||||
for (const f of fields) {
|
||||
out[f] = data[f]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 稳定序列化:用于判断预览请求是否实质变化 */
|
||||
export function buildFlowPreviewRequestKey({
|
||||
formData,
|
||||
initiatorSelectByNode,
|
||||
conditionFields
|
||||
} = {}) {
|
||||
const payload = {
|
||||
formData: pickRelevantFormData(formData, conditionFields),
|
||||
initiatorSelectByNode:
|
||||
initiatorSelectByNode && typeof initiatorSelectByNode === 'object'
|
||||
? initiatorSelectByNode
|
||||
: {}
|
||||
}
|
||||
return JSON.stringify(sortKeysDeep(payload))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null|undefined} prevKey 上次成功预览的 key
|
||||
* @param {string} nextKey 本次请求 key
|
||||
* @param {{ inFlightKey?: string|null }} [opts] 在途请求 key;有在途时仅跳过相同 key,避免回滚竞态
|
||||
*/
|
||||
export function shouldSkipFlowPreviewRequest(prevKey, nextKey, opts = {}) {
|
||||
const inFlightKey = opts.inFlightKey
|
||||
if (inFlightKey != null) {
|
||||
return inFlightKey === nextKey
|
||||
}
|
||||
return prevKey != null && prevKey === nextKey
|
||||
}
|
||||
|
||||
/** 已有预览结果时静默刷新,避免整区「加载中」闪烁 */
|
||||
export function shouldShowFlowPreviewSpinner({ hasCompletedPreview } = {}) {
|
||||
return !hasCompletedPreview
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
collectConditionFields,
|
||||
buildFlowPreviewRequestKey,
|
||||
shouldSkipFlowPreviewRequest,
|
||||
shouldShowFlowPreviewSpinner
|
||||
} from './oa-flow-preview-refresh.js'
|
||||
|
||||
// 无条件分支:表单无关字段变化不应改变 request key
|
||||
{
|
||||
const keyA = buildFlowPreviewRequestKey({
|
||||
formData: { title: 'a', reason: 'x' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: []
|
||||
})
|
||||
const keyB = buildFlowPreviewRequestKey({
|
||||
formData: { title: 'b', reason: 'y' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: []
|
||||
})
|
||||
assert.equal(keyA, keyB)
|
||||
assert.equal(shouldSkipFlowPreviewRequest(keyA, keyB), true)
|
||||
}
|
||||
|
||||
// 有条件字段:仅条件字段变化才改变 key
|
||||
{
|
||||
const fields = ['amount']
|
||||
const keyA = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 100, remark: 'old' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: fields
|
||||
})
|
||||
const keySame = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 100, remark: 'new' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: fields
|
||||
})
|
||||
const keyDiff = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 200, remark: 'new' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: fields
|
||||
})
|
||||
assert.equal(keyA, keySame)
|
||||
assert.notEqual(keyA, keyDiff)
|
||||
assert.equal(shouldSkipFlowPreviewRequest(keyA, keySame), true)
|
||||
assert.equal(shouldSkipFlowPreviewRequest(keyA, keyDiff), false)
|
||||
}
|
||||
|
||||
// 自选审批人变化应触发预览
|
||||
{
|
||||
const keyA = buildFlowPreviewRequestKey({
|
||||
formData: {},
|
||||
initiatorSelectByNode: { n1: ['u1'] },
|
||||
conditionFields: []
|
||||
})
|
||||
const keyB = buildFlowPreviewRequestKey({
|
||||
formData: {},
|
||||
initiatorSelectByNode: { n1: ['u2'] },
|
||||
conditionFields: []
|
||||
})
|
||||
assert.notEqual(keyA, keyB)
|
||||
}
|
||||
|
||||
// 从 flowTree 收集条件字段(对齐后端语义)
|
||||
{
|
||||
const tree = {
|
||||
type: 'start',
|
||||
child: {
|
||||
type: 'condition',
|
||||
conditionGroups: [
|
||||
{
|
||||
rules: [{ field: 'leaveType', op: 'eq', value: '事假' }],
|
||||
child: { type: 'approver', id: 'a1' }
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
child: { type: 'approver', id: 'a2' }
|
||||
}
|
||||
],
|
||||
child: { type: 'end' }
|
||||
}
|
||||
}
|
||||
assert.deepEqual(collectConditionFields(tree), ['leaveType'])
|
||||
}
|
||||
|
||||
// 公式字段 {fieldName}
|
||||
{
|
||||
const tree = {
|
||||
type: 'condition',
|
||||
conditionGroups: [
|
||||
{
|
||||
when: '{days} > 3',
|
||||
child: { type: 'approver' }
|
||||
}
|
||||
]
|
||||
}
|
||||
assert.deepEqual(collectConditionFields(tree), ['days'])
|
||||
}
|
||||
|
||||
// 系统伪字段 __initiator__ 不纳入
|
||||
{
|
||||
const tree = {
|
||||
type: 'condition',
|
||||
conditionGroups: [
|
||||
{
|
||||
rules: [{ field: '__initiator__', op: 'eq', value: 'x' }],
|
||||
child: { type: 'approver' }
|
||||
}
|
||||
]
|
||||
}
|
||||
assert.deepEqual(collectConditionFields(tree), [])
|
||||
}
|
||||
|
||||
// 仅首次预览展示整区 loading
|
||||
assert.equal(shouldShowFlowPreviewSpinner({ hasCompletedPreview: false }), true)
|
||||
assert.equal(shouldShowFlowPreviewSpinner({ hasCompletedPreview: true }), false)
|
||||
|
||||
// 有在途请求时:相同 key 跳过,不同 key(含回滚到上次成功值)必须再请求
|
||||
{
|
||||
const key100 = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 100 },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: ['amount']
|
||||
})
|
||||
const key200 = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 200 },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: ['amount']
|
||||
})
|
||||
assert.equal(
|
||||
shouldSkipFlowPreviewRequest(key100, key200, { inFlightKey: key200 }),
|
||||
true
|
||||
)
|
||||
assert.equal(
|
||||
shouldSkipFlowPreviewRequest(key100, key100, { inFlightKey: key200 }),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
console.log('oa-flow-preview-refresh.test.js ok')
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 或签/会签进度人员行格式化(对齐 web-admin oa-progress-person-lines)
|
||||
* Spec: docs/superpowers/specs/2026-07-23-oa-or-and-sign-display-design.md
|
||||
*
|
||||
* 返回字段:
|
||||
* - name / detail / timestamp:移动端分行展示(姓名+时间同行,意见单独一行)
|
||||
* - body / text:兼容旧纯文本拼接
|
||||
*/
|
||||
|
||||
const ACTED_STATUS = new Set(['approved', 'rejected', 'returned', 'completed', 'transferred'])
|
||||
|
||||
function displayName(p) {
|
||||
return String(p?.nickName || p?.id || '').trim()
|
||||
}
|
||||
|
||||
function transferTargetName(p) {
|
||||
return String(p?.transferToUserName || p?.transferToUserId || '').trim()
|
||||
}
|
||||
|
||||
function statusFallbackLabel(status) {
|
||||
const s = (status || '').trim()
|
||||
if (s === 'approved') return '已同意'
|
||||
if (s === 'rejected') return '已拒绝'
|
||||
if (s === 'returned') return '已打回'
|
||||
if (s === 'completed') return '已完成'
|
||||
if (s === 'transferred') return '已转交'
|
||||
return s || '已处理'
|
||||
}
|
||||
|
||||
function toneForStatus(status) {
|
||||
const s = (status || '').trim()
|
||||
if (s === 'approved' || s === 'completed') return 'ok'
|
||||
if (s === 'pending' || s === 'running' || s === 'not_reached') return 'pending'
|
||||
if (s === 'rejected' || s === 'returned') return 'danger'
|
||||
if (s === 'done' || s === 'transferred' || s === 'cancelled') return 'mute'
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} out
|
||||
* @param {string} name
|
||||
* @param {string} detail 意见/状态文案(可空)
|
||||
* @param {string} timestamp
|
||||
* @param {string} tone
|
||||
* @param {{ isOpinion?: boolean }} [opts] isOpinion=true 时移动端加「意见:」前缀
|
||||
*/
|
||||
function pushLine(out, name, detail, timestamp, tone, opts = {}) {
|
||||
const n = String(name || '').trim()
|
||||
const d = String(detail || '').trim()
|
||||
const ts = String(timestamp || '').trim()
|
||||
const body = d ? `${n} ${d}` : n
|
||||
out.push({
|
||||
name: n,
|
||||
detail: d,
|
||||
isOpinion: !!opts.isOpinion && !!d,
|
||||
body,
|
||||
timestamp: ts,
|
||||
text: ts ? `${body} ${ts}` : body,
|
||||
tone
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 抄送节点人员行:仅姓名 + 时间,样式统一(不套审批态文案/tone)。
|
||||
* @param {{ taskType?: string, signMode?: string, people?: Array, assignee?: string, timestamp?: string }} item
|
||||
*/
|
||||
export function formatCcPersonLines(item) {
|
||||
if (item?.taskType !== 'cc' && item?.signMode !== 'cc') return []
|
||||
const fallbackTs = String(item?.timestamp || '').trim()
|
||||
const people = Array.isArray(item?.people) ? item.people : []
|
||||
const fromPeople = []
|
||||
for (const p of people) {
|
||||
const name = displayName(p)
|
||||
if (!name) continue
|
||||
fromPeople.push({
|
||||
name,
|
||||
timestamp: String(p?.timestamp || '').trim() || fallbackTs
|
||||
})
|
||||
}
|
||||
if (fromPeople.length) return fromPeople
|
||||
const assignee = String(item?.assignee || '').trim()
|
||||
if (!assignee) return []
|
||||
return [{ name: assignee, timestamp: fallbackTs }]
|
||||
}
|
||||
|
||||
/** 将 progress.people 格式化为人员行 */
|
||||
export function formatProgressPersonLines(people) {
|
||||
const list = Array.isArray(people) ? people : []
|
||||
const out = []
|
||||
for (const p of list) {
|
||||
const name = displayName(p)
|
||||
if (!name) continue
|
||||
const status = String(p.status || '').trim()
|
||||
const comment = String(p.commentText || '').trim()
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const tone = toneForStatus(status)
|
||||
|
||||
if (status === 'done' || status === 'cancelled') {
|
||||
pushLine(out, name, '无需处理', '', 'mute')
|
||||
continue
|
||||
}
|
||||
if (status === 'transferred') {
|
||||
const target = transferTargetName(p)
|
||||
const mid = target
|
||||
? comment
|
||||
? `转交给${target} ${comment}`
|
||||
: `转交给${target}`
|
||||
: comment || statusFallbackLabel('transferred')
|
||||
pushLine(out, name, mid, ts, 'mute', { isOpinion: !!comment && !target })
|
||||
continue
|
||||
}
|
||||
if (status === 'pending' || status === 'running' || status === 'not_reached' || !status) {
|
||||
if (!status && (comment || ts)) {
|
||||
const mid = comment || statusFallbackLabel('approved')
|
||||
pushLine(out, name, mid, ts, 'ok', { isOpinion: !!comment })
|
||||
continue
|
||||
}
|
||||
pushLine(out, name, '待处理', '', 'pending')
|
||||
continue
|
||||
}
|
||||
if (ACTED_STATUS.has(status) || comment || ts) {
|
||||
const mid = comment || statusFallbackLabel(status)
|
||||
pushLine(out, name, mid, ts, tone || 'ok', { isOpinion: !!comment })
|
||||
continue
|
||||
}
|
||||
pushLine(out, name, '待处理', '', 'pending')
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
formatCcPersonLines,
|
||||
formatProgressPersonLines
|
||||
} from './oa-progress-person-lines.js'
|
||||
|
||||
const andPartial = formatProgressPersonLines([
|
||||
{
|
||||
nickName: '吴家猛',
|
||||
status: 'approved',
|
||||
commentText: '同意',
|
||||
timestamp: '2026-08-10 14:17:19'
|
||||
},
|
||||
{
|
||||
nickName: '郑德禄',
|
||||
status: 'approved',
|
||||
commentText: '同意',
|
||||
timestamp: '2026-08-10 14:18:00'
|
||||
},
|
||||
{ nickName: '丙', status: 'pending' }
|
||||
])
|
||||
assert.deepEqual(
|
||||
andPartial.map((x) => ({
|
||||
name: x.name,
|
||||
detail: x.detail,
|
||||
timestamp: x.timestamp,
|
||||
isOpinion: x.isOpinion,
|
||||
tone: x.tone
|
||||
})),
|
||||
[
|
||||
{
|
||||
name: '吴家猛',
|
||||
detail: '同意',
|
||||
timestamp: '2026-08-10 14:17:19',
|
||||
isOpinion: true,
|
||||
tone: 'ok'
|
||||
},
|
||||
{
|
||||
name: '郑德禄',
|
||||
detail: '同意',
|
||||
timestamp: '2026-08-10 14:18:00',
|
||||
isOpinion: true,
|
||||
tone: 'ok'
|
||||
},
|
||||
{
|
||||
name: '丙',
|
||||
detail: '待处理',
|
||||
timestamp: '',
|
||||
isOpinion: false,
|
||||
tone: 'pending'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
const notReached = formatProgressPersonLines([
|
||||
{ nickName: '张三', status: 'not_reached' },
|
||||
{ nickName: '李四', status: 'not_reached' }
|
||||
])
|
||||
assert.deepEqual(
|
||||
notReached.map((x) => x.body),
|
||||
['张三 待处理', '李四 待处理']
|
||||
)
|
||||
|
||||
const orDone = formatProgressPersonLines([
|
||||
{
|
||||
nickName: '甲',
|
||||
status: 'approved',
|
||||
commentText: '同意,材料齐全',
|
||||
timestamp: '2026-07-23 10:05:00'
|
||||
},
|
||||
{ nickName: '乙', status: 'done' },
|
||||
{ nickName: '丙', status: 'done' }
|
||||
])
|
||||
assert.deepEqual(
|
||||
orDone.map((x) => x.text),
|
||||
['甲 同意,材料齐全 2026-07-23 10:05:00', '乙 无需处理', '丙 无需处理']
|
||||
)
|
||||
assert.equal(orDone[0].isOpinion, true)
|
||||
|
||||
const emptyComment = formatProgressPersonLines([
|
||||
{
|
||||
nickName: '甲',
|
||||
status: 'approved',
|
||||
commentText: '',
|
||||
timestamp: '2026-07-23 10:05:00'
|
||||
}
|
||||
])
|
||||
assert.equal(emptyComment[0].name, '甲')
|
||||
assert.equal(emptyComment[0].detail, '已同意')
|
||||
assert.equal(emptyComment[0].isOpinion, false)
|
||||
|
||||
// 撤销/打回清待办:cancelled 对齐或签 done →「无需处理」(勿挂「已撤销」)
|
||||
const cancelledTask = formatProgressPersonLines([
|
||||
{
|
||||
nickName: '直接主管张三',
|
||||
status: 'cancelled',
|
||||
commentText: '',
|
||||
timestamp: '2026-08-13 12:47:46'
|
||||
}
|
||||
])
|
||||
assert.deepEqual(
|
||||
cancelledTask.map((x) => ({
|
||||
name: x.name,
|
||||
detail: x.detail,
|
||||
timestamp: x.timestamp,
|
||||
isOpinion: x.isOpinion,
|
||||
tone: x.tone
|
||||
})),
|
||||
[
|
||||
{
|
||||
name: '直接主管张三',
|
||||
detail: '无需处理',
|
||||
timestamp: '',
|
||||
isOpinion: false,
|
||||
tone: 'mute'
|
||||
}
|
||||
]
|
||||
)
|
||||
const cancelledNoTs = formatProgressPersonLines([{ nickName: '李四', status: 'cancelled' }])
|
||||
assert.equal(cancelledNoTs[0].detail, '无需处理')
|
||||
assert.equal(cancelledNoTs[0].tone, 'mute')
|
||||
|
||||
// 抄送:多人只出统一姓名行(无「无需处理」、无审批态 tone)
|
||||
const ccMulti = formatCcPersonLines({
|
||||
taskType: 'cc',
|
||||
timestamp: '2026-08-10 14:27:37',
|
||||
people: [
|
||||
{
|
||||
nickName: '胡倩茜',
|
||||
status: 'done',
|
||||
timestamp: '2026-08-10 14:27:37'
|
||||
},
|
||||
{ nickName: '周斌倩', status: 'done', timestamp: '2026-08-10 14:27:37' }
|
||||
]
|
||||
})
|
||||
assert.deepEqual(ccMulti, [
|
||||
{ name: '胡倩茜', timestamp: '2026-08-10 14:27:37' },
|
||||
{ name: '周斌倩', timestamp: '2026-08-10 14:27:37' }
|
||||
])
|
||||
assert.ok(ccMulti.every((x) => x.tone === undefined))
|
||||
|
||||
const ccSingle = formatCcPersonLines({
|
||||
taskType: 'cc',
|
||||
assignee: '胡圆凯',
|
||||
timestamp: '2026-08-10 14:30:04',
|
||||
people: [{ nickName: '胡圆凯', status: 'done', timestamp: '2026-08-10 14:30:04' }]
|
||||
})
|
||||
assert.deepEqual(ccSingle, [{ name: '胡圆凯', timestamp: '2026-08-10 14:30:04' }])
|
||||
|
||||
const ccAssigneeOnly = formatCcPersonLines({
|
||||
taskType: 'cc',
|
||||
assignee: '张三',
|
||||
timestamp: '2026-08-10 15:00:00',
|
||||
people: []
|
||||
})
|
||||
assert.deepEqual(ccAssigneeOnly, [{ name: '张三', timestamp: '2026-08-10 15:00:00' }])
|
||||
|
||||
assert.deepEqual(formatCcPersonLines({ taskType: 'approval', people: [{ nickName: '甲' }] }), [])
|
||||
|
||||
console.log('oa-progress-person-lines: all assertions passed')
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* OA 审批状态 → 钉钉风印章文案/色类(与 web-admin resolveStamp 对齐)
|
||||
* @param {string|null|undefined} status
|
||||
* @returns {{ stampClass: string, stampText: string }}
|
||||
*/
|
||||
export function resolveStamp(status) {
|
||||
const s = String(status || '').trim()
|
||||
if (s === 'completed' || s === 'approved' || s === 'done') {
|
||||
return { stampClass: 'ok', stampText: '已通过' }
|
||||
}
|
||||
if (s === 'rejected') return { stampClass: 'rej', stampText: '已拒绝' }
|
||||
if (s === 'returned') return { stampClass: 'ret', stampText: '已打回' }
|
||||
if (s === 'cancelled') return { stampClass: 'can', stampText: '已撤销' }
|
||||
if (s === 'running' || s === 'pending') {
|
||||
return { stampClass: 'run', stampText: '审批中' }
|
||||
}
|
||||
return { stampClass: 'run', stampText: '审批中' }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { resolveStamp } from './oa-status-stamp.js'
|
||||
|
||||
assert.deepEqual(resolveStamp('completed'), {
|
||||
stampClass: 'ok',
|
||||
stampText: '已通过'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('approved'), {
|
||||
stampClass: 'ok',
|
||||
stampText: '已通过'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('done'), {
|
||||
stampClass: 'ok',
|
||||
stampText: '已通过'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('rejected'), {
|
||||
stampClass: 'rej',
|
||||
stampText: '已拒绝'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('returned'), {
|
||||
stampClass: 'ret',
|
||||
stampText: '已打回'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('cancelled'), {
|
||||
stampClass: 'can',
|
||||
stampText: '已撤销'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('running'), {
|
||||
stampClass: 'run',
|
||||
stampText: '审批中'
|
||||
})
|
||||
assert.deepEqual(resolveStamp('pending'), {
|
||||
stampClass: 'run',
|
||||
stampText: '审批中'
|
||||
})
|
||||
assert.deepEqual(resolveStamp(''), {
|
||||
stampClass: 'run',
|
||||
stampText: '审批中'
|
||||
})
|
||||
assert.deepEqual(resolveStamp(null), {
|
||||
stampClass: 'run',
|
||||
stampText: '审批中'
|
||||
})
|
||||
|
||||
console.log('oa-status-stamp: all assertions passed')
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
normalizeReceiveAccountModelValue
|
||||
} from '../../utils/receive-account-value.js'
|
||||
import { toRmbUpper } from '../../utils/rmb-upper.js'
|
||||
import { OA_STATUS_TEXT } from '../status-label.js'
|
||||
import { resolveStamp } from '../oa-status-stamp.js'
|
||||
import { buildOaPrintFilename } from './oa-print-filename.js'
|
||||
import { buildFlowItems } from './oa-print-flow.js'
|
||||
|
||||
export { resolveStamp }
|
||||
|
||||
@@ -62,9 +62,6 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function statusText(s) {
|
||||
return OA_STATUS_TEXT[s] || s || '-'
|
||||
}
|
||||
|
||||
function formatDateOnly(createTime) {
|
||||
const raw = String(createTime || '').trim()
|
||||
@@ -367,171 +364,7 @@ function buildFormRows(detail) {
|
||||
return out
|
||||
}
|
||||
|
||||
function peopleDisplayNames(p) {
|
||||
const people = Array.isArray(p?.people) ? p.people : []
|
||||
const fromPeople = people
|
||||
.map((x) => String(x?.nickName || x?.id || '').trim())
|
||||
.filter(Boolean)
|
||||
if (fromPeople.length) return fromPeople
|
||||
const name = String(p?.assigneeUserName || p?.assigneeUserId || '').trim()
|
||||
return name ? [name] : []
|
||||
}
|
||||
|
||||
function formatProgressPersonLines(people) {
|
||||
const list = Array.isArray(people) ? people : []
|
||||
const out = []
|
||||
const ACTED = new Set(['approved', 'rejected', 'returned', 'completed', 'transferred'])
|
||||
for (const p of list) {
|
||||
const name = String(p?.nickName || p?.id || '').trim()
|
||||
if (!name) continue
|
||||
const status = String(p.status || '').trim()
|
||||
const comment = String(p.commentText || '').trim()
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const transferTo = String(p.transferToUserName || p.transferToUserId || '').trim()
|
||||
|
||||
if (status === 'done') {
|
||||
out.push({ left: `${name} 无需处理`, right: '' })
|
||||
continue
|
||||
}
|
||||
if (status === 'transferred') {
|
||||
const mid = transferTo
|
||||
? comment
|
||||
? `转交给${transferTo} ${comment}`
|
||||
: `转交给${transferTo}`
|
||||
: comment || '已转交'
|
||||
out.push({ left: `${name} ${mid}`, right: ts })
|
||||
continue
|
||||
}
|
||||
if (status === 'pending' || status === 'running' || status === 'not_reached' || !status) {
|
||||
if (!status && (comment || ts)) {
|
||||
out.push({ left: `${name} ${comment || '已同意'}`, right: ts })
|
||||
continue
|
||||
}
|
||||
out.push({ left: `${name} 待处理`, right: '' })
|
||||
continue
|
||||
}
|
||||
if (ACTED.has(status) || comment || ts) {
|
||||
const mid =
|
||||
comment ||
|
||||
(status === 'approved'
|
||||
? '已同意'
|
||||
: status === 'rejected'
|
||||
? '已拒绝'
|
||||
: status === 'returned'
|
||||
? '已打回'
|
||||
: status === 'completed'
|
||||
? '已完成'
|
||||
: statusText(status))
|
||||
out.push({ left: `${name} ${mid}`, right: ts })
|
||||
continue
|
||||
}
|
||||
out.push({ left: `${name} 待处理`, right: '' })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function isCcProgressItem(p) {
|
||||
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
||||
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
||||
const title = String(p?.title || p?.nodeName || '').trim()
|
||||
return (
|
||||
taskType === 'cc' ||
|
||||
signMode === 'cc' ||
|
||||
title === '抄送人' ||
|
||||
/^抄送/.test(title)
|
||||
)
|
||||
}
|
||||
|
||||
export function buildFlowItems(detail) {
|
||||
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
const items = []
|
||||
let ccBuf = null
|
||||
const flushCc = () => {
|
||||
if (!ccBuf) return
|
||||
const uniq = Array.from(new Set(ccBuf.names.map((n) => n.trim()).filter(Boolean)))
|
||||
items.push({
|
||||
left: uniq.length ? `抄送: ${uniq.join(', ')}` : '抄送:',
|
||||
right: ccBuf.ts || ''
|
||||
})
|
||||
ccBuf = null
|
||||
}
|
||||
|
||||
for (const p of list) {
|
||||
const kind = String(p.kind || '')
|
||||
const title = String(p.title || '-').trim()
|
||||
const names = peopleDisplayNames(p)
|
||||
const name = names.join(', ')
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const comment = String(p.commentText || '').trim()
|
||||
const st = statusText(String(p.status || ''))
|
||||
const signMode = String(p.signMode || p.sign_mode || '')
|
||||
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
||||
|
||||
if (isCcProgressItem(p)) {
|
||||
if (!ccBuf) ccBuf = { names: [], ts }
|
||||
if (names.length) ccBuf.names.push(...names)
|
||||
if (ts) ccBuf.ts = ts
|
||||
continue
|
||||
}
|
||||
flushCc()
|
||||
|
||||
if (kind === 'submit') {
|
||||
items.push({
|
||||
left: name ? `【${title}】 ${name} ${st}` : `【${title}】 ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (kind === 'terminal') {
|
||||
if (String(p.status || '') === 'returned' || title === '已打回') continue
|
||||
items.push({ left: title || st, right: ts })
|
||||
continue
|
||||
}
|
||||
|
||||
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
||||
const personLines = formatProgressPersonLines(Array.isArray(p?.people) ? p.people : [])
|
||||
if (personLines.length) {
|
||||
items.push({
|
||||
left: `${head} ${st}`,
|
||||
right: '',
|
||||
personLines
|
||||
})
|
||||
continue
|
||||
}
|
||||
items.push({
|
||||
left: name ? `${head} ${name} ${st}` : `${head} ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
}
|
||||
flushCc()
|
||||
|
||||
const hasCcLine = items.some((it) => it.left.startsWith('抄送'))
|
||||
if (!hasCcLine) {
|
||||
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []
|
||||
const ccTasks = tasks.filter((t) => {
|
||||
const tt = String(t?.taskType || t?.task_type || '').trim()
|
||||
const sm = String(t?.signMode || t?.sign_mode || '').trim()
|
||||
return tt === 'cc' || sm === 'cc'
|
||||
})
|
||||
if (ccTasks.length) {
|
||||
const names = ccTasks
|
||||
.map((t) =>
|
||||
String(t?.assigneeUserName || t?.assignee_user_name || t?.assigneeUserId || '').trim()
|
||||
)
|
||||
.filter(Boolean)
|
||||
const uniq = Array.from(new Set(names))
|
||||
let ts = ''
|
||||
for (const t of ccTasks) {
|
||||
const tTs = String(t?.updateTime || t?.createTime || '').trim()
|
||||
if (tTs && (!ts || tTs > ts)) ts = tTs
|
||||
}
|
||||
if (uniq.length) items.push({ left: `抄送: ${uniq.join(', ')}`, right: ts })
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
export { buildFlowItems }
|
||||
|
||||
export function buildOaPrintModel(detail, opts = {}) {
|
||||
const { stampClass, stampText } = resolveStamp(detail?.status)
|
||||
@@ -671,8 +504,7 @@ function buildPrintStylesContent() {
|
||||
justify-content: center; font-size: 32px; font-weight: 700; letter-spacing: 2px;
|
||||
line-height: 1; color: currentColor; }
|
||||
.oa-stamp .main.is-chars3 { font-size: 26px; letter-spacing: 1px; }
|
||||
.oa-print-footer { display: flex; flex-wrap: wrap; justify-content: space-between;
|
||||
gap: 8px; margin-top: 16px; font-size: 12px; color: #646464; }`
|
||||
.oa-print-footer { display: flex; gap: 28px; margin-top: 14px; font-size: 12px; }`
|
||||
}
|
||||
|
||||
function buildPrintPageContent(model) {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 移动端打印「审批流程」行组装(无 html2canvas/API 副作用,便于单测)
|
||||
* Spec: docs/superpowers/specs/2026-08-13-oa-progress-withdraw-cancelled-display-design.md
|
||||
*/
|
||||
import { OA_STATUS_TEXT } from '../status-label.js'
|
||||
import { formatProgressPersonLines as formatSharedPersonLines } from '../oa-progress-person-lines.js'
|
||||
|
||||
function statusText(s) {
|
||||
return OA_STATUS_TEXT[s] || s || '-'
|
||||
}
|
||||
|
||||
/** 任务节点 cancelled=系统清待办 →「无需处理」;终态 cancelled 仍为「已撤销」 */
|
||||
function progressNodeStatusText(p) {
|
||||
const kind = String(p?.kind || '')
|
||||
const s = String(p?.status || '').trim()
|
||||
if (kind !== 'terminal' && s === 'cancelled') return '无需处理'
|
||||
return statusText(s)
|
||||
}
|
||||
|
||||
function peopleDisplayNames(p) {
|
||||
const people = Array.isArray(p?.people) ? p.people : []
|
||||
const fromPeople = people
|
||||
.map((x) => String(x?.nickName || x?.id || '').trim())
|
||||
.filter(Boolean)
|
||||
if (fromPeople.length) return fromPeople
|
||||
const name = String(p?.assigneeUserName || p?.assigneeUserId || '').trim()
|
||||
return name ? [name] : []
|
||||
}
|
||||
|
||||
function formatProgressPersonLines(people) {
|
||||
return formatSharedPersonLines(people).map((x) => ({
|
||||
left: x.body || x.text,
|
||||
right: x.timestamp || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function isCcProgressItem(p) {
|
||||
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
||||
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
||||
const title = String(p?.title || p?.nodeName || '').trim()
|
||||
return (
|
||||
taskType === 'cc' ||
|
||||
signMode === 'cc' ||
|
||||
title === '抄送人' ||
|
||||
/^抄送/.test(title)
|
||||
)
|
||||
}
|
||||
|
||||
export function buildFlowItems(detail) {
|
||||
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
const items = []
|
||||
let ccBuf = null
|
||||
const flushCc = () => {
|
||||
if (!ccBuf) return
|
||||
const uniq = Array.from(new Set(ccBuf.names.map((n) => n.trim()).filter(Boolean)))
|
||||
items.push({
|
||||
left: uniq.length ? `抄送: ${uniq.join(', ')}` : '抄送:',
|
||||
right: ccBuf.ts || ''
|
||||
})
|
||||
ccBuf = null
|
||||
}
|
||||
|
||||
for (const p of list) {
|
||||
const kind = String(p.kind || '')
|
||||
const title = String(p.title || '-').trim()
|
||||
const names = peopleDisplayNames(p)
|
||||
const name = names.join(', ')
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const comment = String(p.commentText || '').trim()
|
||||
const st = progressNodeStatusText(p)
|
||||
const signMode = String(p.signMode || p.sign_mode || '')
|
||||
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
||||
|
||||
if (isCcProgressItem(p)) {
|
||||
if (!ccBuf) ccBuf = { names: [], ts }
|
||||
if (names.length) ccBuf.names.push(...names)
|
||||
if (ts) ccBuf.ts = ts
|
||||
continue
|
||||
}
|
||||
flushCc()
|
||||
|
||||
if (kind === 'submit') {
|
||||
items.push({
|
||||
left: name ? `【${title}】 ${name} ${st}` : `【${title}】 ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (kind === 'terminal') {
|
||||
if (String(p.status || '') === 'returned' || title === '已打回') continue
|
||||
const withdrawName = String(p.assigneeUserName || p.assignee || '').trim()
|
||||
const withdrawTip = comment || (String(p.status || '') === 'cancelled' ? '发起人撤销' : '')
|
||||
const leftExtra =
|
||||
String(p.status || '') === 'cancelled' && (withdrawName || withdrawTip)
|
||||
? ` ${[withdrawName, withdrawTip].filter(Boolean).join(' ')}`
|
||||
: ''
|
||||
items.push({ left: `${title || st}${leftExtra}`, right: ts })
|
||||
continue
|
||||
}
|
||||
|
||||
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
||||
const personLines = formatProgressPersonLines(Array.isArray(p?.people) ? p.people : [])
|
||||
if (personLines.length) {
|
||||
items.push({
|
||||
left: `${head} ${st}`,
|
||||
right: '',
|
||||
personLines
|
||||
})
|
||||
continue
|
||||
}
|
||||
items.push({
|
||||
left: name ? `${head} ${name} ${st}` : `${head} ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
}
|
||||
flushCc()
|
||||
|
||||
const hasCcLine = items.some((it) => it.left.startsWith('抄送'))
|
||||
if (!hasCcLine) {
|
||||
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []
|
||||
const ccTasks = tasks.filter((t) => {
|
||||
const tt = String(t?.taskType || t?.task_type || '').trim()
|
||||
const sm = String(t?.signMode || t?.sign_mode || '').trim()
|
||||
return tt === 'cc' || sm === 'cc'
|
||||
})
|
||||
if (ccTasks.length) {
|
||||
const names = ccTasks
|
||||
.map((t) =>
|
||||
String(t?.assigneeUserName || t?.assignee_user_name || t?.assigneeUserId || '').trim()
|
||||
)
|
||||
.filter(Boolean)
|
||||
const uniq = Array.from(new Set(names))
|
||||
let ts = ''
|
||||
for (const t of ccTasks) {
|
||||
const tTs = String(t?.updateTime || t?.createTime || '').trim()
|
||||
if (tTs && (!ts || tTs > ts)) ts = tTs
|
||||
}
|
||||
if (uniq.length) items.push({ left: `抄送: ${uniq.join(', ')}`, right: ts })
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 移动端打印「审批流程」:cancelled → 无需处理;终态归因发起人
|
||||
*/
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildFlowItems } from './oa-print-flow.js'
|
||||
|
||||
const items = buildFlowItems({
|
||||
progress: [
|
||||
{
|
||||
kind: 'submit',
|
||||
title: '发起人',
|
||||
status: 'submitted',
|
||||
assigneeUserName: '吴子龙',
|
||||
timestamp: '2026-08-13 11:17:38'
|
||||
},
|
||||
{
|
||||
kind: 'task',
|
||||
title: '直接主管',
|
||||
status: 'cancelled',
|
||||
signMode: 'and',
|
||||
timestamp: '',
|
||||
people: [
|
||||
{
|
||||
nickName: '张主管',
|
||||
status: 'cancelled',
|
||||
timestamp: '2026-08-13 12:47:46',
|
||||
commentText: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'terminal',
|
||||
title: '已撤销',
|
||||
status: 'cancelled',
|
||||
assigneeUserName: '吴子龙',
|
||||
commentText: '发起人撤销',
|
||||
timestamp: '2026-08-13 12:47:46'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.equal(items[1].left, '【直接主管·会签】 无需处理')
|
||||
assert.deepEqual(items[1].personLines, [{ left: '张主管 无需处理', right: '' }])
|
||||
assert.equal(items[2].left, '已撤销 吴子龙 发起人撤销')
|
||||
assert.equal(items[2].right, '2026-08-13 12:47:46')
|
||||
|
||||
console.log('oa-print-flow buildFlowItems cancelled: ok')
|
||||
@@ -0,0 +1,36 @@
|
||||
/* 按钮点击加载:文案淡化 + 居中自绘转圈(与发起页暂存/提交一致) */
|
||||
.oa-btn-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.oa-btn-loading.is-loading.van-button--disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.oa-btn-loading.is-loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
.oa-btn-loading.is-loading .oa-btn-loading__label {
|
||||
opacity: 0.25;
|
||||
}
|
||||
.oa-btn-loading__spin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
.oa-btn-loading__spin::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: oa-btn-loading-spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes oa-btn-loading-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url))
|
||||
const css = readFileSync(join(dir, 'oa-btn-loading.css'), 'utf8')
|
||||
|
||||
assert.match(css, /\.oa-btn-loading\.is-loading\s+\.oa-btn-loading__label\s*\{[^}]*opacity:\s*0\.25/)
|
||||
assert.match(css, /\.oa-btn-loading__spin\s*\{/)
|
||||
assert.match(css, /oa-btn-loading-spin/)
|
||||
assert.match(css, /rgba\(0,\s*0,\s*0,\s*0\.06\)/)
|
||||
assert.match(
|
||||
css,
|
||||
/\.oa-btn-loading\.is-loading\.van-button--disabled\s*\{[^}]*opacity:\s*1\s*!important/
|
||||
)
|
||||
|
||||
console.log('oa-btn-loading.css contract ok')
|
||||
@@ -1,4 +1,14 @@
|
||||
/* form-create + Vant:上下布局与左对齐兜底 */
|
||||
.form-create-m {
|
||||
/*
|
||||
* 审批/查看等整表 disabled:Vant 默认灰字(text-color-3)难读,
|
||||
* 改为正文色;仍禁止编辑,仅提升可读性。
|
||||
*/
|
||||
--van-field-input-disabled-text-color: var(--text-primary, #0f291e);
|
||||
--van-field-disabled-text-color: var(--text-primary, #0f291e);
|
||||
--van-cell-disabled-color: var(--text-primary, #0f291e);
|
||||
}
|
||||
|
||||
.form-create-m .van-field__label--top {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
@@ -13,6 +23,23 @@
|
||||
font-size: 13px;
|
||||
min-height: 22px;
|
||||
line-height: 22px;
|
||||
color: var(--text-primary, #0f291e);
|
||||
}
|
||||
|
||||
/*
|
||||
* disabled input 仍可能被 -webkit-text-fill-color 压成灰字,显式覆盖。
|
||||
*/
|
||||
.form-create-m .van-field__control:disabled,
|
||||
.form-create-m .van-field--disabled .van-field__control {
|
||||
color: var(--text-primary, #0f291e) !important;
|
||||
-webkit-text-fill-color: var(--text-primary, #0f291e) !important;
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.form-create-m .van-field--disabled .van-field__label,
|
||||
.form-create-m .van-cell--disabled {
|
||||
color: var(--text-primary, #0f291e);
|
||||
}
|
||||
|
||||
.form-create-m .van-field__control::placeholder {
|
||||
@@ -89,17 +116,7 @@
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 归属人 / 归属部门:不可改选时仍用 disabled 防点开,但展示色用正文色(勿灰字难读)。
|
||||
* Vant disabled 会设 -webkit-text-fill-color,须一并覆盖。
|
||||
*/
|
||||
.form-create-m .oa-owner-field.is-locked .van-field__control,
|
||||
.form-create-m .oa-owner-field.is-locked .van-field__control:disabled {
|
||||
color: var(--text-primary, #0f291e) !important;
|
||||
-webkit-text-fill-color: var(--text-primary, #0f291e) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 归属人 / 归属部门锁定:箭头弱化,值色已由上方 disabled 规则统一为正文色 */
|
||||
.form-create-m .oa-owner-field.is-locked .van-cell__right-icon {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/** 附件下载:优先后端预签名 URL,保证跨域场景保留原始文件名 */
|
||||
|
||||
import { fetchAttachmentDownloadUrl } from '../api/oa.js'
|
||||
import { attachmentFullUrl } from './attachment.js'
|
||||
|
||||
function openDownloadUrl(href, fileName) {
|
||||
// 微信 web-view 对 a[download] 支持差;优先新开页打开带 Content-Disposition 的地址
|
||||
const opened = window.open(href, '_blank')
|
||||
if (opened) return
|
||||
const a = document.createElement('a')
|
||||
a.href = href
|
||||
a.download = fileName || 'download'
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ fileUrl?: string, url?: string, fileName?: string, name?: string }} file
|
||||
*/
|
||||
export async function downloadAttachmentFile(file) {
|
||||
const fileUrl = String(file?.fileUrl || file?.url || '').trim()
|
||||
if (!fileUrl) throw new Error('文件地址无效')
|
||||
const fileName =
|
||||
String(file?.fileName || file?.name || '').trim() ||
|
||||
fileUrl.split('/').pop() ||
|
||||
'download'
|
||||
|
||||
try {
|
||||
const res = await fetchAttachmentDownloadUrl({ fileUrl, fileName })
|
||||
const downloadUrl = String(res?.downloadUrl || '').trim()
|
||||
if (downloadUrl) {
|
||||
openDownloadUrl(downloadUrl, fileName)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 预签名失败时回退直连
|
||||
}
|
||||
|
||||
const href = attachmentFullUrl(fileUrl) || fileUrl
|
||||
if (!href) throw new Error('无法下载附件')
|
||||
openDownloadUrl(href, fileName)
|
||||
}
|
||||
@@ -11,26 +11,26 @@
|
||||
'oa-detail__body--mine-act': (canWithdraw || canCopyRestart) && !canApprove
|
||||
}"
|
||||
>
|
||||
<header class="oa-detail__hd">
|
||||
<h1 class="oa-detail__title">{{ detail?.title || '审批详情' }}</h1>
|
||||
<span
|
||||
v-if="detail?.status"
|
||||
class="oa-detail__status"
|
||||
:class="statusTone(detail.status)"
|
||||
>
|
||||
{{ statusText(detail.status) }}
|
||||
</span>
|
||||
</header>
|
||||
<form-create-mobile
|
||||
v-if="formRule.length"
|
||||
v-model="formData"
|
||||
:rule="formRule"
|
||||
:option="formOption"
|
||||
/>
|
||||
<p v-else class="oa-detail__empty">暂无表单内容</p>
|
||||
<div class="oa-detail__stamp-host">
|
||||
<OaStatusStamp v-if="detail?.status" :status="detail.status" />
|
||||
<form-create-mobile
|
||||
v-if="formRule.length"
|
||||
v-model="formData"
|
||||
:rule="formRule"
|
||||
:option="formOption"
|
||||
:disabled="true"
|
||||
/>
|
||||
<p v-else class="oa-detail__empty">暂无表单内容</p>
|
||||
</div>
|
||||
<OaProgressTimeline :items="timelineItems" />
|
||||
</div>
|
||||
<footer class="oa-detail__ft">
|
||||
<footer
|
||||
class="oa-detail__ft"
|
||||
:class="{
|
||||
'oa-detail__ft--center': !canApprove && !canWithdraw && !canCopyRestart,
|
||||
'oa-detail__ft--equal': !canApprove && (canWithdraw || canCopyRestart)
|
||||
}"
|
||||
>
|
||||
<template v-if="canApprove">
|
||||
<van-button
|
||||
class="oa-detail__btn oa-detail__btn--more"
|
||||
@@ -42,43 +42,41 @@
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="detail?.allowReturn !== false"
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
class="oa-detail__btn oa-detail__btn--ghost oa-btn-loading"
|
||||
:class="{ 'is-loading': acting }"
|
||||
:disabled="acting || printing"
|
||||
@click="openComment('return')"
|
||||
>
|
||||
打回
|
||||
<OaBtnLoadingInner :loading="acting">打回</OaBtnLoadingInner>
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-detail__btn oa-detail__btn--primary"
|
||||
class="oa-detail__btn oa-detail__btn--primary oa-btn-loading"
|
||||
type="primary"
|
||||
:loading="acting"
|
||||
:class="{ 'is-loading': acting }"
|
||||
:disabled="acting || printing"
|
||||
@click="openComment('approve')"
|
||||
>
|
||||
同意
|
||||
<OaBtnLoadingInner :loading="acting">同意</OaBtnLoadingInner>
|
||||
</van-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<van-button class="oa-detail__btn oa-detail__btn--close" @click="onClose">
|
||||
关闭
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-detail__btn oa-detail__btn--ghost oa-detail__btn--print"
|
||||
:loading="printing"
|
||||
class="oa-detail__btn oa-detail__btn--ghost oa-detail__btn--print oa-btn-loading"
|
||||
:class="{ 'is-loading': printing }"
|
||||
:disabled="acting || printing"
|
||||
@click="onPrint"
|
||||
>
|
||||
打印
|
||||
<OaBtnLoadingInner :loading="printing">打印</OaBtnLoadingInner>
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canWithdraw"
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
class="oa-detail__btn oa-detail__btn--primary oa-btn-loading"
|
||||
type="primary"
|
||||
:class="{ 'is-loading': acting }"
|
||||
:disabled="acting"
|
||||
@click="withdraw"
|
||||
>
|
||||
撤销
|
||||
<OaBtnLoadingInner :loading="acting">撤销</OaBtnLoadingInner>
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canCopyRestart"
|
||||
@@ -190,9 +188,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showConfirmDialog, showToast } from 'vant'
|
||||
import { closeToHost } from '../oa/close-to-host.js'
|
||||
import {
|
||||
fetchOaInstanceDetail,
|
||||
fetchCurrentUser,
|
||||
@@ -204,12 +203,13 @@ import {
|
||||
fetchWithdrawOaInstance
|
||||
} from '../api/oa.js'
|
||||
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
|
||||
import { statusText, statusTone } from '../oa/status-label.js'
|
||||
import { exportOaApprovalPdf } from '../oa/print/exportOaApprovalPdf.js'
|
||||
import { defaultComment, buildMoreActions } from '../oa/oa-action-defaults.js'
|
||||
import OaProgressTimeline from '../oa/components/OaProgressTimeline.vue'
|
||||
import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
|
||||
import OaBtnLoadingInner from '../oa/components/OaBtnLoadingInner.vue'
|
||||
import OaPersonSelect from '../oa/components/OaPersonSelect.vue'
|
||||
import OaStatusStamp from '../oa/components/OaStatusStamp.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -268,7 +268,22 @@ const timelineItems = computed(() => {
|
||||
status: p.status || '',
|
||||
timestamp: p.timestamp || '',
|
||||
assignee: p.assigneeUserName || p.assigneeUserId || '',
|
||||
commentText: p.commentText || ''
|
||||
commentText: p.commentText || '',
|
||||
taskType: p.taskType || '',
|
||||
kind: p.kind || '',
|
||||
signMode: p.signMode || '',
|
||||
people: Array.isArray(p.people)
|
||||
? p.people.map((x) => ({
|
||||
id: x.id || '',
|
||||
nickName: x.nickName || x.id || '',
|
||||
status: x.status || '',
|
||||
timestamp: x.timestamp || '',
|
||||
commentText: x.commentText || '',
|
||||
taskId: x.taskId || '',
|
||||
transferToUserId: x.transferToUserId || '',
|
||||
transferToUserName: x.transferToUserName || ''
|
||||
}))
|
||||
: []
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -334,37 +349,10 @@ const commentPlaceholder = computed(() => {
|
||||
|
||||
/** 通知宿主关闭(对齐 OaStart closeToMini) */
|
||||
function closeToMini(payload) {
|
||||
const data = payload || { action: 'oa-detail-close' }
|
||||
try {
|
||||
if (typeof wx !== 'undefined' && wx.miniProgram?.navigateBack) {
|
||||
wx.miniProgram.postMessage({ data })
|
||||
wx.miniProgram.navigateBack()
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
const uniWv = typeof uni !== 'undefined' ? uni.webView : null
|
||||
if (uniWv?.postMessage) {
|
||||
uniWv.postMessage({ data })
|
||||
}
|
||||
if (uniWv?.navigateBack) {
|
||||
uniWv.navigateBack()
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ source: 'zkxw-oa-h5', ...data }, '*')
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
showToast((data && data.message) || '已关闭(非小程序环境)')
|
||||
closeToHost(payload || { action: 'oa-detail-close' }, {
|
||||
showToast: (msg) => showToast(msg),
|
||||
fallbackMessage: '已关闭(非小程序环境)'
|
||||
})
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
@@ -410,7 +398,6 @@ async function withdraw() {
|
||||
acting.value = true
|
||||
try {
|
||||
await fetchWithdrawOaInstance(id)
|
||||
showToast('已撤销')
|
||||
closeToMini({ action: 'oa-detail-withdraw', message: '已撤销' })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '撤销失败')
|
||||
@@ -493,17 +480,14 @@ async function submitComment() {
|
||||
if (kind === 'approve') {
|
||||
await fetchApproveOaTask(task.id, comment || undefined)
|
||||
commentVisible.value = false
|
||||
showToast('已同意')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已同意' })
|
||||
} else if (kind === 'reject') {
|
||||
await fetchRejectOaTask(task.id, comment || undefined)
|
||||
commentVisible.value = false
|
||||
showToast('已拒绝')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已拒绝' })
|
||||
} else if (kind === 'return') {
|
||||
await fetchReturnOaTask(task.id, comment)
|
||||
commentVisible.value = false
|
||||
showToast('已打回')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已打回' })
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -538,7 +522,6 @@ async function submitAddSign() {
|
||||
comment: c || undefined
|
||||
})
|
||||
addSignVisible.value = false
|
||||
showToast('已加签')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已加签' })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '加签失败')
|
||||
@@ -570,7 +553,6 @@ async function submitTransfer() {
|
||||
comment: c || undefined
|
||||
})
|
||||
transferVisible.value = false
|
||||
showToast('已转交')
|
||||
closeToMini({ action: 'oa-detail-done', message: '已转交' })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '转交失败')
|
||||
@@ -632,6 +614,12 @@ async function bootstrap() {
|
||||
onMounted(() => {
|
||||
bootstrap()
|
||||
})
|
||||
|
||||
watch(instanceId, (next, prev) => {
|
||||
if (next && next !== prev) {
|
||||
bootstrap()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -658,43 +646,9 @@ onMounted(() => {
|
||||
.oa-detail__body--mine-act {
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
.oa-detail__hd {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
.oa-detail__title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: var(--text-primary, #0f291e);
|
||||
}
|
||||
.oa-detail__status {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-detail__status.ok {
|
||||
color: var(--success, #10b981);
|
||||
background: var(--brand-green-soft, #ecfdf5);
|
||||
}
|
||||
.oa-detail__status.warn {
|
||||
color: var(--warning, #d97706);
|
||||
background: var(--barley-yellow-soft, #fffbeb);
|
||||
}
|
||||
.oa-detail__status.danger {
|
||||
color: var(--danger, #ef4444);
|
||||
background: #fef2f2;
|
||||
}
|
||||
.oa-detail__status.mute {
|
||||
color: var(--text-muted, #5f7a6d);
|
||||
background: #f1f5f9;
|
||||
.oa-detail__stamp-host {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
.oa-detail__empty {
|
||||
margin: 12px 0;
|
||||
@@ -718,22 +672,38 @@ onMounted(() => {
|
||||
border-top: 1px solid #eef2f6;
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.oa-detail__ft--center {
|
||||
justify-content: center;
|
||||
}
|
||||
.oa-detail__ft--center .oa-detail__btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.oa-detail__ft--center .oa-detail__btn--print {
|
||||
min-width: 120px;
|
||||
padding: 0 28px !important;
|
||||
}
|
||||
.oa-detail__ft--equal .oa-detail__btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.oa-detail__ft--equal .oa-detail__btn--print {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.oa-detail__btn {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
border-radius: 12px !important;
|
||||
font-size: 14px;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
.oa-detail__btn--close {
|
||||
.oa-detail__btn--print {
|
||||
flex: 0 0 auto;
|
||||
min-width: 64px;
|
||||
border: none !important;
|
||||
background: #f1f5f9 !important;
|
||||
color: #334155 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oa-detail__btn--more {
|
||||
flex: 0 0 auto;
|
||||
@@ -748,13 +718,6 @@ onMounted(() => {
|
||||
letter-spacing: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
.oa-detail__btn--close:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.oa-detail__btn--print {
|
||||
flex: 0 0 auto;
|
||||
min-width: 64px;
|
||||
}
|
||||
.oa-detail__btn--ghost {
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
background: #fff !important;
|
||||
|
||||
@@ -44,22 +44,20 @@
|
||||
取消
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-start__btn oa-start__btn--draft"
|
||||
class="oa-start__btn oa-start__btn--draft oa-btn-loading"
|
||||
: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" />
|
||||
<OaBtnLoadingInner :loading="savingDraft">暂存草稿</OaBtnLoadingInner>
|
||||
</van-button>
|
||||
<van-button
|
||||
class="oa-start__btn oa-start__btn--submit"
|
||||
class="oa-start__btn oa-start__btn--submit oa-btn-loading"
|
||||
: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" />
|
||||
<OaBtnLoadingInner :loading="submitting">提交审批</OaBtnLoadingInner>
|
||||
</van-button>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -69,6 +67,7 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { closeToHost } from '../oa/close-to-host.js'
|
||||
import {
|
||||
fetchOaTemplateDetail,
|
||||
fetchOaFlowPreview,
|
||||
@@ -108,6 +107,13 @@ import {
|
||||
} from '../oa/kit-runtime.js'
|
||||
import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
|
||||
import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
|
||||
import OaBtnLoadingInner from '../oa/components/OaBtnLoadingInner.vue'
|
||||
import {
|
||||
buildFlowPreviewRequestKey,
|
||||
collectConditionFields,
|
||||
shouldShowFlowPreviewSpinner,
|
||||
shouldSkipFlowPreviewRequest
|
||||
} from '../oa/oa-flow-preview-refresh.js'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -130,6 +136,12 @@ const currentUser = ref(null)
|
||||
|
||||
let previewTimer = null
|
||||
let skipPreviewWatch = false
|
||||
/** 流程条件字段:仅这些 formData 变化才重拉预览 */
|
||||
let flowConditionFields = []
|
||||
let lastFlowPreviewRequestKey = null
|
||||
let inFlightFlowPreviewKey = null
|
||||
let hasCompletedFlowPreview = false
|
||||
let flowPreviewReqSeq = 0
|
||||
let leaveKitSyncing = false
|
||||
let leaveKitPrevSnapshot = {}
|
||||
let tripKitSyncing = false
|
||||
@@ -176,40 +188,10 @@ const tripDaysManualFields = computed(() => {
|
||||
|
||||
/** 通知宿主(小程序 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 })
|
||||
wx.miniProgram.navigateBack()
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
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) || '已完成(非小程序环境)')
|
||||
closeToHost(payload || { action: 'oa-start-close' }, {
|
||||
showToast: (msg) => showToast(msg),
|
||||
fallbackMessage: '已完成(非小程序环境)'
|
||||
})
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
@@ -261,12 +243,31 @@ function stepsNeedInitiatorSelect(steps) {
|
||||
|
||||
async function loadFlowPreview() {
|
||||
if (!appMeta.value?.id) return
|
||||
previewLoading.value = true
|
||||
const requestKey = buildFlowPreviewRequestKey({
|
||||
formData: formData.value,
|
||||
initiatorSelectByNode: initiatorSelectByNode.value,
|
||||
conditionFields: flowConditionFields
|
||||
})
|
||||
if (
|
||||
shouldSkipFlowPreviewRequest(lastFlowPreviewRequestKey, requestKey, {
|
||||
inFlightKey: inFlightFlowPreviewKey
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
const seq = ++flowPreviewReqSeq
|
||||
inFlightFlowPreviewKey = requestKey
|
||||
const showSpinner = shouldShowFlowPreviewSpinner({
|
||||
hasCompletedPreview: hasCompletedFlowPreview
|
||||
})
|
||||
if (showSpinner) previewLoading.value = true
|
||||
try {
|
||||
const preview = await fetchOaFlowPreview(appMeta.value.id, {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||||
})
|
||||
if (seq !== flowPreviewReqSeq) return
|
||||
lastFlowPreviewRequestKey = requestKey
|
||||
flowResolved.value = preview?.resolved !== false
|
||||
flowPendingMessage.value =
|
||||
preview?.message || '必填信息填写后,流程将自动显示'
|
||||
@@ -275,11 +276,16 @@ async function loadFlowPreview() {
|
||||
await loadUsers()
|
||||
}
|
||||
} catch (e) {
|
||||
if (seq !== flowPreviewReqSeq) return
|
||||
showToast(e?.message || '审批流预览失败')
|
||||
flowSteps.value = []
|
||||
flowResolved.value = true
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
if (seq === flowPreviewReqSeq) {
|
||||
inFlightFlowPreviewKey = null
|
||||
hasCompletedFlowPreview = true
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,6 +607,11 @@ async function loadByAppId(appId, { seedData = null, applyDefaults = true } = {}
|
||||
const prepared = prepareMobileFormSchema(detail?.formSchemaJson)
|
||||
formRule.value = prepared.rule
|
||||
formOption.value = prepared.option
|
||||
flowConditionFields = collectConditionFields(detail?.flowTreeJson)
|
||||
lastFlowPreviewRequestKey = null
|
||||
inFlightFlowPreviewKey = null
|
||||
hasCompletedFlowPreview = false
|
||||
flowPreviewReqSeq += 1
|
||||
if (applyDefaults) {
|
||||
formData.value = await applyKitDefaults(seedData || {})
|
||||
await syncOvertimeKitDurations()
|
||||
@@ -736,8 +747,12 @@ async function saveDraft() {
|
||||
})
|
||||
if (created?.id) editingDraftId.value = created.id
|
||||
}
|
||||
showToast('草稿已保存')
|
||||
closeToMini({ action: 'oa-start-draft', draftId: editingDraftId.value })
|
||||
// 成功提示交给宿主展示,避免 iframe 内 toast + 宿主 toast 叠成两条
|
||||
closeToMini({
|
||||
action: 'oa-start-draft',
|
||||
draftId: editingDraftId.value,
|
||||
message: '草稿已保存'
|
||||
})
|
||||
} catch (e) {
|
||||
showToast(e?.message || '保存草稿失败')
|
||||
} finally {
|
||||
@@ -788,7 +803,7 @@ async function submit() {
|
||||
...payload
|
||||
})
|
||||
}
|
||||
showToast('提交成功')
|
||||
// 成功提示交给宿主展示,避免 iframe 内 toast + 宿主 toast 叠成两条
|
||||
closeToMini({
|
||||
action: 'oa-start-submitted',
|
||||
instanceId: data?.id,
|
||||
@@ -917,33 +932,4 @@ onUnmounted(() => {
|
||||
font-weight: 700;
|
||||
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.14);
|
||||
}
|
||||
/* 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>
|
||||
@@ -0,0 +1,353 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>OA 附件 · Vant 缩略图美化 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--page-bg: #f5f6f8;
|
||||
--text: #1f2a26;
|
||||
--nav-bg: #ecfdf5;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 55%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 920px; margin: 0 auto; padding: 28px 16px 64px; }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.lead { margin: 0; color: #64748b; font-size: 14px; max-width: 720px; }
|
||||
.goals {
|
||||
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
||||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||||
font-size: 13px; color: #065f46;
|
||||
}
|
||||
.goals ul { margin: 6px 0 0; padding-left: 18px; }
|
||||
|
||||
.grid {
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,.95);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 4px; font-size: 15px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.card .sub { margin: 0 0 12px; font-size: 12px; color: #64748b; }
|
||||
.pill {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
||||
font-size: 10px; font-weight: 700;
|
||||
}
|
||||
.pill.now { background: #fff1f2; color: #be123c; border: 1px solid #fecdd3; }
|
||||
.pill.rec { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
|
||||
|
||||
.phone {
|
||||
width: 100%; max-width: 360px; margin: 0 auto;
|
||||
border-radius: 28px; border: 1px solid #cbd5e1; background: #0f172a;
|
||||
padding: 10px; box-shadow: 0 18px 40px rgba(15,23,42,.12);
|
||||
}
|
||||
.screen {
|
||||
border-radius: 20px; overflow: hidden; background: var(--page-bg); min-height: 420px;
|
||||
}
|
||||
.nav {
|
||||
display: flex; align-items: center; gap: 8px; min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
background: linear-gradient(180deg, #d1fae5 0%, var(--nav-bg) 100%);
|
||||
border-bottom: 1px solid #bbf7d0;
|
||||
}
|
||||
.nav__back {
|
||||
width: 28px; height: 28px; border-radius: 999px; display: grid; place-items: center;
|
||||
color: var(--brand-dark); font-weight: 700; background: rgba(255,255,255,.55);
|
||||
}
|
||||
.nav__title { flex: 1; font-size: 15px; font-weight: 700; color: #064e3b; }
|
||||
.body { padding: 14px 12px; }
|
||||
.label { font-size: 13px; font-weight: 650; color: #334155; margin-bottom: 10px; }
|
||||
.mode {
|
||||
display: inline-block; margin-bottom: 10px; padding: 3px 8px; border-radius: 6px;
|
||||
font-size: 11px; font-weight: 650;
|
||||
}
|
||||
.mode.edit { background: #eff6ff; color: #1d4ed8; }
|
||||
.mode.view { background: #f1f5f9; color: #475569; }
|
||||
|
||||
/* ===== 现状(默认 Vant 感) ===== */
|
||||
.now-grid { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.now-tile {
|
||||
position: relative; width: 80px; height: 80px; border-radius: 8px;
|
||||
background: #f7f8fa; border: 1px solid #ebedf0; overflow: hidden;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 6px;
|
||||
}
|
||||
.now-tile.img {
|
||||
background: linear-gradient(145deg, #86efac, #22c55e); color: #fff;
|
||||
font-size: 11px; font-weight: 700; padding: 0;
|
||||
}
|
||||
.now-tile .fname {
|
||||
font-size: 10px; color: #969799; text-align: center; word-break: break-all;
|
||||
line-height: 1.2; max-height: 36px; overflow: hidden;
|
||||
}
|
||||
.now-tile .dl {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(15,23,42,.55); color: #fff; font-size: 9px;
|
||||
text-align: center; padding: 2px;
|
||||
}
|
||||
.now-tile .x {
|
||||
position: absolute; top: 0; right: 0; width: 14px; height: 14px;
|
||||
border-radius: 0 0 0 12px; background: rgba(0,0,0,.7); color: #fff;
|
||||
font-size: 10px; display: grid; place-items: center; line-height: 1;
|
||||
}
|
||||
.now-add {
|
||||
width: 80px; height: 80px; border-radius: 8px; background: #f7f8fa;
|
||||
display: grid; place-items: center; color: #dcdee0; font-size: 28px;
|
||||
}
|
||||
|
||||
/* ===== 美化后(仍是缩略图格) ===== */
|
||||
.nice-grid { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.nice-tile {
|
||||
position: relative;
|
||||
width: 84px; height: 84px;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e4eee9;
|
||||
box-shadow: 0 4px 12px rgba(20, 55, 40, 0.06);
|
||||
overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
padding: 8px 6px 18px;
|
||||
}
|
||||
.nice-tile.img {
|
||||
padding: 0;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(74, 222, 128, 0.35), rgba(10, 159, 109, 0.55)),
|
||||
#0a9f6d;
|
||||
color: #fff; font-size: 11px; font-weight: 700;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
.nice-tile .file-ico {
|
||||
width: 28px; height: 28px; border-radius: 8px;
|
||||
display: grid; place-items: center; color: #fff; margin-bottom: 4px;
|
||||
box-shadow: 0 3px 8px rgba(15, 40, 30, 0.15);
|
||||
}
|
||||
.nice-tile.ft-pdf .file-ico { background: linear-gradient(145deg, #f07171, #ef4444); }
|
||||
.nice-tile.ft-xls .file-ico { background: linear-gradient(145deg, #4fbf8a, #10b981); }
|
||||
.nice-tile.ft-doc .file-ico { background: linear-gradient(145deg, #6ea8ff, #3b82f6); }
|
||||
.nice-tile .file-ico svg { width: 15px; height: 15px; }
|
||||
.nice-tile .fname {
|
||||
font-size: 10px; color: #4d635a; text-align: center;
|
||||
line-height: 1.25; max-height: 26px; overflow: hidden;
|
||||
word-break: break-all; font-weight: 600;
|
||||
}
|
||||
.nice-tile .dl {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
padding: 4px 4px 5px;
|
||||
font-size: 10px; font-weight: 650; letter-spacing: 0.02em;
|
||||
text-align: center; color: #fff;
|
||||
background: linear-gradient(180deg, rgba(8, 127, 87, 0.0), rgba(8, 127, 87, 0.92));
|
||||
}
|
||||
.nice-tile.img .dl {
|
||||
background: linear-gradient(180deg, transparent, rgba(15, 23, 42, 0.55));
|
||||
font-weight: 600;
|
||||
}
|
||||
.nice-tile .x {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
width: 20px; height: 20px; border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
color: #fff; font-size: 12px;
|
||||
display: grid; place-items: center; line-height: 1;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.18);
|
||||
}
|
||||
.nice-add {
|
||||
width: 84px; height: 84px; border-radius: 14px;
|
||||
border: 1.5px dashed #9ad9be;
|
||||
background: linear-gradient(145deg, #f7fdf9, #e9f9f2);
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; gap: 4px;
|
||||
color: var(--brand);
|
||||
}
|
||||
.nice-add .plus {
|
||||
width: 28px; height: 28px; border-radius: 9px;
|
||||
background: linear-gradient(145deg, #4ade80, #0a9f6d);
|
||||
color: #fff; display: grid; place-items: center;
|
||||
font-size: 18px; font-weight: 700;
|
||||
box-shadow: 0 4px 10px rgba(10, 159, 109, 0.28);
|
||||
}
|
||||
.nice-add .txt { font-size: 11px; font-weight: 650; color: var(--brand-dark); }
|
||||
|
||||
.tabs { margin-top: 18px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.tab {
|
||||
border: 1px solid #cbd5e1; background: #fff; border-radius: 999px;
|
||||
padding: 8px 14px; font-size: 13px; font-weight: 650; color: #475569; cursor: pointer;
|
||||
}
|
||||
.tab.is-on {
|
||||
background: var(--brand-soft); border-color: #86efac; color: var(--brand-dark);
|
||||
}
|
||||
.panel[hidden] { display: none !important; }
|
||||
.footnote {
|
||||
margin-top: 18px; font-size: 12px; color: #64748b;
|
||||
padding: 12px 14px; border-radius: 12px; background: #fff; border: 1px solid #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<h1>附件上传 · 保留 Vant 缩略图,只美化样式</h1>
|
||||
<p class="lead">
|
||||
交互不变:格子预览、右上角删除、非图点按下载、详情禁用删除。仅圆角、阴影、删除钮、下载条、添加格做三墙风格打磨。
|
||||
</p>
|
||||
<div class="goals">
|
||||
<strong>美化点</strong>
|
||||
<ul>
|
||||
<li>预览格:14px 圆角 + 浅绿描边/轻阴影</li>
|
||||
<li>删除:圆形半透明钮(替代直角小三角)</li>
|
||||
<li>下载条:品牌绿渐变底 + 更清晰「点击下载」</li>
|
||||
<li>添加格:虚线框 + 绿色渐变 + 号</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab is-on" type="button" data-panel="edit">发起 / 编辑</button>
|
||||
<button class="tab" type="button" data-panel="view">详情(无删除)</button>
|
||||
</div>
|
||||
|
||||
<div class="panel grid" id="panel-edit">
|
||||
<section class="card">
|
||||
<h2>现状 <span class="pill now">修改前</span></h2>
|
||||
<p class="sub">默认 Vant 灰格,删除钮生硬,下载条发黑。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav"><div class="nav__back">‹</div><div class="nav__title">发起 · 附件</div></div>
|
||||
<div class="body">
|
||||
<div class="mode edit">编辑态</div>
|
||||
<div class="label">相关附件</div>
|
||||
<div class="now-grid">
|
||||
<div class="now-tile img">现场照<span class="x">×</span></div>
|
||||
<div class="now-tile">
|
||||
<div class="fname">报销单.pdf</div>
|
||||
<div class="dl">点击下载</div>
|
||||
<span class="x">×</span>
|
||||
</div>
|
||||
<div class="now-tile">
|
||||
<div class="fname">费用.xlsx</div>
|
||||
<div class="dl">点击下载</div>
|
||||
<span class="x">×</span>
|
||||
</div>
|
||||
<div class="now-add">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>美化后 <span class="pill rec">推荐</span></h2>
|
||||
<p class="sub">仍是缩略图格;类型小图标 + 文件名更易认。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav"><div class="nav__back">‹</div><div class="nav__title">发起 · 附件</div></div>
|
||||
<div class="body">
|
||||
<div class="mode edit">编辑态</div>
|
||||
<div class="label">相关附件</div>
|
||||
<div class="nice-grid">
|
||||
<div class="nice-tile img">现场照<span class="x">×</span></div>
|
||||
<div class="nice-tile ft-pdf">
|
||||
<div class="file-ico">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
|
||||
</div>
|
||||
<div class="fname">报销单.pdf</div>
|
||||
<div class="dl">点击下载</div>
|
||||
<span class="x">×</span>
|
||||
</div>
|
||||
<div class="nice-tile ft-xls">
|
||||
<div class="file-ico">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h5"/></svg>
|
||||
</div>
|
||||
<div class="fname">费用.xlsx</div>
|
||||
<div class="dl">点击下载</div>
|
||||
<span class="x">×</span>
|
||||
</div>
|
||||
<div class="nice-add"><div class="plus">+</div><div class="txt">添加</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="panel grid" id="panel-view" hidden>
|
||||
<section class="card">
|
||||
<h2>现状 · 详情</h2>
|
||||
<p class="sub">无删除;下载条仍偏黑。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav"><div class="nav__back">‹</div><div class="nav__title">详情 · 附件</div></div>
|
||||
<div class="body">
|
||||
<div class="mode view">只读</div>
|
||||
<div class="label">相关附件</div>
|
||||
<div class="now-grid">
|
||||
<div class="now-tile img">现场照</div>
|
||||
<div class="now-tile"><div class="fname">报销单.pdf</div><div class="dl">点击下载</div></div>
|
||||
<div class="now-tile"><div class="fname">费用.xlsx</div><div class="dl">点击下载</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>美化后 · 详情 <span class="pill rec">推荐</span></h2>
|
||||
<p class="sub">无删除钮;保留下载条。</p>
|
||||
<div class="phone"><div class="screen">
|
||||
<div class="nav"><div class="nav__back">‹</div><div class="nav__title">详情 · 附件</div></div>
|
||||
<div class="body">
|
||||
<div class="mode view">只读</div>
|
||||
<div class="label">相关附件</div>
|
||||
<div class="nice-grid">
|
||||
<div class="nice-tile img">现场照</div>
|
||||
<div class="nice-tile ft-pdf">
|
||||
<div class="file-ico">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/></svg>
|
||||
</div>
|
||||
<div class="fname">报销单.pdf</div>
|
||||
<div class="dl">点击下载</div>
|
||||
</div>
|
||||
<div class="nice-tile ft-xls">
|
||||
<div class="file-ico">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h5"/></svg>
|
||||
</div>
|
||||
<div class="fname">费用.xlsx</div>
|
||||
<div class="dl">点击下载</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="footnote">
|
||||
实现方式:继续用 <code>van-uploader</code>,用 scoped <code>:deep</code> 覆盖预览格 / 删除钮 / 上传触发区,并增强 <code>preview-cover</code> 下载条。
|
||||
不改成文件行卡。
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelectorAll('.tab').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach((b) => b.classList.remove('is-on'))
|
||||
btn.classList.add('is-on')
|
||||
const id = btn.getAttribute('data-panel')
|
||||
document.getElementById('panel-edit').hidden = id !== 'edit'
|
||||
document.getElementById('panel-view').hidden = id !== 'view'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,564 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>mobile-oa-h5 · 详情头区去掉标题 + 打印印章 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--page-bg: #f5f6f8;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
--nav-bg: #ecfdf5;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
radial-gradient(700px 380px at 92% 0%, rgba(214, 163, 67, 0.1), transparent 50%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1280px; margin: 0 auto; padding: 28px 16px 56px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 820px; }
|
||||
.hero .note {
|
||||
margin-top: 12px; display: inline-block; padding: 8px 12px;
|
||||
border-radius: 10px; background: #fff; border: 1px solid #e2e8f0;
|
||||
font-size: 12px; color: #64748b;
|
||||
}
|
||||
.hero .note strong { color: #0f766e; }
|
||||
|
||||
.goals {
|
||||
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
||||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||||
font-size: 13px; color: #065f46;
|
||||
}
|
||||
.goals ul { margin: 6px 0 0; padding-left: 18px; }
|
||||
|
||||
.grid {
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.card .sub { margin: 0 0 12px; font-size: 12px; color: #64748b; }
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.pill.now { background: #fff1f2; color: #be123c; border: 1px solid #fecdd3; }
|
||||
.pill.rec { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
|
||||
.pill.alt { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
|
||||
|
||||
/* phone frame */
|
||||
.phone {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
margin: 0 auto;
|
||||
border-radius: 28px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #0f172a;
|
||||
padding: 10px;
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
.phone__screen {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--page-bg);
|
||||
min-height: 520px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
background: linear-gradient(180deg, #d1fae5 0%, var(--nav-bg) 100%);
|
||||
border-bottom: 1px solid #bbf7d0;
|
||||
}
|
||||
.nav__back {
|
||||
width: 28px; height: 28px; border-radius: 999px;
|
||||
display: grid; place-items: center;
|
||||
color: var(--brand-dark); font-weight: 700; font-size: 16px;
|
||||
background: rgba(255,255,255,.55);
|
||||
}
|
||||
.nav__title {
|
||||
flex: 1; min-width: 0;
|
||||
font-size: 15px; font-weight: 700;
|
||||
color: #064e3b;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding: 12px 12px 20px;
|
||||
}
|
||||
.body--stamp-pad {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* current header: title + tag */
|
||||
.hd {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
.hd__title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: #0f291e;
|
||||
}
|
||||
.tag {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
.dup-hint {
|
||||
outline: 2px dashed #f43f5e;
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
background: rgba(244, 63, 94, 0.06);
|
||||
}
|
||||
|
||||
/* form fake */
|
||||
.form {
|
||||
background: #fff;
|
||||
border: 1px solid #e8edf2;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.form__row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 1fr;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
min-height: 44px;
|
||||
}
|
||||
.form__row:last-child { border-bottom: 0; }
|
||||
.form__k {
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
background: #fafbfc;
|
||||
border-right: 1px solid #f1f5f9;
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
.form__v {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
color: #0f172a;
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
.tl {
|
||||
margin-top: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8edf2;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.tl h3 { margin: 0 0 8px; font-size: 13px; color: #334155; }
|
||||
.tl p { margin: 0; font-size: 12px; color: #64748b; }
|
||||
|
||||
/* ===== 印章 v4(对齐 web-admin OaApprovalPrintSheet) ===== */
|
||||
.stamp-host {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
.oa-stamp {
|
||||
--stamp: #3ec9a8;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 4px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
transform: rotate(-18deg);
|
||||
opacity: 0.88;
|
||||
color: var(--stamp);
|
||||
}
|
||||
.oa-stamp.ok { --stamp: #3ec9a8; }
|
||||
.oa-stamp.run { --stamp: #f0a020; }
|
||||
.oa-stamp.rej { --stamp: #e85d5d; }
|
||||
.oa-stamp.ret { --stamp: #e07070; }
|
||||
.oa-stamp.can { --stamp: #9aa0a6; }
|
||||
.oa-stamp .outer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 4px solid currentColor;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.oa-stamp .arc-top,
|
||||
.oa-stamp .arc-bottom {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
border: 2px solid currentColor;
|
||||
opacity: 0.95;
|
||||
pointer-events: none;
|
||||
}
|
||||
.oa-stamp .arc-top {
|
||||
top: 9px;
|
||||
height: 28px;
|
||||
border-bottom: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
|
||||
}
|
||||
.oa-stamp .arc-bottom {
|
||||
bottom: 9px;
|
||||
height: 28px;
|
||||
border-top: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-radius: 0 0 50% 50% / 0 0 100% 100%;
|
||||
}
|
||||
.oa-stamp .label-top {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 0; right: 0;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1.5px;
|
||||
line-height: 1;
|
||||
color: currentColor;
|
||||
opacity: 0.95;
|
||||
}
|
||||
.oa-stamp .label-bottom {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0; right: 0;
|
||||
text-align: center;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1;
|
||||
color: currentColor;
|
||||
opacity: 0.9;
|
||||
font-family: Arial, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
.oa-stamp .main {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1;
|
||||
color: currentColor;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-text-stroke: 0.3px currentColor;
|
||||
}
|
||||
.oa-stamp .main.chars-3 {
|
||||
font-size: 20px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* A2: 略小印章浮在表单右上(已选 72px) */
|
||||
.oa-stamp.sm {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
.oa-stamp.sm .outer { border-width: 3px; }
|
||||
.oa-stamp.sm .main { font-size: 17px; }
|
||||
.oa-stamp.sm .main.chars-3 { font-size: 14px; }
|
||||
.oa-stamp.sm .label-top { top: 13px; font-size: 7px; letter-spacing: 1px; }
|
||||
.oa-stamp.sm .label-bottom { bottom: 12px; font-size: 6px; }
|
||||
.oa-stamp.sm .arc-top { top: 7px; height: 20px; left: 8px; right: 8px; }
|
||||
.oa-stamp.sm .arc-bottom { bottom: 7px; height: 20px; left: 8px; right: 8px; }
|
||||
|
||||
/* 对照:上一版 84px */
|
||||
.oa-stamp.md {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
}
|
||||
.oa-stamp.md .main { font-size: 20px; }
|
||||
.oa-stamp.md .main.chars-3 { font-size: 17px; }
|
||||
.oa-stamp.md .label-top { top: 15px; font-size: 8px; }
|
||||
.oa-stamp.md .label-bottom { bottom: 14px; font-size: 7px; }
|
||||
.oa-stamp.md .arc-top { top: 8px; height: 24px; left: 10px; right: 10px; }
|
||||
.oa-stamp.md .arc-bottom { bottom: 8px; height: 24px; left: 10px; right: 10px; }
|
||||
|
||||
.stamp-row {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.stamp-row .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 110px;
|
||||
}
|
||||
.stamp-row .item span {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
.stamp-static {
|
||||
position: relative;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.stamp-static .oa-stamp {
|
||||
position: absolute;
|
||||
top: 0; right: 0;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.legend {
|
||||
margin-top: 18px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
.legend h2 { margin: 0 0 8px; font-size: 15px; color: var(--text); }
|
||||
.legend ol { margin: 0; padding-left: 18px; }
|
||||
.legend li { margin: 4px 0; }
|
||||
.legend code {
|
||||
font-size: 12px;
|
||||
background: #f1f5f9;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<header class="hero">
|
||||
<h1>OA 详情 · 去掉内容区流程标题 + 右上角打印印章</h1>
|
||||
<p>
|
||||
对比当前「标题 + 已完成 Tag」与目标态「顶栏已有标题,内容区不再重复标题;状态改为对齐 web 打印的钉钉风印章,叠在右上角」。
|
||||
改动落点:<code>mobile-oa-h5/src/views/OaDetail.vue</code>(webview 内嵌页)。
|
||||
</p>
|
||||
<div class="note">本地打开本文件即可预览 · 文件:<strong>mobile-ui/zkxw-mobile-oa-detail-header-stamp-demo.html</strong></div>
|
||||
<div class="goals">
|
||||
<strong>目标</strong>
|
||||
<ul>
|
||||
<li>去掉内容区顶部流程名称(与顶栏标题重复)</li>
|
||||
<li>「已完成」等状态 Tag → web 打印同款圆形印章,放在界面右上角</li>
|
||||
<li>文案对齐打印:completed →「已通过」(非「已完成」)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<!-- 现状 -->
|
||||
<section class="card">
|
||||
<h2><span class="pill now">现状</span> 标题 + Tag</h2>
|
||||
<p class="sub">顶栏与内容区标题重复(红框);状态为小胶囊 Tag。</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">差旅报销申请</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<header class="hd">
|
||||
<h1 class="hd__title dup-hint">差旅报销申请</h1>
|
||||
<span class="tag">已完成</span>
|
||||
</header>
|
||||
<div class="form">
|
||||
<div class="form__row"><div class="form__k">申请人</div><div class="form__v">张三</div></div>
|
||||
<div class="form__row"><div class="form__k">金额</div><div class="form__v">¥ 1,280.00</div></div>
|
||||
<div class="form__row"><div class="form__k">事由</div><div class="form__v">客户拜访差旅</div></div>
|
||||
</div>
|
||||
<div class="tl">
|
||||
<h3>审批进度</h3>
|
||||
<p>发起 → 部门负责人(已同意)→ 结束</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 选定:A 布局 + 略小印章 -->
|
||||
<section class="card">
|
||||
<h2><span class="pill rec">已选</span> A 布局 · 印章 72px</h2>
|
||||
<p class="sub">去掉内容区标题;钉钉风印章叠在表单右上(相对 web 打印 118px / 上一版 84px 再小一档)。</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">差旅报销申请</div>
|
||||
</div>
|
||||
<div class="body body--stamp-pad">
|
||||
<div class="stamp-host">
|
||||
<div class="oa-stamp ok sm" aria-label="已通过">
|
||||
<div class="outer"></div>
|
||||
<div class="arc-top"></div>
|
||||
<div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div>
|
||||
<div class="main">已通过</div>
|
||||
<div class="label-bottom">approval</div>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="form__row"><div class="form__k">申请人</div><div class="form__v">张三</div></div>
|
||||
<div class="form__row"><div class="form__k">金额</div><div class="form__v">¥ 1,280.00</div></div>
|
||||
<div class="form__row"><div class="form__k">事由</div><div class="form__v">客户拜访差旅</div></div>
|
||||
<div class="form__row"><div class="form__k">备注</div><div class="form__v">含市内交通</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tl">
|
||||
<h3>审批进度</h3>
|
||||
<p>发起 → 部门负责人(已同意)→ 结束</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 对照:大一号 -->
|
||||
<section class="card">
|
||||
<h2><span class="pill alt">对照</span> 若用 84px(未选)</h2>
|
||||
<p class="sub">仅尺寸对照;落地用左侧 72px。</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">差旅报销申请</div>
|
||||
</div>
|
||||
<div class="body body--stamp-pad">
|
||||
<div class="stamp-host">
|
||||
<div class="oa-stamp ok md" aria-label="已通过">
|
||||
<div class="outer"></div>
|
||||
<div class="arc-top"></div>
|
||||
<div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div>
|
||||
<div class="main">已通过</div>
|
||||
<div class="label-bottom">approval</div>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div class="form__row"><div class="form__k">申请人</div><div class="form__v">张三</div></div>
|
||||
<div class="form__row"><div class="form__k">金额</div><div class="form__v">¥ 1,280.00</div></div>
|
||||
<div class="form__row"><div class="form__k">事由</div><div class="form__v">客户拜访差旅</div></div>
|
||||
<div class="form__row"><div class="form__k">备注</div><div class="form__v">含市内交通</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tl">
|
||||
<h3>审批进度</h3>
|
||||
<p>发起 → 部门负责人(已同意)→ 结束</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="legend">
|
||||
<h2>各状态印章色(与 web 打印一致)</h2>
|
||||
<div class="stamp-row">
|
||||
<div class="item">
|
||||
<div class="stamp-static">
|
||||
<div class="oa-stamp ok">
|
||||
<div class="outer"></div><div class="arc-top"></div><div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div><div class="main">已通过</div><div class="label-bottom">approval</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>completed / approved → ok</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="stamp-static">
|
||||
<div class="oa-stamp run">
|
||||
<div class="outer"></div><div class="arc-top"></div><div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div><div class="main">审批中</div><div class="label-bottom">approval</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>running / pending → run</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="stamp-static">
|
||||
<div class="oa-stamp rej">
|
||||
<div class="outer"></div><div class="arc-top"></div><div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div><div class="main">已拒绝</div><div class="label-bottom">approval</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>rejected → rej</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="stamp-static">
|
||||
<div class="oa-stamp ret">
|
||||
<div class="outer"></div><div class="arc-top"></div><div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div><div class="main">已打回</div><div class="label-bottom">approval</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>returned → ret</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="stamp-static">
|
||||
<div class="oa-stamp can">
|
||||
<div class="outer"></div><div class="arc-top"></div><div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div><div class="main">已撤销</div><div class="label-bottom">approval</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>cancelled → can</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="legend">
|
||||
<h2>当前共识</h2>
|
||||
<ol>
|
||||
<li>去掉内容区流程标题(顶栏已有)</li>
|
||||
<li>状态改为 web 打印同款印章,叠在表单区域右上角</li>
|
||||
<li>尺寸用 <strong>72px</strong>(A 布局 + 再小一档),文案用打印 <code>resolveStamp</code>(如 completed →「已通过」)</li>
|
||||
</ol>
|
||||
<p style="margin:10px 0 0">若 72px 仍偏大/偏小,直接说目标像素即可;否则回复「按此设计」我写 spec 并实现。</p>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,678 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>草稿箱 · 模式 A 美化 Demo(管理位 + 卡片 + 勾选对齐)</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--warn: #d97706;
|
||||
--warn-soft: #fff7e8;
|
||||
--danger: #ef4444;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
--border: #dcebe4;
|
||||
--page-bg: #f5f8f7;
|
||||
--surface: #fff;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.14), transparent 55%),
|
||||
radial-gradient(700px 380px at 92% 0%, rgba(214, 163, 67, 0.1), transparent 50%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 45%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.stage { max-width: 1320px; margin: 0 auto; padding: 28px 16px 64px; }
|
||||
|
||||
.hero h1 { margin: 0 0 8px; font-size: 24px; font-weight: 720; letter-spacing: -0.02em; }
|
||||
.hero p { margin: 0; max-width: 820px; color: #64748b; font-size: 14px; }
|
||||
.hero .note {
|
||||
margin-top: 12px; display: inline-block; padding: 8px 12px;
|
||||
border-radius: 10px; background: #fff; border: 1px solid #e2e8f0;
|
||||
font-size: 12px; color: #64748b;
|
||||
}
|
||||
.hero .note strong { color: #0f766e; }
|
||||
|
||||
.pain {
|
||||
margin: 18px 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.pain-item {
|
||||
background: #fff;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
.pain-item b { color: #b91c1c; }
|
||||
|
||||
.tabs {
|
||||
display: flex; flex-wrap: wrap; gap: 10px; margin: 22px 0 18px;
|
||||
}
|
||||
.tab {
|
||||
appearance: none; border: 1px solid #e2e8f0; background: #fff;
|
||||
border-radius: 999px; padding: 10px 16px; font-size: 13px;
|
||||
font-weight: 600; color: #475569; cursor: pointer;
|
||||
transition: .15s border-color, .15s box-shadow, .15s color, .15s background;
|
||||
}
|
||||
.tab:hover { border-color: #94a3b8; }
|
||||
.tab.is-active {
|
||||
color: var(--brand-dark); border-color: var(--brand);
|
||||
background: var(--brand-soft);
|
||||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.18);
|
||||
}
|
||||
.tab .badge {
|
||||
margin-left: 6px; font-size: 10px; font-weight: 700;
|
||||
padding: 2px 6px; border-radius: 999px;
|
||||
background: #ecfdf5; color: #047857;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 340px) 1fr;
|
||||
gap: 22px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,.92);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
.panel h2 { margin: 0 0 4px; font-size: 16px; }
|
||||
.panel .sub { margin: 0 0 12px; font-size: 12px; color: #64748b; }
|
||||
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
|
||||
.panel li { margin: 6px 0; }
|
||||
.panel .pick {
|
||||
margin-top: 14px; padding: 12px; border-radius: 12px;
|
||||
background: var(--brand-soft); border: 1px solid #c6f0dc;
|
||||
font-size: 12px; color: #166534;
|
||||
}
|
||||
|
||||
.phones {
|
||||
display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;
|
||||
}
|
||||
.phone {
|
||||
width: 375px; background: #fff; border-radius: 28px;
|
||||
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.phone-bar {
|
||||
height: 44px; display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, #0a9f6d, #087f57);
|
||||
color: #fff; font-size: 16px; font-weight: 600;
|
||||
}
|
||||
.phone-cap {
|
||||
padding: 8px 14px; font-size: 11px; font-weight: 650;
|
||||
color: #64748b; background: #f8fafc; border-bottom: 1px solid #eef2f0;
|
||||
text-align: center;
|
||||
}
|
||||
.page { background: var(--page-bg); min-height: 620px; padding-bottom: 18px; }
|
||||
|
||||
.style-block { display: none; }
|
||||
.style-block.is-active { display: block; }
|
||||
|
||||
/* ========== 共用搜索 ========== */
|
||||
.search {
|
||||
height: 36px; display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px; border-radius: 10px; border: 1px solid #e8eaed;
|
||||
background: #fafafa; font-size: 13px; color: #94a3b8; width: 100%;
|
||||
}
|
||||
.search::before {
|
||||
content: ""; width: 12px; height: 12px; border: 1.5px solid #94a3b8;
|
||||
border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ========== 共用卡片(美化后的模式 A) ========== */
|
||||
.card {
|
||||
width: 100%;
|
||||
background: var(--surface);
|
||||
border: 1px solid #e2ebe6;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
.card-body { padding: 14px 14px 12px; }
|
||||
.card-top {
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 15px; font-weight: 700; color: #1f2a26; line-height: 1.35;
|
||||
min-width: 0; flex: 1;
|
||||
}
|
||||
.card-status {
|
||||
flex-shrink: 0; font-size: 11px; font-weight: 700;
|
||||
padding: 3px 9px; border-radius: 999px;
|
||||
background: var(--warn-soft); color: var(--warn);
|
||||
}
|
||||
.card-meta { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.card-meta-line {
|
||||
display: flex; gap: 10px; font-size: 12px; line-height: 1.4;
|
||||
}
|
||||
.card-meta-line .lab { color: #94a3b8; min-width: 56px; flex-shrink: 0; }
|
||||
.card-meta-line .val { color: #475569; min-width: 0; }
|
||||
.card-footer {
|
||||
display: flex; gap: 8px; padding: 10px 12px 12px;
|
||||
border-top: 1px dashed rgba(140, 170, 155, 0.28);
|
||||
}
|
||||
.card-footer button {
|
||||
flex: 1; min-width: 0; height: 34px; border-radius: 10px;
|
||||
border: 1px solid #d8e3de; background: #fff;
|
||||
font-size: 12px; font-weight: 650; color: #334155; cursor: pointer;
|
||||
}
|
||||
.card-footer .p {
|
||||
background: var(--brand); border-color: var(--brand); color: #fff;
|
||||
}
|
||||
.card-footer .d {
|
||||
color: var(--danger); border-color: rgba(239,68,68,.35);
|
||||
background: rgba(239,68,68,.06);
|
||||
}
|
||||
|
||||
.list { padding: 10px 12px 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.end { text-align: center; padding: 14px 0 6px; font-size: 12px; color: #94a3b8; }
|
||||
|
||||
/* —— 卡片变体:更精致 —— */
|
||||
.card.v-soft {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.card.v-accent {
|
||||
border-color: #e8eeeb;
|
||||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04);
|
||||
position: relative;
|
||||
}
|
||||
.card.v-accent::before {
|
||||
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
||||
background: linear-gradient(180deg, #f59e0b, #d6a343);
|
||||
}
|
||||
.card.v-accent .card-body { padding-left: 16px; }
|
||||
.card.v-flat {
|
||||
border: 0;
|
||||
background: #fff;
|
||||
box-shadow: none;
|
||||
border-radius: 12px;
|
||||
outline: 1px solid #e8eeeb;
|
||||
}
|
||||
.card.v-meta-inline .card-meta {
|
||||
flex-direction: row; flex-wrap: wrap; gap: 4px 12px;
|
||||
}
|
||||
.card.v-meta-inline .card-meta-line .lab { min-width: 0; }
|
||||
.card.v-meta-inline .card-meta-line .lab::after { content: " "; }
|
||||
.card.v-meta-inline .sep {
|
||||
color: #cbd5e1; font-size: 12px;
|
||||
}
|
||||
|
||||
/* ========== A0 现状对照 ========== */
|
||||
.a0-toolbar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; background: #fff; border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.a0-manage {
|
||||
flex-shrink: 0; font-size: 14px; font-weight: 600; color: #166534;
|
||||
padding: 0 2px; white-space: nowrap;
|
||||
}
|
||||
.a0-row {
|
||||
display: flex; align-items: flex-start; gap: 8px; width: 100%;
|
||||
}
|
||||
.a0-check {
|
||||
padding-top: 20px; flex-shrink: 0;
|
||||
}
|
||||
.a0-check span {
|
||||
display: flex; width: 20px; height: 20px; border-radius: 50%;
|
||||
border: 1.5px solid #cbd5e1; background: #fff;
|
||||
align-items: center; justify-content: center;
|
||||
font-size: 11px; color: #fff; font-weight: 700;
|
||||
}
|
||||
.a0-check span.on { background: #22c55e; border-color: #22c55e; }
|
||||
.a0-row .card { flex: 1; min-width: 0; }
|
||||
|
||||
/* ========== 方案 1:搜索独占 + 列表头「共N条 | 管理」 ========== */
|
||||
.s1-toolbar { padding: 10px 12px 8px; background: #fff; border-bottom: 1px solid #f1f5f9; }
|
||||
.s1-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 14px 2px;
|
||||
}
|
||||
.s1-count { font-size: 12px; color: var(--text-muted); }
|
||||
.s1-count strong { color: var(--brand-dark); font-weight: 700; }
|
||||
.s1-link {
|
||||
font-size: 13px; font-weight: 650; color: var(--brand-dark);
|
||||
padding: 4px 2px; cursor: pointer; user-select: none;
|
||||
}
|
||||
.s1-link.is-done { color: #64748b; }
|
||||
|
||||
/* ========== 方案 2:顶栏右侧胶囊「管理」 ========== */
|
||||
.s2-toolbar {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 12px; background: #fff; border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.s2-chip {
|
||||
flex-shrink: 0; height: 36px; padding: 0 14px;
|
||||
border-radius: 999px; border: 1px solid #d8e3de;
|
||||
background: #fff; font-size: 13px; font-weight: 650;
|
||||
color: var(--brand-dark); cursor: pointer;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.s2-chip.is-on {
|
||||
background: #f1f5f9; border-color: #e2e8f0; color: #64748b;
|
||||
}
|
||||
.s2-chip svg { width: 14px; height: 14px; }
|
||||
|
||||
/* ========== 方案 3:搜索下方次级工具条 ========== */
|
||||
.s3-toolbar { padding: 10px 12px 0; background: #fff; border-bottom: 1px solid #f1f5f9; }
|
||||
.s3-sub {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-top: 10px; padding: 8px 2px 10px;
|
||||
}
|
||||
.s3-hint { font-size: 12px; color: #94a3b8; }
|
||||
.s3-btn {
|
||||
height: 28px; padding: 0 12px; border-radius: 8px;
|
||||
border: 1px solid #c6f0dc; background: var(--brand-soft);
|
||||
color: var(--brand-dark); font-size: 12px; font-weight: 650; cursor: pointer;
|
||||
}
|
||||
.s3-btn.is-on {
|
||||
background: #f1f5f9; border-color: #e2e8f0; color: #64748b;
|
||||
}
|
||||
|
||||
/* ========== 方案 4:底栏管理(列表干净) ========== */
|
||||
.s4-page { position: relative; padding-bottom: 64px; }
|
||||
.s4-toolbar { padding: 10px 12px; background: #fff; border-bottom: 1px solid #f1f5f9; }
|
||||
.s4-dock {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(255,255,255,.96);
|
||||
border-top: 1px solid #e8eeeb;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.s4-dock .hint { font-size: 12px; color: var(--text-muted); }
|
||||
.s4-dock .btn {
|
||||
height: 36px; padding: 0 18px; border-radius: 10px;
|
||||
border: 0; background: var(--brand); color: #fff;
|
||||
font-size: 13px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.s4-dock.is-manage .btn {
|
||||
background: #f1f5f9; color: #64748b;
|
||||
}
|
||||
.s4-dock .danger {
|
||||
height: 36px; padding: 0 16px; border-radius: 10px;
|
||||
border: 1px solid rgba(239,68,68,.35);
|
||||
background: rgba(239,68,68,.06); color: var(--danger);
|
||||
font-size: 13px; font-weight: 650; cursor: pointer;
|
||||
}
|
||||
|
||||
/* ========== 管理态:勾选垂直居中 ========== */
|
||||
.row {
|
||||
display: flex; align-items: center; gap: 10px; width: 100%;
|
||||
}
|
||||
.row .card { flex: 1; min-width: 0; margin: 0; }
|
||||
.check {
|
||||
flex-shrink: 0; width: 22px; height: 22px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.check span {
|
||||
display: flex; width: 20px; height: 20px; border-radius: 50%;
|
||||
border: 1.5px solid #cbd5e1; background: #fff;
|
||||
align-items: center; justify-content: center;
|
||||
font-size: 11px; color: #fff; font-weight: 700;
|
||||
}
|
||||
.check span.on { background: #22c55e; border-color: #22c55e; }
|
||||
.row.is-on .card {
|
||||
border-color: rgba(34, 197, 94, 0.45);
|
||||
box-shadow: 0 8px 20px rgba(22, 163, 74, 0.12);
|
||||
}
|
||||
|
||||
.batch {
|
||||
margin: 10px 12px 0; padding: 10px 12px; border-radius: 12px;
|
||||
background: #fff; border: 1px solid #c6f0dc;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 13px; color: #166534;
|
||||
}
|
||||
.batch button {
|
||||
height: 30px; padding: 0 12px; border-radius: 8px;
|
||||
border: 1px solid rgba(239,68,68,.35);
|
||||
background: rgba(239,68,68,.06); color: var(--danger);
|
||||
font-size: 12px; font-weight: 650; cursor: pointer;
|
||||
}
|
||||
|
||||
/* 内嵌勾选(方案变体) */
|
||||
.card-check-in {
|
||||
display: flex; gap: 10px; align-items: flex-start;
|
||||
}
|
||||
.card-check-in .check { margin-top: 2px; }
|
||||
|
||||
.meta-inline {
|
||||
margin-top: 8px; font-size: 12px; color: #64748b;
|
||||
}
|
||||
.meta-inline .dot { margin: 0 6px; color: #cbd5e1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>草稿箱 · 模式 A 美化选型</h1>
|
||||
<p>
|
||||
保留现有信息结构(标题 / 草稿态 / 最近保存 / 版本 / 编辑·提交·删除),针对你反馈的三点做方案对比:
|
||||
<strong>管理入口不突兀</strong>、<strong>勾选与卡片垂直居中</strong>、<strong>卡片更好看</strong>。
|
||||
</p>
|
||||
<div class="note">请选 <strong>S1 / S2 / S3 / S4</strong>(卡片可再指定 soft / accent / flat)。选好后我落到 <code>mobile-h5</code>。</div>
|
||||
<div class="pain">
|
||||
<div class="pain-item"><b>痛点 1</b> 「管理」挤在搜索框右侧,突兀</div>
|
||||
<div class="pain-item"><b>痛点 2</b> 管理态勾选与卡片未垂直居中</div>
|
||||
<div class="pain-item"><b>痛点 3</b> 卡片观感偏平、不好看</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs">
|
||||
<button class="tab is-active" data-k="a0">A0 现状对照</button>
|
||||
<button class="tab" data-k="s1">S1 列表头管理<span class="badge">推荐</span></button>
|
||||
<button class="tab" data-k="s2">S2 胶囊按钮</button>
|
||||
<button class="tab" data-k="s3">S3 次级工具条</button>
|
||||
<button class="tab" data-k="s4">S4 底栏管理</button>
|
||||
<button class="tab" data-k="cards">卡片三款</button>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="panel" id="desc"></div>
|
||||
<div class="phones" id="phones"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ICON_GEAR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></svg>';
|
||||
|
||||
function cardHtml(title, time, ver, opts) {
|
||||
opts = opts || {};
|
||||
var cls = "card" + (opts.variant ? " " + opts.variant : "");
|
||||
var footer = opts.noActions ? "" : (
|
||||
'<div class="card-footer">' +
|
||||
'<button class="p">编辑</button>' +
|
||||
'<button>提交审批</button>' +
|
||||
'<button class="d">删除</button>' +
|
||||
'</div>'
|
||||
);
|
||||
var meta = opts.inlineMeta
|
||||
? ('<div class="meta-inline">' + time + '<span class="dot">·</span>版本 ' + ver + '</div>')
|
||||
: (
|
||||
'<div class="card-meta">' +
|
||||
'<div class="card-meta-line"><span class="lab">最近保存</span><span class="val">' + time + '</span></div>' +
|
||||
'<div class="card-meta-line"><span class="lab">版本</span><span class="val">' + ver + '</span></div>' +
|
||||
'</div>'
|
||||
);
|
||||
return (
|
||||
'<div class="' + cls + '">' +
|
||||
'<div class="card-body">' +
|
||||
'<div class="card-top">' +
|
||||
'<div class="card-title">' + title + '</div>' +
|
||||
'<div class="card-status">草稿</div>' +
|
||||
'</div>' +
|
||||
meta +
|
||||
'</div>' +
|
||||
footer +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
const ROWS = [
|
||||
["入帐单", "2026-08-12 17:03:42", "v5"],
|
||||
["出差申请", "2026-08-11 09:18:00", "v3"],
|
||||
["采购报销", "2026-08-10 17:05:11", "v2"]
|
||||
];
|
||||
|
||||
function phone(cap, bodyHtml) {
|
||||
return (
|
||||
'<div class="phone">' +
|
||||
'<div class="phone-bar">草稿箱</div>' +
|
||||
(cap ? '<div class="phone-cap">' + cap + '</div>' : '') +
|
||||
'<div class="page">' + bodyHtml + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
const DESCS = {
|
||||
a0: {
|
||||
title: "A0 · 现状对照",
|
||||
sub: "复现当前问题,方便并排对比。",
|
||||
items: [
|
||||
"「管理」文字直接贴在搜索框右侧,视觉突兀",
|
||||
"管理态勾选 padding-top 硬对齐,无法跟卡片垂直居中",
|
||||
"卡片可用,但边框/按钮节奏偏旧"
|
||||
],
|
||||
pick: "仅作对照,不建议落地。"
|
||||
},
|
||||
s1: {
|
||||
title: "S1 · 列表头管理(推荐)",
|
||||
sub: "搜索独占一行;「共 N 条」与「管理」放在列表上方次级行。",
|
||||
items: [
|
||||
"搜索框不再被挤窄,输入更舒适",
|
||||
"管理入口与列表语义绑定,不再像外挂按钮",
|
||||
"管理态:勾选与卡片 align-items:center 垂直居中",
|
||||
"卡片:轻阴影 + 等分按钮(现状结构保留)"
|
||||
],
|
||||
pick: "最推荐:改动小、符合列表页习惯(微信/钉钉类)。"
|
||||
},
|
||||
s2: {
|
||||
title: "S2 · 搜索旁胶囊按钮",
|
||||
sub: "仍在顶栏,但「管理」做成等高胶囊,降低突兀感。",
|
||||
items: [
|
||||
"与搜索同高的描边胶囊,比纯文字更规整",
|
||||
"可选左侧小图标强化「管理模式」",
|
||||
"勾选垂直居中;完成态胶囊变灰"
|
||||
],
|
||||
pick: "想少改布局、只润色顶栏时选 S2。"
|
||||
},
|
||||
s3: {
|
||||
title: "S3 · 搜索下次级工具条",
|
||||
sub: "搜索下方增加一行提示 + 浅绿小按钮。",
|
||||
items: [
|
||||
"主搜索区干净,管理降为次级操作",
|
||||
"可用文案提示「可批量删除草稿」",
|
||||
"管理态同样垂直居中勾选"
|
||||
],
|
||||
pick: "需要教育用户「可批量管理」时选 S3。"
|
||||
},
|
||||
s4: {
|
||||
title: "S4 · 底栏管理",
|
||||
sub: "列表区完全干净;管理/批量删除沉到底部固定栏。",
|
||||
items: [
|
||||
"顶栏只留搜索,最清爽",
|
||||
"进入管理后底栏切换为「已选 N · 批量删除 · 完成」",
|
||||
"适合单手操作、强调批量删除场景"
|
||||
],
|
||||
pick: "草稿常需批量清理时选 S4;会占一点底部空间。"
|
||||
},
|
||||
cards: {
|
||||
title: "卡片三款细调",
|
||||
sub: "同一信息结构,只换卡片「皮」;可与 S1–S4 任意组合。",
|
||||
items: [
|
||||
"Soft:无描边、更柔阴影",
|
||||
"Accent:左侧琥珀竖条强调草稿",
|
||||
"Flat:细线扁平,信息更密(元信息可一行)"
|
||||
],
|
||||
pick: "建议:S1 布局 + Soft 或 Accent 卡片。"
|
||||
}
|
||||
};
|
||||
|
||||
function renderA0() {
|
||||
var normal = phone("普通态 · 管理突兀",
|
||||
'<div class="a0-toolbar"><div class="search" style="flex:1">搜索流程名称</div><div class="a0-manage">管理</div></div>' +
|
||||
'<div class="list">' +
|
||||
ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2]); }).join("") +
|
||||
'<div class="end">已经到底了</div></div>'
|
||||
);
|
||||
var manage = phone("管理态 · 勾选未居中",
|
||||
'<div class="a0-toolbar"><div class="search" style="flex:1">搜索流程名称</div><div class="a0-manage" style="color:#64748b">完成</div></div>' +
|
||||
'<div class="batch"><span>已选 1 项</span><button>批量删除</button></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="a0-row"><div class="a0-check"><span class="on">✓</span></div>' + cardHtml(ROWS[0][0], ROWS[0][1], ROWS[0][2], { noActions: true }) + '</div>' +
|
||||
'<div class="a0-row"><div class="a0-check"><span></span></div>' + cardHtml(ROWS[1][0], ROWS[1][1], ROWS[1][2], { noActions: true }) + '</div>' +
|
||||
'<div class="a0-row"><div class="a0-check"><span></span></div>' + cardHtml(ROWS[2][0], ROWS[2][1], ROWS[2][2], { noActions: true }) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderS1() {
|
||||
var normal = phone("普通态",
|
||||
'<div class="s1-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="s1-head"><div class="s1-count">共 <strong>3</strong> 条草稿</div><div class="s1-link">管理</div></div>' +
|
||||
'<div class="list">' +
|
||||
ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2], { variant: "v-soft" }); }).join("") +
|
||||
'<div class="end">已经到底了</div></div>'
|
||||
);
|
||||
var manage = phone("管理态 · 勾选居中",
|
||||
'<div class="s1-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="s1-head"><div class="s1-count">已选 <strong>1</strong> / 3</div><div class="s1-link is-done">完成</div></div>' +
|
||||
'<div class="batch"><span>已选 1 项</span><button>批量删除</button></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="row is-on"><div class="check"><span class="on">✓</span></div>' + cardHtml(ROWS[0][0], ROWS[0][1], ROWS[0][2], { noActions: true, variant: "v-soft" }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[1][0], ROWS[1][1], ROWS[1][2], { noActions: true, variant: "v-soft" }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[2][0], ROWS[2][1], ROWS[2][2], { noActions: true, variant: "v-soft" }) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderS2() {
|
||||
var normal = phone("普通态 · 胶囊",
|
||||
'<div class="s2-toolbar"><div class="search" style="flex:1">搜索流程名称</div>' +
|
||||
'<button class="s2-chip">' + ICON_GEAR + '管理</button></div>' +
|
||||
'<div class="list">' +
|
||||
ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2], { variant: "v-accent" }); }).join("") +
|
||||
'<div class="end">已经到底了</div></div>'
|
||||
);
|
||||
var manage = phone("管理态 · 勾选居中",
|
||||
'<div class="s2-toolbar"><div class="search" style="flex:1">搜索流程名称</div>' +
|
||||
'<button class="s2-chip is-on">完成</button></div>' +
|
||||
'<div class="batch"><span>已选 1 项</span><button>批量删除</button></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="row is-on"><div class="check"><span class="on">✓</span></div>' + cardHtml(ROWS[0][0], ROWS[0][1], ROWS[0][2], { noActions: true, variant: "v-accent" }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[1][0], ROWS[1][1], ROWS[1][2], { noActions: true, variant: "v-accent" }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[2][0], ROWS[2][1], ROWS[2][2], { noActions: true, variant: "v-accent" }) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderS3() {
|
||||
var normal = phone("普通态 · 次级条",
|
||||
'<div class="s3-toolbar"><div class="search">搜索流程名称</div>' +
|
||||
'<div class="s3-sub"><span class="s3-hint">可批量删除不用的草稿</span><button class="s3-btn">管理</button></div></div>' +
|
||||
'<div class="list">' +
|
||||
ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2]); }).join("") +
|
||||
'<div class="end">已经到底了</div></div>'
|
||||
);
|
||||
var manage = phone("管理态 · 勾选居中",
|
||||
'<div class="s3-toolbar"><div class="search">搜索流程名称</div>' +
|
||||
'<div class="s3-sub"><span class="s3-hint">点选草稿后可批量删除</span><button class="s3-btn is-on">完成</button></div></div>' +
|
||||
'<div class="batch"><span>已选 1 项</span><button>批量删除</button></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="row is-on"><div class="check"><span class="on">✓</span></div>' + cardHtml(ROWS[0][0], ROWS[0][1], ROWS[0][2], { noActions: true }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[1][0], ROWS[1][1], ROWS[1][2], { noActions: true }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[2][0], ROWS[2][1], ROWS[2][2], { noActions: true }) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderS4() {
|
||||
var normal = phone("普通态 · 底栏",
|
||||
'<div class="s4-page">' +
|
||||
'<div class="s4-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="list">' +
|
||||
ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2], { variant: "v-soft", inlineMeta: true }); }).join("") +
|
||||
'<div class="end">已经到底了</div></div>' +
|
||||
'<div class="s4-dock"><span class="hint">共 3 条草稿</span><button class="btn">管理</button></div>' +
|
||||
'</div>'
|
||||
);
|
||||
var manage = phone("管理态 · 底栏批量",
|
||||
'<div class="s4-page">' +
|
||||
'<div class="s4-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="row is-on"><div class="check"><span class="on">✓</span></div>' + cardHtml(ROWS[0][0], ROWS[0][1], ROWS[0][2], { noActions: true, variant: "v-soft", inlineMeta: true }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[1][0], ROWS[1][1], ROWS[1][2], { noActions: true, variant: "v-soft", inlineMeta: true }) + '</div>' +
|
||||
'<div class="row"><div class="check"><span></span></div>' + cardHtml(ROWS[2][0], ROWS[2][1], ROWS[2][2], { noActions: true, variant: "v-soft", inlineMeta: true }) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="s4-dock is-manage">' +
|
||||
'<span class="hint">已选 1 项</span>' +
|
||||
'<div style="display:flex;gap:8px">' +
|
||||
'<button class="danger">批量删除</button>' +
|
||||
'<button class="btn">完成</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderCards() {
|
||||
return (
|
||||
phone("Soft · 柔阴影",
|
||||
'<div class="s1-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="s1-head"><div class="s1-count">共 <strong>3</strong> 条</div><div class="s1-link">管理</div></div>' +
|
||||
'<div class="list">' + ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2], { variant: "v-soft" }); }).join("") + '</div>'
|
||||
) +
|
||||
phone("Accent · 左色条",
|
||||
'<div class="s1-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="s1-head"><div class="s1-count">共 <strong>3</strong> 条</div><div class="s1-link">管理</div></div>' +
|
||||
'<div class="list">' + ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2], { variant: "v-accent" }); }).join("") + '</div>'
|
||||
) +
|
||||
phone("Flat · 扁平一行元信息",
|
||||
'<div class="s1-toolbar"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="s1-head"><div class="s1-count">共 <strong>3</strong> 条</div><div class="s1-link">管理</div></div>' +
|
||||
'<div class="list">' + ROWS.map(function (r) { return cardHtml(r[0], r[1], r[2], { variant: "v-flat", inlineMeta: true }); }).join("") + '</div>'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const RENDER = {
|
||||
a0: renderA0,
|
||||
s1: renderS1,
|
||||
s2: renderS2,
|
||||
s3: renderS3,
|
||||
s4: renderS4,
|
||||
cards: renderCards
|
||||
};
|
||||
|
||||
const descEl = document.getElementById("desc");
|
||||
const phonesEl = document.getElementById("phones");
|
||||
const tabs = document.getElementById("tabs");
|
||||
|
||||
function show(key) {
|
||||
var d = DESCS[key];
|
||||
descEl.innerHTML =
|
||||
"<h2>" + d.title + "</h2>" +
|
||||
'<p class="sub">' + d.sub + "</p>" +
|
||||
"<ul>" + d.items.map(function (i) { return "<li>" + i + "</li>"; }).join("") + "</ul>" +
|
||||
'<div class="pick">' + d.pick + "</div>";
|
||||
phonesEl.innerHTML = RENDER[key]();
|
||||
}
|
||||
|
||||
tabs.addEventListener("click", function (e) {
|
||||
var btn = e.target.closest(".tab");
|
||||
if (!btn) return;
|
||||
tabs.querySelectorAll(".tab").forEach(function (t) {
|
||||
t.classList.toggle("is-active", t === btn);
|
||||
});
|
||||
show(btn.getAttribute("data-k"));
|
||||
});
|
||||
|
||||
show("a0");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,426 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>草稿箱 · 标题栏管理 + 列表是否显示按钮</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--warn: #d97706;
|
||||
--warn-soft: #fff7e8;
|
||||
--danger: #ef4444;
|
||||
--nav-bg: #ecfdf5;
|
||||
--page-bg: #f5f8f7;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1200px; margin: 0 auto; padding: 28px 16px 56px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 780px; }
|
||||
.hero .note {
|
||||
margin-top: 12px; display: inline-block; padding: 8px 12px;
|
||||
border-radius: 10px; background: #fff; border: 1px solid #e2e8f0;
|
||||
font-size: 12px; color: #64748b;
|
||||
}
|
||||
.hero .note strong { color: #0f766e; }
|
||||
|
||||
.rec {
|
||||
margin: 16px 0 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: var(--brand-soft);
|
||||
border: 1px solid #b7ebd4;
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.rec b { color: var(--brand-dark); }
|
||||
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0 16px; }
|
||||
.tab {
|
||||
appearance: none; border: 1px solid #e2e8f0; background: #fff;
|
||||
border-radius: 999px; padding: 10px 16px; font-size: 13px;
|
||||
font-weight: 600; color: #475569; cursor: pointer;
|
||||
}
|
||||
.tab.is-active {
|
||||
color: var(--brand-dark); border-color: var(--brand);
|
||||
background: var(--brand-soft);
|
||||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
|
||||
}
|
||||
.tab .badge {
|
||||
margin-left: 6px; font-size: 10px; padding: 2px 6px;
|
||||
border-radius: 999px; background: #ecfdf5; color: #047857; font-weight: 700;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||||
gap: 20px; align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.panel h2 { margin: 0 0 4px; font-size: 15px; }
|
||||
.panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
|
||||
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
|
||||
.panel li { margin: 5px 0; }
|
||||
.panel .pick {
|
||||
margin-top: 12px; padding: 10px 12px; border-radius: 10px;
|
||||
background: #fff7e8; border: 1px solid #fde68a;
|
||||
font-size: 12px; color: #92400e;
|
||||
}
|
||||
|
||||
.phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
|
||||
.phone {
|
||||
width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
|
||||
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
|
||||
}
|
||||
.cap {
|
||||
text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
|
||||
padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
|
||||
}
|
||||
|
||||
/* 模拟系统标题栏(对齐 pages.json #ecfdf5) */
|
||||
.nav {
|
||||
height: 44px; display: flex; align-items: center; padding: 0 8px 0 4px;
|
||||
background: var(--nav-bg); border-bottom: 1px solid #d8f3e5;
|
||||
position: relative;
|
||||
}
|
||||
.nav-back {
|
||||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||||
color: #0f291e; flex-shrink: 0;
|
||||
}
|
||||
.nav-back::before {
|
||||
content: ""; width: 10px; height: 10px;
|
||||
border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
|
||||
transform: rotate(45deg); margin-left: 4px;
|
||||
}
|
||||
.nav-title {
|
||||
position: absolute; left: 50%; transform: translateX(-50%);
|
||||
font-size: 16px; font-weight: 650; color: #0f291e;
|
||||
}
|
||||
.nav-right {
|
||||
margin-left: auto; padding: 0 10px; height: 36px;
|
||||
display: inline-flex; align-items: center;
|
||||
font-size: 14px; font-weight: 650; color: var(--brand-dark);
|
||||
cursor: pointer; user-select: none;
|
||||
}
|
||||
.nav-right.is-done { color: #64748b; }
|
||||
|
||||
.page { background: var(--page-bg); min-height: 560px; padding-bottom: 16px; }
|
||||
.search-wrap { padding: 10px 12px; background: #fff; border-bottom: 1px solid #f1f5f9; }
|
||||
.search {
|
||||
height: 36px; display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px; border-radius: 10px; border: 1px solid #e8eaed;
|
||||
background: #fafafa; font-size: 13px; color: #94a3b8;
|
||||
}
|
||||
.search::before {
|
||||
content: ""; width: 12px; height: 12px; border: 1.5px solid #94a3b8;
|
||||
border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list { padding: 10px 12px 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.end { text-align: center; padding: 12px 0; font-size: 12px; color: #94a3b8; }
|
||||
|
||||
.card {
|
||||
width: 100%; background: #fff; border-radius: 14px;
|
||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05);
|
||||
overflow: hidden; border: 1px solid transparent;
|
||||
}
|
||||
.card-body { padding: 14px; }
|
||||
.card-top { display: flex; align-items: flex-start; gap: 10px; }
|
||||
.card-title { flex: 1; min-width: 0; font-size: 15px; font-weight: 700; line-height: 1.35; }
|
||||
.card-status {
|
||||
flex-shrink: 0; font-size: 11px; font-weight: 700;
|
||||
padding: 3px 9px; border-radius: 999px;
|
||||
background: var(--warn-soft); color: var(--warn);
|
||||
}
|
||||
.meta {
|
||||
margin-top: 8px; font-size: 12px; color: #64748b;
|
||||
}
|
||||
.meta .dot { margin: 0 6px; color: #cbd5e1; }
|
||||
.hint-tap {
|
||||
margin-top: 8px; font-size: 11px; color: #94a3b8;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex; gap: 8px; padding: 10px 12px 12px;
|
||||
border-top: 1px dashed rgba(140, 170, 155, 0.28);
|
||||
}
|
||||
.card-footer button {
|
||||
flex: 1; height: 34px; border-radius: 10px; border: 1px solid #d8e3de;
|
||||
background: #fff; font-size: 12px; font-weight: 650; color: #334155;
|
||||
}
|
||||
.card-footer .p { background: var(--brand); border-color: var(--brand); color: #fff; }
|
||||
.card-footer .d {
|
||||
color: var(--danger); border-color: rgba(239,68,68,.35);
|
||||
background: rgba(239,68,68,.06);
|
||||
}
|
||||
|
||||
.row { display: flex; align-items: center; gap: 10px; }
|
||||
.row .card { flex: 1; min-width: 0; }
|
||||
.check {
|
||||
width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid #cbd5e1;
|
||||
background: #fff; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 11px; color: #fff; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.check.on { background: #22c55e; border-color: #22c55e; }
|
||||
.row.is-on .card {
|
||||
border-color: rgba(34, 197, 94, 0.45);
|
||||
box-shadow: 0 8px 20px rgba(22, 163, 74, 0.12);
|
||||
}
|
||||
.batch {
|
||||
margin: 10px 12px 0; padding: 10px 12px; border-radius: 12px;
|
||||
background: #fff; border: 1px solid #c6f0dc;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 13px; color: #166534;
|
||||
}
|
||||
.batch button {
|
||||
height: 30px; padding: 0 12px; border-radius: 8px;
|
||||
border: 1px solid rgba(239,68,68,.35);
|
||||
background: rgba(239,68,68,.06); color: var(--danger);
|
||||
font-size: 12px; font-weight: 650;
|
||||
}
|
||||
|
||||
/* 详情底栏示意 */
|
||||
.detail {
|
||||
display: flex; flex-direction: column; min-height: 560px;
|
||||
background: var(--page-bg);
|
||||
}
|
||||
.detail-body { flex: 1; padding: 12px; }
|
||||
.detail-sheet {
|
||||
background: #fff; border-radius: 14px; padding: 16px;
|
||||
box-shadow: 0 8px 22px rgba(15,23,42,.05);
|
||||
}
|
||||
.detail-sheet h3 { margin: 0 0 12px; font-size: 17px; }
|
||||
.detail-sheet .line {
|
||||
display: flex; gap: 10px; font-size: 13px; margin: 8px 0; color: #475569;
|
||||
}
|
||||
.detail-sheet .line span:first-child { color: #94a3b8; min-width: 64px; }
|
||||
.detail-foot {
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(255,255,255,.96);
|
||||
border-top: 1px solid #e8eeeb;
|
||||
display: flex; gap: 8px;
|
||||
}
|
||||
.detail-foot button {
|
||||
height: 44px; border-radius: 12px; border: 0;
|
||||
font-size: 15px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.detail-foot .ghost { flex: 0 0 72px; background: #f1f5f9; color: #64748b; }
|
||||
.detail-foot .main { flex: 1; background: linear-gradient(135deg, #0a9f6d, #087f57); color: #fff; }
|
||||
.detail-foot .danger { flex: 0 0 72px; background: rgba(239,68,68,.08); color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>草稿箱 · 标题栏「管理」+ 列表是否放按钮</h1>
|
||||
<p>
|
||||
回应两点:①「管理」放到导航栏右侧;②列表卡片是否还要「编辑 / 提交 / 删除」。
|
||||
右侧每个方案可左右滑动对比普通态 / 管理态(或详情底栏)。
|
||||
</p>
|
||||
<div class="note">落地时草稿页可用系统导航栏右侧文字按钮,或自定义顶栏(与工作台一致)。</div>
|
||||
<div class="rec">
|
||||
<b>建议:</b>标题栏右侧「管理」+ <b>列表不展示操作按钮</b>,点卡片进入编辑/详情,
|
||||
「提交审批 / 删除」放详情(或编辑页)底部。列表更干净,也符合多数 OA / 邮件 App 习惯。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs">
|
||||
<button class="tab is-active" data-k="nav_clean">标题栏管理 · 列表无按钮<span class="badge">推荐</span></button>
|
||||
<button class="tab" data-k="nav_btns">标题栏管理 · 列表仍有按钮</button>
|
||||
<button class="tab" data-k="detail">详情底栏操作示意</button>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="panel" id="desc"></div>
|
||||
<div class="phones" id="phones"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ROWS = [
|
||||
["入帐单", "08-12 17:03", "v5"],
|
||||
["出差申请", "08-11 09:18", "v3"],
|
||||
["采购报销", "08-10 17:05", "v2"]
|
||||
];
|
||||
|
||||
function nav(rightText, rightDone) {
|
||||
return (
|
||||
'<div class="nav">' +
|
||||
'<div class="nav-back"></div>' +
|
||||
'<div class="nav-title">草稿箱</div>' +
|
||||
'<div class="nav-right' + (rightDone ? ' is-done' : '') + '">' + rightText + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function card(title, time, ver, withBtns) {
|
||||
return (
|
||||
'<div class="card">' +
|
||||
'<div class="card-body">' +
|
||||
'<div class="card-top">' +
|
||||
'<div class="card-title">' + title + '</div>' +
|
||||
'<div class="card-status">草稿</div>' +
|
||||
'</div>' +
|
||||
'<div class="meta">' + time + '<span class="dot">·</span>版本 ' + ver + '</div>' +
|
||||
(withBtns ? '' : '<div class="hint-tap">点击继续编辑</div>') +
|
||||
'</div>' +
|
||||
(withBtns
|
||||
? '<div class="card-footer"><button class="p">编辑</button><button>提交审批</button><button class="d">删除</button></div>'
|
||||
: '') +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function phone(cap, html) {
|
||||
return '<div class="phone"><div class="cap">' + cap + '</div>' + html + '</div>';
|
||||
}
|
||||
|
||||
const DESCS = {
|
||||
nav_clean: {
|
||||
title: "推荐 · 标题栏管理 + 列表无按钮",
|
||||
sub: "顶栏右侧「管理」;卡片只展示信息,整卡可点。",
|
||||
items: [
|
||||
"「管理」进导航栏右侧,搜索区干净",
|
||||
"列表无编辑/提交/删除,一屏信息更疏朗",
|
||||
"点卡片 → 进入编辑(现有逻辑已支持)",
|
||||
"删除用「管理」批量;提交放详情/编辑底栏",
|
||||
"管理态:勾选与卡片垂直居中"
|
||||
],
|
||||
pick: "与钉钉/飞书/企业微信草稿、邮件列表习惯一致。"
|
||||
},
|
||||
nav_btns: {
|
||||
title: "标题栏管理 · 列表保留按钮",
|
||||
sub: "管理上移后,卡片仍保留三按钮(当前能力不丢)。",
|
||||
items: [
|
||||
"管理入口问题已解决",
|
||||
"列表仍可一键提交/删除,少一次跳转",
|
||||
"代价:卡片更高、更挤,尤其短流程名时按钮区抢戏"
|
||||
],
|
||||
pick: "若强依赖「列表内直接提交」,可选此方案。"
|
||||
},
|
||||
detail: {
|
||||
title: "详情 / 编辑页底部操作",
|
||||
sub: "列表无按钮时,动作收口到详情底栏。",
|
||||
items: [
|
||||
"主按钮:提交审批(最高优先级)",
|
||||
"次按钮:删除(或更多里放删除)",
|
||||
"返回即退出编辑;草稿自动保存可沿用现逻辑"
|
||||
],
|
||||
pick: "与「列表无按钮」配套使用。"
|
||||
}
|
||||
};
|
||||
|
||||
function renderClean() {
|
||||
var list = ROWS.map(function (r) { return card(r[0], r[1], r[2], false); }).join("");
|
||||
var normal = phone("普通态",
|
||||
nav("管理", false) +
|
||||
'<div class="page"><div class="search-wrap"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="list">' + list + '<div class="end">已经到底了</div></div></div>'
|
||||
);
|
||||
var manage = phone("管理态 · 勾选居中",
|
||||
nav("完成", true) +
|
||||
'<div class="page"><div class="search-wrap"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="batch"><span>已选 1 项</span><button>批量删除</button></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="row is-on"><div class="check on">✓</div>' + card(ROWS[0][0], ROWS[0][1], ROWS[0][2], false).replace('hint-tap">点击继续编辑', 'hint-tap" style="display:none">') + '</div>' +
|
||||
'<div class="row"><div class="check"></div>' + card(ROWS[1][0], ROWS[1][1], ROWS[1][2], false).replace('hint-tap">点击继续编辑', 'hint-tap" style="display:none">') + '</div>' +
|
||||
'<div class="row"><div class="check"></div>' + card(ROWS[2][0], ROWS[2][1], ROWS[2][2], false).replace('hint-tap">点击继续编辑', 'hint-tap" style="display:none">') + '</div>' +
|
||||
'</div></div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderBtns() {
|
||||
var list = ROWS.map(function (r) { return card(r[0], r[1], r[2], true); }).join("");
|
||||
var normal = phone("普通态 · 仍有按钮",
|
||||
nav("管理", false) +
|
||||
'<div class="page"><div class="search-wrap"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="list">' + list + '<div class="end">已经到底了</div></div></div>'
|
||||
);
|
||||
var manage = phone("管理态",
|
||||
nav("完成", true) +
|
||||
'<div class="page"><div class="search-wrap"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="batch"><span>已选 1 项</span><button>批量删除</button></div>' +
|
||||
'<div class="list">' +
|
||||
'<div class="row is-on"><div class="check on">✓</div>' + card(ROWS[0][0], ROWS[0][1], ROWS[0][2], false) + '</div>' +
|
||||
'<div class="row"><div class="check"></div>' + card(ROWS[1][0], ROWS[1][1], ROWS[1][2], false) + '</div>' +
|
||||
'</div></div>'
|
||||
);
|
||||
return normal + manage;
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
return phone("编辑 / 详情 · 底栏",
|
||||
nav("", false).replace('nav-right">', 'nav-right" style="display:none">') +
|
||||
'<div class="detail">' +
|
||||
'<div class="detail-body">' +
|
||||
'<div class="detail-sheet">' +
|
||||
'<h3>入帐单</h3>' +
|
||||
'<div class="line"><span>状态</span><span>草稿</span></div>' +
|
||||
'<div class="line"><span>最近保存</span><span>2026-08-12 17:03:42</span></div>' +
|
||||
'<div class="line"><span>版本</span><span>v5</span></div>' +
|
||||
'<div class="line"><span>说明</span><span>表单正文区域…</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="detail-foot">' +
|
||||
'<button class="danger">删除</button>' +
|
||||
'<button class="main">提交审批</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
) + phone("列表侧(无按钮)",
|
||||
nav("管理", false) +
|
||||
'<div class="page"><div class="search-wrap"><div class="search">搜索流程名称</div></div>' +
|
||||
'<div class="list">' +
|
||||
ROWS.map(function (r) { return card(r[0], r[1], r[2], false); }).join("") +
|
||||
'</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
const RENDER = { nav_clean: renderClean, nav_btns: renderBtns, detail: renderDetail };
|
||||
const descEl = document.getElementById("desc");
|
||||
const phonesEl = document.getElementById("phones");
|
||||
const tabs = document.getElementById("tabs");
|
||||
|
||||
function show(k) {
|
||||
var d = DESCS[k];
|
||||
descEl.innerHTML =
|
||||
"<h2>" + d.title + "</h2>" +
|
||||
'<p class="sub">' + d.sub + "</p>" +
|
||||
"<ul>" + d.items.map(function (i) { return "<li>" + i + "</li>"; }).join("") + "</ul>" +
|
||||
'<div class="pick">' + d.pick + "</div>";
|
||||
phonesEl.innerHTML = RENDER[k]();
|
||||
}
|
||||
|
||||
tabs.addEventListener("click", function (e) {
|
||||
var btn = e.target.closest(".tab");
|
||||
if (!btn) return;
|
||||
tabs.querySelectorAll(".tab").forEach(function (t) {
|
||||
t.classList.toggle("is-active", t === btn);
|
||||
});
|
||||
show(btn.getAttribute("data-k"));
|
||||
});
|
||||
|
||||
show("nav_clean");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,593 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>OA 五列表 · 序号圆圈背景 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--warn: #d97706;
|
||||
--danger: #ef4444;
|
||||
--nav-bg: #ecfdf5;
|
||||
--page-bg: #f5f6f8;
|
||||
--text: #1f2329;
|
||||
--text-muted: #8f959e;
|
||||
--border: #e8eaed;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1180px; margin: 0 auto; padding: 28px 16px 64px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 920px; }
|
||||
.rec {
|
||||
margin: 16px 0 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: var(--brand-soft);
|
||||
border: 1px solid #b7ebd4;
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.rec b { color: var(--brand-dark); }
|
||||
|
||||
.tabs {
|
||||
display: flex; flex-wrap: wrap; gap: 10px;
|
||||
margin: 20px 0 18px;
|
||||
}
|
||||
.tab {
|
||||
appearance: none; border: 1px solid #e2e8f0; background: #fff;
|
||||
border-radius: 999px; padding: 10px 16px; font-size: 13px;
|
||||
font-weight: 600; color: #475569; cursor: pointer;
|
||||
}
|
||||
.tab.is-active {
|
||||
color: var(--brand-dark); border-color: var(--brand);
|
||||
background: var(--brand-soft);
|
||||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
|
||||
}
|
||||
.tab .badge {
|
||||
display: inline-block; margin-left: 6px;
|
||||
padding: 1px 7px; border-radius: 999px;
|
||||
font-size: 11px; font-weight: 700;
|
||||
background: #fef3c7; color: #b45309;
|
||||
}
|
||||
.tab.is-active .badge {
|
||||
background: #fff; color: var(--brand-dark);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||||
gap: 20px; align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.panel h2 { margin: 0 0 4px; font-size: 15px; }
|
||||
.panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
|
||||
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
|
||||
.panel li { margin: 6px 0; }
|
||||
.panel .ok {
|
||||
margin-top: 12px; padding: 10px 12px; border-radius: 10px;
|
||||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||||
font-size: 12px; color: #047857;
|
||||
}
|
||||
.panel .pick {
|
||||
margin-top: 10px; padding: 10px 12px; border-radius: 10px;
|
||||
background: #fff7e8; border: 1px solid #fde68a;
|
||||
font-size: 12px; color: #92400e;
|
||||
}
|
||||
|
||||
.phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
|
||||
.phone {
|
||||
width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
|
||||
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
|
||||
}
|
||||
.cap {
|
||||
text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
|
||||
padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
|
||||
}
|
||||
.nav {
|
||||
height: 44px; display: flex; align-items: center; padding: 0 8px 0 4px;
|
||||
background: var(--nav-bg); border-bottom: 1px solid #d8f3e5;
|
||||
position: relative;
|
||||
}
|
||||
.nav-back {
|
||||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||||
color: #0f291e; flex-shrink: 0;
|
||||
}
|
||||
.nav-back::before {
|
||||
content: ""; width: 10px; height: 10px;
|
||||
border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
|
||||
transform: rotate(45deg); margin-left: 4px;
|
||||
}
|
||||
.nav-title {
|
||||
position: absolute; left: 50%; transform: translateX(-50%);
|
||||
font-size: 16px; font-weight: 650; color: #0f291e;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 640px;
|
||||
background: var(--page-bg);
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
.toolbar {
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.search-box {
|
||||
height: 36px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px; border-radius: 10px;
|
||||
background: #fafafa; border: 1px solid #e8eaed;
|
||||
}
|
||||
.search-ico {
|
||||
width: 13px; height: 13px; border: 1.5px solid #94a3b8; border-radius: 50%;
|
||||
position: relative; flex-shrink: 0;
|
||||
}
|
||||
.search-ico::after {
|
||||
content: ""; position: absolute; width: 6px; height: 1.5px;
|
||||
right: -4px; bottom: -2px; background: #94a3b8; border-radius: 2px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.search-ph { font-size: 13px; color: #b0b7c3; }
|
||||
|
||||
.list {
|
||||
padding: 10px 12px 0;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
|
||||
.dd-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 14px 14px 12px;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
.dd-card__head {
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
}
|
||||
.dd-card__title {
|
||||
flex: 1; min-width: 0;
|
||||
font-size: 15px; font-weight: 700; line-height: 1.4; color: #1f2329;
|
||||
}
|
||||
.dd-card__time {
|
||||
flex-shrink: 0; font-size: 12px; line-height: 1.4; color: #8f959e; padding-top: 2px;
|
||||
}
|
||||
.dd-card__meta { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.dd-card__meta-line {
|
||||
font-size: 13px; line-height: 1.45; color: #8f959e;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.dd-card__foot {
|
||||
margin-top: 12px; display: flex; align-items: center;
|
||||
justify-content: space-between; gap: 8px;
|
||||
}
|
||||
.dd-card__hint-tag {
|
||||
max-width: 70%;
|
||||
font-size: 11px; font-weight: 650; line-height: 1.2;
|
||||
padding: 4px 9px; border-radius: 999px;
|
||||
background: rgba(217, 119, 6, 0.12); color: #d97706;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.dd-card__hint-tag.is-success {
|
||||
background: rgba(10, 159, 109, 0.12); color: #0a9f6d;
|
||||
}
|
||||
.dd-card__hint-tag.is-danger {
|
||||
background: rgba(239, 68, 68, 0.12); color: #ef4444;
|
||||
}
|
||||
.dd-card__status {
|
||||
flex-shrink: 0; font-size: 12px; font-weight: 600; color: #d97706;
|
||||
}
|
||||
.dd-card__status.is-success { color: #0a9f6d; }
|
||||
.dd-card__status.is-danger { color: #ef4444; }
|
||||
|
||||
/* ---------- 0:现状折角 ---------- */
|
||||
.style-0 .idx {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 0; height: 0;
|
||||
border-style: solid;
|
||||
border-width: 34px 34px 0 0;
|
||||
border-color: var(--brand) transparent transparent transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
.style-0 .idx span {
|
||||
position: absolute; top: -30px; left: 4px;
|
||||
font-size: 10px; font-weight: 800; color: #fff;
|
||||
line-height: 1; letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* 圆圈共用:左上绝对定位 */
|
||||
.circle-idx {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.style-a .dd-card,
|
||||
.style-b .dd-card,
|
||||
.style-c .dd-card,
|
||||
.style-d .dd-card {
|
||||
padding-left: 42px;
|
||||
}
|
||||
|
||||
/* A:实心品牌绿圆(推荐) */
|
||||
.style-a .circle-idx {
|
||||
color: #fff;
|
||||
background: var(--brand);
|
||||
box-shadow: 0 2px 6px rgba(10, 159, 109, 0.28);
|
||||
}
|
||||
|
||||
/* B:浅绿底 + 深绿字 */
|
||||
.style-b .circle-idx {
|
||||
color: var(--brand-dark);
|
||||
background: var(--brand-soft);
|
||||
border: 1px solid #b7ebd4;
|
||||
}
|
||||
|
||||
/* C:描边空心圆 */
|
||||
.style-c .circle-idx {
|
||||
color: var(--brand-dark);
|
||||
background: #fff;
|
||||
border: 1.5px solid var(--brand);
|
||||
}
|
||||
|
||||
/* D:贴角半出入圆(更像「角标」) */
|
||||
.style-d .dd-card { overflow: visible; }
|
||||
.style-d .circle-idx {
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
background: linear-gradient(145deg, #12b981 0%, #0a9f6d 55%, #087f57 100%);
|
||||
box-shadow: 0 4px 10px rgba(10, 159, 109, 0.28);
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
.style-d .dd-card { padding-left: 14px; }
|
||||
|
||||
.swatches {
|
||||
margin-top: 18px;
|
||||
display: flex; flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.swatch {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; border-radius: 999px;
|
||||
background: #fff; border: 1px solid #e2e8f0;
|
||||
font-size: 12px; color: #475569;
|
||||
}
|
||||
.swatch .dot {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 9px; font-weight: 800;
|
||||
}
|
||||
.swatch .dot.a { color: #fff; background: var(--brand); }
|
||||
.swatch .dot.b { color: var(--brand-dark); background: var(--brand-soft); border: 1px solid #b7ebd4; }
|
||||
.swatch .dot.c { color: var(--brand-dark); background: #fff; border: 1.5px solid var(--brand); }
|
||||
.swatch .dot.d {
|
||||
color: #fff;
|
||||
background: linear-gradient(145deg, #12b981, #087f57);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 6px rgba(10, 159, 109, 0.25);
|
||||
}
|
||||
|
||||
.legend {
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.legend .box {
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
|
||||
padding: 12px 14px; font-size: 12px; color: #475569;
|
||||
}
|
||||
.legend h3 { margin: 0 0 6px; font-size: 13px; color: #0f291e; }
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 11px; background: #f1f5f9; padding: 1px 5px; border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>OA 五列表 · 序号改为圆圈背景</h1>
|
||||
<p>
|
||||
覆盖草稿箱 / 待办 / 已办 / 我发起的 / 抄送我的。现状为左上三角折角;
|
||||
本 Demo 对比「圆圈背景」几种落法。序号规则不变:<code>01</code>、<code>02</code>…跨页连续。
|
||||
</p>
|
||||
<div class="rec">
|
||||
<b>推荐:A 实心绿圆</b> —— 仍在卡片左上,品牌绿实心圆 + 白字两位序号;
|
||||
只改 <code>.dd-card__corner</code> 样式,五页 DOM 可基本不动。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs">
|
||||
<button type="button" class="tab" data-k="0">0 现状折角</button>
|
||||
<button type="button" class="tab is-active" data-k="a">A 实心绿圆<span class="badge">推荐</span></button>
|
||||
<button type="button" class="tab" data-k="b">B 浅绿圆</button>
|
||||
<button type="button" class="tab" data-k="c">C 描边圆</button>
|
||||
<button type="button" class="tab" data-k="d">D 贴角出入圆</button>
|
||||
</div>
|
||||
|
||||
<div class="swatches" aria-hidden="true">
|
||||
<span class="swatch"><span class="dot a">01</span>A 实心</span>
|
||||
<span class="swatch"><span class="dot b">02</span>B 浅绿</span>
|
||||
<span class="swatch"><span class="dot c">03</span>C 描边</span>
|
||||
<span class="swatch"><span class="dot d">04</span>D 贴角</span>
|
||||
</div>
|
||||
|
||||
<div class="layout" style="margin-top: 18px;">
|
||||
<aside class="panel" id="side"></aside>
|
||||
<div class="phones">
|
||||
<div class="phone">
|
||||
<div class="cap" id="cap">A 实心绿圆 · 推荐 · 375px</div>
|
||||
<div class="nav">
|
||||
<div class="nav-back" aria-hidden="true"></div>
|
||||
<div class="nav-title">待办中心</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<div class="toolbar">
|
||||
<div class="search-box">
|
||||
<span class="search-ico" aria-hidden="true"></span>
|
||||
<span class="search-ph">搜索流程名称</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list style-a" id="list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="box">
|
||||
<h3>落地建议</h3>
|
||||
仅改 <code>oa-task-list.scss</code> 中 <code>.dd-card__corner</code> /
|
||||
<code>.dd-card__corner-num</code>;五页模板结构可保留,避免重复改 Vue。
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3>序号规则</h3>
|
||||
仍用 <code>formatCornerIndex</code>:<10 补零;
|
||||
跨页 <code>(page-1)*pageSize + index + 1</code>。
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3>注意</h3>
|
||||
A/B/C 需给标题区留左内边距(约 42px),避免圆圈压住标题;
|
||||
D 贴角方案可不加左 padding,但卡片需 <code>overflow: visible</code>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ROWS = [
|
||||
{
|
||||
title: '费用报销',
|
||||
time: '09:12',
|
||||
meta: ['报销金额:1280.00', '事由:差旅交通'],
|
||||
hint: '待张敏处理',
|
||||
status: '进行中',
|
||||
tone: 'warn'
|
||||
},
|
||||
{
|
||||
title: '请假申请',
|
||||
time: '昨天',
|
||||
meta: ['请假类型:事假', '请假天数:1'],
|
||||
hint: '待李华处理',
|
||||
status: '进行中',
|
||||
tone: 'warn'
|
||||
},
|
||||
{
|
||||
title: '采购申请',
|
||||
time: '前天',
|
||||
meta: ['采购金额:8600.00', '供应商:华东物资'],
|
||||
hint: '审批完成',
|
||||
status: '已完成',
|
||||
tone: 'success'
|
||||
},
|
||||
{
|
||||
title: '出差申请',
|
||||
time: '3天前',
|
||||
meta: ['目的地:上海', '天数:2'],
|
||||
hint: '已打回',
|
||||
status: '已打回',
|
||||
tone: 'danger'
|
||||
},
|
||||
{
|
||||
title: '用章申请 · 两位数序号样例',
|
||||
time: '2026.08.01',
|
||||
meta: ['用章类型:公章', '份数:2'],
|
||||
hint: '抄送我的',
|
||||
status: '已撤销',
|
||||
tone: 'mute',
|
||||
forceN: 12
|
||||
}
|
||||
]
|
||||
|
||||
const SIDE = {
|
||||
'0': {
|
||||
title: '0 现状折角',
|
||||
sub: '三角折角 + 白字',
|
||||
items: [
|
||||
'当前五列表已上线样式',
|
||||
'视觉强,但标题区被三角切角',
|
||||
'两位数在三角里偏挤'
|
||||
],
|
||||
note: 'pick',
|
||||
noteText: '将被圆圈方案替换。'
|
||||
},
|
||||
a: {
|
||||
title: 'A 实心绿圆',
|
||||
sub: '左上 24px 圆 · 白字',
|
||||
items: [
|
||||
'品牌绿实心,辨识度接近折角',
|
||||
'圆内两位序号更规整',
|
||||
'卡片加左 padding,标题不被遮'
|
||||
],
|
||||
note: 'ok',
|
||||
noteText: '推荐默认方案;改动面最小。'
|
||||
},
|
||||
b: {
|
||||
title: 'B 浅绿圆',
|
||||
sub: '浅绿底 + 深绿字',
|
||||
items: [
|
||||
'更轻、更「信息角标」',
|
||||
'与三墙 soft 色一致',
|
||||
'在白卡片上对比略弱于 A'
|
||||
],
|
||||
note: 'ok',
|
||||
noteText: '若希望序号不抢眼,选 B。'
|
||||
},
|
||||
c: {
|
||||
title: 'C 描边圆',
|
||||
sub: '白底 + 绿描边',
|
||||
items: [
|
||||
'最轻量',
|
||||
'适合信息密度高的列表',
|
||||
'远看不如实心醒目'
|
||||
],
|
||||
note: 'pick',
|
||||
noteText: '备选;偏「弱序号」。'
|
||||
},
|
||||
d: {
|
||||
title: 'D 贴角出入圆',
|
||||
sub: '压住左上圆角 · 半出入',
|
||||
items: [
|
||||
'更像「角标徽章」',
|
||||
'不挤标题左 padding',
|
||||
'需 overflow:visible,注意阴影裁切'
|
||||
],
|
||||
note: 'ok',
|
||||
noteText: '想保留「角标感」时优先于 A。'
|
||||
}
|
||||
}
|
||||
|
||||
const CAP = {
|
||||
'0': '0 现状折角 · 375px',
|
||||
a: 'A 实心绿圆 · 推荐 · 375px',
|
||||
b: 'B 浅绿圆 · 375px',
|
||||
c: 'C 描边圆 · 375px',
|
||||
d: 'D 贴角出入圆 · 375px'
|
||||
}
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : String(n)
|
||||
}
|
||||
|
||||
function render(style) {
|
||||
const list = document.getElementById('list')
|
||||
list.className = 'list style-' + style
|
||||
list.innerHTML = ROWS.map((row, i) => {
|
||||
const n = row.forceN != null ? row.forceN : i + 1
|
||||
const hintClass =
|
||||
'dd-card__hint-tag' +
|
||||
(row.tone === 'success' ? ' is-success' : '') +
|
||||
(row.tone === 'danger' ? ' is-danger' : '')
|
||||
const statusClass =
|
||||
'dd-card__status' +
|
||||
(row.tone === 'success' ? ' is-success' : '') +
|
||||
(row.tone === 'danger' ? ' is-danger' : '')
|
||||
|
||||
let badge = ''
|
||||
if (style === '0') {
|
||||
badge = '<div class="idx" aria-hidden="true"><span>' + pad(n) + '</span></div>'
|
||||
} else {
|
||||
badge = '<div class="circle-idx" aria-hidden="true">' + pad(n) + '</div>'
|
||||
}
|
||||
|
||||
const meta = row.meta
|
||||
.map((m) => '<span class="dd-card__meta-line">' + m + '</span>')
|
||||
.join('')
|
||||
|
||||
return (
|
||||
'<div class="dd-card">' +
|
||||
badge +
|
||||
'<div class="dd-card__head">' +
|
||||
'<div class="dd-card__title">' +
|
||||
row.title +
|
||||
'</div>' +
|
||||
'<div class="dd-card__time">' +
|
||||
row.time +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="dd-card__meta">' +
|
||||
meta +
|
||||
'</div>' +
|
||||
'<div class="dd-card__foot">' +
|
||||
'<span class="' +
|
||||
hintClass +
|
||||
'">' +
|
||||
row.hint +
|
||||
'</span>' +
|
||||
'<span class="' +
|
||||
statusClass +
|
||||
'">' +
|
||||
row.status +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)
|
||||
}).join('')
|
||||
|
||||
const s = SIDE[style]
|
||||
document.getElementById('side').innerHTML =
|
||||
'<h2>' +
|
||||
s.title +
|
||||
'</h2>' +
|
||||
'<p class="sub">' +
|
||||
s.sub +
|
||||
'</p>' +
|
||||
'<ul>' +
|
||||
s.items.map((t) => '<li>' + t + '</li>').join('') +
|
||||
'</ul>' +
|
||||
'<div class="' +
|
||||
s.note +
|
||||
'">' +
|
||||
s.noteText +
|
||||
'</div>'
|
||||
|
||||
document.getElementById('cap').textContent = CAP[style]
|
||||
}
|
||||
|
||||
document.getElementById('tabs').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.tab')
|
||||
if (!btn) return
|
||||
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('is-active'))
|
||||
btn.classList.add('is-active')
|
||||
render(btn.dataset.k)
|
||||
})
|
||||
|
||||
render('a')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,484 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>mobile-h5 · OA 列表空态 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--warn: #d97706;
|
||||
--warn-soft: #fff7e8;
|
||||
--nav-bg: #ecfdf5;
|
||||
--page-bg: #f5f6f8;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
radial-gradient(700px 380px at 92% 0%, rgba(214, 163, 67, 0.1), transparent 50%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1280px; margin: 0 auto; padding: 28px 16px 56px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 800px; }
|
||||
.hero .note {
|
||||
margin-top: 12px; display: inline-block; padding: 8px 12px;
|
||||
border-radius: 10px; background: #fff; border: 1px solid #e2e8f0;
|
||||
font-size: 12px; color: #64748b;
|
||||
}
|
||||
.hero .note strong { color: #0f766e; }
|
||||
|
||||
.pain {
|
||||
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
||||
background: #fff1f2; border: 1px solid #fecdd3;
|
||||
font-size: 13px; color: #9f1239;
|
||||
}
|
||||
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0 16px; }
|
||||
.tab {
|
||||
appearance: none; border: 1px solid #e2e8f0; background: #fff;
|
||||
border-radius: 999px; padding: 10px 16px; font-size: 13px;
|
||||
font-weight: 600; color: #475569; cursor: pointer;
|
||||
}
|
||||
.tab.is-active {
|
||||
color: var(--brand-dark); border-color: var(--brand);
|
||||
background: var(--brand-soft);
|
||||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
|
||||
}
|
||||
.tab .badge {
|
||||
margin-left: 6px; font-size: 10px; padding: 2px 6px;
|
||||
border-radius: 999px; background: #ecfdf5; color: #047857; font-weight: 700;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||||
gap: 20px; align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.panel h2 { margin: 0 0 4px; font-size: 15px; }
|
||||
.panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
|
||||
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
|
||||
.panel li { margin: 5px 0; }
|
||||
.panel .pick {
|
||||
margin-top: 12px; padding: 10px 12px; border-radius: 10px;
|
||||
background: var(--brand-soft); border: 1px solid #b7ebd4;
|
||||
font-size: 12px; color: #166534;
|
||||
}
|
||||
|
||||
.phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
|
||||
.phone {
|
||||
width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
|
||||
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
|
||||
}
|
||||
.cap {
|
||||
text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
|
||||
padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
|
||||
}
|
||||
.nav {
|
||||
height: 44px; display: flex; align-items: center; padding: 0 8px 0 4px;
|
||||
background: var(--nav-bg); border-bottom: 1px solid #d8f3e5; position: relative;
|
||||
}
|
||||
.nav-back {
|
||||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.nav-back::before {
|
||||
content: ""; width: 10px; height: 10px;
|
||||
border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
|
||||
transform: rotate(45deg); margin-left: 4px;
|
||||
}
|
||||
.nav-title {
|
||||
position: absolute; left: 50%; transform: translateX(-50%);
|
||||
font-size: 16px; font-weight: 650; color: #0f291e;
|
||||
}
|
||||
.nav-right {
|
||||
margin-left: auto; padding: 0 10px; font-size: 14px;
|
||||
font-weight: 650; color: var(--brand-dark);
|
||||
}
|
||||
.page { background: var(--page-bg); min-height: 520px; }
|
||||
.search-wrap { padding: 10px 12px; background: #fff; border-bottom: 1px solid #f1f5f9; }
|
||||
.search {
|
||||
height: 36px; display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px; border-radius: 10px; border: 1px solid #e8eaed;
|
||||
background: #fafafa; font-size: 13px; color: #94a3b8;
|
||||
}
|
||||
.search::before {
|
||||
content: ""; width: 12px; height: 12px; border: 1.5px solid #94a3b8;
|
||||
border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* —— A0 现状 —— */
|
||||
.e0 {
|
||||
padding: 72px 16px; text-align: center;
|
||||
font-size: 14px; color: #5f7a6d; letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* —— E1 插画 + 主副文案 —— */
|
||||
.e1 {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 56px 28px 40px; text-align: center;
|
||||
}
|
||||
.e1-art {
|
||||
width: 120px; height: 120px; border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #fff, #d8f3e7 72%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 18px;
|
||||
box-shadow: 0 10px 28px rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
.e1-art svg { width: 56px; height: 56px; }
|
||||
.e1-title { font-size: 16px; font-weight: 700; color: #1f2a26; margin-bottom: 8px; }
|
||||
.e1-desc { font-size: 13px; color: #6b7b74; line-height: 1.55; max-width: 240px; }
|
||||
|
||||
/* —— E2 虚线软卡(对齐首页 VanEmpty) —— */
|
||||
.e2-wrap { padding: 28px 16px; }
|
||||
.e2 {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 36px 20px 28px; text-align: center;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(165deg, #eef9f3 0%, #ffffff 55%);
|
||||
border: 1.5px dashed #b7e5d0;
|
||||
}
|
||||
.e2-blob {
|
||||
width: 72px; height: 72px; border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #fff, #d8f3e7 70%);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.e2-blob svg { width: 32px; height: 32px; }
|
||||
.e2-title { font-size: 14px; font-weight: 650; color: #5d746a; margin-bottom: 6px; }
|
||||
.e2-desc { font-size: 12px; color: #8aa198; line-height: 1.5; }
|
||||
|
||||
/* —— E3 带主按钮 CTA —— */
|
||||
.e3 {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 48px 28px 36px; text-align: center;
|
||||
}
|
||||
.e3-ico {
|
||||
width: 88px; height: 88px; border-radius: 28px;
|
||||
background: linear-gradient(145deg, #e9f9f2, #d1fae5);
|
||||
border: 1px solid #b7ebd4;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 8px 22px rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
.e3-ico svg { width: 40px; height: 40px; }
|
||||
.e3-title { font-size: 16px; font-weight: 700; margin-bottom: 8px; }
|
||||
.e3-desc { font-size: 13px; color: #6b7b74; margin-bottom: 20px; max-width: 250px; line-height: 1.55; }
|
||||
.e3-btn {
|
||||
height: 44px; padding: 0 28px; border: 0; border-radius: 12px;
|
||||
background: linear-gradient(135deg, #0a9f6d, #087f57); color: #fff;
|
||||
font-size: 15px; font-weight: 700; cursor: pointer;
|
||||
box-shadow: 0 8px 18px rgba(10, 159, 109, 0.28);
|
||||
}
|
||||
.e3-link {
|
||||
margin-top: 14px; font-size: 13px; color: var(--brand-dark);
|
||||
font-weight: 650; cursor: pointer;
|
||||
}
|
||||
|
||||
/* —— E4 场景插画卡片 —— */
|
||||
.e4-wrap { padding: 24px 16px; }
|
||||
.e4 {
|
||||
background: #fff; border-radius: 18px; padding: 28px 20px 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
.e4-scene {
|
||||
height: 110px; margin-bottom: 16px; position: relative;
|
||||
display: flex; align-items: flex-end; justify-content: center;
|
||||
}
|
||||
.e4-desk {
|
||||
width: 160px; height: 14px; border-radius: 8px;
|
||||
background: #e2e8e6; position: absolute; bottom: 8px;
|
||||
}
|
||||
.e4-paper {
|
||||
width: 72px; height: 88px; border-radius: 10px;
|
||||
background: #fff; border: 2px solid #c6f0dc;
|
||||
box-shadow: 0 8px 16px rgba(15,23,42,.06);
|
||||
position: relative; z-index: 1;
|
||||
display: flex; flex-direction: column; gap: 8px; padding: 14px 12px;
|
||||
}
|
||||
.e4-paper i {
|
||||
display: block; height: 6px; border-radius: 3px; background: #d8f3e5;
|
||||
}
|
||||
.e4-paper i:nth-child(2) { width: 70%; }
|
||||
.e4-paper i:nth-child(3) { width: 45%; }
|
||||
.e4-badge {
|
||||
position: absolute; right: 88px; top: 10px; z-index: 2;
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
background: var(--warn-soft); border: 2px solid #f0d9a8;
|
||||
color: var(--warn); font-size: 18px; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.e4-title { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
|
||||
.e4-desc { font-size: 12px; color: #6b7b74; line-height: 1.5; margin-bottom: 16px; }
|
||||
.e4-btn {
|
||||
height: 40px; padding: 0 22px; border-radius: 10px; border: 0;
|
||||
background: var(--brand); color: #fff; font-size: 13px; font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* —— E5 搜索无结果变体 —— */
|
||||
.e5 {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 48px 28px; text-align: center;
|
||||
}
|
||||
.e5-ico {
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.e5-ico svg { width: 36px; height: 36px; }
|
||||
.e5-title { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
|
||||
.e5-desc { font-size: 13px; color: #6b7b74; margin-bottom: 16px; }
|
||||
.e5-chip {
|
||||
height: 34px; padding: 0 16px; border-radius: 999px;
|
||||
border: 1px solid #d8e3de; background: #fff;
|
||||
font-size: 13px; font-weight: 650; color: var(--brand-dark);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* —— E6 极简大字 + 微插画 —— */
|
||||
.e6 {
|
||||
padding: 64px 24px; text-align: center;
|
||||
}
|
||||
.e6-emoji {
|
||||
width: 64px; height: 64px; margin: 0 auto 14px;
|
||||
border-radius: 20px; background: var(--brand-soft);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.e6-emoji svg { width: 30px; height: 30px; }
|
||||
.e6-title { font-size: 15px; font-weight: 650; color: #334155; margin-bottom: 6px; }
|
||||
.e6-desc { font-size: 12px; color: #94a3b8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>OA 列表空态 · 风格选型</h1>
|
||||
<p>
|
||||
针对草稿箱 / 待办 / 已办 / 抄送等列表页。现状仅为灰色「暂无草稿」一行字。
|
||||
Token 对齐三墙绿;可与首页已有虚线软卡空态统一或做列表专用加强版。
|
||||
</p>
|
||||
<div class="note">请选 <strong>E1–E6</strong>(或组合:如 E3 布局 + E4 插画)。选定后落到 <code>mobile-h5</code>。</div>
|
||||
<div class="pain">现状痛点:信息弱、无引导、与钉钉风列表不协调。</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="tabs">
|
||||
<button class="tab is-active" data-k="e0">E0 现状</button>
|
||||
<button class="tab" data-k="e1">E1 圆形插画</button>
|
||||
<button class="tab" data-k="e2">E2 虚线软卡</button>
|
||||
<button class="tab" data-k="e3">E3 带 CTA<span class="badge">推荐</span></button>
|
||||
<button class="tab" data-k="e4">E4 场景卡片</button>
|
||||
<button class="tab" data-k="e5">E5 搜索无结果</button>
|
||||
<button class="tab" data-k="e6">E6 极简加强</button>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="panel" id="desc"></div>
|
||||
<div class="phones" id="phones"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||
<symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="#0a9f6d" stroke-width="1.7" 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"/>
|
||||
</symbol>
|
||||
<symbol id="i-doc" viewBox="0 0 24 24" fill="none" stroke="#0a9f6d" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="8" y1="13" x2="16" y2="13"/>
|
||||
<line x1="8" y1="17" x2="13" y2="17"/>
|
||||
</symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="7"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="#0a9f6d" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<script>
|
||||
const DESCS = {
|
||||
e0: {
|
||||
title: "E0 · 现状对照",
|
||||
sub: "OA 列表页当前实现:一行灰字。",
|
||||
items: ["无插画、无说明、无下一步引导", "与钉钉风卡片列表反差大", "仅作对照"],
|
||||
pick: "不建议保留。"
|
||||
},
|
||||
e1: {
|
||||
title: "E1 · 圆形插画",
|
||||
sub: "大圆形浅绿底 + 图标 + 主副文案,居中留白。",
|
||||
items: ["视觉重心明确", "无按钮,适合纯浏览空列表", "实现成本低"],
|
||||
pick: "适合已办/抄送等「没有也正常」的列表。"
|
||||
},
|
||||
e2: {
|
||||
title: "E2 · 虚线软卡",
|
||||
sub: "与首页 VanEmpty(方案 B)一致。",
|
||||
items: ["全站空态统一", "虚线卡略像「占位块」", "可直接复用组件"],
|
||||
pick: "想和首页空态完全一致时选 E2。"
|
||||
},
|
||||
e3: {
|
||||
title: "E3 · 插画 + 主按钮(推荐)",
|
||||
sub: "说明原因,并给出「去发起审批」主行动。",
|
||||
items: ["解决「空了之后做什么」", "主按钮视觉符合三墙绿", "可加次级文字链(如下拉刷新说明)"],
|
||||
pick: "草稿箱 / 我发起的 最推荐。"
|
||||
},
|
||||
e4: {
|
||||
title: "E4 · 场景卡片",
|
||||
sub: "白卡片内嵌简易场景插画(纸张 + 草稿标记)。",
|
||||
items: ["更有「产品感」", "CSS 绘制、无额外图片依赖", "带一个主按钮"],
|
||||
pick: "想要比 E3 更精致一点时选 E4。"
|
||||
},
|
||||
e5: {
|
||||
title: "E5 · 搜索无结果",
|
||||
sub: "有关键词但搜不到时的专用空态。",
|
||||
items: ["文案强调「未找到」", "提供「清空搜索」胶囊按钮", "与「真的没有数据」区分"],
|
||||
pick: "建议与 E3/E4 搭配:无数据用 E3,搜索空用 E5。"
|
||||
},
|
||||
e6: {
|
||||
title: "E6 · 极简加强",
|
||||
sub: "比现状多一个图标底板,仍保持克制。",
|
||||
items: ["改动最小", "仍偏素", "适合不想加按钮的页面"],
|
||||
pick: "过渡方案;长期更建议 E3/E4。"
|
||||
}
|
||||
};
|
||||
|
||||
function shell(cap, body, opts) {
|
||||
opts = opts || {};
|
||||
var kw = opts.keyword || "";
|
||||
return (
|
||||
'<div class="phone"><div class="cap">' + cap + '</div>' +
|
||||
'<div class="nav"><div class="nav-back"></div><div class="nav-title">草稿箱</div><div class="nav-right">管理</div></div>' +
|
||||
'<div class="page"><div class="search-wrap"><div class="search">' +
|
||||
(kw || "搜索流程名称") +
|
||||
'</div></div>' + body + '</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
const RENDER = {
|
||||
e0: function () {
|
||||
return shell("现状 · 纯文字", '<div class="e0">暂无草稿</div>');
|
||||
},
|
||||
e1: function () {
|
||||
return shell("圆形插画",
|
||||
'<div class="e1">' +
|
||||
'<div class="e1-art"><svg><use href="#i-inbox"/></svg></div>' +
|
||||
'<div class="e1-title">暂无草稿</div>' +
|
||||
'<div class="e1-desc">发起审批时保存的草稿会显示在这里</div>' +
|
||||
'</div>'
|
||||
) + shell("待办变体",
|
||||
'<div class="e1">' +
|
||||
'<div class="e1-art"><svg><use href="#i-check"/></svg></div>' +
|
||||
'<div class="e1-title">暂无待办</div>' +
|
||||
'<div class="e1-desc">当前没有需要你处理的审批</div>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
e2: function () {
|
||||
return shell("虚线软卡 · 对齐首页",
|
||||
'<div class="e2-wrap"><div class="e2">' +
|
||||
'<div class="e2-blob"><svg><use href="#i-inbox"/></svg></div>' +
|
||||
'<div class="e2-title">暂无草稿</div>' +
|
||||
'<div class="e2-desc">保存过的审批草稿会出现在这里</div>' +
|
||||
'</div></div>'
|
||||
);
|
||||
},
|
||||
e3: function () {
|
||||
return shell("推荐 · 带主按钮",
|
||||
'<div class="e3">' +
|
||||
'<div class="e3-ico"><svg><use href="#i-doc"/></svg></div>' +
|
||||
'<div class="e3-title">暂无草稿</div>' +
|
||||
'<div class="e3-desc">填写审批时可随时保存为草稿,方便下次继续</div>' +
|
||||
'<button class="e3-btn">去发起审批</button>' +
|
||||
'<div class="e3-link">了解如何使用草稿</div>' +
|
||||
'</div>'
|
||||
) + shell("待办 · 无 CTA 也可",
|
||||
'<div class="e3">' +
|
||||
'<div class="e3-ico"><svg><use href="#i-check"/></svg></div>' +
|
||||
'<div class="e3-title">暂无待办</div>' +
|
||||
'<div class="e3-desc">真棒,当前没有待你处理的审批</div>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
e4: function () {
|
||||
return shell("场景插画卡",
|
||||
'<div class="e4-wrap"><div class="e4">' +
|
||||
'<div class="e4-scene">' +
|
||||
'<div class="e4-badge">!</div>' +
|
||||
'<div class="e4-paper"><i></i><i></i><i></i></div>' +
|
||||
'<div class="e4-desk"></div>' +
|
||||
'</div>' +
|
||||
'<div class="e4-title">还没有草稿</div>' +
|
||||
'<div class="e4-desc">从发起审批开始,未提交的内容会自动出现在这里</div>' +
|
||||
'<button class="e4-btn">去发起审批</button>' +
|
||||
'</div></div>'
|
||||
);
|
||||
},
|
||||
e5: function () {
|
||||
return shell("搜索无结果",
|
||||
'<div class="e5">' +
|
||||
'<div class="e5-ico"><svg><use href="#i-search"/></svg></div>' +
|
||||
'<div class="e5-title">未找到相关草稿</div>' +
|
||||
'<div class="e5-desc">没有匹配「入帐」的结果,试试其他关键词</div>' +
|
||||
'<button class="e5-chip">清空搜索</button>' +
|
||||
'</div>',
|
||||
{ keyword: "入帐" }
|
||||
);
|
||||
},
|
||||
e6: function () {
|
||||
return shell("极简加强",
|
||||
'<div class="e6">' +
|
||||
'<div class="e6-emoji"><svg><use href="#i-inbox"/></svg></div>' +
|
||||
'<div class="e6-title">暂无草稿</div>' +
|
||||
'<div class="e6-desc">保存的草稿会显示在这里</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const descEl = document.getElementById("desc");
|
||||
const phonesEl = document.getElementById("phones");
|
||||
const tabs = document.getElementById("tabs");
|
||||
|
||||
function show(k) {
|
||||
var d = DESCS[k];
|
||||
descEl.innerHTML =
|
||||
"<h2>" + d.title + "</h2>" +
|
||||
'<p class="sub">' + d.sub + "</p>" +
|
||||
"<ul>" + d.items.map(function (i) { return "<li>" + i + "</li>"; }).join("") + "</ul>" +
|
||||
'<div class="pick">' + d.pick + "</div>";
|
||||
phonesEl.innerHTML = RENDER[k]();
|
||||
}
|
||||
|
||||
tabs.addEventListener("click", function (e) {
|
||||
var btn = e.target.closest(".tab");
|
||||
if (!btn) return;
|
||||
tabs.querySelectorAll(".tab").forEach(function (t) {
|
||||
t.classList.toggle("is-active", t === btn);
|
||||
});
|
||||
show(btn.getAttribute("data-k"));
|
||||
});
|
||||
|
||||
show("e0");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,695 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>待办中心 · 批量通过 / 打回 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--warn: #d97706;
|
||||
--warn-soft: #fff7e8;
|
||||
--danger: #ef4444;
|
||||
--danger-soft: #fef2f2;
|
||||
--nav-bg: #ecfdf5;
|
||||
--page-bg: #f5f8f7;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
--border: #e8eaed;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1180px; margin: 0 auto; padding: 28px 16px 64px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 820px; }
|
||||
.rec {
|
||||
margin: 16px 0 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: var(--brand-soft);
|
||||
border: 1px solid #b7ebd4;
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.rec b { color: var(--brand-dark); }
|
||||
|
||||
.tabs {
|
||||
display: flex; flex-wrap: wrap; gap: 10px;
|
||||
margin: 20px 0 18px;
|
||||
}
|
||||
.tab {
|
||||
appearance: none; border: 1px solid #e2e8f0; background: #fff;
|
||||
border-radius: 999px; padding: 10px 16px; font-size: 13px;
|
||||
font-weight: 600; color: #475569; cursor: pointer;
|
||||
}
|
||||
.tab.is-active {
|
||||
color: var(--brand-dark); border-color: var(--brand);
|
||||
background: var(--brand-soft);
|
||||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||||
gap: 20px; align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.panel h2 { margin: 0 0 4px; font-size: 15px; }
|
||||
.panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
|
||||
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
|
||||
.panel li { margin: 6px 0; }
|
||||
.panel .pick {
|
||||
margin-top: 12px; padding: 10px 12px; border-radius: 10px;
|
||||
background: #fff7e8; border: 1px solid #fde68a;
|
||||
font-size: 12px; color: #92400e;
|
||||
}
|
||||
.panel .ok {
|
||||
margin-top: 10px; padding: 10px 12px; border-radius: 10px;
|
||||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||||
font-size: 12px; color: #047857;
|
||||
}
|
||||
|
||||
.phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
|
||||
.phone {
|
||||
width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
|
||||
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
|
||||
position: relative;
|
||||
}
|
||||
.cap {
|
||||
text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
|
||||
padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 44px; display: flex; align-items: center; padding: 0 8px 0 4px;
|
||||
background: var(--nav-bg); border-bottom: 1px solid #d8f3e5;
|
||||
position: relative;
|
||||
}
|
||||
.nav-back {
|
||||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||||
color: #0f291e; flex-shrink: 0;
|
||||
}
|
||||
.nav-back::before {
|
||||
content: ""; width: 10px; height: 10px;
|
||||
border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
|
||||
transform: rotate(45deg); margin-left: 4px;
|
||||
}
|
||||
.nav-title {
|
||||
position: absolute; left: 50%; transform: translateX(-50%);
|
||||
font-size: 16px; font-weight: 650; color: #0f291e;
|
||||
}
|
||||
.nav-action {
|
||||
margin-left: auto;
|
||||
min-width: 48px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: var(--brand-dark);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.nav-action.is-done { color: #64748b; }
|
||||
.nav-action:active { opacity: 0.72; }
|
||||
|
||||
.page {
|
||||
min-height: 640px;
|
||||
background: var(--page-bg);
|
||||
padding-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.search-box {
|
||||
height: 36px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 10px; border-radius: 10px;
|
||||
background: #f1f5f9; border: 1px solid transparent;
|
||||
}
|
||||
.search-ico {
|
||||
width: 13px; height: 13px; border: 1.5px solid #94a3b8; border-radius: 50%;
|
||||
position: relative; flex-shrink: 0;
|
||||
}
|
||||
.search-ico::after {
|
||||
content: ""; position: absolute; width: 6px; height: 1.5px;
|
||||
right: -4px; bottom: -2px; background: #94a3b8; border-radius: 2px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.search-ph { font-size: 13px; color: #b0b7c3; }
|
||||
|
||||
.batch-bar {
|
||||
display: none;
|
||||
align-items: center; justify-content: space-between; gap: 8px;
|
||||
margin: 8px 12px 0; padding: 10px 12px;
|
||||
border-radius: 10px; background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
.batch-bar.is-show { display: flex; }
|
||||
.batch-bar__hint { font-size: 13px; color: #646a73; flex-shrink: 0; }
|
||||
.batch-bar__actions { display: flex; gap: 8px; margin-left: auto; }
|
||||
.batch-bar__btn {
|
||||
min-height: 32px; padding: 0 14px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px; font-size: 13px; font-weight: 600;
|
||||
border: none; cursor: pointer;
|
||||
}
|
||||
.batch-bar__btn.is-ok {
|
||||
color: #fff; background: var(--brand);
|
||||
}
|
||||
.batch-bar__btn.is-return {
|
||||
color: var(--warn); background: var(--warn-soft);
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
.batch-bar__btn:disabled {
|
||||
opacity: 0.45; cursor: not-allowed;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 10px 12px 0;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.row {
|
||||
display: flex; align-items: stretch; gap: 0;
|
||||
}
|
||||
.check {
|
||||
display: none; align-items: flex-start;
|
||||
padding: 18px 0 0 2px; margin-right: 6px; flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page.is-manage .check { display: flex; }
|
||||
.check__box {
|
||||
width: 20px; height: 20px; border-radius: 50%;
|
||||
border: 1.5px solid #cbd5e1; background: #fff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; color: #fff; font-weight: 700;
|
||||
}
|
||||
.check__box.on {
|
||||
background: var(--brand); border-color: var(--brand);
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1; min-width: 0;
|
||||
background: #fff; border-radius: 14px;
|
||||
border: 1px solid #d8f3e5;
|
||||
box-shadow: 0 4px 16px rgba(22, 163, 74, 0.07);
|
||||
padding: 12px 14px; cursor: pointer;
|
||||
}
|
||||
.card.is-selected {
|
||||
border-color: #86efac;
|
||||
box-shadow: 0 0 0 2px rgba(10, 159, 109, 0.18);
|
||||
}
|
||||
.card__head {
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
|
||||
}
|
||||
.card__title {
|
||||
font-size: 15px; font-weight: 700; color: #0f291e; line-height: 1.35;
|
||||
}
|
||||
.card__time { flex-shrink: 0; font-size: 11px; color: #94a3b8; margin-top: 2px; }
|
||||
.card__meta { margin-top: 8px; }
|
||||
.card__meta-line {
|
||||
display: block; font-size: 12px; color: var(--text-muted); line-height: 1.5;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.card__foot {
|
||||
margin-top: 10px; display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.card__user { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||||
.card__avatar {
|
||||
width: 22px; height: 22px; border-radius: 6px;
|
||||
background: var(--brand); color: #fff;
|
||||
font-size: 10px; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.card__user-text { font-size: 12px; color: #64748b; }
|
||||
.card__status {
|
||||
font-size: 11px; font-weight: 650; padding: 3px 8px; border-radius: 999px;
|
||||
background: rgba(217, 119, 6, 0.12); color: var(--warn);
|
||||
}
|
||||
|
||||
/* 底部意见弹层 */
|
||||
.sheet-mask {
|
||||
display: none; position: absolute; inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45); z-index: 20;
|
||||
}
|
||||
.sheet-mask.is-show { display: block; }
|
||||
.sheet {
|
||||
position: absolute; left: 0; right: 0; bottom: 0; z-index: 21;
|
||||
background: #fff; border-radius: 16px 16px 0 0;
|
||||
padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
transform: translateY(110%);
|
||||
transition: transform .22s ease;
|
||||
box-shadow: 0 -8px 28px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
.sheet.is-show { transform: translateY(0); }
|
||||
.sheet__hd {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sheet__title { font-size: 16px; font-weight: 700; }
|
||||
.sheet__close {
|
||||
border: none; background: transparent; color: #64748b;
|
||||
font-size: 13px; cursor: pointer; padding: 4px 0;
|
||||
}
|
||||
.sheet__hint {
|
||||
margin: 0 0 8px; font-size: 12px; color: #64748b;
|
||||
}
|
||||
.sheet__hint strong { color: var(--brand-dark); }
|
||||
.sheet__ta {
|
||||
width: 100%; min-height: 96px; resize: none;
|
||||
border: 1px solid #e2e8f0; border-radius: 10px;
|
||||
padding: 10px 12px; font: inherit; font-size: 14px;
|
||||
color: var(--text); outline: none;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.sheet__ta:focus {
|
||||
border-color: #86efac; background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.12);
|
||||
}
|
||||
.sheet__ft {
|
||||
margin-top: 12px; display: flex; gap: 10px;
|
||||
}
|
||||
.sheet__btn {
|
||||
flex: 1; height: 40px; border-radius: 10px;
|
||||
border: none; font-size: 15px; font-weight: 650; cursor: pointer;
|
||||
}
|
||||
.sheet__btn.is-cancel {
|
||||
background: #f1f5f9; color: #475569;
|
||||
}
|
||||
.sheet__btn.is-ok {
|
||||
background: var(--brand); color: #fff;
|
||||
}
|
||||
.sheet__btn.is-return {
|
||||
background: var(--warn); color: #fff;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: absolute; left: 50%; bottom: 88px; z-index: 30;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 14px; border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.88); color: #fff;
|
||||
font-size: 13px; white-space: nowrap;
|
||||
opacity: 0; pointer-events: none; transition: opacity .2s;
|
||||
}
|
||||
.toast.is-show { opacity: 1; }
|
||||
|
||||
.legend {
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.legend .box {
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
|
||||
padding: 12px 14px; font-size: 12px; color: #475569;
|
||||
}
|
||||
.legend h3 { margin: 0 0 6px; font-size: 13px; color: #0f291e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>待办中心 · 批量通过 / 打回</h1>
|
||||
<p>
|
||||
交互对齐抄送「管理 + 多选 + 底栏批量操作」。右侧手机可点击演示:进入管理 → 勾选 → 批量通过 / 打回 → 填写统一意见。
|
||||
</p>
|
||||
<div class="rec">
|
||||
<b>推荐方案(本 Demo):</b>
|
||||
<b>标题栏右侧「管理 / 完成」</b>(对齐草稿箱自定义顶栏);底栏「批量通过」「批量打回」;
|
||||
统一意见框(通过默认「同意」可空,打回必填);前端串行调现有单条接口,汇总成功/失败。不含批量拒绝。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="sceneTabs">
|
||||
<button type="button" class="tab is-active" data-scene="browse">① 浏览态</button>
|
||||
<button type="button" class="tab" data-scene="manage">② 管理多选</button>
|
||||
<button type="button" class="tab" data-scene="approve">③ 批量通过弹层</button>
|
||||
<button type="button" class="tab" data-scene="return">④ 批量打回弹层</button>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<aside class="panel" id="sidePanel">
|
||||
<h2>① 浏览态</h2>
|
||||
<p class="sub">日常查看 / 点卡片进详情审批</p>
|
||||
<ul>
|
||||
<li>标题栏右侧「管理」(对齐草稿箱自定义顶栏)</li>
|
||||
<li>搜索区仅保留检索,不再放管理按钮</li>
|
||||
<li>点卡片仍进入审批详情(webview)</li>
|
||||
</ul>
|
||||
<div class="pick">右侧手机可点标题栏「管理」亲自走一遍流程。</div>
|
||||
</aside>
|
||||
|
||||
<div class="phones">
|
||||
<div class="phone">
|
||||
<div class="cap">可交互 Demo · 375px</div>
|
||||
<div class="nav">
|
||||
<div class="nav-back" aria-hidden="true"></div>
|
||||
<div class="nav-title">待办中心</div>
|
||||
<div class="nav-action" id="manageBtn">管理</div>
|
||||
</div>
|
||||
<div class="page" id="page">
|
||||
<div class="toolbar">
|
||||
<div class="search-box">
|
||||
<span class="search-ico" aria-hidden="true"></span>
|
||||
<span class="search-ph">搜索流程名称、申请人</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-bar" id="batchBar">
|
||||
<span class="batch-bar__hint" id="batchHint">已选 0 项</span>
|
||||
<div class="batch-bar__actions">
|
||||
<button type="button" class="batch-bar__btn is-return" id="btnReturn">批量打回</button>
|
||||
<button type="button" class="batch-bar__btn is-ok" id="btnApprove">批量通过</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list" id="list"></div>
|
||||
|
||||
<div class="sheet-mask" id="sheetMask"></div>
|
||||
<div class="sheet" id="sheet">
|
||||
<div class="sheet__hd">
|
||||
<div class="sheet__title" id="sheetTitle">批量通过</div>
|
||||
<button type="button" class="sheet__close" id="sheetClose">取消</button>
|
||||
</div>
|
||||
<p class="sheet__hint" id="sheetHint">将对 <strong>2</strong> 条待办执行操作</p>
|
||||
<textarea class="sheet__ta" id="sheetInput" placeholder="审批意见"></textarea>
|
||||
<div class="sheet__ft">
|
||||
<button type="button" class="sheet__btn is-cancel" id="sheetCancel">取消</button>
|
||||
<button type="button" class="sheet__btn is-ok" id="sheetConfirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="box">
|
||||
<h3>意见规则</h3>
|
||||
批量通过:默认填入「同意」,可改,可空(与单条一致)。<br />
|
||||
批量打回:意见必填,占位「请填写打回意见(必填)」。
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3>接口策略</h3>
|
||||
暂无批量 API:前端对选中 taskId 串行调用<br />
|
||||
<code>POST /tasks/{id}/approve</code>、<code>/return</code>。<br />
|
||||
结束后 toast:「成功 n,失败 m」。
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3>边界</h3>
|
||||
仅当前页已加载列表可勾选;不包含「拒绝」。<br />
|
||||
若流程关闭打回(allowReturn=false),单条失败计入失败数。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const TASKS = [
|
||||
{
|
||||
id: 't1',
|
||||
title: '请假申请',
|
||||
time: '今天 09:12',
|
||||
meta: ['请假类型:事假', '请假天数:1'],
|
||||
user: '王品龙',
|
||||
avatar: '王品'
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
title: '费用报销',
|
||||
time: '昨天 18:40',
|
||||
meta: ['报销金额:1280.00', '事由:差旅交通'],
|
||||
user: '吴家猛',
|
||||
avatar: '吴家'
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
title: '加班申请',
|
||||
time: '08-10',
|
||||
meta: ['加班时长:3小时', '加班日期:2026-08-09'],
|
||||
user: '郑德禄',
|
||||
avatar: '郑德'
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
title: '出差申请',
|
||||
time: '08-09',
|
||||
meta: ['目的地:合肥', '出差天数:2'],
|
||||
user: '李明',
|
||||
avatar: '李明'
|
||||
}
|
||||
]
|
||||
|
||||
const state = {
|
||||
manage: false,
|
||||
selected: new Set(),
|
||||
sheetKind: null // approve | return
|
||||
}
|
||||
|
||||
const page = document.getElementById('page')
|
||||
const listEl = document.getElementById('list')
|
||||
const manageBtn = document.getElementById('manageBtn')
|
||||
const batchBar = document.getElementById('batchBar')
|
||||
const batchHint = document.getElementById('batchHint')
|
||||
const btnApprove = document.getElementById('btnApprove')
|
||||
const btnReturn = document.getElementById('btnReturn')
|
||||
const sheet = document.getElementById('sheet')
|
||||
const sheetMask = document.getElementById('sheetMask')
|
||||
const sheetTitle = document.getElementById('sheetTitle')
|
||||
const sheetHint = document.getElementById('sheetHint')
|
||||
const sheetInput = document.getElementById('sheetInput')
|
||||
const sheetConfirm = document.getElementById('sheetConfirm')
|
||||
const toastEl = document.getElementById('toast')
|
||||
const sidePanel = document.getElementById('sidePanel')
|
||||
|
||||
const SCENE_COPY = {
|
||||
browse: {
|
||||
title: '① 浏览态',
|
||||
sub: '日常查看 / 点卡片进详情审批',
|
||||
items: [
|
||||
'标题栏右侧「管理」(对齐草稿箱自定义顶栏)',
|
||||
'搜索区仅保留检索,不再放管理按钮',
|
||||
'点卡片仍进入审批详情(webview)'
|
||||
],
|
||||
tip: '右侧手机可点标题栏「管理」亲自走一遍流程。',
|
||||
tipClass: 'pick'
|
||||
},
|
||||
manage: {
|
||||
title: '② 管理多选',
|
||||
sub: '勾选多条待办,底栏出现批量操作',
|
||||
items: [
|
||||
'标题栏按钮变为「完成」,点按退出并清空勾选',
|
||||
'左侧圆形勾选,选中卡片高亮描边',
|
||||
'底栏:已选 N 项 + 批量打回 + 批量通过',
|
||||
'未勾选时两个按钮禁用'
|
||||
],
|
||||
tip: '主行动为「批量通过」(品牌绿);打回为次要警告色。',
|
||||
tipClass: 'ok'
|
||||
},
|
||||
approve: {
|
||||
title: '③ 批量通过弹层',
|
||||
sub: '统一意见,一次确认',
|
||||
items: [
|
||||
'标题「批量通过」,提示将对 N 条执行',
|
||||
'默认意见「同意」,可修改或清空',
|
||||
'确定后串行 approve,刷新列表'
|
||||
],
|
||||
tip: '与详情页单条「同意」语义一致。',
|
||||
tipClass: 'ok'
|
||||
},
|
||||
return: {
|
||||
title: '④ 批量打回弹层',
|
||||
sub: '打回意见必填',
|
||||
items: [
|
||||
'标题「批量打回」,主按钮琥珀警告色',
|
||||
'意见为空时 toast「打回意见必填」',
|
||||
'部分失败时提示成功/失败条数'
|
||||
],
|
||||
tip: '不在此 Demo 做「批量拒绝」,避免误操作风险。',
|
||||
tipClass: 'pick'
|
||||
}
|
||||
}
|
||||
|
||||
function renderSide(scene) {
|
||||
const c = SCENE_COPY[scene]
|
||||
sidePanel.innerHTML =
|
||||
'<h2>' + c.title + '</h2>' +
|
||||
'<p class="sub">' + c.sub + '</p>' +
|
||||
'<ul>' + c.items.map((x) => '<li>' + x + '</li>').join('') + '</ul>' +
|
||||
'<div class="' + c.tipClass + '">' + c.tip + '</div>'
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
listEl.innerHTML = TASKS.map((t) => {
|
||||
const on = state.selected.has(t.id)
|
||||
return (
|
||||
'<div class="row" data-id="' + t.id + '">' +
|
||||
'<div class="check" data-check="' + t.id + '">' +
|
||||
'<div class="check__box' + (on ? ' on' : '') + '">' + (on ? '✓' : '') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="card' + (on ? ' is-selected' : '') + '" data-card="' + t.id + '">' +
|
||||
'<div class="card__head">' +
|
||||
'<div class="card__title">' + t.title + '</div>' +
|
||||
'<div class="card__time">' + t.time + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="card__meta">' +
|
||||
t.meta.map((m) => '<span class="card__meta-line">' + m + '</span>').join('') +
|
||||
'</div>' +
|
||||
'<div class="card__foot">' +
|
||||
'<div class="card__user">' +
|
||||
'<div class="card__avatar">' + t.avatar + '</div>' +
|
||||
'<div class="card__user-text">由' + t.user + '提交</div>' +
|
||||
'</div>' +
|
||||
'<div class="card__status">进行中</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function syncChrome() {
|
||||
page.classList.toggle('is-manage', state.manage)
|
||||
manageBtn.textContent = state.manage ? '完成' : '管理'
|
||||
manageBtn.classList.toggle('is-done', state.manage)
|
||||
const n = state.selected.size
|
||||
batchBar.classList.toggle('is-show', state.manage && n > 0)
|
||||
batchHint.textContent = '已选 ' + n + ' 项'
|
||||
btnApprove.disabled = n === 0
|
||||
btnReturn.disabled = n === 0
|
||||
}
|
||||
|
||||
function toast(msg) {
|
||||
toastEl.textContent = msg
|
||||
toastEl.classList.add('is-show')
|
||||
clearTimeout(toast._t)
|
||||
toast._t = setTimeout(() => toastEl.classList.remove('is-show'), 1800)
|
||||
}
|
||||
|
||||
function openSheet(kind) {
|
||||
const n = state.selected.size
|
||||
if (!n) return
|
||||
state.sheetKind = kind
|
||||
sheetTitle.textContent = kind === 'approve' ? '批量通过' : '批量打回'
|
||||
sheetHint.innerHTML = '将对 <strong>' + n + '</strong> 条待办执行操作'
|
||||
sheetInput.value = kind === 'approve' ? '同意' : ''
|
||||
sheetInput.placeholder =
|
||||
kind === 'approve' ? '审批意见(可选)' : '请填写打回意见(必填)'
|
||||
sheetConfirm.textContent = '确定'
|
||||
sheetConfirm.className =
|
||||
'sheet__btn ' + (kind === 'approve' ? 'is-ok' : 'is-return')
|
||||
sheetMask.classList.add('is-show')
|
||||
sheet.classList.add('is-show')
|
||||
setTimeout(() => sheetInput.focus(), 80)
|
||||
}
|
||||
|
||||
function closeSheet() {
|
||||
state.sheetKind = null
|
||||
sheetMask.classList.remove('is-show')
|
||||
sheet.classList.remove('is-show')
|
||||
}
|
||||
|
||||
function setManage(on) {
|
||||
state.manage = on
|
||||
if (!on) state.selected.clear()
|
||||
renderList()
|
||||
syncChrome()
|
||||
}
|
||||
|
||||
function applyScene(scene) {
|
||||
document.querySelectorAll('#sceneTabs .tab').forEach((el) => {
|
||||
el.classList.toggle('is-active', el.dataset.scene === scene)
|
||||
})
|
||||
renderSide(scene)
|
||||
closeSheet()
|
||||
if (scene === 'browse') {
|
||||
setManage(false)
|
||||
} else if (scene === 'manage') {
|
||||
state.selected = new Set(['t1', 't2'])
|
||||
setManage(true)
|
||||
} else if (scene === 'approve') {
|
||||
state.selected = new Set(['t1', 't2'])
|
||||
setManage(true)
|
||||
openSheet('approve')
|
||||
} else if (scene === 'return') {
|
||||
state.selected = new Set(['t1', 't3'])
|
||||
setManage(true)
|
||||
openSheet('return')
|
||||
}
|
||||
}
|
||||
|
||||
manageBtn.addEventListener('click', () => setManage(!state.manage))
|
||||
|
||||
listEl.addEventListener('click', (e) => {
|
||||
const check = e.target.closest('[data-check]')
|
||||
const card = e.target.closest('[data-card]')
|
||||
const id = (check && check.dataset.check) || (card && card.dataset.card)
|
||||
if (!id) return
|
||||
if (state.manage) {
|
||||
if (state.selected.has(id)) state.selected.delete(id)
|
||||
else state.selected.add(id)
|
||||
renderList()
|
||||
syncChrome()
|
||||
return
|
||||
}
|
||||
if (card) toast('进入审批详情(Demo)')
|
||||
})
|
||||
|
||||
btnApprove.addEventListener('click', () => openSheet('approve'))
|
||||
btnReturn.addEventListener('click', () => openSheet('return'))
|
||||
sheetMask.addEventListener('click', closeSheet)
|
||||
document.getElementById('sheetClose').addEventListener('click', closeSheet)
|
||||
document.getElementById('sheetCancel').addEventListener('click', closeSheet)
|
||||
|
||||
sheetConfirm.addEventListener('click', () => {
|
||||
const kind = state.sheetKind
|
||||
const comment = String(sheetInput.value || '').trim()
|
||||
const n = state.selected.size
|
||||
if (kind === 'return' && !comment) {
|
||||
toast('打回意见必填')
|
||||
return
|
||||
}
|
||||
closeSheet()
|
||||
const label = kind === 'approve' ? '通过' : '打回'
|
||||
toast('已' + label + ' ' + n + ' 条(Demo)')
|
||||
state.selected.clear()
|
||||
setManage(false)
|
||||
})
|
||||
|
||||
document.getElementById('sceneTabs').addEventListener('click', (e) => {
|
||||
const tab = e.target.closest('.tab')
|
||||
if (!tab) return
|
||||
applyScene(tab.dataset.scene)
|
||||
})
|
||||
|
||||
renderList()
|
||||
syncChrome()
|
||||
renderSide('browse')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,694 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>mobile-oa-h5 · 审批进度 Tag 配色 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--brand-soft: #e9f9f2;
|
||||
--page-bg: #f5f8f7;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
--border: #dcebe4;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
radial-gradient(700px 380px at 92% 0%, rgba(214, 163, 67, 0.1), transparent 50%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1280px; margin: 0 auto; padding: 28px 16px 64px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 900px; }
|
||||
.goals {
|
||||
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
||||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||||
font-size: 13px; color: #065f46;
|
||||
}
|
||||
.goals ul { margin: 6px 0 0; padding-left: 18px; }
|
||||
|
||||
.grid {
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
}
|
||||
.card .sub { margin: 0 0 12px; font-size: 12px; color: #64748b; }
|
||||
.pill {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
||||
font-size: 10px; font-weight: 700; letter-spacing: 0.02em;
|
||||
}
|
||||
.pill.now { background: #fff1f2; color: #be123c; border: 1px solid #fecdd3; }
|
||||
.pill.rec { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
|
||||
.pill.alt { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
|
||||
|
||||
.phone {
|
||||
width: 100%; max-width: 360px; margin: 0 auto;
|
||||
border-radius: 28px; border: 1px solid #cbd5e1; background: #0f172a;
|
||||
padding: 10px; box-shadow: 0 18px 40px rgba(15,23,42,.12);
|
||||
}
|
||||
.phone__screen {
|
||||
border-radius: 20px; overflow: hidden; background: var(--page-bg);
|
||||
min-height: 420px; display: flex; flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
display: flex; align-items: center; gap: 8px; min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
background: linear-gradient(180deg, #d1fae5 0%, #ecfdf5 100%);
|
||||
border-bottom: 1px solid #bbf7d0;
|
||||
}
|
||||
.nav__back {
|
||||
width: 28px; height: 28px; border-radius: 999px;
|
||||
display: grid; place-items: center;
|
||||
color: var(--brand-dark); font-weight: 700; font-size: 16px;
|
||||
background: rgba(255,255,255,.55);
|
||||
}
|
||||
.nav__title { flex: 1; font-size: 15px; font-weight: 700; color: #064e3b; }
|
||||
.body { padding: 12px; display: grid; gap: 10px; }
|
||||
|
||||
.progress {
|
||||
background: #fff; border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 12px;
|
||||
}
|
||||
.progress__title {
|
||||
margin: 0 0 10px; font-size: 15px; font-weight: 700; color: #0f291e;
|
||||
}
|
||||
.step { display: flex; gap: 10px; min-height: 52px; }
|
||||
.rail { display: flex; flex-direction: column; align-items: center; width: 16px; }
|
||||
.dot {
|
||||
width: 10px; height: 10px; margin-top: 6px; border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
}
|
||||
.dot.ok { background: #189b65; }
|
||||
.dot.warn { background: #cf8a19; }
|
||||
.dot.danger { background: #d14343; }
|
||||
.dot.mute { background: #94a3b8; }
|
||||
.line { flex: 1; width: 2px; margin: 4px 0 0; background: #e2e8f0; min-height: 28px; }
|
||||
.step-card { flex: 1; min-width: 0; padding-bottom: 12px; }
|
||||
.row {
|
||||
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
|
||||
}
|
||||
.name { font-size: 14px; font-weight: 600; color: #0f291e; }
|
||||
.person {
|
||||
margin-top: 6px; font-size: 12px; color: #5f7a6d;
|
||||
display: flex; justify-content: space-between; gap: 8px;
|
||||
}
|
||||
.person strong { color: #1f2a26; font-weight: 600; }
|
||||
|
||||
/* ===== badge base ===== */
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* —— 现状 —— */
|
||||
.now .badge.ok { color: #10b981; background: #ecfdf5; }
|
||||
.now .badge.warn { color: #d97706; background: #fffbeb; }
|
||||
.now .badge.danger { color: #ef4444; background: #fef2f2; }
|
||||
.now .badge.mute { color: #5f7a6d; background: #f1f5f9; }
|
||||
.now .badge.and { color: #2563eb; background: #eff6ff; }
|
||||
.now .badge.or { color: #d97706; background: #fff7ed; }
|
||||
.now .badge.cc { color: #0891b2; background: #ecfeff; }
|
||||
.now .badge.end { color: #5f7a6d; background: #f1f5f9; }
|
||||
|
||||
/* —— 方案 A:三墙语义 + 类型独立色相(浅底实心) —— */
|
||||
.a .badge.ok { color: #087f57; background: #e9f9f2; }
|
||||
.a .badge.warn { color: #a66b10; background: #fff7e8; }
|
||||
.a .badge.danger { color: #b42318; background: #fef3f2; }
|
||||
.a .badge.mute { color: #5f7a6d; background: #f1f5f3; }
|
||||
.a .badge.and { color: #4338ca; background: #eef2ff; } /* 会签·靛蓝 */
|
||||
.a .badge.or { color: #0e7490; background: #ecfeff; } /* 或签·青蓝,避开琥珀冲突 */
|
||||
.a .badge.cc { color: #6d28d9; background: #f5f3ff; } /* 抄送·淡紫 */
|
||||
.a .badge.end { color: #64748b; background: #f1f5f9; }
|
||||
|
||||
/* —— 方案 B:状态浅底 / 类型描边(推荐视觉区分) —— */
|
||||
.b .badge.ok { color: #087f57; background: #e9f9f2; }
|
||||
.b .badge.warn { color: #a66b10; background: #fff7e8; }
|
||||
.b .badge.danger { color: #b42318; background: #fef3f2; }
|
||||
.b .badge.mute { color: #5f7a6d; background: #f1f5f3; }
|
||||
.b .badge.and {
|
||||
color: #3730a3; background: #fff; border-color: #c7d2fe;
|
||||
}
|
||||
.b .badge.or {
|
||||
color: #0e7490; background: #fff; border-color: #a5f3fc;
|
||||
}
|
||||
.b .badge.cc {
|
||||
color: #6d28d9; background: #fff; border-color: #ddd6fe;
|
||||
}
|
||||
.b .badge.end {
|
||||
color: #64748b; background: #fff; border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* —— 方案 C:低饱和克制 —— */
|
||||
.c .badge.ok { color: #0f766e; background: #f0fdfa; }
|
||||
.c .badge.warn { color: #92400e; background: #fffbeb; }
|
||||
.c .badge.danger { color: #9f1239; background: #fff1f2; }
|
||||
.c .badge.mute { color: #64748b; background: #f8fafc; }
|
||||
.c .badge.and { color: #475569; background: #f1f5f9; border-color: #cbd5e1; }
|
||||
.c .badge.or { color: #475569; background: #f8fafc; border-color: #e2e8f0; }
|
||||
.c .badge.cc { color: #64748b; background: #f8fafc; border-color: #e2e8f0; }
|
||||
.c .badge.end { color: #94a3b8; background: #f8fafc; }
|
||||
|
||||
.swatch-block { margin-top: 12px; }
|
||||
.swatch-block h3 {
|
||||
margin: 0 0 8px; font-size: 12px; color: #64748b; font-weight: 600;
|
||||
}
|
||||
.swatches { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.legend {
|
||||
margin-top: 10px; padding: 8px 10px; border-radius: 8px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0;
|
||||
font-size: 11px; color: #64748b;
|
||||
}
|
||||
.legend strong { color: #334155; }
|
||||
|
||||
.pick-bar {
|
||||
position: sticky; bottom: 0; z-index: 5;
|
||||
margin-top: 28px; padding: 14px 16px;
|
||||
background: rgba(255,255,255,.92);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid #e2e8f0; border-radius: 14px;
|
||||
box-shadow: 0 -8px 24px rgba(15,23,42,.06);
|
||||
display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
|
||||
}
|
||||
.pick-bar span { font-size: 13px; color: #475569; margin-right: 4px; }
|
||||
.pick-bar button {
|
||||
appearance: none; border: 1px solid #cbd5e1; background: #fff;
|
||||
border-radius: 999px; padding: 8px 14px; font-size: 13px;
|
||||
color: #334155; cursor: pointer; font-weight: 600;
|
||||
}
|
||||
.pick-bar button.rec {
|
||||
border-color: #6ee7b7; background: #ecfdf5; color: #047857;
|
||||
}
|
||||
.pick-bar button:hover { border-color: #0a9f6d; color: #087f57; }
|
||||
.pick-note {
|
||||
width: 100%; margin: 0; font-size: 12px; color: #94a3b8;
|
||||
}
|
||||
|
||||
.palette-table {
|
||||
margin-top: 22px;
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px 18px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.palette-table h2 { margin: 0 0 8px; font-size: 15px; }
|
||||
.palette-table p { margin: 0 0 12px; font-size: 12px; color: #64748b; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th, td {
|
||||
border-bottom: 1px solid #eef2f7; padding: 8px 10px;
|
||||
text-align: left; vertical-align: middle;
|
||||
}
|
||||
th { color: #64748b; font-weight: 600; background: #f8fafc; }
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 11px; color: #475569;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>审批进度 Tag 配色 Demo</h1>
|
||||
<p>
|
||||
对比「现状」与 3 套方案。目标:状态语义清晰、会签/或签/抄送不与「待处理」琥珀撞色、
|
||||
整体对齐三墙 Token(品牌绿 / 大麦黄 / 语义色),避免同屏高饱和堆叠。
|
||||
</p>
|
||||
<div class="goals">
|
||||
<strong>设计约束</strong>
|
||||
<ul>
|
||||
<li>状态 tag:成功绿 / 待办琥珀 / 危险红 / 中性灰(语义优先)</li>
|
||||
<li>类型 tag:会签 / 或签 / 抄送 / 结束 — 与状态区分,避免「或签」与「待处理」同色</li>
|
||||
<li>同一节点可同时出现「状态 + 类型」两枚 tag</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- 现状 -->
|
||||
<section class="card">
|
||||
<h2>现状 <span class="pill now">NOW</span></h2>
|
||||
<p class="sub">会签蓝偏艳;或签与待处理同系琥珀;成功色偏 web-admin mint(#10b981)</p>
|
||||
<div class="phone now">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批进度 · 现状</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起申请</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person"><strong>张三</strong><span>03-12 09:20</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">部门审批</span>
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person"><strong>李四 · 王五</strong><span>03-12 14:01</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot warn"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">分管领导</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge or">或签</span>
|
||||
</div>
|
||||
<div class="person"><strong>赵六 / 钱七</strong><span>—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">相关知会</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">流程结束</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch-block">
|
||||
<h3>全量色板</h3>
|
||||
<div class="swatches">
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge ok">已完成</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge warn">进行中</span>
|
||||
<span class="badge danger">已拒绝</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
<span class="badge danger">已打回</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge mute">已转交</span>
|
||||
<span class="badge and">会签</span>
|
||||
<span class="badge or">或签</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">问题:<strong>或签 ≈ 待处理</strong> 同屏难辨;会签蓝过亮;与三墙主色链不完全一致。</div>
|
||||
</section>
|
||||
|
||||
<!-- 方案 A -->
|
||||
<section class="card">
|
||||
<h2>方案 A · 浅底实心 <span class="pill alt">ALT</span></h2>
|
||||
<p class="sub">状态对齐三墙语义色;类型用「靛蓝 / 青蓝 / 淡紫」拉开色相,全部浅底实心</p>
|
||||
<div class="phone a">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批进度 · 方案 A</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起申请</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person"><strong>张三</strong><span>03-12 09:20</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">部门审批</span>
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person"><strong>李四 · 王五</strong><span>03-12 14:01</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot warn"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">分管领导</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge or">或签</span>
|
||||
</div>
|
||||
<div class="person"><strong>赵六 / 钱七</strong><span>—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">相关知会</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">流程结束</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch-block">
|
||||
<h3>全量色板</h3>
|
||||
<div class="swatches">
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge ok">已完成</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge warn">进行中</span>
|
||||
<span class="badge danger">已拒绝</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
<span class="badge danger">已打回</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge mute">已转交</span>
|
||||
<span class="badge and">会签</span>
|
||||
<span class="badge or">或签</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">优点:实现简单、与现状结构一致。注意:类型仍是实心,同排两枚 tag 略抢眼。</div>
|
||||
</section>
|
||||
|
||||
<!-- 方案 B -->
|
||||
<section class="card">
|
||||
<h2>方案 B · 状态实心 / 类型描边 <span class="pill rec">推荐</span></h2>
|
||||
<p class="sub">状态保留浅底语义;类型改为白底描边,一眼区分「结果态」与「节点属性」</p>
|
||||
<div class="phone b">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批进度 · 方案 B</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起申请</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person"><strong>张三</strong><span>03-12 09:20</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">部门审批</span>
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person"><strong>李四 · 王五</strong><span>03-12 14:01</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot warn"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">分管领导</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge or">或签</span>
|
||||
</div>
|
||||
<div class="person"><strong>赵六 / 钱七</strong><span>—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">相关知会</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">流程结束</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch-block">
|
||||
<h3>全量色板</h3>
|
||||
<div class="swatches">
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge ok">已完成</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge warn">进行中</span>
|
||||
<span class="badge danger">已拒绝</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
<span class="badge danger">已打回</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge mute">已转交</span>
|
||||
<span class="badge and">会签</span>
|
||||
<span class="badge or">或签</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">推荐理由:状态更抢眼、类型更克制;「待处理 + 或签」同排不再撞色糊成一团。</div>
|
||||
</section>
|
||||
|
||||
<!-- 方案 C -->
|
||||
<section class="card">
|
||||
<h2>方案 C · 低饱和克制 <span class="pill alt">ALT</span></h2>
|
||||
<p class="sub">类型几乎中性灰描边,仅状态保留语义色;最克制,但会签/或签/抄送辨识变弱</p>
|
||||
<div class="phone c">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批进度 · 方案 C</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起申请</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person"><strong>张三</strong><span>03-12 09:20</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">部门审批</span>
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person"><strong>李四 · 王五</strong><span>03-12 14:01</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot warn"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">分管领导</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge or">或签</span>
|
||||
</div>
|
||||
<div class="person"><strong>赵六 / 钱七</strong><span>—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">相关知会</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">流程结束</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch-block">
|
||||
<h3>全量色板</h3>
|
||||
<div class="swatches">
|
||||
<span class="badge ok">已同意</span>
|
||||
<span class="badge ok">已完成</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
<span class="badge warn">待处理</span>
|
||||
<span class="badge warn">进行中</span>
|
||||
<span class="badge danger">已拒绝</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
<span class="badge danger">已打回</span>
|
||||
<span class="badge mute">已结束</span>
|
||||
<span class="badge mute">未到达</span>
|
||||
<span class="badge mute">已转交</span>
|
||||
<span class="badge and">会签</span>
|
||||
<span class="badge or">或签</span>
|
||||
<span class="badge cc">抄送</span>
|
||||
<span class="badge end">结束</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">适合极简审美;代价是类型 tag 几乎靠文字区分,色相辨识弱。</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="palette-table">
|
||||
<h2>方案 B 色值明细(推荐落地稿)</h2>
|
||||
<p>状态 = 浅底实心;类型 = 白底 + 1px 描边。字号 11px / 圆角 4px / padding 1×6,与现状结构一致。</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类别</th>
|
||||
<th>文案</th>
|
||||
<th>class</th>
|
||||
<th>文字色</th>
|
||||
<th>背景</th>
|
||||
<th>边框</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>状态</td><td>已同意 / 已完成 / 已提交</td><td><code>ok</code></td>
|
||||
<td><code>#087f57</code></td><td><code>#e9f9f2</code></td><td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>状态</td><td>待处理 / 进行中</td><td><code>warn</code></td>
|
||||
<td><code>#a66b10</code></td><td><code>#fff7e8</code></td><td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>状态</td><td>已拒绝 / 已撤销 / 已打回</td><td><code>danger</code></td>
|
||||
<td><code>#b42318</code></td><td><code>#fef3f2</code></td><td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>状态</td><td>未到达 / 已转交 / 已结束</td><td><code>mute</code></td>
|
||||
<td><code>#5f7a6d</code></td><td><code>#f1f5f3</code></td><td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>类型</td><td>会签</td><td><code>and</code></td>
|
||||
<td><code>#3730a3</code></td><td><code>#fff</code></td><td><code>#c7d2fe</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>类型</td><td>或签</td><td><code>or</code></td>
|
||||
<td><code>#0e7490</code></td><td><code>#fff</code></td><td><code>#a5f3fc</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>类型</td><td>抄送</td><td><code>cc</code></td>
|
||||
<td><code>#6d28d9</code></td><td><code>#fff</code></td><td><code>#ddd6fe</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>类型</td><td>结束</td><td><code>end</code></td>
|
||||
<td><code>#64748b</code></td><td><code>#fff</code></td><td><code>#e2e8f0</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="pick-bar">
|
||||
<span>选一套方案继续细化:</span>
|
||||
<button type="button" data-choice="A">方案 A 浅底实心</button>
|
||||
<button type="button" class="rec" data-choice="B">方案 B 双轨(推荐)</button>
|
||||
<button type="button" data-choice="C">方案 C 低饱和</button>
|
||||
<p class="pick-note" id="pickNote">直接回复「选 B」或点按钮即可。也可说「A + 会签改成某某色」微调。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.pick-bar button').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const choice = btn.getAttribute('data-choice')
|
||||
const note = document.getElementById('pickNote')
|
||||
note.textContent = '已记录偏好:方案 ' + choice + '。请回到对话里告诉我,或直接说「就用 ' + choice + '」。'
|
||||
note.style.color = '#047857'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,534 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<title>mobile-oa-h5 · 撤销后审批进度展示 Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--brand: #0a9f6d;
|
||||
--brand-dark: #087f57;
|
||||
--page-bg: #f5f8f7;
|
||||
--text: #1f2a26;
|
||||
--text-muted: #6b7b74;
|
||||
--border: #dcebe4;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
|
||||
radial-gradient(700px 380px at 92% 0%, rgba(214, 163, 67, 0.1), transparent 50%),
|
||||
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.stage { max-width: 1320px; margin: 0 auto; padding: 28px 16px 64px; }
|
||||
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
|
||||
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 960px; }
|
||||
.goals {
|
||||
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
||||
background: #ecfdf5; border: 1px solid #a7f3d0;
|
||||
font-size: 13px; color: #065f46;
|
||||
}
|
||||
.goals ul { margin: 6px 0 0; padding-left: 18px; }
|
||||
.scene {
|
||||
margin-top: 16px; padding: 12px 14px; border-radius: 12px;
|
||||
background: #fff; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;
|
||||
}
|
||||
.scene code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px; background: #f1f5f9; padding: 1px 6px; border-radius: 4px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin-top: 22px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255,255,255,.94);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 10px 28px rgba(15,23,42,.05);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
}
|
||||
.card .sub { margin: 0 0 12px; font-size: 12px; color: #64748b; }
|
||||
.pill {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
||||
font-size: 10px; font-weight: 700; letter-spacing: 0.02em;
|
||||
}
|
||||
.pill.now { background: #fff1f2; color: #be123c; border: 1px solid #fecdd3; }
|
||||
.pill.rec { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
|
||||
.pill.alt { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
|
||||
|
||||
.phone {
|
||||
width: 100%; max-width: 360px; margin: 0 auto;
|
||||
border-radius: 28px; border: 1px solid #cbd5e1; background: #0f172a;
|
||||
padding: 10px; box-shadow: 0 18px 40px rgba(15,23,42,.12);
|
||||
}
|
||||
.phone__screen {
|
||||
border-radius: 20px; overflow: hidden; background: var(--page-bg);
|
||||
min-height: 460px; display: flex; flex-direction: column;
|
||||
}
|
||||
.nav {
|
||||
display: flex; align-items: center; gap: 8px; min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
background: linear-gradient(180deg, #d1fae5 0%, #ecfdf5 100%);
|
||||
border-bottom: 1px solid #bbf7d0;
|
||||
}
|
||||
.nav__back {
|
||||
width: 28px; height: 28px; border-radius: 999px;
|
||||
display: grid; place-items: center;
|
||||
color: var(--brand-dark); font-weight: 700; font-size: 16px;
|
||||
background: rgba(255,255,255,.55);
|
||||
}
|
||||
.nav__title { flex: 1; font-size: 15px; font-weight: 700; color: #064e3b; }
|
||||
.body { padding: 12px; display: grid; gap: 10px; }
|
||||
|
||||
.stamp-row {
|
||||
position: relative;
|
||||
background: #fff; border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 12px 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stamp-row__meta { font-size: 12px; color: var(--text-muted); }
|
||||
.stamp-row__title { margin-top: 4px; font-size: 15px; font-weight: 700; }
|
||||
.stamp {
|
||||
position: absolute; right: 10px; top: 8px;
|
||||
width: 64px; height: 64px; border-radius: 50%;
|
||||
border: 2px solid #be123c; color: #be123c;
|
||||
display: grid; place-items: center;
|
||||
font-size: 13px; font-weight: 800;
|
||||
transform: rotate(-18deg);
|
||||
opacity: .85;
|
||||
background: rgba(255,241,242,.55);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background: #fff; border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 12px;
|
||||
}
|
||||
.progress__title {
|
||||
margin: 0 0 10px; font-size: 15px; font-weight: 700; color: #0f291e;
|
||||
}
|
||||
.step { display: flex; gap: 10px; min-height: 52px; }
|
||||
.rail { display: flex; flex-direction: column; align-items: center; width: 16px; }
|
||||
.dot {
|
||||
width: 10px; height: 10px; margin-top: 6px; border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
}
|
||||
.dot.ok { background: #189b65; }
|
||||
.dot.warn { background: #cf8a19; }
|
||||
.dot.danger { background: #d14343; }
|
||||
.dot.mute { background: #94a3b8; }
|
||||
.line { flex: 1; width: 2px; margin: 4px 0 0; background: #e2e8f0; min-height: 28px; }
|
||||
.step-card { flex: 1; min-width: 0; padding-bottom: 12px; }
|
||||
.step:last-child .step-card { padding-bottom: 0; }
|
||||
.row {
|
||||
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
|
||||
}
|
||||
.name { font-size: 14px; font-weight: 600; color: #0f291e; }
|
||||
.person-block { margin-top: 6px; }
|
||||
.person {
|
||||
font-size: 12px; color: #5f7a6d;
|
||||
display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
|
||||
}
|
||||
.person strong { color: #1f2a26; font-weight: 600; }
|
||||
.person.is-ok strong { color: #189b65; }
|
||||
.person.is-danger strong { color: #d14343; }
|
||||
.person.is-mute strong,
|
||||
.person.is-mute .detail { color: #94a3b8; }
|
||||
.person .time { color: #94a3b8; white-space: nowrap; font-size: 11px; }
|
||||
.person .detail { margin-left: 4px; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: 1px 7px; border-radius: 999px;
|
||||
font-size: 11px; font-weight: 650; line-height: 18px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.badge.ok { color: #047857; background: #ecfdf5; border-color: #a7f3d0; }
|
||||
.badge.warn { color: #b45309; background: #fffbeb; border-color: #fde68a; }
|
||||
.badge.danger { color: #b91c1c; background: #fef2f2; border-color: #fecaca; }
|
||||
.badge.mute { color: #64748b; background: #f8fafc; border-color: #e2e8f0; }
|
||||
.badge.and { color: #0f766e; background: #f0fdfa; border-color: #99f6e4; }
|
||||
|
||||
.callout {
|
||||
margin-top: 10px; padding: 8px 10px; border-radius: 8px;
|
||||
font-size: 12px; line-height: 1.5;
|
||||
}
|
||||
.callout.bad { background: #fff1f2; color: #9f1239; border: 1px solid #fecdd3; }
|
||||
.callout.good { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
|
||||
.callout.neutral { background: #f8fafc; color: #475569; border: 1px solid #e2e8f0; }
|
||||
|
||||
.legend {
|
||||
margin-top: 28px; padding: 14px 16px; border-radius: 14px;
|
||||
background: rgba(255,255,255,.94); border: 1px solid #e2e8f0;
|
||||
font-size: 13px; color: #334155;
|
||||
}
|
||||
.legend h3 { margin: 0 0 8px; font-size: 14px; }
|
||||
.legend table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.legend th, .legend td {
|
||||
text-align: left; padding: 8px 6px; border-bottom: 1px solid #f1f5f9;
|
||||
vertical-align: top;
|
||||
}
|
||||
.legend th { color: #64748b; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="stage">
|
||||
<div class="hero">
|
||||
<h1>撤销后 · 审批进度怎么展示?</h1>
|
||||
<p>
|
||||
场景:发起人提交后,在「直接主管」尚未审批时撤销。库表里该节点任务
|
||||
<code>status=cancelled</code> 是引擎为清掉待办写的,不等于主管做了「撤销」。
|
||||
「已撤销」语义应归发起人/实例终态,不应挂在主管节点上。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="goals">
|
||||
<strong>设计目标</strong>
|
||||
<ul>
|
||||
<li>不把发起人撤销误读成「直接主管撤销」</li>
|
||||
<li>「已撤销」只出现在实例终态(印章 / 进度末行)</li>
|
||||
<li>被系统取消的待办节点,展示应对齐或签免处理(「无需处理」)或弱化</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="scene">
|
||||
<strong>样例数据</strong>:实例
|
||||
<code>E40F649F…</code> · 11:17 提交 → 12:47 发起人
|
||||
<code>withdraw</code> · 「直接主管」任务从未审批,仅被系统改为
|
||||
<code>cancelled</code>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- A: 现状问题 -->
|
||||
<section class="card">
|
||||
<h2>
|
||||
A. 现状(有问题)
|
||||
<span class="pill now">当前</span>
|
||||
</h2>
|
||||
<p class="sub">节点徽章 + 人员行都写「已撤销」→ 像是主管撤销了</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批详情</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="stamp-row">
|
||||
<div class="stamp-row__meta">差旅报销 · 我发起的</div>
|
||||
<div class="stamp-row__title">出差申请单</div>
|
||||
<div class="stamp">已撤销</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起人</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-ok">
|
||||
<span><strong>吴子龙</strong></span>
|
||||
<span class="time">08-13 11:17</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot danger"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">直接主管</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-danger">
|
||||
<span><strong>张主管</strong><span class="detail">已撤销</span></span>
|
||||
<span class="time">08-13 12:47</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot danger"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">已撤销</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callout bad">
|
||||
✗ 主管节点挂了「已撤销」,语义错位;终态行再写一次,重复且误导。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- B: 推荐 -->
|
||||
<section class="card">
|
||||
<h2>
|
||||
B. 推荐:主管「无需处理」
|
||||
<span class="pill rec">推荐</span>
|
||||
</h2>
|
||||
<p class="sub">对齐或签免处理:系统取消的待办 = 无需处理;「已撤销」只在终态</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批详情</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="stamp-row">
|
||||
<div class="stamp-row__meta">差旅报销 · 我发起的</div>
|
||||
<div class="stamp-row__title">出差申请单</div>
|
||||
<div class="stamp">已撤销</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起人</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-ok">
|
||||
<span><strong>吴子龙</strong></span>
|
||||
<span class="time">08-13 11:17</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">直接主管</span>
|
||||
<span class="badge mute">无需处理</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-mute">
|
||||
<span><strong>张主管</strong><span class="detail">无需处理</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot danger"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">已撤销</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-danger">
|
||||
<span><strong>吴子龙</strong><span class="detail">发起人撤销</span></span>
|
||||
<span class="time">08-13 12:47</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callout good">
|
||||
✓ 主管只是「未轮到 / 被取消」;谁撤销、何时撤销在终态说清楚。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- C: 备选:不展示取消节点 -->
|
||||
<section class="card">
|
||||
<h2>
|
||||
C. 备选:不展示未批节点
|
||||
<span class="pill alt">备选</span>
|
||||
</h2>
|
||||
<p class="sub">进度只保留「已发生」动作:提交 + 终态撤销;主管卡不出现</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批详情</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="stamp-row">
|
||||
<div class="stamp-row__meta">差旅报销 · 我发起的</div>
|
||||
<div class="stamp-row__title">出差申请单</div>
|
||||
<div class="stamp">已撤销</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起人</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-ok">
|
||||
<span><strong>吴子龙</strong></span>
|
||||
<span class="time">08-13 11:17</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot danger"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">已撤销</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-danger">
|
||||
<span><strong>吴子龙</strong><span class="detail">发起人撤销</span></span>
|
||||
<span class="time">08-13 12:47</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callout neutral">
|
||||
○ 最干净,但看不到「曾经到过主管节点」;多人会签部分已批、部分 cancelled 时更难统一。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- D: 备选:节点无状态,仅姓名 -->
|
||||
<section class="card">
|
||||
<h2>
|
||||
D. 备选:节点弱化、无徽章
|
||||
<span class="pill alt">备选</span>
|
||||
</h2>
|
||||
<p class="sub">主管卡只留姓名灰显,不写「已撤销/无需处理」;语义全靠终态</p>
|
||||
<div class="phone">
|
||||
<div class="phone__screen">
|
||||
<div class="nav">
|
||||
<div class="nav__back">‹</div>
|
||||
<div class="nav__title">审批详情</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="stamp-row">
|
||||
<div class="stamp-row__meta">差旅报销 · 我发起的</div>
|
||||
<div class="stamp-row__title">出差申请单</div>
|
||||
<div class="stamp">已撤销</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress__title">审批进度</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot ok"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">发起人</span>
|
||||
<span class="badge ok">已提交</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-ok">
|
||||
<span><strong>吴子龙</strong></span>
|
||||
<span class="time">08-13 11:17</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot mute"></div><div class="line"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">直接主管</span>
|
||||
<span class="badge and">会签</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-mute">
|
||||
<span><strong>张主管</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="rail"><div class="dot danger"></div></div>
|
||||
<div class="step-card">
|
||||
<div class="row">
|
||||
<span class="name">已撤销</span>
|
||||
<span class="badge danger">已撤销</span>
|
||||
</div>
|
||||
<div class="person-block">
|
||||
<div class="person is-danger">
|
||||
<span><strong>吴子龙</strong><span class="detail">发起人撤销</span></span>
|
||||
<span class="time">08-13 12:47</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="callout neutral">
|
||||
○ 不误导,但主管行为态不明确;用户可能疑惑「卡在主管了吗」。
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<h3>映射建议(若选 B)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>库表 / API</th>
|
||||
<th>节点徽章</th>
|
||||
<th>人员行</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>任务 <code>cancelled</code>(撤销/打回清待办)</td>
|
||||
<td><span class="badge mute">无需处理</span></td>
|
||||
<td>姓名 +「无需处理」(无时间,mute)</td>
|
||||
<td>与或签 <code>done</code> 同文案;勿写「已撤销」</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>实例 <code>cancelled</code> + 终态项</td>
|
||||
<td><span class="badge danger">已撤销</span></td>
|
||||
<td>发起人 +「发起人撤销」+ 撤销时间</td>
|
||||
<td>唯一表达「谁撤销了本单」</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>打回时其它人的 <code>cancelled</code></td>
|
||||
<td>同上「无需处理」</td>
|
||||
<td>同上</td>
|
||||
<td>打回结论落在操作人任务上,不落在这些取消任务上</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="margin:12px 0 0;color:#64748b;font-size:12px;">
|
||||
打开方式:用浏览器直接打开本文件即可对比。选定方案后告诉我,再落设计说明与实现。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,11 @@
|
||||
# Graph Report - src (2026-08-12)
|
||||
# Graph Report - src (2026-08-13)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 967 files · ~631,821 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
- Large corpus: 972 files · ~633,368 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
|
||||
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 1076 edges (avg confidence: 0.8)
|
||||
- 3436 nodes · 4893 edges · 71 communities detected
|
||||
- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 1084 edges (avg confidence: 0.8)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
@@ -50,60 +50,60 @@
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 40|Community 40]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
- [[_COMMUNITY_Community 46|Community 46]]
|
||||
- [[_COMMUNITY_Community 47|Community 47]]
|
||||
- [[_COMMUNITY_Community 50|Community 50]]
|
||||
- [[_COMMUNITY_Community 48|Community 48]]
|
||||
- [[_COMMUNITY_Community 52|Community 52]]
|
||||
- [[_COMMUNITY_Community 53|Community 53]]
|
||||
- [[_COMMUNITY_Community 55|Community 55]]
|
||||
- [[_COMMUNITY_Community 54|Community 54]]
|
||||
- [[_COMMUNITY_Community 56|Community 56]]
|
||||
- [[_COMMUNITY_Community 57|Community 57]]
|
||||
- [[_COMMUNITY_Community 58|Community 58]]
|
||||
- [[_COMMUNITY_Community 59|Community 59]]
|
||||
- [[_COMMUNITY_Community 60|Community 60]]
|
||||
- [[_COMMUNITY_Community 62|Community 62]]
|
||||
- [[_COMMUNITY_Community 61|Community 61]]
|
||||
- [[_COMMUNITY_Community 63|Community 63]]
|
||||
- [[_COMMUNITY_Community 66|Community 66]]
|
||||
- [[_COMMUNITY_Community 68|Community 68]]
|
||||
- [[_COMMUNITY_Community 64|Community 64]]
|
||||
- [[_COMMUNITY_Community 67|Community 67]]
|
||||
- [[_COMMUNITY_Community 69|Community 69]]
|
||||
- [[_COMMUNITY_Community 74|Community 74]]
|
||||
- [[_COMMUNITY_Community 76|Community 76]]
|
||||
- [[_COMMUNITY_Community 77|Community 77]]
|
||||
- [[_COMMUNITY_Community 78|Community 78]]
|
||||
- [[_COMMUNITY_Community 79|Community 79]]
|
||||
- [[_COMMUNITY_Community 80|Community 80]]
|
||||
- [[_COMMUNITY_Community 81|Community 81]]
|
||||
- [[_COMMUNITY_Community 86|Community 86]]
|
||||
- [[_COMMUNITY_Community 84|Community 84]]
|
||||
- [[_COMMUNITY_Community 97|Community 97]]
|
||||
- [[_COMMUNITY_Community 99|Community 99]]
|
||||
- [[_COMMUNITY_Community 100|Community 100]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `get()` - 306 edges
|
||||
2. `push()` - 119 edges
|
||||
2. `push()` - 120 edges
|
||||
3. `emit()` - 30 edges
|
||||
4. `WebSocketClient` - 26 edges
|
||||
5. `set()` - 21 edges
|
||||
6. `handleDynamicRoutes()` - 19 edges
|
||||
7. `parseProtocolStandardImportText()` - 17 edges
|
||||
8. `walkRules()` - 16 edges
|
||||
9. `onPick()` - 16 edges
|
||||
8. `onPick()` - 17 edges
|
||||
9. `walkRules()` - 16 edges
|
||||
10. `scopeQuery()` - 16 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
|
||||
views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
|
||||
- `onPick()` --calls--> `ensureOvertimeKitRuleProps()` [INFERRED]
|
||||
components/oa-approval/OaStartFormDialog.vue → utils/oa-overtime-kit-runtime.ts
|
||||
- `formatReceiveAccountLine()` --calls--> `push()` [INFERRED]
|
||||
utils/oa-receive-account-value.ts → views/operations/storage-facility/index.vue
|
||||
- `dash()` --calls--> `formatDisplayOrDash()` [INFERRED]
|
||||
components/business/infusion-schedule-outbound-chain-view/index.vue → utils/datetime-display.ts
|
||||
- `dash()` --calls--> `formatDisplayOrDash()` [INFERRED]
|
||||
components/business/clinical-schedule-payment-detail/index.vue → utils/datetime-display.ts
|
||||
- `formatDetectMethods()` --calls--> `push()` [INFERRED]
|
||||
utils/qc-work-order-card-meta.ts → views/operations/storage-facility/index.vue
|
||||
- `buildCellStorageGroupContext()` --calls--> `push()` [INFERRED]
|
||||
utils/qc-work-order-card-meta.ts → views/operations/storage-facility/index.vue
|
||||
- `buildDonationGroupContext()` --calls--> `push()` [INFERRED]
|
||||
utils/qc-work-order-card-meta.ts → views/operations/storage-facility/index.vue
|
||||
- `buildOutboundPilotGroupContext()` --calls--> `push()` [INFERRED]
|
||||
utils/qc-work-order-card-meta.ts → views/operations/storage-facility/index.vue
|
||||
- `buildOutboundBasicExperimentGroupContext()` --calls--> `push()` [INFERRED]
|
||||
utils/qc-work-order-card-meta.ts → views/operations/storage-facility/index.vue
|
||||
|
||||
## Communities
|
||||
|
||||
@@ -112,270 +112,270 @@ Cohesion: 0.0
|
||||
Nodes (200): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+192 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.03
|
||||
Nodes (86): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail(), fetchPilotCheckupList() (+78 more)
|
||||
Cohesion: 0.02
|
||||
Nodes (170): fetchDeptTree(), fetchUploadAttachment(), fetchOaFlowPreview(), fetchOaRelatedSummaries(), fetchOaTemplateDetail(), handleReportUpload(), handleReportUpload(), walk() (+162 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.03
|
||||
Nodes (74): fetchUploadAttachment(), FireworkSystem, handleReportUpload(), handleReportUpload(), walk(), if(), async(), for() (+66 more)
|
||||
Nodes (88): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), markPilotCheckupChecked() (+80 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.03
|
||||
Nodes (79): fetchEthnicList(), formatNowDatetime(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), if(), loadEthnicOptions() (+71 more)
|
||||
Cohesion: 0.02
|
||||
Nodes (72): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage() (+64 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.03
|
||||
Nodes (62): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaInstanceDetail() (+54 more)
|
||||
Nodes (79): fetchEthnicList(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), if(), loadEthnicOptions(), normalizeSampleRow() (+71 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.03
|
||||
Nodes (34): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+26 more)
|
||||
Nodes (64): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchUpdateCheckupInstitution(), fetchUpdateCheckupInstitutionInvoice(), fetchUpdateCheckupInstitutionProject() (+56 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.03
|
||||
Nodes (63): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchUpdateCheckupInstitution(), fetchUpdateCheckupInstitutionInvoice(), fetchUpdateCheckupInstitutionProject() (+55 more)
|
||||
Nodes (62): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaInstanceDetail() (+54 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.03
|
||||
Nodes (57): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), buildBaseFields(), buildFeeSection() (+49 more)
|
||||
Nodes (34): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+26 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.03
|
||||
Nodes (56): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage() (+48 more)
|
||||
Nodes (47): handleBlur(), handleFocus(), handleInput(), fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed() (+39 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.03
|
||||
Nodes (57): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchCreateProtocolInvoiceRequest(), fetchInvoiceEligiblePayments(), fetchPaymentDetailList() (+49 more)
|
||||
Nodes (55): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTemporaryViewSections(), buildTransportSection(), resolveInboundProtocolStatus(), buildInsertedSiblingCard() (+47 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.03
|
||||
Nodes (41): fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), handleDialogClose(), nodeSummary(), onAdd(), onAddInGroup(), createDefaultFlow() (+33 more)
|
||||
Nodes (59): fetchFullmoonTodoList(), fetchUpdateFullmoonTodoInsurance(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolDetail(), fetchCollectionProtocolList(), fetchCreateProtocolInvoiceRequest(), fetchInvoiceEligiblePayments(), fetchPaymentDetailList() (+51 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.04
|
||||
Nodes (49): buildTemporaryViewSections(), buildInsertedSiblingCard(), resolveShowProtocolPaymentAtGroup(), appendTaskResultSections(), batchTasksForPanelSlot(), buildConfiguredOnlySection(), for(), for() (+41 more)
|
||||
Cohesion: 0.03
|
||||
Nodes (57): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), updateInfusionScheduleStatus(), handleDialogClose(), handleAdd(), handleDelete() (+49 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.03
|
||||
Nodes (51): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), if(), branchConditionLines(), collectConditionReferencedFields() (+43 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (42): buildBaseFields(), buildFeeSection(), buildInboundBasicSection(), buildInboundCellStorageProductionViewSections(), buildInboundResearchDonationProductionViewSections(), buildOutboundBasicExperimentProductionViewSections(), buildOutboundClinicalProductionViewSections(), buildOutboundDetail() (+34 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.04
|
||||
Nodes (53): fetchDeptTree(), fetchOaFlowPreview(), fetchOaTemplateDetail(), chipTitle(), confirmPicker(), memberOptionLabel(), removeUser(), resolveUser() (+45 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.05
|
||||
Nodes (53): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), toLogin(), fetchUserInfo() (+45 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.04
|
||||
Nodes (43): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchCreateMenu(), fetchDeleteCellSampleType(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateCellSampleType(), fetchUpdateMenu() (+35 more)
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.03
|
||||
Nodes (22): fetchProductionWorkOrderQcInfo(), fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), dash() (+14 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (38): attachmentFullUrl(), dash(), handleAttachmentDownload(), handleAttachmentPreview(), buildPaymentFlowSteps(), dash(), formatAmount(), hasFinanceReceipt() (+30 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.05
|
||||
Nodes (33): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), fetchMarkQcMaterialUsageFullyUsed(), collectIds(), postorder(), preorder() (+25 more)
|
||||
Nodes (26): fetchUserList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), formatHandoverUserLabel(), formatNowDatetime(), fullFileUrl(), handlePreviewAttachment() (+18 more)
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.06
|
||||
Nodes (49): fetchOaRelatedSummaries(), buildFlowItems(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf() (+41 more)
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.06
|
||||
Nodes (41): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), fetchClinicalRefundConfirmed(), buildConfirmedParams(), buildPendingParams() (+33 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.05
|
||||
Nodes (23): handleBlur(), handleFocus(), handleInput(), fetchClinicalReceiptConfirmed(), buildConfirmedParams(), buildPendingParams(), formatAmount(), handleConfirmedReset() (+15 more)
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.06
|
||||
Nodes (38): fetchUserList(), fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit() (+30 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.07
|
||||
Nodes (39): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), updateInfusionScheduleStatus(), handleAdd(), handleBatchDelete(), handleDelete() (+31 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.08
|
||||
Nodes (37): formatDatetimeToMinute(), handleChineseDatetimePaste(), parseChineseDatetimeText(), parseBirthOrder(), parseChineseNumeral(), addSampleHint(), earliestCollectTime(), isMaternalBloodEmpty() (+29 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.06
|
||||
Nodes (21): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+13 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.1
|
||||
Nodes (2): WebSocketClient, TableCache
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.06
|
||||
Nodes (13): fetchProductionWorkOrderQcInfo(), dash(), fmtMoney(), auditorDisplay(), dash(), ensureUnitsInOptions(), expressName(), formatYm() (+5 more)
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.09
|
||||
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.08
|
||||
Nodes (20): createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments(), serializeEquipmentIds() (+12 more)
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.09
|
||||
Nodes (30): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchProductionTaskList() (+22 more)
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.07
|
||||
Nodes (19): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+11 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 0.09
|
||||
Nodes (22): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderTabStepCounts(), fetchProductionWorkOrderTabStepCountsDeduped() (+14 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 0.09
|
||||
Nodes (22): fetchOaAppList(), fetchOaGroupList(), fetchPublishOaApp(), loadData(), loadGroups(), resetForm(), onSuccess(), goBack() (+14 more)
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 0.1
|
||||
Nodes (21): displayPeople(), nodeStatusText(), personLines(), personStatusHint(), personTitle(), showNodeComment(), signBadge(), signBadgeText() (+13 more)
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 0.11
|
||||
Nodes (24): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), handleBatchComplete(), handleBatchStart(), handleBatchVoid() (+16 more)
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.09
|
||||
Nodes (12): fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload(), reloadTabCounts() (+4 more)
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.18
|
||||
Nodes (22): buildPaymentFlowSteps(), dash(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit(), receiptSubmitTime() (+14 more)
|
||||
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 0.1
|
||||
Nodes (12): fetchOutboundReleaseMaterial(), fetchOutboundReleaseReviewDetail(), fetchOutboundReleaseReviewList(), fetchReleaseReviewList(), handleReset(), handleSearch(), handleTabChange(), handleToggleSelect() (+4 more)
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.13
|
||||
Nodes (18): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchDeleteCellSampleType(), fetchUpdateCellSampleType(), handleAdd(), handleDelete(), handleDropdownCommand(), handleEdit() (+10 more)
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 0.14
|
||||
Nodes (13): displayPeople(), personLines(), personTitle(), showNodeComment(), signBadge(), signBadgeText(), statusText(), displayName() (+5 more)
|
||||
Nodes (14): if(), calcTripLegDays(), dayIndex(), emptyTripLeg(), endOffset(), startOffset(), sumTripLegDays(), buildTripKitChildren() (+6 more)
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 0.19
|
||||
Nodes (15): fetchAttachmentDownloadUrl(), attachmentFullUrl(), ensureUniqueZipEntryName(), fetchAttachmentBlob(), getAttachmentExtension(), resolveAttachmentFileName(), sanitizeZipPathSegment(), applyInboundLabStorageCodePrefix() (+7 more)
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 0.13
|
||||
Nodes (8): chipTitle(), confirmPicker(), memberOptionLabel(), removeUser(), resolveUser(), signBadge(), signBadgeText(), updateSelect()
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.2
|
||||
Nodes (14): applyFlagToRule(), applySchema(), countRules(), getActiveRule(), getDesigner(), getSetupState(), onMessage(), readFlag() (+6 more)
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.12
|
||||
Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
|
||||
|
||||
### Community 38 - "Community 38"
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.12
|
||||
Nodes (6): setupOaConfirmationKit(), ensureOaKitMenu(), setupOaLeaveKit(), setupOaOvertimeKit(), setupOaResignationKit(), setupOaTripKit()
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 0.12
|
||||
Nodes (6): attachmentFullUrl(), dash(), handleAttachmentDownload(), handleAttachmentPreview(), ensureOaUploadRuleProps(), resolveUploadResponseUrl()
|
||||
|
||||
### Community 40 - "Community 40"
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 0.16
|
||||
Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 0.27
|
||||
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): fullFileUrl(), handleDownloadAttachment(), handlePreviewAttachment(), isVideoFile(), normalizeDisplayValue(), resolveReviewCellType(), resolveReviewQuantity(), resolveReviewSpecText() (+1 more)
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 0.14
|
||||
Nodes (8): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), themeAnimation(), toggleTheme(), toggleTransition()
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
### Community 40 - "Community 40"
|
||||
Cohesion: 0.19
|
||||
Nodes (6): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData(), switchTab()
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.15
|
||||
Nodes (4): fetchNextDonationSampleCode(), fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
|
||||
|
||||
### Community 46 - "Community 46"
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 0.24
|
||||
Nodes (8): fullFileUrl(), handlePreviewAttachment(), isVideoFile(), normalizeDisplayValue(), resolveReviewCellType(), resolveReviewQuantity(), resolveReviewSpecText(), uniqueJoined()
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 0.31
|
||||
Nodes (4): isSlotAvailable(), resolveStorageSlotRangeFill(), slotFacilityNodeId(), sortBoxSlots()
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 0.39
|
||||
Nodes (8): formatReceiveAccountDisplay(), formatReceiveAccountLine(), isReceiveAccountSnapshot(), isReceiveAccountType(), normalizeReceiveAccountModelValue(), optionLabelFromRow(), snapshotFromRow(), trimSnapshot()
|
||||
|
||||
### Community 46 - "Community 46"
|
||||
Cohesion: 0.44
|
||||
Nodes (6): fileNameHint(), fileUrlHint(), getOaUploadDisplayName(), isOaUploadImageFile(), isOaUploadVideoFile(), mimeHint()
|
||||
|
||||
### Community 47 - "Community 47"
|
||||
Cohesion: 0.25
|
||||
Nodes (2): handleTouchEnd(), setTransition()
|
||||
|
||||
### Community 50 - "Community 50"
|
||||
### Community 48 - "Community 48"
|
||||
Cohesion: 0.28
|
||||
Nodes (4): fetchMyProductionMaterialConsumptions(), handleReset(), handleSearch(), loadData()
|
||||
Nodes (1): FireworkSystem
|
||||
|
||||
### Community 52 - "Community 52"
|
||||
Cohesion: 0.39
|
||||
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
|
||||
Nodes (6): assignQcItemParamRowKeys(), formatQcParamOptionText(), hydrateQcItemParamsForForm(), parseQcParamOptions(), serializeQcItemParamsForSave(), validateQcItemParamsBeforeSave()
|
||||
|
||||
### Community 53 - "Community 53"
|
||||
Cohesion: 0.39
|
||||
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
|
||||
|
||||
### Community 54 - "Community 54"
|
||||
Cohesion: 0.36
|
||||
Nodes (1): TableHeightCalculator
|
||||
|
||||
### Community 55 - "Community 55"
|
||||
### Community 56 - "Community 56"
|
||||
Cohesion: 0.33
|
||||
Nodes (2): logApiCryptoBootInfo(), maskSecret()
|
||||
|
||||
### Community 56 - "Community 56"
|
||||
### Community 57 - "Community 57"
|
||||
Cohesion: 0.38
|
||||
Nodes (4): handleFocus(), handleInput(), parseDecimalToNumber(), sanitizeDecimalInput()
|
||||
|
||||
### Community 57 - "Community 57"
|
||||
### Community 58 - "Community 58"
|
||||
Cohesion: 0.4
|
||||
Nodes (2): isUnfinishedQcWorkOrder(), resolveQcWorkOrderUnfinishedBadgeCount()
|
||||
|
||||
### Community 58 - "Community 58"
|
||||
### Community 59 - "Community 59"
|
||||
Cohesion: 0.47
|
||||
Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
|
||||
|
||||
### Community 59 - "Community 59"
|
||||
### Community 60 - "Community 60"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): formatProtocolDocDeleteConfirm(), formatProtocolDocSubject()
|
||||
|
||||
### Community 60 - "Community 60"
|
||||
### Community 61 - "Community 61"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): parseEquipmentIds(), syncEquipmentNamesFromOptions()
|
||||
|
||||
### Community 62 - "Community 62"
|
||||
### Community 63 - "Community 63"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): focusInput(), handleKeydown()
|
||||
|
||||
### Community 63 - "Community 63"
|
||||
### Community 64 - "Community 64"
|
||||
Cohesion: 0.4
|
||||
Nodes (1): fetchQcMaterialAmountAuthUsers()
|
||||
|
||||
### Community 66 - "Community 66"
|
||||
### Community 67 - "Community 67"
|
||||
Cohesion: 0.67
|
||||
Nodes (2): if(), logSidebarMenu()
|
||||
|
||||
### Community 68 - "Community 68"
|
||||
Cohesion: 0.5
|
||||
Nodes (1): fetchSampleSmsNotificationList()
|
||||
|
||||
### Community 69 - "Community 69"
|
||||
Cohesion: 0.5
|
||||
Nodes (1): fetchQcSmsList()
|
||||
Nodes (1): fetchStorageEntrySmsList()
|
||||
|
||||
### Community 74 - "Community 74"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): getSettingDefaults(), resetToDefaults()
|
||||
|
||||
### Community 77 - "Community 77"
|
||||
### Community 76 - "Community 76"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): buildOvertimeKitChildren(), uniqueId()
|
||||
|
||||
### Community 78 - "Community 78"
|
||||
### Community 77 - "Community 77"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): buildLeaveKitChildren(), uniqueId()
|
||||
|
||||
### Community 79 - "Community 79"
|
||||
### Community 78 - "Community 78"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): buildConfirmationKitChildren(), uniqueId()
|
||||
|
||||
### Community 80 - "Community 80"
|
||||
### Community 79 - "Community 79"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): buildResignationKitChildren(), uniqueId()
|
||||
|
||||
### Community 81 - "Community 81"
|
||||
### Community 80 - "Community 80"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): copyTextToClipboard(), copyTextWithFallback()
|
||||
|
||||
### Community 86 - "Community 86"
|
||||
### Community 84 - "Community 84"
|
||||
Cohesion: 1.0
|
||||
Nodes (2): getSourceProtocolAttachment(), normalizeAttachmentFiles()
|
||||
|
||||
@@ -392,43 +392,43 @@ Cohesion: 1.0
|
||||
Nodes (2): emptyTabCounts(), useQcWorkOrderTabUnfinishedCounts()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **Thin community `Community 23`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
|
||||
- **Thin community `Community 19`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 47`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 53`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
|
||||
- **Thin community `Community 48`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 55`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
|
||||
- **Thin community `Community 54`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 57`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
|
||||
- **Thin community `Community 56`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 59`** (5 nodes): `formatProtocolDocBatchDeleteConfirm()`, `formatProtocolDocDeleteConfirm()`, `formatProtocolDocSubject()`, `getProtocolDocTypeLabel()`, `protocol-doc-label.ts`
|
||||
- **Thin community `Community 58`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 60`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
|
||||
- **Thin community `Community 60`** (5 nodes): `formatProtocolDocBatchDeleteConfirm()`, `formatProtocolDocDeleteConfirm()`, `formatProtocolDocSubject()`, `getProtocolDocTypeLabel()`, `protocol-doc-label.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 62`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
|
||||
- **Thin community `Community 61`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 63`** (5 nodes): `fetchBatchRevokeQcMaterialAmountAuth()`, `fetchGrantQcMaterialAmountAuth()`, `fetchQcMaterialAmountAuthUsers()`, `fetchRevokeQcMaterialAmountAuth()`, `qc-material-amount-auth.ts`
|
||||
- **Thin community `Community 63`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 66`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
|
||||
- **Thin community `Community 64`** (5 nodes): `fetchBatchRevokeQcMaterialAmountAuth()`, `fetchGrantQcMaterialAmountAuth()`, `fetchQcMaterialAmountAuthUsers()`, `fetchRevokeQcMaterialAmountAuth()`, `qc-material-amount-auth.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 68`** (4 nodes): `fetchGenerateSms()`, `fetchSampleSmsNotificationList()`, `fetchToggleSmsNotified()`, `sample-sms-notification.ts`
|
||||
- **Thin community `Community 67`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 69`** (4 nodes): `fetchGenerateQcSms()`, `fetchQcSmsList()`, `fetchToggleQcSmsNotified()`, `qc-sms-notification.ts`
|
||||
- **Thin community `Community 69`** (4 nodes): `fetchGenerateStorageEntrySms()`, `fetchStorageEntrySmsList()`, `fetchToggleStorageEntrySmsNotified()`, `storage-entry-sms-notification.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 74`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 77`** (3 nodes): `buildOvertimeKitChildren()`, `oa-overtime-kit-rule.ts`, `uniqueId()`
|
||||
- **Thin community `Community 76`** (3 nodes): `buildOvertimeKitChildren()`, `oa-overtime-kit-rule.ts`, `uniqueId()`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 78`** (3 nodes): `buildLeaveKitChildren()`, `oa-leave-kit-rule.ts`, `uniqueId()`
|
||||
- **Thin community `Community 77`** (3 nodes): `buildLeaveKitChildren()`, `oa-leave-kit-rule.ts`, `uniqueId()`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 79`** (3 nodes): `buildConfirmationKitChildren()`, `oa-confirmation-kit-rule.ts`, `uniqueId()`
|
||||
- **Thin community `Community 78`** (3 nodes): `buildConfirmationKitChildren()`, `oa-confirmation-kit-rule.ts`, `uniqueId()`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 80`** (3 nodes): `buildResignationKitChildren()`, `oa-resignation-kit-rule.ts`, `uniqueId()`
|
||||
- **Thin community `Community 79`** (3 nodes): `buildResignationKitChildren()`, `oa-resignation-kit-rule.ts`, `uniqueId()`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 81`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
|
||||
- **Thin community `Community 80`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 86`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
|
||||
- **Thin community `Community 84`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 97`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
@@ -440,16 +440,16 @@ Nodes (2): emptyTabCounts(), useQcWorkOrderTabUnfinishedCounts()
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `push()` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 27`, `Community 28`, `Community 29`, `Community 34`, `Community 35`?**
|
||||
_High betweenness centrality (0.299) - this node is a cross-community bridge._
|
||||
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 3`, `Community 4`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 31`, `Community 33`, `Community 37`, `Community 43`, `Community 45`, `Community 46`, `Community 50`, `Community 63`, `Community 68`, `Community 69`?**
|
||||
_High betweenness centrality (0.256) - this node is a cross-community bridge._
|
||||
- **Why does `handleSubmit()` connect `Community 9` to `Community 2`, `Community 18`, `Community 6`?**
|
||||
_High betweenness centrality (0.029) - this node is a cross-community bridge._
|
||||
- **Why does `push()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 26`, `Community 27`, `Community 29`, `Community 32`, `Community 33`, `Community 34`, `Community 45`, `Community 48`?**
|
||||
_High betweenness centrality (0.295) - this node is a cross-community bridge._
|
||||
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 16`, `Community 18`, `Community 20`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 28`, `Community 29`, `Community 30`, `Community 40`, `Community 42`, `Community 44`, `Community 64`, `Community 69`?**
|
||||
_High betweenness centrality (0.220) - this node is a cross-community bridge._
|
||||
- **Why does `fetchNotDeletedRegions()` connect `Community 0` to `Community 1`?**
|
||||
_High betweenness centrality (0.033) - this node is a cross-community bridge._
|
||||
- **Are the 305 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
|
||||
_`get()` has 305 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 118 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
|
||||
_`push()` has 118 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 119 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
|
||||
_`push()` has 119 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 29 inferred relationships involving `emit()` (e.g. with `handleSubmit()` and `handleDialogClose()`) actually correct?**
|
||||
_`emit()` has 29 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 20 inferred relationships involving `set()` (e.g. with `buildNodeCodePathFromFlat()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
|
||||
|
||||
@@ -57,12 +57,12 @@
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
<div id="stats">3417 nodes · 4865 edges · 386 communities</div>
|
||||
<div id="stats">3436 nodes · 4893 edges · 389 communities</div>
|
||||
</div>
|
||||
<script>
|
||||
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 55, "community_name": "Community 55", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 55, "community_name": "Community 55", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 55, "community_name": "Community 55", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 55, "community_name": "Community 55", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 55, "community_name": "Community 55", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 55, "community_name": "Community 55", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 146, "community_name": "Community 146", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 147, "community_name": "Community 147", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 32, "community_name": "Community 32", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 32, "community_name": "Community 32", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 32, "community_name": "Community 32", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 32, "community_name": "Community 32", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 32, "community_name": "Community 32", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective",Line truncated
|
||||
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 552}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 117}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 110}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 110}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 107}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 102}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 100}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 95}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 89}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 88}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 84}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 82}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 79}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 74}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 69}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 69}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 62}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 59}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 54}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 52}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 51}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 50}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 47}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 42}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 37}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 36}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 35}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 34}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 33}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 25}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 24}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 24}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 22}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 22}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 21}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 20}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 19}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 19}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 17}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 17}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 17}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 16}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 14}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 14}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 13}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 13}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 9}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 9}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 9}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 9}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 9}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 9}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 8}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 8}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 8}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 7}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 7}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 6}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 6}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 5}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 5}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 5}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 5}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 5}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 5}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 4}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 4}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 4}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 4}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 4}, {"cid": 70, "color": "#4E79A7", "label": "CommunitLine truncated
|
||||
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 56, "community_name": "Community 56", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 56, "community_name": "Community 56", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 56, "community_name": "Community 56", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 56, "community_name": "Community 56", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 56, "community_name": "Community 56", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 56, "community_name": "Community 56", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 146, "community_name": "Community 146", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#FF9DA7", "border": "#FF9DA7", "highlight": {"background": "#ffffff", "border": "#FF9DA7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 147, "community_name": "Community 147", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 31, "community_name": "Community 31", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 31, "community_name": "Community 31", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 31, "community_name": "Community 31", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 31, "community_name": "Community 31", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 31, "community_name": "Community 31", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_setuprolesdirective",Line truncated
|
||||
const RAW_EDGES = [{"from": "app_vue", "to": "src_app_refreshbuttonautoicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "src_app_refreshpaneltitleicons", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "app_vue", "to": "main_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "utils_sys_console_ts", "label": "imports_from", "title": "imports_from [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_masksecret", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "main_ts", "to": "src_main_logapicryptobootinfo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "src_main_masksecret", "to": "src_main_logapicryptobootinfo", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_ts", "to": "directives_index_setupglobdirectives", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_index_setupglobdirectives", "to": "core_auth_setupauthdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "core_roles_setuprolesdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_highlight_setuphighlightdirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_index_setupglobdirectives", "to": "business_ripple_setuprippledirective", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "directives_core_roles_ts", "to": "core_roles_checkrolepermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_roles_ts", "to": "core_roles_setuprolesdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_roles_checkrolepermission", "to": "core_roles_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_checkauthpermission", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_removeelement", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_core_auth_ts", "to": "core_auth_setupauthdirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "core_auth_checkauthpermission", "to": "core_auth_removeelement", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_ripple_ts", "to": "business_ripple_setuprippledirective", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_highlightcode", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_insertlinenumbers", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "directives_business_highlight_ts", "to": "business_highlight_addcopybutton", "label": "contains", "title": "contains [EXTRACTED]", "dLine truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 546}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 221}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 124}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 118}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 110}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 108}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 107}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 101}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 96}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 90}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 90}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 88}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 74}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 69}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 69}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 55}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 51}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 47}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 43}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 42}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 41}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 37}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 36}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 36}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 35}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 34}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 33}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 31}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 29}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 25}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 24}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 22}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 21}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 20}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 20}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 19}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 17}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 17}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 16}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 15}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 14}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 13}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 13}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 13}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 9}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 9}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 9}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 9}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 9}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 9}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 9}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 9}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 8}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 8}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 8}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 8}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 7}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 7}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 6}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 6}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 5}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 5}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 5}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 5}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 5}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 5}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 4}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 4}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 4}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 4}, {"cid": 70, "color": "#4E79A7", "label": "CommunityLine truncated
|
||||
|
||||
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||
function esc(s) {
|
||||
|
||||
@@ -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": 1786506822.9277327,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1786506719.2191331,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1786601057.3350704,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1786601019.5323057,
|
||||
"/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,
|
||||
@@ -52,6 +52,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fullscreen-loading.ts": 1781272556.3849921,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/chinese-numeral-parse.ts": 1782960575.662593,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sample-production-reception-fields.ts": 1782806741.4880455,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-flow-preview-refresh.ts": 1786590578.7629158,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-upload-file-type.ts": 1786413851.5280254,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-owner-person-select-rule.ts": 1785401001.9203484,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-form-create-post.ts": 1785394335.3053484,
|
||||
@@ -76,6 +77,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-overtime-hours.test.ts": 1786373618.5292711,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/expected-cell-count.ts": 1782705039.2515786,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-payment-labels.ts": 1782893910.9937537,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-approval-print-flow.ts": 1786601489.1530275,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1785206796.3246918,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/production-work-order-card-meta.ts": 1785209080.8034942,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fetch-paged-list.ts": 1781272556.3903377,
|
||||
@@ -98,6 +100,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/outbound-qc-pilot-view-sections.ts": 1781566346.7618802,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-app-icons.ts": 1785992369.9430938,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-resignation-kit-rule.ts": 1785488987.364553,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-approval-print-flow.test.ts": 1786601522.7362416,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-person-select-rule.ts": 1785385076.8703918,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-work-order-groups.ts": 1782400129.6394563,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-inspection-batch.ts": 1782569790.8550596,
|
||||
@@ -118,7 +121,7 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-confirmation-date.ts": 1785403063.4649312,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sample-reception-view-sections.ts": 1783389099.9667985,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/production-labels.ts": 1783306730.912153,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-approval-print.ts": 1786413856.470563,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-approval-print.ts": 1786601519.3637354,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-owner-dept-select-rule.ts": 1785401003.0360808,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/inbound-sample-special-mark.ts": 1782889894.1376855,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-receive-account-value.test.ts": 1785390443.3642433,
|
||||
@@ -164,13 +167,15 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/production-task-editor-cache.ts": 1782602746.754815,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-pay-mode-switch.ts": 1785145536.399596,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-resignation-kit-runtime.ts": 1785405017.8343015,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-progress-person-lines.ts": 1786375203.836954,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-progress-person-lines.ts": 1786600724.5366488,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-form-create-person.ts": 1785385009.8220263,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-post-select-rule.ts": 1785394334.9268312,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-payment-installment.ts": 1783329873.2829163,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-source-attachment.ts": 1783921716.666595,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/download-attachment.ts": 1781272556.4160507,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-flow-preview-refresh.test.ts": 1786590563.254121,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-form-create-dept.ts": 1785148503.14446,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-progress-person-lines.test.ts": 1786600948.2912912,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/ui/colors.ts": 1781272556.3798137,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/ui/emojo.ts": 1781272556.3802173,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/ui/ep-icon-map.ts": 1781272556.38063,
|
||||
@@ -335,12 +340,12 @@
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaFcRow.vue": 1785145511.6907504,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaFormDesignerEmbed.vue": 1785145511.6925569,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaRelatedApproval.vue": 1786415204.9434047,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaProgressChain.vue": 1785835679.6218681,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaProgressChain.vue": 1786600903.7654822,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaInstanceDetailDialog.vue": 1786415214.1541882,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaResignationKit.vue": 1785405000.8028867,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaConfirmationKit.vue": 1785403411.176012,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaOwnerPersonSelect.vue": 1785401010.5990565,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaStartFormDialog.vue": 1786374809.2291305,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaStartFormDialog.vue": 1786590587.9781795,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaDeptSelect.vue": 1785382264.265851,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaTripItinerary.vue": 1785489392.2403016,
|
||||
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaMoneyField.vue": 1785382264.3025115,
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
<div class="oa-flow-card__row">
|
||||
<span class="oa-flow-card__name">{{ item.title }}</span>
|
||||
<span v-if="statusBadge(item)" class="oa-flow-badge" :class="statusBadge(item)">
|
||||
{{ statusText(item.status) }}
|
||||
{{ nodeStatusText(item) }}
|
||||
</span>
|
||||
<span v-if="signBadge(item)" class="oa-flow-badge" :class="signBadge(item)">
|
||||
{{ signBadgeText(item) }}
|
||||
</span>
|
||||
<span v-else-if="item.taskType === 'cc'" class="oa-flow-badge cc">抄送</span>
|
||||
<span v-else-if="item.taskType === 'end'" class="oa-flow-badge mute">结束</span>
|
||||
<template v-if="item.timestamp">
|
||||
<template v-if="showNodeTimestamp(item)">
|
||||
<span class="oa-flow-card__sep">·</span>
|
||||
<span class="oa-flow-card__meta" :title="item.timestamp">{{ item.timestamp }}</span>
|
||||
</template>
|
||||
@@ -66,6 +66,12 @@
|
||||
{{ line.text }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.kind === 'terminal' && item.status === 'cancelled' && terminalWithdrawText(item)"
|
||||
class="oa-person-lines"
|
||||
>
|
||||
<p class="oa-person-line is-danger">{{ terminalWithdrawText(item) }}</p>
|
||||
</div>
|
||||
<div v-else-if="showNodeComment(item)" class="oa-flow-card__comment">
|
||||
意见:{{ item.commentText }}
|
||||
</div>
|
||||
@@ -134,9 +140,17 @@
|
||||
return map[s] || s
|
||||
}
|
||||
|
||||
/** 任务节点 cancelled=系统清待办 →「无需处理」;终态 cancelled 仍为「已撤销」 */
|
||||
function nodeStatusText(item: OaProgressChainItem) {
|
||||
const s = item.status || ''
|
||||
if (item.kind !== 'terminal' && s === 'cancelled') return '无需处理'
|
||||
return statusText(s)
|
||||
}
|
||||
|
||||
/** 徽章色:与 Fresh Mint 语义一致 */
|
||||
function statusBadge(item: OaProgressChainItem) {
|
||||
const s = item.status || ''
|
||||
if (item.kind !== 'terminal' && s === 'cancelled') return 'mute'
|
||||
if (s === 'approved' || s === 'completed' || s === 'done' || s === 'submitted') return 'ok'
|
||||
if (s === 'pending' || s === 'running') return 'or'
|
||||
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
||||
@@ -183,15 +197,31 @@
|
||||
function showNodeComment(item: OaProgressChainItem): boolean {
|
||||
// 有按人行时不再展示节点级拼接意见,避免双份
|
||||
if (personLines(item).length) return false
|
||||
if (item.kind === 'terminal' && item.status === 'cancelled') return false
|
||||
return Boolean(item.commentText)
|
||||
}
|
||||
|
||||
function terminalWithdrawText(item: OaProgressChainItem): string {
|
||||
const name = (item.assignee || '').trim()
|
||||
const tip = (item.commentText || '').trim() || '发起人撤销'
|
||||
return name ? `${name} ${tip}` : tip
|
||||
}
|
||||
|
||||
/** 任务节点 cancelled 为系统清待办,不展示时间(时间归终态「已撤销」) */
|
||||
function showNodeTimestamp(item: OaProgressChainItem): boolean {
|
||||
const ts = (item.timestamp || '').trim()
|
||||
if (!ts) return false
|
||||
if (item.kind !== 'terminal' && item.status === 'cancelled') return false
|
||||
return true
|
||||
}
|
||||
|
||||
function personChipClass(status?: string) {
|
||||
const s = status || ''
|
||||
if (s === 'approved' || s === 'completed') return 'is-ok'
|
||||
if (s === 'pending' || s === 'running') return 'is-pending'
|
||||
if (s === 'rejected' || s === 'returned' || s === 'cancelled') return 'is-danger'
|
||||
if (s === 'done' || s === 'not_reached' || s === 'transferred') return 'is-mute'
|
||||
if (s === 'rejected' || s === 'returned') return 'is-danger'
|
||||
if (s === 'done' || s === 'not_reached' || s === 'transferred' || s === 'cancelled')
|
||||
return 'is-mute'
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -201,7 +231,7 @@
|
||||
if (s === 'rejected') return '已拒绝'
|
||||
if (s === 'returned') return '已打回'
|
||||
if (s === 'pending' || s === 'running') return '待处理'
|
||||
if (s === 'done') return '无需处理'
|
||||
if (s === 'done' || s === 'cancelled') return '无需处理'
|
||||
if (s === 'not_reached') return '未到达'
|
||||
if (s === 'transferred') return '已转交'
|
||||
return ''
|
||||
@@ -212,13 +242,17 @@
|
||||
const statusLabel =
|
||||
p.status === 'transferred' && target
|
||||
? `转交给${target}`
|
||||
: statusText(p.status || '')
|
||||
const parts = [p.nickName || '', statusLabel, p.timestamp || ''].filter(Boolean)
|
||||
: personStatusHint(p.status) || statusText(p.status || '')
|
||||
// cancelled/done 人员行不展示操作时间
|
||||
const ts =
|
||||
p.status === 'cancelled' || p.status === 'done' ? '' : String(p.timestamp || '').trim()
|
||||
const parts = [p.nickName || '', statusLabel, ts].filter(Boolean)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
function dotClass(item: OaProgressChainItem) {
|
||||
const s = item.status || ''
|
||||
if (item.kind !== 'terminal' && s === 'cancelled') return ''
|
||||
if (s === 'approved' || s === 'completed' || s === 'done' || s === 'submitted') return 'ok'
|
||||
if (s === 'pending' || s === 'running') return 'pending'
|
||||
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
||||
|
||||
@@ -129,6 +129,12 @@
|
||||
listTripKits,
|
||||
TRIP_MANUAL_PREFIX
|
||||
} from '@/utils/oa-trip-kit-runtime'
|
||||
import {
|
||||
buildFlowPreviewRequestKey,
|
||||
collectConditionFields,
|
||||
shouldShowFlowPreviewSpinner,
|
||||
shouldSkipFlowPreviewRequest
|
||||
} from '@/utils/oa-flow-preview-refresh'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'OaStartFormDialog' })
|
||||
@@ -162,6 +168,12 @@
|
||||
const flowPendingMessage = ref('必填信息填写后,流程将自动显示')
|
||||
const previewLoading = ref(false)
|
||||
let previewTimer: ReturnType<typeof setTimeout> | null = null
|
||||
/** 流程条件字段:仅这些 formData 变化才重拉预览 */
|
||||
let flowConditionFields: string[] = []
|
||||
let lastFlowPreviewRequestKey: string | null = null
|
||||
let inFlightFlowPreviewKey: string | null = null
|
||||
let hasCompletedFlowPreview = false
|
||||
let flowPreviewReqSeq = 0
|
||||
/** 避免时长自动回写再次触发手改判定 */
|
||||
let leaveKitSyncing = false
|
||||
let leaveKitPrevSnapshot: Record<string, unknown> = {}
|
||||
@@ -242,23 +254,56 @@
|
||||
}
|
||||
|
||||
async function loadFlowPreview(appId: string) {
|
||||
previewLoading.value = true
|
||||
if (!appId) return
|
||||
const requestKey = buildFlowPreviewRequestKey({
|
||||
formData: formData.value,
|
||||
initiatorSelectByNode: initiatorSelectByNode.value,
|
||||
conditionFields: flowConditionFields
|
||||
})
|
||||
if (
|
||||
shouldSkipFlowPreviewRequest(lastFlowPreviewRequestKey, requestKey, {
|
||||
inFlightKey: inFlightFlowPreviewKey
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
const seq = ++flowPreviewReqSeq
|
||||
inFlightFlowPreviewKey = requestKey
|
||||
const showSpinner = shouldShowFlowPreviewSpinner({
|
||||
hasCompletedPreview: hasCompletedFlowPreview
|
||||
})
|
||||
if (showSpinner) previewLoading.value = true
|
||||
try {
|
||||
const preview = await fetchOaFlowPreview(appId, {
|
||||
formData: { ...formData.value },
|
||||
initiatorSelectByNode: { ...initiatorSelectByNode.value }
|
||||
})
|
||||
if (seq !== flowPreviewReqSeq) return
|
||||
lastFlowPreviewRequestKey = requestKey
|
||||
flowResolved.value = preview?.resolved !== false
|
||||
flowPendingMessage.value = preview?.message || '必填信息填写后,流程将自动显示'
|
||||
flowSteps.value = flowResolved.value ? preview?.steps || [] : []
|
||||
} catch {
|
||||
if (seq !== flowPreviewReqSeq) return
|
||||
flowSteps.value = []
|
||||
flowResolved.value = true
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
if (seq === flowPreviewReqSeq) {
|
||||
inFlightFlowPreviewKey = null
|
||||
hasCompletedFlowPreview = true
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetFlowPreviewState(flowRoot: unknown) {
|
||||
flowConditionFields = collectConditionFields(flowRoot)
|
||||
lastFlowPreviewRequestKey = null
|
||||
inFlightFlowPreviewKey = null
|
||||
hasCompletedFlowPreview = false
|
||||
flowPreviewReqSeq += 1
|
||||
}
|
||||
|
||||
function scheduleFlowPreview() {
|
||||
if (!dlgVisible.value || !picked.value?.id) return
|
||||
if (previewTimer) clearTimeout(previewTimer)
|
||||
@@ -335,6 +380,7 @@
|
||||
void syncOvertimeKitDurations()
|
||||
const flow = detail?.flowTreeJson || {}
|
||||
const root = flow.root || flow
|
||||
resetFlowPreviewState(root)
|
||||
needInitiatorSelect.value = flowNeedsInitiatorSelect(root)
|
||||
if (needInitiatorSelect.value && !userOptions.value.length) {
|
||||
await loadUsers()
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 打印「审批流程」:撤销清待办 cancelled → 无需处理;终态归因发起人
|
||||
* Spec: docs/superpowers/specs/2026-08-13-oa-progress-withdraw-cancelled-display-design.md
|
||||
*/
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildFlowItems } from './oa-approval-print-flow'
|
||||
|
||||
describe('buildFlowItems · withdraw cancelled', () => {
|
||||
it('maps cancelled approval task to 无需处理 and terminal to 发起人撤销', () => {
|
||||
const items = buildFlowItems({
|
||||
progress: [
|
||||
{
|
||||
kind: 'submit',
|
||||
title: '发起人',
|
||||
status: 'submitted',
|
||||
assigneeUserName: '吴子龙',
|
||||
timestamp: '2026-08-13 11:17:38'
|
||||
},
|
||||
{
|
||||
kind: 'task',
|
||||
title: '直接主管',
|
||||
status: 'cancelled',
|
||||
signMode: 'and',
|
||||
timestamp: '',
|
||||
people: [
|
||||
{
|
||||
nickName: '张主管',
|
||||
status: 'cancelled',
|
||||
timestamp: '2026-08-13 12:47:46',
|
||||
commentText: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'terminal',
|
||||
title: '已撤销',
|
||||
status: 'cancelled',
|
||||
assigneeUserName: '吴子龙',
|
||||
commentText: '发起人撤销',
|
||||
timestamp: '2026-08-13 12:47:46'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.equal(items[0].left.includes('已提交'), true)
|
||||
assert.equal(items[1].left, '【直接主管·会签】 无需处理')
|
||||
assert.deepEqual(items[1].personLines, [{ left: '张主管 无需处理', right: '' }])
|
||||
assert.equal(items[2].left, '已撤销 吴子龙 发起人撤销')
|
||||
assert.equal(items[2].right, '2026-08-13 12:47:46')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* OA 打印「审批流程」行组装(无 Vue/canvas 副作用,便于单测)
|
||||
* Spec: docs/superpowers/specs/2026-08-13-oa-progress-withdraw-cancelled-display-design.md
|
||||
*/
|
||||
import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines'
|
||||
|
||||
export type OaPrintPersonLine = {
|
||||
left: string
|
||||
right: string
|
||||
}
|
||||
|
||||
export type OaPrintFlowItem = {
|
||||
left: string
|
||||
right: string
|
||||
/** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
|
||||
comment?: string
|
||||
/** 或签/会签按人行:左侧文案 + 右侧时间(右对齐) */
|
||||
personLines?: OaPrintPersonLine[]
|
||||
}
|
||||
|
||||
const STATUS_TEXT: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
approved: '已同意',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成',
|
||||
cancelled: '已撤销',
|
||||
done: '已结束',
|
||||
submitted: '已提交',
|
||||
not_reached: '未到达',
|
||||
returned: '已打回',
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
function statusText(s: string) {
|
||||
return STATUS_TEXT[s] || s || '-'
|
||||
}
|
||||
|
||||
/** 任务节点 cancelled=系统清待办 →「无需处理」;终态 cancelled 仍为「已撤销」 */
|
||||
function progressNodeStatusText(p: any): string {
|
||||
const kind = String(p?.kind || '')
|
||||
const s = String(p?.status || '').trim()
|
||||
if (kind !== 'terminal' && s === 'cancelled') return '无需处理'
|
||||
return statusText(s)
|
||||
}
|
||||
|
||||
function peopleDisplayNames(p: any): string[] {
|
||||
const people = Array.isArray(p?.people) ? p.people : []
|
||||
const fromPeople = people
|
||||
.map((x: any) => String(x?.nickName || x?.id || '').trim())
|
||||
.filter(Boolean)
|
||||
if (fromPeople.length) return fromPeople
|
||||
const name = String(p?.assigneeUserName || p?.assigneeUserId || '').trim()
|
||||
return name ? [name] : []
|
||||
}
|
||||
|
||||
function progressPersonLinesFromItem(p: any): OaPrintPersonLine[] {
|
||||
const people = Array.isArray(p?.people) ? p.people : []
|
||||
if (!people.length) return []
|
||||
return formatProgressPersonLines(people).map((x) => ({
|
||||
left: x.body || x.text,
|
||||
right: x.timestamp || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function isCcProgressItem(p: any): boolean {
|
||||
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
||||
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
||||
const title = String(p?.title || p?.nodeName || '').trim()
|
||||
return (
|
||||
taskType === 'cc' ||
|
||||
signMode === 'cc' ||
|
||||
title === '抄送人' ||
|
||||
/^抄送/.test(title)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 progress 组装审批流程行;抄送节点聚合为「抄送A, B, C」对齐钉钉导出。
|
||||
* 若 progress 漏抄送,则用 tasks 兜底补一行。
|
||||
*/
|
||||
export function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
||||
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
const items: OaPrintFlowItem[] = []
|
||||
|
||||
let ccBuf: { names: string[]; ts: string } | null = null
|
||||
const flushCc = () => {
|
||||
if (!ccBuf) return
|
||||
const uniq = Array.from(new Set(ccBuf.names.map((n) => n.trim()).filter(Boolean)))
|
||||
items.push({
|
||||
left: uniq.length ? `抄送: ${uniq.join(', ')}` : '抄送:',
|
||||
right: ccBuf.ts || ''
|
||||
})
|
||||
ccBuf = null
|
||||
}
|
||||
|
||||
for (const p of list) {
|
||||
const kind = String(p.kind || '')
|
||||
const title = String(p.title || '-').trim()
|
||||
const names = peopleDisplayNames(p)
|
||||
const name = names.join(', ')
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const comment = String(p.commentText || '').trim()
|
||||
const st = progressNodeStatusText(p)
|
||||
const signMode = String(p.signMode || p.sign_mode || '')
|
||||
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
||||
|
||||
if (isCcProgressItem(p)) {
|
||||
if (!ccBuf) ccBuf = { names: [], ts }
|
||||
if (names.length) ccBuf.names.push(...names)
|
||||
if (ts) ccBuf.ts = ts
|
||||
continue
|
||||
}
|
||||
flushCc()
|
||||
|
||||
if (kind === 'submit') {
|
||||
items.push({
|
||||
left: name ? `【${title}】 ${name} ${st}` : `【${title}】 ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (kind === 'terminal') {
|
||||
if (String(p.status || '') === 'returned' || title === '已打回') {
|
||||
continue
|
||||
}
|
||||
const withdrawName = String(p.assigneeUserName || p.assignee || '').trim()
|
||||
const withdrawTip = comment || (String(p.status || '') === 'cancelled' ? '发起人撤销' : '')
|
||||
const leftExtra =
|
||||
String(p.status || '') === 'cancelled' && (withdrawName || withdrawTip)
|
||||
? ` ${[withdrawName, withdrawTip].filter(Boolean).join(' ')}`
|
||||
: ''
|
||||
items.push({
|
||||
left: `${title || st}${leftExtra}`,
|
||||
right: ts
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (kind === 'pending_node') {
|
||||
const personLines = progressPersonLinesFromItem(p)
|
||||
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
||||
items.push({
|
||||
left: personLines.length
|
||||
? `${head} ${st}`
|
||||
: name
|
||||
? `${head} ${name} ${st}`
|
||||
: `${head} ${st}`,
|
||||
right: personLines.length ? '' : ts,
|
||||
personLines: personLines.length ? personLines : undefined
|
||||
})
|
||||
continue
|
||||
}
|
||||
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
||||
const personLines = progressPersonLinesFromItem(p)
|
||||
if (personLines.length) {
|
||||
items.push({
|
||||
left: `${head} ${st}`,
|
||||
right: '',
|
||||
personLines
|
||||
})
|
||||
continue
|
||||
}
|
||||
items.push({
|
||||
left: name ? `${head} ${name} ${st}` : `${head} ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
}
|
||||
flushCc()
|
||||
|
||||
const hasCcLine = items.some((it) => it.left.startsWith('抄送'))
|
||||
if (!hasCcLine) {
|
||||
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []
|
||||
const ccTasks = tasks.filter((t: any) => {
|
||||
const tt = String(t?.taskType || t?.task_type || '').trim()
|
||||
const sm = String(t?.signMode || t?.sign_mode || '').trim()
|
||||
return tt === 'cc' || sm === 'cc'
|
||||
})
|
||||
if (ccTasks.length) {
|
||||
const names = ccTasks
|
||||
.map((t: any) =>
|
||||
String(t?.assigneeUserName || t?.assignee_user_name || t?.assigneeUserId || '').trim()
|
||||
)
|
||||
.filter(Boolean)
|
||||
const uniq = Array.from(new Set(names))
|
||||
let ts = ''
|
||||
for (const t of ccTasks) {
|
||||
const tTs = String(t?.updateTime || t?.createTime || '').trim()
|
||||
if (tTs && (!ts || tTs > ts)) ts = tTs
|
||||
}
|
||||
if (uniq.length) {
|
||||
items.push({ left: `抄送: ${uniq.join(', ')}`, right: ts })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
normalizeRelatedIdsFromFormValue,
|
||||
type RelatedPrintBlock
|
||||
} from '@/utils/oa-approval-print-related'
|
||||
import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines'
|
||||
import { formatDeptDisplay } from '@/utils/oa-dept-value'
|
||||
import { formatPersonDisplay } from '@/utils/oa-person-value'
|
||||
import { formatPostDisplay } from '@/utils/oa-post-value'
|
||||
@@ -23,6 +22,11 @@ import {
|
||||
} from '@/utils/oa-form-value-display'
|
||||
import { toRmbUpper } from '@/utils/rmb-upper'
|
||||
import { formatOaUploadPathsForPrint } from '@/utils/oa-upload-file-type'
|
||||
import {
|
||||
buildFlowItems,
|
||||
type OaPrintFlowItem,
|
||||
type OaPrintPersonLine
|
||||
} from '@/utils/oa-approval-print-flow'
|
||||
|
||||
export const OA_PRINT_COMPANY_NAME = '安徽中科兴皖医学研究有限公司'
|
||||
|
||||
@@ -40,19 +44,8 @@ export type OaPrintFieldRow = {
|
||||
blocks?: RelatedPrintBlock[]
|
||||
}
|
||||
|
||||
export type OaPrintPersonLine = {
|
||||
left: string
|
||||
right: string
|
||||
}
|
||||
|
||||
export type OaPrintFlowItem = {
|
||||
left: string
|
||||
right: string
|
||||
/** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
|
||||
comment?: string
|
||||
/** 或签/会签按人行:左侧文案 + 右侧时间(右对齐) */
|
||||
personLines?: OaPrintPersonLine[]
|
||||
}
|
||||
export type { OaPrintPersonLine, OaPrintFlowItem }
|
||||
export { buildFlowItems }
|
||||
|
||||
export type OaPrintModel = {
|
||||
title: string
|
||||
@@ -67,20 +60,6 @@ export type OaPrintModel = {
|
||||
watermarkName: string
|
||||
}
|
||||
|
||||
const STATUS_TEXT: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
approved: '已同意',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成',
|
||||
cancelled: '已撤销',
|
||||
done: '已结束',
|
||||
submitted: '已提交',
|
||||
not_reached: '未到达',
|
||||
returned: '已打回',
|
||||
transferred: '已转交'
|
||||
}
|
||||
|
||||
const UPLOAD_TYPES = new Set([
|
||||
'upload',
|
||||
'fcUpload',
|
||||
@@ -130,10 +109,6 @@ export function resolveStamp(status: string | undefined | null): {
|
||||
return { stampClass: 'run', stampText: '审批中' }
|
||||
}
|
||||
|
||||
function statusText(s: string) {
|
||||
return STATUS_TEXT[s] || s || '-'
|
||||
}
|
||||
|
||||
function formatDateOnly(createTime: string | undefined | null): string {
|
||||
const raw = String(createTime || '').trim()
|
||||
if (!raw) return '-'
|
||||
@@ -392,160 +367,6 @@ async function buildFormRows(detail: any): Promise<OaPrintFieldRow[]> {
|
||||
return out
|
||||
}
|
||||
|
||||
/** 钉钉风格:人名不加状态括号,状态放在整行末尾 */
|
||||
function peopleDisplayNames(p: any): string[] {
|
||||
const people = Array.isArray(p?.people) ? p.people : []
|
||||
const fromPeople = people
|
||||
.map((x: any) => String(x?.nickName || x?.id || '').trim())
|
||||
.filter(Boolean)
|
||||
if (fromPeople.length) return fromPeople
|
||||
const name = String(p?.assigneeUserName || p?.assigneeUserId || '').trim()
|
||||
return name ? [name] : []
|
||||
}
|
||||
|
||||
function progressPersonLinesFromItem(p: any): OaPrintPersonLine[] {
|
||||
const people = Array.isArray(p?.people) ? p.people : []
|
||||
if (!people.length) return []
|
||||
return formatProgressPersonLines(people).map((x) => ({
|
||||
left: x.body || x.text,
|
||||
right: x.timestamp || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function isCcProgressItem(p: any): boolean {
|
||||
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
||||
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
||||
const title = String(p?.title || p?.nodeName || '').trim()
|
||||
// 主判据:taskType / signMode;标题兜底仅匹配默认「抄送人」或「抄送…」节点名
|
||||
return (
|
||||
taskType === 'cc' ||
|
||||
signMode === 'cc' ||
|
||||
title === '抄送人' ||
|
||||
/^抄送/.test(title)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 progress 组装审批流程行;抄送节点聚合为「抄送A, B, C」对齐钉钉导出。
|
||||
* 若 progress 漏抄送,则用 tasks 兜底补一行。
|
||||
*/
|
||||
export function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
||||
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
const items: OaPrintFlowItem[] = []
|
||||
|
||||
// 聚合连续抄送任务,对齐钉钉「抄送A, B, C」
|
||||
let ccBuf: { names: string[]; ts: string } | null = null
|
||||
const flushCc = () => {
|
||||
if (!ccBuf) return
|
||||
const uniq = Array.from(new Set(ccBuf.names.map((n) => n.trim()).filter(Boolean)))
|
||||
items.push({
|
||||
left: uniq.length ? `抄送: ${uniq.join(', ')}` : '抄送:',
|
||||
right: ccBuf.ts || ''
|
||||
})
|
||||
ccBuf = null
|
||||
}
|
||||
|
||||
for (const p of list) {
|
||||
const kind = String(p.kind || '')
|
||||
const title = String(p.title || '-').trim()
|
||||
const names = peopleDisplayNames(p)
|
||||
const name = names.join(', ')
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const comment = String(p.commentText || '').trim()
|
||||
const st = statusText(String(p.status || ''))
|
||||
const signMode = String(p.signMode || p.sign_mode || '')
|
||||
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
||||
|
||||
if (isCcProgressItem(p)) {
|
||||
if (!ccBuf) ccBuf = { names: [], ts }
|
||||
if (names.length) ccBuf.names.push(...names)
|
||||
if (ts) ccBuf.ts = ts
|
||||
continue
|
||||
}
|
||||
flushCc()
|
||||
|
||||
if (kind === 'submit') {
|
||||
items.push({
|
||||
left: name ? `【${title}】 ${name} ${st}` : `【${title}】 ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (kind === 'terminal') {
|
||||
// 打回结果已落在审批人节点,不单独输出终态「已打回」行
|
||||
if (String(p.status || '') === 'returned' || title === '已打回') {
|
||||
continue
|
||||
}
|
||||
items.push({
|
||||
left: title || st,
|
||||
right: ts
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (kind === 'pending_node') {
|
||||
const personLines = progressPersonLinesFromItem(p)
|
||||
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
||||
items.push({
|
||||
left: personLines.length
|
||||
? `${head} ${st}`
|
||||
: name
|
||||
? `${head} ${name} ${st}`
|
||||
: `${head} ${st}`,
|
||||
right: personLines.length ? '' : ts,
|
||||
personLines: personLines.length ? personLines : undefined
|
||||
})
|
||||
continue
|
||||
}
|
||||
// task(或签/会签同节点多人已合并)
|
||||
const head = signLabel ? `【${title}·${signLabel}】` : `【${title}】`
|
||||
const personLines = progressPersonLinesFromItem(p)
|
||||
if (personLines.length) {
|
||||
items.push({
|
||||
left: `${head} ${st}`,
|
||||
right: '',
|
||||
personLines
|
||||
})
|
||||
continue
|
||||
}
|
||||
items.push({
|
||||
left: name ? `${head} ${name} ${st}` : `${head} ${st}`,
|
||||
right: ts,
|
||||
comment: comment || undefined
|
||||
})
|
||||
}
|
||||
flushCc()
|
||||
|
||||
// progress 未带抄送时,用 tasks 兜底(对齐钉钉:流程末展示抄送人)
|
||||
const hasCcLine = items.some((it) => it.left.startsWith('抄送'))
|
||||
if (!hasCcLine) {
|
||||
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : []
|
||||
const ccTasks = tasks.filter((t: any) => {
|
||||
const tt = String(t?.taskType || t?.task_type || '').trim()
|
||||
const sm = String(t?.signMode || t?.sign_mode || '').trim()
|
||||
return tt === 'cc' || sm === 'cc'
|
||||
})
|
||||
if (ccTasks.length) {
|
||||
const names = ccTasks
|
||||
.map((t: any) =>
|
||||
String(t?.assigneeUserName || t?.assignee_user_name || t?.assigneeUserId || '').trim()
|
||||
)
|
||||
.filter(Boolean)
|
||||
const uniq = Array.from(new Set(names))
|
||||
let ts = ''
|
||||
for (const t of ccTasks) {
|
||||
const tTs = String(t?.updateTime || t?.createTime || '').trim()
|
||||
if (tTs && (!ts || tTs > ts)) ts = tTs
|
||||
}
|
||||
if (uniq.length) {
|
||||
items.push({ left: `抄送: ${uniq.join(', ')}`, right: ts })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export async function buildOaPrintModel(
|
||||
detail: any,
|
||||
opts: { printerName: string; printTime?: string }
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
buildFlowPreviewRequestKey,
|
||||
collectConditionFields,
|
||||
shouldShowFlowPreviewSpinner,
|
||||
shouldSkipFlowPreviewRequest
|
||||
} from './oa-flow-preview-refresh'
|
||||
|
||||
describe('oa-flow-preview-refresh', () => {
|
||||
it('ignores unrelated form fields when no condition fields', () => {
|
||||
const keyA = buildFlowPreviewRequestKey({
|
||||
formData: { title: 'a', reason: 'x' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: []
|
||||
})
|
||||
const keyB = buildFlowPreviewRequestKey({
|
||||
formData: { title: 'b', reason: 'y' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: []
|
||||
})
|
||||
assert.equal(keyA, keyB)
|
||||
assert.equal(shouldSkipFlowPreviewRequest(keyA, keyB), true)
|
||||
})
|
||||
|
||||
it('only condition fields affect request key', () => {
|
||||
const fields = ['amount']
|
||||
const keyA = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 100, remark: 'old' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: fields
|
||||
})
|
||||
const keySame = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 100, remark: 'new' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: fields
|
||||
})
|
||||
const keyDiff = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 200, remark: 'new' },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: fields
|
||||
})
|
||||
assert.equal(keyA, keySame)
|
||||
assert.notEqual(keyA, keyDiff)
|
||||
})
|
||||
|
||||
it('initiator select changes request key', () => {
|
||||
const keyA = buildFlowPreviewRequestKey({
|
||||
formData: {},
|
||||
initiatorSelectByNode: { n1: ['u1'] },
|
||||
conditionFields: []
|
||||
})
|
||||
const keyB = buildFlowPreviewRequestKey({
|
||||
formData: {},
|
||||
initiatorSelectByNode: { n1: ['u2'] },
|
||||
conditionFields: []
|
||||
})
|
||||
assert.notEqual(keyA, keyB)
|
||||
})
|
||||
|
||||
it('collects condition fields from flow tree', () => {
|
||||
const tree = {
|
||||
type: 'start',
|
||||
child: {
|
||||
type: 'condition',
|
||||
conditionGroups: [
|
||||
{
|
||||
rules: [{ field: 'leaveType', op: 'eq', value: '事假' }],
|
||||
child: { type: 'approver', id: 'a1' }
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
child: { type: 'approver', id: 'a2' }
|
||||
}
|
||||
],
|
||||
child: { type: 'end' }
|
||||
}
|
||||
}
|
||||
assert.deepEqual(collectConditionFields(tree), ['leaveType'])
|
||||
})
|
||||
|
||||
it('collects formula fields and skips __initiator__', () => {
|
||||
assert.deepEqual(
|
||||
collectConditionFields({
|
||||
type: 'condition',
|
||||
conditionGroups: [{ when: '{days} > 3', child: { type: 'approver' } }]
|
||||
}),
|
||||
['days']
|
||||
)
|
||||
assert.deepEqual(
|
||||
collectConditionFields({
|
||||
type: 'condition',
|
||||
conditionGroups: [
|
||||
{
|
||||
rules: [{ field: '__initiator__', op: 'eq', value: 'x' }],
|
||||
child: { type: 'approver' }
|
||||
}
|
||||
]
|
||||
}),
|
||||
[]
|
||||
)
|
||||
})
|
||||
|
||||
it('shows spinner only before first completed preview', () => {
|
||||
assert.equal(shouldShowFlowPreviewSpinner({ hasCompletedPreview: false }), true)
|
||||
assert.equal(shouldShowFlowPreviewSpinner({ hasCompletedPreview: true }), false)
|
||||
})
|
||||
|
||||
it('with in-flight key, allows rollback to previous successful key', () => {
|
||||
const key100 = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 100 },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: ['amount']
|
||||
})
|
||||
const key200 = buildFlowPreviewRequestKey({
|
||||
formData: { amount: 200 },
|
||||
initiatorSelectByNode: {},
|
||||
conditionFields: ['amount']
|
||||
})
|
||||
assert.equal(shouldSkipFlowPreviewRequest(key100, key200, { inFlightKey: key200 }), true)
|
||||
assert.equal(shouldSkipFlowPreviewRequest(key100, key100, { inFlightKey: key200 }), false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
/** 与后端 oa_engine.INITIATOR_CONDITION_FIELD 对齐:不依赖 formData */
|
||||
const INITIATOR_CONDITION_FIELD = '__initiator__'
|
||||
const FORMULA_FIELD_RE = /\{([^{}]+)\}/g
|
||||
|
||||
function nodeType(node: any): string {
|
||||
return String((node && (node.type || node.nodeType)) || '').trim()
|
||||
}
|
||||
|
||||
function addField(name: unknown, fields: string[], seen: Set<string>) {
|
||||
const key = String(name || '').trim()
|
||||
if (!key || seen.has(key)) return
|
||||
if (key === INITIATOR_CONDITION_FIELD) return
|
||||
seen.add(key)
|
||||
fields.push(key)
|
||||
}
|
||||
|
||||
function addFormulaFields(text: unknown, fields: string[], seen: Set<string>) {
|
||||
if (!text) return
|
||||
FORMULA_FIELD_RE.lastIndex = 0
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = FORMULA_FIELD_RE.exec(String(text)))) {
|
||||
addField(m[1], fields, seen)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集流程树中非默认条件分支引用的表单字段(去重保序)。
|
||||
* 语义对齐 backend `oa_engine._collect_condition_fields`。
|
||||
*/
|
||||
export function collectConditionFields(node: unknown): string[] {
|
||||
const fields: string[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
function walk(n: any) {
|
||||
if (!n || typeof n !== 'object') return
|
||||
if (nodeType(n) === 'condition') {
|
||||
for (const g of n.conditionGroups || []) {
|
||||
if (!g || typeof g !== 'object') continue
|
||||
const rules = g.rules
|
||||
const when = g.when
|
||||
if (g.isDefault || (!rules && !when)) {
|
||||
if (g.child && typeof g.child === 'object') walk(g.child)
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(rules) && rules.length) {
|
||||
for (const r of rules) {
|
||||
if (r && typeof r === 'object' && r.field != null) {
|
||||
addField(r.field, fields, seen)
|
||||
}
|
||||
}
|
||||
} else if (typeof rules === 'string' && rules.trim()) {
|
||||
addFormulaFields(rules, fields, seen)
|
||||
} else if (when) {
|
||||
addFormulaFields(when, fields, seen)
|
||||
}
|
||||
if (g.child && typeof g.child === 'object') walk(g.child)
|
||||
}
|
||||
if (n.child && typeof n.child === 'object') walk(n.child)
|
||||
return
|
||||
}
|
||||
if (n.child && typeof n.child === 'object') walk(n.child)
|
||||
for (const g of n.conditionGroups || []) {
|
||||
if (g && typeof g === 'object' && g.child && typeof g.child === 'object') {
|
||||
walk(g.child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(node)
|
||||
return fields
|
||||
}
|
||||
|
||||
function sortKeysDeep(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(sortKeysDeep)
|
||||
if (value && typeof value === 'object') {
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const k of Object.keys(value as Record<string, unknown>).sort()) {
|
||||
out[k] = sortKeysDeep((value as Record<string, unknown>)[k])
|
||||
}
|
||||
return out
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function pickRelevantFormData(
|
||||
formData: Record<string, unknown> | null | undefined,
|
||||
conditionFields: string[] | null | undefined
|
||||
): Record<string, unknown> {
|
||||
const data = formData && typeof formData === 'object' ? formData : {}
|
||||
const fields = Array.isArray(conditionFields) ? conditionFields : []
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const f of fields) {
|
||||
out[f] = data[f]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function buildFlowPreviewRequestKey(opts?: {
|
||||
formData?: Record<string, unknown> | null
|
||||
initiatorSelectByNode?: Record<string, string[]> | null
|
||||
conditionFields?: string[] | null
|
||||
}): string {
|
||||
const payload = {
|
||||
formData: pickRelevantFormData(opts?.formData, opts?.conditionFields),
|
||||
initiatorSelectByNode:
|
||||
opts?.initiatorSelectByNode && typeof opts.initiatorSelectByNode === 'object'
|
||||
? opts.initiatorSelectByNode
|
||||
: {}
|
||||
}
|
||||
return JSON.stringify(sortKeysDeep(payload))
|
||||
}
|
||||
|
||||
export function shouldSkipFlowPreviewRequest(
|
||||
prevKey: string | null | undefined,
|
||||
nextKey: string,
|
||||
opts: { inFlightKey?: string | null } = {}
|
||||
): boolean {
|
||||
const inFlightKey = opts.inFlightKey
|
||||
if (inFlightKey != null) {
|
||||
return inFlightKey === nextKey
|
||||
}
|
||||
return prevKey != null && prevKey === nextKey
|
||||
}
|
||||
|
||||
/** 已有预览结果时静默刷新,避免整区 loading 闪烁 */
|
||||
export function shouldShowFlowPreviewSpinner(opts?: {
|
||||
hasCompletedPreview?: boolean
|
||||
}): boolean {
|
||||
return !opts?.hasCompletedPreview
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* OA 进度人员行:cancelled(撤销/打回清待办)对齐或签 done →「无需处理」
|
||||
* Spec: docs/superpowers/specs/2026-08-13-oa-progress-withdraw-cancelled-display-design.md
|
||||
*/
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { formatProgressPersonLines } from './oa-progress-person-lines'
|
||||
|
||||
describe('formatProgressPersonLines · cancelled', () => {
|
||||
it('maps cancelled to 无需处理 (mute, no timestamp)', () => {
|
||||
const lines = formatProgressPersonLines([
|
||||
{
|
||||
nickName: '张主管',
|
||||
status: 'cancelled',
|
||||
commentText: '',
|
||||
timestamp: '2026-08-13 12:47:46'
|
||||
}
|
||||
])
|
||||
assert.deepEqual(lines, [
|
||||
{
|
||||
body: '张主管 无需处理',
|
||||
timestamp: '',
|
||||
text: '张主管 无需处理',
|
||||
tone: 'mute'
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -52,8 +52,8 @@ function toneForStatus(status: string): PersonLineTone {
|
||||
const s = (status || '').trim()
|
||||
if (s === 'approved' || s === 'completed') return 'ok'
|
||||
if (s === 'pending' || s === 'running' || s === 'not_reached') return 'pending'
|
||||
if (s === 'rejected' || s === 'returned' || s === 'cancelled') return 'danger'
|
||||
if (s === 'done' || s === 'transferred') return 'mute'
|
||||
if (s === 'rejected' || s === 'returned') return 'danger'
|
||||
if (s === 'done' || s === 'transferred' || s === 'cancelled') return 'mute'
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function formatProgressPersonLines(
|
||||
const ts = String(p.timestamp || '').trim()
|
||||
const tone = toneForStatus(status)
|
||||
|
||||
if (status === 'done') {
|
||||
if (status === 'done' || status === 'cancelled') {
|
||||
pushLine(out, `${name} 无需处理`, '', 'mute')
|
||||
continue
|
||||
}
|
||||
|
||||