feat: 完善协议缴费分期、样本编码搜索与鸿蒙附件选择

支持移动端样本编码远程关键字检索、分期起始日刷新与鸿蒙微信附件选取,并同步收款账户选择、角色页与相关设计文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-22 09:23:17 +08:00
1 parent 6e2656f999
commit 063727a7d8
45 files changed
+42669 -34713

No files matched your search

+2
View File
@@ -384,6 +384,7 @@ def _register_protocol_routes(scope_kind: ScopeKind) -> None:
region_id: Optional[str] = Query(None, alias='regionId'),
business_id: Optional[str] = Query(None, alias='businessId'),
code_filter: Optional[str] = Query(None, alias='codeFilter'),
keyword: Optional[str] = Query(None, max_length=64),
user: dict = Depends(permission_guard),
db: Session = Depends(get_db),
):
@@ -397,6 +398,7 @@ def _register_protocol_routes(scope_kind: ScopeKind) -> None:
region_id=region_id,
business_id=business_id,
code_filter=code_filter,
sample_code_keyword=keyword,
)
)
except (PermissionError, ValueError) as exc:
+6
View File
@@ -309,6 +309,7 @@ def get_form_options(
business_id: Optional[str] = None,
user_keyword: Optional[str] = None,
code_filter: Optional[str] = None,
sample_code_keyword: Optional[str] = None,
) -> Dict[str, Any]:
"""返回移动表单最小选项集;不会返回联系方式或证件等 PII。"""
_verify_identity(ctx, user_info)
@@ -488,6 +489,11 @@ def get_form_options(
sample_code_query = sample_code_query.filter(
BizSampleCode.sample_code.like(f'%{cf}%')
)
code_keyword = (sample_code_keyword or '').strip()
if code_keyword:
sample_code_query = sample_code_query.filter(
BizSampleCode.sample_code.like(f'%{code_keyword}%')
)
sample_codes = (
sample_code_query.order_by(
BizSampleCode.create_time.desc(), BizSampleCode.sample_code.desc()
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+93 -88
View File
@@ -1,11 +1,11 @@
# Graph Report - app (2026-10-20)
# Graph Report - app (2026-10-21)
## Corpus Check
- Large corpus: 310 files · ~379,799 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 312 files · ~381,667 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 6373 nodes · 35339 edges · 42 communities detected
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 24180 edges (avg confidence: 0.6)
- 6406 nodes · 35474 edges · 43 communities detected
- Extraction: 32% EXTRACTED · 68% INFERRED · 0% AMBIGUOUS · INFERRED: 24253 edges (avg confidence: 0.6)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -45,17 +45,18 @@
- [[_COMMUNITY_Community 33|Community 33]]
- [[_COMMUNITY_Community 34|Community 34]]
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_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]]
## God Nodes (most connected - your core abstractions)
1. `success_response()` - 830 edges
2. `SysUser` - 733 edges
3. `error_response()` - 699 edges
1. `success_response()` - 834 edges
2. `SysUser` - 736 edges
3. `error_response()` - 703 edges
4. `BizQcTaskResult` - 388 edges
5. `BizProductionMaterial` - 372 edges
6. `BizProductionMaterialUsage` - 365 edges
@@ -73,78 +74,78 @@
core/menu_guard.py → models/models.py
- `读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。` --uses--> `SysUser` [INFERRED]
core/menu_guard.py → models/models.py
- `判断用户是否拥有指定权限码(不拦截请求,用于字段/按钮级能力控制)。` --uses--> `SysUser` [INFERRED]
- `接口菜单权限守卫(已统一放行 menuCode 校验,仅保留运营端登录)。` --uses--> `SysUser` [INFERRED]
core/menu_guard.py → models/models.py
## Communities
### Community 0 - "Community 0"
Cohesion: 0.01
Nodes (1187): _assert_user_tenant_access(), attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu() (+1179 more)
Nodes (1171): _assert_user_tenant_access(), attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu() (+1163 more)
### Community 1 - "Community 1"
Cohesion: 0.01
Nodes (882): validate_ymd(), BaseSettings, Config, secret_key_must_be_strong(), Settings, generate_uuid(), delete(), get_multi_with_filters() (+874 more)
Nodes (830): validate_ymd(), BaseSettings, Config, secret_key_must_be_strong(), Settings, generate_uuid(), ApiRequestLogCRUD, delete() (+822 more)
### Community 2 - "Community 2"
Cohesion: 0.01
Nodes (607): BatchPaymentIdsBody, BatchSubmitReceiptBody, BatchSubmitRefundBody, ConfirmReceiptBody, ConfirmRefundBody, PaymentIdBody, SubmitReceiptBody, SubmitRefundBody (+599 more)
Nodes (730): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+722 more)
### Community 3 - "Community 3"
Cohesion: 0.05
Nodes (528): 环境检测 — 检测项维护 + 登记管理 API, InsuranceCompanyCreateBody, InsuranceCompanyInvoiceCreateBody, InsuranceCompanyInvoiceUpdateBody, InsuranceCompanyUpdateBody, InsuranceProjectPriceCreateBody, InsuranceProjectPriceUpdateBody, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格) (+520 more)
Cohesion: 0.04
Nodes (754): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+746 more)
### Community 4 - "Community 4"
Cohesion: 0.01
Nodes (420): _normalize_date_bound(), _persist_protocol_sub_resources(), assert_protocol_status_transition(), build_void_type_label_map(), format_protocol_status_label(), 状态流转被拒绝时的提示,状态码展示为中文名称。, resolve_void_type_label(), status_transition_denied_message() (+412 more)
Nodes (402): _normalize_date_bound(), assert_protocol_sensitive_fields(), _attach_handler_name(), BatchRefundFinanceSubmitBody, create_collection_protocol(), delete_collection_protocol(), get_collection_protocol_detail(), _get_current_user_id() (+394 more)
### Community 5 - "Community 5"
Cohesion: 0.05
Nodes (457): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+449 more)
Cohesion: 0.01
Nodes (390): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), _load_lineage_qc_for_nodes(), batch_review() (+382 more)
### Community 6 - "Community 6"
Cohesion: 0.02
Nodes (258): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _copied_app_name(), copy_app(), create_app(), _create_version(), delete_app() (+250 more)
Nodes (285): 截止日期:仅日期时扩到当天 23:59:59。, 发起前预览审批链(条件齐后解析最终路径与审批人)。, 兼容旧 GET:无 formData,含条件分支时多为待确定占位。, _app_to_dict(), _copied_app_name(), copy_app(), create_app(), _create_version() (+277 more)
### Community 7 - "Community 7"
Cohesion: 0.02
Nodes (194): 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。, 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, web_qr_confirm(), web_qr_scanned(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线) (+186 more)
Nodes (135): 小程序鉴权:微信登录 / 绑定 openid。, 当前用户是否已绑定微信服务号 openid。, 服务号网页授权 code 绑定当前用户(需 Bearer)。, 微信登录:openid 已绑定时可静默换票;否则须授权手机号且与账号 phone_no 一致。, 密码登录后绑定当前微信 openid(需 Bearer)。, web_qr_confirm(), web_qr_scanned(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线) (+127 more)
### Community 8 - "Community 8"
Cohesion: 0.02
Nodes (215): eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci(), batch_review(), _dec() (+207 more)
Nodes (181): _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(), create_cell_product() (+173 more)
### Community 9 - "Community 9"
Cohesion: 0.02
Nodes (184): _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(), create_cell_products_batch() (+176 more)
Cohesion: 0.03
Nodes (138): list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), _resolve_user_role_scopes(), list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), create_leave() (+130 more)
### Community 10 - "Community 10"
Cohesion: 0.03
Nodes (170): _save_sample_payments(), deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 耗材变动记录 CRUD(biz_consumable_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减耗材库存并生成变动记录。调用方控制事务,本函数不 commit。 (+162 more)
Nodes (109): _register_attachment_routes(), _register_freight_routes(), _register_payment_routes(), _register_presign_routes(), _register_protocol_routes(), _collect_descendants(), _current_user(), _find_mobile_root() (+101 more)
### Community 11 - "Community 11"
Cohesion: 0.02
Nodes (156): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+148 more)
Cohesion: 0.04
Nodes (97): get_customer_service_dict(), _get_tenant_no(), _get_current_user_id(), get_sample_insurance(), get_sample_storage_cert(), _get_tenant_no(), put_sample_insurance(), put_sample_storage_cert() (+89 more)
### Community 12 - "Community 12"
Cohesion: 0.03
Nodes (131): list_ethnics_for_select(), _leave_local_callback(), apply_data_scope(), _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), get_dept(), list_depts() (+123 more)
Cohesion: 0.06
Nodes (103): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, list_storage_slot_stats(), OccupancyCreateBody (+95 more)
### Community 13 - "Community 13"
Cohesion: 0.04
Nodes (96): get_customer_service_dict(), _get_tenant_no(), _get_current_user_id(), get_sample_insurance(), get_sample_storage_cert(), _get_tenant_no(), put_sample_insurance(), put_sample_storage_cert() (+88 more)
Cohesion: 0.05
Nodes (99): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 耗材变动记录 CRUD(biz_consumable_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减耗材库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+91 more)
### Community 14 - "Community 14"
Cohesion: 0.06
Nodes (72): create_sample_freight(), delete_sample_freight(), _get_current_user_id(), get_sample_freight(), _get_tenant_no(), link_sample_freight(), list_sample_freight_options(), list_sample_freights() (+64 more)
Cohesion: 0.04
Nodes (78): get_designer_data(), 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json() (+70 more)
### Community 15 - "Community 15"
Cohesion: 0.08
Nodes (56): aggregate_slot_stats_by_facility(), _apply_slot_select_base_filters(), _apply_slot_select_query_filters(), batch_create_facilities(), batch_create_slots(), _build_node_path_cache(), _build_slot_keyword_sql_filters(), collect_descendant_ids() (+48 more)
Cohesion: 0.05
Nodes (89): _save_sample_payments(), add_transfer(), apply_fq_to_fgd_merge(), _assert_locked_schedule_structure(), assert_sample_payment_structure_editable(), _build_list_summary_row(), _build_periods_array(), _compute_progress_for_payment() (+81 more)
### Community 16 - "Community 16"
Cohesion: 0.05
Nodes (22): extract_avatar_storage_key(), 用户头像 URL 解析(sys_user.avatar 可能存 MinIO 相对路径或完整 URL), 从 upload 返回值或历史完整 URL 中提取 avatar/ 相对路径。, 服务端内部拼完整 URL(邮件等);Web API 应返回库中原始路径。, resolve_avatar_display_url(), BaseStorage, convert_heic_to_jpeg(), get_storage() (+14 more)
Cohesion: 0.08
Nodes (61): add_transfer(), assert_presign_convertible(), confirm_receipt(), confirmed_deposit_amount(), confirmed_deposit_amount_map(), convert_to_protocol(), create_presign_order(), _dec() (+53 more)
### Community 17 - "Community 17"
Cohesion: 0.09
@@ -152,44 +153,44 @@ Nodes (50): complete_application(), create_basic_experiment_sale_schedules(), cr
### Community 18 - "Community 18"
Cohesion: 0.12
Nodes (30): join_equipment_ids(), parse_equipment_ids(), 工艺/步骤设备多选:逗号分隔 ID 与名称快照解析, 校验设备列表,返回 (equipment_ids 逗号串, equipment_name 逗号串快照)。, resolve_equipment_list_snapshot(), add_step(), copy_step(), _create_step_consumable() (+22 more)
### Community 19 - "Community 19"
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 20 - "Community 20"
### Community 19 - "Community 19"
Cohesion: 0.14
Nodes (26): _assert_routes_exist(), collect_missing_route_ids(), create_set(), create_set_from_work_order(), create_tasks_from_set(), delete_set(), get_set(), _item_count_map() (+18 more)
Nodes (25): _assert_routes_exist(), create_set(), create_set_from_work_order(), create_tasks_from_set(), delete_set(), get_set(), _item_count_map(), _item_to_dict() (+17 more)
### Community 20 - "Community 20"
Cohesion: 0.17
Nodes (23): apply_overtime_kit_hours(), _build_rest_day_segments(), _build_workday_windows(), calc_overtime_hours(), _day_at_minutes(), _day_start_ms(), _extract_rules(), _format_ymd() (+15 more)
### Community 21 - "Community 21"
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)
Cohesion: 0.16
Nodes (22): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), get_material_entity(), get_my_usages(), _is_enabled_status() (+14 more)
### Community 22 - "Community 22"
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)
Cohesion: 0.16
Nodes (20): batch_create_checkups(), create_checkup(), delete_actual_item(), delete_reserve_item(), ensure_checkup_for_patient(), get_checkup_by_application(), get_checkup_by_id(), list_actual_items() (+12 more)
### Community 23 - "Community 23"
Cohesion: 0.21
Nodes (19): _consumable_row_to_dict(), create_process(), _create_sub_consumable(), _create_sub_data_collection(), _create_sub_output(), _create_sub_reagent(), _data_collection_row_to_dict(), get_process() (+11 more)
### Community 24 - "Community 24"
Cohesion: 0.27
Nodes (12): _field_label(), _is_empty(), _is_required(), _parse_money(), OA 表单设计器:金额控件 form_data 校验, 校验 form_data 中金额控件的值格式,返回中文错误列表(空表示通过)。, 解析金额值。成功返回 (number, None);失败返回 (None, error_key)。, _rule_props() (+4 more)
Cohesion: 0.19
Nodes (17): apply_trip_kit_days(), calc_trip_leg_days(), _day_index(), _end_offset(), _extract_rules(), is_half_day_multiple(), _kit_trip_fields(), 出差套件:半日制行程时长(与前端 oa-trip-days.ts 对齐) (+9 more)
### Community 25 - "Community 25"
Cohesion: 0.32
Nodes (12): _attach_owner_names(), create_account(), _filtered_query(), get_account(), list_accounts(), list_options(), _owner_name_map(), OA 收款账户 CRUD(oa_receive_account) (+4 more)
### Community 26 - "Community 26"
Cohesion: 0.29
Nodes (11): get_detail(), _load_sample_context(), _row_to_dict(), upsert(), dumps_payload(), empty_detail(), loads_payload(), normalize_payload() (+3 more)
### Community 26 - "Community 26"
### Community 27 - "Community 27"
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 27 - "Community 27"
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 28 - "Community 28"
Cohesion: 0.29
Nodes (2): BaseModel, SoftDeleteModel
@@ -203,91 +204,95 @@ Cohesion: 0.4
Nodes (5): batch_matches_stock_status(), normalize_batch_stock_status(), 质检物料入库批次:按剩余数量过滤的语义辅助。, 纯函数判定(供单测);list_batches 用 SQL 等价条件。, 返回 'available' | 'exhausted';全部 / 空 / 非法 → None(不加剩余数量条件)。
### Community 31 - "Community 31"
Cohesion: 0.6
Nodes (5): _dec(), _fmt_dt(), list_stock_logs(), _row_to_dict(), _user_nick_name_map()
### Community 32 - "Community 32"
Cohesion: 0.5
Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
### Community 32 - "Community 32"
### Community 33 - "Community 33"
Cohesion: 0.67
Nodes (1): 转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。
### Community 33 - "Community 33"
### Community 34 - "Community 34"
Cohesion: 1.0
Nodes (1): 质检物料金额查看授权:专用角色与菜单权限常量。
### Community 34 - "Community 34"
### Community 35 - "Community 35"
Cohesion: 1.0
Nodes (1): 请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
### Community 35 - "Community 35"
### Community 36 - "Community 36"
Cohesion: 1.0
Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
### Community 38 - "Community 38"
### Community 39 - "Community 39"
Cohesion: 1.0
Nodes (1): 发送订单状态通知 Args: openid: 用户openid ord
### Community 39 - "Community 39"
### Community 40 - "Community 40"
Cohesion: 1.0
Nodes (1): 发送支付成功通知 Args: openid: 用户openid ord
### Community 40 - "Community 40"
### Community 41 - "Community 41"
Cohesion: 1.0
Nodes (1): 发送配送通知 Args: openid: 配送员openid task
### Community 41 - "Community 41"
### Community 42 - "Community 42"
Cohesion: 1.0
Nodes (1): 发送配送时效预警 Args: openid: 配送员openid ta
### Community 42 - "Community 42"
### Community 43 - "Community 43"
Cohesion: 1.0
Nodes (1): 发送退款结果通知 Args: openid: 用户openid ord
### Community 43 - "Community 43"
### Community 44 - "Community 44"
Cohesion: 1.0
Nodes (1): 发送催单通知给配送员 Args: deliver_openid: 配送员openid
## Knowledge Gaps
- **211 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `严格判断当前租户用户是否拥有指定移动市场权限码。`, `为移动市场 API 提供租户化、精确匹配的菜单权限依赖。` (+206 more)
- **208 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `严格判断当前租户用户是否拥有指定移动市场权限码。`, `为移动市场 API 提供租户化、精确匹配的菜单权限依赖。` (+203 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__()`
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 32`** (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): `add_months_ymd()`, `oa_confirmation_date.py`, `转正套件:入职日 + 试用期月数 → 建议转正日(YYYY-MM-DD)。`
- **Thin community `Community 33`** (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 33`** (2 nodes): `qc_material_amount_auth_constants.py`, `质检物料金额查看授权:专用角色与菜单权限常量。`
- **Thin community `Community 34`** (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 34`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
- **Thin community `Community 35`** (2 nodes): `context.py`, `请求级别的用户上下文变量 通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 35`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
- **Thin community `Community 36`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 38`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
- **Thin community `Community 39`** (1 nodes): `发送订单状态通知 Args: openid: 用户openid ord`
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 40`** (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 task`
- **Thin community `Community 41`** (1 nodes): `发送配送通知 Args: openid: 配送员openid task`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 41`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
- **Thin community `Community 42`** (1 nodes): `发送配送时效预警 Args: openid: 配送员openid ta`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 42`** (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 43`** (1 nodes): `发送催单通知给配送员 Args: deliver_openid: 配送员openid`
- **Thin community `Community 44`** (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 5` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 26`?**
_High betweenness centrality (0.134) - 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 14`, `Community 15`, `Community 18`, `Community 20`, `Community 23`, `Community 25`, `Community 26`, `Community 27`?**
_High betweenness centrality (0.041) - this node is a cross-community bridge._
- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 7`, `Community 11`, `Community 12`, `Community 13`, `Community 14`?**
_High betweenness centrality (0.036) - this node is a cross-community bridge._
- **Are the 1169 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
_`str` has 1169 INFERRED edges - model-reasoned connections that need verification._
- **Are the 829 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
_`success_response()` has 829 INFERRED edges - model-reasoned connections that need verification._
- **Are the 731 inferred relationships involving `SysUser` (e.g. with `菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)` and `运营端角色守卫:admin / super_admin 可访问。`) actually correct?**
_`SysUser` has 731 INFERRED edges - model-reasoned connections that need verification._
- **Are the 698 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
_`error_response()` has 698 INFERRED edges - model-reasoned connections that need verification._
- **Why does `SysUser` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 21`, `Community 25`, `Community 27`?**
_High betweenness centrality (0.109) - this node is a cross-community bridge._
- **Why does `success_response()` connect `Community 0` to `Community 2`, `Community 4`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`?**
_High betweenness centrality (0.038) - this node is a cross-community bridge._
- **Why does `generate_uuid()` connect `Community 1` to `Community 0`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 19`, `Community 21`, `Community 23`, `Community 25`, `Community 26`, `Community 27`?**
_High betweenness centrality (0.025) - this node is a cross-community bridge._
- **Are the 1181 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `get_current_user_by_token()`) actually correct?**
_`str` has 1181 INFERRED edges - model-reasoned connections that need verification._
- **Are the 833 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
_`success_response()` has 833 INFERRED edges - model-reasoned connections that need verification._
- **Are the 734 inferred relationships involving `SysUser` (e.g. with `菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms)` and `运营端角色守卫:admin / super_admin 可访问。`) actually correct?**
_`SysUser` has 734 INFERRED edges - model-reasoned connections that need verification._
- **Are the 702 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
_`error_response()` has 702 INFERRED edges - model-reasoned connections that need verification._
+19064 -17147
View File
File diff suppressed because it is too large. Load diff
+14 -12
View File
@@ -34,7 +34,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/models/material.py": 1782894693.6389399,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/production_material.py": 1782404882.9262497,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/qc_param.py": 1782379593.174925,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1792199580.9304104,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1792576366.7658954,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/cleanroom_garment.py": 1781347080.6165943,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/cn_holiday.py": 1786373798.242758,
"/Users/wuzilong/work/00code/zkxw/backend/app/models/__init__.py": 1791386246.9181905,
@@ -64,7 +64,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/inquiry.py": 1778578258.095357,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/order.py": 1778578258.0960286,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/__init__.py": 1781502992.2721803,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/market_mobile.py": 1791922115.798361,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/market_mobile.py": 1792578205.3646982,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/order_adjust.py": 1778578258.0963318,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/cell_product.py": 1782711015.11309,
"/Users/wuzilong/work/00code/zkxw/backend/app/schemas/menu.py": 1778578258.0957444,
@@ -85,6 +85,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/equipment.py": 1782717312.9816003,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1786958454.8959644,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_route.py": 1788231203.61016,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sf_express_settlement.py": 1792580020.6752806,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_code.py": 1782709076.5173395,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_result.py": 1785289887.2915215,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/outbound_release_review.py": 1785314186.7271576,
@@ -159,7 +160,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/research_donation.py": 1792199678.6192756,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_stock_in_no.py": 1787114586.3799527,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/ethnic.py": 1779265849.547566,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_freight.py": 1792078818.772425,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_freight.py": 1792579247.5842276,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_material_formula.py": 1781344175.5871844,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_checkup.py": 1779935785.233911,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/env_detect_item.py": 1781145689.9525824,
@@ -175,7 +176,7 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer.py": 1791648050.9604611,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/workflow.py": 1779265993.677624,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_customer_service.py": 1791860094.4207802,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_receive_account.py": 1785390044.8154407,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/oa_receive_account.py": 1792579491.9561448,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_sms_notification.py": 1781063495.0317714,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception.py": 1791777806.124801,
"/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable_pack.py": 1779265715.416005,
@@ -218,24 +219,24 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_confirm.py": 1779657043.1389203,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_schedule_mgmt.py": 1779940938.18027,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1791860598.0884578,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market_mobile.py": 1792218205.107924,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/market_mobile.py": 1792578366.9390223,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_entry_sms_notification.py": 1780325524.9170487,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/customer_service_dict.py": 1791859621.7209744,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_application.py": 1781079904.0516074,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pre_transit_sample.py": 1786938943.6929982,
"/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": 1791187409.0427368,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/oa_approval.py": 1792140985.8147361,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/oa_approval.py": 1792579495.6813116,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_refund_record.py": 1792197256.603858,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/operations.py": 1785213144.2881503,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1791860293.770292,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1792580899.7326677,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_delivery.py": 1779655584.820777,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_cert_insurance.py": 1791912978.5275548,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/admin.py": 1790217215.444356,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/env_detect.py": 1786945487.513935,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/research_donation.py": 1792199703.3429806,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/ethnic.py": 1779266243.9511197,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_freight.py": 1792078588.4232695,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_freight.py": 1792580031.1094093,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_checkup.py": 1779935751.2658868,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/mini_auth.py": 1789710909.065258,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_service.py": 1781599314.0221174,
@@ -249,7 +250,8 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/api/protocol_sample_payment.py": 1791913783.0750406,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1790217143.1910717,
"/Users/wuzilong/work/00code/zkxw/backend/app/api/clinical_payment.py": 1781750841.074568,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1792462664.4385648,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_engine.py": 1792580527.7126133,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sf_express_settlement.py": 1792575921.313689,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_info_list_enrich.py": 1792376536.1772416,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_payment_period_summary.py": 1791799261.7547023,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_datetime_normalize.py": 1792462649.0641665,
@@ -289,19 +291,19 @@
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_payment_progress.py": 1792202352.3417196,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_resignation_kit_sync.py": 1785405020.285242,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_mp_auth.py": 1786615330.0733685,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_support.py": 1792225523.0536156,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_support.py": 1792574711.417624,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_assignee.py": 1790665524.6969597,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/wechat_login_service.py": 1786615952.6840453,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_protocol.py": 1792392055.0493736,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_field_scope.py": 1791651783.1531875,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/customer_relations.py": 1791383150.5354238,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_receive_account.py": 1785390254.4121296,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_receive_account.py": 1792579495.1077259,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_presign.py": 1791713904.5883873,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_owner_fields.py": 1785401151.5200977,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_money_field.py": 1785380625.3526123,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/sample_storage_cert_qc.py": 1791520419.7485287,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/protocol_pay_mode_switch.py": 1791922115.8003666,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_freight.py": 1792482296.0678396,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/market_freight.py": 1792578248.775886,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_post_select.py": 1785394392.5716882,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_wechat_notify.py": 1789108034.959046,
"/Users/wuzilong/work/00code/zkxw/backend/app/services/oa_confirmation_kit_sync.py": 1785403667.4270058,
+38 -3
View File
@@ -22,6 +22,9 @@
# 正式写入(去掉 --dry-run)
python scripts/reset_weak_password_111111.py --database-url '...'
# 生产后端容器:脚本可放任意路径,容器内已有 DATABASE_URL 与 /app
docker exec <backend容器> python /tmp/reset_weak_password_111111.py --dry-run
范围:del_flag=0(含停用账号 status!=1)。
"""
from __future__ import annotations
@@ -37,7 +40,39 @@ from urllib.parse import quote_plus
WEAK_PASSWORD = "111111"
NEW_PASSWORD = "Zkxw@123"
BACKEND_ROOT = Path(__file__).resolve().parent.parent
def _candidate_dirs(start: Path):
current = start.resolve()
if current.is_file():
current = current.parent
while True:
yield current
if current.parent == current:
break
current = current.parent
def _has_app_package(directory: Path) -> bool:
return (directory / "app" / "__init__.py").is_file()
def _find_backend_root() -> Path:
"""定位含 app 包的目录,不依赖脚本放在 backend/scripts 下。"""
seen = set()
starts = [Path(__file__), Path.cwd(), Path("/app")]
for start in starts:
if not start.exists():
continue
for directory in _candidate_dirs(start):
key = str(directory)
if key in seen:
continue
seen.add(key)
if _has_app_package(directory):
return directory
raise SystemExit(
"找不到后端 app 包。请在后端容器内执行,或在含 app 目录的环境中运行本脚本。"
)
def _mask_database_url(url: str) -> str:
@@ -113,7 +148,7 @@ def _resolve_connection(args: argparse.Namespace) -> tuple[str, str]:
if env_url:
return ("env DATABASE_URL", env_url)
env_file = args.env_file or os.environ.get("ENV_FILE") or str(BACKEND_ROOT / ".env")
env_file = args.env_file or os.environ.get("ENV_FILE") or str(_find_backend_root() / ".env")
env_file_path = Path(env_file).expanduser()
if not env_file_path.is_absolute():
env_file_path = (Path.cwd() / env_file_path).resolve()
@@ -131,7 +166,7 @@ def main(argv: list[str] | None = None) -> int:
args = _parse_args(argv)
source, early_url = _resolve_connection(args)
sys.path.insert(0, str(BACKEND_ROOT))
sys.path.insert(0, str(_find_backend_root()))
# 必须在设置 ENV_FILE / DATABASE_URL 之后再 import,否则 Settings 已冻结
from sqlalchemy import create_engine
+73
View File
@@ -442,6 +442,79 @@ def test_protocol_form_options_filter_sample_codes_by_code_filter():
assert 'OTHER-002' in codes
def test_protocol_form_options_search_sample_codes_by_keyword():
"""弹窗输入的编码走 keyword,并仍受样本类型 codeFilter 约束,不受最新 100 条截断。"""
from datetime import datetime, timedelta
from app.services.market_support import get_form_options
db = _session()
_seed_options(db)
base = datetime(2020, 1, 1)
db.add(
BizSampleCode(
id='sample-code-target',
sample_code='ZKZX34000987',
tenant_no='tenant-a',
create_dept_id='',
del_flag=0,
create_time=base,
)
)
db.add(
BizSampleCode(
id='sample-code-other-hit',
sample_code='OTHER34000987',
tenant_no='tenant-a',
create_dept_id='',
del_flag=0,
create_time=base + timedelta(days=400),
)
)
db.add_all(
[
BizSampleCode(
id=f'sample-code-new-{index}',
sample_code=f'ZKZXN{index:05d}',
tenant_no='tenant-a',
create_dept_id='',
del_flag=0,
create_time=base + timedelta(days=index + 1),
)
for index in range(201)
]
)
db.commit()
hidden = get_form_options(
db,
module='protocol',
ctx=_ctx('manager', 'user-b'),
user_info=_user('user-b'),
region_id='rb',
code_filter='ZKZX',
)
hidden_codes = {item['sampleCode'] for item in hidden['sampleCodes']}
assert 'ZKZX34000987' not in hidden_codes
found = get_form_options(
db,
module='protocol',
ctx=_ctx('manager', 'user-b'),
user_info=_user('user-b'),
region_id='rb',
code_filter='ZKZX',
sample_code_keyword='34000987',
)
assert found['sampleCodes'] == [
{
'value': 'sample-code-target',
'label': 'ZKZX34000987',
'sampleCode': 'ZKZX34000987',
}
]
def test_protocol_form_options_hide_occupied_sample_codes():
from app.services.market_support import get_form_options
@@ -0,0 +1,721 @@
# OA 收款账户所属人 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 收款账户的所属人沿用 `create_by`;管理页展示所属人,发起审批时只能选到并提交当前用户自己的账户。
**Architecture:** 不改表。`oa_receive_account` 的读模型补上只读 `ownerUserId` / `ownerName`。`list_options` 按当前用户过滤。新函数 `validate_receive_account_ownership` 在 `start_instance` 的格式校验之后核对快照 `id`。Web 管理页展示所属人;Web 与移动端 H5 选择器只改空态文案,选项仍只请求 options 接口。
**Tech Stack:** FastAPI · SQLAlchemy · pytest · Vue 3 · Element Plus · 移动端 H5(Vue + Vant)
**Spec:** `docs/superpowers/specs/2026-10-21-oa-receive-account-owner-design.md`
---
## File map
| Path | Responsibility |
|------|----------------|
| Modify: `backend/app/crud/oa_receive_account.py` | 所属人姓名;`list_options` 按 `create_by` 过滤 |
| Modify: `backend/app/services/oa_receive_account.py` | `validate_receive_account_ownership` |
| Modify: `backend/app/api/oa_approval.py` | options 传入当前用户 ID |
| Modify: `backend/app/services/oa_engine.py` | `start_instance` 在格式校验后调用归属校验 |
| Test: `backend/tests/test_oa_receive_account.py` | 列表、选项、编辑不改所属人、归属校验、发起拒绝 |
| Modify: `web-admin/src/views/oa-approval/receive-accounts/index.vue` | 列表列、查看、编辑只读所属人 |
| Modify: `web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue` | 空态文案 |
| Modify: `mobile-oa-h5/src/oa/components/OaReceiveAccountSelect.vue` | 空态文案 |
| Modify: `docs/superpowers/specs/2026-10-21-oa-receive-account-owner-design.md` | 实现完成后把状态改为已实现 |
不做:新字段、SQL、管理页按人筛选、转让所属人、快照增加所属人、草稿保存时校验、小程序、同实例再次提交。
---
### Task 1: 所属人查询与归属校验的失败测试
**Files:**
- Test: `backend/tests/test_oa_receive_account.py`
- [ ] **Step 1: 在现有测试文件末尾追加失败用例**
保留文件顶部已有的 `normalize_account_fields` / `validate_receive_account_form_values` 导入,并增加:
```python
import json
from datetime import datetime
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.crud import oa_receive_account as ora_crud
from app.models.base import Base
from app.models.models import OaApp, OaAppVersion, OaReceiveAccount, SysUser
from app.services.oa_engine import OaEngineError, start_instance
from app.services.oa_receive_account import validate_receive_account_ownership
```
在 `if __name__` 之前追加:
```python
def _snap(account_id: str) -> dict:
return {
"id": account_id,
"accountType": "alipay",
"accountName": "张三",
"accountNo": "13800138000",
"bankName": "",
"bankProvince": "",
"bankCity": "",
"bankBranch": "",
"remark": "",
}
def _schema(*fields: str) -> dict:
return {
"rule": [
{"type": "receiveAccount", "field": field, "title": field}
for field in fields
]
}
class TestReceiveAccountOwner(unittest.TestCase):
def setUp(self):
self.engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(
self.engine,
tables=[
OaReceiveAccount.__table__,
SysUser.__table__,
OaApp.__table__,
OaAppVersion.__table__,
],
)
self.db = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)()
now = datetime(2026, 10, 21, 10, 0, 0)
self.db.add_all(
[
SysUser(id="me", user_no="me", nick_name="我自己", password="", tenant_no="zkxw", create_dept_id="", create_time=now, update_time=now, del_flag=0),
SysUser(id="other", user_no="other", nick_name="别人", password="", tenant_no="zkxw", create_dept_id="", create_time=now, update_time=now, del_flag=0),
]
)
self.db.add_all(
[
self._account("mine", "me", 0),
self._account("padded", " me ", 0),
self._account("theirs", "other", 0),
self._account("blank", "", 0),
self._account("none", None, 0),
self._account("gone", "me", 1),
self._account("other-tenant", "me", 0, tenant_no="other"),
]
)
self.db.commit()
def tearDown(self):
self.db.close()
def _account(self, account_id, create_by, del_flag, tenant_no="zkxw"):
now = datetime(2026, 10, 21, 10, 0, 0)
return OaReceiveAccount(
id=account_id,
account_type="alipay",
account_name=account_id,
account_no=account_id,
tenant_no=tenant_no,
create_by=create_by,
create_dept_id="",
create_time=now,
update_time=now,
del_flag=del_flag,
)
def test_options_only_current_owner(self):
ids = {row["id"] for row in ora_crud.list_options(self.db, tenant_no="zkxw", user_id="me")}
self.assertEqual(ids, {"padded", "mine"})
def test_list_keeps_every_tenant_account_and_owner_name(self):
rows, total = ora_crud.list_accounts(self.db, tenant_no="zkxw", page=1, page_size=20)
by_id = {row["id"]: row for row in rows}
self.assertEqual(total, 5)
self.assertEqual(by_id["mine"]["ownerUserId"], "me")
self.assertEqual(by_id["mine"]["ownerName"], "我自己")
self.assertEqual(by_id["padded"]["ownerUserId"], "me")
self.assertEqual(by_id["padded"]["ownerName"], "我自己")
self.assertEqual(by_id["blank"]["ownerUserId"], "")
self.assertEqual(by_id["blank"]["ownerName"], "—")
self.assertEqual(by_id["none"]["ownerName"], "—")
self.assertNotIn("gone", by_id)
self.assertNotIn("other-tenant", by_id)
def test_missing_user_still_selectable_with_dash_name(self):
self.db.add(self._account("orphan", "ghost", 0))
self.db.commit()
ids = [row["id"] for row in ora_crud.list_options(self.db, tenant_no="zkxw", user_id="ghost")]
self.assertEqual(ids, ["orphan"])
row = ora_crud.get_account(self.db, "orphan", "zkxw")
self.assertEqual(row["ownerUserId"], "ghost")
self.assertEqual(row["ownerName"], "—")
def test_create_sets_current_user_as_owner(self):
created = ora_crud.create_account(
self.db,
tenant_no="zkxw",
user_id="me",
dept_id="",
fields={
"account_type": "alipay",
"account_name": "新户",
"account_no": "6222",
"bank_name": "",
"bank_province": "",
"bank_city": "",
"bank_branch": "",
"remark": "",
},
)
self.assertEqual(created["ownerUserId"], "me")
self.assertEqual(created["ownerName"], "我自己")
ids = {row["id"] for row in ora_crud.list_options(self.db, tenant_no="zkxw", user_id="me")}
self.assertIn(created["id"], ids)
def test_update_does_not_change_owner(self):
ora_crud.update_account(
self.db,
"mine",
"zkxw",
"other",
{
"account_type": "alipay",
"account_name": "已改户名",
"account_no": "mine",
"bank_name": "",
"bank_province": "",
"bank_city": "",
"bank_branch": "",
"remark": "",
},
)
row = self.db.query(OaReceiveAccount).filter(OaReceiveAccount.id == "mine").one()
self.assertEqual(row.create_by, "me")
self.assertEqual(row.account_name, "已改户名")
def test_ownership_accepts_own_account(self):
errs = validate_receive_account_ownership(
self.db,
_schema("acc"),
{"acc": [_snap("mine")]},
tenant_no="zkxw",
initiator_user_id="me",
)
self.assertEqual(errs, [])
def test_ownership_rejects_in_schema_order(self):
errs = validate_receive_account_ownership(
self.db,
_schema("first", "second"),
{"first": [_snap("mine")], "second": [_snap("theirs"), _snap("blank")]},
tenant_no="zkxw",
initiator_user_id="me",
)
self.assertEqual(errs, ["「second」不属于当前用户"])
def test_ownership_error_messages(self):
cases = [
("gone", "「acc」不存在或已删除"),
("other-tenant", "「acc」不存在或已删除"),
("missing", "「acc」不存在或已删除"),
("theirs", "「acc」不属于当前用户"),
("blank", "「acc」不属于当前用户"),
("none", "「acc」不属于当前用户"),
]
for account_id, message in cases:
errs = validate_receive_account_ownership(
self.db,
_schema("acc"),
{"acc": [_snap(account_id)]},
tenant_no="zkxw",
initiator_user_id="me",
)
self.assertEqual(errs, [message], account_id)
def _published_app(self):
now = datetime(2026, 10, 21, 10, 0, 0)
schema = {"rule": [{"type": "receiveAccount", "field": "acc", "title": "收款账户"}]}
self.db.add(
OaApp(
id="app1",
app_name="付款",
initiator_scope="all",
admin_scope="all_oa_admin",
status="1",
tenant_no="zkxw",
create_dept_id="",
create_time=now,
update_time=now,
del_flag=0,
)
)
self.db.add(
OaAppVersion(
id="v1",
app_id="app1",
version_no=1,
status="published",
is_latest_published="1",
form_schema_json=json.dumps(schema, ensure_ascii=False),
flow_tree_json="{}",
advanced_settings_json="{}",
tenant_no="zkxw",
create_dept_id="",
create_time=now,
update_time=now,
del_flag=0,
)
)
self.db.commit()
def test_start_instance_rejects_other_owner_before_flow(self):
self._published_app()
with self.assertRaises(OaEngineError) as ctx:
start_instance(
self.db,
tenant_no="zkxw",
app_id="app1",
initiator_user_id="me",
form_data={"acc": [_snap("theirs")]},
)
self.assertEqual(str(ctx.exception), "「收款账户」不属于当前用户")
```
`list_accounts` 与 `list_options` 都按 `create_time DESC`。测试数据的创建时间相同,SQLite 不保证同时间的顺序,所以断言用 id 集合或 `by_id`,不要断言列表顺序。
- [ ] **Step 2: 运行测试,确认失败**
Run: `cd backend && python -m pytest tests/test_oa_receive_account.py::TestReceiveAccountOwner -q`
Expected: FAIL。`validate_receive_account_ownership` 尚未定义,或 `list_options()` 不接受 `user_id`。原有 `TestNormalizeAccountFields` 与 `TestValidateReceiveAccountFormValues` 不要在这一步改坏;本步只要求新类失败。
- [ ] **Step 3: Commit**
```bash
git add backend/tests/test_oa_receive_account.py
git commit -m "$(cat <<'EOF'
test: 锁定收款账户按登记人过滤和发起归属校验
这些用例先失败,用来约束后续的选项接口和管理页展示。
EOF
)"
```
---
### Task 2: 实现所属人读模型、选项过滤和归属校验
**Files:**
- Modify: `backend/app/crud/oa_receive_account.py`
- Modify: `backend/app/services/oa_receive_account.py`
- Modify: `backend/app/api/oa_approval.py`
- Test: `backend/tests/test_oa_receive_account.py`
- [ ] **Step 1: 给 CRUD 补上所属人,并按登记人过滤选项**
在 `backend/app/crud/oa_receive_account.py` 增加导入 `SysUser`。把 `row_to_dict` 的返回字典加上:
```python
"ownerUserId": (r.create_by or "").strip(),
```
不要在 `row_to_dict` 里查库。在该函数下方增加:
```python
def _owner_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
ids = [item for item in {uid.strip() for uid in user_ids if uid and uid.strip()}]
if not ids:
return {}
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
return {row.id: (row.nick_name or "").strip() for row in rows}
def _attach_owner_names(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
names = _owner_name_map(db, [item.get("ownerUserId") or "" for item in items])
for item in items:
uid = item.get("ownerUserId") or ""
item["ownerName"] = names.get(uid) or "—"
return items
```
`list_accounts` 的返回改为:
```python
return _attach_owner_names(db, [row_to_dict(x) for x in rows]), int(total)
```
`get_account` 在找到行后:
```python
return _attach_owner_names(db, [row_to_dict(row)])[0]
```
`create_account` 与 `update_account` 在 `return row_to_dict(obj)` 处改为:
```python
return _attach_owner_names(db, [row_to_dict(obj)])[0]
```
`update_account` 继续不要给 `obj.create_by` 赋值。
把 `list_options` 替换为:
```python
def list_options(db: Session, *, tenant_no: str, user_id: str) -> List[Dict[str, Any]]:
owner_id = (user_id or "").strip()
if not owner_id:
return []
rows = (
db.query(OaReceiveAccount)
.filter(
OaReceiveAccount.tenant_no == tenant_no,
OaReceiveAccount.del_flag == 0,
func.trim(OaReceiveAccount.create_by) == owner_id,
)
.order_by(OaReceiveAccount.create_time.desc())
.all()
)
return _attach_owner_names(db, [row_to_dict(x) for x in rows])
```
文件已导入 `func`。
- [ ] **Step 2: 实现归属校验**
在 `backend/app/services/oa_receive_account.py` 增加:
```python
from sqlalchemy.orm import Session
from app.models.models import OaReceiveAccount
```
并追加:
```python
def validate_receive_account_ownership(
db: Session,
schema: Any,
form_data: Optional[Dict[str, Any]],
*,
tenant_no: str,
initiator_user_id: str,
) -> List[str]:
"""校验收款账户快照 id 属于发起人。调用前须已通过格式校验。"""
data = form_data or {}
owner_id = (initiator_user_id or "").strip()
for rule in _schema_rules(schema):
if str(rule.get("type") or "") != "receiveAccount":
continue
field = str(rule.get("field") or "").strip()
if not field:
continue
value = data.get(field)
if not isinstance(value, list):
continue
label = _field_label(rule)
for item in value:
if not isinstance(item, dict):
continue
item_id = str(item.get("id") or "").strip()
if not item_id:
continue
row = (
db.query(OaReceiveAccount)
.filter(
OaReceiveAccount.id == item_id,
OaReceiveAccount.tenant_no == tenant_no,
)
.first()
)
if row is None or int(row.del_flag or 0) == 1:
return [f"「{label}」不存在或已删除"]
if (row.create_by or "").strip() != owner_id:
return [f"「{label}」不属于当前用户"]
return []
```
找到第一条错误就返回,不要继续扫后面的字段或数组元素。不要改写 `form_data`。
- [ ] **Step 3: 选项接口传入当前用户**
`backend/app/api/oa_approval.py` 的 `list_receive_account_options`:
```python
return success_response(
{
"list": ora_crud.list_options(
db,
tenant_no=tenant_no,
user_id=user.get("user_id") or "",
)
}
)
```
- [ ] **Step 4: 运行除发起集成以外的测试**
`test_start_instance_rejects_other_owner_before_flow` 要到 Task 3 才通过。本步运行:
Run: `cd backend && python -m pytest tests/test_oa_receive_account.py -q -k "not test_start_instance_rejects_other_owner_before_flow"`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add backend/app/crud/oa_receive_account.py backend/app/services/oa_receive_account.py backend/app/api/oa_approval.py backend/tests/test_oa_receive_account.py
git commit -m "$(cat <<'EOF'
feat: 收款账户选项只返回当前登记人并带上所属人姓名
发起校验能识别已删除、他租户和他人账户,编辑账户不会改掉 create_by。
EOF
)"
```
---
### Task 3: 发起实例时拒绝不属于发起人的账户
**Files:**
- Modify: `backend/app/services/oa_engine.py`(`start_instance` 里 `validate_receive_account_form_values` 调用之后,约 1889–1891 行)
- Test: `backend/tests/test_oa_receive_account.py`
- [ ] **Step 1: 确认 Task 1 的发起测试仍然失败**
Run: `cd backend && python -m pytest tests/test_oa_receive_account.py::TestReceiveAccountOwner::test_start_instance_rejects_other_owner_before_flow -q`
Expected: FAIL。错误文本不是 `「收款账户」不属于当前用户`(当前会在归属校验缺失后进入流程树)。
- [ ] **Step 2: 在格式校验之后接入归属校验**
把现有片段:
```python
recv_errs = validate_receive_account_form_values(schema, data)
if recv_errs:
raise OaEngineError(recv_errs[0])
```
改为:
```python
recv_errs = validate_receive_account_form_values(schema, data)
if recv_errs:
raise OaEngineError(recv_errs[0])
from app.services.oa_receive_account import validate_receive_account_ownership
own_errs = validate_receive_account_ownership(
db,
schema,
data,
tenant_no=tenant_no,
initiator_user_id=initiator_user_id,
)
if own_errs:
raise OaEngineError(own_errs[0])
```
`data` 仍是格式校验之后的原快照。不要用主数据覆盖户名、账号或银行字段。不要在草稿保存函数里调用归属校验。`submit_draft` 已经调用 `start_instance`,不要再包一层。
- [ ] **Step 3: 运行收款账户测试**
Run: `cd backend && python -m pytest tests/test_oa_receive_account.py -q`
Expected: PASS
- [ ] **Step 4: 更新后端代码图谱**
Run: `./scripts/update-graph.sh backend`
Expected: 命令成功结束。
- [ ] **Step 5: Commit**
只提交本任务改过的文件。工作区里原先已有的 `backend/graphify-out` 改动不要一并放进这次提交;图谱没有新差异时,下面的 `git add` 去掉 `backend/graphify-out`。
```bash
git add backend/app/services/oa_engine.py backend/tests/test_oa_receive_account.py
git commit -m "$(cat <<'EOF'
feat: 发起审批时拒绝不属于发起人的收款账户
直接发起和草稿提交都走 start_instance,因此只在这一处核对账户归属。
EOF
)"
```
---
### Task 4: Web 管理页展示所属人
**Files:**
- Modify: `web-admin/src/views/oa-approval/receive-accounts/index.vue`
- [ ] **Step 1: 列表在户名后增加所属人列**
紧接 `label="户名"` 的 `el-table-column` 之后插入:
```vue
<el-table-column
label="所属人"
min-width="120"
align="left"
header-align="left"
show-overflow-tooltip
>
<template #default="{ row }">{{ formatDisplayOrDash(row.ownerName) }}</template>
</el-table-column>
```
- [ ] **Step 2: 查看弹窗在户名后展示所属人**
在查看弹窗「户名」那个 `view-item` 后面插入:
```vue
<div class="view-item">
<div class="k">所属人</div>
<div class="v">{{ formatDisplayOrDash(viewRow.ownerName) }}</div>
</div>
```
- [ ] **Step 3: 编辑弹窗只读展示所属人,新增弹窗不展示**
在 `const isEdit = ref(false)` 附近增加:
```ts
const editingOwnerName = ref('')
```
`handleAdd` 里 `resetForm()` 之后加 `editingOwnerName.value = ''`。
`handleEdit` 里在打开弹窗前加:
```ts
editingOwnerName.value = row.ownerName || '—'
```
在编辑表单「户名」`el-form-item` 后面插入:
```vue
<el-form-item v-if="isEdit" label="所属人">
<el-input :model-value="editingOwnerName" disabled />
</el-form-item>
```
`handleSubmit` 的 `payload` 保持现状,不要加入 `ownerUserId` 或 `ownerName`。
- [ ] **Step 4: Commit**
```bash
git add web-admin/src/views/oa-approval/receive-accounts/index.vue
git commit -m "$(cat <<'EOF'
feat: 收款账户管理页展示不可修改的所属人
列表和查看能看出登记人,编辑时只读,避免把账户转给别人。
EOF
)"
```
---
### Task 5: 两端选择器的空态文案
**Files:**
- Modify: `web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue`
- Modify: `mobile-oa-h5/src/oa/components/OaReceiveAccountSelect.vue`
- [ ] **Step 1: Web 抽屉和 PC 下拉使用同一句空态**
把抽屉里的 `暂无收款账户` 改为 `暂无本人的收款账户`。
在 `el-select` 的 `el-option` 循环之后、`#footer` 之前插入:
```vue
<template #empty>
<div class="oa-receive-account-select__empty">暂无本人的收款账户</div>
</template>
```
不要在这个组件里再请求列表接口或按 `ownerUserId` 过滤。新增成功后已有的 `await loadOptions()` 保持不动。
- [ ] **Step 2: 移动端空态使用同一句**
`mobile-oa-h5/src/oa/components/OaReceiveAccountSelect.vue` 中:
```vue
<van-empty v-if="!loading && !filtered.length" description="暂无本人的收款账户" />
```
不要新增收款账户管理页。新增成功后已有的 `loadOptions()` 保持不动。
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue mobile-oa-h5/src/oa/components/OaReceiveAccountSelect.vue
git commit -m "$(cat <<'EOF'
fix: 发起审批时说明当前用户还没有收款账户
两端选择器都只展示 options 接口返回的本人账户,空列表不再像是系统没有账户。
EOF
)"
```
---
### Task 6: 手工核对并标记规格已实现
**Files:**
- Modify: `docs/superpowers/specs/2026-10-21-oa-receive-account-owner-design.md`
- [ ] **Step 1: 再跑一遍后端测试**
Run: `cd backend && python -m pytest tests/test_oa_receive_account.py -q`
Expected: PASS
- [ ] **Step 2: 核对 Web 管理页**
用有「收款账户管理」菜单的账号打开页面:
- 列表在户名后有「所属人」,姓名来自用户名;对不上人时为「—」
- 查看弹窗有所属人
- 新增弹窗没有所属人输入;保存后该行所属人是当前用户
- 编辑弹窗所属人禁用;改户名后所属人不变
- 仍可编辑、删除他人账户
- [ ] **Step 3: 核对 Web 与移动端发起**
打开一个含「收款账户」控件的审批:
- 选择器只有当前用户的账户,看不到他人账户和所属人为空的老账户
- 没有账户时,Web 抽屉、Web 下拉、移动端空态都是「暂无本人的收款账户」
- 在选择器里新增一条后,刷新出的选项里有它,并且可以选中
- 详情、审批、打印仍只显示账户快照,没有所属人
- [ ] **Step 4: 把规格状态改为已实现**
仅当 Step 2 和 Step 3 都通过。把规格文首 `**状态**: 待审阅` 改为 `**状态**: 已实现`。
```bash
git add docs/superpowers/specs/2026-10-21-oa-receive-account-owner-design.md
git commit -m "$(cat <<'EOF'
docs: 标记收款账户所属人设计已实现
管理页展示和两端发起筛选已按规格核对。
EOF
)"
```
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,171 @@
# OA 收款账户所属人设计
**模块**: OA 审批 · 收款账户管理 · 发起审批
**日期**: 2026-10-21
**状态**: 待审阅
**关联**: `docs/superpowers/specs/2026-07-30-oa-receive-account-design.md`
**决策记录**:
- 不新增字段。所属人就是现有 `oa_receive_account.create_by`
- 管理页仍看本租户全部未删除账户,并展示所属人
- 有菜单权限的人仍可编辑、删除任意账户。编辑不改变所属人
- 发起审批的选项只含当前用户名下、且 `create_by` 非空的账户
- 已有数据直接沿用 `create_by`。为空的账户管理页可见,任何人都不能在发起时选到
- 表单仍存完整快照。详情、审批、打印不回查主数据,也不展示所属人
- Web 管理页与 Web / 移动端 H5 选择器都要改。小程序没有该控件,不改
本设计取代 2026-07-30 规格中的非目标「个人私有账户、数据权限按创建人过滤」。管理页的增删改权限保持当时的「有菜单即可管理」。
---
## 1. 背景与目标
收款账户现在是租户级公共主数据。`GET /receive-accounts/options` 返回本租户全部未删除账户,Web 与移动端 H5 的「收款账户」控件都用这份列表。新增时已经把当前用户 ID 写入 `create_by`,但列表不展示,选项也不按它过滤。
业务要求账户归登记人。发起含该控件的审批时,只能选自己登记的账户。
**成功标准**:
- 管理页能看到本租户全部账户,并能看出每条属于谁
- 新增账户的所属人是当前登录用户,之后不能改
- Web 与移动端发起时,选择器只出现本人账户
- 直接发起和草稿提交都不能把他人账户、无所属人账户、已删除账户写进新单据
- 已经提交的单据仍按当时快照展示
---
## 2. 范围
### 2.1 在范围内
| 项 | 说明 |
|----|------|
| 后端列表 | `GET /receive-accounts` 增加只读 `ownerUserId`、`ownerName` |
| 后端选项 | `GET /receive-accounts/options` 只返回当前用户的账户 |
| 发起校验 | `start_instance` 在现有格式校验之后核对快照 `id` 的归属 |
| Web 管理页 | 列表、查看、编辑展示所属人;编辑只读 |
| Web 选择器 | 空态文案;继续只消费 options |
| 移动端 H5 选择器 | 同上 |
| 测试 | 扩展 `backend/tests/test_oa_receive_account.py` |
### 2.2 非目标
- 不新增 `owner_user_id`,不改表结构,不写 SQL
- 不提供转让所属人,新增弹窗不出现所属人输入
- 不按所属人限制管理页的编辑和删除
- 管理页不增加「所属人」筛选
- 不把所属人写入表单快照,不改详情、审批、打印版式
- 草稿保存不做归属校验
- 提交时不按主数据回写快照里的户名、账号、银行信息
- 不改小程序
- 不恢复已废弃的同实例再次提交
---
## 3. 数据与接口
所属人等于 `create_by`。创建逻辑保持现状:`create_account` 把当前用户 ID 写入 `create_by`。`update_account` 与软删除都不改 `create_by`。
`create_by` 去掉首尾空白后为空,视为无所属人。
### 3.1 读模型
`row_to_dict` 增加两个只读字段,不进入新增、编辑的请求体:
| 字段 | 规则 |
|------|------|
| `ownerUserId` | `create_by` 去空白后的值;为空则 `""` |
| `ownerName` | 用 `ownerUserId` 查 `sys_user.nick_name`;用户不存在、姓名为空,或 `ownerUserId` 为空时,返回 `"—"`。用户记录缺失不影响选项资格,仍按 `create_by` 判断 |
列表批量查用户,避免逐行查询。详情、创建成功响应使用同一规则。
### 3.2 两条读路径
```
管理页
GET /web/admin/oa-approval/receive-accounts
条件:tenant_no = 当前租户 AND del_flag = 0
不按 create_by 过滤
返回 ownerUserId、ownerName
发起选择器(Web 与移动端 H5)
GET /web/admin/oa-approval/receive-accounts/options
条件:tenant_no = 当前租户
AND del_flag = 0
AND trim(create_by) = 当前登录用户 ID
AND trim(create_by) 非空
```
选项接口在服务端过滤。前端不得先拉全量再自己滤,避免把他人账号发到浏览器。
### 3.3 发起校验
直接发起和草稿提交都进入 `start_instance`。草稿提交把草稿所属人作为 `initiator_user_id`。归属比对使用这个发起人,不另写一条草稿校验。
在现有 `validate_receive_account_form_values` 通过之后,对每个 `receiveAccount` 字段里的每个快照 `id`:
1. 按 `id + tenant_no` 查 `oa_receive_account`
2. 查不到,或 `del_flag = 1`:失败,文案 `「{字段标题}」不存在或已删除`
3. `create_by` 去空白后为空,或不等于 `initiator_user_id`:失败,文案 `「{字段标题}」不属于当前用户`
4. 多个收款账户字段按现有格式校验的 schema 顺序检查;同一字段内按数组顺序检查。返回第一条归属错误。`start_instance` 仍只抛出第一条 `OaEngineError`
通过后,原样保存客户端提交的快照。不从主数据覆盖户名、账号或银行字段。
已创建的实例在详情、审批、打印时不重新做这层校验。复制旧单再发起会再次进入 `start_instance`,按新的发起人重查。
---
## 4. 界面
### 4.1 Web 管理页
文件:`web-admin/src/views/oa-approval/receive-accounts/index.vue`
- 列表在「户名」后增加「所属人」列,`min-width`,纯文本展示 `ownerName`
- 查看弹窗增加「所属人」,展示 `ownerName`
- 新增弹窗不增加所属人控件。保存后所属人即当前登录用户
- 编辑弹窗展示只读「所属人」,不可输入、不可提交该字段
- 筛选仍为关键词、账户类型。工具栏与编辑、删除权限不变
### 4.2 发起选择器
| 端 | 文件 | 改动 |
|----|------|------|
| Web | `web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue` | 下拉空态与窄屏抽屉空态改为「暂无本人的收款账户」 |
| 移动端 H5 | `mobile-oa-h5/src/oa/components/OaReceiveAccountSelect.vue` | `van-empty` 文案改为同一句 |
两边继续只请求 options。选择器内「新增收款账户」仍走原创建接口;成功后重新请求 options,新账户属于当前用户,应出现在列表中并沿用现有的自动选中。
移动端没有收款账户管理页,不新增该页。
### 4.3 已填单据
详情、审批、打印继续用快照格式化函数。快照结构不增加所属人。选择器因过滤而缺少某个旧 `id` 时,已选中的快照文本仍按现有逻辑展示;再次提交若该 `id` 不属于发起人,由第 3.3 节拒绝。
---
## 5. 测试
后端扩展 `backend/tests/test_oa_receive_account.py`:
- options 只含当前用户、`create_by` 非空、未删除的账户;不含他人账户、空所属人、已删除账户
- 管理列表含本租户全部未删除账户,并返回正确的 `ownerUserId`、`ownerName`;对不上人时 `ownerName` 为 `—`
- 新增后的 `create_by` 为当前用户;编辑户名后 `create_by` 不变
- 归属校验:本人未删除账户通过;他人账户、空所属人、已删除、他租户 `id` 分别返回第 3.3 节对应文案
- `start_instance` 在格式校验之后调用该归属校验。草稿提交因转调 `start_instance`,不单独复制整套用例
手工核对:
- Web 管理页:所属人列、查看弹窗、编辑时只读
- Web 与移动端 H5:发起时只见本人账户;当场新增后能选到;无账户时显示「暂无本人的收款账户」
- 用他人账户 `id` 调发起接口,单据不创建,错误文案与第 3.3 节一致
---
## 6. 实现顺序
1. 列表与选项的查询、所属人姓名、归属校验函数及测试
2. `start_instance` 接入归属校验
3. Web 管理页展示
4. Web 与移动端选择器空态
5. 按第 5 节手工核对两端发起流程
@@ -0,0 +1,153 @@
# 顺丰快递月结 — 设计
> 日期:2026-10-21
> 状态:已确认
> 关联:
>
> - `docs/superpowers/specs/2026-10-07-sample-freight-batch-design.md`(运费批次)
> - `docs/superpowers/specs/2026-10-12-freight-config-cancel-two-button-design.md`(配置 / 取消运费)
> - 实现入口:网页协议单与运费信息、移动端 H5 协议单配置运费、移动端 H5 运费信息、微信小程序通过 webview 打开的同一套页面。顺丰月结菜单只加在网页。
## 1. 背景与结论
运费批次 `biz_sample_freight` 目前只有运输方式和必填金额。运输方式字典 `sample_transport_mode` 中,顺丰快递的值是 `sfkd`。网页、移动端 H5、微信小程序里的协议单配置运费,以及运费信息的新增和编辑,都要求填写金额。没有顺丰单号,也没有结算状态。微信小程序(`mobile-h5`)没有单独的运费页面,市场菜单通过 webview 打开 `mobile-oa-h5` 的协议单和运费信息。
本需求把顺丰快递改成月结:配置时不填金额,展示「月结」,并必填顺丰单号。财务在「缴费财务确认」上方的新菜单里查看这些单号,导出,并标记已结算。结算时运费选填;不填则继续展示「月结」。
一笔运费批次对应一张顺丰单号。同一次配置勾选的样本共用这一张单号,结算也按批次进行。
## 2. 非目标
- 顺丰月结菜单和页面只做网页。不在 `mobile-oa-h5` 增加路由或页面,不在微信小程序 `mobile-h5` 的市场菜单里增加入口。移动端和小程序都不能调用结算、补填、导出接口。
- 不新建独立的顺丰单据表;单号和结算字段挂在现有运费批次上。
- 不校验顺丰单号位数,同一租户内不要求单号唯一。
- 不改专线车及其他运输方式的「必填金额」规则。
- 不把运费并进协议缴费金额。
- 已结算且已有金额的批次,不能改金额。
- 不记录结算人。结算只记结算时间。
## 3. 展示规则
运输方式为 `sfkd` 且 `freight_amount` 为空:运费展示「月结」。
有金额:展示金额。多个样本共用一批时,样本上的运费文案仍带「共用」。
非顺丰批次始终展示金额。
## 4. 配置与编辑
覆盖这些配置和展示,规则相同:
- Web 协议单样本运输(`SampleTransportFormSection`)
- Web 运费信息页的配置弹窗与编辑运费(`FreightConfigDialog`、`FreightEditDialog`)
- 移动端协议单配置运费(`ProtocolSampleTransportPanel`)
- 移动端运费信息的新增、编辑、列表和详情(`modules/freight.js`、`list-presenter.js`)
- 移动端协议详情里的运费金额(`modules/protocol.js`)
微信小程序打开的就是上面这套移动端页面,改移动端后小程序里的协议单和运费信息一起生效。小程序本身不另写运费表单,也不做顺丰月结页。
| 运输方式 | 运费金额 | 顺丰单号 |
|----------|----------|----------|
| 顺丰快递 `sfkd` | 不出现输入框,界面显示「月结」。保存时金额一律存空 | 必填 |
| 其他 | 必填,且不小于 0 | 不出现,保存时必须为空 |
顺丰单号保存前去掉首尾空格。空串视为未填。长度上限 64。
编辑未结算批次:
- 顺丰改为其他方式:必须补金额,清掉单号、结算状态、结算时间。
- 其他方式改为顺丰:必须填单号,金额改为空,结算状态为未结算,结算时间为空。
已结算批次:单号和运输方式锁定,配置端与运费编辑端不可改。仍为「月结」时,金额只能在顺丰月结菜单补填。
## 5. 数据
表 `biz_sample_freight` 增加:
| 字段 | 类型 | 说明 |
|------|------|------|
| `waybill_no` | VARCHAR(64) NULL | 顺丰单号 |
| `settle_status` | VARCHAR(8) NULL | 仅顺丰使用:`wjs` 未结算,`yjs` 已结算 |
| `settle_time` | DATETIME NULL | 结算时间 |
`freight_amount` 改为可空。空表示顺丰月结尚未填写金额。
迁移脚本放在 `docs/sql/migrations/`,并同步 `docs/sql/zkxw.sql`。
历史顺丰批次:保留原金额;`settle_status` 补为 `wjs`;无单号的列表显示「—」。不把历史金额清空成「月结」。非顺丰批次的结算字段保持空。
## 6. 校验
管理端运费接口与市场端运费接口使用同一套规则。
- 新建或编辑为顺丰:单号必填;无论客户端是否提交金额,金额存空;`settle_status = wjs`(已结算批次不走这条,见下)。
- 新建或编辑为非顺丰:金额必填;单号、结算状态、结算时间存空。
- 已结算批次拒绝修改单号、运输方式、删除、继续关联样本、取消关联。
- 标记已结算:仅 `wjs` 的顺丰批次。已结算拒绝重复结算。
- 补填运费:仅 `yjs` 且金额为空的顺丰批次。已有金额的拒绝再改。补填不改 `settle_time`。
- 金额若填写,须为不小于 0,整数最多 16 位、小数最多 2 位(`DECIMAL(18,2)`)。
- 查询与写入按当前用户 `tenant_no` 隔离。
标记已结算时不填金额:
- 当前金额为空:保持为空,继续展示「月结」。
- 当前金额已有值:保留原金额。
填写了金额:覆盖为该金额。两种情况都把 `settle_status` 写成 `yjs`,`settle_time` 写成操作当时。
批量标记已结算只更新状态和时间,不接收金额。需要填写金额的逐条处理。
## 7. 顺丰月结菜单
位置:「市场 → 存储服务」,排在「运费信息」和「缴费财务确认」之间。只加网页菜单。移动端路由和小程序市场菜单保持现状,不出现「顺丰月结」。
| 项 | 值 |
|----|----|
| 标题 | 顺丰月结 |
| 路由 | `/#/market/cell-storage/sf-express-settlement` |
| 组件 | `web-admin/src/views/cell-storage/sf-express-settlement/index.vue` |
| menuCode | `cell-storage:sf-express-settlement:list` |
同步三处:`web-admin/src/router/modules/sqxp.ts`、`backend/app/api/menu.py`、菜单 SQL。授权对象与「缴费财务确认」相同。界面模式按标准列表(P01,参考 `src/views/system/unit/index.vue`)。
列表只含 `transport_mode = sfkd` 且未删除的批次。默认排序 `create_time DESC`。
筛选:顺丰单号、结算状态、创建时间。
列:顺丰单号、创建人、创建时间、结算状态、运费、结算时间。创建人展示姓名。结算状态用标签:未结算为 `info`,已结算为 `success`。运费列按第 3 节展示。
工具栏:
- 「已选 N 项」在最前。
- 主按钮「标记已结算」。未选、或所选含已结算时不可用。选中 1 条:打开结算弹窗,运费选填,提示「不填则保持当前展示:月结或原金额」。选中多条:确认后只改状态和时间。
- 描边按钮「导出」。按当前筛选导出 xlsx,列与列表一致;运费列导出「月结」或金额文本。筛选结果为空时提示「没有可导出的数据」,不下载空文件。
- 行内「补填运费」:仅已结算且运费为「月结」时显示。
已结算批次在运费信息页不可删除,不可再配置关联或取消关联。未结算批次沿用现有删除与取消运费。
## 8. 接口
前缀保持现有 `/api` 管理端与市场端运费路由,并增加结算专用接口。结算接口挂在管理端,不注册到市场端。
| 动作 | 行为 |
|------|------|
| 创建 / 更新运费 | 增加 `waybillNo`。金额与单号按第 6 节取舍 |
| 顺丰月结列表 | 筛选、分页,只返回顺丰批次 |
| 标记已结算 | 单条可带可选 `freightAmount`;批量不带金额 |
| 补填运费 | 必填 `freightAmount`,不改结算时间 |
| 导出 | 与列表同一筛选,返回全部行的 JSON;页面用 exceljs 生成 xlsx。筛选为空时接口返回「没有可导出的数据」,页面不下载 |
错误信息使用中文业务文案,例如「请填写顺丰单号」「已结算的运费不能修改」「已结算的运费不能删除」。
## 9. 验收
- 网页协议单、网页运费信息、移动端协议单配置运费、移动端运费信息新增/编辑:选顺丰时显示「月结」和必填单号,没有金额框;选专线车时仍必填金额。微信小程序打开这些页面时表现相同。
- 移动端和小程序的运费列表、运费详情、协议详情:顺丰且金额为空时显示「月结」,有金额时显示金额,共用批次仍带「共用」。
- 网页、移动端、小程序都没有顺丰月结菜单;结算、补填、导出只在网页。
- 新顺丰批次在顺丰月结列表中为未结算,运费为「月结」。
- 单条结算不填金额:新单继续「月结」;历史已有金额的单保留金额。填写金额后各处展示该金额。结算时间有值,状态为已结算。
- 批量结算只改状态和时间。
- 补填运费后展示金额,结算时间不变。
- 导出列与列表一致。无数据时提示,不下载文件。
- 样本运费展示与批次一致;多笔样本共用时仍带「共用」。
- 已结算批次不能改单号、改运输方式、删除、再关联或取消关联。
- 市场端调用结算接口被拒绝。
@@ -0,0 +1,85 @@
<template>
<van-action-sheet
v-model:show="visible"
:actions="HARMONY_FILE_SOURCES"
cancel-text="取消"
close-on-click-action
@select="onSelect"
/>
<input
v-for="source in HARMONY_FILE_SOURCES"
:key="source.value"
:ref="(el) => setInput(source.value, el)"
class="harmony-file-source__input"
type="file"
:accept="inputAttrs(source.value).accept"
:capture="inputAttrs(source.value).capture || undefined"
:multiple="inputAttrs(source.value).multiple ? true : undefined"
@change="onChange"
/>
</template>
<script setup>
import { ref } from 'vue'
import { showToast } from 'vant'
import {
HARMONY_FILE_SOURCES,
collectAcceptedAttachments,
harmonyFileInputAttrs
} from '../utils/harmony-file-picker.js'
const props = defineProps({
accept: { type: String, default: '' },
maxBytes: { type: Number, default: 0 }
})
const emit = defineEmits(['pick'])
const visible = ref(false)
const inputs = {}
function inputAttrs(source) {
return harmonyFileInputAttrs(source)
}
function setInput(source, element) {
if (element) inputs[source] = element
else delete inputs[source]
}
function open() {
visible.value = true
}
function onSelect(action) {
const input = inputs[action?.value]
if (!input) return
input.value = ''
input.click()
}
function onChange(event) {
const input = event.target
const files = Array.from(input?.files || [])
if (input) input.value = ''
const { accepted, message } = collectAcceptedAttachments(files, {
accept: props.accept,
maxBytes: props.maxBytes
})
if (message) showToast(message)
if (accepted.length) emit('pick', accepted)
}
defineExpose({ open })
</script>
<style scoped>
.harmony-file-source__input {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
</style>
@@ -1,7 +1,7 @@
<template>
<div class="market-attachment-cards">
<van-uploader
v-if="editable"
v-if="editable && !useHarmonyPicker"
class="market-attachment-cards__uploader"
:disabled="legacyUploading"
:after-read="afterRead"
@@ -16,6 +16,24 @@
<span>{{ legacyUploading ? '上传中…' : '添加' }}</span>
</div>
</van-uploader>
<button
v-else-if="editable"
type="button"
class="market-attachment-card is-add"
:disabled="legacyUploading"
@click="openHarmonyPicker"
>
<van-loading v-if="legacyUploading" size="22" color="#0a9f6d" />
<span v-else class="market-attachment-card__plus">+</span>
<span>{{ legacyUploading ? '上传中…' : '添加' }}</span>
</button>
<HarmonyFileSource
v-if="editable && useHarmonyPicker"
ref="harmonySourceRef"
:accept="accept"
:max-bytes="maxSize"
@pick="onHarmonyPick"
/>
<article
v-for="(file, index) in files"
@@ -107,6 +125,8 @@ import {
isAuthorizedImagePreviewStale
} from '../authorized-image-preview.js'
import { downloadAttachmentFile } from '../../utils/download-attachment.js'
import { isHarmonyWechat } from '../../utils/harmony-file-picker.js'
import HarmonyFileSource from '../../components/HarmonyFileSource.vue'
const props = defineProps({
modelValue: { type: [Array, String, Object], default: '' },
@@ -120,6 +140,10 @@ const props = defineProps({
})
const emit = defineEmits(['upload', 'remove'])
const useHarmonyPicker = isHarmonyWechat(
typeof navigator === 'undefined' ? '' : navigator.userAgent
)
const harmonySourceRef = ref(null)
const route = useRoute()
const context = computed(() => getMarketRouteContext(route))
@@ -258,6 +282,15 @@ function afterRead(value) {
}
}
function openHarmonyPicker() {
if (legacyUploading.value) return
harmonySourceRef.value?.open()
}
function onHarmonyPick(files) {
for (const file of files) emit('upload', file)
}
async function openAttachment(file) {
if (file.isImage) {
const version = previewLoadVersion
@@ -321,6 +354,18 @@ async function downloadAttachment(file) {
box-shadow: 0 4px 12px rgb(20 55 40 / 6%);
}
button.market-attachment-card.is-add {
padding: 0;
appearance: none;
font: inherit;
cursor: pointer;
}
button.market-attachment-card.is-add:disabled {
cursor: default;
opacity: 0.72;
}
.market-attachment-card.is-add {
display: flex;
flex-direction: column;
@@ -75,7 +75,7 @@
@upload="$emit('upload', $event)"
@remove="removeAttachment"
/>
<div v-else-if="field.type === 'static'" class="market-form-field__static">{{ field.staticText }}</div>
<div v-else-if="field.type === 'static'" class="market-form-field__control market-form-field__control--text">{{ field.staticText }}</div>
<van-field
v-else
:id="controlId"
@@ -67,6 +67,7 @@ const visibleFields = computed(() => (props.section.fields || []).filter(
(field) =>
!field.hidden &&
(!field.source || field.source === props.source) &&
!(Array.isArray(field.hiddenSources) && field.hiddenSources.includes(props.source)) &&
!(typeof field.showWhen === 'function' && field.showWhen(value.value) === false) &&
!(field.createOnly && props.mode !== 'create')
))
@@ -562,10 +562,16 @@ watch(
{ deep: true, immediate: true }
)
function patchPay(pIdx, patch, rebuild = false) {
function todayYmd() {
const now = new Date()
const pad = (n) => String(n).padStart(2, '0')
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
}
function patchPay(pIdx, patch, rebuild = false, rebuildOptions) {
const list = [...(props.modelValue || [])]
let next = { ...list[pIdx], ...patch }
if (rebuild) next = rebuildSampleSchedules(next)
if (rebuild) next = rebuildSampleSchedules(next, rebuildOptions)
list[pIdx] = next
emitList(list)
}
@@ -703,11 +709,14 @@ async function onPayModeSelect(action) {
patch.installmentIntervalMonths = 1
patch.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
patch.installmentRatios = []
if (mode === 'fq' && !String(prev.installmentStartDate || '').trim()) {
patch.installmentStartDate = todayYmd()
}
} else {
patch.installmentCount = null
patch.installmentRatios = []
}
patchPay(pIdx, patch, true)
patchPay(pIdx, patch, true, { refreshDueDates: mode === 'fq' })
} catch (error) {
if (error === 'cancel' || error?.message === 'cancel') return
showToast(error?.message || '无法切换缴费模式')
@@ -748,7 +757,14 @@ function onDateConfirm({ selectedValues }) {
emitList(list)
return
}
patchPay(target.pIdx, { [target.field]: ymd }, target.field === 'installmentStartDate')
if (target.field === 'installmentStartDate') {
const mode = String((props.modelValue || [])[target.pIdx]?.payMode || '')
patchPay(target.pIdx, { installmentStartDate: ymd }, true, {
refreshDueDates: mode === 'fq' && !!ymd
})
return
}
patchPay(target.pIdx, { [target.field]: ymd }, false)
return
}
if (target.type === 'schedule') {
@@ -140,10 +140,13 @@
<PresignSelectField
:model-value="draft.sampleCodeId"
:options="codeSelectOptions"
:loading="sampleCodeLoading"
searchable
remote
title="选择样本编码"
placeholder="搜索样本编码"
@update:model-value="onCodeIdChange"
@search="onSampleCodeSearch"
/>
</div>
</div>
@@ -250,7 +253,7 @@
</div>
<div v-if="freightForm.transportMode === 'sfkd'" class="market-form-field">
<label>运费金额</label>
<div class="market-form-field__control">月结</div>
<div class="market-form-field__control market-form-field__control--text">月结</div>
</div>
<div v-if="freightForm.transportMode === 'sfkd'" class="market-form-field">
<label>顺丰单号<b>*</b></label>
@@ -388,6 +391,7 @@ import {
buildFreightDraft,
buildFreightItemBodies,
buildFreightUnlinkCandidates,
buildSampleCodeSearchParams,
emptySampleRow,
formatFreightCreatorName,
formatFreightLinkedSampleCodes,
@@ -406,6 +410,7 @@ const props = defineProps({
marketRole: { type: String, default: 'specialist' },
sampleTypes: { type: Array, default: () => [] },
sampleCodes: { type: Array, default: () => [] },
sampleCodeLoading: { type: Boolean, default: false },
transportModes: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false }
})
@@ -468,11 +473,16 @@ const typeSelectOptions = computed(() =>
}))
)
function emitSampleCodeRefresh(sampleTypeId) {
function emitSampleCodeRefresh(sampleTypeId, keyword = '') {
if (autoGenerateCode.value) return
emit('refresh-sample-codes', {
codeFilter: resolveSampleCodeFilter(props.sampleTypes, sampleTypeId)
})
emit('refresh-sample-codes', buildSampleCodeSearchParams({
codeFilter: resolveSampleCodeFilter(props.sampleTypes, sampleTypeId),
keyword
}))
}
function onSampleCodeSearch(keyword) {
emitSampleCodeRefresh(draft.sampleTypeId, keyword)
}
const statusSelectOptions = SAMPLE_STATUS_OPTIONS.map((opt) => ({
label: opt.label,
@@ -490,13 +500,26 @@ const codeSelectOptions = computed(() => {
.map((row, idx) => (idx === editorIndex.value ? '' : String(row.sampleCodeId || '')))
.filter(Boolean)
)
return (props.sampleCodes || [])
const options = (props.sampleCodes || [])
.filter((opt) => !used.has(String(opt.value || opt.id || '')))
.map((opt) => ({
label: opt.label || opt.sampleCode || opt.value,
value: opt.value || opt.id,
sampleCode: opt.sampleCode || opt.label || ''
}))
const selectedId = String(draft.sampleCodeId || '')
if (
selectedId &&
!used.has(selectedId) &&
!options.some((opt) => String(opt.value) === selectedId)
) {
options.unshift({
label: draft.sampleCode || selectedId,
value: draft.sampleCodeId,
sampleCode: draft.sampleCode || ''
})
}
return options
})
watch(
@@ -238,6 +238,25 @@ test('sf express freight form requires a waybill and skips an empty amount', ()
)
})
test('research donation still submits hidden parent fields so edits do not wipe them', () => {
const sections = getMarketModule('protocol').formSections
const payload = pickConfiguredPayload(sections, {
source: 'research_donation',
customer: {
motherName: '李四',
motherPhone: '13800000000',
fatherName: '王五',
fatherPhone: '13900000000',
newbornName: '宝宝'
}
})
assert.equal(payload.customer.motherName, '李四')
assert.equal(payload.customer.motherPhone, '13800000000')
assert.equal(payload.customer.fatherName, '王五')
assert.equal(payload.customer.fatherPhone, '13900000000')
assert.equal(payload.customer.newbornName, '宝宝')
})
test('keeps only configured writable fields including nested and repeated sections', () => {
const sections = [
{ key: 'base', fields: [{ key: 'name' }] },
@@ -699,12 +699,33 @@ test('protocol create and edit forms expose every writable web business field',
)
assert.deepEqual(
customer.fields.find((field) => field.key === 'fatherName'),
{ key: 'fatherName', label: '父亲姓名', type: 'customerSearch', role: 'father' }
{ key: 'fatherName', label: '父亲姓名', type: 'customerSearch', role: 'father', hiddenSources: ['research_donation'] }
)
assert.deepEqual(
customer.fields.find((field) => field.key === 'motherEthnic'),
{ key: 'motherEthnic', label: '母亲民族', type: 'select', optionKey: 'ethnicities', searchable: true }
{
key: 'motherEthnic',
label: '母亲民族',
type: 'select',
optionKey: 'ethnicities',
searchable: true,
hiddenSources: ['research_donation']
}
)
const donationHiddenParents = [
'motherPhone', 'motherIdCard', 'motherEmail', 'motherBirthYm', 'motherEthnic',
'motherAddress', 'fatherName', 'fatherPhone', 'fatherIdCard'
]
for (const key of donationHiddenParents) {
assert.deepEqual(
customer.fields.find((field) => field.key === key).hiddenSources,
['research_donation']
)
}
for (const key of ['motherName', 'newbornName', 'newbornSex', 'expectedDeliveryDate', 'newbornDeliveryTime', 'newbornIdCard', 'deliveryMode', 'birthOrder', 'newbornWeightGram', 'apgar1min']) {
assert.equal(customer.fields.find((field) => field.key === key).hiddenSources, undefined)
}
assert.match(source('./components/MarketFormSection.vue'), /hiddenSources/)
const form = source('../views/market/MarketModuleForm.vue')
assert.match(form, /ProtocolSampleTransportPanel/)
assert.match(form, /buildProtocolSamplesPayload/)
@@ -716,7 +737,10 @@ test('protocol create and edit forms expose every writable web business field',
assert.match(form, /sampleContractSum/)
assert.match(form, /refreshSampleCodes/)
assert.match(form, /@refresh-sample-codes="refreshSampleCodes"/)
assert.match(form, /buildSampleCodeSearchParams/)
assert.match(form, /sampleCodeLoading/)
assert.match(form, /codeFilter/)
assert.match(form, /keyword/)
const samplePanel = source('../market/components/ProtocolSampleTransportPanel.vue')
assert.match(samplePanel, /添加样本/)
assert.match(samplePanel, /配置运费/)
@@ -734,6 +758,9 @@ test('protocol create and edit forms expose every writable web business field',
assert.match(samplePanel, /resolveSampleCodeFilter/)
assert.match(samplePanel, /refresh-sample-codes/)
assert.match(samplePanel, /sampleCodeFilter/)
assert.match(samplePanel, /remote/)
assert.match(samplePanel, /@search="onSampleCodeSearch"/)
assert.match(samplePanel, /buildSampleCodeSearchParams/)
const payPanel = source('../market/components/ProtocolSamplePaymentPanel.vue')
const transferEditor = source('../market/components/ScheduleTransferEditor.vue')
assert.match(payPanel, /applyPreferentialPriceToPayment/)
@@ -122,6 +122,8 @@ test('protocol adapters preserve nested detail sections', () => {
const protocolLabels = protocol.flatMap((section) => (section.fields || []).map((field) => field.label))
assert.ok(!protocolLabels.includes('协议编号'))
assert.ok(protocolLabels.includes('母亲姓名'))
assert.ok(protocolLabels.includes('父亲姓名'))
assert.ok(protocolLabels.includes('母亲联系电话'))
assert.ok(!protocolLabels.includes('新生儿评分(5分钟)'))
assert.equal(protocol.at(-1).key, 'statusChange')
})
+16 -10
View File
@@ -15,6 +15,12 @@ import {
} from '../status-tag-registry.js'
const SEX_LABELS = Object.freeze({ '1': '男', '0': '女' })
/** 科研捐赠客户信息只展示母亲姓名;这些父母字段仍参与提交,避免编辑时清空已有数据 */
const DONATION_HIDDEN_PARENT_KEYS = new Set([
'motherPhone', 'motherIdCard', 'motherEmail', 'motherBirthYm', 'motherEthnic',
'motherAddress', 'fatherName', 'fatherPhone', 'fatherIdCard'
])
const DONATION_PARENT_FIELD_HIDE = Object.freeze({ hiddenSources: ['research_donation'] })
const DELIVERY_LABELS = Object.freeze({ sc: '顺产', pgc: '剖宫产' })
const SAMPLE_STATUS_LABELS = Object.freeze({
ycj: '已采集', jcz: '检测中', yrk: '已入库', yzf: '已作废'
@@ -480,7 +486,7 @@ export default {
['deliveryMode', '分娩方式', { map: (item) => label(DELIVERY_LABELS, item) }],
['birthOrder', '胎次'], ['newbornWeightGram', '新生儿体重(克)'],
['apgar1min', '新生儿评分(1分钟)']
]),
].filter(([key]) => !isDonation || !DONATION_HIDDEN_PARENT_KEYS.has(key))),
cards: []
},
{
@@ -705,15 +711,15 @@ export default {
] },
{ key: 'customer', title: '客户信息', nested: true, fields: [
{ key: 'motherName', label: '母亲姓名', type: 'customerSearch', role: 'mother', required: true },
{ key: 'motherPhone', label: '联系电话', type: 'tel' },
{ key: 'motherIdCard', label: '母亲身份证号' },
{ key: 'motherEmail', label: '母亲邮箱' },
{ key: 'motherBirthYm', label: '母亲出生年月' },
{ key: 'motherEthnic', label: '母亲民族', type: 'select', optionKey: 'ethnicities', searchable: true },
{ key: 'motherAddress', label: '母亲家庭住址', type: 'textarea' },
{ key: 'fatherName', label: '父亲姓名', type: 'customerSearch', role: 'father' },
{ key: 'fatherPhone', label: '父亲电话', type: 'tel' },
{ key: 'fatherIdCard', label: '父亲身份证号' },
{ key: 'motherPhone', label: '联系电话', type: 'tel', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'motherIdCard', label: '母亲身份证号', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'motherEmail', label: '母亲邮箱', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'motherBirthYm', label: '母亲出生年月', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'motherEthnic', label: '母亲民族', type: 'select', optionKey: 'ethnicities', searchable: true, ...DONATION_PARENT_FIELD_HIDE },
{ key: 'motherAddress', label: '母亲家庭住址', type: 'textarea', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'fatherName', label: '父亲姓名', type: 'customerSearch', role: 'father', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'fatherPhone', label: '父亲电话', type: 'tel', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'fatherIdCard', label: '父亲身份证号', ...DONATION_PARENT_FIELD_HIDE },
{ key: 'newbornName', label: '新生儿姓名' },
{ key: 'newbornSex', label: '新生儿性别', type: 'tabs', options: [
{ label: '男', value: '1' }, { label: '女', value: '0' }
@@ -136,6 +136,13 @@ test('donation detail follows the web view and omits price and payment', () => {
assert.equal(sections.find((section) => section.key === 'payment'), undefined)
const attachmentLabels = sections.find((section) => section.key === 'attachments').fields.map((field) => field.label)
assert.deepEqual(attachmentLabels, ['知情同意书', '传染病检测报告', '其他附件'])
assert.deepEqual(
sections.find((section) => section.key === 'customer').fields.map((field) => field.label),
[
'母亲姓名', '新生儿姓名', '新生儿性别', '预产期', '新生儿分娩时间',
'新生儿身份证', '分娩方式', '胎次', '新生儿体重(克)', '新生儿评分(1分钟)'
]
)
})
test('donation detail shows audit history and convert-to-storage status history', () => {
@@ -63,6 +63,14 @@ export function calcFqInstallmentDueDate(
return base
}
/** 第 1 期 = 开始日,第 2 期 = 第 1 期 + 1 年,第 3 期起每期 + 半年 */
export function calcStandardFqDueDate(startDate, index) {
const start = String(startDate || '').replace(/-/g, '')
if (start.length !== 8 || !Number.isInteger(index) || index < 0) return ''
if (index === 0) return start
return addMonthsYmd(start, 12 + (index - 1) * 6)
}
export function defaultInstallmentAmounts(contractAmount, count) {
if (contractAmount == null || !count || count < 1) return []
const totalCents = moneyToMinorUnits(contractAmount)
@@ -2,6 +2,7 @@
import {
calcFqInstallmentDueDate,
calcStandardFqDueDate,
defaultInstallmentAmounts,
DEFAULT_INSTALLMENT_ADVANCE_DAYS
} from './protocol-payment-installment.js'
@@ -358,8 +359,12 @@ export function applyLumpSumDeadlineToLastDueDate(schedules, deadline, isLocked)
return schedules.map((s, i) => (i === lastIdx ? { ...s, dueDate: due } : { ...s }))
}
function scheduleDueLocked(schedule) {
return (schedule?.transfers || []).some((transfer) => transfer.payStatus === 'yjf')
}
/** 按缴费参数重建应缴批次(对齐 SamplePaymentPanel.rebuildSchedules 精简版) */
export function rebuildSampleSchedules(pay) {
export function rebuildSampleSchedules(pay, options = {}) {
const mode = pay.payMode || 'dj'
if (mode === 'dj') {
const prev = (pay.schedules || []).find((s) => Number(s.scheduleNo || 0) === 1)
@@ -381,9 +386,11 @@ export function rebuildSampleSchedules(pay) {
const interval = Number(pay.installmentIntervalMonths) || 0
const startDate = pay.installmentStartDate || ''
const advanceDays = Number(pay.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS)
if (!count || !interval || !startDate) {
const canBuild = mode === 'fq' ? Boolean(count && startDate) : Boolean(count && interval && startDate)
if (!canBuild) {
return { ...pay, schedules: [] }
}
const refreshDueDates = options.refreshDueDates === true
const prevByNo = new Map(
(pay.schedules || []).map((s) => [Number(s.scheduleNo || 0), s])
)
@@ -393,13 +400,22 @@ export function rebuildSampleSchedules(pay) {
const scheduleNo = i + 1
const prev = prevByNo.get(scheduleNo)
const prevAmt = prev?.dueAmount
const computedDue = mode === 'fq'
? calcStandardFqDueDate(startDate, i)
: calcFqInstallmentDueDate(startDate, i, interval, advanceDays) || ''
const locked = prev ? scheduleDueLocked(prev) : false
const dueDate = locked && prev?.dueDate
? String(prev.dueDate)
: !refreshDueDates && prev?.dueDate
? String(prev.dueDate)
: computedDue
return {
id: prev?.id,
scheduleNo,
dueAmount: prevAmt != null && compareMoney(prevAmt, 0) > 0
? normalizeMoney(prevAmt)
: null,
dueDate: prev?.dueDate || calcFqInstallmentDueDate(startDate, i, interval, advanceDays) || '',
dueDate,
scheduleStatus: prev?.scheduleStatus || 'open',
transfers: [...(prev?.transfers || [])]
}
@@ -12,8 +12,10 @@ import {
scheduleFinanceConfirmedPaidArrears,
shouldFollowProtocolTotal,
nextProtocolTotalFromSamples,
sumSampleContractAmounts
sumSampleContractAmounts,
rebuildSampleSchedules
} from './protocol-sample-payment.js'
import { calcStandardFqDueDate } from './protocol-payment-installment.js'
describe('decimal-safe totals', () => {
it('sums contracts and confirmed transfers in integer minor units', () => {
@@ -163,6 +165,65 @@ describe('applyPreferentialPriceToPayment', () => {
})
})
describe('calcStandardFqDueDate', () => {
it('builds six periods as start, +1 year, then +half year', () => {
assert.deepEqual(
[0, 1, 2, 3, 4, 5].map((index) => calcStandardFqDueDate('20261021', index)),
['20261021', '20271021', '20280421', '20281021', '20290421', '20291021']
)
})
it('clamps month-end when adding half a year', () => {
assert.equal(calcStandardFqDueDate('20260831', 2), '20280229')
})
})
describe('rebuildSampleSchedules fq due dates', () => {
it('fills installment dates from the start date', () => {
const next = rebuildSampleSchedules({
payMode: 'fq',
installmentCount: 6,
installmentStartDate: '20261021',
schedules: []
}, { refreshDueDates: true })
assert.deepEqual(
next.schedules.map((schedule) => schedule.dueDate),
['20261021', '20271021', '20280421', '20281021', '20290421', '20291021']
)
})
it('keeps a hand-edited date until the start date changes', () => {
const kept = rebuildSampleSchedules({
payMode: 'fq',
installmentCount: 2,
installmentStartDate: '20261021',
schedules: [
{ scheduleNo: 1, dueDate: '20261101', transfers: [] },
{ scheduleNo: 2, dueDate: '20271101', transfers: [] }
]
})
assert.equal(kept.schedules[0].dueDate, '20261101')
const refreshed = rebuildSampleSchedules({
...kept,
installmentStartDate: '20261101'
}, { refreshDueDates: true })
assert.deepEqual(refreshed.schedules.map((schedule) => schedule.dueDate), ['20261101', '20271101'])
})
it('keeps a finance-confirmed due date when the start date changes', () => {
const next = rebuildSampleSchedules({
payMode: 'fq',
installmentCount: 2,
installmentStartDate: '20261021',
schedules: [
{ scheduleNo: 1, dueDate: '20260101', transfers: [{ payStatus: 'yjf' }] }
]
}, { refreshDueDates: true })
assert.equal(next.schedules[0].dueDate, '20260101')
assert.equal(next.schedules[1].dueDate, '20271021')
})
})
describe('protocol total follow', () => {
it('shouldFollowProtocolTotal', () => {
assert.equal(shouldFollowProtocolTotal(null, 100), true)
@@ -57,6 +57,16 @@ export function applySampleTypeDefaults(sample, sampleType, { showPrice = true }
return next
}
/** 样本编码检索参数:类型前缀走 codeFilter,弹窗输入走 keyword,由后台过滤。 */
export function buildSampleCodeSearchParams({ codeFilter = '', keyword = '' } = {}) {
const filter = String(codeFilter || '').trim()
const query = String(keyword || '').trim()
const params = {}
if (filter) params.codeFilter = filter
if (query) params.keyword = query
return params
}
/** 从样本类型选项解析编码过滤文本(对齐 Web sampleCodeFilter → codeFilter) */
export function resolveSampleCodeFilter(sampleTypes, sampleTypeId) {
const id = String(sampleTypeId || '').trim()
@@ -93,6 +93,18 @@ test('resolveSampleCodeFilter reads type sampleCodeFilter for code list refresh'
assert.equal(resolveSampleCodeFilter(types, 'missing'), '')
})
test('buildSampleCodeSearchParams sends type prefix and typed keyword separately', () => {
const { buildSampleCodeSearchParams } = sampleTransport
assert.deepEqual(
buildSampleCodeSearchParams({ codeFilter: 'ZKZX', keyword: '34000987' }),
{ codeFilter: 'ZKZX', keyword: '34000987' }
)
assert.deepEqual(buildSampleCodeSearchParams({ codeFilter: ' ZKZX ', keyword: ' ' }), {
codeFilter: 'ZKZX'
})
assert.deepEqual(buildSampleCodeSearchParams({}), {})
})
test('sample type defaults preserve the maximum DECIMAL(18,2) value exactly', () => {
const next = applySampleTypeDefaults(
{ sampleCodeId: 'c1', sampleCode: 'S-1' },
@@ -64,9 +64,15 @@
class="oa-cmt__file"
type="file"
multiple
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.rar"
:accept="COMMENT_ATTACHMENT_ACCEPT"
@change="onUploadFiles"
/>
<HarmonyFileSource
v-if="useHarmonyPicker"
ref="harmonySourceRef"
:accept="COMMENT_ATTACHMENT_ACCEPT"
@pick="onHarmonyPick"
/>
<van-popup v-model:show="attachVisible" position="bottom" round teleport="body" :style="{ maxHeight: '72%' }">
<div class="oa-cmt-sheet">
@@ -145,8 +151,11 @@ import {
} from '../../api/oa.js'
import OaCommentFileCards from './OaCommentFileCards.vue'
import { attachmentFullUrl } from '../../utils/attachment.js'
import { isHarmonyWechat } from '../../utils/harmony-file-picker.js'
import HarmonyFileSource from '../../components/HarmonyFileSource.vue'
const IMAGE_EXT = /\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/i
const COMMENT_ATTACHMENT_ACCEPT = 'image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.rar'
const props = defineProps({
instanceId: { type: String, default: '' },
@@ -160,6 +169,10 @@ const picked = ref([])
const sending = ref(false)
const uploading = ref(false)
const fileInputRef = ref(null)
const harmonySourceRef = ref(null)
const useHarmonyPicker = isHarmonyWechat(
typeof navigator === 'undefined' ? '' : navigator.userAgent
)
const mentionUsers = ref([])
const mentionVisible = ref(false)
const mentionLoading = ref(false)
@@ -415,9 +428,18 @@ function removePicked(index) {
}
function pickUpload() {
if (uploading.value) return
if (useHarmonyPicker) {
harmonySourceRef.value?.open()
return
}
fileInputRef.value?.click()
}
function onHarmonyPick(files) {
onUploadFiles({ target: { files } })
}
async function onUploadFiles(ev) {
const input = ev.target
const files = Array.from(input?.files || [])
@@ -63,13 +63,8 @@
</div>
</van-popup>
<van-popup
v-model:show="createVisible"
position="bottom"
round
:style="{ height: '88%' }"
>
<div class="oa-recv__sheet">
<van-popup v-model:show="createVisible" position="bottom" round>
<div class="oa-recv__sheet oa-recv__sheet--create">
<div class="oa-recv__hd">
<span @click="createVisible = false">取消</span>
<strong>新增收款账户</strong>
@@ -82,84 +77,57 @@
</span>
</div>
<div class="oa-recv__form">
<van-field
:model-value="accountTypeLabel"
label="账户类型"
label-align="top"
input-align="left"
readonly
is-link
required
placeholder="请选择"
@click="typePickerVisible = true"
/>
<van-field
v-model="createForm.accountName"
label="户名"
label-align="top"
input-align="left"
required
maxlength="128"
placeholder="请输入户名"
/>
<van-field
v-model="createForm.accountNo"
label="账号"
label-align="top"
input-align="left"
required
maxlength="128"
placeholder="请输入账号"
/>
<div class="oa-recv-field">
<div class="oa-recv-field__label">账户类型<b>*</b></div>
<button type="button" class="oa-recv-field__box" @click="typePickerVisible = true">
<span class="oa-recv-field__value">{{ accountTypeLabel || '请选择' }}</span>
<van-icon class="oa-recv-field__arrow" name="arrow" />
</button>
</div>
<label class="oa-recv-field">
<span class="oa-recv-field__label">户名<b>*</b></span>
<span class="oa-recv-field__box">
<input v-model="createForm.accountName" maxlength="128" placeholder="请输入户名" />
</span>
</label>
<label class="oa-recv-field">
<span class="oa-recv-field__label">账号<b>*</b></span>
<span class="oa-recv-field__box">
<input v-model="createForm.accountNo" maxlength="128" placeholder="请输入账号" />
</span>
</label>
<template v-if="createForm.accountType !== 'alipay'">
<van-field
v-model="createForm.bankName"
label="银行"
label-align="top"
input-align="left"
required
maxlength="128"
placeholder="如:中国工商银行"
/>
<van-field
v-model="createForm.bankProvince"
label="银行所在省"
label-align="top"
input-align="left"
required
maxlength="64"
placeholder="如:安徽省"
/>
<van-field
v-model="createForm.bankCity"
label="银行所在市"
label-align="top"
input-align="left"
required
maxlength="64"
placeholder="如:合肥市"
/>
<van-field
v-model="createForm.bankBranch"
label="银行支行"
label-align="top"
input-align="left"
required
maxlength="256"
placeholder="请输入开户支行全称"
/>
<label class="oa-recv-field">
<span class="oa-recv-field__label">银行<b>*</b></span>
<span class="oa-recv-field__box">
<input v-model="createForm.bankName" maxlength="128" placeholder="如:中国工商银行" />
</span>
</label>
<label class="oa-recv-field">
<span class="oa-recv-field__label">银行所在省<b>*</b></span>
<span class="oa-recv-field__box">
<input v-model="createForm.bankProvince" maxlength="64" placeholder="如:安徽省" />
</span>
</label>
<label class="oa-recv-field">
<span class="oa-recv-field__label">银行所在市<b>*</b></span>
<span class="oa-recv-field__box">
<input v-model="createForm.bankCity" maxlength="64" placeholder="如:合肥市" />
</span>
</label>
<label class="oa-recv-field">
<span class="oa-recv-field__label">银行支行<b>*</b></span>
<span class="oa-recv-field__box">
<input v-model="createForm.bankBranch" maxlength="256" placeholder="请输入开户支行全称" />
</span>
</label>
</template>
<van-field
v-model="createForm.remark"
label="备注"
label-align="top"
input-align="left"
type="textarea"
rows="2"
autosize
maxlength="512"
placeholder="选填"
/>
<label class="oa-recv-field">
<span class="oa-recv-field__label">备注</span>
<span class="oa-recv-field__box oa-recv-field__box--area">
<textarea v-model="createForm.remark" maxlength="512" rows="2" placeholder="选填" />
</span>
</label>
</div>
</div>
</van-popup>
@@ -461,10 +429,102 @@ onMounted(() => {
font-weight: 600;
}
.oa-recv__sheet--create {
height: auto;
max-height: 88vh;
}
.oa-recv__form {
flex: 1;
overflow: auto;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
padding: 16px 16px calc(20px + env(safe-area-inset-bottom));
background: #f4faf7;
}
.oa-recv-field {
display: block;
margin-bottom: 14px;
}
.oa-recv-field__label {
display: block;
margin-bottom: 6px;
color: #3d524a;
font-size: 13px;
font-weight: 650;
line-height: 1.4;
}
.oa-recv-field__label b {
margin-left: 2px;
color: #d14343;
}
.oa-recv-field__box {
display: flex;
align-items: center;
width: 100%;
min-height: 48px;
margin: 0;
padding: 0 14px;
box-sizing: border-box;
border: 1px solid transparent;
border-radius: 12px;
background: #fff;
color: #0f291e;
font-family: inherit;
font-size: 16px;
font-weight: 500;
line-height: 1.4;
text-align: left;
appearance: none;
}
.oa-recv-field__box:focus-within {
border-color: var(--brand-green, #22c55e);
}
.oa-recv-field__box input,
.oa-recv-field__box textarea {
flex: 1;
width: 100%;
min-width: 0;
margin: 0;
padding: 0;
border: 0;
outline: none;
background: transparent;
color: inherit;
font: inherit;
}
.oa-recv-field__box input::placeholder,
.oa-recv-field__box textarea::placeholder {
color: #8aa396;
font-weight: 400;
}
.oa-recv-field__value {
flex: 1;
min-width: 0;
}
.oa-recv-field__arrow {
flex: none;
margin-left: 8px;
color: #8aa396;
font-size: 16px;
}
.oa-recv-field__box--area {
align-items: flex-start;
min-height: 88px;
padding: 12px 14px;
}
.oa-recv-field__box textarea {
min-height: 64px;
resize: none;
}
.oa-recv__ok.is-busy {
@@ -1,5 +1,5 @@
<template>
<div class="oa-upload" :class="{ 'is-readonly': isReadonly }">
<div class="oa-upload" :class="{ 'is-readonly': isReadonly, 'is-harmony-picker': useHarmonyPicker }">
<van-uploader
v-model="fileList"
:disabled="false"
@@ -15,7 +15,7 @@
multiple
@click-preview="onClickPreview"
>
<div class="oa-upload__add">
<div class="oa-upload__add" @click="onHarmonyAdd">
<div class="oa-upload__add-plus">+</div>
<div class="oa-upload__add-txt">添加</div>
</div>
@@ -50,6 +50,12 @@
</div>
</template>
</van-uploader>
<HarmonyFileSource
v-if="useHarmonyPicker && !isReadonly"
ref="harmonySourceRef"
:accept="accept"
@pick="onHarmonyPick"
/>
<div v-if="tipText" class="oa-upload__tip">
{{ tipText }}
</div>
@@ -63,6 +69,8 @@ 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'
import { isHarmonyWechat } from '../../utils/harmony-file-picker.js'
import HarmonyFileSource from '../../components/HarmonyFileSource.vue'
defineOptions({ name: 'upload' })
@@ -220,6 +228,10 @@ function urlsToFileList(urls) {
}
const fileList = ref(urlsToFileList(toUrlList(props.modelValue)))
const useHarmonyPicker = isHarmonyWechat(
typeof navigator === 'undefined' ? '' : navigator.userAgent
)
const harmonySourceRef = ref(null)
watch(
() => props.modelValue,
@@ -260,6 +272,7 @@ async function onAfterRead(item) {
const rawUrl = String(res?.fileUrl || res?.data?.fileUrl || '').trim()
if (!rawUrl) throw new Error('未返回文件地址')
const fullUrl = attachmentFullUrl(rawUrl) || rawUrl
if (String(f.url || '').startsWith('blob:')) URL.revokeObjectURL(f.url)
const originalName = String(
(f.file && f.file.name) ||
res?.fileName ||
@@ -274,6 +287,7 @@ async function onAfterRead(item) {
f.status = 'done'
f.isString = true
} catch (e) {
if (String(f.url || '').startsWith('blob:')) URL.revokeObjectURL(f.url)
f.status = 'failed'
showToast(e?.message || '上传失败')
}
@@ -287,6 +301,30 @@ function onBeforeDelete() {
return true
}
function onHarmonyAdd(event) {
if (!useHarmonyPicker || isReadonly.value) return
event.preventDefault()
event.stopPropagation()
harmonySourceRef.value?.open()
}
function onHarmonyPick(files) {
const limit = Number(props.limit) || 9
const room = Math.max(0, limit - fileList.value.length)
const batch = files.slice(0, room)
if (batch.length < files.length) showToast(`最多上传${limit}个附件`)
if (!batch.length) return
const items = batch.map((file) => ({
file,
name: file.name,
status: 'uploading',
isImage: String(file.type || '').startsWith('image/'),
url: String(file.type || '').startsWith('image/') ? URL.createObjectURL(file) : ''
}))
fileList.value = [...fileList.value, ...items]
onAfterRead(items)
}
async function onClickPreview(item) {
if (isImageFileItem(item)) return
const url = String(item?.url || '').trim()
@@ -311,6 +349,10 @@ async function onClickPreview(item) {
.oa-upload {
padding: 4px 12px 8px;
}
.oa-upload.is-harmony-picker :deep(.van-uploader__input) {
pointer-events: none;
}
.oa-upload__tip {
margin-top: 6px;
color: var(--text-muted, #6b7b74);
+6
View File
@@ -1252,6 +1252,12 @@ body.van-toast--unclickable .presign-order-page * {
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}
.market-form-field__control--text {
display: flex;
align-items: center;
padding: 0 16px;
}
.market-form-field > .van-field:focus-within,
.market-form-field__control:focus-within {
background: #fff;
@@ -0,0 +1,89 @@
/** 鸿蒙微信 web-view 里,扩展名 accept 点不开系统文件选择器。 */
export const HARMONY_FILE_SOURCES = Object.freeze([
Object.freeze({ name: '拍照', value: 'camera' }),
Object.freeze({ name: '从相册选择', value: 'album' }),
Object.freeze({ name: '从文件选择', value: 'file' })
])
const IMAGE_MIME_EXTENSIONS = {
'image/jpeg': ['jpg', 'jpeg'],
'image/png': ['png'],
'image/webp': ['webp'],
'image/gif': ['gif'],
'image/bmp': ['bmp'],
'image/heic': ['heic'],
'image/heif': ['heif']
}
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'heic', 'heif']
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'ogg', 'mov', 'avi']
export function isHarmonyWechat(userAgent) {
const ua = String(userAgent || '')
return /MicroMessenger/i.test(ua) && /ArkWeb|OpenHarmony/i.test(ua)
}
export function harmonyFileInputAttrs(source) {
if (source === 'camera') {
return { accept: 'image/*', capture: 'environment', multiple: false }
}
if (source === 'album') {
return { accept: 'image/*', capture: undefined, multiple: true }
}
if (source === 'file') {
return { accept: '', capture: undefined, multiple: true }
}
return null
}
function acceptTokens(accept) {
return String(accept || '')
.split(',')
.map((token) => token.trim().toLowerCase())
.filter(Boolean)
}
function fileExtension(file) {
const match = String(file?.name || '').match(/\.([^.]+)$/)
return match ? match[1].toLowerCase() : ''
}
export function isAttachmentTypeAllowed(file, accept) {
const tokens = acceptTokens(accept)
if (!tokens.length) return true
const extension = fileExtension(file)
const mime = String(file?.type || '').trim().toLowerCase()
if (extension && tokens.includes(`.${extension}`)) return true
if (mime && tokens.includes(mime)) return true
if (mime.startsWith('image/') && tokens.includes('image/*')) return true
if (mime.startsWith('video/') && tokens.includes('video/*')) return true
const implied = IMAGE_MIME_EXTENSIONS[mime] || []
if (implied.some((item) => tokens.includes(`.${item}`))) return true
if (!mime && tokens.includes('image/*') && IMAGE_EXTENSIONS.includes(extension)) return true
if (!mime && tokens.includes('video/*') && VIDEO_EXTENSIONS.includes(extension)) return true
return false
}
export function attachmentRejectionMessage(file, { accept = '', maxBytes = 0 } = {}) {
const limit = Number(maxBytes) || 0
if (limit > 0 && Number(file?.size) > limit) {
return `单个附件不能超过${Math.floor(limit / 1024 / 1024)}MB`
}
if (!isAttachmentTypeAllowed(file, accept)) return '不支持该附件类型'
return ''
}
export function collectAcceptedAttachments(files, options) {
const accepted = []
let message = ''
for (const file of files || []) {
const reason = attachmentRejectionMessage(file, options)
if (reason) {
if (!message) message = reason
continue
}
accepted.push(file)
}
return { accepted, message }
}
@@ -0,0 +1,142 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import {
HARMONY_FILE_SOURCES,
attachmentRejectionMessage,
collectAcceptedAttachments,
harmonyFileInputAttrs,
isHarmonyWechat
} from './harmony-file-picker.js'
const HARMONY_WECHAT_UA =
'Mozilla/5.0 (Phone; OpenHarmony 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile MicroMessenger/8.0.12'
const HUAWEI_ANDROID_WECHAT_UA =
'Mozilla/5.0 (Linux; Android 12; HarmonyOS; NOH-AN00) AppleWebKit/537.36 Chrome/99.0.4844.88 Mobile Safari/537.36 MicroMessenger/8.0.30'
const IPHONE_WECHAT_UA =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.42'
test('detects HarmonyOS WeChat by ArkWeb or OpenHarmony plus MicroMessenger', () => {
assert.equal(isHarmonyWechat(HARMONY_WECHAT_UA), true)
assert.equal(
isHarmonyWechat(
'Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 Mobile MicroMessenger/8.0.5'
),
true
)
assert.equal(isHarmonyWechat(HUAWEI_ANDROID_WECHAT_UA), false)
assert.equal(isHarmonyWechat(IPHONE_WECHAT_UA), false)
assert.equal(
isHarmonyWechat(
'Mozilla/5.0 (Phone; OpenHarmony 6.0) AppleWebKit/537.36 ArkWeb/4.1.6.1 Mobile'
),
false
)
assert.equal(isHarmonyWechat(''), false)
})
test('offers camera, album, and system file sources', () => {
assert.deepEqual(
HARMONY_FILE_SOURCES.map((item) => item.name),
['拍照', '从相册选择', '从文件选择']
)
assert.deepEqual(
HARMONY_FILE_SOURCES.map((item) => item.value),
['camera', 'album', 'file']
)
})
test('maps each source to the file input that Harmony WeChat can open', () => {
assert.deepEqual(harmonyFileInputAttrs('camera'), {
accept: 'image/*',
capture: 'environment',
multiple: false
})
assert.deepEqual(harmonyFileInputAttrs('album'), {
accept: 'image/*',
capture: undefined,
multiple: true
})
assert.deepEqual(harmonyFileInputAttrs('file'), {
accept: '',
capture: undefined,
multiple: true
})
assert.equal(harmonyFileInputAttrs('other'), null)
})
test('keeps files whose extension or image mime is on the original accept list', () => {
const accept = '.jpg,.jpeg,.png,.pdf,.doc,.docx'
assert.equal(attachmentRejectionMessage(file('a.PDF', 'application/pdf'), { accept }), '')
assert.equal(attachmentRejectionMessage(file('a.jpg', ''), { accept }), '')
assert.equal(
attachmentRejectionMessage(file('camera', 'image/jpeg'), { accept }),
''
)
assert.equal(
attachmentRejectionMessage(file('clip.gif', 'image/gif'), { accept }),
'不支持该附件类型'
)
assert.equal(
attachmentRejectionMessage(file('note.txt', 'text/plain'), { accept }),
'不支持该附件类型'
)
})
test('treats image and video wildcards as their mime families', () => {
const accept = 'image/*,video/*,.pdf'
assert.equal(attachmentRejectionMessage(file('a.png', 'image/png'), { accept }), '')
assert.equal(attachmentRejectionMessage(file('a.mov', 'video/quicktime'), { accept }), '')
assert.equal(attachmentRejectionMessage(file('a.pdf', ''), { accept }), '')
assert.equal(
attachmentRejectionMessage(file('a.zip', 'application/zip'), { accept }),
'不支持该附件类型'
)
})
test('rejects oversized files before type and reports one message for a mixed batch', () => {
const accept = '.jpg,.pdf'
const maxBytes = 10 * 1024 * 1024
assert.equal(
attachmentRejectionMessage(file('a.pdf', 'application/pdf', maxBytes), { accept, maxBytes }),
''
)
assert.equal(
attachmentRejectionMessage(file('a.pdf', 'application/pdf', maxBytes + 1), { accept, maxBytes }),
'单个附件不能超过10MB'
)
const result = collectAcceptedAttachments(
[
file('bad.txt', 'text/plain', 1),
file('ok.pdf', 'application/pdf', 4),
file('big.pdf', 'application/pdf', maxBytes + 1)
],
{ accept, maxBytes }
)
assert.deepEqual(result.accepted.map((item) => item.name), ['ok.pdf'])
assert.equal(result.message, '不支持该附件类型')
})
function file(name, type, size = 1) {
return { name, type, size }
}
test('market and oa attachment entries share the harmony source sheet', async () => {
const [cards, upload, comment, source] = await Promise.all([
readFile(new URL('../market/components/MarketAttachmentCards.vue', import.meta.url), 'utf8'),
readFile(new URL('../oa/components/OaUploadField.vue', import.meta.url), 'utf8'),
readFile(new URL('../oa/components/OaCommentComposer.vue', import.meta.url), 'utf8'),
readFile(new URL('../components/HarmonyFileSource.vue', import.meta.url), 'utf8')
])
for (const content of [cards, upload, comment]) {
assert.match(content, /HarmonyFileSource/)
assert.match(content, /isHarmonyWechat/)
}
assert.match(cards, /<van-uploader/)
assert.match(cards, /v-if="editable && !useHarmonyPicker"/)
assert.match(upload, /pointer-events:\s*none/)
assert.match(source, /input\.click\(\)/)
assert.match(source, /collectAcceptedAttachments/)
assert.match(source, /harmonyFileInputAttrs/)
})
@@ -59,6 +59,7 @@
:market-role="context.marketRole"
:sample-types="formOptions.sampleTypes || []"
:sample-codes="formOptions.sampleCodes || []"
:sample-code-loading="sampleCodeLoading"
:transport-modes="formOptions.transportModes || []"
:disabled="fieldsLocked"
@update:model-value="onSamplesChange"
@@ -237,7 +238,10 @@ import ProtocolSamplePaymentPanel from '../../market/components/ProtocolSamplePa
import ProtocolV1PaymentPanel from '../../market/components/ProtocolV1PaymentPanel.vue'
import ProtocolSampleTransportPanel from '../../market/components/ProtocolSampleTransportPanel.vue'
import { applyProtocolCreateDefaults, todayYmd } from '../../market/protocol-form-defaults.js'
import { buildProtocolSamplesPayload } from '../../market/protocol-sample-transport.js'
import {
buildProtocolSamplesPayload,
buildSampleCodeSearchParams
} from '../../market/protocol-sample-transport.js'
import {
createPendingMarketAttachment,
dropPendingMarketAttachment,
@@ -339,6 +343,7 @@ let loadController = null
let sampleOptionsController = null
let cascadeOptionsController = null
let sampleCodeOptionsController = null
const sampleCodeLoading = ref(false)
let submitController = null
const uploadControllers = new Set()
let allowLeave = false
@@ -449,19 +454,19 @@ function onSamplesChange(value) {
dirty.value = true
}
/** 按样本类型 sampleCodeFilter 重拉可选编码(对齐 Web codeFilter) */
async function refreshSampleCodes({ codeFilter } = {}) {
/** 按样本类型前缀和弹窗输入重拉可选编码(后台 keyword 检索) */
async function refreshSampleCodes({ codeFilter, keyword } = {}) {
if (context.value.module !== 'protocol') return
sampleCodeOptionsController?.abort()
const controller = new AbortController()
sampleCodeOptionsController = controller
const filter = String(codeFilter || '').trim()
sampleCodeLoading.value = true
try {
const options = await fetchMarketFormOptions(
context.value,
{
...(id.value ? { businessId: id.value } : {}),
...(filter ? { codeFilter: filter } : {})
...buildSampleCodeSearchParams({ codeFilter, keyword })
},
{ signal: controller.signal }
)
@@ -481,6 +486,8 @@ async function refreshSampleCodes({ codeFilter } = {}) {
) {
showToast(marketErrorMessage(error, '样本编码加载失败'))
}
} finally {
if (sampleCodeOptionsController === controller) sampleCodeLoading.value = false
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+137 -125
View File
@@ -1,11 +1,11 @@
# Graph Report - src (2026-10-19)
# Graph Report - src (2026-10-21)
## Corpus Check
- Large corpus: 1037 files · ~689,168 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 1044 files · ~691,775 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 3836 nodes · 5595 edges · 61 communities detected
- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 1336 edges (avg confidence: 0.8)
- 3856 nodes · 5622 edges · 63 communities detected
- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 1344 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
@@ -52,29 +52,31 @@
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 49|Community 49]]
- [[_COMMUNITY_Community 48|Community 48]]
- [[_COMMUNITY_Community 50|Community 50]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 51|Community 51]]
- [[_COMMUNITY_Community 53|Community 53]]
- [[_COMMUNITY_Community 54|Community 54]]
- [[_COMMUNITY_Community 55|Community 55]]
- [[_COMMUNITY_Community 56|Community 56]]
- [[_COMMUNITY_Community 57|Community 57]]
- [[_COMMUNITY_Community 59|Community 59]]
- [[_COMMUNITY_Community 60|Community 60]]
- [[_COMMUNITY_Community 61|Community 61]]
- [[_COMMUNITY_Community 63|Community 63]]
- [[_COMMUNITY_Community 68|Community 68]]
- [[_COMMUNITY_Community 71|Community 71]]
- [[_COMMUNITY_Community 73|Community 73]]
- [[_COMMUNITY_Community 89|Community 89]]
- [[_COMMUNITY_Community 92|Community 92]]
- [[_COMMUNITY_Community 93|Community 93]]
- [[_COMMUNITY_Community 64|Community 64]]
- [[_COMMUNITY_Community 70|Community 70]]
- [[_COMMUNITY_Community 74|Community 74]]
- [[_COMMUNITY_Community 78|Community 78]]
- [[_COMMUNITY_Community 91|Community 91]]
- [[_COMMUNITY_Community 94|Community 94]]
- [[_COMMUNITY_Community 95|Community 95]]
## God Nodes (most connected - your core abstractions)
1. `get()` - 345 edges
1. `get()` - 347 edges
2. `push()` - 128 edges
3. `Number()` - 102 edges
3. `Number()` - 106 edges
4. `emit()` - 35 edges
5. `WebSocketClient` - 26 edges
6. `set()` - 23 edges
@@ -93,29 +95,29 @@
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/operations/exhibition-appointment/index.vue → utils/fullscreen-loading.ts
- `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED]
views/basic-info/cell-sample-type/index.vue → utils/fullscreen-loading.ts
views/system/tenant/index.vue → utils/fullscreen-loading.ts
## Communities
### Community 0 - "Community 0"
Cohesion: 0.0
Nodes (225): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+217 more)
Cohesion: 0.01
Nodes (195): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+187 more)
### Community 1 - "Community 1"
Cohesion: 0.02
Nodes (136): fetchMarkQcMaterialUsageFullyUsed(), if(), Number(), applyPreferentialContractForV1Mode(), createEmptyTransport(), datetimeToYmd(), defaultInstallmentStartDateYmd(), if() (+128 more)
Nodes (145): formatTodayDate(), if(), Number(), addPaymentDetail(), applyPreferentialContractForV1Mode(), createEmptyTransport(), datetimeToYmd(), defaultInstallmentStartDateYmd() (+137 more)
### Community 2 - "Community 2"
Cohesion: 0.02
Nodes (109): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), buildTemporaryViewSections(), buildInsertedSiblingCard(), fetchMineTasksForWorkOrders(), resolveShowProtocolPaymentAtGroup() (+101 more)
Nodes (109): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCollectionRegionList(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchCreateCollectionRegion(), fetchCreateHospital() (+101 more)
### Community 3 - "Community 3"
Cohesion: 0.02
Nodes (87): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchProductionTaskList() (+79 more)
Nodes (96): fetchUpdatePostBoundRoles(), fetchUploadAttachment(), FireworkSystem, handleReportUpload(), handleReportUpload(), walk(), branchConditionLines(), collectConditionReferencedFields() (+88 more)
### Community 4 - "Community 4"
Cohesion: 0.02
Nodes (94): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), fetchUpdatePostBoundRoles() (+86 more)
Nodes (89): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), markPilotCheckupChecked() (+81 more)
### Community 5 - "Community 5"
Cohesion: 0.02
@@ -123,272 +125,282 @@ Nodes (73): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(),
### Community 6 - "Community 6"
Cohesion: 0.03
Nodes (80): fetchUploadAttachment(), FireworkSystem, handleReportUpload(), handleReportUpload(), walk(), async(), addDispatchItem(), createDispatchItem() (+72 more)
Nodes (64): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchReassignProductionTask() (+56 more)
### Community 7 - "Community 7"
Cohesion: 0.02
Nodes (80): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchCreateItemUnit(), fetchDeleteItemUnit(), fetchItemUnitList() (+72 more)
Cohesion: 0.03
Nodes (39): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+31 more)
### Community 8 - "Community 8"
Cohesion: 0.03
Nodes (34): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator, RouteRegistry, RouteTransformer, useAppMode() (+26 more)
Nodes (69): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchCreateItemUnit(), fetchCreateMenu(), fetchCreateTenant(), fetchDeleteCellSampleType(), fetchDeleteItemUnit(), fetchDeleteMenu() (+61 more)
### Community 9 - "Community 9"
Cohesion: 0.03
Nodes (54): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), if(), chipTitle(), confirmPicker() (+46 more)
Nodes (63): fetchOaRelatedSummaries(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), if(), chipTitle() (+55 more)
### Community 10 - "Community 10"
Cohesion: 0.04
Nodes (55): handleBlur(), handleFocus(), handleInput(), fetchCreatePaymentTransfer(), fetchDeletePaymentTransfer(), fetchPaymentChaseList(), fetchPaymentTransferList(), fetchPaymentTransferReceiptConfirmedList() (+47 more)
Cohesion: 0.03
Nodes (58): fetchDictDataOptions(), fetchAttachmentDownloadUrl(), formatProtocolAmount(), handleAttachmentPreview(), resolveFileUrl(), showStepContent(), visibleFields(), qcAttachmentFullUrl() (+50 more)
### Community 11 - "Community 11"
Cohesion: 0.03
Nodes (52): fetchAttachmentDownloadUrl(), formatProtocolAmount(), handleAttachmentPreview(), resolveFileUrl(), showStepContent(), visibleFields(), qcAttachmentFullUrl(), qcDownloadAttachment() (+44 more)
Nodes (59): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTemporaryViewSections(), buildTransportSection(), resolveInboundProtocolStatus(), buildInsertedSiblingCard() (+51 more)
### Community 12 - "Community 12"
Cohesion: 0.04
Nodes (70): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), openForDraft(), remoteSearchHandoverUsers(), applySchema(), flowNeedsInitiatorSelect() (+62 more)
Cohesion: 0.02
Nodes (29): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllEquipments(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents() (+21 more)
### Community 13 - "Community 13"
Cohesion: 0.04
Nodes (49): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), collectIds(), buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections() (+41 more)
Cohesion: 0.03
Nodes (58): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), fetchMineTasksForWorkOrders(), useInboundSiblingNavigation(), shouldApplyTriggerSourceFilter(), useQcWorkOrderTabPage() (+50 more)
### Community 14 - "Community 14"
Cohesion: 0.04
Nodes (46): buildBaseFields(), buildFeeSection(), buildInboundBasicSection(), buildInboundCellStorageProductionViewSections(), buildInboundResearchDonationProductionViewSections(), buildOutboundBasicExperimentProductionViewSections(), buildOutboundClinicalProductionViewSections(), buildOutboundDetail() (+38 more)
Nodes (70): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), openForDraft(), remoteSearchHandoverUsers(), applySchema(), flowNeedsInitiatorSelect() (+62 more)
### Community 15 - "Community 15"
Cohesion: 0.04
Nodes (57): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchCreateRegionStaff(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchDeleteRegionStaff(), fetchHospitalList() (+49 more)
Nodes (55): handleBlur(), handleFocus(), handleInput(), fetchCreatePaymentTransfer(), fetchDeletePaymentTransfer(), fetchPaymentChaseList(), fetchPaymentTransferList(), fetchPaymentTransferReceiptConfirmedList() (+47 more)
### Community 16 - "Community 16"
Cohesion: 0.05
Nodes (53): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), fetchUserInfo() (+45 more)
Cohesion: 0.04
Nodes (45): buildBaseFields(), buildInboundBasicSection(), buildInboundCellStorageProductionViewSections(), buildInboundResearchDonationProductionViewSections(), buildOutboundBasicExperimentProductionViewSections(), buildOutboundClinicalProductionViewSections(), buildOutboundDetail(), buildOutboundPilotProductionViewSections() (+37 more)
### Community 17 - "Community 17"
Cohesion: 0.04
Nodes (18): fetchProductionWorkOrderQcInfo(), fetchSampleInsurance(), fetchSampleStorageCert(), fetchUpdateSampleStorageCert(), dash(), fmtMoney(), auditorDisplay(), dash() (+10 more)
### Community 18 - "Community 18"
Cohesion: 0.04
Nodes (40): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+32 more)
Cohesion: 0.05
Nodes (34): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+26 more)
### Community 19 - "Community 19"
Cohesion: 0.05
Nodes (56): fetchOaRelatedSummaries(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls() (+48 more)
Nodes (38): fetchProductionTaskList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), buildPayload(), ensureDefaultHandler(), initDialog(), loadAssignees() (+30 more)
### Community 20 - "Community 20"
Cohesion: 0.04
Nodes (41): fetchCreateExhibitionAppointment(), fetchDeleteExhibitionAppointment(), fetchExhibitionAppointmentList(), fetchUpdateExhibitionAppointment(), fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList() (+33 more)
Cohesion: 0.06
Nodes (47): buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls(), buildFlowItems() (+39 more)
### Community 21 - "Community 21"
Cohesion: 0.05
Nodes (34): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+26 more)
Nodes (46): completePilotApplication(), deletePilotApplication(), fetchCreateExhibitionAppointment(), fetchDeleteExhibitionAppointment(), fetchExhibitionAppointmentList(), fetchPilotApplicationList(), fetchUpdateExhibitionAppointment(), startPilotApplication() (+38 more)
### Community 22 - "Community 22"
Cohesion: 0.05
Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more)
### Community 23 - "Community 23"
Cohesion: 0.06
Nodes (25): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+17 more)
### Community 24 - "Community 24"
Cohesion: 0.1
Nodes (2): WebSocketClient, TableCache
### Community 24 - "Community 24"
### Community 25 - "Community 25"
Cohesion: 0.08
Nodes (33): fetchRefreshToken(), toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url() (+25 more)
### Community 26 - "Community 26"
Cohesion: 0.09
Nodes (35): formatDueDate(), formatDueDate(), buildPaymentFlowSteps(), dash(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit() (+27 more)
### Community 27 - "Community 27"
Cohesion: 0.06
Nodes (21): fetchEthnicList(), fetchSampleFreightDetail(), fetchSampleFreightList(), fetchSampleFreightSampleOptions(), fetchUpdateSampleFreight(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList() (+13 more)
### Community 28 - "Community 28"
Cohesion: 0.09
Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
### Community 25 - "Community 25"
### Community 29 - "Community 29"
Cohesion: 0.08
Nodes (22): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderTabStepCounts(), fetchProductionWorkOrderTabStepCountsDeduped() (+14 more)
### Community 26 - "Community 26"
Cohesion: 0.08
Nodes (20): createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments(), serializeEquipmentIds() (+12 more)
### Community 30 - "Community 30"
Cohesion: 0.07
Nodes (19): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+11 more)
### Community 27 - "Community 27"
### Community 31 - "Community 31"
Cohesion: 0.09
Nodes (22): fetchOaAppList(), fetchOaGroupList(), fetchPublishOaApp(), loadData(), loadGroups(), resetForm(), onSuccess(), goBack() (+14 more)
### Community 28 - "Community 28"
### Community 32 - "Community 32"
Cohesion: 0.11
Nodes (25): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), clearTableSelection(), handleBatchComplete(), handleBatchStart() (+17 more)
Nodes (21): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), fetchUserInfo(), buildWebEncryptedLoginBody() (+13 more)
### Community 29 - "Community 29"
### Community 33 - "Community 33"
Cohesion: 0.09
Nodes (16): fetchEthnicList(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), loadEthnicOptions(), normalizeSampleRow(), resetForm() (+8 more)
Nodes (12): fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload(), reloadTabCounts() (+4 more)
### Community 30 - "Community 30"
Cohesion: 0.07
Nodes (11): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), financeAttachmentsToUrlString(), ensureTabMounted() (+3 more)
### Community 31 - "Community 31"
### Community 34 - "Community 34"
Cohesion: 0.11
Nodes (12): fetchCollectionProtocolList(), handleBatchDelete(), handleDelete(), handleDropdownCommand(), handleEdit(), handleReset(), handleSearch(), loadData() (+4 more)
### Community 32 - "Community 32"
### Community 35 - "Community 35"
Cohesion: 0.1
Nodes (12): fetchOutboundReleaseMaterial(), fetchOutboundReleaseReviewDetail(), fetchOutboundReleaseReviewList(), fetchReleaseReviewList(), handleReset(), handleSearch(), handleTabChange(), handleToggleSelect() (+4 more)
### Community 33 - "Community 33"
Cohesion: 0.13
Nodes (19): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchDeleteCellSampleType(), fetchUpdateCellSampleType(), handleAdd(), handleBatchDelete(), handleDelete(), handleDropdownCommand() (+11 more)
### Community 34 - "Community 34"
### Community 36 - "Community 36"
Cohesion: 0.12
Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
### Community 35 - "Community 35"
### Community 37 - "Community 37"
Cohesion: 0.2
Nodes (14): applyFlagToRule(), applySchema(), countRules(), getActiveRule(), getDesigner(), getSetupState(), onMessage(), readFlag() (+6 more)
### Community 36 - "Community 36"
### Community 38 - "Community 38"
Cohesion: 0.16
Nodes (16): fetchCreateTenant(), fetchDeleteTenant(), fetchTenantList(), fetchUpdateTenant(), handleAdd(), handleDelete(), handleDropdownCommand(), handleEdit() (+8 more)
Nodes (14): fetchCreateEnvDetectItem(), fetchDeleteEnvDetectItem(), fetchEnvDetectItemDetail(), fetchEnvDetectItemList(), fetchEnvDetectItemsActive(), fetchUpdateEnvDetectItem(), handleAdd(), handleBatchDelete() (+6 more)
### Community 37 - "Community 37"
### Community 39 - "Community 39"
Cohesion: 0.12
Nodes (6): setupOaConfirmationKit(), ensureOaKitMenu(), setupOaLeaveKit(), setupOaOvertimeKit(), setupOaResignationKit(), setupOaTripKit()
### Community 38 - "Community 38"
### Community 40 - "Community 40"
Cohesion: 0.16
Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
### Community 39 - "Community 39"
### Community 41 - "Community 41"
Cohesion: 0.27
Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
### Community 40 - "Community 40"
Cohesion: 0.33
Nodes (13): buildProtocolPaymentFlowSteps(), dash(), financeReceiptTime(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit() (+5 more)
### Community 41 - "Community 41"
### Community 42 - "Community 42"
Cohesion: 0.19
Nodes (6): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData(), switchTab()
### Community 42 - "Community 42"
### Community 43 - "Community 43"
Cohesion: 0.24
Nodes (10): buildPrintDocumentHtml(), cleanupPrintIframe(), embedSopImagesAsDataUrls(), getSopA4ContentHeightPx(), getSopA4EditorHeightPx(), normalizeSopCheckboxes(), prepareSopHtmlForPrint(), printSopHtml() (+2 more)
### Community 44 - "Community 44"
Cohesion: 0.26
Nodes (8): fetchSampleStorageCertQc(), saveSampleStorageCertQc(), applyDetail(), handleSave(), loadDetail(), resetFormKeys(), resolveStorageCertQcTypeKey(), supportsStorageCertQc()
### Community 45 - "Community 45"
Cohesion: 0.25
Nodes (2): handleTouchEnd(), setTransition()
### Community 49 - "Community 49"
### Community 48 - "Community 48"
Cohesion: 0.25
Nodes (3): companyProgressTagType(), companyProgressText(), companyGroupProgress()
### Community 50 - "Community 50"
Cohesion: 0.39
Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
### Community 50 - "Community 50"
### Community 51 - "Community 51"
Cohesion: 0.36
Nodes (1): TableHeightCalculator
### Community 52 - "Community 52"
### Community 53 - "Community 53"
Cohesion: 0.33
Nodes (2): logApiCryptoBootInfo(), maskSecret()
### Community 53 - "Community 53"
### Community 54 - "Community 54"
Cohesion: 0.38
Nodes (4): handleFocus(), handleInput(), parseDecimalToNumber(), sanitizeDecimalInput()
### Community 54 - "Community 54"
### Community 55 - "Community 55"
Cohesion: 0.29
Nodes (1): fetchQcMonthTemplateList()
### Community 56 - "Community 56"
Cohesion: 0.4
Nodes (2): isUnfinishedQcWorkOrder(), resolveQcWorkOrderUnfinishedBadgeCount()
### Community 55 - "Community 55"
### Community 57 - "Community 57"
Cohesion: 0.5
Nodes (2): parseEquipmentIds(), syncEquipmentNamesFromOptions()
### Community 57 - "Community 57"
### Community 59 - "Community 59"
Cohesion: 0.5
Nodes (2): focusInput(), handleKeydown()
### Community 60 - "Community 60"
Cohesion: 0.4
Nodes (1): fetchQcMaterialAmountAuthUsers()
### Community 63 - "Community 63"
Cohesion: 0.67
Nodes (2): isCurrentPending(), isNotReached()
### Community 61 - "Community 61"
### Community 64 - "Community 64"
Cohesion: 0.67
Nodes (2): if(), logSidebarMenu()
### Community 63 - "Community 63"
Cohesion: 0.5
Nodes (1): fetchStorageEntrySmsList()
### Community 68 - "Community 68"
### Community 70 - "Community 70"
Cohesion: 1.0
Nodes (2): getSettingDefaults(), resetToDefaults()
### Community 71 - "Community 71"
### Community 74 - "Community 74"
Cohesion: 1.0
Nodes (2): copyTextToClipboard(), copyTextWithFallback()
### Community 73 - "Community 73"
### Community 78 - "Community 78"
Cohesion: 1.0
Nodes (2): getSourceProtocolAttachment(), normalizeAttachmentFiles()
### Community 89 - "Community 89"
### Community 91 - "Community 91"
Cohesion: 1.0
Nodes (2): emptyTabCounts(), useProductionWorkOrderTabStepCounts()
### Community 92 - "Community 92"
### Community 94 - "Community 94"
Cohesion: 1.0
Nodes (2): initialProgress(), useQcWorkOrderAttachmentExport()
### Community 93 - "Community 93"
### Community 95 - "Community 95"
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 24`** (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 45`** (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 50`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
- **Thin community `Community 51`** (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 52`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
- **Thin community `Community 53`** (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 54`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts`
- **Thin community `Community 55`** (7 nodes): `fetchConfirmQcMonthImport()`, `fetchCreateQcMonthTemplate()`, `fetchDeleteQcMonthTemplate()`, `fetchPreviewQcMonthImport()`, `fetchQcMonthTemplateList()`, `fetchUpdateQcMonthTemplate()`, `qc-material-month-template.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 55`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts`
- **Thin community `Community 56`** (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 57`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
- **Thin community `Community 57`** (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 60`** (4 nodes): `isCurrentPending()`, `isNotReached()`, `mergeProgressWithComments()`, `oa-progress-merge-comments.ts`
- **Thin community `Community 59`** (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 61`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
- **Thin community `Community 60`** (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 63`** (4 nodes): `fetchGenerateStorageEntrySms()`, `fetchStorageEntrySmsList()`, `fetchToggleStorageEntrySmsNotified()`, `storage-entry-sms-notification.ts`
- **Thin community `Community 63`** (4 nodes): `isCurrentPending()`, `isNotReached()`, `mergeProgressWithComments()`, `oa-progress-merge-comments.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 68`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
- **Thin community `Community 64`** (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 71`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts`
- **Thin community `Community 70`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 73`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts`
- **Thin community `Community 74`** (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 89`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
- **Thin community `Community 78`** (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 92`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
- **Thin community `Community 91`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 93`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.ts`
- **Thin community `Community 94`** (3 nodes): `initialProgress()`, `useQcWorkOrderAttachmentExport()`, `useQcWorkOrderAttachmentExport.ts`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 95`** (3 nodes): `emptyTabCounts()`, `useQcWorkOrderTabUnfinishedCounts()`, `useQcWorkOrderTabUnfinishedCounts.ts`
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 `push()` connect `Community 6` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 26`, `Community 27`?**
_High betweenness centrality (0.276) - 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 7`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 25`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 36`, `Community 41`, `Community 44`, `Community 63`?**
_High betweenness centrality (0.258) - this node is a cross-community bridge._
- **Why does `Number()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 26`, `Community 29`, `Community 39`, `Community 40`, `Community 53`?**
_High betweenness centrality (0.159) - this node is a cross-community bridge._
- **Are the 344 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
_`get()` has 344 INFERRED edges - model-reasoned connections that need verification._
- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `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 19`, `Community 21`, `Community 22`, `Community 23`, `Community 27`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 38`, `Community 42`, `Community 55`, `Community 60`?**
_High betweenness centrality (0.282) - this node is a cross-community bridge._
- **Why does `push()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 24`, `Community 25`, `Community 26`, `Community 29`, `Community 31`, `Community 33`?**
_High betweenness centrality (0.235) - this node is a cross-community bridge._
- **Why does `Number()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 9`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 23`, `Community 25`, `Community 26`, `Community 27`, `Community 30`, `Community 41`, `Community 54`?**
_High betweenness centrality (0.133) - this node is a cross-community bridge._
- **Are the 346 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?**
_`get()` has 346 INFERRED edges - model-reasoned connections that need verification._
- **Are the 127 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?**
_`push()` has 127 INFERRED edges - model-reasoned connections that need verification._
- **Are the 97 inferred relationships involving `Number()` (e.g. with `slotOrderNum()` and `isSlotAvailable()`) actually correct?**
_`Number()` has 97 INFERRED edges - model-reasoned connections that need verification._
- **Are the 101 inferred relationships involving `Number()` (e.g. with `slotOrderNum()` and `isSlotAvailable()`) actually correct?**
_`Number()` has 101 INFERRED edges - model-reasoned connections that need verification._
- **Are the 34 inferred relationships involving `emit()` (e.g. with `updateSamples()` and `notifyChanged()`) actually correct?**
_`emit()` has 34 INFERRED edges - model-reasoned connections that need verification._
+4 -4
View File
@@ -57,12 +57,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">3836 nodes &middot; 5595 edges &middot; 399 communities</div>
<div id="stats">3856 nodes &middot; 5622 edges &middot; 403 communities</div>
</div>
<script>
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 52, "community_name": "Community 52", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 52, "community_name": "Community 52", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 52, "community_name": "Community 52", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 52, "community_name": "Community 52", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 52, "community_name": "Community 52", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 52, "community_name": "Community 52", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#BAB0AC", "border": "#BAB0AC", "highlight": {"background": "#ffffff", "border": "#BAB0AC"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 139, "community_name": "Community 139", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 140, "community_name": "Community 140", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 34, "community_name": "Community 34", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 34, "community_name": "Community 34", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 34, "community_name": "Community 34", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 34, "community_name": "Community 34", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 34, "community_name": "Community 34", "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": 608}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 189}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 167}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 146}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 138}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 129}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 126}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 119}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 102}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 88}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 86}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 85}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 84}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 83}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 79}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 73}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 70}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 68}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 68}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 67}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 63}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 61}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 57}, {"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": 35}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 35}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 33}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 30}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 29}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 28}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 24}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 24}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 23}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 22}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 19}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 19}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 17}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 17}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 16}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 14}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 14}, {"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": 12}, {"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": 8}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 8}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 8}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 7}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 7}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 6}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 5}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 5}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 5}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 4}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 4}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 4}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 4}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 4}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 4}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 4}, {"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": 3}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 3}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 3}, {"cid": 70, "color": "#4E79A7", "label": "CommuniLine truncated
const RAW_NODES = [{"id": "app_vue", "label": "App.vue", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "App.vue", "community": 53, "community_name": "Community 53", "source_file": "App.vue", "file_type": "code", "degree": 3}, {"id": "src_app_refreshbuttonautoicons", "label": "refreshButtonAutoIcons()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshButtonAutoIcons()", "community": 53, "community_name": "Community 53", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "src_app_refreshpaneltitleicons", "label": "refreshPanelTitleIcons()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "refreshPanelTitleIcons()", "community": 53, "community_name": "Community 53", "source_file": "App.vue", "file_type": "code", "degree": 1}, {"id": "main_ts", "label": "main.ts", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "main.ts", "community": 53, "community_name": "Community 53", "source_file": "main.ts", "file_type": "code", "degree": 4}, {"id": "src_main_masksecret", "label": "maskSecret()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "maskSecret()", "community": 53, "community_name": "Community 53", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "src_main_logapicryptobootinfo", "label": "logApiCryptoBootInfo()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "logApiCryptoBootInfo()", "community": 53, "community_name": "Community 53", "source_file": "main.ts", "file_type": "code", "degree": 2}, {"id": "env_d_ts", "label": "env.d.ts", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "env.d.ts", "community": 140, "community_name": "Community 140", "source_file": "env.d.ts", "file_type": "code", "degree": 0}, {"id": "shims_loose_d_ts", "label": "shims-loose.d.ts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.0, "font": {"size": 0, "color": "#ffffff"}, "title": "shims-loose.d.ts", "community": 141, "community_name": "Community 141", "source_file": "shims-loose.d.ts", "file_type": "code", "degree": 0}, {"id": "directives_index_ts", "label": "index.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.1, "font": {"size": 0, "color": "#ffffff"}, "title": "index.ts", "community": 36, "community_name": "Community 36", "source_file": "directives/index.ts", "file_type": "code", "degree": 1}, {"id": "directives_index_setupglobdirectives", "label": "setupGlobDirectives()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "setupGlobDirectives()", "community": 36, "community_name": "Community 36", "source_file": "directives/index.ts", "file_type": "code", "degree": 5}, {"id": "directives_core_roles_ts", "label": "roles.ts", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "roles.ts", "community": 36, "community_name": "Community 36", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 3}, {"id": "core_roles_checkrolepermission", "label": "checkRolePermission()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "checkRolePermission()", "community": 36, "community_name": "Community 36", "source_file": "directives/core/roles.ts", "file_type": "code", "degree": 2}, {"id": "core_roles_removeelement", "label": "removeElement()", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 10.2, "font": {"size": 0, "color": "#ffffff"}, "title": "removeElement()", "community": 36, "community_name": "Community 36", "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": 511}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 203}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 158}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 145}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 131}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 129}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 110}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 110}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 99}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 97}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 97}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 94}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 92}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 90}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 84}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 83}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 78}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 68}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 61}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 60}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 58}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 57}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 57}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 46}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 42}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 41}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 41}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 39}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 37}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 35}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 35}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 33}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 30}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 25}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 24}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 24}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 22}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 19}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 19}, {"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": 13}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 13}, {"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": 8}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 8}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 8}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 7}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 7}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 7}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 6}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 5}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 5}, {"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": 4}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 4}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 4}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 4}, {"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": 3}, {"cid": 70, "color": "#4E79A7", "label": "CommunitLine truncated
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
function esc(s) {
+25 -18
View File
@@ -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": 1792377603.7593923,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1792376809.475621,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1792481775.0412781,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1792481757.2811291,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1787887312.735774,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1787887320.8486114,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/composables/useAttachmentActions.ts": 1788162977.6691713,
@@ -45,6 +45,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-menu-badge.ts": 1782396392.0954244,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sample-production-reception-fields.test.ts": 1788159504.2955287,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/patch-el-select-filterable.ts": 1781272556.3791182,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sf-express-freight.test.ts": 1792581160.5268056,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-sample-payment.test.ts": 1791874229.7030468,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/storage-slot-range-fill.ts": 1785235070.1226468,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/storage-slot-options.ts": 1785235070.1219137,
@@ -64,6 +65,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-trip-kit-runtime.ts": 1785412937.5821183,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-form-create-overtime-kit.ts": 1785488982.6957128,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-approval-print-related.ts": 1785145511.865531,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sf-express-settlement-export.test.ts": 1792584730.6241236,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/presign-save-auto-submit.ts": 1792075222.9989974,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-material-ui.ts": 1781525352.6976836,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sop-print.ts": 1787899876.3465168,
@@ -82,6 +84,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/production-mine-task-groups.ts": 1785209209.7549982,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-mine-task-groups.ts": 1786958777.7790954,
"/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/protocol-payment-installment.test.ts": 1792573372.611052,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/expected-cell-count.ts": 1782705039.2515786,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-company-task-group.test.ts": 1788330359.6029022,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sample-freight-flatten.ts": 1791468382.3859549,
@@ -104,6 +107,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/storage-cert-qc-fields.ts": 1791520662.1349554,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/void-type-dict.ts": 1788315481.8114185,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/qc-work-order-attachment-export.ts": 1782962844.3685946,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sf-express-settlement-export.ts": 1792584841.7560863,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-receive-account-value.ts": 1785390441.8286228,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/datetime-display.ts": 1791016843.1695142,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-form-value-display.test.ts": 1786364545.4796336,
@@ -139,6 +143,7 @@
"/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": 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/sf-express-freight.ts": 1792581599.4926653,
"/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,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/oa-person-value.ts": 1785384573.025628,
@@ -193,7 +198,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-sample-payment.ts": 1791874236.114397,
"/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": 1791914298.8769357,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-payment-installment.ts": 1792573345.9845612,
"/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,
@@ -319,10 +324,10 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-video-player/index.vue": 1782893851.5118508,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-cutter-img/index.vue": 1782893851.4950457,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/TinyMceEditor/index.vue": 1787643214.0412707,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-transport-form-section/index.vue": 1792145332.1622937,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-transport-form-section/index.vue": 1792581600.0417428,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-attachment-mini-readonly/index.vue": 1785145511.689019,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/cell-product-lineage-qc-dialog/index.vue": 1785290453.1924367,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-freight-pick-table/index.vue": 1791540667.8527935,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-freight-pick-table/index.vue": 1792581783.8595502,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/index.vue": 1785145511.6893718,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/finance-attachments.ts": 1781272556.6256514,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1787107329.207847,
@@ -367,7 +372,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaPersonSelect.vue": 1785388355.4534001,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaOvertimeKit.vue": 1786374594.648936,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaApprovalPrintSheet.vue": 1786413858.4258535,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue": 1785391747.3254373,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue": 1792581468.1034913,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaAppIcon.vue": 1791328376.3074796,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaLeaveKit.vue": 1785398262.9313874,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/components/oa-approval/OaPrintRelatedTree.vue": 1786413860.094557,
@@ -412,7 +417,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/leave.ts": 1781272556.654047,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception-check-item.ts": 1781272556.654672,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production.ts": 1785145511.6866825,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-info-handle.ts": 1792377682.8071346,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-info-handle.ts": 1792377877.9787571,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-task-set.ts": 1788231763.140203,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/infusion-schedule-handover.ts": 1781272556.6558568,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/outbound-release-review.ts": 1785303420.050366,
@@ -440,7 +445,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/customer-service-dict.ts": 1791860298.5249445,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/ethnic.ts": 1781272556.6658764,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation-sample-collection.ts": 1791187765.9137378,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-freight.ts": 1791955262.6401658,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-freight.ts": 1792581425.5253823,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-lab-test-item.ts": 1781272556.666375,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts": 1785290097.6330564,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-sms-notification.ts": 1781272556.666838,
@@ -448,6 +453,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/oaApproval.ts": 1792141011.8097997,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material-stock-check.ts": 1781427280.4992144,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation.ts": 1792199726.4842353,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sf-express-settlement.ts": 1792592146.3890388,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task.ts": 1783327864.9737554,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-item-param.ts": 1782446790.5508618,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/env-detect-item.ts": 1781272556.668322,
@@ -464,23 +470,24 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/protocol-sample-payment.ts": 1792075236.7229176,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1791862690.2468123,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1789715240.9777095,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sf-express-settlement/index.vue": 1792592149.369939,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/presign-order/index.vue": 1792202745.7217324,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue": 1792143951.183733,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1782893854.3402505,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/index.vue": 1792029084.1036322,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue": 1792029084.6662757,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/FreightEditDialog.vue": 1791424830.3864272,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/index.vue": 1792582237.8665264,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue": 1792581433.4896936,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-freight/FreightEditDialog.vue": 1792581440.7957807,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/my-material-usage/index.vue": 1791296731.262585,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1792202737.8532119,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue": 1782894806.0188534,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordDialog.vue": 1792202738.902946,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1792202724.60167,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue": 1792582243.533744,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue": 1791162227.1000674,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue": 1792143951.1766114,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue": 1792078851.1718132,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue": 1792573421.4305565,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue": 1782893854.3675368,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/index.vue": 1791187848.6336563,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/SampleInfoQuickRegisterDialog.vue": 1792377727.9778452,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/SampleInfoQuickRegisterDialog.vue": 1792377877.350023,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/UnifiedSampleInfoPage.vue": 1791868912.5231571,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-info/QuickRegisterAttachmentField.vue": 1792377594.7399898,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/sample-reception-info/index.vue": 1791200162.920515,
@@ -512,14 +519,14 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/sample-info/index.vue": 1791187849.9149377,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/index.vue": 1792143951.182839,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationAuditDialog.vue": 1782893853.885547,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1792199835.0448852,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue": 1792582244.2224164,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationFormDialog.vue": 1792143951.1814156,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue": 1782894808.1240149,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/dashboard/console/index.vue": 1791855531.6714394,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/tenant/index.vue": 1791328376.332648,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/order/index.vue": 1791328448.0895212,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/unit/index.vue": 1791328376.3340044,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/role/index.vue": 1791328448.0916607,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/role/index.vue": 1792548300.6470034,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/post/index.vue": 1791328376.334926,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/category/index.vue": 1791328448.086823,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/express/index.vue": 1791328376.3359313,
@@ -540,7 +547,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dept/index.vue": 1791328403.042935,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dict/index.vue": 1791328376.3420467,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/leave/index.vue": 1791328448.0883634,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/oa-approval/receive-accounts/index.vue": 1791328376.3427362,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/oa-approval/receive-accounts/index.vue": 1792580894.563632,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/oa-approval/drafts/index.vue": 1791296731.2786758,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/oa-approval/start/index.vue": 1790671876.9827359,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/oa-approval/mentions/index.vue": 1791296731.2806265,
@@ -782,7 +789,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/RoutePermissionValidator.ts": 1781272559.4235668,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/index.ts": 1781272559.424284,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/IframeRouteManager.ts": 1781272559.425215,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1791860293.80043,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1792580898.1361918,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/index.ts": 1781272559.5419338,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/exception.ts": 1781272559.549901,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/router/routes/staticRoutes.ts": 1781272559.596198,
@@ -0,0 +1,23 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { calcStandardFqDueDate } from './protocol-payment-installment'
describe('calcStandardFqDueDate', () => {
it('builds six periods as start, +1 year, then +half year', () => {
const start = '20261021'
assert.deepEqual(
[0, 1, 2, 3, 4, 5].map((i) => calcStandardFqDueDate(start, i)),
['20261021', '20271021', '20280421', '20281021', '20290421', '20291021']
)
})
it('clamps month-end when adding half a year', () => {
assert.equal(calcStandardFqDueDate('20260831', 2), '20280229')
assert.equal(calcStandardFqDueDate('20260831', 1), '20270831')
})
it('returns empty for an invalid start date', () => {
assert.equal(calcStandardFqDueDate('', 0), '')
assert.equal(calcStandardFqDueDate('2026-10-21', -1), '')
})
})
@@ -215,6 +215,30 @@ export function calcFqInstallmentDueDate(
return base
}
/** 按日历加月,月末超出时落到该月最后一天 */
function addCalendarMonthsYmd(dateStr: string, months: number): string {
const compact = String(dateStr || '').replace(/-/g, '')
if (compact.length !== 8 || !Number.isFinite(months)) return ''
const y = Number(compact.slice(0, 4))
const monthIndex = Number(compact.slice(4, 6)) - 1 + Math.trunc(months)
const ny = y + Math.floor(monthIndex / 12)
const nm = monthIndex % 12
const lastDay = new Date(ny, nm + 1, 0).getDate()
const nd = Math.min(Number(compact.slice(6, 8)), lastDay)
return `${ny}${String(nm + 1).padStart(2, '0')}${String(nd).padStart(2, '0')}`
}
/**
* 分期默认应缴日(与每期间隔、提前天数无关):
* 第 1 期 = 开始日,第 2 期 = 第 1 期 + 1 年,第 3 期起每期在上一期基础上 + 半年。
*/
export function calcStandardFqDueDate(startDate: string, index: number): string {
const start = String(startDate || '').replace(/-/g, '')
if (start.length !== 8 || !Number.isInteger(index) || index < 0) return ''
if (index === 0) return start
return addCalendarMonthsYmd(start, 12 + (index - 1) * 6)
}
export function calcInstallmentDueAmounts(
contractAmount: number | null | undefined,
count: number
@@ -359,6 +359,7 @@
import {
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
calcFqInstallmentDueDate,
calcStandardFqDueDate,
normalizeInstallmentAmounts
} from '@/utils/protocol-payment-installment'
import {
@@ -607,7 +608,27 @@
return [...(scheduleByNo(pay, scheduleNo)?.transfers || [])]
}
function rebuildSchedules(pay: Record<string, any>): Record<string, any> {
function todayYmd(): string {
const now = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
}
function computedScheduleDueDate(
mode: string,
startDate: string,
index: number,
interval: number,
advanceDays: number
): string {
if (mode === 'fq') return calcStandardFqDueDate(startDate, index)
return calcFqInstallmentDueDate(startDate, index, interval, advanceDays) || ''
}
function rebuildSchedules(
pay: Record<string, any>,
options?: { refreshDueDates?: boolean }
): Record<string, any> {
const mode = pay.payMode || 'dj'
if (mode === 'dj') {
const prev = scheduleByNo(pay, 1)
@@ -626,6 +647,7 @@
}
}
// fq / fgd / djfq:按期数生成批次;金额无模板时留空由用户手填
// 分期应缴日:第 1 期=开始日,第 2 期=+1 年,其后每期 +半年
const count = Number(pay.installmentCount) || 0
const interval = Number(pay.installmentIntervalMonths) || 0
const startDate = pay.installmentStartDate || ''
@@ -640,9 +662,12 @@
amounts = Array.from({ length: count }, () => null)
}
}
if (!count || !interval || !startDate) {
const canBuild =
mode === 'fq' ? Boolean(count && startDate) : Boolean(count && interval && startDate)
if (!canBuild) {
return { ...pay, installmentRatios: hasManualAmounts ? amounts : [], schedules: [] }
}
const refreshDueDates = options?.refreshDueDates === true
return {
...pay,
installmentRatios: hasManualAmounts ? amounts : [],
@@ -654,14 +679,19 @@
!hasManualAmounts && prevAmt != null && Number(prevAmt) > 0
? Number(prevAmt)
: null
const computedDue = computedScheduleDueDate(mode, startDate, i, interval, advanceDays)
const locked = prev ? isScheduleDueLocked(prev) : false
const dueDate =
locked && prev?.dueDate
? String(prev.dueDate)
: !refreshDueDates && prev?.dueDate
? String(prev.dueDate)
: computedDue
return {
id: prev?.id,
scheduleNo,
dueAmount: hasManualAmounts ? amounts[i] : keepPrev,
dueDate:
prev?.dueDate ||
calcFqInstallmentDueDate(startDate, i, interval, advanceDays) ||
'',
dueDate,
scheduleStatus: prev?.scheduleStatus || 'open',
transfers: transfersForSchedule(pay, scheduleNo)
}
@@ -675,10 +705,15 @@
emit('update:modelValue', [...(props.modelValue || [])])
}
function patchPay(pIdx: number, patch: Record<string, any>, rebuild = false) {
function patchPay(
pIdx: number,
patch: Record<string, any>,
rebuild = false,
rebuildOptions?: { refreshDueDates?: boolean }
) {
const list = [...(props.modelValue || [])]
let next = { ...list[pIdx], ...patch }
if (rebuild) next = rebuildSchedules(next)
if (rebuild) next = rebuildSchedules(next, rebuildOptions)
list[pIdx] = next
emit('update:modelValue', list)
}
@@ -794,11 +829,14 @@
patch.installmentIntervalMonths = 1
patch.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
patch.installmentRatios = []
if (mode === 'fq' && !String(prev.installmentStartDate || '').trim()) {
patch.installmentStartDate = todayYmd()
}
} else {
patch.installmentCount = null
patch.installmentRatios = []
}
patchPay(pIdx, patch, true)
patchPay(pIdx, patch, true, { refreshDueDates: mode === 'fq' })
}
function onContractAmountChange(pIdx: number, v: number | null) {
@@ -817,7 +855,11 @@
}
function onFqStartDateChange(pIdx: number, v: string) {
patchPay(pIdx, { installmentStartDate: v || '' }, true)
const start = v || ''
const mode = String((props.modelValue || [])[pIdx]?.payMode || '')
patchPay(pIdx, { installmentStartDate: start }, true, {
refreshDueDates: mode === 'fq' && !!start
})
}
function addTransfer(pIdx: number, sIdx: number) {
+17 -2
View File
@@ -256,6 +256,7 @@
<DialogFormBody :loading="menuDialogLoading">
<!-- 父子节点独立勾选:父级可不选子级、子级可不选父级(如页面权限与按钮权限分开配置) -->
<el-tree
:key="menuTreeRenderKey"
ref="menuTreeRef"
:data="menuTreeData"
show-checkbox
@@ -541,6 +542,7 @@
const menuTreeRef = ref()
const menuTreeData = ref<Api.Menu.MenuItem[]>([])
const menuCheckedKeys = ref<string[]>([])
const menuTreeRenderKey = ref(0)
const menuEditingRoleId = ref<string>('')
// ---- 数据权限弹窗 ----
@@ -822,14 +824,27 @@
}
// ---- 菜单配置 ----
/**
* el-tree 的 default-checked-keys 只会增量勾选,不会取消已勾节点。
* 连续打开不同角色时,必须重挂载树并用 setCheckedKeys 整表替换勾选。
*/
async function syncMenuTreeChecks(menuIds: string[]) {
menuCheckedKeys.value = [...menuIds]
menuTreeRenderKey.value += 1
await nextTick()
menuTreeRef.value?.setCheckedKeys?.(menuCheckedKeys.value)
}
async function handleMenuConfig(row: Api.Role.RoleItem) {
menuEditingRoleId.value = row.id
menuDialogVisible.value = true
menuDialogLoading.value = true
const pending = Promise.all([fetchMenuTree(), fetchRoleMenuIds(row.id)])
try {
const [tree, res] = await Promise.all([fetchMenuTree(), fetchRoleMenuIds(row.id)])
await syncMenuTreeChecks([])
const [tree, res] = await pending
menuTreeData.value = tree || []
menuCheckedKeys.value = res?.menuIds || []
await syncMenuTreeChecks(res?.menuIds || [])
} finally {
menuDialogLoading.value = false
}