提交“

This commit is contained in:
wuzilong committed 2026-10-19 18:28:02 +08:00
1 parent 5d9fb5ce19
commit 9177e1b7f1
231 files changed
+21207

No files matched your search

@@ -0,0 +1,28 @@
"""运输计划列表 / 归档 / 合同完善导出共用的段次×产品槽位。"""
from __future__ import annotations
from typing import Any, Optional
def format_stop_segment_label(kind: str, delivery_seq: Optional[int] = None) -> str:
if kind == "load":
return "装货"
return f"送货 #{delivery_seq or 1}"
def iter_segment_product_slots(load_read: Any, delivery_reads: Any):
"""按列表展平顺序产出 (kind, load, delivery, delivery_seq, product)。"""
if load_read is not None:
load_products = list(getattr(load_read, "products", None) or []) or [None]
for product in load_products:
yield ("load", load_read, None, None, product)
deliveries = list(delivery_reads or []) or ([None] if load_read is not None else [])
for index, delivery in enumerate(deliveries):
seq = getattr(delivery, "seq", None) if delivery is not None else (index + 1)
if seq is None:
seq = index + 1
products = list(getattr(delivery, "products", None) or []) if delivery else []
if not products:
products = [None]
for product in products:
yield ("delivery", None, delivery, seq, product)
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_plan_list_export_py", "label": "test_pl_plan_list_export.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L1"}, {"id": "tests_test_pl_plan_list_export_test_normalize_plan_export_columns_maps_list_props", "label": "test_normalize_plan_export_columns_maps_list_props()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L15"}, {"id": "tests_test_pl_plan_list_export_test_format_plan_list_export_brand_only_required", "label": "test_format_plan_list_export_brand_only_required()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L42"}, {"id": "tests_test_pl_plan_list_export_test_filter_export_columns_keeps_list_props_after_normalize", "label": "test_filter_export_columns_keeps_list_props_after_normalize()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L49"}, {"id": "tests_test_pl_plan_list_export_test_plan_list_export_filename_stem_by_stock_role", "label": "test_plan_list_export_filename_stem_by_stock_role()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L87"}, {"id": "tests_test_pl_plan_list_export_test_build_plan_list_export_rows_expands_products_with_crm_names", "label": "test_build_plan_list_export_rows_expands_products_with_crm_names()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L93"}, {"id": "tests_test_pl_plan_list_export_test_build_plan_list_export_rows_expands_stop_segments", "label": "test_build_plan_list_export_rows_expands_stop_segments()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L250"}, {"id": "tests_test_pl_plan_list_export_test_build_plan_list_export_rows_empty_delivery_products_placeholder", "label": "test_build_plan_list_export_rows_empty_delivery_products_placeholder()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L399"}, {"id": "tests_test_pl_plan_list_export_test_build_plan_list_export_rows_legacy_without_stops", "label": "test_build_plan_list_export_rows_legacy_without_stops()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L500"}, {"id": "tests_test_pl_plan_list_export_test_build_plan_list_export_rows_internal_customer_contract_placeholder", "label": "test_build_plan_list_export_rows_internal_customer_contract_placeholder()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L580"}, {"id": "tests_test_pl_plan_list_export_test_plan_list_merge_sheet_follows_list_rowspan", "label": "test_plan_list_merge_sheet_follows_list_rowspan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L694"}, {"id": "tests_test_pl_plan_list_export_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212\u5217\u8868\u5bfc\u51fa\uff1a\u4ea7\u54c1\u540d\u79f0 / \u54c1\u724c / \u522b\u540d / \u72b6\u6001\u4e2d\u6587 / \u5217\u8868\u5217\u5bf9\u9f50\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L1"}, {"id": "tests_test_pl_plan_list_export_rationale_251", "label": "\u4e00\u88c5\u591a\u9001\uff1a\u5148\u88c5\u8d27\u4ea7\u54c1\u884c\uff0c\u518d\u5404\u9001\u8d27\u6bb5\u4ea7\u54c1\u884c\uff1b\u4fa7\u680f\u5b57\u6bb5\u6309\u884c\u586b\u5199\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L251"}, {"id": "tests_test_pl_plan_list_export_rationale_501", "label": "\u65e0\u6bb5\u7ed3\u6784\uff1a\u56de\u9000\u8ba1\u5212\u7ea7\u4ea7\u54c1\uff1bstopSegment \u4e3a\u7a7a\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L501"}, {"id": "tests_test_pl_plan_list_export_rationale_581", "label": "\u5c71\u5ce1/\u4e91\u680b\u7f3a\u5408\u540c\uff1a\u88c5\u8d27\u884c\u5408\u540c\u4e3a\u51fa\u5e93\uff0c\u9001\u8d27\u884c\u4e3a\u5165\u5e93\uff08\u4e0e\u5217\u8868 UI \u4e00\u81f4\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_list_export.py", "source_location": "L581"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_tests_tLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_py", "label": "pl_plan_archive_export.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L1"}, {"id": "services_pl_plan_archive_export_unique_contract_nos", "label": "_unique_contract_nos()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L24"}, {"id": "services_pl_plan_archive_export_contract_nos_from_stop_links", "label": "_contract_nos_from_stop_links()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L35"}, {"id": "services_pl_plan_archive_export_format_stop_contract_export_label", "label": "format_stop_contract_export_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L59"}, {"id": "services_pl_plan_archive_export_load_contract_nos_by_plan", "label": "_load_contract_nos_by_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L73"}, {"id": "services_pl_plan_archive_export_resolve_archive_list_io_column", "label": "resolve_archive_list_io_column()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L128"}, {"id": "services_pl_plan_archive_export_format_date_value", "label": "_format_date_value()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L136"}, {"id": "services_pl_plan_archive_export_format_plate_display", "label": "_format_plate_display()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L142"}, {"id": "services_pl_plan_archive_export_get_stop_archive_actual_qty", "label": "_get_stop_archive_actual_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L152"}, {"id": "services_pl_plan_archive_export_get_stop_archive_unit_price", "label": "_get_stop_archive_unit_price()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L156"}, {"id": "services_pl_plan_archive_export_get_stop_archive_amount", "label": "_get_stop_archive_amount()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L160"}, {"id": "services_pl_plan_archive_export_stop_time", "label": "_stop_time()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L171"}, {"id": "services_pl_plan_archive_export_load_legacy_products_by_plan", "label": "_load_legacy_products_by_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L177"}, {"id": "services_pl_plan_archive_export_plan_common_export_fields", "label": "_plan_common_export_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L198"}, {"id": "services_pl_plan_archive_export_empty_pair_side_fields", "label": "_empty_pair_side_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L229"}, {"id": "services_pl_plan_archive_export_empty_product_identity_fields", "label": "_empty_product_identity_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L245"}, {"id": "services_pl_plan_archive_export_format_export_brand_name", "label": "_format_export_brand_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L256"}, {"id": "services_pl_plan_archive_export_resolve_export_product_display", "label": "_resolve_export_product_display()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L265"}, {"id": "services_pl_plan_archive_export_lookup_plan_product", "label": "_lookup_plan_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L290"}, {"id": "services_pl_plan_archive_export_product_identity_from_stop", "label": "_product_identity_from_stLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "label": "test_pl_plan_archive_export.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L1"}, {"id": "tests_test_pl_plan_archive_export_stop_product", "label": "_stop_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L17"}, {"id": "tests_test_pl_plan_archive_export_test_resolve_archive_list_io_column", "label": "test_resolve_archive_list_io_column()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L36"}, {"id": "tests_test_pl_plan_archive_export_bare_plan", "label": "_bare_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L49"}, {"id": "tests_test_pl_plan_archive_export_archive_rows", "label": "_archive_rows()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L73"}, {"id": "tests_test_pl_plan_archive_export_test_build_plan_archive_export_rows_segment_rows_and_single_side", "label": "test_build_plan_archive_export_rows_segment_rows_and_single_side()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L104"}, {"id": "tests_test_pl_plan_archive_export_fakeplan", "label": "_FakePlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L200"}, {"id": "tests_test_pl_plan_archive_export_test_plan_common_export_fields", "label": "test_plan_common_export_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L215"}, {"id": "tests_test_pl_plan_archive_export_test_build_plan_archive_export_rows_includes_brand_and_alias", "label": "test_build_plan_archive_export_rows_includes_brand_and_alias()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L227"}, {"id": "tests_test_pl_plan_archive_export_test_plan_common_export_fields_merges_link_contract_nos", "label": "test_plan_common_export_fields_merges_link_contract_nos()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L281"}, {"id": "tests_test_pl_plan_archive_export_test_build_plan_archive_export_rows_internal_customer_contract_per_segment", "label": "test_build_plan_archive_export_rows_internal_customer_contract_per_segment()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L290"}, {"id": "tests_test_pl_plan_archive_export_cellresolver", "label": "_CellResolver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L327"}, {"id": "tests_test_pl_plan_archive_export_cellresolver_row_to_cells", "label": ".row_to_cells()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L328"}, {"id": "tests_test_pl_plan_archive_export_merged_ranges", "label": "_merged_ranges()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L343"}, {"id": "tests_test_pl_plan_archive_export_test_prepare_single_delivery_puts_delivery_customer_on_first_row", "label": "test_prepare_single_delivery_puts_delivery_customer_on_first_row()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L347"}, {"id": "tests_test_pl_plan_archive_export_test_merge_sheet_matches_list_rowspan", "label": "test_merge_sheet_matches_list_rowspan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L389"}, {"id": "tests_test_pl_plan_archive_export_rationale_105", "label": "\u9ed8\u8ba4\u6309\u6bb5\u6b21\u00d7\u4ea7\u54c1\u5c55\u5f00\uff1a1 \u4e2a\u88c5\u8d27\u4ea7\u54c1 + 1 \u4e2a\u9001\u8d27\u4ea7\u54c1 = 2 \u884c\uff1b\u5355\u4fa7\u7b5b\u9009\u53ea\u7559\u5bf9\u5e94\u6bb5\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_plan_archive_export.py", "source_location": "L105"}, {"id": "tests_test_pl_plan_archive_export_rationale_291", "label": "\u5185\u90e8\u5ba2\u6237\u7f3a\u5408\u540c\uff1a\u88c5\u8d27\u884c\u5408\u540c\u4e3a\u51fa\u5e93\ufLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_export_attachment_zip_py", "label": "export_attachment_zip.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L1"}, {"id": "services_export_attachment_zip_sanitize_zip_segment", "label": "sanitize_zip_segment()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L28"}, {"id": "services_export_attachment_zip_short_hash", "label": "short_hash()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L34"}, {"id": "services_export_attachment_zip_guess_ext_from_url", "label": "guess_ext_from_url()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L38"}, {"id": "services_export_attachment_zip_build_attachment_filename", "label": "build_attachment_filename()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L49"}, {"id": "services_export_attachment_zip_first_row_no_by_entity_id", "label": "first_row_no_by_entity_id()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L56"}, {"id": "services_export_attachment_zip_build_hyperlink_formula", "label": "build_hyperlink_formula()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L71"}, {"id": "services_export_attachment_zip_parse_url_list", "label": "parse_url_list()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L77"}, {"id": "services_export_attachment_zip_urls_to_attachment_items", "label": "urls_to_attachment_items()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L92"}, {"id": "services_export_attachment_zip_default_download", "label": "_default_download()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L102"}, {"id": "services_export_attachment_zip_attach_field", "label": "_attach_field()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L108"}, {"id": "services_export_attachment_zip_plan_attachment_jobs", "label": "_plan_attachment_jobs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L112"}, {"id": "services_export_attachment_zip_build_xlsx_bytes", "label": "_build_xlsx_bytes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L155"}, {"id": "services_export_attachment_zip_build_attachment_zip_bytes", "label": "build_attachment_zip_bytes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L215"}, {"id": "services_export_attachment_zip_iter_attachment_zip_chunks", "label": "iter_attachment_zip_chunks()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L250"}, {"id": "services_export_attachment_zip_build_attachment_zip_streaming_response", "label": "build_attachment_zip_streaming_response()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L345"}, {"id": "services_export_attachment_zip_rationale_1", "label": "\u5217\u8868\u5bfc\u51fa\uff1aExcel \u603b\u8868 + \u9644\u4ef6\u76ee\u5f55 ZIP\uff08\u8ba1\u5212\u5f52\u6863 / \u5145\u503c\u7533\u8bf7\u7b49\u5171\u7528\uff09\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L1"}, {"id": "services_export_attachment_zip_rationale_95", "label": "\u5c06\u9017\u53f7/JSON URL \u5217\u8868\u8f6c\u4e3a {url, label} \u5217\u8868\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/export_attachment_zip.py", "source_location": "L95"}, {"id": "services_export_attachment_zip_rationale_119", "label": "\u9884\u5148\u786e\u5b9a\u9644\u4ef6 zip \u8def\u5f84\u4e0e\u8d85\u94fe\u63a5\uff08\u5c1a\u672a\u4e0b\u8f7d\uff09\u3002 \u8fd4\u56de: entity_file_meta: entity_id -> [(zip_pat", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_py", "label": "pl_plan_archive_export.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L1"}, {"id": "services_pl_plan_archive_export_unique_contract_nos", "label": "_unique_contract_nos()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L24"}, {"id": "services_pl_plan_archive_export_contract_nos_from_stop_links", "label": "_contract_nos_from_stop_links()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L35"}, {"id": "services_pl_plan_archive_export_format_stop_contract_export_label", "label": "format_stop_contract_export_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L59"}, {"id": "services_pl_plan_archive_export_load_contract_nos_by_plan", "label": "_load_contract_nos_by_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L73"}, {"id": "services_pl_plan_archive_export_resolve_archive_list_io_column", "label": "resolve_archive_list_io_column()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L128"}, {"id": "services_pl_plan_archive_export_format_date_value", "label": "_format_date_value()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L136"}, {"id": "services_pl_plan_archive_export_format_plate_display", "label": "_format_plate_display()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L142"}, {"id": "services_pl_plan_archive_export_get_stop_archive_actual_qty", "label": "_get_stop_archive_actual_qty()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L152"}, {"id": "services_pl_plan_archive_export_get_stop_archive_unit_price", "label": "_get_stop_archive_unit_price()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L156"}, {"id": "services_pl_plan_archive_export_get_stop_archive_amount", "label": "_get_stop_archive_amount()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L160"}, {"id": "services_pl_plan_archive_export_stop_time", "label": "_stop_time()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L171"}, {"id": "services_pl_plan_archive_export_load_legacy_products_by_plan", "label": "_load_legacy_products_by_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L177"}, {"id": "services_pl_plan_archive_export_plan_common_export_fields", "label": "_plan_common_export_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L198"}, {"id": "services_pl_plan_archive_export_empty_pair_side_fields", "label": "_empty_pair_side_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L229"}, {"id": "services_pl_plan_archive_export_empty_product_identity_fields", "label": "_empty_product_identity_fields()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L245"}, {"id": "services_pl_plan_archive_export_format_export_brand_name", "label": "_format_export_brand_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L256"}, {"id": "services_pl_plan_archive_export_resolve_export_product_display", "label": "_resolve_export_product_display()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L265"}, {"id": "services_pl_plan_archive_export_lookup_plan_product", "label": "_lookup_plan_product()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py", "source_location": "L290"}, {"id": "services_pl_plan_archive_export_product_identity_from_stop", "label": "_product_identity_from_stLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_resolve_list_product_display", "label": "_resolve_list_product_display()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L197"}, {"id": "pl_plan_resolve_create_date", "label": "_resolve_create_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L213"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L217"}, {"id": "pl_plan_format_subpack_slip_no", "label": "_format_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L249"}, {"id": "pl_plan_normalize_subpack_slip_no", "label": "_normalize_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L253"}, {"id": "pl_plan_subpack_slip_no_seq", "label": "_subpack_slip_no_seq()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L278"}, {"id": "pl_plan_ensure_subpack_slip_no", "label": "_ensure_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L286"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L318"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L323"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L332"}, {"id": "pl_plan_parse_status_csv", "label": "_parse_status_csv()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L352"}, {"id": "pl_plan_apply_follow_status_filter", "label": "_apply_follow_status_filter()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L358"}, {"id": "pl_plan_apply_exclude_follow_status_filter", "label": "_apply_exclude_follow_status_filter()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L367"}, {"id": "pl_plan_merge_query", "label": "_merge_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L376"}, {"id": "pl_plan_param_get", "label": "_param_get()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L386"}, {"id": "pl_plan_filters_from_params", "label": "_filters_from_params()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L393"}, {"id": "pl_plan_body_to_params", "label": "_body_to_params()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L506"}, {"id": "pl_plan_plancolumnfacetbody", "label": "PlanColumnFacetBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L517"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_plan_truthy_param", "label": "_truthy_param()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L530"}, {"id": "pl_plan_build_plan_facet_base_query", "label": "_build_plan_facet_base_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L538"}, {"id": "pl_plan_plan_column_facet_values", "label": "plan_column_facet_values()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L627"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L789"}, {"id": "pl_plan_normalize_plan_export_columns", "label": "_normalize_plan_export_columns()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_resolve_list_product_display", "label": "_resolve_list_product_display()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L196"}, {"id": "pl_plan_resolve_create_date", "label": "_resolve_create_date()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L212"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L216"}, {"id": "pl_plan_format_subpack_slip_no", "label": "_format_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L248"}, {"id": "pl_plan_normalize_subpack_slip_no", "label": "_normalize_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L252"}, {"id": "pl_plan_subpack_slip_no_seq", "label": "_subpack_slip_no_seq()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L277"}, {"id": "pl_plan_ensure_subpack_slip_no", "label": "_ensure_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L285"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L317"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L322"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L331"}, {"id": "pl_plan_parse_status_csv", "label": "_parse_status_csv()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L351"}, {"id": "pl_plan_apply_follow_status_filter", "label": "_apply_follow_status_filter()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L357"}, {"id": "pl_plan_apply_exclude_follow_status_filter", "label": "_apply_exclude_follow_status_filter()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L366"}, {"id": "pl_plan_merge_query", "label": "_merge_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L375"}, {"id": "pl_plan_param_get", "label": "_param_get()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L385"}, {"id": "pl_plan_filters_from_params", "label": "_filters_from_params()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L392"}, {"id": "pl_plan_body_to_params", "label": "_body_to_params()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L505"}, {"id": "pl_plan_plancolumnfacetbody", "label": "PlanColumnFacetBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L516"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_plan_truthy_param", "label": "_truthy_param()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L529"}, {"id": "pl_plan_build_plan_facet_base_query", "label": "_build_plan_facet_base_query()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L537"}, {"id": "pl_plan_plan_column_facet_values", "label": "plan_column_facet_values()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L626"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L788"}, {"id": "pl_plan_normalize_plan_export_columns", "label": "_normalize_plan_export_columns()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "Line truncated
@@ -0,0 +1,24 @@
"""CRM 产品读模型契约测试。"""
from __future__ import annotations
from app.crud.crm.product import _product_row_to_dict
from app.models.crm_models import CrmProduct
from app.schemas.crm.product import ProductCreateBody, ProductUpdateBody
def test_product_read_contract_exposes_creator_for_delete_permission():
row = CrmProduct(
id="product-1",
product_no="CP-001",
product_name="工业甲醇",
create_by="user-1",
)
payload = _product_row_to_dict(row)
assert payload["createBy"] == "user-1"
def test_product_creator_stays_read_only_in_write_schemas():
assert "create_by" not in ProductCreateBody.model_fields
assert "create_by" not in ProductUpdateBody.model_fields
@@ -0,0 +1,199 @@
"""合同完善导出:按段次×产品展开,缺口类型跟本行段。"""
from datetime import datetime
from app.api.pl.plan import (
CONTRACT_GAP_EXPORT_COLUMNS,
build_contract_gap_export_rows,
)
from app.schemas.pl.plan import (
PlanContractGapRead,
PlanContractLink,
PlanDeliveryRead,
PlanLoadRead,
PlanStopProductRead,
)
from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
NOW = datetime(2026, 10, 11, 0, 0, 0)
INTERNAL = next(iter(INTERNAL_CUSTOMER_NAMES))
def _stop_product(name: str, *, product_id: str) -> PlanStopProductRead:
return PlanStopProductRead(
id=f"sp-{product_id}",
planId="p1",
tenantNo="t1",
productId=product_id,
productNo=f"NO-{product_id}",
productName=name,
createTime=NOW,
updateTime=NOW,
)
def _load(*, customer_name: str, contracts: list, products: list) -> PlanLoadRead:
return PlanLoadRead(
id="load1",
planId="p1",
tenantNo="t1",
customerName=customer_name,
products=products,
contracts=contracts,
createTime=NOW,
updateTime=NOW,
)
def _delivery(
*,
stop_id: str,
seq: int,
customer_name: str,
delivery_time: str,
contracts: list,
products: list,
) -> PlanDeliveryRead:
return PlanDeliveryRead(
id=stop_id,
planId="p1",
tenantNo="t1",
seq=seq,
customerName=customer_name,
deliveryTime=delivery_time,
products=products,
contracts=contracts,
createTime=NOW,
updateTime=NOW,
)
def _gap_item() -> PlanContractGapRead:
return PlanContractGapRead(
id="p1",
planNo="PL001",
planDate="2026-10-11",
planType="direct",
followStatus="arranged",
supplierName="供方甲",
customerName="需方乙",
supplierContractExempt=False,
demanderContractExempt=False,
)
def test_contract_gap_export_columns_match_segment_rows():
assert [column.field for column in CONTRACT_GAP_EXPORT_COLUMNS] == [
"planNo",
"stopSegment",
"planDate",
"deliveryTimes",
"planType",
"followStatus",
"loadCustomer",
"deliveryCustomer",
"productName",
"supplierName",
"customerName",
"gapType",
]
assert [column.title for column in CONTRACT_GAP_EXPORT_COLUMNS] == [
"计划编号",
"段次",
"计划日期",
"送货时间",
"单据类型",
"跟进状态",
"装货客户",
"送货客户",
"产品名称",
"供方",
"需方",
"缺口类型",
]
def test_build_contract_gap_export_rows_expands_multi_segment_products():
"""装货缺合同 + 两送货段(一段有合同、一段两产品缺合同)展开为 4 行。"""
load = _load(
customer_name="外部装货客户",
contracts=[],
products=[_stop_product("甲醇", product_id="prod-load")],
)
delivery_ok = _delivery(
stop_id="d1",
seq=1,
customer_name="外部送货客户A",
delivery_time="2026-10-12",
contracts=[PlanContractLink(contractId="c1", contractNo="HT001")],
products=[_stop_product("甲醇", product_id="prod-d1")],
)
delivery_gap = _delivery(
stop_id="d2",
seq=2,
customer_name="外部送货客户B",
delivery_time="2026-10-13",
contracts=[],
products=[
_stop_product("乙醇", product_id="prod-d2a"),
_stop_product("丙酮", product_id="prod-d2b"),
],
)
rows = build_contract_gap_export_rows(
[_gap_item()],
{"p1": (load, [delivery_ok, delivery_gap])},
)
assert len(rows) == 4
assert [row["stopSegment"] for row in rows] == ["装货", "送货 #1", "送货 #2", "送货 #2"]
assert [row["productName"] for row in rows] == ["甲醇", "甲醇", "乙醇", "丙酮"]
assert [row["gapType"] for row in rows] == ["缺供方合同", "", "缺需方合同", "缺需方合同"]
load_row = rows[0]
assert load_row["loadCustomer"] == "外部装货客户"
assert load_row["deliveryCustomer"] == ""
assert load_row["deliveryTimes"] == ""
ok_row = rows[1]
assert ok_row["loadCustomer"] == ""
assert ok_row["deliveryCustomer"] == "外部送货客户A"
assert ok_row["deliveryTimes"] == "2026-10-12"
gap_row = rows[3]
assert gap_row["deliveryCustomer"] == "外部送货客户B"
assert gap_row["deliveryTimes"] == "2026-10-13"
assert gap_row["loadCustomer"] == ""
for row in rows:
assert row["planNo"] == "PL001"
assert row["planDate"] == "2026-10-11"
assert row["planType"] == "直销单"
assert row["followStatus"] == "已安排"
assert row["supplierName"] == "供方甲"
assert row["customerName"] == "需方乙"
def test_build_contract_gap_export_rows_internal_side_has_empty_gap_type():
"""内置客户免合同:装货行缺口为空,外送段仍标缺需方合同。"""
load = _load(
customer_name=INTERNAL,
contracts=[],
products=[_stop_product("甲醇", product_id="prod-load")],
)
delivery = _delivery(
stop_id="d1",
seq=1,
customer_name="外部送货客户",
delivery_time="2026-10-12T08:30:00",
contracts=[],
products=[_stop_product("甲醇", product_id="prod-d1")],
)
rows = build_contract_gap_export_rows(
[_gap_item()],
{"p1": (load, [delivery])},
)
assert [row["stopSegment"] for row in rows] == ["装货", "送货 #1"]
assert [row["gapType"] for row in rows] == ["", "缺需方合同"]
assert rows[1]["deliveryTimes"] == "2026-10-12"
@@ -0,0 +1,77 @@
"""证书保存状态保护:仅使用数据库边界替身,不连接业务数据库。"""
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from app.services import pl_plan_service as svc
@pytest.fixture
def cert_case(monkeypatch):
plan = SimpleNamespace(
archive_status="pending", follow_status="pending_arrange",
update_by="original", hazard_cert_ready=0,
)
product = SimpleNamespace(
id="product-1", product_id="crm-1", product_name="测试危化品",
purchase_cert_url="old.pdf", transport_cert_url=None,
legal_use_cert_url=None, update_by="original",
)
db = MagicMock()
plan_query, product_query = MagicMock(), MagicMock()
plan_query.filter.return_value.first.return_value = plan
product_query.filter.return_value.first.return_value = product
product_query.filter.return_value.all.return_value = [product]
db.query.side_effect = lambda model: plan_query if model is svc.PlPlan else product_query
monkeypatch.setattr(svc, "fetch_hazard_flags_map", lambda *args: {})
monkeypatch.setattr(svc, "resolve_hazard_flags_from_crm", lambda *args, **kwargs: (True, False))
auto_ready = MagicMock()
monkeypatch.setattr(svc, "maybe_auto_set_hazard_cert_ready", auto_ready)
item = SimpleNamespace(
id=product.id, purchase_cert_url=" new.pdf,other.pdf ",
transport_cert_url=" ", legal_use_cert_url=None,
)
def save():
return svc.update_plan_product_certs(
db, tenant_no="test-tenant", plan_id="plan-1", items=[item], user_id="editor"
)
return SimpleNamespace(
plan=plan, product=product, db=db, save=save,
auto_ready=auto_ready, plan_query=plan_query,
)
@pytest.mark.parametrize("state", ["cancelled", "archived", "missing"])
def test_invalid_plan_rejected_before_any_certificate_write(cert_case, state):
c = cert_case
if state == "cancelled":
c.plan.follow_status = "cancelled"
elif state == "archived":
c.plan.archive_status = "archived"
else:
c.plan_query.filter.return_value.first.return_value = None
with pytest.raises(HTTPException) as error:
c.save()
assert error.value.status_code == (404 if state == "missing" else 400)
if state == "cancelled":
assert "已作废" in error.value.detail
assert c.product.purchase_cert_url == "old.pdf"
assert c.product.update_by == c.plan.update_by == "original"
assert c.plan.hazard_cert_ready == 0
c.db.query.assert_called_once_with(svc.PlPlan)
c.db.commit.assert_not_called()
c.auto_ready.assert_not_called()
def test_active_plan_can_save_and_clear_certificate_urls(cert_case):
c = cert_case
assert c.save() == [c.product]
assert c.product.purchase_cert_url == "new.pdf,other.pdf"
assert c.product.transport_cert_url is None
assert c.product.legal_use_cert_url is None
assert c.product.update_by == c.plan.update_by == "editor"
c.db.commit.assert_called_once()
c.db.refresh.assert_called_once_with(c.product)
c.auto_ready.assert_called_once()
@@ -0,0 +1,703 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>山峡化工 · H5 附件上传与回显 Demo</title>
<style>
:root {
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-soft: #e9f9f2;
--bg: #f5f8f7;
--surface: #fff;
--text: #1f2a26;
--muted: #6b7b74;
--border: #dcebe4;
--danger: #dc2626;
--header: linear-gradient(180deg, #087f57 0%, #087f57 28%, #0a9f6d 78%, #0a9f6d 100%);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
button { font: inherit; cursor: pointer; }
body {
background: #cfd9d4;
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
}
body.is-view .only-edit { display: none !important; }
.stage { max-width: 1180px; margin: 0 auto; padding: 16px 12px 64px; }
.banner {
background: #1f2a26;
color: #fff;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.65;
}
.banner strong { color: #9fe3c4; }
.switcher { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.switcher button {
height: 36px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid #b7c9c0;
background: #fff;
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.switcher button[aria-pressed="true"] {
background: var(--brand);
border-color: var(--brand);
color: #fff;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
align-items: start;
}
.col-label {
margin-bottom: 8px;
padding: 10px 12px;
border-radius: 10px;
background: #fff;
border: 1px solid #b7c9c0;
font-size: 12px;
line-height: 1.55;
min-height: 108px;
}
.col-label h2 { font-size: 15px; margin-bottom: 4px; }
.col-label .rec { color: var(--brand-dark); font-weight: 700; }
.col-label.rec-box { border-color: #b7ebce; background: var(--brand-soft); }
.phone {
position: relative;
background: var(--bg);
border-radius: 28px;
overflow: hidden;
border: 8px solid #24312c;
height: 740px;
box-shadow: 0 18px 40px rgba(0,0,0,.16);
display: flex;
flex-direction: column;
}
.status-bar {
flex: 0 0 auto;
height: 28px;
background: var(--brand-dark);
color: rgba(255,255,255,.88);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
font-size: 11px;
}
.header {
flex: 0 0 auto;
background: var(--header);
color: #fff;
padding: 6px 12px 14px;
border-radius: 0 0 22px 22px;
}
.header-row { display: flex; align-items: center; gap: 8px; min-height: 40px; }
.back { font-size: 20px; width: 32px; }
.header h1 { flex: 1; font-size: 17px; font-weight: 700; }
.mode-pill {
height: 26px;
padding: 0 10px;
border-radius: 999px;
background: rgba(255,255,255,.18);
font-size: 12px;
line-height: 26px;
}
.body { flex: 1; min-height: 0; overflow: auto; padding: 12px; }
.section {
margin-bottom: 12px;
padding: 14px;
background: #fff;
border: 1px solid var(--border);
border-radius: 14px;
}
.section h3 { margin-bottom: 4px; font-size: 15px; }
.hint { margin-bottom: 10px; font-size: 12px; color: var(--muted); }
.toast {
position: fixed;
left: 50%;
bottom: 24px;
z-index: 20;
transform: translateX(-50%) translateY(12px);
padding: 10px 16px;
border-radius: 999px;
background: rgba(31,42,38,.92);
color: #fff;
font-size: 13px;
opacity: 0;
pointer-events: none;
transition: .2s ease;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* 共用预览弹层,留在手机框内 */
.mask {
position: absolute;
inset: 28px 0 0;
display: none;
background: rgba(15,23,20,.45);
z-index: 5;
}
.mask.open { display: block; }
.sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
max-height: 78%;
padding: 8px 16px 18px;
background: #fff;
border-radius: 18px 18px 0 0;
box-shadow: 0 -8px 24px rgba(0,0,0,.12);
}
.sheet-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
font-weight: 700;
}
.sheet-bar button {
border: 0;
background: transparent;
color: var(--brand);
font-size: 14px;
}
.preview-stage {
display: flex;
align-items: center;
justify-content: center;
min-height: 220px;
margin-bottom: 12px;
border-radius: 12px;
background: #f3f7f5;
overflow: hidden;
}
.preview-stage img { width: 100%; height: 240px; object-fit: cover; }
.preview-doc { text-align: center; }
.preview-doc strong {
display: block;
font-size: 28px;
color: var(--brand-dark);
}
.preview-doc small { color: var(--muted); }
.preview-name { margin-bottom: 12px; font-size: 14px; font-weight: 650; word-break: break-all; }
.sheet-actions { display: flex; gap: 8px; }
.sheet-actions button {
flex: 1;
height: 40px;
border-radius: 10px;
border: 1px solid var(--border);
background: #fff;
font-weight: 650;
}
.sheet-actions .primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.thumb {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
background:
linear-gradient(160deg, #d7efe4, #f4fbf7 40%, #c9e7d8);
}
.ext {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: #f3f7f5;
color: var(--brand-dark);
}
.ext b { font-size: 16px; letter-spacing: .04em; }
.ext.doc { color: #1d4ed8; background: #eff6ff; }
.uploading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,.72);
color: var(--brand-dark);
font-size: 12px;
font-weight: 700;
}
/* A 宫格 */
.grid-files {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.tile {
position: relative;
aspect-ratio: 1;
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
background: #fff;
}
.tile-hit {
display: block;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: transparent;
}
.tile-add {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
border: 1px dashed #8dcfb4;
background: var(--brand-soft);
color: var(--brand-dark);
font-size: 22px;
font-weight: 700;
line-height: 1;
}
.tile-add span { font-size: 12px; line-height: 1.2; }
.tile-name {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 12px 6px 4px;
font-size: 10px;
color: #fff;
pointer-events: none;
background: linear-gradient(transparent, rgba(15,23,20,.62));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon-btn {
position: absolute;
z-index: 1;
width: 22px;
height: 22px;
border: 0;
border-radius: 999px;
color: #fff;
font-size: 12px;
line-height: 22px;
}
.icon-btn.dl { right: 4px; top: 4px; background: rgba(8,127,87,.92); }
.icon-btn.del { left: 4px; top: 4px; background: rgba(220,38,38,.92); }
/* B 文件行 */
.add-row {
width: 100%;
height: 40px;
margin-bottom: 8px;
border: 1px dashed #8dcfb4;
border-radius: 10px;
background: var(--brand-soft);
color: var(--brand-dark);
font-weight: 700;
}
.file-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
border-top: 1px solid #eef4f1;
}
.file-row:first-of-type { border-top: 0; }
.file-ico {
flex: 0 0 40px;
height: 40px;
border-radius: 10px;
overflow: hidden;
}
.file-main {
flex: 1;
min-width: 0;
padding: 0;
border: 0;
background: transparent;
text-align: left;
}
.file-main b {
display: block;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-main small { color: var(--muted); font-size: 11px; }
.text-btn {
flex: 0 0 auto;
height: 30px;
padding: 0 8px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
font-size: 12px;
font-weight: 650;
}
.text-btn.danger { color: var(--danger); border-color: #fecaca; }
/* C 横滑卡 */
.scroller {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
}
.card {
position: relative;
flex: 0 0 112px;
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
background: #fff;
}
.card-add {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
height: 148px;
border: 1px dashed #8dcfb4;
background: var(--brand-soft);
color: var(--brand-dark);
font-size: 12px;
font-weight: 700;
}
.card-add span { font-size: 22px; line-height: 1; }
.card-hit {
display: block;
width: 100%;
height: 84px;
padding: 0;
border: 0;
background: transparent;
}
.card-foot { padding: 6px 8px 8px; }
.card-foot b {
display: block;
font-size: 11px;
font-weight: 650;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-foot button {
margin-top: 4px;
height: 24px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid var(--border);
background: #fff;
color: var(--brand-dark);
font-size: 11px;
font-weight: 700;
}
</style>
</head>
<body data-mode="edit">
<div class="stage">
<div class="banner">
<strong>附件上传与回显 · 三选一。</strong>
三套都遵守同一条规则:点缩略图或文件名打开查看弹层;回显上始终可以下载;编辑模式可以删除,查看模式不出现删除和添加。上传中的文件只在编辑模式出现。色值用三墙 <code style="background:rgba(255,255,255,.12);padding:1px 6px;border-radius:4px">#0a9f6d</code>。
</div>
<div class="switcher" role="tablist" aria-label="附件模式">
<button type="button" data-mode="edit" aria-pressed="true">编辑模式</button>
<button type="button" data-mode="view" aria-pressed="false">查看模式</button>
</div>
<div class="grid">
<section>
<div class="col-label rec-box">
<h2>A · 宫格缩略图 <span class="rec">推荐</span></h2>
图片直接看缩略图,PDF、Word 用类型角标。证照和附件混在一起时最好认。下载、删除都在格子上。
</div>
<div class="phone" data-style="grid">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<header class="header">
<div class="header-row">
<span class="back">←</span>
<h1>客户资料</h1>
<span class="mode-pill" data-mode-label>编辑</span>
</div>
</header>
<div class="body">
<section class="section">
<h3>资质附件</h3>
<p class="hint">图片、PDF、Word 均可。单个不超过 20MB。</p>
<div class="grid-files">
<button class="tile tile-add only-edit" type="button" data-add>+<span>上传</span></button>
<article class="tile" data-file data-name="营业执照.jpg" data-kind="image" data-meta="JPG · 860KB">
<button class="icon-btn del only-edit" type="button" data-del aria-label="删除营业执照">×</button>
<button class="icon-btn dl" type="button" data-dl aria-label="下载营业执照">↓</button>
<button class="tile-hit" type="button" data-preview>
<img class="thumb" alt="" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect fill='%23d7efe4' width='80' height='80'/%3E%3Crect x='18' y='16' width='44' height='48' rx='4' fill='%23fff' stroke='%23087f57'/%3E%3Ccircle cx='40' cy='34' r='8' fill='%23e9f9f2' stroke='%230a9f6d'/%3E%3Cpath d='M26 54h28' stroke='%23087f57'/%3E%3C/svg%3E" />
</button>
<span class="tile-name">营业执照.jpg</span>
</article>
<article class="tile" data-file data-name="危化品经营许可证.pdf" data-kind="pdf" data-meta="PDF · 1.2MB">
<button class="icon-btn del only-edit" type="button" data-del aria-label="删除许可证">×</button>
<button class="icon-btn dl" type="button" data-dl aria-label="下载许可证">↓</button>
<button class="tile-hit" type="button" data-preview><span class="ext"><b>PDF</b></span></button>
<span class="tile-name">危化品经营许可证.pdf</span>
</article>
<article class="tile" data-file data-name="报价说明.docx" data-kind="doc" data-meta="DOCX · 240KB">
<button class="icon-btn del only-edit" type="button" data-del aria-label="删除报价说明">×</button>
<button class="icon-btn dl" type="button" data-dl aria-label="下载报价说明">↓</button>
<button class="tile-hit" type="button" data-preview><span class="ext doc"><b>DOC</b></span></button>
<span class="tile-name">报价说明.docx</span>
</article>
<article class="tile only-edit" data-file data-name="合同扫描件.pdf" data-kind="pdf" data-meta="上传中">
<button class="tile-hit" type="button" disabled><span class="ext"><b>PDF</b></span></button>
<span class="uploading">上传中…</span>
</article>
</div>
</section>
</div>
<div class="mask" data-mask>
<div class="sheet">
<div class="sheet-bar"><button type="button" data-close>关闭</button><span>查看附件</span><span></span></div>
<div class="preview-stage" data-stage></div>
<p class="preview-name" data-preview-name></p>
<div class="sheet-actions">
<button class="primary" type="button" data-sheet-dl>下载</button>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="col-label">
<h2>B · 文件行</h2>
文件名、类型和大小放在一行。合同、证书这类长文件名更不容易被格子裁掉。点文件名查看,下载和删除是独立按钮。
</div>
<div class="phone" data-style="list">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<header class="header">
<div class="header-row">
<span class="back">←</span>
<h1>合同详情</h1>
<span class="mode-pill" data-mode-label>编辑</span>
</div>
</header>
<div class="body">
<section class="section">
<h3>扫描件 / 附件</h3>
<p class="hint">点文件名查看,下载不进入弹层。</p>
<button class="add-row only-edit" type="button" data-add>+ 上传附件</button>
<div class="file-row" data-file data-name="营业执照.jpg" data-kind="image" data-meta="JPG · 860KB">
<div class="file-ico"><img class="thumb" alt="" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect fill='%23d7efe4' width='80' height='80'/%3E%3Crect x='18' y='16' width='44' height='48' rx='4' fill='%23fff' stroke='%23087f57'/%3E%3Ccircle cx='40' cy='34' r='8' fill='%23e9f9f2' stroke='%230a9f6d'/%3E%3Cpath d='M26 54h28' stroke='%23087f57'/%3E%3C/svg%3E" /></div>
<button class="file-main" type="button" data-preview>
<b>营业执照.jpg</b>
<small>JPG · 860KB</small>
</button>
<button class="text-btn" type="button" data-dl>下载</button>
<button class="text-btn danger only-edit" type="button" data-del>删除</button>
</div>
<div class="file-row" data-file data-name="危化品经营许可证.pdf" data-kind="pdf" data-meta="PDF · 1.2MB">
<div class="file-ico"><span class="ext"><b>PDF</b></span></div>
<button class="file-main" type="button" data-preview>
<b>危化品经营许可证.pdf</b>
<small>PDF · 1.2MB</small>
</button>
<button class="text-btn" type="button" data-dl>下载</button>
<button class="text-btn danger only-edit" type="button" data-del>删除</button>
</div>
<div class="file-row" data-file data-name="报价说明.docx" data-kind="doc" data-meta="DOCX · 240KB">
<div class="file-ico"><span class="ext doc"><b>DOC</b></span></div>
<button class="file-main" type="button" data-preview>
<b>报价说明.docx</b>
<small>DOCX · 240KB</small>
</button>
<button class="text-btn" type="button" data-dl>下载</button>
<button class="text-btn danger only-edit" type="button" data-del>删除</button>
</div>
<div class="file-row only-edit">
<div class="file-ico"><span class="ext"><b>PDF</b></span></div>
<div class="file-main"><b>合同扫描件.pdf</b><small>上传中…</small></div>
</div>
</section>
</div>
<div class="mask" data-mask>
<div class="sheet">
<div class="sheet-bar"><button type="button" data-close>关闭</button><span>查看附件</span><span></span></div>
<div class="preview-stage" data-stage></div>
<p class="preview-name" data-preview-name></p>
<div class="sheet-actions">
<button class="primary" type="button" data-sheet-dl>下载</button>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="col-label">
<h2>C · 横滑附件卡</h2>
虚线上传卡和已传文件同一行横滑,接近管理端附件卡。文件一多需要左右滑,手机上不如宫格一眼看全。
</div>
<div class="phone" data-style="cards">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<header class="header">
<div class="header-row">
<span class="back">←</span>
<h1>危化品证</h1>
<span class="mode-pill" data-mode-label>编辑</span>
</div>
</header>
<div class="body">
<section class="section">
<h3>证书附件</h3>
<p class="hint">向左滑可看到更多文件。</p>
<div class="scroller">
<button class="card card-add only-edit" type="button" data-add><span>+</span>上传附件</button>
<article class="card" data-file data-name="营业执照.jpg" data-kind="image" data-meta="JPG · 860KB">
<button class="icon-btn del only-edit" type="button" data-del aria-label="删除">×</button>
<button class="card-hit" type="button" data-preview>
<img class="thumb" alt="" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect fill='%23d7efe4' width='80' height='80'/%3E%3Crect x='18' y='16' width='44' height='48' rx='4' fill='%23fff' stroke='%23087f57'/%3E%3Ccircle cx='40' cy='34' r='8' fill='%23e9f9f2' stroke='%230a9f6d'/%3E%3Cpath d='M26 54h28' stroke='%23087f57'/%3E%3C/svg%3E" />
</button>
<div class="card-foot">
<b>营业执照.jpg</b>
<button type="button" data-dl>下载</button>
</div>
</article>
<article class="card" data-file data-name="危化品经营许可证.pdf" data-kind="pdf" data-meta="PDF · 1.2MB">
<button class="icon-btn del only-edit" type="button" data-del aria-label="删除">×</button>
<button class="card-hit" type="button" data-preview><span class="ext"><b>PDF</b></span></button>
<div class="card-foot">
<b>危化品经营许可证.pdf</b>
<button type="button" data-dl>下载</button>
</div>
</article>
<article class="card" data-file data-name="报价说明.docx" data-kind="doc" data-meta="DOCX · 240KB">
<button class="icon-btn del only-edit" type="button" data-del aria-label="删除">×</button>
<button class="card-hit" type="button" data-preview><span class="ext doc"><b>DOC</b></span></button>
<div class="card-foot">
<b>报价说明.docx</b>
<button type="button" data-dl>下载</button>
</div>
</article>
<article class="card only-edit">
<div class="card-hit" style="position:relative"><span class="ext"><b>PDF</b></span><span class="uploading">上传中…</span></div>
<div class="card-foot"><b>合同扫描件.pdf</b></div>
</article>
</div>
</section>
</div>
<div class="mask" data-mask>
<div class="sheet">
<div class="sheet-bar"><button type="button" data-close>关闭</button><span>查看附件</span><span></span></div>
<div class="preview-stage" data-stage></div>
<p class="preview-name" data-preview-name></p>
<div class="sheet-actions">
<button class="primary" type="button" data-sheet-dl>下载</button>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="toast" id="toast" role="status"></div>
<script>
const toastEl = document.getElementById('toast')
let toastTimer = 0
const photo = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Crect fill='%23d7efe4' width='80' height='80'/%3E%3Crect x='18' y='16' width='44' height='48' rx='4' fill='%23fff' stroke='%23087f57'/%3E%3Ccircle cx='40' cy='34' r='8' fill='%23e9f9f2' stroke='%230a9f6d'/%3E%3Cpath d='M26 54h28' stroke='%23087f57'/%3E%3C/svg%3E"
function toast(text) {
toastEl.textContent = text
toastEl.classList.add('show')
clearTimeout(toastTimer)
toastTimer = setTimeout(() => toastEl.classList.remove('show'), 1600)
}
function setMode(mode) {
document.body.classList.toggle('is-view', mode === 'view')
document.body.dataset.mode = mode
document.querySelectorAll('.switcher button').forEach((button) => {
button.setAttribute('aria-pressed', button.dataset.mode === mode ? 'true' : 'false')
})
document.querySelectorAll('[data-mode-label]').forEach((node) => {
node.textContent = mode === 'view' ? '查看' : '编辑'
})
}
document.querySelectorAll('.switcher button').forEach((button) => {
button.addEventListener('click', () => setMode(button.dataset.mode))
})
function fileOf(node) {
return node.closest('[data-file]')
}
function openPreview(file, phone) {
const mask = phone.querySelector('[data-mask]')
const stage = mask.querySelector('[data-stage]')
const name = file.dataset.name
const kind = file.dataset.kind
mask.dataset.current = name
mask.querySelector('[data-preview-name]').textContent = name + ' · ' + file.dataset.meta
if (kind === 'image') {
stage.innerHTML = '<img alt="" src="' + photo + '" />'
} else {
const label = kind === 'pdf' ? 'PDF' : 'DOC'
stage.innerHTML = '<div class="preview-doc"><strong>' + label + '</strong><small>' + file.dataset.meta + '</small></div>'
}
mask.classList.add('open')
}
document.querySelectorAll('.phone').forEach((phone) => {
phone.addEventListener('click', (event) => {
const target = event.target.closest('button')
if (!target || !phone.contains(target)) return
if (target.hasAttribute('data-add')) {
toast('打开相册或文件选择')
return
}
if (target.hasAttribute('data-dl') || target.hasAttribute('data-sheet-dl')) {
event.stopPropagation()
const file = fileOf(target)
const name = file ? file.dataset.name : phone.querySelector('[data-mask]').dataset.current
toast('开始下载 ' + name)
return
}
if (target.hasAttribute('data-del')) {
event.stopPropagation()
const file = fileOf(target)
const name = file.dataset.name
file.remove()
toast('已删除 ' + name)
return
}
if (target.hasAttribute('data-preview')) {
openPreview(fileOf(target), phone)
return
}
if (target.hasAttribute('data-close') || target === phone.querySelector('[data-mask]')) {
phone.querySelector('[data-mask]').classList.remove('open')
}
})
phone.querySelector('[data-mask]').addEventListener('click', (event) => {
if (event.target.hasAttribute('data-mask')) event.currentTarget.classList.remove('open')
})
})
</script>
</body>
</html>
@@ -0,0 +1,473 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>山峡化工 · 客户管理列表卡片美化 Demo</title>
<style>
:root {
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-soft: #e9f9f2;
--bg: #f3f6f5;
--surface: #fff;
--text: #1f2a26;
--muted: #6b7b74;
--border: #dcebe4;
--radius: 14px;
--shadow: 0 6px 16px rgba(10, 159, 109, 0.08);
--header: linear-gradient(180deg, #087f57 0%, #087f57 28%, #0a9f6d 78%, #0a9f6d 100%);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
button { font: inherit; cursor: pointer; }
body {
background: #cfd9d4;
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
font-size: 14px;
line-height: 1.45;
min-height: 100vh;
}
.stage { max-width: 1100px; margin: 0 auto; padding: 16px 12px 48px; }
.banner {
background: #1f2a26; color: #fff; border-radius: 12px; padding: 14px 16px;
margin-bottom: 14px; font-size: 13px; line-height: 1.6;
}
.banner strong { color: #9fe3c4; }
.banner code { background: rgba(255,255,255,.12); padding: 1px 6px; border-radius: 4px; font-size: 12px; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
align-items: start;
}
.col-label {
margin-bottom: 8px; padding: 8px 10px; border-radius: 10px;
background: #fff; border: 1px solid #b7c9c0; font-size: 12px; line-height: 1.5;
}
.col-label h2 { font-size: 14px; margin-bottom: 2px; }
.col-label .rec { color: var(--brand-dark); font-weight: 700; }
.col-label.rec-box { border-color: #b7ebce; background: var(--brand-soft); }
.phone {
background: var(--bg);
border-radius: 28px;
overflow: hidden;
border: 8px solid #24312c;
height: 720px;
box-shadow: 0 18px 40px rgba(0,0,0,.16);
display: flex;
flex-direction: column;
}
.status-bar {
flex: 0 0 auto;
height: 28px; background: var(--brand-dark); color: rgba(255,255,255,.85);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; font-size: 11px;
}
.header {
flex: 0 0 auto;
background: var(--header); color: #fff;
padding: 8px 12px 12px; border-radius: 0 0 22px 22px;
}
.header-row { display: flex; align-items: center; gap: 8px; min-height: 40px; }
.back { border: 0; background: transparent; color: #fff; font-size: 20px; padding: 4px 8px; }
.header h1 { flex: 1; font-size: 17px; font-weight: 700; }
.search {
width: 100%; margin-top: 8px; border: 0; border-radius: 999px;
padding: 9px 14px; background: #fff; color: var(--text); font-size: 13px;
}
.tabs {
display: flex; gap: 4px; margin-top: 8px; overflow-x: auto;
}
.tabs span {
flex: 0 0 auto; padding: 4px 12px; border-radius: 999px;
font-size: 12px; color: rgba(255,255,255,.88);
}
.tabs span.on { background: rgba(255,255,255,.22); color: #fff; font-weight: 700; }
.list {
flex: 1; overflow-y: auto; padding: 12px;
-webkit-overflow-scrolling: touch;
}
.hint {
text-align: center; font-size: 11px; color: #8a9a93; padding: 8px 0 20px;
}
/* 共用卡片壳 */
.card {
background: var(--surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid rgba(220, 235, 228, 0.9);
padding: 14px 14px 12px;
margin-bottom: 12px;
}
.head {
display: flex; gap: 8px; align-items: flex-start; justify-content: space-between;
}
.title {
font-size: 15px; font-weight: 700; color: var(--text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sub { margin-top: 3px; font-size: 12px; color: var(--muted); }
.tag {
flex-shrink: 0; display: inline-flex; align-items: center;
min-height: 24px; padding: 2px 8px; font-size: 11px; font-weight: 600;
border-radius: 999px; border: 1px solid #b7ebce;
color: var(--brand-dark); background: var(--brand-soft);
}
.tag.warn {
color: #a16207; background: #fffbeb; border-color: #fde68a;
}
.actions {
display: flex; gap: 8px; margin-top: 12px; padding-top: 10px;
border-top: 1px dashed var(--border);
}
.btn {
flex: 1; height: 36px; border-radius: 8px; border: 1px solid var(--border);
background: #fff; color: var(--text); font-size: 13px; font-weight: 600;
}
.btn.primary {
background: var(--brand); border-color: var(--brand); color: #fff;
}
/* —— 现状:双色块 —— */
.cur .emphasis {
display: flex; gap: 10px; align-items: baseline; justify-content: space-between;
margin-top: 10px; padding: 8px 10px; font-size: 13px;
color: var(--brand-dark); background: var(--brand-soft); border-radius: 10px;
}
.cur .emphasis strong { font-size: 14px; color: #065f46; }
.cur .kv {
margin-top: 10px; padding: 10px 12px;
background: #f3f7f5; border: 1px solid var(--border); border-radius: 10px;
}
.cur .kv-row {
display: flex; gap: 12px; justify-content: space-between;
font-size: 13px; padding: 2px 0;
}
.cur .kv-row + .kv-row { margin-top: 6px; }
.cur .kv-row span:first-child { color: var(--muted); font-size: 11px; min-width: 4.5em; }
.cur .kv-row span:last-child { font-weight: 500; color: #223b33; text-align: right; }
/* —— 方案 A:扁平 + 展开(对齐详情联系人卡) —— */
.a .meta {
margin-top: 4px; font-size: 12px; line-height: 1.45; color: var(--muted);
}
.a .chips {
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
}
.a .chip {
font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px;
color: var(--brand-dark); background: transparent; border: 1px solid #b7ebce;
}
.a .chip.level {
color: #065f46; border-color: #87d4b0; background: rgba(233, 249, 242, 0.45);
}
.toggle {
display: flex; gap: 4px; align-items: center; justify-content: center;
width: 100%; margin-top: 10px; padding: 8px 0 2px;
font-size: 13px; font-weight: 600; color: var(--brand-dark);
background: none; border: 0; border-top: 1px dashed var(--border);
}
.toggle .ico { font-size: 11px; }
.extra { margin-top: 8px; display: none; }
.extra.open { display: block; }
.row {
display: flex; gap: 12px; align-items: flex-start;
padding: 4px 0; font-size: 13px; line-height: 1.45;
}
.row > span:first-child {
flex: 0 0 auto; min-width: 5em; color: var(--muted); white-space: nowrap;
}
.row > span:last-child {
flex: 1; min-width: 0; color: var(--text); text-align: right; word-break: break-word;
}
/* —— 方案 B:更干净,类型/级别进摘要行,默认更短 —— */
.b .meta {
margin-top: 4px; font-size: 12px; line-height: 1.5; color: var(--muted);
}
.b .meta em {
font-style: normal; color: var(--brand-dark); font-weight: 600;
}
.b .preview {
margin-top: 8px; font-size: 13px; color: #223b33;
display: flex; flex-direction: column; gap: 4px;
}
.b .preview .line {
display: flex; justify-content: space-between; gap: 10px;
}
.b .preview .line span:first-child { color: var(--muted); font-size: 12px; }
.b .preview .line span:last-child { font-weight: 500; text-align: right; }
.note {
margin-top: 18px; padding: 12px 14px; background: #fff; border-radius: 12px;
border: 1px solid #b7c9c0; font-size: 12px; color: var(--muted); line-height: 1.6;
}
.note strong { color: var(--text); }
</style>
</head>
<body>
<div class="stage">
<div class="banner">
<strong>客户管理 · 列表卡片美化对比</strong><br />
问题:现状卡片内有<strong>两块背景色区</strong>(绿强调条 + 灰绿 KV 块),信息层级多、视觉偏复杂。<br />
下方三列可点「展开 / 收起」。Token:三墙绿 <code>#0a9f6d</code> / <code>#e9f9f2</code>。推荐默认选 <strong>方案 A</strong>。
</div>
<div class="grid">
<!-- 现状 -->
<div>
<div class="col-label">
<h2>现状(Current)</h2>
绿强调条 + 灰绿 KV 底块,字段全展开,无折叠。
</div>
<div class="phone">
<div class="status-bar"><span>9:41</span><span>▮▮▮</span></div>
<div class="header">
<div class="header-row">
<button class="back" type="button" aria-label="返回">‹</button>
<h1>客户管理</h1>
</div>
<input class="search" type="search" placeholder="客户名称 / 编号 / 手机号" readonly />
<div class="tabs">
<span class="on">全部</span><span>我的</span><span>待跟进</span><span>重点</span>
</div>
</div>
<div class="list cur">
<article class="card">
<div class="head">
<div>
<div class="title">杭州山峡精细化工有限公司</div>
<div class="sub">下次跟进 10-20 09:00</div>
</div>
<span class="tag">正常</span>
</div>
<div class="emphasis">
<span>生产企业</span>
<strong>A级</strong>
</div>
<div class="kv">
<div class="kv-row"><span>客户编号</span><span>C20261018001</span></div>
<div class="kv-row"><span>简称</span><span>山峡精细</span></div>
<div class="kv-row"><span>手机号</span><span>138****6621</span></div>
<div class="kv-row"><span>负责人</span><span>王敏</span></div>
<div class="kv-row"><span>下次跟进</span><span>10-20 09:00</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">跟进</button>
<button class="btn primary" type="button">查看</button>
</div>
</article>
<article class="card">
<div class="head">
<div>
<div class="title">宁波东海物流有限公司</div>
<div class="sub">创建于 10-12 14:22</div>
</div>
<span class="tag warn">待跟进</span>
</div>
<div class="emphasis">
<span>贸易商</span>
<strong>B级</strong>
</div>
<div class="kv">
<div class="kv-row"><span>客户编号</span><span>C20261012088</span></div>
<div class="kv-row"><span>简称</span><span>东海物流</span></div>
<div class="kv-row"><span>手机号</span><span>159****3308</span></div>
<div class="kv-row"><span>负责人</span><span>李强</span></div>
<div class="kv-row"><span>下次跟进</span><span>—</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">跟进</button>
<button class="btn primary" type="button">查看</button>
</div>
</article>
<p class="hint">已加载 2 / 128</p>
</div>
</div>
</div>
<!-- 方案 A -->
<div>
<div class="col-label rec-box">
<h2>方案 A · 扁平 + 展开 <span class="rec">推荐</span></h2>
去掉两块色底;类型/级别用轻 Chip;字段默认收起,对齐详情联系人卡。
</div>
<div class="phone">
<div class="status-bar"><span>9:41</span><span>▮▮▮</span></div>
<div class="header">
<div class="header-row">
<button class="back" type="button" aria-label="返回">‹</button>
<h1>客户管理</h1>
</div>
<input class="search" type="search" placeholder="客户名称 / 编号 / 手机号" readonly />
<div class="tabs">
<span class="on">全部</span><span>我的</span><span>待跟进</span><span>重点</span>
</div>
</div>
<div class="list a">
<article class="card" data-card="a1">
<div class="head">
<div style="min-width:0;flex:1">
<div class="title">杭州山峡精细化工有限公司</div>
<div class="meta">C20261018001 · 王敏 · 10-20 09:00</div>
<div class="chips">
<span class="chip">生产企业</span>
<span class="chip level">A级</span>
</div>
</div>
<span class="tag">正常</span>
</div>
<button class="toggle" type="button" data-toggle="a1">
展开<span class="ico">▾</span>
</button>
<div class="extra" id="extra-a1">
<div class="row"><span>简称</span><span>山峡精细</span></div>
<div class="row"><span>手机号</span><span>138****6621</span></div>
<div class="row"><span>负责人</span><span>王敏</span></div>
<div class="row"><span>下次跟进</span><span>10-20 09:00</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">跟进</button>
<button class="btn primary" type="button">查看</button>
</div>
</article>
<article class="card" data-card="a2">
<div class="head">
<div style="min-width:0;flex:1">
<div class="title">宁波东海物流有限公司</div>
<div class="meta">C20261012088 · 李强 · —</div>
<div class="chips">
<span class="chip">贸易商</span>
<span class="chip level">B级</span>
</div>
</div>
<span class="tag warn">待跟进</span>
</div>
<button class="toggle" type="button" data-toggle="a2">
展开<span class="ico">▾</span>
</button>
<div class="extra" id="extra-a2">
<div class="row"><span>简称</span><span>东海物流</span></div>
<div class="row"><span>手机号</span><span>159****3308</span></div>
<div class="row"><span>负责人</span><span>李强</span></div>
<div class="row"><span>下次跟进</span><span>—</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">跟进</button>
<button class="btn primary" type="button">查看</button>
</div>
</article>
<p class="hint">已加载 2 / 128 · 点「展开」看字段</p>
</div>
</div>
</div>
<!-- 方案 B -->
<div>
<div class="col-label">
<h2>方案 B · 极简摘要</h2>
类型/级别并入一行摘要;默认只露 2 行关键信息,其余折叠。
</div>
<div class="phone">
<div class="status-bar"><span>9:41</span><span>▮▮▮</span></div>
<div class="header">
<div class="header-row">
<button class="back" type="button" aria-label="返回">‹</button>
<h1>客户管理</h1>
</div>
<input class="search" type="search" placeholder="客户名称 / 编号 / 手机号" readonly />
<div class="tabs">
<span class="on">全部</span><span>我的</span><span>待跟进</span><span>重点</span>
</div>
</div>
<div class="list b">
<article class="card" data-card="b1">
<div class="head">
<div style="min-width:0;flex:1">
<div class="title">杭州山峡精细化工有限公司</div>
<div class="meta"><em>生产企业 · A级</em> · 王敏 · 下次 10-20</div>
</div>
<span class="tag">正常</span>
</div>
<div class="preview">
<div class="line"><span>手机号</span><span>138****6621</span></div>
<div class="line"><span>简称</span><span>山峡精细</span></div>
</div>
<button class="toggle" type="button" data-toggle="b1">
更多字段<span class="ico">▾</span>
</button>
<div class="extra" id="extra-b1">
<div class="row"><span>客户编号</span><span>C20261018001</span></div>
<div class="row"><span>负责人</span><span>王敏</span></div>
<div class="row"><span>下次跟进</span><span>10-20 09:00</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">跟进</button>
<button class="btn primary" type="button">查看</button>
</div>
</article>
<article class="card" data-card="b2">
<div class="head">
<div style="min-width:0;flex:1">
<div class="title">宁波东海物流有限公司</div>
<div class="meta"><em>贸易商 · B级</em> · 李强 · 下次 —</div>
</div>
<span class="tag warn">待跟进</span>
</div>
<div class="preview">
<div class="line"><span>手机号</span><span>159****3308</span></div>
<div class="line"><span>简称</span><span>东海物流</span></div>
</div>
<button class="toggle" type="button" data-toggle="b2">
更多字段<span class="ico">▾</span>
</button>
<div class="extra" id="extra-b2">
<div class="row"><span>客户编号</span><span>C20261012088</span></div>
<div class="row"><span>负责人</span><span>李强</span></div>
<div class="row"><span>下次跟进</span><span>—</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">跟进</button>
<button class="btn primary" type="button">查看</button>
</div>
</article>
<p class="hint">已加载 2 / 128 · 点「更多字段」</p>
</div>
</div>
</div>
</div>
<div class="note">
<strong>实现落点:</strong>
<code>web-admin/src/views/m/crm/customer/index.vue</code> +
<code>web-admin/src/styles/mp-webview.scss</code> 中
<code>.mp-customer-list-card__emphasis</code> / <code>.mp-wv-card__kv</code> 色块。<br />
展开交互可复用详情 Tab 已有的 <code>.mp-customer-detail-card__toggle</code> 节奏。
打开文件:<code>bak/mobile-ui/shanxia-mp-crm-customer-list-card-demo.html</code>
</div>
</div>
<script>
document.querySelectorAll('[data-toggle]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-toggle')
const extra = document.getElementById('extra-' + id)
if (!extra) return
const open = extra.classList.toggle('open')
const isB = id.startsWith('b')
btn.innerHTML = open
? (isB ? '收起<span class="ico">▴</span>' : '收起<span class="ico">▴</span>')
: (isB ? '更多字段<span class="ico">▾</span>' : '展开<span class="ico">▾</span>')
})
})
</script>
</body>
</html>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>山峡化工 · 产品管理列表卡 · 去色块 + 折叠 Demo</title>
<style>
:root {
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-soft: #e9f9f2;
--bg: #f5f8f7;
--surface: #fff;
--text: #1f2a26;
--muted: #6b7b74;
--border: #dcebe4;
--danger: #b91c1c;
--header: linear-gradient(180deg, #087f57 0%, #087f57 28%, #0a9f6d 78%, #0a9f6d 100%);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
button, input { font: inherit; }
body {
background: #cfd9d4;
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
font-size: 14px;
line-height: 1.45;
min-height: 100vh;
}
.stage { max-width: 920px; margin: 0 auto; padding: 16px 12px 48px; }
.banner {
background: #1f2a26; color: #fff; border-radius: 12px; padding: 12px 14px;
margin-bottom: 12px; font-size: 12px; line-height: 1.55;
}
.banner strong { color: #9fe3c4; }
.banner li { margin-left: 1.1em; margin-top: 4px; }
.mode-bar {
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px;
}
.mode-bar button {
border: 1px solid #b7c9c0; background: #fff; border-radius: 10px;
padding: 10px 14px; font-size: 13px; font-weight: 600; color: var(--text); cursor: pointer;
}
.mode-bar button.active {
background: var(--brand-soft); border-color: #b7ebce; color: var(--brand-dark);
}
.phones {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 860px) {
.phones.side-by-side { grid-template-columns: 1fr 1fr; }
}
.phone-wrap { display: none; }
.phone-wrap.show { display: block; }
.phones.side-by-side .phone-wrap { display: block; }
.phone-label {
font-size: 12px; font-weight: 700; color: #24312c; margin-bottom: 8px;
display: flex; align-items: center; gap: 8px;
}
.phone-label .pill {
font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 999px;
}
.pill--old { background: #fee2e2; color: #b91c1c; }
.pill--new { background: var(--brand-soft); color: var(--brand-dark); }
.phone {
position: relative;
background: var(--bg);
border-radius: 28px;
overflow: hidden;
border: 8px solid #24312c;
height: 720px;
box-shadow: 0 18px 40px rgba(0,0,0,.16);
display: flex;
flex-direction: column;
}
.status-bar {
flex: 0 0 auto;
height: 28px; background: var(--brand-dark); color: rgba(255,255,255,.85);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; font-size: 11px;
}
.header {
flex: 0 0 auto;
background: var(--header); color: #fff;
padding: 8px 12px 12px; border-radius: 0 0 22px 22px;
box-shadow: 0 10px 18px -10px rgba(8, 127, 87, 0.35);
}
.header-row { display: flex; align-items: center; gap: 8px; min-height: 44px; }
.back {
border: 0; background: transparent; color: #fff;
font-size: 20px; cursor: pointer; padding: 4px 8px; line-height: 1;
}
.header h1 {
flex: 1; min-width: 0; font-size: 17px; font-weight: 700;
}
.status-tabs {
display: flex; gap: 8px; overflow-x: auto; margin-top: 6px; padding-bottom: 2px;
-webkit-overflow-scrolling: touch;
}
.status-tabs::-webkit-scrollbar { display: none; }
.filter-tag {
flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center;
padding: 4px 12px; border-radius: 14px; font-size: 12px; font-weight: 500;
border: 1px solid rgba(255,255,255,.35); color: rgba(255,255,255,.88);
background: rgba(255,255,255,.12); cursor: default;
}
.filter-tag.active {
color: var(--brand-dark); background: #fff; border-color: #fff;
}
.search {
width: 100%; margin-top: 10px; border: 0; border-radius: 999px;
padding: 10px 14px; background: #fff; color: var(--text); font-size: 13px;
}
.search::placeholder { color: #9aa8a1; }
.filter-bar {
display: grid; grid-template-columns: 1fr 1fr 1fr;
background: #fff; border-bottom: 1px solid var(--border); flex: 0 0 auto;
}
.filter-trig {
display: flex; align-items: center; justify-content: center; gap: 4px;
height: 42px; border: 0; background: transparent; color: var(--text);
font-size: 13px; cursor: default; position: relative; min-width: 0;
}
.filter-trig + .filter-trig::before {
content: ''; position: absolute; left: 0; top: 12px; bottom: 12px;
width: 1px; background: var(--border);
}
.filter-trig .arrow { font-size: 10px; color: var(--muted); }
.scroll {
flex: 1; min-height: 0; overflow-y: auto; padding: 10px 10px 88px;
-webkit-overflow-scrolling: touch;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 12px;
margin-bottom: 10px;
box-shadow: 0 6px 16px rgba(10, 159, 109, 0.06);
}
.card__head {
display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
}
.card__title { font-size: 15px; font-weight: 700; color: #1f2a26; line-height: 1.35; }
.card__sub { margin-top: 2px; font-size: 12px; color: var(--muted); }
.card__meta {
margin-top: 4px; font-size: 12px; line-height: 1.5; color: var(--muted);
}
.status-tag {
flex-shrink: 0;
display: inline-flex; align-items: center;
padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600;
border: 1px solid transparent;
}
.status-tag--success {
color: #047857; background: #ecfdf5; border-color: #a7f3d0;
}
.status-tag--danger {
color: #b91c1c; background: #fef2f2; border-color: #fecaca;
}
.status-tag--warning {
color: #a16207; background: #fffbeb; border-color: #fde68a;
}
/* —— 现状:两处色块 —— */
.emphasis {
display: flex; gap: 10px; align-items: baseline; justify-content: space-between;
margin-top: 10px; padding: 8px 10px; font-size: 13px; color: var(--brand-dark);
background: var(--brand-soft); border-radius: 10px;
}
.emphasis strong { flex-shrink: 0; font-size: 14px; font-weight: 700; color: #065f46; }
.kv-block {
margin-top: 10px; padding: 10px 12px;
background: #f3f7f5; border: 1px solid var(--border); border-radius: 10px;
}
.kv-row {
display: flex; gap: 10px; justify-content: space-between; align-items: flex-start;
font-size: 13px;
}
.kv-row + .kv-row { margin-top: 6px; }
.kv-row > span:first-child { min-width: 4.5em; max-width: 42%; font-size: 11px; color: var(--muted); }
.kv-row > span:last-child { font-weight: 500; color: #223b33; text-align: right; }
/* —— 方案:扁平摘要 + 预览 / 展开 —— */
.preview { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.line {
display: flex; gap: 10px; justify-content: space-between; font-size: 13px; color: #223b33;
}
.line > span:first-child { color: var(--muted); font-size: 12px; }
.line > span:last-child { font-weight: 500; text-align: right; }
.toggle {
display: flex; gap: 4px; align-items: center; justify-content: center;
width: 100%; margin-top: 10px; padding: 8px 0 2px;
font-size: 13px; font-weight: 600; color: var(--brand-dark);
background: none; border: 0; cursor: pointer;
}
.toggle-icon { font-size: 11px; line-height: 1; }
.extra { margin-top: 8px; }
.extra[hidden] { display: none !important; }
.row {
display: flex; gap: 12px; align-items: flex-start;
padding: 4px 0; font-size: 13px; line-height: 1.45;
}
.row > span:first-child {
flex: 0 0 auto; min-width: 5em; color: var(--muted); white-space: nowrap;
}
.row > span:last-child {
flex: 1; min-width: 0; color: #1f2a26; text-align: right; word-break: break-word;
}
.tag-inline { display: inline-flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.actions {
display: flex; gap: 8px; margin-top: 12px; padding-top: 10px;
border-top: 1px dashed var(--border);
}
.btn {
flex: 1; height: 36px; border-radius: 8px; border: 1px solid var(--border);
background: #fff; color: var(--text); font-size: 13px; cursor: pointer;
}
.btn--primary {
background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600;
}
.btn--danger {
color: var(--danger); background: #fff7f7; border-color: #fecaca;
}
.hint {
text-align: center; font-size: 12px; color: #8a9a93; padding: 8px 0 16px;
}
.fab {
position: absolute; right: 16px; bottom: 20px; z-index: 5;
width: 48px; height: 48px; border-radius: 50%; border: 0;
background: var(--brand); color: #fff; font-size: 28px; line-height: 1;
box-shadow: 0 8px 20px rgba(10, 159, 109, 0.35); cursor: default;
}
.note {
margin-top: 14px; padding: 12px 14px; background: #fff; border-radius: 12px;
border: 1px solid #b7c9c0; font-size: 12px; color: #44554e; line-height: 1.55;
}
.note h3 { font-size: 13px; margin-bottom: 6px; color: var(--text); }
</style>
</head>
<body>
<div class="stage">
<div class="banner">
<strong>产品管理列表卡 Demo</strong> — 字段对齐 Web「产品管理」列表,多字段折叠
<ul>
<li>去掉顶栏「全部 / 上架 / 下架」(固定只查上架,与 PC 一致)</li>
<li>去掉绿底编号条 + 灰底 KV 两处色块;改扁平摘要 +「更多字段」</li>
<li>字段集对齐 PC 默认可见列:名称(+毒/爆)、编号、分类、品牌、别名、规格型号、标准价、状态(不含成本价)</li>
<li>默认预览 2 行(分类、规格型号);其余进展开;无额外字段则不显示展开按钮</li>
</ul>
</div>
<div class="mode-bar">
<button type="button" class="active" data-mode="compare" id="btnCompare">左右对比</button>
<button type="button" data-mode="old" id="btnOld">仅看现状</button>
<button type="button" data-mode="new" id="btnNew">仅看方案</button>
</div>
<div class="phones side-by-side" id="phones">
<!-- 现状 -->
<div class="phone-wrap show" data-panel="old">
<div class="phone-label">
现状 <span class="pill pill--old">两处色块 + 状态 Tab</span>
</div>
<div class="phone">
<div class="status-bar"><span>9:41</span><span>Demo</span></div>
<header class="header">
<div class="header-row">
<button class="back" type="button" aria-label="返回">‹</button>
<h1>产品管理</h1>
</div>
<div class="status-tabs" aria-label="产品状态">
<span class="filter-tag active">全部</span>
<span class="filter-tag">上架</span>
<span class="filter-tag">下架</span>
</div>
<input class="search" type="search" placeholder="产品名称 / 编号 / 拼音" readonly />
</header>
<div class="filter-bar">
<button class="filter-trig" type="button"><span>全部分类</span><span class="arrow">▾</span></button>
<button class="filter-trig" type="button"><span>全部型号</span><span class="arrow">▾</span></button>
<button class="filter-trig" type="button"><span>更多</span><span class="arrow">▾</span></button>
</div>
<div class="scroll">
<article class="card">
<div class="card__head">
<div>
<div class="card__title">浓硫酸(工业级)</div>
<div class="card__sub">2026-10-12 14:20</div>
</div>
<span class="status-tag status-tag--success">上架</span>
</div>
<div class="emphasis">
<span>产品编号</span>
<strong>P-H2SO4-001</strong>
</div>
<div class="kv-block">
<div class="kv-row"><span>产品分类</span><span>危化品 / 酸类 / 硫酸</span></div>
<div class="kv-row"><span>规格型号</span><span>98% · 吨罐</span></div>
<div class="kv-row">
<span>危化标识</span>
<span class="tag-inline">
<span class="status-tag status-tag--danger">易制毒</span>
<span class="status-tag status-tag--warning">易制爆</span>
</span>
</div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">下架</button>
<button class="btn btn--danger" type="button">删除</button>
<button class="btn btn--primary" type="button">查看</button>
</div>
</article>
<article class="card">
<div class="card__head">
<div>
<div class="card__title">氢氧化钠片碱</div>
<div class="card__sub">2026-10-08 09:05</div>
</div>
<span class="status-tag status-tag--success">上架</span>
</div>
<div class="emphasis">
<span>产品编号</span>
<strong>P-NaOH-218</strong>
</div>
<div class="kv-block">
<div class="kv-row"><span>产品分类</span><span>基础化工 / 碱类</span></div>
<div class="kv-row"><span>规格型号</span><span>99% · 25kg袋</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">下架</button>
<button class="btn btn--danger" type="button">删除</button>
<button class="btn btn--primary" type="button">查看</button>
</div>
</article>
<p class="hint">共 2 条(示意)</p>
</div>
<button class="fab" type="button" aria-label="快捷操作">+</button>
</div>
</div>
<!-- 方案 -->
<div class="phone-wrap show" data-panel="new">
<div class="phone-label">
方案 <span class="pill pill--new">无色块 + 折叠展开</span>
</div>
<div class="phone">
<div class="status-bar"><span>9:41</span><span>Demo</span></div>
<header class="header">
<div class="header-row">
<button class="back" type="button" aria-label="返回">‹</button>
<h1>产品管理</h1>
</div>
<!-- 无状态 Tab:固定只查上架 -->
<input class="search" type="search" placeholder="产品名称 / 编号 / 拼音" readonly />
</header>
<div class="filter-bar">
<button class="filter-trig" type="button"><span>全部分类</span><span class="arrow">▾</span></button>
<button class="filter-trig" type="button"><span>全部型号</span><span class="arrow">▾</span></button>
<button class="filter-trig" type="button"><span>更多</span><span class="arrow">▾</span></button>
</div>
<div class="scroll">
<!-- 卡 1:对齐 PC 列,字段多 → 预览 2 行 + 更多字段 -->
<article class="card" data-card="1">
<div class="card__head">
<div>
<div class="card__title">
浓硫酸(工业级)
<span class="tag-inline" style="display:inline-flex;margin-left:6px;vertical-align:middle">
<span class="status-tag status-tag--danger">毒</span>
<span class="status-tag status-tag--warning">爆</span>
</span>
</div>
<div class="card__meta">上架 · P-H2SO4-001 · 2026-10-12 14:20</div>
</div>
<span class="status-tag status-tag--success">上架</span>
</div>
<div class="preview">
<div class="line"><span>产品分类</span><span>危化品 / 酸类 / 硫酸</span></div>
<div class="line"><span>规格型号</span><span>98% · 吨罐</span></div>
</div>
<button class="toggle" type="button" aria-expanded="false" data-toggle="1">
更多字段 <span class="toggle-icon" aria-hidden="true">▾</span>
</button>
<div class="extra" hidden data-extra="1">
<div class="row"><span>产品编号</span><span>P-H2SO4-001</span></div>
<div class="row"><span>品牌</span><span>山峡自营 · 华恒</span></div>
<div class="row"><span>别名</span><span>硫酸 · H2SO4</span></div>
<div class="row"><span>标准价(元)</span><span>1,280.00</span></div>
<div class="row"><span>状态</span><span>上架</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">下架</button>
<button class="btn btn--danger" type="button">删除</button>
<button class="btn btn--primary" type="button">查看</button>
</div>
</article>
<!-- 卡 2:部分字段为空时仍展示有值列;展开剩余 -->
<article class="card" data-card="2">
<div class="card__head">
<div>
<div class="card__title">氢氧化钠片碱</div>
<div class="card__meta">上架 · P-NaOH-218 · 2026-10-08 09:05</div>
</div>
<span class="status-tag status-tag--success">上架</span>
</div>
<div class="preview">
<div class="line"><span>产品分类</span><span>基础化工 / 碱类</span></div>
<div class="line"><span>规格型号</span><span>99% · 25kg袋</span></div>
</div>
<button class="toggle" type="button" aria-expanded="false" data-toggle="2">
更多字段 <span class="toggle-icon" aria-hidden="true">▾</span>
</button>
<div class="extra" hidden data-extra="2">
<div class="row"><span>产品编号</span><span>P-NaOH-218</span></div>
<div class="row"><span>品牌</span><span>—</span></div>
<div class="row"><span>别名</span><span>片碱 · 烧碱</span></div>
<div class="row"><span>标准价(元)</span><span>2,560.00</span></div>
<div class="row"><span>状态</span><span>上架</span></div>
</div>
<div class="actions">
<button class="btn" type="button">编辑</button>
<button class="btn" type="button">下架</button>
<button class="btn btn--danger" type="button">删除</button>
<button class="btn btn--primary" type="button">查看</button>
</div>
</article>
<p class="hint">共 2 条 · 仅上架产品(示意)</p>
</div>
<button class="fab" type="button" aria-label="快捷操作">+</button>
</div>
</div>
</div>
<div class="note">
<h3>字段对照(Web 产品管理列表 → 移动卡)</h3>
<p>
<strong>标题区:</strong>产品名称(旁贴「毒」「爆」小标,对齐 PC 名称列)+ 右上角状态 Tag「上架」;
meta 行:<code>上架 · 编号 · 创建时间</code>。<br />
<strong>预览(默认可见):</strong>产品分类、规格型号。<br />
<strong>展开:</strong>产品编号、品牌、别名、标准价(元)、状态。<br />
不含成本价(对齐 PC 默认可见列)。列表请求固定 <code>status=on</code>;下架后该卡从列表消失。
</p>
</div>
</div>
<script>
const phones = document.getElementById('phones')
const panels = {
old: document.querySelector('[data-panel="old"]'),
new: document.querySelector('[data-panel="new"]')
}
const buttons = document.querySelectorAll('.mode-bar button')
function setMode(mode) {
buttons.forEach((b) => b.classList.toggle('active', b.dataset.mode === mode))
if (mode === 'compare') {
phones.classList.add('side-by-side')
panels.old.classList.add('show')
panels.new.classList.add('show')
return
}
phones.classList.remove('side-by-side')
panels.old.classList.toggle('show', mode === 'old')
panels.new.classList.toggle('show', mode === 'new')
}
buttons.forEach((b) => b.addEventListener('click', () => setMode(b.dataset.mode)))
document.querySelectorAll('[data-toggle]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-toggle')
const extra = document.querySelector(`[data-extra="${id}"]`)
const open = btn.getAttribute('aria-expanded') === 'true'
const next = !open
btn.setAttribute('aria-expanded', String(next))
btn.childNodes[0].textContent = next ? '收起 ' : '更多字段 '
const icon = btn.querySelector('.toggle-icon')
if (icon) icon.textContent = next ? '▴' : '▾'
if (extra) extra.hidden = !next
})
})
</script>
</body>
</html>
+615
View File
@@ -0,0 +1,615 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>山峡化工 · H5 通用加载效果 Demo</title>
<style>
:root {
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-soft: #e9f9f2;
--bg: #f5f8f7;
--surface: #fff;
--text: #1f2a26;
--muted: #6b7b74;
--border: #dcebe4;
--header: linear-gradient(180deg, #087f57 0%, #087f57 28%, #0a9f6d 78%, #0a9f6d 100%);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
button { font: inherit; cursor: pointer; }
body {
background: #cfd9d4;
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
}
.stage { max-width: 1280px; margin: 0 auto; padding: 16px 12px 48px; }
.banner {
background: #1f2a26;
color: #fff;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.65;
}
.banner strong { color: #9fe3c4; }
.banner code {
background: rgba(255,255,255,.12);
padding: 1px 6px;
border-radius: 4px;
font-size: 12px;
}
.switcher {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.switcher button {
height: 36px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid #b7c9c0;
background: #fff;
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.switcher button[aria-pressed="true"] {
background: var(--brand);
border-color: var(--brand);
color: #fff;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
align-items: start;
}
.col-label {
margin-bottom: 8px;
padding: 10px 12px;
border-radius: 10px;
background: #fff;
border: 1px solid #b7c9c0;
font-size: 12px;
line-height: 1.55;
min-height: 108px;
}
.col-label h2 { font-size: 15px; margin-bottom: 4px; }
.col-label .rec { color: var(--brand-dark); font-weight: 700; }
.col-label.rec-box { border-color: #b7ebce; background: var(--brand-soft); }
.phone {
position: relative;
background: var(--bg);
border-radius: 28px;
overflow: hidden;
border: 8px solid #24312c;
height: 740px;
box-shadow: 0 18px 40px rgba(0,0,0,.16);
display: flex;
flex-direction: column;
}
.status-bar {
flex: 0 0 auto;
height: 28px;
background: var(--brand-dark);
color: rgba(255,255,255,.88);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
font-size: 11px;
}
.scene { display: none; flex: 1; min-height: 0; flex-direction: column; }
body[data-scene="list"] .scene-list,
body[data-scene="more"] .scene-more,
body[data-scene="detail"] .scene-detail,
body[data-scene="sheet"] .scene-sheet { display: flex; }
.header {
flex: 0 0 auto;
background: var(--header);
color: #fff;
padding: 6px 12px 14px;
border-radius: 0 0 22px 22px;
}
.header-row { display: flex; align-items: center; gap: 8px; min-height: 40px; }
.back { color: #fff; font-size: 20px; width: 32px; }
.header h1 { flex: 1; font-size: 17px; font-weight: 700; }
.search, .search-skel {
margin-top: 8px;
height: 36px;
border-radius: 999px;
background: #fff;
}
.search {
display: flex;
align-items: center;
padding: 0 14px;
color: #94a3b8;
font-size: 13px;
}
.filters {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
margin-top: 10px;
}
.filters span {
height: 28px;
border-radius: 8px;
background: rgba(255,255,255,.16);
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 12px;
}
.card {
background: var(--surface);
border: 1px solid rgba(220,235,228,.9);
border-radius: 14px;
box-shadow: 0 6px 16px rgba(10,159,109,.06);
padding: 14px;
margin-bottom: 12px;
}
.card-top, .skel-top { display: flex; justify-content: space-between; gap: 10px; }
.card h3 { font-size: 15px; }
.meta { margin-top: 4px; font-size: 12px; color: var(--muted); }
.tag {
flex-shrink: 0;
height: 22px;
padding: 0 8px;
border-radius: 999px;
background: var(--brand-soft);
color: var(--brand-dark);
font-size: 11px;
font-weight: 700;
display: inline-flex;
align-items: center;
}
.kv { margin-top: 10px; display: grid; gap: 6px; }
.kv div { display: flex; justify-content: space-between; font-size: 13px; }
.kv span:first-child { color: var(--muted); }
.foot-hint {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin: 4px 0 16px;
font-size: 12px;
color: var(--muted);
}
.skel {
border-radius: 6px;
background: linear-gradient(90deg, #e7f3ed 25%, #f7fbf9 37%, #e7f3ed 63%);
background-size: 400% 100%;
animation: shimmer 1.2s ease infinite;
}
.skel.title { width: 58%; height: 16px; }
.skel.sub { width: 42%; height: 12px; margin-top: 8px; }
.skel.tag { width: 52px; height: 22px; border-radius: 999px; }
.skel.line { width: 100%; height: 12px; }
.skel.short { width: 46%; }
.skel.mid { width: 72%; }
.skel.btn { width: 64px; height: 28px; border-radius: 8px; }
.skel-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
.skel-block { height: 36px; border-radius: 10px; margin-top: 10px; }
.caption {
margin: 2px 0 12px;
text-align: center;
font-size: 12px;
color: var(--muted);
}
.pulse {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 280px;
margin: 12px;
padding: 28px 20px;
text-align: center;
background:
radial-gradient(120% 80% at 50% 0%, rgba(10,159,109,.14), transparent 55%),
linear-gradient(180deg, #f3faf6, #f5f8f7);
border-radius: 16px;
}
.pulse.compact { min-height: 0; margin: 0; padding: 28px 16px 36px; background: transparent; }
.orb {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--brand);
box-shadow: 0 0 0 6px rgba(10,159,109,.14);
animation: pulse 1.2s ease-in-out infinite;
}
.dot.sm { width: 8px; height: 8px; box-shadow: 0 0 0 4px rgba(10,159,109,.14); }
.pulse h2, .spin-box h2 { font-size: 15px; font-weight: 700; }
.pulse p, .spin-box p { max-width: 220px; font-size: 12px; color: var(--muted); }
.bars { width: min(220px, 100%); display: grid; gap: 8px; margin-top: 12px; }
.progress {
height: 3px;
background: rgba(255,255,255,.28);
overflow: hidden;
}
.progress i, .sheet-progress i {
display: block;
width: 38%;
height: 100%;
background: #fff;
animation: slide 1.05s ease-in-out infinite;
}
.sheet-progress { height: 3px; background: #e7f3ed; overflow: hidden; }
.sheet-progress i { background: var(--brand); }
.ghost { opacity: .55; }
.spin-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 240px;
text-align: center;
padding: 24px;
}
.ring {
width: 28px;
height: 28px;
border: 3px solid #b7ebce;
border-top-color: var(--brand);
border-radius: 50%;
animation: spin .7s linear infinite;
}
.ring.sm { width: 16px; height: 16px; border-width: 2px; }
.ring.md { width: 22px; height: 22px; }
.scene-sheet { position: relative; background: var(--bg); }
.dim {
position: absolute;
inset: 0;
background: rgba(15, 23, 20, .45);
}
.sheet {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 62%;
background: #fff;
border-radius: 18px 18px 0 0;
display: flex;
flex-direction: column;
box-shadow: 0 -8px 24px rgba(0,0,0,.12);
}
.sheet-bar {
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
font-weight: 700;
}
.sheet-bar button {
border: 0;
background: transparent;
color: var(--brand);
font-size: 14px;
}
.sheet-body { flex: 1; overflow: auto; padding: 8px 16px 20px; }
.field { margin-bottom: 14px; }
.field label { display: block; margin-bottom: 6px; font-size: 12px; color: var(--muted); }
.fake-input {
height: 36px;
border-radius: 8px;
background: #f3f7f5;
border: 1px solid var(--border);
}
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: 0 0; }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(.82); opacity: .7; }
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes slide {
0% { transform: translateX(-120%); }
100% { transform: translateX(320%); }
}
</style>
</head>
<body data-scene="list">
<div class="stage">
<div class="banner">
<strong>通用加载 · 四选一。</strong>
四种方案共用一条规则:绿头标题始终是真实页面名;整块内容未出用「页面态」,列表已有数据再追加用「追加态」,弹层里等数据用「弹层态」。壳层登录和业务请求用同一种内容区效果,打开页面只出现一次加载。
色值走三墙 <code>#0a9f6d</code> / <code>#087f57</code>。点下面的场景,四台手机一起切换。
</div>
<div class="switcher" role="tablist" aria-label="加载场景">
<button type="button" data-scene="list" aria-pressed="true">列表首屏</button>
<button type="button" data-scene="more" aria-pressed="false">上拉加载更多</button>
<button type="button" data-scene="detail" aria-pressed="false">详情 / 表单</button>
<button type="button" data-scene="sheet" aria-pressed="false">底部弹层</button>
</div>
<div class="grid">
<section>
<div class="col-label rec-box">
<h2>A · 结构骨架 <span class="rec">推荐</span></h2>
占位和正式列表、详情、表单同结构。用户能认出即将出现的是卡片还是字段。翻页只在末尾补一张骨架卡。
</div>
<div class="phone" aria-label="方案 A 结构骨架">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<div class="scene scene-list">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
<div class="filters"><span>客户类型 ▾</span><span>客户级别 ▾</span><span>更多</span></div>
</header>
<div class="body">
<p class="caption">正在加载客户列表</p>
<article class="card"><div class="skel-top"><div style="flex:1"><div class="skel title"></div><div class="skel sub"></div></div><div class="skel tag"></div></div><div class="skel skel-block"></div><div class="skel-actions"><div class="skel btn"></div><div class="skel btn"></div><div class="skel btn"></div></div></article>
<article class="card"><div class="skel-top"><div style="flex:1"><div class="skel title"></div><div class="skel sub"></div></div><div class="skel tag"></div></div><div class="skel skel-block"></div><div class="skel-actions"><div class="skel btn"></div><div class="skel btn"></div><div class="skel btn"></div></div></article>
<article class="card"><div class="skel-top"><div style="flex:1"><div class="skel title"></div><div class="skel sub"></div></div><div class="skel tag"></div></div><div class="skel skel-block"></div><div class="skel-actions"><div class="skel btn"></div><div class="skel btn"></div><div class="skel btn"></div></div></article>
</div>
</div>
<div class="scene scene-more">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
<div class="filters"><span>客户类型 ▾</span><span>客户级别 ▾</span><span>更多</span></div>
</header>
<div class="body">
<article class="card">
<div class="card-top"><div><h3>安庆化轻贸易</h3><div class="meta">KH20261018001 · 经销商</div></div><span class="tag">跟进中</span></div>
<div class="kv"><div><span>负责人</span><span>王敏</span></div><div><span>最近跟进</span><span>昨天 16:20</span></div></div>
</article>
<article class="card"><div class="skel-top"><div style="flex:1"><div class="skel title"></div><div class="skel sub"></div></div><div class="skel tag"></div></div><div class="skel skel-block"></div></article>
<p class="caption">正在加载更多</p>
</div>
</div>
<div class="scene scene-detail">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户详情</h1></div>
</header>
<div class="body">
<p class="caption">正在加载客户详情</p>
<article class="card">
<div class="skel title" style="width:46%"></div>
<div class="kv" style="margin-top:16px">
<div class="skel line"></div><div class="skel line mid"></div><div class="skel line"></div><div class="skel line short"></div><div class="skel line"></div><div class="skel line mid"></div>
</div>
</article>
<article class="card">
<div class="skel sub" style="width:28%"></div>
<div class="skel skel-block"></div>
<div class="skel skel-block"></div>
</article>
</div>
</div>
<div class="scene scene-sheet">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
</header>
<div class="dim"></div>
<div class="sheet">
<div class="sheet-bar"><button type="button">关闭</button><span>更多筛选</span><span></span></div>
<div class="sheet-body">
<p class="caption">正在加载筛选项</p>
<div class="field"><div class="skel sub"></div><div class="skel skel-block"></div></div>
<div class="field"><div class="skel sub" style="width:36%"></div><div class="skel skel-block"></div></div>
<div class="field"><div class="skel sub" style="width:24%"></div><div class="skel skel-block"></div></div>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="col-label">
<h2>B · 呼吸点</h2>
把现在运输计划那套收成唯一组件:绿点、两行字、四条闪烁。列表、详情、弹层都用它,只改文案和尺寸。
</div>
<div class="phone" aria-label="方案 B 呼吸点">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<div class="scene scene-list">
<header class="header"><div class="header-row"><span class="back">←</span><h1>客户管理</h1></div></header>
<div class="pulse">
<div class="orb"><span class="dot"></span></div>
<h2>正在加载客户列表</h2>
<p>请稍候,正在获取客户数据…</p>
<div class="bars"><div class="skel line"></div><div class="skel line mid"></div><div class="skel line"></div><div class="skel line short"></div></div>
</div>
</div>
<div class="scene scene-more">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
</header>
<div class="body">
<article class="card">
<div class="card-top"><div><h3>安庆化轻贸易</h3><div class="meta">KH20261018001 · 经销商</div></div><span class="tag">跟进中</span></div>
</article>
<p class="foot-hint"><span class="dot sm"></span>正在加载更多</p>
</div>
</div>
<div class="scene scene-detail">
<header class="header"><div class="header-row"><span class="back">←</span><h1>客户详情</h1></div></header>
<div class="pulse">
<div class="orb"><span class="dot"></span></div>
<h2>正在加载客户详情</h2>
<p>请稍候,正在获取客户资料…</p>
<div class="bars"><div class="skel line"></div><div class="skel line mid"></div><div class="skel line"></div><div class="skel line short"></div></div>
</div>
</div>
<div class="scene scene-sheet">
<header class="header"><div class="header-row"><span class="back">←</span><h1>客户管理</h1></div></header>
<div class="dim"></div>
<div class="sheet">
<div class="sheet-bar"><button type="button">关闭</button><span>选择产品</span><span></span></div>
<div class="pulse compact">
<div class="orb"><span class="dot"></span></div>
<h2>正在加载产品</h2>
<p>请稍候…</p>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="col-label">
<h2>C · 顶栏进度</h2>
绿头底部一条白色不定进度。内容用很淡的骨架,已加载的卡片保持原样。适合觉得整页大加载太重的时候。
</div>
<div class="phone" aria-label="方案 C 顶栏进度">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<div class="scene scene-list">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
<div class="filters"><span>客户类型 ▾</span><span>客户级别 ▾</span><span>更多</span></div>
</header>
<div class="progress" aria-hidden="true"><i></i></div>
<div class="body ghost">
<article class="card"><div class="skel-top"><div style="flex:1"><div class="skel title"></div><div class="skel sub"></div></div><div class="skel tag"></div></div><div class="skel skel-block"></div></article>
<article class="card"><div class="skel-top"><div style="flex:1"><div class="skel title"></div><div class="skel sub"></div></div><div class="skel tag"></div></div><div class="skel skel-block"></div></article>
</div>
</div>
<div class="scene scene-more">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
</header>
<div class="progress" aria-hidden="true"><i></i></div>
<div class="body">
<article class="card">
<div class="card-top"><div><h3>安庆化轻贸易</h3><div class="meta">KH20261018001 · 经销商</div></div><span class="tag">跟进中</span></div>
</article>
<article class="card">
<div class="card-top"><div><h3>池州新材料</h3><div class="meta">KH20261012018 · 终端</div></div><span class="tag">重点</span></div>
</article>
</div>
</div>
<div class="scene scene-detail">
<header class="header"><div class="header-row"><span class="back">←</span><h1>客户详情</h1></div></header>
<div class="progress" aria-hidden="true"><i></i></div>
<div class="body ghost">
<article class="card"><div class="skel title" style="width:40%"></div><div class="kv" style="margin-top:16px"><div class="skel line"></div><div class="skel line mid"></div><div class="skel line"></div><div class="skel line short"></div></div></article>
</div>
</div>
<div class="scene scene-sheet">
<header class="header"><div class="header-row"><span class="back">←</span><h1>客户管理</h1></div></header>
<div class="dim"></div>
<div class="sheet">
<div class="sheet-bar"><button type="button">关闭</button><span>更多筛选</span><span></span></div>
<div class="sheet-progress" aria-hidden="true"><i></i></div>
<div class="sheet-body ghost">
<div class="field"><label>客户类型</label><div class="fake-input"></div></div>
<div class="field"><label>客户级别</label><div class="fake-input"></div></div>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="col-label">
<h2>D · 单一转圈</h2>
全站只留一种绿色圆环。页面用 28px 纵向,翻页用 16px 横排,弹层用 22px。文案统一成「正在加载 + 对象名」。
</div>
<div class="phone" aria-label="方案 D 单一转圈">
<div class="status-bar"><span>9:41</span><span>山峡化工</span></div>
<div class="scene scene-list">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
<div class="filters"><span>客户类型 ▾</span><span>客户级别 ▾</span><span>更多</span></div>
</header>
<div class="spin-box">
<span class="ring"></span>
<h2>正在加载客户列表</h2>
</div>
</div>
<div class="scene scene-more">
<header class="header">
<div class="header-row"><span class="back">←</span><h1>客户管理</h1></div>
<div class="search">客户名称 / 编号 / 手机号</div>
</header>
<div class="body">
<article class="card">
<div class="card-top"><div><h3>安庆化轻贸易</h3><div class="meta">KH20261018001 · 经销商</div></div><span class="tag">跟进中</span></div>
</article>
<p class="foot-hint"><span class="ring sm"></span>正在加载更多</p>
</div>
</div>
<div class="scene scene-detail">
<header class="header"><div class="header-row"><span class="back">←</span><h1>编辑客户</h1></div></header>
<div class="spin-box">
<span class="ring"></span>
<h2>正在加载客户资料</h2>
</div>
</div>
<div class="scene scene-sheet">
<header class="header"><div class="header-row"><span class="back">←</span><h1>客户管理</h1></div></header>
<div class="dim"></div>
<div class="sheet">
<div class="sheet-bar"><button type="button">关闭</button><span>选择产品</span><span></span></div>
<div class="spin-box">
<span class="ring md"></span>
<h2>正在加载产品</h2>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
const buttons = document.querySelectorAll(".switcher button");
buttons.forEach((button) => {
button.addEventListener("click", () => {
const scene = button.dataset.scene;
document.body.dataset.scene = scene;
buttons.forEach((item) => {
item.setAttribute("aria-pressed", item === button ? "true" : "false");
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,42 @@
-- 小程序 CRM 客户管理 H5 入口。仅新增/恢复移动菜单及相应角色绑定,不修改 Web 菜单。
SET NAMES utf8mb4;
START TRANSACTION;
INSERT INTO sys_menu (
id, menu_name, parent_id, menu_type, path, component, perms,
is_frame, visible, status, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
)
VALUES
('MINI_CRM_ROOT', 'CRM管理', '1', 'M', '/mini/crm', NULL, NULL,
1, '1', '1', 'shanxia2.0', 2, '小程序CRM管理', 'system', '', NOW(), NULL, NOW(), 0),
('MINI_CRM_CUSTOMER', '客户管理', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL, 'mini:crm:customer:list',
1, '1', '1', 'shanxia2.0', 6, '小程序 WebView CRM 客户管理', 'system', '', NOW(), NULL, NOW(), 0)
ON DUPLICATE KEY UPDATE del_flag = 0, update_time = NOW();
-- 根据当前 Web 客户菜单权限查找角色,不依赖历史菜单 ID;补齐全部祖先。
INSERT INTO sys_role_menu (
id, role_id, menu_id, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
)
SELECT
MD5(CONCAT(r.id, '|', target.menu_id, '|mini-crm-customer-webview|v1')),
r.id, target.menu_id, r.tenant_no, 0, '小程序客户管理:随 Web crm:customer:list 补绑',
'system', '', NOW(), NULL, NOW(), 0
FROM sys_role r
CROSS JOIN (
SELECT '1' AS menu_id UNION ALL SELECT 'MINI_CRM_ROOT' UNION ALL SELECT 'MINI_CRM_CUSTOMER'
) target
WHERE r.del_flag = 0
AND EXISTS (
SELECT 1 FROM sys_role_menu rm
JOIN sys_menu m ON m.id = rm.menu_id AND m.del_flag = 0
WHERE rm.role_id = r.id AND rm.del_flag = 0 AND m.perms = 'crm:customer:list'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu existing
WHERE existing.role_id = r.id AND existing.menu_id = target.menu_id AND existing.del_flag = 0
)
ON DUPLICATE KEY UPDATE del_flag = 0, update_time = NOW();
COMMIT;
@@ -0,0 +1,77 @@
-- 小程序运输管理 → 危化品证上传(独立 H5)。MySQL 5.7。
-- 2026-10-17 已只读确认测试库:1(移动端) → MINI_PL_ROOT(运输管理)。
-- 执行前备份 sys_menu / sys_role_menu,并确认目标库及下面的父级查询返回一行。
-- 不新建/更改父级,不改变 Web 菜单和业务表,不额外授予业务写权限。
SET NAMES utf8mb4;
START TRANSACTION;
SELECT p.id, p.menu_name, p.parent_id, p.tenant_no
FROM sys_menu p
JOIN sys_menu root ON root.id = p.parent_id
WHERE p.id = 'MINI_PL_ROOT' AND p.parent_id = '1'
AND p.path = '/mini/pl' AND p.tenant_no = 'shanxia2.0' AND p.del_flag = 0
AND root.path = '/mini' AND root.parent_id = '0'
AND root.tenant_no = p.tenant_no AND root.del_flag = 0;
INSERT INTO sys_menu (
id, menu_name, parent_id, menu_type, path, component, perms,
is_frame, visible, status, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
)
SELECT
'329823eb-e3af-49b2-b656-08f2c2a713f3', '危化品证上传', p.id, 'C', 'mp-webview', NULL,
'mini:pl:plan-cert:list', 1, '1', '1', p.tenant_no, 5,
'小程序 WebView 危化品证上传', 'system', '', NOW(), NULL, NOW(), 0
FROM sys_menu p
JOIN sys_menu root ON root.id = p.parent_id
WHERE p.id = 'MINI_PL_ROOT' AND p.parent_id = '1'
AND p.path = '/mini/pl' AND p.tenant_no = 'shanxia2.0' AND p.del_flag = 0
AND root.path = '/mini' AND root.parent_id = '0'
AND root.tenant_no = p.tenant_no AND root.del_flag = 0
ON DUPLICATE KEY UPDATE visible = '1', status = '1', del_flag = 0, update_time = NOW();
-- 随已有 Web 列表权限角色补齐移动根、运输目录、叶子;不依赖历史 Web 菜单 ID。
INSERT INTO sys_role_menu (
id, role_id, menu_id, tenant_no, order_num, remark,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
)
SELECT
MD5(CONCAT(r.id, '|', target.menu_id, '|mini-pl-plan-cert-webview|v1')),
r.id, target.menu_id, r.tenant_no, 0, '小程序危化品证上传:随 Web 列表权限补绑',
'system', '', NOW(), NULL, NOW(), 0
FROM sys_role r
CROSS JOIN (
SELECT '1' AS menu_id UNION ALL SELECT 'MINI_PL_ROOT'
UNION ALL SELECT '329823eb-e3af-49b2-b656-08f2c2a713f3'
) target
WHERE r.del_flag = 0 AND r.tenant_no = 'shanxia2.0'
AND EXISTS (
SELECT 1 FROM sys_menu leaf
JOIN sys_menu p ON p.id = leaf.parent_id
JOIN sys_menu root ON root.id = p.parent_id
WHERE leaf.id = '329823eb-e3af-49b2-b656-08f2c2a713f3'
AND leaf.perms = 'mini:pl:plan-cert:list' AND leaf.del_flag = 0
AND leaf.tenant_no = r.tenant_no
AND p.id = 'MINI_PL_ROOT' AND p.parent_id = '1' AND p.path = '/mini/pl'
AND p.tenant_no = r.tenant_no AND p.del_flag = 0
AND root.path = '/mini' AND root.parent_id = '0'
AND root.tenant_no = r.tenant_no AND root.del_flag = 0
)
AND EXISTS (
SELECT 1 FROM sys_role_menu web
JOIN sys_menu m ON m.id = web.menu_id AND m.del_flag = 0
WHERE web.role_id = r.id AND web.del_flag = 0
AND web.tenant_no = r.tenant_no AND m.tenant_no = r.tenant_no
AND m.perms = 'pl:plan-cert:list'
)
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu existing
WHERE existing.role_id = r.id AND existing.menu_id = target.menu_id
AND existing.tenant_no = r.tenant_no AND existing.del_flag = 0
)
ON DUPLICATE KEY UPDATE del_flag = 0, update_time = NOW();
COMMIT;
SELECT id, menu_name, parent_id, perms, visible, status, del_flag
FROM sys_menu WHERE id = '329823eb-e3af-49b2-b656-08f2c2a713f3';
@@ -0,0 +1,683 @@
# 小程序 WebView · CRM 产品管理 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:** 在小程序工作台开放「CRM管理 → 产品管理」,通过既有 `mp-webview` 壳打开 `web-admin` `/#/m/crm/product*`,提供与桌面产品管理同等核心能力(列表/详情/增改删/单条上下架/页内分类维护),不含导入导出与批量上下架。
**Architecture:** 复用运输计划 WebView 模板(ticket → exchange → `MpWebviewShell`)。新建移动端产品页,样式对齐 `/#/m/pl/plan*`(`mp-webview.scss` 三墙 Token)。业务 API 全部复用 `@/api/crm/product`。桌面 `ProductFormDialog` 增加 `variant="page"` 供 H5 全屏复用;分类维护做独立移动页(不硬套桌面 `ProductCategoryMaintainPanel` 表格)。菜单 SQL 幂等写入 `MINI_CRM_*`,并给已有 `crm:product:list` 的角色自动补绑。
**Tech Stack:** Vue 3 · Vue Router hash · Element Plus · Pinia · uni-app web-view · MySQL `sys_menu`/`sys_role_menu` · Node test(`pnpm exec tsx --test`)
**Spec:** `docs/superpowers/specs/2026-10-15-mp-webview-crm-product-design.md`
---
## 文件结构
| 路径 | 职责 |
|------|------|
| `web-admin/src/utils/mp-crm-product-card.ts` | 列表卡片展示字段、状态筛选映射(可单测) |
| `web-admin/src/utils/mp-crm-product-card.test.ts` | 纯函数单测 |
| `web-admin/src/utils/mp-webview.ts` | 增加 `MP_CRM_PRODUCT_LIST_PATH` 常量 |
| `web-admin/src/views/m/crm/product/index.vue` | 列表(搜索/状态筛选/卡片/Speed Dial) |
| `web-admin/src/views/m/crm/product/detail.vue` | 详情 + 底栏编辑/上下架/删除 |
| `web-admin/src/views/m/crm/product/form.vue` | 路由壳,挂载 `ProductFormDialog variant="page"` |
| `web-admin/src/views/m/crm/product/categories.vue` | 分类树维护(移动端) |
| `web-admin/src/components/business/product-form-dialog/index.vue` | 增加 `variant: 'dialog' \| 'page'` |
| `web-admin/src/router/routes/staticRoutes.ts` | 注册 `/m/crm/product*`(catch-all 前) |
| `miniprogram/config/workbench-menus.js` | CRM `visible: true`;产品 `enabled` + code + perms |
| `miniprogram/config/api.js` | `WORKBENCH_ROUTE_MAP['crm-product-webview']` |
| `docs/sql/migrations/2026-10-15-mini-crm-product-webview-menus.sql` | 菜单 + 角色补绑 |
| `docs/web-admin-mp-webview-modules.md` | 补充 CRM 产品对照行 |
**明确不改:** `mp-webview.vue` 鉴权协议、后端产品业务 API、桌面 `/#/crm/product` 默认行为(仅表单组件增加可选 `variant`,默认仍为 `dialog`)。
---
### Task 1: 列表卡片纯函数 + 单测
**Files:**
- Create: `web-admin/src/utils/mp-crm-product-card.ts`
- Create: `web-admin/src/utils/mp-crm-product-card.test.ts`
- [ ] **Step 1: 写失败单测**
```ts
// web-admin/src/utils/mp-crm-product-card.test.ts
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
MP_PRODUCT_STATUS_FILTERS,
buildProductListQuery,
buildProductCard,
productStatusLabel,
productStatusTagType
} from './mp-crm-product-card'
describe('MP_PRODUCT_STATUS_FILTERS', () => {
it('defaults on first and includes all/off', () => {
assert.equal(MP_PRODUCT_STATUS_FILTERS[0].value, 'on')
assert.deepEqual(
MP_PRODUCT_STATUS_FILTERS.map((x) => x.value),
['on', 'all', 'off']
)
})
})
describe('buildProductListQuery', () => {
it('defaults statusIn on and passes keyword/page', () => {
assert.deepEqual(
buildProductListQuery({ page: 1, pageSize: 20, statusFilter: 'on', keyword: ' 酸 ' }),
{ page: 1, pageSize: 20, statusIn: 'on', keyword: '酸' }
)
})
it('omits statusIn when filter is all', () => {
const q = buildProductListQuery({ page: 2, pageSize: 10, statusFilter: 'all', keyword: '' })
assert.equal(q.page, 2)
assert.equal(q.statusIn, undefined)
assert.equal('keyword' in q, false)
})
it('sets statusIn off', () => {
assert.equal(
buildProductListQuery({ page: 1, pageSize: 20, statusFilter: 'off', keyword: '' }).statusIn,
'off'
)
})
})
describe('buildProductCard', () => {
it('maps fields and tags', () => {
const card = buildProductCard({
id: 'p1',
productNo: 'SX001',
productName: '硫酸',
categoryPath: '化工 / 酸类',
model: '98%',
status: 'on',
isToxic: 1,
isExplosive: 0
})
assert.equal(card.id, 'p1')
assert.equal(card.productNo, 'SX001')
assert.equal(card.statusLabel, '上架')
assert.equal(card.statusTagType, 'success')
assert.equal(card.showToxic, true)
assert.equal(card.showExplosive, false)
})
})
describe('productStatusTagType', () => {
it('maps on→success off→info', () => {
assert.equal(productStatusTagType('on'), 'success')
assert.equal(productStatusTagType('off'), 'info')
assert.equal(productStatusLabel('on'), '上架')
assert.equal(productStatusLabel('off'), '下架')
})
})
```
- [ ] **Step 2: 跑测确认失败**
```bash
cd web-admin && pnpm exec tsx --test src/utils/mp-crm-product-card.test.ts
```
Expected: 无法解析 `./mp-crm-product-card` 或 FAIL
- [ ] **Step 3: 实现纯函数**
```ts
// web-admin/src/utils/mp-crm-product-card.ts
export type MpProductStatusFilter = 'on' | 'all' | 'off'
export const MP_PRODUCT_STATUS_FILTERS: Array<{ value: MpProductStatusFilter; label: string }> = [
{ value: 'on', label: '上架' },
{ value: 'all', label: '全部' },
{ value: 'off', label: '下架' }
]
export function productStatusLabel(status?: string): string {
if (status === 'off') return '下架'
if (status === 'on') return '上架'
return status || '—'
}
export function productStatusTagType(status?: string): 'success' | 'info' | 'warning' {
if (status === 'on') return 'success'
if (status === 'off') return 'info'
return 'warning'
}
export function buildProductListQuery(input: {
page: number
pageSize: number
statusFilter: MpProductStatusFilter
keyword: string
}): Record<string, unknown> {
const params: Record<string, unknown> = {
page: input.page,
pageSize: input.pageSize
}
if (input.statusFilter === 'on' || input.statusFilter === 'off') {
params.statusIn = input.statusFilter
}
const kw = String(input.keyword || '').trim()
if (kw) params.keyword = kw
return params
}
export function buildProductCard(row: {
id: string
productNo?: string
productName?: string
categoryPath?: string
categoryName?: string
model?: string
status?: string
isToxic?: number
isExplosive?: number
}) {
return {
id: row.id,
productNo: row.productNo || '—',
productName: row.productName || '—',
categoryPath: row.categoryPath || row.categoryName || '—',
model: row.model || '—',
status: row.status || '',
statusLabel: productStatusLabel(row.status),
statusTagType: productStatusTagType(row.status),
showToxic: Number(row.isToxic) === 1,
showExplosive: Number(row.isExplosive) === 1
}
}
```
- [ ] **Step 4: 跑测确认通过**
```bash
cd web-admin && pnpm exec tsx --test src/utils/mp-crm-product-card.test.ts
```
Expected: 全部 pass
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/utils/mp-crm-product-card.ts web-admin/src/utils/mp-crm-product-card.test.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp CRM product card query helpers.
EOF
)"
```
---
### Task 2: 菜单 SQL(移动端 CRM + 产品 + 角色补绑)
**Files:**
- Create: `docs/sql/migrations/2026-10-15-mini-crm-product-webview-menus.sql`
- [ ] **Step 1: 写入完整幂等 SQL**
```sql
-- 小程序:CRM管理 → 产品管理(WebView)
-- 给已有 Web 产品管理菜单 CRM00000000000000000000000000003(crm:product:list)的角色自动补绑
SET NAMES utf8mb4;
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_CRM_ROOT', 'CRM管理', '1', 'M', '/mini/crm', NULL, NULL,
1, '1', '1', 'shanxia2.0', 2, '小程序CRM管理',
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_ROOT' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_CRM_PRODUCT', '产品管理', 'MINI_CRM_ROOT', 'C', 'mp-webview', NULL,
'mini:crm:product:list',
1, '1', '1', 'shanxia2.0', 1, '小程序 WebView CRM 产品管理',
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_CRM_PRODUCT' AND `del_flag` = 0
);
INSERT INTO `sys_role_menu` (
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
MD5(CONCAT(r.id, '|', m.menu_id, '|mini-crm-product-webview|v1')),
r.id,
m.menu_id,
IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'),
0,
'小程序CRM产品管理:随 Web 产品权限补绑',
'system', '', NOW(), NULL, NOW(), 0
FROM `sys_role` r
JOIN `sys_role_menu` web
ON web.role_id = r.id
AND web.menu_id = 'CRM00000000000000000000000000003'
AND web.del_flag = 0
JOIN (
SELECT '1' AS menu_id UNION ALL
SELECT 'MINI_CRM_ROOT' UNION ALL
SELECT 'MINI_CRM_PRODUCT'
) m
WHERE r.del_flag = 0
AND NOT EXISTS (
SELECT 1 FROM `sys_role_menu` x
WHERE x.role_id = r.id AND x.menu_id = m.menu_id AND x.del_flag = 0
);
```
- [ ] **Step 2: Commit(本 Task 只交 SQL;测试/生产库实施时执行)**
```bash
git add docs/sql/migrations/2026-10-15-mini-crm-product-webview-menus.sql
git commit -m "$(cat <<'EOF'
docs(sql): add miniprogram CRM product webview menus and role bind.
EOF
)"
```
---
### Task 3: 静态路由 + 路径常量
**Files:**
- Modify: `web-admin/src/utils/mp-webview.ts`
- Modify: `web-admin/src/router/routes/staticRoutes.ts`
- [ ] **Step 1: 增加列表路径常量**
在 `mp-webview.ts` 的 `MP_PLAN_LIST_PATH` 旁增加:
```ts
export const MP_CRM_PRODUCT_LIST_PATH = '/m/crm/product'
```
- [ ] **Step 2: 在 catch-all 之前注册路由(顺序:list → new → categories → :id/edit → :id)**
```ts
{
path: '/m/crm/product',
name: 'MpCrmProductList',
component: () => import('@views/m/crm/product/index.vue'),
meta: { title: '产品管理', isHideTab: true, isMpWebview: true }
},
{
path: '/m/crm/product/new',
name: 'MpCrmProductNew',
component: () => import('@views/m/crm/product/form.vue'),
meta: { title: '新增产品', isHideTab: true, isMpWebview: true }
},
{
path: '/m/crm/product/categories',
name: 'MpCrmProductCategories',
component: () => import('@views/m/crm/product/categories.vue'),
meta: { title: '产品分类', isHideTab: true, isMpWebview: true }
},
{
path: '/m/crm/product/:id/edit',
name: 'MpCrmProductEdit',
component: () => import('@views/m/crm/product/form.vue'),
meta: { title: '编辑产品', isHideTab: true, isMpWebview: true }
},
{
path: '/m/crm/product/:id',
name: 'MpCrmProductDetail',
component: () => import('@views/m/crm/product/detail.vue'),
meta: { title: '产品详情', isHideTab: true, isMpWebview: true }
},
```
本步可先建空壳 vue(仅 `MpWebviewShell` + 标题),或与 Task 4–7 同批落地页面;**不得**把路由放在 `/:pathMatch(.*)*` 之后。
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/utils/mp-webview.ts web-admin/src/router/routes/staticRoutes.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): register /m/crm/product static routes.
EOF
)"
```
---
### Task 4: 移动端列表页
**Files:**
- Create: `web-admin/src/views/m/crm/product/index.vue`
对照:`web-admin/src/views/m/pl/plan/index.vue`(Header / filter / card / speed-dial / `goBack` → 工作台)。
- [ ] **Step 1: 实现列表页骨架与数据加载**
关键行为:
1. `MpWebviewShell @ready` 后再拉列表
2. `fetchProductList(buildProductListQuery(...))`;解析 `res.list` / `res.total`(与桌面一致)
3. 分类全路径:先 `fetchCategoryTree`,用 `buildPathLabelMapFromTreeRoots` 或扁平 + `buildTreePathLabelFromFlat`(`@/utils/tree-path`,分隔符 ` / `)填到卡片 `categoryPath`
4. 状态筛选面板用 `MP_PRODUCT_STATUS_FILTERS`;默认 `on`
5. 搜索框 enter / 清除后 `reload`
6. 卡片点击 → `router.push(\`/m/crm/product/${id}\`)`
7. Speed Dial:新增 → `/m/crm/product/new`;产品分类 → `/m/crm/product/categories`
8. 返回:`goMpWebviewWorkbench()`(与运输计划列表相同,来自 `@/utils/mp-webview`)
9. `import '@/styles/mp-webview.scss'`
10. **不要**出现导入/导出/批量上下架/多选
列表卡片模板要点:
```vue
<article
v-for="card in cards"
:key="card.id"
class="mp-wv-card"
@click="goDetail(card.id)"
>
<div class="mp-wv-card__title">{{ card.productName }}</div>
<div class="mp-wv-card__meta">{{ card.productNo }} · {{ card.model }}</div>
<div class="mp-wv-card__meta">{{ card.categoryPath }}</div>
<div class="mp-wv-card__tags">
<el-tag :type="card.statusTagType" size="small">{{ card.statusLabel }}</el-tag>
<el-tag v-if="card.showToxic" type="danger" size="small">易制毒</el-tag>
<el-tag v-if="card.showExplosive" type="warning" size="small">易制爆</el-tag>
</div>
</article>
```
分页:首版可用「加载更多」或底部分页;与运输计划现网一致即可(若运输计划是一次拉满/滚加载,产品跟同样模式;否则 `page`/`pageSize` + 触底加载)。
- [ ] **Step 2: 本地验证**
```bash
cd web-admin && pnpm dev
```
浏览器打开(需已登录桌面或按现网 mp 调试方式)`/#/m/crm/product`,确认无权限/有权限两种表现、默认只见上架、搜索可用。
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/m/crm/product/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp webview CRM product list page.
EOF
)"
```
---
### Task 5: 详情页(编辑 / 单条上下架 / 删除)
**Files:**
- Create: `web-admin/src/views/m/crm/product/detail.vue`
对照:`web-admin/src/views/m/pl/plan/detail.vue` 的 header + section + 底栏;字段对齐桌面 `product-view-dialog` / `crm/product` 查看区。
- [ ] **Step 1: 实现详情**
1. `fetchProductDetail({ id })`
2. 分区展示:基础信息(编号/名称/拼音/分类全路径/型号/条码/税率/库存/预警/状态/毒爆)、描述备注、图片(逗号拆 URL 预览)、品牌列表、别名列表
3. 底栏:
- 编辑 → `/m/crm/product/:id/edit`
- 上架/下架:当前 `on` 则显示「下架」,否则「上架」;确认后 `fetchUpdateProductStatus({ ids: [id], status: 'on'|'off' })` 成功后刷新
- 删除:`:disabled="!canDeleteByCreator(detail)"`,提示用现有 `CREATOR_DELETE_TIP`;确认后 `fetchDeleteProduct({ id })`,成功回列表
4. 返回列表:`router.push(MP_CRM_PRODUCT_LIST_PATH)`
5. 加载中用运输计划同款 skeleton class
- [ ] **Step 2: 手工验证上下架与删除权限**
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/m/crm/product/detail.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp webview CRM product detail actions.
EOF
)"
```
---
### Task 6: ProductFormDialog `variant="page"` + form 路由壳
**Files:**
- Modify: `web-admin/src/components/business/product-form-dialog/index.vue`
- Create: `web-admin/src/views/m/crm/product/form.vue`
对照:`PlPlanFormDialog` 的 `variant` / `isPageVariant`;`views/m/pl/plan/form.vue`。
- [ ] **Step 1: 给 ProductFormDialog 增加 variant(默认 dialog,桌面行为不变)**
```ts
const props = withDefaults(
defineProps<{
modelValue: boolean
productId?: string | null
variant?: 'dialog' | 'page'
}>(),
{ variant: 'dialog' }
)
const isPageVariant = computed(() => props.variant === 'page')
```
模板:
- `variant=dialog`:保持现有 `el-dialog`
- `variant=page`:不渲染 dialog 外壳;根节点为 `div.mp-product-form-page`(全宽、底栏 sticky);`label-position="top"`;栅格改为单列(`form-grid-1` 或覆盖 `form-grid-3` 为 1 列);页内提供取消/确定底栏(图标 Close/Check)
- `visible`:page 模式下 `modelValue` 可恒为 `true`;`@close`/`success` 事件保留
- 打开时仍加载详情 / 分类树;提交逻辑不变(`fetchCreateProduct` / `fetchUpdateProduct`,含 brands、aliases、images)
- page 模式下「维护分类」可改为 `router.push('/m/crm/product/categories')`(若桌面有分类入口按钮则分支;没有则仅靠 Speed Dial / 表单旁链)
桌面调用处**不要**传 `variant`,保持 dialog。
- [ ] **Step 2: 实现 form.vue 壳**
```vue
<template>
<MpWebviewShell>
<header class="mp-wv-header">
<div class="mp-wv-header__row">
<button class="mp-wv-header__back" type="button" @click="goBack">←</button>
<h1 class="mp-wv-header__title">{{ pageTitle }}</h1>
</div>
</header>
<ProductFormDialog
variant="page"
:model-value="true"
:product-id="productId"
@success="onSuccess"
@close="goBack"
/>
</MpWebviewShell>
</template>
```
- `productId`:`MpCrmProductNew` → `undefined`;edit → `route.params.id`
- 成功:回详情或列表(新增建议进详情 `id`;若接口返回 id 用返回值,否则回列表)
- `import '@/styles/mp-webview.scss'`
- [ ] **Step 3: 验证全字段可保存(含图片、品牌、别名)**
- [ ] **Step 4: Commit**
```bash
git add web-admin/src/components/business/product-form-dialog/index.vue web-admin/src/views/m/crm/product/form.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add page variant for product form in mp webview.
EOF
)"
```
---
### Task 7: 分类维护移动页
**Files:**
- Create: `web-admin/src/views/m/crm/product/categories.vue`
- [ ] **Step 1: 实现移动端分类树**
不要直接嵌桌面 `ProductCategoryMaintainPanel`(表格过重)。实现:
1. `fetchCategoryTree` 展示可展开树卡片(本级名称;缩进表示层级)
2. 顶栏:返回产品列表 +「新增」
3. 节点操作:新增子级 / 编辑 / 删除(删除用 `canDeleteByCreator` + `fetchDeleteCategory`)
4. 新增/编辑:Bottom Sheet 或页内表单(字段与桌面分类表单一致:名称、父级、状态等——对照 `product-category-maintain-panel` 提交字段)
5. 成功后刷新树;返回 `MP_CRM_PRODUCT_LIST_PATH` 或 `router.back()`
API:
- `fetchCategoryTree` / `fetchCreateCategory` / `fetchUpdateCategory` / `fetchDeleteCategory`
- [ ] **Step 2: 手工验证增删改后,产品表单树选可见新节点**
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/m/crm/product/categories.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add mp webview CRM product category page.
EOF
)"
```
---
### Task 8: 小程序工作台入口
**Files:**
- Modify: `miniprogram/config/workbench-menus.js`
- Modify: `miniprogram/config/api.js`
- [ ] **Step 1: 打开 CRM 模块并配置产品菜单**
将 CRM 块改为:
```js
{
key: 'crm',
title: 'CRM 管理',
desc: '产品管理',
iconText: 'CRM',
visible: true,
menus: [
{
name: '产品管理',
enabled: true,
code: 'crm-product-webview',
icon: '📦',
perms: 'mini:crm:product:list'
}
// 其它 CRM 叶子本期可删除或保留 enabled:false;若保留且无 code,须确保过滤器不展示
]
}
```
建议本期 **只保留产品管理一项** enabled,避免灰卡噪音;其它菜单可暂时从数组移除或 `enabled: false` 且无 perms(过滤器应隐藏)。
- [ ] **Step 2: WORKBENCH_ROUTE_MAP**
在 `miniprogram/config/api.js`:
```js
'crm-product-webview':
PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/product')
```
- [ ] **Step 3: Commit**
```bash
git add miniprogram/config/workbench-menus.js miniprogram/config/api.js
git commit -m "$(cat <<'EOF'
feat(miniprogram): enable CRM product webview workbench entry.
EOF
)"
```
---
### Task 9: 接入文档更新 + 回归清单
**Files:**
- Modify: `docs/web-admin-mp-webview-modules.md`
- [ ] **Step 1: 在「运输计划对照」旁增加 CRM 产品对照**
```markdown
## CRM 产品对照
- 列表 `/m/crm/product`,详情 `/:id`,表单 `/new` `/:id/edit`,分类 `/categories`
- 工作台 code `crm-product-webview`,perms `mini:crm:product:list`
- 业务 API:现有 `/web/crm/products/*`、`/web/crm/product-categories/*`
- 不做:导入/导出/批量上下架
```
- [ ] **Step 2: 回归(对照 spec §10)**
在测试库执行 Task 2 SQL 后:
1. 有 Web 产品权的角色:小程序工作台可见「CRM管理 → 产品管理」
2. 进入后列表与桌面同账号数据一致(默认上架)
3. 搜索、状态筛选、详情、新增、编辑、单条上下架、删除(非创建人不可删)
4. 分类维护后表单可选
5. 图片/品牌/别名保存回显
6. 401 回小程序,不到桌面登录
7. 移动端无导入/导出/批量上下架
8. 桌面 `/#/crm/product` 仍正常(dialog 形态)
- [ ] **Step 3: Commit**
```bash
git add docs/web-admin-mp-webview-modules.md
git commit -m "$(cat <<'EOF'
docs: document CRM product as mp webview module.
EOF
)"
```
---
## Spec 覆盖自检
| Spec 项 | Task |
|---------|------|
| 嵌套 H5 / 复用壳 | 3–8(不改 ticket 协议) |
| 列表+详情+增改删+单条上下架 | 4, 5, 6 |
| 不做导入导出批量 | 4 明确禁止;手工回归 |
| 全字段表单 | 6 |
| 页内分类 CRUD | 7 |
| 三墙样式对齐运输计划 | 4–7 import `mp-webview.scss` |
| 菜单 MINI_CRM_* + 角色补绑 | 2 |
| 工作台 visible + route map | 8 |
| 双权限 | 2 + 8 + 现有 API |
| 发版说明 | 9 + spec §9 |
---
## 执行说明
实施前建议在隔离 worktree(`using-git-worktrees`)。每 Task 完成后跑对应单测/手工点,再 commit。后端无新接口;本地需能访问现有 CRM 产品 API。
@@ -0,0 +1,288 @@
# 运输计划相关列表导出扩展 · 段次×产品行 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:** 危化品证、运输归档、合同完善导出对齐列表段次×产品行(方案 A);出库单已完成仅验收。
**Architecture:** 复用基线 `_iter_segment_product_slots` / `_format_stop_segment_label`;归档改造 `build_plan_archive_export_rows` 为段次多行 + `planQty`/`actualQty`/`unitPrice`/`amount`;合同完善 `export_contract_gaps` 对缺口计划做段次展开;证件仅前端注入 `stopSegment`。
**Tech Stack:** Python/FastAPI、pytest、Vue 3
**Spec:** `docs/superpowers/specs/2026-10-15-pl-segment-export-extend-design.md`
**Do NOT git commit unless user explicitly asks.**
---
## File map
| 文件 | 职责 |
|------|------|
| `web-admin/src/views/pl/plan-cert/index.vue` | 导出列注入 `stopSegment` |
| `backend/app/api/pl/plan.py` | `PLAN_ARCHIVE_EXPORT_COLUMNS`、`ARCHIVE_PRICE_EXPORT_FIELDS`、`CONTRACT_GAP_EXPORT_COLUMNS`、`export_contract_gaps` |
| `backend/app/services/pl_plan_archive_export.py` | 段次展开替换配对宽表默认路径 |
| `backend/tests/test_pl_plan_archive_export.py` | 归档段次导出用例 |
| `backend/tests/test_pl_plan_contract_gap_export.py`(新建或扩现有) | 合同完善段次导出用例 |
| `web-admin/src/views/pl/plan-archive/index.vue` | 导出可见列映射对齐新 field(`stopSegment`、去掉旧宽列 prop) |
| `web-admin/src/views/pl/plan-contract/index.vue` | `contractGapExportColumns` 对齐新列 |
---
### Task 1: 危化品证上传 — 注入 stopSegment
**Files:**
- Modify: `web-admin/src/views/pl/plan-cert/index.vue`
- [ ] **Step 1: 改 `planCertExportColumns`**
对照运输计划列表写法,在 `planNo` 后插入 `stopSegment`:
```typescript
const planCertExportColumns = computed(() => {
const skip = new Set([
'loadCustomer',
'deliveryCustomer',
'loadAddressTime',
'deliveryAddressTime'
])
const cols = visibleColumns.value
.filter((col) => !skip.has(col))
.map((col) => PLAN_CERT_EXPORT_COLUMN_FIELD_MAP[col] || col)
if (!cols.includes('planNo')) cols.unshift('planNo')
const planNoIdx = cols.indexOf('planNo')
if (!cols.includes('stopSegment')) {
cols.splice(planNoIdx + 1, 0, 'stopSegment')
}
return cols
})
```
- [ ] **Step 2: 自检** — 读文件确认 splice 存在;无需后端改动。
---
### Task 2: 归档导出 — 失败测试(段次×产品)
**Files:**
- Modify: `backend/tests/test_pl_plan_archive_export.py`
- [ ] **Step 1: 新增/改写主用例为段次多行**
将 `test_build_plan_archive_export_rows_pair_mode_and_single_side` 的「默认 stop_kind=None → 1 行配对」改为期望:
```python
# stop_kind=None → 段次展开:装货 1 行 + 送货 1 行 = 2 行
rows = build_plan_archive_export_rows(..., stop_kind=None)
assert len(rows) == 2
assert [r["stopSegment"] for r in rows] == ["装货", "送货 #2"]
assert rows[0]["loadCustomer"] == internal
assert rows[0]["deliveryCustomer"] == ""
assert rows[0]["planQty"] == ... # load product plan_qty
assert rows[0]["actualQty"] == Decimal("10") # inbound
assert rows[0]["unitPrice"] == Decimal("100")
assert rows[1]["deliveryCustomer"] == "外部客户"
assert rows[1]["loadCustomer"] == ""
assert rows[1]["actualQty"] == Decimal("9")
# 不得再有 loadPlanQty + deliveryPlanQty 同排有值的配对语义
assert "loadPlanQty" not in rows[0] or rows[0].get("loadPlanQty") is None # 更好:列定义已无此 field,行 dict 可不含
```
`stop_kind="load"` / `"delivery"` 仍只输出对应侧行(可用 `stopSegment` 断言,兼容旧 `stopKind` 可删)。
- [ ] **Step 2: 改写依赖配对的测试**
- `test_append_paired_product_rows_*`:若配对函数保留仅供内部/删除,则删除或改为测新 append helper。
- 品牌/别名用例改为走 `build_plan_archive_export_rows` 段次路径断言。
- [ ] **Step 3: 跑测确认 RED**
```bash
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_archive_export.py -v --tb=line
```
---
### Task 3: 归档导出 — 实现段次列与行构建
**Files:**
- Modify: `backend/app/api/pl/plan.py`(`PLAN_ARCHIVE_EXPORT_COLUMNS`、`ARCHIVE_PRICE_EXPORT_FIELDS`)
- Modify: `backend/app/services/pl_plan_archive_export.py`
- [ ] **Step 1: 替换 `PLAN_ARCHIVE_EXPORT_COLUMNS`**
```python
PLAN_ARCHIVE_EXPORT_COLUMNS = [
ExportColumn(title="序号", field="rowNo"),
ExportColumn(title="计划编号", field="planNo"),
ExportColumn(title="段次", field="stopSegment"),
ExportColumn(title="合同编号", field="contractNo"),
ExportColumn(title="计划日期", field="planDate"),
ExportColumn(title="归档状态", field="archiveStatus"),
ExportColumn(title="跟进状态", field="followStatus"),
ExportColumn(title="单据类型", field="planType"),
ExportColumn(title="装货客户", field="loadCustomer"),
ExportColumn(title="装货地址", field="loadAddress"),
ExportColumn(title="装货时间", field="loadTime"),
ExportColumn(title="送货客户", field="deliveryCustomer"),
ExportColumn(title="送货地址", field="deliveryAddress"),
ExportColumn(title="送货时间", field="deliveryTime"),
ExportColumn(title="产品名称", field="productName"),
ExportColumn(title="品牌", field="brandName"),
ExportColumn(title="别名", field="aliasName"),
ExportColumn(title="型号/规格", field="model"),
ExportColumn(title="包装", field="packageSpec"),
ExportColumn(title="单位", field="unit", dict_type="crm_product_unit"),
ExportColumn(title="计划量", field="planQty"),
ExportColumn(title="实际量", field="actualQty"),
ExportColumn(title="单价(元)", field="unitPrice"),
ExportColumn(title="金额(元)", field="amount"),
ExportColumn(title="车牌号", field="licensePlate"),
ExportColumn(title="运费单位", field="freightType"),
ExportColumn(title="运费单价", field="freightPrice"),
ExportColumn(title="运费金额", field="freightAmount"),
ExportColumn(title="ERP", field="erpEntered"),
ExportColumn(title="备注", field="remark"),
]
```
`ARCHIVE_PRICE_EXPORT_FIELDS` 保留 `unitPrice`/`amount`/`freightPrice`/`freightAmount`;可移除已删除的 `loadUnitPrice` 等(过滤仍安全)。
- [ ] **Step 2: 重写 `build_plan_archive_export_rows` 默认路径**
伪逻辑:
```python
from app.api.pl.plan import _iter_segment_product_slots, _format_stop_segment_label
# 避免循环 import:把 _iter_segment_product_slots / _format_stop_segment_label
# 抽到 app/services/pl/plan_list_export_segments.py 更干净(推荐本 Task 内抽取)
```
**推荐抽取**(若 `plan.py` ↔ `pl_plan_archive_export.py` 循环导入):
- Create: `backend/app/services/pl/plan_list_export_segments.py`
- `iter_segment_product_slots`
- `format_stop_segment_label`
- Update `plan.py` 改为从该模块 import(保持 `_iter_segment_product_slots` 别名或直接替换调用)
- Archive/contract 从该模块 import
对每个 plan:
1. `plan_common = _plan_common_export_fields(...)`
2. 取 stops;legacy 无段 → 旧 `_append_legacy_plan_product_rows` 但补 `stopSegment=""`、`planQty`/`actualQty`
3. 有段:`slots = iter_segment_product_slots(load, deliveries)`;若 `stop_kind=="load"` 只保留 load;`=="delivery"` 只保留 delivery
4. 每槽位一行:
- `stopSegment = format_stop_segment_label(kind, seq)`
- 装货行:填 loadCustomer/Address/Time,delivery 侧空;送货行相反
- `contractNo` = 本段合同 label(`format_stop_contract_export_label`)
- 产品 identity + `planQty`/`actualQty`/`unitPrice`/`amount` 用现有 `_get_stop_archive_*`
5. `stop_customer_keywords`:保留——装或送客户匹配即保留该行(按行侧客户过滤)
可删除或停用 `_append_paired_product_rows`(若测试不再引用)。
- [ ] **Step 3: 测试全绿**
```bash
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_archive_export.py tests/test_pl_plan_list_export.py -v --tb=line
```
- [ ] **Step 4: 前端归档导出列**
`plan-archive/index.vue`:导出 columns 映射增加 `stopSegment`;若前端曾请求 `loadPlanQty` 等,改为 `planQty`/`actualQty`/`unitPrice`/`amount`(与列表 prop 一致)。找到构建 export columns 的 computed,对齐新 `PLAN_ARCHIVE_EXPORT_COLUMNS`。
---
### Task 4: 合同完善导出 — 测试 + 实现
**Files:**
- Create/Modify: `backend/tests/test_pl_plan_contract_gap_export.py`(或扩现有相关测)
- Modify: `backend/app/api/pl/plan.py`(`CONTRACT_GAP_EXPORT_COLUMNS`、`export_contract_gaps`)
- Modify: `web-admin/src/views/pl/plan-contract/index.vue`
- [ ] **Step 1: 写失败测试**
对 `export_contract_gaps` 的行构建逻辑抽函数更易测,例如:
```python
def build_contract_gap_export_rows(db, tenant_no, gap_items, stops_by_plan) -> list[dict]:
...
```
测试:一计划,装货缺合同 + 两送货段有产品 → 多行;装货行 `gapType=="缺供方合同"`(或与现文案一致的短标签);送货行若无缺口则 `gapType==""`;`stopSegment` 正确。
缺口判定复用 `stop_contract_gap(customer_name, len(contracts))`(`pl_plan_contract_gap.py`)。
- [ ] **Step 2: 更新列定义**
```python
CONTRACT_GAP_EXPORT_COLUMNS = [
ExportColumn(title="计划编号", field="planNo"),
ExportColumn(title="段次", field="stopSegment"),
ExportColumn(title="计划日期", field="planDate"),
ExportColumn(title="送货时间", field="deliveryTimes"),
ExportColumn(title="单据类型", field="planType"),
ExportColumn(title="跟进状态", field="followStatus"),
ExportColumn(title="装货客户", field="loadCustomer"),
ExportColumn(title="送货客户", field="deliveryCustomer"),
ExportColumn(title="产品名称", field="productName"),
ExportColumn(title="供方", field="supplierName"),
ExportColumn(title="需方", field="customerName"),
ExportColumn(title="缺口类型", field="gapType"),
]
```
- [ ] **Step 3: 实现 `export_contract_gaps`**
1. 仍 `list_plan_contract_gaps(...)` 得 items(含 plan id)
2. `load_plan_stops_grouped` for those ids
3. 对每个 gap item / plan:展开 slots;填计划级字段 + 段字段 + 产品名 + `gapType`
4. followStatus/planType 中文标签保持现有
- [ ] **Step 4: 前端 `contractGapExportColumns`**
```typescript
const contractGapExportColumns = [
'planNo',
'stopSegment',
'planDate',
'deliveryTimes',
'planType',
'followStatus',
'loadCustomer',
'deliveryCustomer',
'productName',
'supplierName',
'customerName',
'gapType'
]
```
- [ ] **Step 5: 跑测**
```bash
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_contract_gap_export.py tests/test_pl_plan_list_export.py tests/test_pl_plan_archive_export.py -v --tb=line
```
---
### Task 5: 收尾
- [ ] **Step 1: 全量相关测**
```bash
cd backend && . venv/bin/activate && PYTHONPATH=. pytest tests/test_pl_plan_list_export.py tests/test_pl_plan_archive_export.py tests/test_pl_plan_contract_gap_export.py -v
```
- [ ] **Step 2: Spec 对照** E1–E6 均有落点;更新 graphify(改过的 backend/web-admin)
- [ ] **Step 3: 验收清单(人工)**
- 证件 / 归档 / 合同完善 / 出库单各导出一次一装多送计划
---
## Self-review
1. Spec E1–E6 均有 Task。
2. 循环 import 风险已用「抽取 `plan_list_export_segments.py`」处理。
3. 价格字段过滤与 zip 同源行模型已写明。
4. Commit 步骤省略(用户未要求不提交)。
@@ -0,0 +1,497 @@
# 运输计划 / 库存列表导出 · 段次×产品行 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:** 让运输计划列表与库存入库/出库导出的 Excel 行模型对齐列表展平(先装货产品行,再各送货段产品行),每行带「段次」与本段计划量/实际量。
**Architecture:** 仍走现有 plan 导出 API;改造 `_build_plan_list_export_rows`,按 `stops_by_plan` 段×产品展开(对齐前端 `flattenPlanArchivePairRows`)。复用归档侧已有语义:`_stop_kind_label` / `_get_stop_archive_actual_qty`(或同名内联)。不改归档配对宽表。
**Tech Stack:** Python / FastAPI、pytest、Vue 3(`plan/index.vue` 导出列)
**Spec:** `docs/superpowers/specs/2026-10-15-plan-list-segment-export-design.md`
---
## File map
| 文件 | 职责 |
|------|------|
| `backend/tests/test_pl_plan_list_export.py` | 段次展开、占位、legacy、列过滤用例 |
| `backend/app/api/pl/plan.py` | `PLAN_EXPORT_COLUMNS` + `_build_plan_list_export_rows` + stop 字段 helper |
| `web-admin/src/views/pl/plan/index.vue` | `planExportColumns` 固定注入 `stopSegment`(库存页复用同一组件) |
可选(若 `plan.py` 内联过长):`backend/app/services/pl/plan_list_export_rows.py` 抽出展开逻辑;默认优先内联以最小改动为准。
---
### Task 1: 失败测试 — 一装多送段次展开
**Files:**
- Modify: `backend/tests/test_pl_plan_list_export.py`
- [ ] **Step 1: 追加一装多送用例(期望当前失败)**
在 `test_pl_plan_list_export.py` 末尾追加(可复用文件内已有 patch 模式):
```python
def test_build_plan_list_export_rows_expands_stop_segments():
"""一装多送:先装货产品行,再各送货段产品行;侧栏字段按行填写。"""
plan = SimpleNamespace(
id="p-multi",
plan_no="PO20261009016",
follow_status="arranged",
plan_date="2026-10-11",
plan_type="inbound",
supplier_name="供方",
customer_name="需方",
license_plate="皖A12345",
trailer_no=None,
driver_name="司机",
transport_status="arranged",
archive_status="unarchived",
erp_entered=0,
remark="",
products=[],
)
db = MagicMock()
query = MagicMock()
db.query.return_value = query
query.filter.return_value = query
query.order_by.return_value = query
# 有段结构时不应依赖计划级产品展开;返回空列表即可
query.all.return_value = []
load_product = SimpleNamespace(
id="lp1",
plan_product_id="pp1",
product_id="prod1",
product_name="尿素别名",
model="工业级",
package_spec="吨袋",
unit="ton",
plan_qty=30,
inbound_qty=29,
outbound_qty=None,
brand_mode=None,
brand_name=None,
)
del1_product = SimpleNamespace(
id="dp1",
plan_product_id="pp1",
product_id="prod1",
product_name="尿素别名",
model="工业级",
package_spec="吨袋",
unit="ton",
plan_qty=20,
inbound_qty=None,
outbound_qty=19,
brand_mode=None,
brand_name=None,
)
del2_product = SimpleNamespace(
id="dp2",
plan_product_id="pp1",
product_id="prod1",
product_name="尿素别名",
model="工业级",
package_spec="吨袋",
unit="ton",
plan_qty=10,
inbound_qty=None,
outbound_qty=9,
brand_mode=None,
brand_name=None,
)
load_stop = SimpleNamespace(
customer_name="装货客户A",
address="装货路1号",
load_time="2026-10-11",
generate_dibang=1,
dibang_direction="outbound",
contracts=[SimpleNamespace(contract_no="HT-L")],
products=[load_product],
)
delivery1 = SimpleNamespace(
seq=1,
customer_name="送货客户B1",
address="送货路B1",
delivery_time="2026-10-12",
generate_dibang=1,
dibang_direction="inbound",
contracts=[SimpleNamespace(contract_no="HT-B1")],
products=[del1_product],
)
delivery2 = SimpleNamespace(
seq=2,
customer_name="送货客户B2",
address="送货路B2",
delivery_time="2026-10-13",
generate_dibang=0,
dibang_direction=None,
contracts=[SimpleNamespace(contract_no="HT-B2")],
products=[del2_product],
)
fake_dump = {
"planNo": "PO20261009016",
"belongsCompany": "shanxia",
"followStatus": "arranged",
"planDate": "2026-10-11",
"planType": "inbound",
"supplierName": "供方",
"customerName": "需方",
"licensePlate": "皖A12345",
"trailerNo": None,
"personnelSummary": "司机张三",
"followUserName": "跟进人李四",
"driverName": "司机",
"transportStatus": "arranged",
"archiveStatus": "unarchived",
"erpEntered": 0,
"remark": "",
"products": [],
}
with patch(
"app.api.pl.plan.PlanWithProducts.model_validate",
return_value=MagicMock(model_dump=MagicMock(return_value=dict(fake_dump))),
), patch(
"app.api.pl.plan.load_crm_product_alias_maps",
return_value=({"prod1": "尿素"}, {"prod1": ["尿素别名"]}),
):
rows = _build_plan_list_export_rows(
db,
tenant_no="t1",
plans=[plan],
stops_by_plan={"p-multi": (load_stop, [delivery1, delivery2])},
)
assert len(rows) == 3
assert [r["stopSegment"] for r in rows] == ["装货", "送货 #1", "送货 #2"]
assert [r["planQty"] for r in rows] == [30, 20, 10]
assert [r["actualQty"] for r in rows] == [29, 19, 9]
assert rows[0]["loadCustomer"] == "装货客户A"
assert rows[0]["deliveryCustomer"] == ""
assert rows[0]["productContract"] == "HT-L"
assert rows[1]["loadCustomer"] == ""
assert rows[1]["deliveryCustomer"] == "送货客户B1"
assert rows[1]["deliveryTimes"] == "2026-10-12"
assert rows[1]["productContract"] == "HT-B1"
assert rows[2]["deliveryCustomer"] == "送货客户B2"
assert rows[2]["deliveryDibang"] == ""
assert rows[0]["productName"] == "尿素"
```
- [ ] **Step 2: 运行确认失败**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_export.py::test_build_plan_list_export_rows_expands_stop_segments -v
```
Expected: FAIL(当前按计划级产品展开,行数/字段不符;或无 `stopSegment`)
- [ ] **Step 3: 追加空产品占位与 legacy 用例**
```python
def test_build_plan_list_export_rows_empty_delivery_products_placeholder():
plan = SimpleNamespace(id="p-empty", plan_no="PL-E", products=[])
# …同模式 fake_dump / patches…
load_stop = SimpleNamespace(
customer_name="装A",
address="L",
load_time="2026-10-11",
generate_dibang=0,
dibang_direction=None,
contracts=[],
products=[
SimpleNamespace(
id="lp",
plan_product_id=None,
product_id="prod1",
product_name="尿素",
model="",
package_spec="",
unit="ton",
plan_qty=5,
inbound_qty=None,
outbound_qty=None,
)
],
)
delivery = SimpleNamespace(
seq=1,
customer_name="送B",
address="D",
delivery_time="2026-10-12",
generate_dibang=0,
dibang_direction=None,
contracts=[],
products=[], # 空 → 占位一行
)
# stops_by_plan={"p-empty": (load_stop, [delivery])}
# assert len(rows)==2; rows[1]["stopSegment"]=="送货 #1"; rows[1]["productName"]==""; rows[1]["planQty"] is None
def test_build_plan_list_export_rows_legacy_without_stops():
"""无段结构:回退计划级产品;stopSegment 为空。"""
# 复用现有 test_build_plan_list_export_rows_expands_products_with_crm_names 的产品 mock
# stops_by_plan={"p1": (None, [])} 或缺省
# assert rows[0]["stopSegment"] == ""
# assert rows[0]["planQty"] == 10 # 计划级 quantity
```
(实现时把占位/legacy 写成完整可运行断言,勿留注释占位。)
- [ ] **Step 4: 运行整文件确认新用例失败、旧用例仍绿或一并待修**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_export.py -v
```
---
### Task 2: 实现列定义与按段 stop 字段 helper
**Files:**
- Modify: `backend/app/api/pl/plan.py`(约 `PLAN_EXPORT_COLUMNS`、`_apply_plan_list_export_stop_fields`)
- [ ] **Step 1: `PLAN_EXPORT_COLUMNS` 在 `planNo` 后插入段次**
```python
PLAN_EXPORT_COLUMNS = [
ExportColumn(title="计划编号", field="planNo"),
ExportColumn(title="段次", field="stopSegment"),
ExportColumn(title="归属公司", field="belongsCompany"),
# …其余保持不变…
]
```
- [ ] **Step 2: 将整单拼接的 `_apply_plan_list_export_stop_fields` 改为按单侧填写**
替换为(或新增并改调用方):
```python
def _format_stop_segment_label(kind: str, delivery_seq: Optional[int] = None) -> str:
if kind == "load":
return "装货"
return f"送货 #{delivery_seq or 1}"
def _apply_plan_list_export_segment_stop_fields(
data: dict,
*,
kind: str,
load_read: Any = None,
delivery_read: Any = None,
delivery_seq: Optional[int] = None,
) -> None:
"""按当前导出行所属段填写装/送侧字段;对侧留空。"""
data["stopSegment"] = _format_stop_segment_label(kind, delivery_seq)
data["loadCustomer"] = ""
data["deliveryCustomer"] = ""
data["loadAddressTime"] = ""
data["deliveryAddressTime"] = ""
data["deliveryTimes"] = ""
data["productContract"] = ""
data["loadDibang"] = ""
data["deliveryDibang"] = ""
if kind == "load" and load_read is not None:
data["loadCustomer"] = (getattr(load_read, "customer_name", None) or "").strip()
data["loadAddressTime"] = _format_stop_address_time(
getattr(load_read, "address", None),
getattr(load_read, "load_time", None),
)
data["loadDibang"] = _format_stop_dibang_label(load_read)
data["productContract"] = " / ".join(_collect_stop_contract_nos([load_read]))
return
if kind == "delivery" and delivery_read is not None:
data["deliveryCustomer"] = (
getattr(delivery_read, "customer_name", None) or ""
).strip()
data["deliveryAddressTime"] = _format_stop_address_time(
getattr(delivery_read, "address", None),
getattr(delivery_read, "delivery_time", None),
)
data["deliveryTimes"] = _format_export_date_text(
getattr(delivery_read, "delivery_time", None)
)
data["deliveryDibang"] = _format_stop_dibang_label(delivery_read)
data["productContract"] = " / ".join(
_collect_stop_contract_nos([delivery_read])
)
```
可删除或保留旧 `_apply_plan_list_export_stop_fields`(若无引用则删除,避免死代码)。
- [ ] **Step 3: Commit(若用户要求提交再执行;否则跳过本步)**
```bash
git add backend/app/api/pl/plan.py backend/tests/test_pl_plan_list_export.py
git commit -m "$(cat <<'EOF'
feat(pl): add stopSegment column helpers for list export
Prepare segment-scoped stop fields for plan/stock list Excel export.
EOF
)"
```
---
### Task 3: 重写 `_build_plan_list_export_rows` 段次展开
**Files:**
- Modify: `backend/app/api/pl/plan.py`(`_build_plan_list_export_rows`)
- [ ] **Step 1: 实现展开逻辑(最小可用)**
核心行为:
1. 构建计划公共 `data`(与现有相同:`PlanWithProducts` dump、标签、车牌、ERP 是/否)。
2. `load_read, delivery_reads = stops_by_plan.get(plan.id, (None, []))`。
3. **若** `load_read is None and not delivery_reads`:走 **legacy**——查 `products_by_plan`,按计划级产品展开;每行 `stopSegment=""`;`planQty`/`actualQty` 用计划级 `quantity`/`actual_qty`;stop 侧字段清空或留空。
4. **否则**(有段):
- 批量加载计划级产品,仅用于品牌 lookup(`plan_product_id` / `product_id` → `brand_mode`/`brand_name`),对齐归档 `_lookup_plan_product` + `_format_plan_list_export_brand`。
- CRM 别名:收集段产品 `product_id` 调用 `load_crm_product_alias_maps`。
- 装货:`products = list(load_read.products or []) or [None]`(空则占位);每项一行,`kind=load`。
- 送货:若 `delivery_reads` 为空,视为 `[None]` 占位一段(与列表 `deliveries.length ? … : [null]` 一致);否则按顺序;每段 `products or [None]`;`delivery_seq = delivery.seq or index+1`。
- 每行:
- `row = dict(data)`
- `_apply_plan_list_export_segment_stop_fields(...)`
- 若 `product is None`:产品列空,`planQty`/`actualQty` = `None`
- 否则:`resolve_product_display_names`;`planQty = product.plan_qty`;`actualQty = product.inbound_qty if kind=="load" else product.outbound_qty`(对齐列表 `getRowActualQty`)
- 品牌:经 plan product lookup 后 `_format_plan_list_export_brand`
参考片段(伪代码):
```python
def _iter_segment_product_slots(load_read, delivery_reads):
if load_read is not None:
load_products = list(getattr(load_read, "products", None) or []) or [None]
for product in load_products:
yield ("load", load_read, None, None, product)
deliveries = list(delivery_reads or []) or ([None] if load_read is not None else [])
for index, delivery in enumerate(deliveries):
seq = getattr(delivery, "seq", None) if delivery is not None else (index + 1)
if seq is None:
seq = index + 1
products = list(getattr(delivery, "products", None) or []) if delivery else []
if not products:
products = [None]
for product in products:
yield ("delivery", None, delivery, seq, product)
```
注意:仅有送货无装货时仍展开送货段;仅有装货无送货时,`deliveries = [None]` 出送货占位行(与列表一致)。
- [ ] **Step 2: 跑测试至全绿**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_export.py -v
```
Expected: PASS(含旧用例;旧用例 `stops_by_plan` 有单装单送时需改为段展开——**更新** `test_build_plan_list_export_rows_expands_products_with_crm_names`:
- 给 `load_stop` / `delivery_stop` 挂上 `products=[product_as_stop]`(字段用 `plan_qty`/`inbound_qty`/`outbound_qty`)
- 断言 `stopSegment`、装货行/送货行侧栏空非空
- 或拆成「有段单装单送」+ 保留 legacy 断言
- [ ] **Step 3: 归档导出回归(确认未误伤)**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_archive_export.py -v --tb=line
```
Expected: PASS
- [ ] **Step 4: Commit(用户要求时)**
```bash
git commit -m "$(cat <<'EOF'
feat(pl): expand plan list export by stop segment and product
Align Excel rows with flattenPlanArchivePairRows for stock and plan lists.
EOF
)"
```
---
### Task 4: 前端导出列注入「段次」
**Files:**
- Modify: `web-admin/src/views/pl/plan/index.vue`(`planExportColumns`)
说明:列表无「段次」可见列;导出按可见列过滤时会被丢掉。固定在导出列中注入,保证入库/出库/计划列表 Excel 都有该列。
- [ ] **Step 1: 修改 `planExportColumns`**
```typescript
const planExportColumns = computed(() => {
const skip = new Set(['arrangeCopy', 'planAttachments'])
const cols = [...visibleColumns.value]
.filter((col) => !skip.has(col))
.map((col) => PLAN_EXPORT_COLUMN_FIELD_MAP[col] || col)
if (!cols.includes('planNo')) cols.unshift('planNo')
// 导出专用:列表无此列,始终紧挨计划编号
const planNoIdx = cols.indexOf('planNo')
if (!cols.includes('stopSegment')) {
cols.splice(planNoIdx + 1, 0, 'stopSegment')
}
return cols
})
```
- [ ] **Step 2: 手动验收要点**
1. 打开运输计划列表、入库单、出库单,各导出一次(筛选含一装多送计划更佳)。
2. Excel:计划编号后有「段次」;一装多送多行;装货行送货客户空、送货行装货客户空;计划量为本段。
3. 文件名仍为 `入库单_*.xlsx` / `出库单_*.xlsx` / `计划列表_*.xlsx`。
- [ ] **Step 3: Commit(用户要求时)**
```bash
git commit -m "$(cat <<'EOF'
feat(web-admin): always include stopSegment in plan list export columns
Ensure stock inbound/outbound and plan list Excel show segment labels.
EOF
)"
```
---
### Task 5: 收尾核对
- [ ] **Step 1: 对照 spec 清单**
| Spec 项 | 对应 Task |
|---------|-----------|
| D1 段次×产品多行 | Task 3 |
| D2 三入口共用 | 同一 API + Task 4 |
| D3 段次列文案 | Task 2–3 |
| D4 侧栏按行、不拼接 | Task 2–3 |
| D5 计划字段重复 | Task 3 |
| D6 归档不改 | Task 3 回归 |
| D7 文件名不变 | 已有逻辑 + 旧测 |
| 空产品占位 / legacy | Task 1+3 |
| 实际量 inbound/outbound | Task 3 |
- [ ] **Step 2: 最终测试**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_export.py tests/test_pl_plan_archive_export.py -v
```
Expected: 全部 PASS
---
## Self-review (plan author)
1. **Spec coverage:** D1–D7、占位、legacy、实际量语义、前端注入均有 Task。
2. **No placeholders:** 测试与 helper 给出可粘贴代码;legacy/占位在 Task 1 Step 3 要求写成完整断言。
3. **Types:** field 统一 `stopSegment`;实际量装货 `inbound_qty`、送货 `outbound_qty`。
4. **Commits:** 标为「用户要求时」以符合仓库「未明确要求不提交」规则。
@@ -0,0 +1,718 @@
# web-admin 发版后强制刷新 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:** 生产环境的 web-admin 在仍打开旧标签页时,发现新构建就弹出不可关闭的「立即刷新」。
**Architecture:** 每次 `pnpm build` 用「短 git hash + 构建时间戳」生成 `buildId`,注入 `__APP_BUILD_ID__` 并写入 `dist/version.json`。运行时只在生产环境拉取该文件(`cache: 'no-store'` + `?t=`),`buildId` 不同则 `ElMessageBox.alert` 强制刷新。纯判断与调度放在可单测模块;浏览器和弹窗只在很薄的启动函数里接线。
**Tech Stack:** Vite 7 · Vue 3 · Element Plus `ElMessageBox`(`src/types/import/auto-imports.d.ts` 已全局声明,`.ts` 里不要手写 import)· Node test(`pnpm exec tsx --test`)
**Spec:** `docs/superpowers/specs/2026-10-16-web-admin-version-refresh-design.md`
---
## 文件结构
| 路径 | 职责 |
|------|------|
| `web-admin/scripts/app-build-meta.ts` | 生成 `buildId`、拼 `version.json` 文本、写入 `outDir/version.json` |
| `web-admin/scripts/app-build-meta.test.ts` | 上述纯函数与写文件 |
| `web-admin/src/utils/app-update.ts` | URL、是否提示、文案、30 秒防抖、调度器(依赖注入,无 DOM) |
| `web-admin/src/utils/app-update.test.ts` | 调度器与文案 |
| `web-admin/src/utils/check-app-update.ts` | 生产环境接线:`fetch`、`ElMessageBox`、`location.reload`、可见性 |
| `web-admin/src/env.d.ts` | 声明 `__APP_BUILD_ID__` |
| `web-admin/vite.config.ts` | `define` 注入 `buildId`;`apply: 'build'` 插件写出 `dist/version.json` |
| `web-admin/src/main.ts` | `app.mount` 之后调用 `startAppUpdateCheck()` |
**明确不改:** 后端、Nginx 配置、`VITE_VERSION` 的人工升版流程、小程序更新、未保存表单拦截。`index.html` / `version.json` 的短缓存仍由现有发布方式保证(规格第 6 节)。
---
### Task 1: 构建指纹与 version.json 文本
**Files:**
- Create: `web-admin/scripts/app-build-meta.ts`
- Test: `web-admin/scripts/app-build-meta.test.ts`
- [ ] **Step 1: Write the failing test**
```ts
// web-admin/scripts/app-build-meta.test.ts
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { describe, it } from 'node:test'
import {
createAppBuildMeta,
readGitShort,
versionJsonText,
writeVersionJson
} from './app-build-meta.ts'
describe('createAppBuildMeta', () => {
it('joins git short hash and timestamp', () => {
assert.deepEqual(createAppBuildMeta('3.0.2', 1730000000000, 'a1b2c3d'), {
version: '3.0.2',
buildId: 'a1b2c3d-1730000000000'
})
})
it('uses nogit when the hash is empty', () => {
assert.equal(createAppBuildMeta('3.0.2', 5, ' ').buildId, 'nogit-5')
})
it('changes buildId when the timestamp changes', () => {
const a = createAppBuildMeta('3.0.2', 1, 'abc')
const b = createAppBuildMeta('3.0.2', 2, 'abc')
assert.notEqual(a.buildId, b.buildId)
})
})
describe('readGitShort', () => {
it('trims command output', () => {
assert.equal(readGitShort(() => 'abc123\n'), 'abc123')
})
it('returns nogit when the command throws or is blank', () => {
assert.equal(
readGitShort(() => {
throw new Error('no git')
}),
'nogit'
)
assert.equal(readGitShort(() => ' '), 'nogit')
})
})
describe('version.json', () => {
it('writes pretty json with a trailing newline', () => {
const text = versionJsonText({ version: '3.0.2', buildId: 'abc-1' })
assert.equal(text, '{\n "version": "3.0.2",\n "buildId": "abc-1"\n}\n')
})
it('writes dist/version.json', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'app-ver-'))
writeVersionJson(dir, { version: '3.0.3', buildId: 'def-9' })
const raw = fs.readFileSync(path.join(dir, 'version.json'), 'utf8')
assert.deepEqual(JSON.parse(raw), { version: '3.0.3', buildId: 'def-9' })
})
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd web-admin && pnpm exec tsx --test scripts/app-build-meta.test.ts`
Expected: FAIL with cannot find module `./app-build-meta.ts`
- [ ] **Step 3: Write minimal implementation**
```ts
// web-admin/scripts/app-build-meta.ts
import fs from 'node:fs'
import path from 'node:path'
export type AppBuildMeta = {
version: string
buildId: string
}
export function readGitShort(run: () => string): string {
try {
const text = run().trim()
return text || 'nogit'
} catch {
return 'nogit'
}
}
export function createAppBuildMeta(version: string, nowMs: number, gitShort: string): AppBuildMeta {
const hash = gitShort.trim() || 'nogit'
return { version, buildId: `${hash}-${nowMs}` }
}
export function versionJsonText(meta: AppBuildMeta): string {
return `${JSON.stringify({ version: meta.version, buildId: meta.buildId }, null, 2)}\n`
}
export function writeVersionJson(outDir: string, meta: AppBuildMeta): void {
fs.mkdirSync(outDir, { recursive: true })
fs.writeFileSync(path.join(outDir, 'version.json'), versionJsonText(meta), 'utf8')
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd web-admin && pnpm exec tsx --test scripts/app-build-meta.test.ts`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add web-admin/scripts/app-build-meta.ts web-admin/scripts/app-build-meta.test.ts
git commit -m "$(cat <<'EOF'
test: add web-admin build id and version.json helpers.
EOF
)"
```
---
### Task 2: 更新判断与调度器
**Files:**
- Create: `web-admin/src/utils/app-update.ts`
- Test: `web-admin/src/utils/app-update.test.ts`
- [ ] **Step 1: Write the failing test**
```ts
// web-admin/src/utils/app-update.test.ts
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
APP_UPDATE_POLL_MS,
APP_UPDATE_VISIBILITY_DEBOUNCE_MS,
formatUpdateMessage,
shouldPromptUpdate,
shouldSkipVisibilityCheck,
startAppUpdateChecker,
versionJsonUrl
} from './app-update.ts'
function flush() {
return new Promise((resolve) => setImmediate(resolve))
}
describe('versionJsonUrl', () => {
it('appends a cache-busting query on the base url', () => {
assert.equal(versionJsonUrl('/', 5), '/version.json?t=5')
assert.equal(versionJsonUrl('/admin/', 5), '/admin/version.json?t=5')
assert.equal(versionJsonUrl('/admin', 5), '/admin/version.json?t=5')
})
})
describe('shouldPromptUpdate', () => {
it('prompts only when remote buildId is a different non-empty string', () => {
assert.equal(shouldPromptUpdate('a-1', { buildId: 'b-2' }), true)
assert.equal(shouldPromptUpdate('a-1', { buildId: 'a-1' }), false)
assert.equal(shouldPromptUpdate('a-1', { buildId: '' }), false)
assert.equal(shouldPromptUpdate('a-1', { buildId: 2 }), false)
assert.equal(shouldPromptUpdate('a-1', {}), false)
assert.equal(shouldPromptUpdate('a-1', null), false)
})
})
describe('formatUpdateMessage', () => {
it('includes escaped versions when remote version is a non-empty string', () => {
const text = formatUpdateMessage('3.0.2', '3.0.3')
assert.match(text, /系统已发布更新/)
assert.match(text, /当前 v3\.0\.2/)
assert.match(text, /最新 v3\.0\.3/)
})
it('omits the version line when remote version is missing or blank', () => {
assert.doesNotMatch(formatUpdateMessage('3.0.2', ''), /当前 v/)
assert.doesNotMatch(formatUpdateMessage('3.0.2', ' '), /当前 v/)
assert.equal(formatUpdateMessage('3.0.2', null), formatUpdateMessage('3.0.2', undefined))
})
it('escapes html in version strings', () => {
const text = formatUpdateMessage('3<0', '3>1')
assert.match(text, /v3&lt;0/)
assert.match(text, /v3&gt;1/)
assert.doesNotMatch(text, /v3<0/)
})
})
describe('shouldSkipVisibilityCheck', () => {
it('skips only when the gap is under 30 seconds', () => {
assert.equal(APP_UPDATE_VISIBILITY_DEBOUNCE_MS, 30_000)
assert.equal(shouldSkipVisibilityCheck(1_000, 1_000 + 29_999), true)
assert.equal(shouldSkipVisibilityCheck(1_000, 1_000 + 30_000), false)
})
})
describe('startAppUpdateChecker', () => {
it('does nothing outside production', async () => {
let fetches = 0
const stop = startAppUpdateChecker({
isProd: false,
localBuildId: 'a-1',
localVersion: '3.0.2',
baseUrl: '/',
now: () => 1,
fetchRemote: async () => {
fetches += 1
return { buildId: 'b-2', version: '3.0.3' }
},
onReload: () => {},
alertUpdate: async () => {},
setInterval: () => 1,
clearInterval: () => {},
subscribeVisible: () => () => {}
})
await flush()
assert.equal(fetches, 0)
stop()
})
it('polls every 10 minutes and stays quiet when buildId matches', async () => {
let fetches = 0
const timers: Array<{ fn: () => void; ms: number }> = []
startAppUpdateChecker({
isProd: true,
localBuildId: 'a-1',
localVersion: '3.0.2',
baseUrl: '/',
now: () => 10,
fetchRemote: async (url) => {
fetches += 1
assert.equal(url, '/version.json?t=10')
return { buildId: 'a-1', version: '3.0.2' }
},
onReload: () => {},
alertUpdate: async () => {
throw new Error('should not alert')
},
setInterval: (fn, ms) => {
timers.push({ fn, ms })
return timers.length
},
clearInterval: () => {},
subscribeVisible: () => () => {}
})
await flush()
assert.equal(fetches, 1)
assert.equal(timers.length, 1)
assert.equal(timers[0].ms, APP_UPDATE_POLL_MS)
assert.equal(APP_UPDATE_POLL_MS, 600_000)
})
it('alerts once, reloads once, and stops later checks when buildId differs', async () => {
let fetches = 0
let reloads = 0
const alerts: string[] = []
let cleared = 0
let unsubscribed = 0
let visible: (() => void) | null = null
const timers: Array<{ fn: () => void }> = []
startAppUpdateChecker({
isProd: true,
localBuildId: 'a-1',
localVersion: '3.0.2',
baseUrl: '/',
now: () => 10,
fetchRemote: async () => {
fetches += 1
return { buildId: 'b-2', version: '3.0.3' }
},
onReload: () => {
reloads += 1
},
alertUpdate: async (message) => {
alerts.push(message)
},
setInterval: (fn) => {
timers.push({ fn })
return 1
},
clearInterval: () => {
cleared += 1
},
subscribeVisible: (handler) => {
visible = handler
return () => {
unsubscribed += 1
}
}
})
await flush()
assert.equal(alerts.length, 1)
assert.match(alerts[0], /最新 v3\.0\.3/)
assert.equal(reloads, 1)
assert.equal(cleared, 1)
assert.equal(unsubscribed, 1)
visible?.()
timers[0]?.fn()
await flush()
assert.equal(fetches, 1)
assert.equal(reloads, 1)
})
it('keeps polling when fetch fails or the payload is unusable', async () => {
const urls = ['throw', 'bad', 'empty']
let fetches = 0
let alerts = 0
const timers: Array<{ fn: () => void }> = []
startAppUpdateChecker({
isProd: true,
localBuildId: 'a-1',
localVersion: '3.0.2',
baseUrl: '/',
now: () => 10,
fetchRemote: async () => {
fetches += 1
const kind = urls[fetches - 1]
if (kind === 'throw') throw new Error('offline')
if (kind === 'bad') return { nope: true }
return { buildId: '' }
},
onReload: () => {
throw new Error('should not reload')
},
alertUpdate: async () => {
alerts += 1
},
setInterval: (fn) => {
timers.push({ fn })
return 1
},
clearInterval: () => {
throw new Error('should not stop')
},
subscribeVisible: () => () => {}
})
await flush()
timers[0].fn()
await flush()
timers[0].fn()
await flush()
assert.equal(fetches, 3)
assert.equal(alerts, 0)
})
it('skips a visible-tab check inside 30 seconds and runs one after that', async () => {
let now = 1_000_000
let fetches = 0
let visible: (() => void) | null = null
startAppUpdateChecker({
isProd: true,
localBuildId: 'a-1',
localVersion: '3.0.2',
baseUrl: '/',
now: () => now,
fetchRemote: async () => {
fetches += 1
return { buildId: 'a-1' }
},
onReload: () => {},
alertUpdate: async () => {},
setInterval: () => 1,
clearInterval: () => {},
subscribeVisible: (handler) => {
visible = handler
return () => {}
}
})
await flush()
assert.equal(fetches, 1)
now = 1_000_000 + 10_000
visible?.()
await flush()
assert.equal(fetches, 1)
now = 1_000_000 + 30_000
visible?.()
await flush()
assert.equal(fetches, 2)
})
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd web-admin && pnpm exec tsx --test src/utils/app-update.test.ts`
Expected: FAIL with cannot find module `./app-update.ts`
- [ ] **Step 3: Write minimal implementation**
```ts
// web-admin/src/utils/app-update.ts
export const APP_UPDATE_POLL_MS = 10 * 60 * 1000
export const APP_UPDATE_VISIBILITY_DEBOUNCE_MS = 30 * 1000
const UPDATE_BODY = '系统已发布更新,当前页面仍在使用旧功能。请刷新后继续操作。'
export type RemoteVersion = {
version?: unknown
buildId?: unknown
} | null
export type AppUpdateCheckerDeps = {
isProd: boolean
localBuildId: string
localVersion: string
baseUrl: string
now: () => number
fetchRemote: (url: string) => Promise<unknown>
onReload: () => void
alertUpdate: (message: string) => Promise<void>
setInterval: (fn: () => void, ms: number) => ReturnType<typeof setInterval>
clearInterval: (id: ReturnType<typeof setInterval>) => void
subscribeVisible: (handler: () => void) => () => void
}
export function versionJsonUrl(baseUrl: string, nowMs: number): string {
const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`
return `${base}version.json?t=${nowMs}`
}
export function shouldPromptUpdate(localBuildId: string, remote: unknown): boolean {
if (!remote || typeof remote !== 'object') return false
const buildId = (remote as { buildId?: unknown }).buildId
return typeof buildId === 'string' && buildId.length > 0 && buildId !== localBuildId
}
function escapeHtml(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
export function formatUpdateMessage(localVersion: string, remoteVersion: unknown): string {
if (typeof remoteVersion !== 'string' || !remoteVersion.trim()) return UPDATE_BODY
const current = escapeHtml(localVersion)
const latest = escapeHtml(remoteVersion.trim())
return `${UPDATE_BODY}<br>当前 v${current} / 最新 v${latest}`
}
export function shouldSkipVisibilityCheck(lastStartedAt: number, nowMs: number): boolean {
return nowMs - lastStartedAt < APP_UPDATE_VISIBILITY_DEBOUNCE_MS
}
export function startAppUpdateChecker(deps: AppUpdateCheckerDeps): () => void {
if (!deps.isProd) return () => {}
let prompted = false
let reloading = false
let inFlight = false
let lastStartedAt = 0
let timer: ReturnType<typeof setInterval> | null = null
let unsubscribe = () => {}
const stop = () => {
if (timer != null) deps.clearInterval(timer)
timer = null
unsubscribe()
unsubscribe = () => {}
}
const check = async () => {
if (prompted || inFlight) return
inFlight = true
lastStartedAt = deps.now()
try {
const remote = await deps.fetchRemote(versionJsonUrl(deps.baseUrl, lastStartedAt))
if (!shouldPromptUpdate(deps.localBuildId, remote)) return
prompted = true
stop()
const version =
remote && typeof remote === 'object' ? (remote as { version?: unknown }).version : undefined
await deps.alertUpdate(formatUpdateMessage(deps.localVersion, version))
if (reloading) return
reloading = true
deps.onReload()
} catch {
// 下一轮再试
} finally {
inFlight = false
}
}
const checkFromVisibility = () => {
if (prompted) return
const now = deps.now()
if (lastStartedAt !== 0 && shouldSkipVisibilityCheck(lastStartedAt, now)) return
void check()
}
void check()
timer = deps.setInterval(() => {
void check()
}, APP_UPDATE_POLL_MS)
unsubscribe = deps.subscribeVisible(checkFromVisibility)
return stop
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd web-admin && pnpm exec tsx --test src/utils/app-update.test.ts`
Expected: PASS(5 组 describe 全部通过)
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/utils/app-update.ts web-admin/src/utils/app-update.test.ts
git commit -m "$(cat <<'EOF'
test: add web-admin stale-tab update checker.
EOF
)"
```
---
### Task 3: 构建注入与生产环境弹窗
**Files:**
- Modify: `web-admin/vite.config.ts`(`define` 与 `plugins`)
- Modify: `web-admin/src/env.d.ts`(`__APP_VERSION__` 声明旁)
- Create: `web-admin/src/utils/check-app-update.ts`
- Modify: `web-admin/src/main.ts`(`app.mount('#app')` 之后)
- [ ] **Step 1: Declare the build id**
在 `web-admin/src/env.d.ts` 现有行旁增加:
```ts
declare const __APP_VERSION__: string // 版本号
declare const __APP_BUILD_ID__: string // 构建指纹:短 git hash + 构建时间戳
```
- [ ] **Step 2: Inject the same build id and write version.json on build**
`web-admin/vite.config.ts` 顶部增加:
```ts
import { execSync } from 'node:child_process'
import { createAppBuildMeta, readGitShort, writeVersionJson } from './scripts/app-build-meta'
```
`path` 已从 `'path'` 导入。在 `export default ({ mode }) => {` 里、`defineConfig` 之前:
```ts
const gitShort = readGitShort(() =>
execSync('git rev-parse --short HEAD', {
stdio: ['ignore', 'pipe', 'ignore']
}).toString()
)
const appBuildMeta = createAppBuildMeta(VITE_VERSION || '', Date.now(), gitShort)
```
把现有 `define` 改成同时注入两个常量(仍使用同一次算出的 `appBuildMeta`):
```ts
define: {
__APP_VERSION__: JSON.stringify(VITE_VERSION),
__APP_BUILD_ID__: JSON.stringify(appBuildMeta.buildId),
global: 'globalThis'
},
```
在 `plugins` 数组最前面加入仅生产构建写文件的插件(`closeBundle` 时 `dist` 已存在):
```ts
{
name: 'emit-version-json',
apply: 'build',
closeBundle() {
writeVersionJson(path.resolve(root, 'dist'), appBuildMeta)
}
},
```
`root` 已是 `process.cwd()`。不要在 `apply: 'serve'` 时写 `version.json`。
- [ ] **Step 3: Wire the browser**
```ts
// web-admin/src/utils/check-app-update.ts
import { startAppUpdateChecker } from './app-update'
/** 生产环境检查 dist/version.json,发现新 buildId 后强制刷新。 */
export function startAppUpdateCheck(): () => void {
return startAppUpdateChecker({
isProd: import.meta.env.PROD,
localBuildId: __APP_BUILD_ID__,
localVersion: __APP_VERSION__,
baseUrl: import.meta.env.BASE_URL,
now: () => Date.now(),
fetchRemote: async (url) => {
const res = await fetch(url, { cache: 'no-store' })
if (!res.ok) throw new Error(String(res.status))
return res.json()
},
onReload: () => {
location.reload()
},
alertUpdate: async (message) => {
await ElMessageBox.alert(message, '发现新版本', {
confirmButtonText: '立即刷新',
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false,
showCancelButton: false,
dangerouslyUseHTMLString: true
})
},
setInterval: (fn, ms) => window.setInterval(fn, ms),
clearInterval: (id) => window.clearInterval(id),
subscribeVisible: (handler) => {
const listener = () => {
if (document.visibilityState === 'visible') handler()
}
document.addEventListener('visibilitychange', listener)
return () => document.removeEventListener('visibilitychange', listener)
}
})
}
```
`ElMessageBox` 使用自动导入,不要写 `import { ElMessageBox } from 'element-plus'`。
`web-admin/src/main.ts` 在 `app.mount('#app')` 之后:
```ts
import { startAppUpdateCheck } from './utils/check-app-update'
```
该 import 与文件顶部其它 import 放在一起。调用:
```ts
app.mount('#app')
startAppUpdateCheck()
```
- [ ] **Step 4: Run unit tests again**
Run: `cd web-admin && pnpm exec tsx --test scripts/app-build-meta.test.ts src/utils/app-update.test.ts`
Expected: PASS
不在此步骤跑完整 `pnpm build`(该构建内存占用大)。指纹与写文件已由 Task 1 覆盖;`vite.config.ts` 只负责把同一 `appBuildMeta` 注入并在 `closeBundle` 调用 `writeVersionJson`。
- [ ] **Step 5: Commit**
```bash
git add web-admin/vite.config.ts web-admin/src/env.d.ts web-admin/src/utils/check-app-update.ts web-admin/src/main.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): force a refresh when a newer build is deployed.
EOF
)"
```
---
## 规格对照
| 规格 | 任务 |
|------|------|
| `version` + `buildId`(hash-时间戳,空 hash 为 `nogit`) | Task 1、Task 3 |
| 每次构建 `buildId` 变化 | Task 1 `timestamp changes` |
| 生产环境才检查;`pnpm dev` 不请求 | Task 2 `does nothing outside production`;Task 3 `import.meta.env.PROD` |
| 立即查、每 10 分钟、可见且距上次 ≥ 30 秒再查 | Task 2 |
| 失败 / 非 2xx / 坏 JSON / 空 `buildId` 不打断 | Task 2;Task 3 `!res.ok` 抛错进入 catch |
| 强制弹窗文案、版本行、只刷新一次、弹出后停止监听 | Task 2、Task 3 `ElMessageBox` 选项 |
| `cache: 'no-store'` 与 `?t=` | Task 2 URL、Task 3 fetch |
| 不改 Nginx | 无任务 |
## 手工验证(实现合并前)
1. `pnpm dev` 打开管理端,网络面板没有 `version.json`。
2. 若需要看真实弹窗:把 `check-app-update.ts` 里临时将 `isProd` 设为 `true`,并在 `public/` 放一份 `buildId` 不同的 `version.json`(验证完还原,不要提交这处临时改动)。切走标签页超过 30 秒再回来应弹窗;点「立即刷新」会重载。遮罩点击和 Esc 不能关掉弹窗。
@@ -0,0 +1,64 @@
# 小程序 CRM 六界面统一样式 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:** 按工作台 WebView 标准统一 CRM 六个菜单入口及其直接详情页,同时完整保留现有业务能力。
**Architecture:** 先修订视觉权威并建立局部共用展示原语,再按共享实现分组迁移报价、合同、产品、客户。共用层只负责结构与显示,API、权限和审批状态机继续留在业务模块。
**Tech Stack:** Vue 3、TypeScript、SCSS、Element Plus、Playwright、Node test/tsx、uni-app WebView。
---
### Task 1:视觉权威与共用 DOM 契约
**Files:** `openspec/.../ui-conventions/spec.md`, `docs/miniprogram-ui-spec-sanqiang.md`, style-align Demo, `web-admin/src/components/mp/`, `web-admin/src/styles/mp-webview.scss`, new DOM test.
- [x] 写失败测试,断言普通详情头、无 ticket、状态 Tab、三栏筛选、More 红点、expense-card 层级、浅底状态、44px 命中和72px附件。
- [x] 新增小型展示组件/样式 modifier,不改变 API 或业务状态;不得全局把旧筛选条改为三列。
- [x] 运行 DOM 契约测试,确认共用原语通过,并对现有非 CRM 消费者做截图冒烟。
### Task 2:报价管理与报价审批
**Files:** `web-admin/src/views/m/crm/quote/MpQuoteList.vue`, `detail.vue`, related tests.
- [x] 写管理/审批 mode 的失败测试:列表顺序、Tab、三栏筛选、金额卡、独立详情普通头、Tab、审批时间轴和动作权限。
- [x] 用同一共享列表实现两个入口的新骨架,保留 `from=approve`、当前用户过滤、分页与 Speed Dial 差异。
- [x] 详情删除动态顶栏摘要,将编号/状态/金额移入正文;增加字段 Tab 与时间轴。
- [x] 跑 quote 纯函数、浏览器路由/交互、375/414 截图并修复回归。
### Task 3:合同管理与合同审批
**Files:** `web-admin/src/views/m/crm/contract/MpContractList.vue`, `detail.vue`, related tests.
- [x] 写失败测试覆盖列表骨架、金额卡、多选后批量底栏、单/批审批部分成功、独立详情和72px附件。
- [x] 迁移共享列表和详情,保留管理/审批入口差异、审批人选择与全部业务限制。
- [x] 将文本附件/隐藏上传改为移动附件宫格,不改变存储字段和上传 API。
- [x] 跑 contract 纯函数、浏览器测试与375/414截图。
### Task 4:产品管理
**Files:** `web-admin/src/views/m/crm/product/index.vue`, `detail.vue`, related tests.
- [x] 写失败测试覆盖全部/上架/下架 Tab、三栏筛选、无金额 expense-card、双项 Speed Dial、独立详情普通头和详情 Tab。
- [x] 迁移列表与详情,保留分类全路径、上下架、创建者删除限制、分类入口和图片功能。
- [x] 跑 product 纯函数、浏览器测试与375/414截图。
### Task 5:客户管理
**Files:** `web-admin/src/views/m/crm/customer/index.vue`, `detail.vue`, `CustomerFilters.vue`, attachments, `customer.scss`, `scripts/test-mp-crm-customer.mjs`.
- [x] 将现有 Playwright 用例改为新骨架的失败断言,继续覆盖四视图、完整筛选、跟进/联系人/商机/合同/开票/日志、公海、删除与失败重试。
- [x] 移除两行工具条;将新增迁入 Speed Dial、筛选接入三栏/More、卡片改为 expense-card。
- [x] 详情使用普通固定标题,保留七个业务 Tab;客户证件与跟进附件改72px宫格。
- [x] 跑 customer model、Playwright、375/414截图。
### Task 6:全量验证与交付
**Files:** CRM delivery/issues docs, SQL issue record, graph outputs.
- [x] 执行四组纯逻辑测试、六入口浏览器回归、目标 lint/stylelint、构建和类型基线对比。
- [x] 检查六个菜单权限、WebView 映射、详情直达/返回、401/ticket 和双权限场景;不写真实数据库。
- [x] 记录全量 SQL 缺少 `MINI_CRM_*`、旧 migration 软删除幂等、过期 e2e 文档等非本轮界面问题,不顺手修改未授权系统。
- [x] 更新 frontend/miniprogram/root 图谱,逐项审计目标后再声明完成。
@@ -0,0 +1,17 @@
# 小程序 H5 客户管理实施计划
目标:按照同日需求文档完成客户管理移动迁移,排除导出,保留 Web 行为。
架构:在现有 H5 工程新增独立客户页面与局部组件,调用既有 CRM API。复用 WebView、鉴权、字典和附件能力。
- [x] 核对 Web 实际入口、详情关联模块、表单字段和移动参考样式。
- [x] 新建 `views/m/crm/customer/model.ts` 与测试:表单字段、旧证件兼容、响应归一化、精确 ID、时间转换、创建后开票失败的可重试流程。
- [x] 新建 `CustomerFields.vue`、`CustomerAttachments.vue`、`InvoiceFields.vue`:手机表单与证件卡片。
- [x] 新建 `index.vue`、`CustomerFilters.vue`:四种视图、筛选排序、字段显示、分页与批量动作。
- [x] 新建 `form.vue`:全字段、校验、查重、默认负责人、首次开票及失败重试。
- [x] 新建 `detail.vue` 及相关面板:跟进、联系人、开票、商机/合同、操作日志;补齐关联商机详情动作。
- [x] 注册 H5 静态路由、小程序工作台映射;新增菜单与权限 SQL;更新接入文档。
- [x] 执行单测、浏览器交互、构建、类型基线对比、ESLint、小程序配置语法及图谱更新;字段一致性脚本缺失已记录并人工核对 payload。
- [x] 对照需求检查差异,完成只读代码评审、问题记录与交付说明。未执行生产发布及真机验收。
测试重点:ID 不转 Number、空附件清除、旧附件字段不覆盖新值、分页失败不跳页、搜索竞态不覆盖新结果、上传时不能提交、客户创建成功但开票失败重试不重复创建客户。
@@ -0,0 +1,72 @@
# 危化品证上传移动 H5 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** 将 Web 危化品证上传完整迁移为小程序运输管理下的移动 H5,移除导出,保留五个 Tab 与全部其他业务能力。
**Architecture:** 新增独立 `views/m/pl/plan-cert/` 模块,读取既有 API、规则和展示工具,不修改 Web 页面或共享逻辑。页面壳、底部筛选、附件组件复用既有移动模式;独立菜单 SQL 只补入口与祖先角色授权。
**Tech Stack:** Vue 3、TypeScript、Element Plus、SCSS、uni-app WebView、Node test/tsx、Playwright、MySQL 5.7 菜单 SQL。
## Task 0:用户反馈第三次校准(2026-10-17)
**Files:** `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md`, `docs/miniprogram-ui-spec-sanqiang.md`, `bak/mobile-ui/shanxia-mp-crm-pl-cert-style-align-demo.html`, `web-admin/src/router/routes/staticRoutes.ts`, `web-admin/scripts/mp-pl-plan-cert-entry.test.mjs`, `web-admin/scripts/test-mp-pl-plan-cert.mjs`, `web-admin/src/views/m/pl/plan-cert/index.vue`, `detail.vue`, `CertDetail.vue`, `CertUpload.vue`, `CertFilters.vue`, `cert.scss`, `docs/web-admin-mp-webview-modules.md`, delivery/issues docs。
- [x] **Step 1:写失败测试。** 入口测试断言 `/m/pl/plan-cert/:id` 映射 `detail.vue`;浏览器测试断言列表使用标准 `mp-wv-header`(17px 标题、单胶囊搜索、无副标题/独立搜索按钮)、列表卡横向 x=0/宽=viewport、查看后 URL 进入独立详情、页面不存在“计划详情”Sheet/`ticket-*`、三 Tab 存在;筛选/上传 Sheet 底栏两个按钮各约 96px、间距 ≥12px、整体水平居中、字号14px、触达 ≥44px。运行并确认因当前结构失败。
- [x] **Step 2:更新视觉权威。** OpenSpec、UI 规范和 Demo 将详情 SHALL 改为“独立路由 + 普通绿顶栏 + Tab + 信息卡 + 底栏”,删除通用票根要求;为危化品证补“无顶部特殊区、列表全宽、Sheet 按钮居中适宽”场景,Code review 清单同步。
- [x] **Step 3:标准化列表。** `index.vue` 复用 `mp-wv-header` 和单个胶囊 search input,移除自定义副标题/搜索按钮;保留搜索→状态Tab→三栏筛选顺序。目标列表区用局部 full-bleed 布局取消双重水平留白,提示文字自行加内边距;不改其他页面。
- [x] **Step 4:创建独立详情。** 新增 `detail.vue` 与动态路由;把现有详情内容从 `MpBottomSheet` 容器迁入独立 `MpWebviewShell` 页面,删除票根,保留三 Tab、所有字段、运输复制、合同关联、反馈、证书与附件。详情自行加载权限/ID/数据,并编排上传、齐备、取消齐备和原位刷新;列表与关联计划统一 `router.push`。
- [x] **Step 5:修正 Sheet 底栏。** 只在 plan-cert 的 `CertFilters.vue`、`CertUpload.vue`/模块样式中使用居中 action group;按钮 `flex:0 0 96px`、间距12px、14px字体、44px命中高度,避免共享 `MpBottomSheet` 全局回归。
- [x] **Step 6:回归功能。** 保留五 Tab、查询筛选、分页、批量、上传预览/下载/删除、保存重试、权限、状态限制、合同和复制动作;独立详情新增直达/刷新/路由ID切换测试。运行 E2E 确认由红转绿。
- [ ] **Step 7:验证与交付。** 执行 model/入口测试、目标 ESLint/Stylelint/Prettier、生产构建、类型基线对比,查看 375/414px 列表/详情/Sheet/附件截图;更新交付与问题文档并更新 frontend/miniprogram/root 图谱。
## Task 1:独立移动模块(实现子任务)
Files (new, only): `web-admin/src/views/m/pl/plan-cert/model.ts`, `model.test.ts`, `index.vue`, `CertFilters.vue`, `CertUpload.vue`, `CertDetail.vue`, `CertAttachments.vue`, `PlanCard.vue`, `cert.scss`; 可将详情内运输复制和段次展示拆成同目录单一职责组件。
- [x] 先写 model 测试并执行 `cd web-admin && pnpm exec tsx --test src/views/m/pl/plan-cert/model.test.ts`,确认缺失实现导致失败。
- [x] 建立查询与操作纯函数:
```ts
buildCertQuery(tab, keyword, filters, page, pageSize)
canUploadCert(plan) // archiveStatus !== archived && followStatus !== cancelled
eligibleReadyPlans(plans, ready) // 含危化品、可操作且当前状态不同
certPayload(products) // 仅 id、purchaseCertUrl、transportCertUrl、legalUseCertUrl
```
测试五 Tab `hasHazardProduct=1`、未上传 `certUploadStatus=incomplete/hazardCertReady=0`、默认 followStatus 排除 cancelled、Tab 冲突优先、显式已取消筛选、空/多 URL 保留和去除无关字段、归档/已取消及批量跳过。
- [x] 实现列表:五个固定顺序文字 Tab,关键词、日期快捷、全部字段筛选、排序、刷新、分页重试、可展开卡片、多选和清除选中。请求版本号拒绝过期响应;只有成功后更新分页;Tab/查询变化清空选中。按 Web 配对行工具过滤卡片段次,保留全部原列信息。
- [x] 实现筛选面板:直接读 Web `columnFilterDefs`,enum 静态选项/动态 facet,datetime 范围、number 操作符映射到 queryKeyPrefix,text 映射 queryKey;应用前校验范围。复用 Web 装送客户/合同侧别参数转换,不改原文件。
- [x] 实现上传面板:每次打开取最新详情并 enrich 危化属性;使用共享 getRequiredCertSlots、AttachmentUploadField;暴露上传中检查,保存前必须检查全部实例;允许空附件,不自动齐备;保存失败保留内容;上传 PATCH 只提交产品证书字段。
- [x] 实现详情和卡片附件:显示全部段次/产品及数量、基本信息、车辆、运输回传、运费、备注、证书和合同关联;仅保留 Web 本菜单原操作。复制给司机/现场及磅单二维码沿用 usePlPlanArrangeActions,不引入新增计划/作废/安排运输。
- [x] 权限先获取 `fetchGetMenuCodes()`,上传/齐备分别判断 Web 按钮权限;失败关闭写入口。提交二次确认,批量逐条处理并汇报成功/失败/跳过,完成后刷新当前详情及列表。
- [x] module 样式只在本模块 `.cert-module` / `.cert-*` 及挂载期间的弹层范围内,三墙绿、44px 触达、375/414px 不横向溢出,附件同列换行。所有新增 Vue API/Element 组件使用已有自动导入。
- [x] 执行 model 测试、目标 ESLint/Prettier,报告结果及发现但未改的其他模块问题。
- [x] 独立规格审查通过后进行代码质量审查,修复本次新增问题。
## Task 2:入口和菜单(主任务集成)
Files: `web-admin/src/router/routes/staticRoutes.ts`, `miniprogram/config/api.js`, `miniprogram/config/workbench-menus.js`, `docs/sql/migrations/2026-10-17-mini-pl-plan-cert-webview-menus.sql`, `docs/miniprogram-ui-spec-sanqiang.md`, `docs/web-admin-mp-webview-modules.md`。
- [x] 先新增 `web-admin/scripts/mp-pl-plan-cert-entry.test.mjs`,断言路由、工作台所属组、映射和 SQL 不修改 Web 菜单,运行 node test 验证缺入口失败。
- [x] 只读确认测试库移动端运输父菜单;绝不打印连接密码,不写数据库。
- [x] 路由接入:
```ts
{ path: '/m/pl/plan-cert', name: 'MpPlPlanCert',
component: () => import('@/views/m/pl/plan-cert/index.vue'),
meta: { title: '危化品证上传', isHideTab: true, isMpWebview: true } }
```
- [x] 工作台加入 `{name:'危化品证上传', enabled:true, code:'pl-plan-cert-webview', icon:'📑', perms:'mini:pl:plan-cert:list'}`;路由映射 `PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/pl/plan-cert')`。
- [x] SQL 使用实际查到的移动根/运输父菜单关系,不新建同名父级;事务中新增移动叶子并对有 Web 列表权限角色补祖先,保持幂等、租户隔离、仅本模块菜单。未授权不执行。
- [x] 写移动页面结构规范、模块路径/权限/首次发布说明,复跑入口测试与 node --check 两个 mini 配置。
## Task 3:集成验证、审查、交付
Files: `web-admin/scripts/test-mp-pl-plan-cert.mjs`, `docs/superpowers/specs/2026-10-17-mp-pl-plan-cert-delivery.md`, `docs/superpowers/specs/2026-10-17-mp-pl-plan-cert-existing-issues.md`。
- [x] 参考客户测试建立仅在 API 边界模拟的浏览器测试,页面模块使用真实代码;拦截 `/api/` 请求,不误拦截 `/src/api/`,未知业务写请求直接失败。
- [x] 测试五 Tab 参数、全字段筛选代表路径、关键词日期、分页失败重试、选中重置、单/批齐备和取消、无权限、归档/取消限制、详情关联信息、附件选择/上传等待/删除/保存失败与重试,断言没有导出按钮。
- [x] 截图375和414宽度的列表/筛选/详情/附件,检查无横向溢出、关键信息可见、附件卡片一致。
- [x] `pnpm build`、`pnpm exec vue-tsc --noEmit`、新增测试与目标 lint;类型检查如失败和前序675错误基线比较,新增错误必须清零,不修其他模块错误。
- [x] 对实施前保存的 Web/后端/共享组件 SHA256 作逐项比较,证明未改用户禁止范围。
- [x] 规格审查→代码质量审查→修复→复测,记录真实结果,不以模拟测试代替真机验收。
- [x] `./scripts/update-graph.sh frontend`、`graphify update miniprogram`、`graphify update .` 更新对应图谱;图谱体量限制或缺少工具如实记录。
- [x] 交付文档覆盖完整需求、执行命令结果、待发布/SQL步骤和既有问题。不提交/推送混有用户更改的工作区,不写生产库。
@@ -0,0 +1,83 @@
# 客户详情四 Tab UI 对齐 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:** 将客户详情中商机/合同/开票/操作日志四个 Tab 的列表对齐联系人范式,详情弹层对齐资料 Tab。
**Architecture:** `InvoicePanel` 独立先改;`RelatedPanel` 按 `kind` 统一列表卡 + 三栏筛选,合同改为弹层查看;复用 `mp-customer-detail-card` 与联系人 teleport 约定;不改 API。
**Tech Stack:** Vue 3 + Vant 4 + SCSS(三墙绿);参考 `ContactPanel.vue`。
**Spec:** `docs/superpowers/specs/2026-10-18-mp-crm-customer-detail-tabs-design.md`
---
## File map
| File | Responsibility |
|------|----------------|
| `web-admin/src/views/m/crm/customer/InvoicePanel.vue` | 开票列表骨架 + 展开操作 + 表单 teleport |
| `web-admin/src/views/m/crm/customer/RelatedPanel.vue` | 商机/合同/日志列表 + 资料风弹层 |
| `web-admin/src/views/m/crm/customer/customer.scss` | 相关 panel 筛选/卡样式(复用 contact 节奏) |
| `docs/superpowers/specs/2026-10-18-mp-crm-customer-detail-tabs-design.md` | 状态改为已确认 |
不强制新建共用组件;`LocalFilters.vue` 本轮仅从上述两文件解除引用,文件保留。
---
### Task 1: 规格状态 + InvoicePanel 列表骨架
**Files:**
- Modify: `docs/superpowers/specs/2026-10-18-mp-crm-customer-detail-tabs-design.md`(状态行)
- Modify: `web-admin/src/views/m/crm/customer/InvoicePanel.vue`
- Modify: `web-admin/src/views/m/crm/customer/customer.scss`(如需 `.mp-customer-invoice-panel` 复用 contact-dd)
- [x] **Step 1:** 规格状态改为「已确认」。
- [x] **Step 2:** 重写 `InvoicePanel` template/script:
- 根 `section.mp-customer-invoice-panel`(或复用 `mp-customer-contact-panel` 类结构)
- 操作栏:新增 + 刷新
- `input.mp-wv-search` 占位「单位名称 / 税号 / 银行」
- `van-dropdown-menu`:`isDefault`(全部/默认/非默认)、`taxFilter`(全部/有税号/无税号)、更多 Sheet(单位名/税号/地址/法人/代理人/电话/银行/账号文本,来自 `invoiceFields` 除已在快捷的)
- 卡片:`mp-customer-detail-card`,标题 `companyName`,Tag 默认/非默认,摘要 `taxNo · bankName`,默认收叠,展开 `invoiceFields` 行 + 设默认/编辑/删除
- 编辑/新增 popup、`defaultOpen` picker 均 `teleport="body"`
- 去掉 `LocalFilters` 与列表态 `InvoiceFields`/`van-cell-group`
- [x] **Step 3:** 手工核对:筛选逻辑客户端过滤;`save`/`setDefault`/`remove`/`load` API 不变。
- [ ] **Step 4:** Commit(若用户要求再提交;默认本会话可暂不 commit)
### Task 2: RelatedPanel 列表骨架(三 kind)
**Files:**
- Modify: `web-admin/src/views/m/crm/customer/RelatedPanel.vue`
- Modify: `web-admin/src/views/m/crm/customer/customer.scss`
- [x] **Step 1:** 列表区改为联系人式:检索 + 两快捷 + 更多 + 扁平卡 + 展开字段 +「查看」;保留加载更多与 `total`。
- [x] **Step 2:** 按 kind 配置快捷/占位/卡摘要(见 spec §4.1–4.4);更多 Sheet 字段映射现有 `buildDetail*ColumnFilterDefs` 的 text/datetime/number/enum(可用简化:text 用 van-field,enum 用 picker,日期可先文本包含匹配以控范围)。
- [x] **Step 3:** 去掉列表态 `LocalFilters`、`van-field` 标签检索、`van-cell-group` 列表。
### Task 3: RelatedPanel 详情弹层资料风 + 合同不跳转
**Files:**
- Modify: `web-admin/src/views/m/crm/customer/RelatedPanel.vue`
- [x] **Step 1:** 合同 `open`:取消 `router.push`;打开弹层,`detail = row`,标题「合同详情」。
- [x] **Step 2:** 弹层 DOM 对齐资料:`section.mp-wv-section` + `h3` + `CustomerFields`;`teleport="body"`;日志加请求参数 section。
- [x] **Step 3:** 商机:保留 `OpportunityActions` + 跟进/阶段;子项改为 `mp-customer-detail-card` 可收叠。
### Task 4: 验收与图谱
- [x] **Step 1:** 确认 `LocalFilters` 是否仍被引用;仅 Invoice/Related 解除即可。
- [x] **Step 2:** `./scripts/update-graph.sh frontend`(或 `graphify update`)。
- [x] **Step 3:** 对照 spec §7 自检清单。
---
## Spec coverage
| Spec | Task |
|------|------|
| §3 共用列表骨架 | 1–2 |
| §4.3 开票 | 1 |
| §4.1/4.2/4.4 商机合同日志列表 | 2 |
| §4.1–4.2/4.4 弹层 + 合同不跳转 | 3 |
| §5 DOM 契约 | 3 |
| §7 验收 | 4 |
@@ -0,0 +1,13 @@
# 客户管理列表卡(全量 Web 列)Implementation Plan
> 已在同会话落地(2026-10-18)。
**Goal:** 客户列表卡对齐产品卡范式;固定展示 Web 客户管理全部列;去掉「显示字段」配置。
**Done:**
- [x] `model.ts`:`CUSTOMER_LIST_CARD_FIELDS` + 新 meta / split
- [x] `model.test.ts` 更新并通过
- [x] `index.vue`:固定预览/展开;meta 改为状态·编号·时间
- [x] `CustomerFilters.vue`:去掉显示字段
- [x] `mp-webview.scss`:客户卡 toggle 去虚线
- [x] Spec:`docs/superpowers/specs/2026-10-18-mp-crm-customer-list-card-design.md`
@@ -0,0 +1,17 @@
# 产品管理对齐客户样式 Implementation Plan
> **For agentic workers:** Use executing-plans / subagent-driven-development. Steps use checkbox syntax.
**Goal:** 产品 H5 整模块视觉交互对齐客户管理(方案 A)。
**Architecture:** 列表绿头内 Tab/三栏;详情 sticky van-tabs;表单外壳 Vant loading;分类 Vant popup。API 不变。
**Tech Stack:** Vue 3 + Vant + 三墙绿;参考 customer index/detail。
---
- [x] Task 1: 规格 `docs/superpowers/specs/2026-10-18-mp-crm-product-align-customer-design.md`
- [x] Task 2: 列表 `index.vue` + `product.scss` + `mp-webview.scss` 共用列表样式
- [x] Task 3: 详情 `detail.vue`
- [x] Task 4: 表单外壳 `form.vue`
- [x] Task 5: 分类 `categories.vue`
@@ -0,0 +1,555 @@
# 产品管理列表卡(去色块 + 折叠)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:** 小程序 WebView 产品管理列表去掉状态 Tab 与双色块,固定只查上架,卡片对齐 PC 默认可见列并用客户列表式「更多字段」折叠。
**Architecture:** 在 `mp-crm-product-card.ts` 集中查询固定 `statusIn=on`、卡片 meta/预览/展开字段与品牌别名价格格式化;`index.vue` 去掉 Tab、改 DOM;`mp-webview.scss` 将产品卡样式改为客户卡扁平结构。不改后端契约。
**Tech Stack:** Vue 3 + Vant(WebView)+ TypeScript;node:test 单测;三墙 Token。
**Spec:** `docs/superpowers/specs/2026-10-18-mp-crm-product-list-card-design.md`
**Demo:** `bak/mobile-ui/shanxia-mp-crm-product-list-card-demo.html`
---
## File map
| File | Responsibility |
|------|----------------|
| `web-admin/src/utils/mp-crm-product-card.ts` | 查询固定上架;卡片模型(meta、preview、extra、格式化) |
| `web-admin/src/utils/mp-crm-product-card.test.ts` | 上述行为单测 |
| `web-admin/src/views/m/crm/product/index.vue` | 去 Tab;折叠卡 DOM;expandedIds |
| `web-admin/src/styles/mp-webview.scss` | 删除产品色块样式;加产品扁平卡样式 |
| `web-admin/src/utils/money-format.ts` | 复用 `formatMoneyDisplay`(只读,不改) |
---
### Task 1: 单测驱动 — 查询固定上架 + 卡片字段拆分
**Files:**
- Modify: `web-admin/src/utils/mp-crm-product-card.test.ts`
- Modify: `web-admin/src/utils/mp-crm-product-card.ts`
- [x] **Step 1: 重写失败测试**
将 `mp-crm-product-card.test.ts` 替换为:
```ts
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
buildProductListQuery,
buildProductCard,
productStatusLabel,
productStatusTagType
} from './mp-crm-product-card'
describe('buildProductListQuery', () => {
it('always sets statusIn on (list is on-shelf only)', () => {
assert.deepEqual(
buildProductListQuery({
page: 1,
pageSize: 20,
keyword: ' 酸 ',
categoryId: 'cat-1',
model: ' 98% ',
priceMin: 10,
priceMax: 30
}),
{
page: 1,
pageSize: 20,
statusIn: 'on',
keyword: '酸',
categoryId: 'cat-1',
model: '98%',
priceMin: 10,
priceMax: 30
}
)
})
it('omits blank keyword and blank category/model', () => {
const q = buildProductListQuery({ page: 2, pageSize: 10, keyword: ' ' })
assert.deepEqual(q, { page: 2, pageSize: 10, statusIn: 'on' })
})
})
describe('buildProductCard', () => {
it('builds meta, preview, extra without cost; formats brands/aliases/price', () => {
const card = buildProductCard({
id: 'p1',
productNo: 'SX001',
productName: '硫酸',
categoryPath: '化工 / 酸类',
model: '98%',
status: 'on',
isToxic: 1,
isExplosive: 0,
createTime: '2026-10-17 09:00:00',
images: '/fixture/product.png',
createBy: 'user-1',
brands: [{ brandName: '山峡' }, { brandName: '华恒' }],
aliases: [{ aliasName: 'H2SO4' }],
price: 1280
})
assert.equal(card.id, 'p1')
assert.equal(card.productName, '硫酸')
assert.equal(card.statusLabel, '上架')
assert.equal(card.statusTagType, 'success')
assert.equal(card.showToxic, true)
assert.equal(card.showExplosive, false)
assert.equal(card.metaLine, '上架 · SX001 · 2026-10-17 09:00:00')
assert.deepEqual(card.previewFields, [
{ key: 'categoryPath', label: '产品分类', value: '化工 / 酸类' },
{ key: 'model', label: '规格型号', value: '98%' }
])
assert.deepEqual(
card.extraFields.map((f) => f.key),
['productNo', 'brands', 'aliases', 'price', 'status']
)
assert.equal(card.extraFields.find((f) => f.key === 'brands')?.value, '山峡、华恒')
assert.equal(card.extraFields.find((f) => f.key === 'aliases')?.value, 'H2SO4')
assert.equal(card.extraFields.find((f) => f.key === 'price')?.value, '¥1,280.00')
assert.equal(card.extraFields.find((f) => f.key === 'status')?.value, '上架')
assert.equal(card.extraFields.some((f) => f.key === 'cost'), false)
assert.equal(card.createBy, 'user-1')
})
it('uses em dash for empty brands/aliases/price/model', () => {
const card = buildProductCard({
id: 'p2',
productName: '片碱',
status: 'on'
})
assert.equal(card.metaLine, '上架 · — · —')
assert.equal(card.previewFields[0].value, '—')
assert.equal(card.previewFields[1].value, '—')
assert.equal(card.extraFields.find((f) => f.key === 'brands')?.value, '—')
assert.equal(card.extraFields.find((f) => f.key === 'price')?.value, '—')
})
})
describe('productStatusTagType', () => {
it('maps on→success off→info', () => {
assert.equal(productStatusTagType('on'), 'success')
assert.equal(productStatusTagType('off'), 'info')
assert.equal(productStatusLabel('on'), '上架')
assert.equal(productStatusLabel('off'), '下架')
})
})
```
- [x] **Step 2: 跑测确认失败**
Run(在 `web-admin` 目录):
```bash
pnpm exec tsx --test src/utils/mp-crm-product-card.test.ts
```
Expected: FAIL(`statusFilter` 仍为旧 API / 无 `metaLine` 等)
- [x] **Step 3: 实现 `mp-crm-product-card.ts`**
完整替换为:
```ts
import { formatMoneyDisplay } from '@/utils/money-format'
export type ProductBrandLike = { brandName?: string }
export type ProductAliasLike = { aliasName?: string }
export type ProductCardField = { key: string; label: string; value: string }
export function productStatusLabel(status?: string): string {
if (status === 'off') return '下架'
if (status === 'on') return '上架'
return status || '—'
}
export function productStatusTagType(status?: string): 'success' | 'info' | 'warning' {
if (status === 'on') return 'success'
if (status === 'off') return 'info'
return 'warning'
}
function dash(value?: string | null): string {
const t = String(value ?? '').trim()
return t || '—'
}
export function formatProductBrandsForCard(brands?: ProductBrandLike[]): string {
if (!brands?.length) return '—'
const names = brands.map((b) => String(b.brandName || '').trim()).filter(Boolean)
return names.length ? names.join('、') : '—'
}
export function formatProductAliasesForCard(aliases?: ProductAliasLike[]): string {
if (!aliases?.length) return '—'
const names = aliases.map((a) => String(a.aliasName || '').trim()).filter(Boolean)
return names.length ? names.join('、') : '—'
}
export function formatProductPriceForCard(value?: number | string | null): string {
if (value === null || value === undefined || value === '') return '—'
const num = Number(value)
if (Number.isNaN(num) || num === 0) return '—'
return `¥${formatMoneyDisplay(num)}`
}
/** 产品管理列表固定只查上架 */
export function buildProductListQuery(input: {
page: number
pageSize: number
keyword: string
categoryId?: string
model?: string
priceMin?: number
priceMax?: number
}): Record<string, unknown> {
const params: Record<string, unknown> = {
page: input.page,
pageSize: input.pageSize,
statusIn: 'on'
}
const kw = String(input.keyword || '').trim()
if (kw) params.keyword = kw
const categoryId = String(input.categoryId || '').trim()
if (categoryId) params.categoryId = categoryId
const model = String(input.model || '').trim()
if (model) params.model = model
if (input.priceMin !== undefined && Number.isFinite(input.priceMin))
params.priceMin = input.priceMin
if (input.priceMax !== undefined && Number.isFinite(input.priceMax))
params.priceMax = input.priceMax
return params
}
export function buildProductCard(row: {
id: string
productNo?: string
productName?: string
categoryPath?: string
categoryName?: string
model?: string
status?: string
isToxic?: number
isExplosive?: number
createTime?: string
images?: string
createBy?: string
brands?: ProductBrandLike[]
aliases?: ProductAliasLike[]
price?: number | string | null
}) {
const productNo = dash(row.productNo)
const statusLabel = productStatusLabel(row.status || 'on')
const createTime = dash(row.createTime)
const categoryPath = dash(row.categoryPath || row.categoryName)
const model = dash(row.model)
const coverImage = String(row.images || '')
.split(',')
.map((item) => item.trim())
.find(Boolean)
const previewFields: ProductCardField[] = [
{ key: 'categoryPath', label: '产品分类', value: categoryPath },
{ key: 'model', label: '规格型号', value: model }
]
const extraFields: ProductCardField[] = [
{ key: 'productNo', label: '产品编号', value: productNo },
{ key: 'brands', label: '品牌', value: formatProductBrandsForCard(row.brands) },
{ key: 'aliases', label: '别名', value: formatProductAliasesForCard(row.aliases) },
{ key: 'price', label: '标准价(元)', value: formatProductPriceForCard(row.price) },
{ key: 'status', label: '状态', value: statusLabel }
]
return {
id: row.id,
productNo,
productName: dash(row.productName) === '—' ? '—' : String(row.productName || '').trim() || '—',
categoryPath,
model,
status: row.status || '',
statusLabel,
statusTagType: productStatusTagType(row.status || 'on'),
showToxic: Number(row.isToxic) === 1,
showExplosive: Number(row.isExplosive) === 1,
createTime: row.createTime || '',
coverImage: coverImage || '',
createBy: row.createBy || '',
metaLine: `${statusLabel} · ${productNo} · ${createTime}`,
previewFields,
extraFields
}
}
```
- [x] **Step 4: 跑测确认通过**
```bash
pnpm exec tsx --test src/utils/mp-crm-product-card.test.ts
```
Expected: PASS
注:若 `@/` 别名在 tsx 测试中解析失败,改为相对导入 `./money-format`(同目录层级:`import { formatMoneyDisplay } from './money-format'`)。
---
### Task 2: 列表页 DOM — 去 Tab + 折叠卡
**Files:**
- Modify: `web-admin/src/views/m/crm/product/index.vue`
- [x] **Step 1: 更新 script 导入与状态**
删除对 `MP_PRODUCT_STATUS_FILTERS`、`MpProductStatusFilter` 的导入与使用。
- 删除 `statusFilter` ref、`onStatusChange`
- `requestSnapshot` / `fetchPage` / `onShellReady` 去掉 statusFilter
- `buildProductListQuery` 调用去掉 `statusFilter`
- 增加:
```ts
const expandedIds = ref<Set<string>>(new Set())
function isExpanded(id: string) {
return expandedIds.value.has(String(id))
}
function toggleExpand(id: string, e?: Event) {
e?.stopPropagation()
const key = String(id)
const next = new Set(expandedIds.value)
if (next.has(key)) next.delete(key)
else next.add(key)
expandedIds.value = next
}
```
- `reload` / `fetchPage(true)` 时清空:`expandedIds.value = new Set()`
- `ProductRow` 增加可选:`brands?: Array<{ brandName?: string }>`、`aliases?: Array<{ aliasName?: string }>`、`price?: number | string`
- [x] **Step 2: 更新模板 header**
删除整个 `#before-search` 的 `van-tabs` 块。`MpBusinessHeader` 仅保留搜索与 `#after-search` 下拉。
- [x] **Step 3: 更新卡片模板**
将 `article.mp-product-list-card` 内 head 以下至 actions 之前替换为(对齐客户列表):
```vue
<div class="mp-wv-card__head">
<div class="mp-product-list-card__head-main">
<div class="mp-product-list-card__titles">
<div class="mp-wv-card__no">
{{ card.productName }}
<span v-if="card.showToxic" class="status-tag status-tag--danger mp-product-hazard">毒</span>
<span v-if="card.showExplosive" class="status-tag status-tag--warning mp-product-hazard">爆</span>
</div>
<div class="mp-product-list-card__meta">{{ card.metaLine }}</div>
</div>
</div>
<span class="status-tag" :class="`status-tag--${card.statusTagType}`">{{
card.statusLabel
}}</span>
</div>
<div class="mp-product-list-card__preview">
<div
v-for="field in card.previewFields"
:key="field.key"
class="mp-product-list-card__line"
>
<span>{{ field.label }}</span>
<span>{{ field.value }}</span>
</div>
</div>
<button
class="mp-product-list-card__toggle"
type="button"
:aria-expanded="isExpanded(card.id)"
@click="toggleExpand(card.id, $event)"
>
{{ isExpanded(card.id) ? '收起' : '更多字段' }}
<span class="mp-product-list-card__toggle-icon" aria-hidden="true">{{
isExpanded(card.id) ? '▴' : '▾'
}}</span>
</button>
<div v-show="isExpanded(card.id)" class="mp-product-list-card__extra">
<div
v-for="field in card.extraFields"
:key="field.key"
class="mp-product-list-card__row"
>
<span>{{ field.label }}</span>
<span>{{ field.value }}</span>
</div>
</div>
```
保留原有 `mp-wv-card-actions` 四按钮不变。
- [x] **Step 4: 手动冒烟检查点**
打开 `/#/m/crm/product`:无状态 Tab;卡无绿/灰色块;可展开更多字段含品牌/别名/标准价;下架后卡消失。
---
### Task 3: 样式 — 去色块、对齐客户扁平卡
**Files:**
- Modify: `web-admin/src/styles/mp-webview.scss`
- [x] **Step 1: 替换产品列表卡样式块**
删除注释 `/* 产品列表:保留强调条 + KV 色块 */` 起至该段结束的 `.mp-product-list .mp-product-list-card { ... }` 整块,改为:
```scss
/* 产品列表:扁平摘要 + 预览 / 展开(无色块,对齐客户列表) */
.mp-product-list .mp-product-list-card {
.mp-product-list-card__meta {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: #6b7b74;
}
.mp-wv-card__no .mp-product-hazard {
margin-left: 6px;
vertical-align: middle;
}
.mp-product-list-card__preview {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
}
.mp-product-list-card__line {
display: flex;
gap: 10px;
justify-content: space-between;
font-size: 13px;
color: #223b33;
> span:first-child {
color: #6b7b74;
font-size: 12px;
}
> span:last-child {
font-weight: 500;
text-align: right;
}
}
.mp-product-list-card__toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
width: 100%;
margin-top: 10px;
padding: 8px 0 2px;
font-size: 13px;
font-weight: 600;
color: #087f57;
background: none;
border: 0;
border-top: 1px dashed #dcebe4;
cursor: pointer;
}
.mp-product-list-card__toggle-icon {
font-size: 11px;
line-height: 1;
}
.mp-product-list-card__extra {
margin-top: 8px;
}
.mp-product-list-card__row {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 4px 0;
font-size: 13px;
line-height: 1.45;
> span:first-child {
flex: 0 0 auto;
min-width: 5em;
color: #6b7b74;
white-space: nowrap;
}
> span:last-child {
flex: 1;
min-width: 0;
color: #1f2a26;
text-align: right;
word-break: break-word;
}
}
}
```
- [x] **Step 2: 确认 `product.scss` 无需状态 Tab 残留依赖**
已从 `mp-webview.scss` 移除无引用的 `.mp-product-view-tabs` 选择器;`product.scss` 的 `.mp-product-tag-row` 仍被详情页使用,保留。
---
### Task 4: 图谱更新 + 验证
**Files:**
- Run: `./scripts/update-graph.sh frontend` 或 `graphify update .`(按仓库惯例)
- [x] **Step 1: 再跑单测**
```bash
cd web-admin && pnpm exec tsx --test src/utils/mp-crm-product-card.test.ts
```
Expected: PASS
- [x] **Step 2: 更新知识图谱**
```bash
./scripts/update-graph.sh frontend
```
或在仓库根目录:`graphify update .`(若脚本要求在子目录则用脚本)。
- [x] **Step 3: 完成核对(对照 spec §5.3)**
- [x] 无状态 Tab
- [x] 无绿底编号条 / 灰底 KV
- [x] 预览分类+型号;展开含编号/品牌/别名/标准价/状态;无成本价
- [x] 毒/爆小标 + 右上上架 Tag
- [x] 查询固定上架
---
## Spec coverage check
| Spec 项 | Task |
|---------|------|
| 去状态 Tab / 固定上架 | Task 1 + 2 |
| 去双色块 | Task 2 + 3 |
| PC 默认列字段(无成本价) | Task 1 |
| 折叠交互 | Task 2 + 3 |
| meta `上架 · 编号 · 时间` | Task 1 |
| 验证清单 | Task 4 |
## Commit note
按用户仓库规则:**未明确要求则不自动 git commit**。任务完成后由用户决定是否提交。
@@ -0,0 +1,27 @@
# 报价单 WebView 扁平卡对齐 Implementation Plan
> **For agentic workers:** 按任务顺序落地;本会话由用户授权跳过逐步确认。
**Goal:** 将报价单管理/审批列表与详情、表单外壳、确认弹窗对齐客户/产品扁平卡规范。
**Architecture:** 扩展 `mp-crm-quote-card.ts` 输出 preview/extra;`MpQuoteList` 改 Vant Tab + 三栏 + 扁平卡;详情确认走 `showCrmConfirmDialog`;样式复用 `mp-webview.scss` 客户/产品卡段。
**Tech Stack:** Vue 3、Vant、`mp-wv-*`、三墙 Token
---
### Task 1: 卡片模型与单测
**Files:** `web-admin/src/utils/mp-crm-quote-card.ts`、`mp-crm-quote-card.test.ts`
### Task 2: MpQuoteList 换皮
**Files:** `web-admin/src/views/m/crm/quote/MpQuoteList.vue`、`mp-webview.scss`、`scripts/test-mp-crm-quote-dom.mjs`
### Task 3: 详情确认 + 表单外壳
**Files:** `detail.vue`、`form.vue`
### Task 4: 验证
Run: `pnpm exec tsx --test src/utils/mp-crm-quote-card.test.ts`;`node scripts/test-mp-crm-quote-dom.mjs`
@@ -0,0 +1,227 @@
# 小程序 WebView · CRM 产品管理 — 设计文档
> **日期**: 2026-10-15
> **状态**: 待实现
> **方案**: 复用运输计划 WebView 模板;在 `web-admin` 新增 `/#/m/crm/product*`;工作台开放「CRM管理 → 产品管理」
> **权威接入清单**: `docs/web-admin-mp-webview-modules.md`
> **样式参考**: `/#/m/pl/plan*`(三墙 Token,`mp-webview.scss`)
## 1. 目标
在微信小程序中提供与 Web 管理端 **CRM管理 → 产品管理**(`/#/crm/product`)同等核心能力(列表 / 详情 / 新增编辑 / 删除 / 单条上下架 / 产品内分类维护),以 **WebView 加载 H5** 落地:
- 业务改动优先只发 `web-admin` 静态资源
- 不复制鉴权、不新建第二个 `web-view` 壳页
- 移动端视觉对齐「运输计划」H5,不套桌面 Fresh Mint
## 2. 已确认决策
| # | 项 | 决策 |
|---|-----|------|
| D1 | 接入方式 | **方案 1:嵌套 H5**(同运输计划),不写原生 `pages/crm/*` |
| D2 | 功能范围 | 列表 + 查看详情 + 新增/编辑 + 删除 + **单条**上下架 |
| D3 | 明确不做 | 导入、导出、批量上架、批量下架;桌面列头 facet / 列显隐 |
| D4 | 表单字段 | **全字段对齐 Web**:编号/名称/拼音/分类/型号/条码/税率/库存/预警/状态/易制毒爆/图片/描述/备注 + 品牌子表 + 别名子表 |
| D5 | 产品分类 | **产品页内**一并做分类增删改(树选 + 维护入口);不单独占工作台菜单 |
| D6 | H5 落点 | `web-admin` `/#/m/crm/product*`,复用现有 `/web/crm/products/*` 与 `product-categories/*` |
| D7 | 视觉 | 三墙绿 `#0a9f6d` / `#087f57`(`mp-webview.scss`),对齐运输计划骨架;**不**用管理端 `#10b981` 作主色 |
| D8 | 菜单 | `sys_menu`:移动端根下建「CRM管理」→「产品管理」;工作台硬编码打开 CRM 模块与产品入口 |
| D9 | 角色绑定 | 已有 Web `crm:product:list`(菜单 id `CRM00000000000000000000000000003`)的角色,**自动补绑**移动端父菜单与产品菜单 |
| D10 | 双权限 | 入口 `mini:crm:product:list`;业务 API 仍校验现有 Web 产品鉴权 |
## 3. 非目标
- 不把桌面 `el-table` / 多选工具栏原样塞进 WebView
- 不改产品后端业务规则、租户隔离与数据权限语义
- 本期不开放 CRM 其它叶子(线索/客户/合同等仍灰卡或 `enabled: false`)
- 不新建独立 `mobile-h5` 工程,不新增第二个 `mp-webview` 页面
- 不输出独立 HTML Demo 文件(样式直接对齐现网 `/m/pl/plan`)
## 4. 架构
```text
小程序工作台「CRM管理」→「产品管理」
→ pages/common/mp-webview?path=/m/crm/product
1) POST /api/web/auth/mp-webview-ticket (小程序 Bearer)
2) web-view src = {WEB_ORIGIN}/?mpTicket=…#/m/crm/product?mpTicket=…
3) H5:MpWebviewShell → POST /api/web/auth/mp-webview-exchange
4) 之后与桌面同账号调用 /web/crm/products/*、product-categories/*
```
```text
┌─────────────┐ ticket ┌────────────────────────┐ JWT ┌────────────┐
│ miniprogram│ ──────────────► │ web-admin /#/m/crm/… │ ───────────► │ FastAPI │
│ mp-webview │ ◄────────────── │ MpWebviewShell │ ◄─────────── │ 既有 CRM │
└─────────────┘ 401→reLaunch └────────────────────────┘ 租户/权限 └────────────┘
```
**边界**
| 单元 | 职责 | 依赖 |
|------|------|------|
| `pages/common/mp-webview` | 取票、拼 URL、承载 `<web-view>`(**已有,不改协议**) | 小程序 token、`WEB_ORIGIN` |
| `MpWebviewShell` | 无侧栏、安全区、401 桥接(已有) | 换票、`wx.miniProgram` |
| `/#/m/crm/product*` | 产品移动端 UI | 现有 `api/crm/product`、分类 API、上传 |
| 工作台 | 入口可见性与跳转 | `workbench-menus.js` + `WORKBENCH_ROUTE_MAP` + `menu-codes` |
| 菜单 SQL | 移动端目录/叶子 + 角色补绑 | `sys_menu` / `sys_role_menu` |
## 5. H5 路由与页面
### 5.1 静态路由
在 `web-admin/src/router/routes/staticRoutes.ts`(catch-all 之前)增加,且:
`meta: { isHideTab: true, isMpWebview: true }`
| 路径 | 文件(建议) | 用途 |
|------|--------------|------|
| `/m/crm/product` | `views/m/crm/product/index.vue` | 列表 |
| `/m/crm/product/new` | `views/m/crm/product/form.vue` | 新增 |
| `/m/crm/product/:id` | `views/m/crm/product/detail.vue` | 详情 |
| `/m/crm/product/:id/edit` | `views/m/crm/product/form.vue` | 编辑 |
| `/m/crm/product/categories` | `views/m/crm/product/categories.vue` | 分类维护 |
路由顺序注意:`/new`、`/categories` 须写在 `/:id` 之前,避免被当成 id。
### 5.2 列表(对齐运输计划骨架)
- **Header**:返回工作台 + 标题「产品管理」+ 搜索框(产品名称 / 编号 / 拼音首字母)
- **筛选**:状态(默认「上架」`statusIn: 'on'`;可选全部 / 下架)。**本期不做**列表侧分类筛选(分类仅在表单树选与分类维护页使用)
- **卡片流**:编号、名称、分类全路径(分隔符 ` / `)、型号、状态 Tag、易制毒/易制爆 Tag;点击进详情
- **Speed Dial**:主「新增产品」→ `/m/crm/product/new`;次「产品分类」→ `/m/crm/product/categories`
- **不做**:多选、导入导出、批量上下架、列头 facet、列显隐
### 5.3 详情
- 全字段只读(含品牌、别名、图片)
- 底栏:**编辑** / **上架或下架**(单条,调现有 `update-status`) / **删除**
- 删除按钮可用性与 Web 一致:前端 `canDeleteByCreator`;后端规则不变
### 5.4 表单(新增 / 编辑)
- 字段与 Web `ProductFormDialog` **全对齐**(见 D4)
- 分类:树选;可跳转分类维护页后返回
- 图片:复用现有附件上传能力,`folder: crm/product`,最多 5 张
- UI:使用 `mp-webview.scss` 与运输计划同类布局(页内表单 / Bottom Sheet);可对桌面表单抽 `variant="page"` 复用校验与提交逻辑,但视觉须移动端化,禁止桌面三列栅格原样塞手机
### 5.5 分类维护
- 树列表 + 新增 / 编辑 / 删除,调用现有 `product-categories` API
- 仅从产品模块进入,**不**在工作台再挂「产品分类」菜单
## 6. 小程序工作台
### 6.1 `workbench-menus.js`
- CRM 模块:`visible: true`
- 「产品管理」:`enabled: true`,`code: 'crm-product-webview'`,`perms: 'mini:crm:product:list'`
- 其它 CRM 叶子本期保持 `enabled: false`(可不展示或灰卡)
### 6.2 `config/api.js` · `WORKBENCH_ROUTE_MAP`
```text
'crm-product-webview' → PAGE_PATHS.mpWebview + '?path=' + encodeURIComponent('/m/crm/product')
```
禁止为 CRM 再建第二个 web-view 页。
## 7. 菜单与权限(SQL)
### 7.1 菜单树
挂在「移动端」根 `id = '1'`:
```text
移动端 (1)
└─ CRM管理 (MINI_CRM_ROOT) 目录;perms 可空
└─ 产品管理 (MINI_CRM_PRODUCT) perms = mini:crm:product:list
```
建议稳定 id(与其它 `MINI_*` 一致、幂等):
| id | menu_name | parent_id | perms |
|----|-----------|-----------|-------|
| `MINI_CRM_ROOT` | CRM管理 | `1` | (空或目录约定) |
| `MINI_CRM_PRODUCT` | 产品管理 | `MINI_CRM_ROOT` | `mini:crm:product:list` |
脚本路径建议:`docs/sql/migrations/2026-10-15-mini-crm-product-webview-menus.sql`(`INSERT … WHERE NOT EXISTS` 幂等)。
### 7.2 角色自动补绑
对所有在 `sys_role_menu` 中已绑定 Web 产品菜单
`CRM00000000000000000000000000003`(`crm:product:list`)的角色,补绑:
1. 移动端根 `1`(若尚未绑定)
2. `MINI_CRM_ROOT`
3. `MINI_CRM_PRODUCT`
全部使用 `NOT EXISTS` 防重。超管本身 `menu-codes` 为 `all: true`,不依赖此补绑也能见入口,但仍应写入菜单定义。
### 7.3 双权限说明
| 层 | 校验 |
|----|------|
| 工作台是否显示入口 | `mini:crm:product:list` ∈ `menu-codes` |
| list/detail/add/update/delete/update-status/分类 | 现有 Web CRM 产品鉴权(与桌面同) |
仅有移动入口、无 Web 产品权 → H5 内接口失败 / 展示「无权限」。
仅有 Web 权、未跑补绑 SQL → 工作台看不到入口。
## 8. 数据流与 API
**不新建业务 API。** 复用:
| 能力 | 既有能力(示意) |
|------|------------------|
| 列表 | 产品 list;默认 `statusIn: 'on'`;支持 keyword 搜索 |
| 详情 | product detail |
| 增改 | product add / update(含 brands、aliases、images) |
| 删除 | product delete |
| 单条上下架 | `update-status`(单 id,非批量工具栏) |
| 分类 | product-categories tree / CRUD |
| 上传 | 现有附件上传(`crm/product`) |
租户隔离、数据权限、默认排序与桌面同账号一致。
## 9. 错误处理与发版
| 场景 | 行为 |
|------|------|
| 无 `mini:crm:product:list` | 工作台不显示产品入口 |
| H5 业务 403/无权限 | 页内「无权限」 |
| 401 | 通知小程序回登录 / 工作台,**不**进 `/#/auth/login` |
| ticket 二次 exchange | 失败提示从工作台重进 |
**发版**
- H5 UI/业务:发 `web-admin` 即可(业务域名已配时)
- 工作台映射 / 菜单 code:需发带变更的 **小程序** 包
- 菜单与角色:在目标库执行迁移 SQL
## 10. 测试要点
- ticket 换票后列表数据与桌面同账号一致(同租户、默认同「上架」)
- 搜索、状态筛选、进详情、新增、编辑、单条上下架、删除(非创建人不可删)
- 分类维护增删改后,表单树选可见新节点
- 图片上传与品牌/别名子表可保存并回显
- 401 回小程序,不到桌面登录页
- 无 `mini:crm:…` 的账号工作台无入口;有 Web 产品权且跑过 SQL 的角色可见
- 导入/导出/批量上下架在移动端 **不出现**
- 桌面 `/#/crm/product` 行为不受影响
## 11. 实现清单(供后续 plan 拆任务)
1. `views/m/crm/product/*` + `MpWebviewShell` + `mp-webview.scss`
2. `staticRoutes.ts` 注册 `/m/crm/product*`
3. `workbench-menus.js` 打开 CRM + 产品;`WORKBENCH_ROUTE_MAP` 指向同一壳
4. SQL:`MINI_CRM_ROOT` + `MINI_CRM_PRODUCT` + 按 `crm:product:list` 角色补绑
5. 回归:对照 §10;对照 `docs/web-admin-mp-webview-modules.md` 五步清单
## 12. 参考
- `docs/web-admin-mp-webview-modules.md`
- `docs/superpowers/specs/2026-09-14-mp-webview-transport-plan-design.md`
- `docs/superpowers/specs/2026-09-15-mp-webview-pl-plan-mobile-redesign-design.md`
- Web 产品页:`web-admin/src/views/crm/product/index.vue`
- Web 表单:`web-admin/src/components/business/product-form-dialog/index.vue`
- 运输计划 H5:`web-admin/src/views/m/pl/plan/*`
@@ -0,0 +1,126 @@
# 运输计划相关列表导出扩展 · 段次×产品行 — 设计文档
> **日期**: 2026-10-15
> **状态**: 待实现
> **方案**: A — 与列表展平行一致(延续 `2026-10-15-plan-list-segment-export-design.md`)
> **前置**: 运输计划列表 + 入库单 + **出库单** 已落地段次导出(出库单复用 `PlanListPage`,**本扩展无需再改**)
## 1. 目标
将下列界面的 Excel 导出行模型对齐各自列表(`flattenPlanArchivePairRows`):
| 界面 | 路由/入口 | 当前导出 | 目标 |
|------|-----------|----------|------|
| 危化品证上传 | `plan-cert` | `planApi.export`(后端已段次展开;前端缺「段次」列) | 注入 `stopSegment` |
| 运输归档 | `plan-archive` | `exportFormat=archive` 配对宽表 | **改为段次×产品多行**(方案 A) |
| 合同完善 | `plan-contract` | `exportContractGaps` 一计划一行 | **改为段次×产品多行** + 本段缺口语义 |
| 出库单 | `outbound` → `PlanListPage` | 已共用列表导出 | **已完成,本扩展不改** |
## 2. 已确认决策
| # | 项 | 决策 |
|---|-----|------|
| E1 | 行结构 | 与基线设计 D1/D3/D4/D5 相同:先装货产品行,再各送货段;「段次」;侧栏按行、不拼接 |
| E2 | 归档列形态 | **方案 A**:去掉装/送同排配对宽列;本段 `计划量`/`实际量`;单价/金额只填本行所属侧 |
| E3 | 归档 ZIP | `archive_zip` 内 Excel **与** `archive` xlsx **同一行模型**(仅附件打包方式不变) |
| E4 | 合同完善 | 导出对齐列表展平;每行带段次与产品;缺口类型按**本行段**(装货↔供方、送货↔需方) |
| E5 | 证件上传 | 仍走列表导出 API;前端 `planCertExportColumns` 固定注入 `stopSegment` |
| E6 | 出库单 | 不改代码;验收确认即可 |
**修正基线 D6**:原「归档导出不改」作废,由本扩展 E2/E3 替代。
## 3. 非目标
- 不改各页列表 UI / 合并单元格
- 不改归档业务规则、价格权限(无价格权限用户仍隐藏单价/金额列)
- 不改合同完善的缺口判定业务逻辑(只改导出行展开与列)
- 不新增独立导出 HTTP 路径(可复用/改造现有 endpoint)
## 4. 分页面设计
### 4.1 危化品证上传(最小改动)
- **后端**:已用 `_build_plan_list_export_rows`,无需改行模型。
- **前端** `plan-cert/index.vue`:`planCertExportColumns` 在 `planNo` 后插入 `stopSegment`(与运输计划列表相同写法)。
- 文件名保持 `计划证件.xlsx`。
### 4.2 运输归档(方案 A)
**行模型**:复用与列表相同的段次×产品槽位迭代(可直接调用 `_iter_segment_product_slots` / 与 `_build_plan_list_export_rows` 同序的辅助函数)。
**列(替换现有 `PLAN_ARCHIVE_EXPORT_COLUMNS` 宽表)** 建议:
| field | 表头 | 规则 |
|-------|------|------|
| `rowNo` | 序号 | 可选保留 |
| `planNo` | 计划编号 | 计划级,每行重复 |
| `stopSegment` | 段次 | `装货` / `送货 #N` |
| `contractNo` | 合同编号 | **本行段**合同 |
| 计划日期 / 归档状态 / 跟进状态 / 单据类型 | … | 计划级 |
| `loadCustomer` / `loadAddress` / `loadTime` | 装货侧 | **仅装货行** |
| `deliveryCustomer` / `deliveryAddress` / `deliveryTime` | 送货侧 | **仅送货行** |
| 产品名称 / 品牌 / 别名 / 型号 / 包装 / 单位 | … | 本行段产品 |
| `planQty` / `actualQty` | 计划量 / 实际量 | 本段;实际量语义同列表(装货 inbound / 送货 outbound) |
| `unitPrice` / `amount` | 单价 / 金额 | **仅本侧**归档价额;对侧行不填另一侧价 |
| 车牌 / 运费类字段 / ERP / 备注 | … | 计划级(运费仍整单重复,与列表习惯一致) |
**删除**:`loadPlanQty`/`deliveryPlanQty` 及成对的 `loadActualQty`/`deliveryActualQty`/`loadInboundQty`/…/`deliveryAmount` 等同排宽列;`deliveryStopKind` 由统一 `stopSegment` 替代。
**实现落点**:
- 改造 `build_plan_archive_export_rows`(或新建 `build_plan_archive_segment_export_rows` 并切换调用)走段次槽位,**不再** `_append_paired_product_rows` 默认路径。
- 单价/金额:复用归档已有 `_get_stop_archive_unit_price` / `_get_stop_archive_amount`(按 kind)。
- 单测:重写/新增 `test_pl_plan_archive_export.py` 中配对宽表断言 → 段次多行断言;保留价格权限列过滤行为。
### 4.3 合同完善
**现状**:`POST /contract-gaps/export` 对 gap 列表 **一计划一行**(供方/需方/缺口类型摘要),与列表 `displayRows`(段×产品)不一致。
**目标行模型**:
1. 仍用现有筛选得到「有缺口的计划」集合(`list_plan_contract_gaps` 的计划 id 集或等价查询)。
2. 对这些计划加载 stops,按 `_iter_segment_product_slots` 展开行。
3. 列建议:
| field | 表头 | 规则 |
|-------|------|------|
| `planNo` / `planDate` / `planType` / `followStatus` | … | 计划级 |
| `stopSegment` | 段次 | |
| `loadCustomer` / `deliveryCustomer` | 装/送客户 | 按行侧填写 |
| `deliveryTimes` | 送货时间 | 仅送货行本段日期(或与列表一致) |
| `productName` 等 | 产品 | 本行段产品(可与列表可见列对齐,最小集:产品名称) |
| `gapType` | 缺口类型 | **本行段**:装货行若缺供方合同 →「缺供方合同」;送货行若缺需方 →「缺需方合同」;本段无缺口可空或「—」(导出是否过滤无缺口段:默认**仍导出该计划全部段×产品行**,与列表展示一致;若某计划仅因一侧缺口入选,另一侧行 `gapType` 可空) |
| `supplierName` / `customerName` | 供方/需方 | 计划级保留(可选) |
**实现落点**:`export_contract_gaps` 改为:查 gap 计划 → `load_plan_stops_grouped` → 段次展开 → 填缺口标签;更新 `CONTRACT_GAP_EXPORT_COLUMNS`;前端 `contractGapExportColumns` 增加 `stopSegment` 等与新列对齐。
### 4.4 出库单
已由基线实现覆盖。本扩展只要求手工验收:出库单导出含「段次」且一装多送多行。
## 5. 架构关系
```text
基线(已完成)
_build_plan_list_export_rows ← 运输计划 / 入库 / 出库 /(证件仅缺前端列)
本扩展
plan-cert 前端注入 stopSegment
build_plan_archive_export_rows → 段次×产品(方案 A 列)
export_contract_gaps → 段次×产品 + 本段 gapType
```
共享:`_iter_segment_product_slots`、`_format_stop_segment_label`、段侧字段填写、CRM 别名/品牌 lookup、实际量 inbound/outbound 语义。
## 6. 测试与验收
1. **证件**:导出含「段次」;行数与证件列表展平一致(同筛选)。
2. **归档**:一装多送 ≥3 行;无装/送同排配对量列;本行有计划量/实际量;有价格权限时单价/金额仅本侧有值;`archive` 与 zip 内表结构一致;旧配对列测试更新后全绿。
3. **合同完善**:一装多送多行;段次正确;缺口列按段;文件名仍为「计划合同完善_*」。
4. **出库单**:抽测一装多送(回归)。
## 7. 验收标准
- 上述四页(含出库抽测)Excel 均可按段次对照列表阅读一装多送。
- 归档不再出现「一行同时装货量+多送货量拼接」的旧宽表形态。
- 无价格权限用户归档导出仍不含单价/金额列。
@@ -0,0 +1,144 @@
# 运输计划 / 库存列表导出 · 段次×产品行 — 设计文档
> **日期**: 2026-10-15
> **状态**: 待实现
> **方案**: A — 完全对齐列表展平行(`flattenPlanArchivePairRows`)
> **范围**: 运输计划列表 + 库存管理入库单/出库单(共用同一套导出逻辑)
## 1. 目标
列表导出 Excel 的行模型与列表 UI 一致:
- 一装多送时,能按段次拆成多行(先装货产品行,再各送货段产品行)
- 每行的「计划量 / 实际量」为本行所属段上的数量,不再把多段产品/客户压成一行
- 库存管理(`stockRole=inbound|outbound`)与运输计划列表走**同一**后端导出构建函数
## 2. 已确认决策
| # | 项 | 决策 |
|---|-----|------|
| D1 | 行结构 | **方案 A**:段次×产品多行,对齐 `flattenPlanArchivePairRows` |
| D2 | 作用面 | 运输计划列表 + 入库单 + 出库单,共用逻辑 |
| D3 | 段次列 | 新增「段次」列:`装货` / `送货 #1` / `送货 #2`… |
| D4 | 客户/地址列 | 装货行只填装货侧;送货行只填对应送货段;**不再**用 ` / ` 拼接多送客户 |
| D5 | 计划级字段 | 计划编号等每行重复(Excel 不做合并单元格) |
| D6 | 归档导出 | **不改**归档配对宽表(`build_plan_archive_export_rows`) |
| D7 | 文件名 / Sheet | 保持现有:`入库单` / `出库单` / `计划列表` |
## 3. 非目标
- 不改列表 UI、合并单元格、斑马纹等展示逻辑
- 不改计划归档导出的配对宽表与「装货量+送货量同列」结构
- 不新增独立导出 API;仍走现有 `plan` 列表导出入口
- 不做前端本地拼 Excel;行展开在后端完成
## 4. 现状问题
| 层 | 现状 | 问题 |
|----|------|------|
| 列表 | `flattenPlanArchivePairRows`:装货产品行 → 各送货段产品行;`planQty`/`actualQty` 取本段产品 | 正确 |
| 列表导出 | `_build_plan_list_export_rows`:按**计划级** `PlPlanProduct` 展开;装/送客户、地址用 ` / ` 汇总整单 | 一装多送时一行;数量无法按段对照列表 |
## 5. 行模型
对每个计划,按 `stops_by_plan`(`load_plan_stops_grouped`)展开:
```text
装货段.products[] → 行:段次=装货,产品/量=装货段产品
送货段1.products[] → 行:段次=送货 #1,产品/量=该送货段产品
送货段2.products[] → 行:段次=送货 #2,…
```
### 5.1 顺序与占位
1. **先**全部装货段产品行,**再**按送货 `seq` 依次展开各送货段产品行(与列表一致)。
2. 某侧无产品:仍输出该侧**占位行**(产品列、计划量、实际量为空),与列表「空产品仍占一行」行为一致。
3. **无段结构(legacy)**:无 load/delivery 段时,回退为计划级 `PlPlanProduct` 一行(段次为空),兼容旧数据。
4. 筛选条件(含 `stockRole`、列头筛选等)仍由现有列表查询决定「导出哪些计划」;本设计只改**单计划内行展开**。
### 5.2 示例
计划 `PO20261009016`:装货 1 个产品、送货 2 段各 1 个产品 → 导出 **3 行**:
| 段次 | 装货客户 | 送货客户 | 产品 | 计划量 |
|------|----------|----------|------|--------|
| 装货 | A | (空) | 尿素 | 30 |
| 送货 #1 | (空) | B1 | 尿素 | 20 |
| 送货 #2 | (空) | B2 | 尿素 | 10 |
## 6. 列语义
### 6.1 新增列
| field | 表头 | 取值 |
|-------|------|------|
| `stopSegment` | 段次 | `装货`;`送货 #{seq}`(seq 缺省时用展开序号) |
默认导出列顺序:建议紧挨「计划编号」之后。前端可见列导出映射:若列表无独立段次 prop,仍由后端默认列包含 `stopSegment`;前端 `planExportColumns` 增加 `{ prop: 'stopSegment', label: '段次' }`(或等价别名),以便「按可见列导出」时可选。
### 6.2 按行填写规则
| 列 | 装货行 | 送货行 |
|----|--------|--------|
| 计划级(编号、归属公司、跟进状态、计划日期、单据类型、车牌、人员、跟进人、供方、需方、司机、运输/归档状态、ERP、备注等) | 重复填计划值 | 同左 |
| 送货时间(`deliveryTimes`) | 空 | 本送货段日期(单值,不再整单 ` / ` 拼接) |
| 装货客户 / 装货地址时间 / 装货地磅 | 本装货段 | 空 |
| 送货客户 / 送货地址时间 / 送货地磅 | 空 | 本送货段 |
| 合同(`productContract`) | 本装货段合同号 | 本送货段合同号 |
| 产品名称 / 品牌 / 别名 / 型号规格 / 包装规格 / 单位 | 本行段产品(CRM 标准名/别名解析保持现有) | 同左 |
| 计划量 / 实际量 | 本行段产品 `planQty`;实际量语义对齐列表 `getRowActualQty`(装货读 inbound 侧实际量,送货读 outbound 侧实际量) | 同左 |
枚举中文标签(跟进状态、单据类型、运输/归档状态、归属公司等)与文件名/Sheet 标题逻辑**保持**现有实现。
### 6.3 字典与别名
- 单位等 `dict_type` 列仍走现有 `export_rows_to_xlsx_response` 字典翻译。
- `_PLAN_EXPORT_COLUMN_ALIASES`:如前端 prop 与 field 不一致则补映射;`stopSegment` 本身可直接作为 prop。
## 7. 架构与落点
```text
ListExportDropdown(运输计划 / 入库 / 出库)
→ POST 现有 plan 导出 API(带 filters + stockRole + columns)
→ 查询 plans(不变)
→ load_plan_stops_grouped
→ _build_plan_list_export_rows 【改为段次×产品展开】
→ PLAN_EXPORT_COLUMNS(+ stopSegment)
→ xlsx(filename/sheet 按 stockRole)
```
| 单元 | 职责 | 依赖 |
|------|------|------|
| `_build_plan_list_export_rows` | 单计划 → 多行 dict;段次与侧栏字段按行写入 | `stops_by_plan`、CRM 别名 map、段产品 |
| `PLAN_EXPORT_COLUMNS` | 列定义含 `stopSegment` | — |
| `_apply_plan_list_export_stop_fields` | 改为**按单侧 stop**填写,或拆成 load/delivery 专用 helper,避免整单拼接 | 单条 load 或单条 delivery |
| `web-admin` `plan/index.vue`(及库存页复用) | 导出列清单增加段次;文件名已按 stockRole | 共用 `PlanListPage` |
| `tests/test_pl_plan_list_export.py` | 一装多送行数、段次文案、侧栏字段空/非空、qty | — |
**建议**:段次展开逻辑可抽到 `backend/app/services/pl/` 下小函数(如 `iter_plan_list_export_segments`),便于单测且与 `plan.py` 路由层解耦;若改动面小,也可先内联在 `_build_plan_list_export_rows`,以最小必要为准。
## 8. 实际量语义(与列表对齐)
列表:
- `getRowPlanQty` ← 本行 `PlanStopProduct.planQty`
- `getRowActualQty` ← `getStopArchiveActualQty`:装货用 `inboundQty`,送货用 `outboundQty`
导出 MUST 使用同一语义读取段产品字段(后端 stop product 模型上的对应属性),不得再写计划级 `PlPlanProduct.actual_qty` 作为有段计划的实际量来源。
## 9. 测试要求
至少覆盖:
1. **一装多送**:1 装货产品 + 2 送货段各 1 产品 → 3 行;段次为 `装货`/`送货 #1`/`送货 #2`;各行 `planQty` 为段量;装货行 `deliveryCustomer` 为空、送货行 `loadCustomer` 为空。
2. **单装单送**:行为与列值正确;中文标签回归(跟进状态、单据类型等)。
3. **空产品占位**:送货段无产品仍有一行,产品/量为空。
4. **legacy**:无 stops 时仍按计划级产品展开,段次为空。
5. **文件名**:`stockRole` → `入库单_` / `出库单_` / `计划列表_`(现有用例保留)。
6. **列过滤**:请求 `stopSegment` 时出现在导出列;别名 normalize 不破坏既有 prop。
## 10. 验收标准
- 对 `PO20261009016` 类一装多送计划:导出 Excel 行数与列表展平行数一致(同筛选、同计划集合前提下)。
- 打开「入库单.xlsx / 出库单.xlsx / 计划列表.xlsx」均可看到「段次」及按段拆分的计划量/实际量。
- 计划归档导出行为不变(回归抽测或现有 `test_pl_plan_archive_export` 全绿)。
@@ -0,0 +1,43 @@
# 小程序 H5 客户管理交付说明
## 交付范围
入口:小程序工作台 → CRM管理 → 客户管理;H5 路径 `/#/m/crm/customer`。
- 四类客户视图、搜索、字段筛选、排序、分页、卡片字段显隐。
- 新增、编辑、详情、单条/批量删除、放入公海;名称查重与负责人默认值。
- 营业执照/经营许可证/其他证件、跟进附件沿用现有卡片式即时上传和预览/下载/删除能力。
- 跟进、联系人、开票资料(默认项、快速识别)、日志详情。
- 关联合同打开现有移动合同详情;关联商机含详情、修改、阶段推进、跟进维护、删除,保留原状态和创建人限制。
- 不提供导出。不修改 Web 客户/商机页面、共享 API 和后端;工作区原有其他改动保留。
实现位置:`web-admin/src/views/m/crm/customer/`。界面沿用现有 H5 样式,页面参照 P08/P12/P25 与移动产品、合同页面;评审补齐了商机专用跟进字典、类型兜底选项、跟进时间字段及筛选上下文。
## 验证
- `pnpm exec tsx --test src/views/m/crm/customer/model.test.ts`:5 项通过,覆盖精确 ID、附件清空/兼容、响应解包、本地时间、创建后开票重试、商机阶段建议。
- `node scripts/test-mp-crm-customer.mjs`:隔离模拟 API 的手机浏览器回归;四视图、筛选、客户跟进、联系人、开票默认、日志转义、商机修改/阶段/跟进增改删、附件上传、客户新增与编辑、开票失败重试、批量公海与删除、列表重试;375/414px 无横向溢出。不会写入真实业务库。
- 客户模块 ESLint、Prettier、小程序两份入口配置的语法检查通过。
- 生产构建通过;全量 TypeScript 错误与开发前基线一致(675 条原有错误),新增客户模块无新增错误,不能视为全项目类型检查通过。
- 已执行前端、小程序与根目录的 AST 图谱更新。图谱超过 HTML 可视化节点上限,因此更新 JSON 和报告,工具跳过 HTML 重建。
- 字段一致性脚本缺失,已记录;未涉及后端业务 schema/CRUD 改动,已核对移动字段与原接口。
## 首次发布顺序与待验收项
1. 在目标数据库备份和确认后执行 `docs/sql/migrations/2026-10-17-mini-crm-customer-webview-menus.sql`。此脚本仅补充移动菜单及拥有 Web 客户权限角色的移动菜单祖先绑定;本次没有执行数据库迁移。
2. 构建并发布 H5 工程。业务域名、HTTPS、既有 WebView ticket 交换与附件白名单配置沿用已有模块。
3. 发布包含工作台客户入口的小程序包。第一次增加入口需要更新小程序包;后续页面变更可仅发布 H5。
4. 真机验收:不同角色/租户的可见性与数据范围;登录票据过期与重新进入;图片/PDF选择、上传、预览/下载;软键盘与底部保存栏;合同详情返回导航。
本次完成仓库内开发和本地模拟验证,未发布生产、未运行微信真机验收。
### 测试库执行记录(2026-10-17,用户后续授权)
- 已在 `backend/.env` 对应测试库 `47.99.164.66:3306 / shanxia2.0` 执行上述菜单 SQL,事务提交成功;未连接生产库执行写入。
- 新增 `MINI_CRM_CUSTOMER` 菜单,绑定管理员、销售主管、销售计划员、销售实录员、销售办证员 5 个已有 Web 客户权限角色。
- 提交后通过新连接复核:菜单可见/启用、5 个角色授权有效、移动端与 CRM 父菜单权限无缺失;未修改业务表结构,其他菜单数据保持不变。
- 执行前相关菜单及角色菜单快照:`/tmp/shanxia-customer-menu-backup.jBxXRg/before.json`(本机临时文件)。H5/小程序发布和真机验收仍未执行。
## 原有问题
见 `2026-10-17-mp-crm-customer-existing-issues.md`:Web 客户跟进联系人解包错误、操作日志 HTML 注入风险、Web 商机跟进默认时间 UTC 偏移。均只记录,未修改。
@@ -0,0 +1,38 @@
# 客户管理移动迁移:原有问题记录
记录日期:2026-10-17。迁移阶段仅记录以下原有问题。后续用户明确授权修复 1、2、3,现已完成 Web 前端修复;未修改后端业务。
## 1. Web 客户跟进弹窗的联系人选项为空(已修复)
- 位置:`web-admin/src/views/crm/customer/FollowFormDialog.vue`,`loadContactList`。
- 证据:调用 `fetchContactByCustomerId` 后读取 `res.data || []`;该 API 直接使用统一 `request.post`,HTTP 包装层已经返回 `res.data.data`;后端 byCustomer 返回数组。因此正常数组响应会被错误替换为空数组。
- 复现:客户有联系人时,在 Web 客户详情点击添加跟进,展开关联联系人下拉。
- 影响:不能选择已存在的关联联系人。
- 修复:读取已解包数组,同时兼容旧 `{ data: [...] }` 响应;空响应或请求失败时清空列表。`customer/components/ContactList.vue` 使用的 API 同样返回已解包数组,已修复同类解析问题。
## 2. Web 客户操作日志详情直接插入 HTML(已修复)
- 位置:`web-admin/src/views/crm/customer/CustomerDetailDialog.vue`,`handleViewLogDetail`。
- 证据:模板字符串直接插入 `row.operParam` 等值,并启用 `dangerouslyUseHTMLString: true`,未见相应 HTML 转义。
- 风险:含 HTML 的业务参数可能被作为标签解释,存在注入风险。此结论来自代码检查,未向真实业务库写入攻击样本。
- 修复:弹窗改用 Vue VNode 与文本子节点,移除 `dangerouslyUseHTMLString`。模块、人员、时间、URL 和请求参数均不再按 HTML 解释;保留分段、参数换行和滚动展示。
## 3. Web 新增商机跟进的自动默认时间使用 UTC(已修复)
- 位置:`web-admin/src/views/crm/opportunity/components/FollowDialog.vue`,打开新增弹窗时的 `new Date().toISOString().slice(0, 19).replace('T', ' ')`。
- 证据:`toISOString()` 输出 UTC,截去时区标记后又作为本地日期显示/提交。在 Asia/Shanghai 中比本地时间早 8 小时。
- 复现:北京时间 10:00 打开新增商机跟进,检查默认跟进时间(该表达式得到 02:00)。
- 范围澄清:只针对“商机 → 添加跟进”的 `followTime` 自动默认值,不是客户跟进卡片顶部的 `createTime`,也不影响手动选择的“下次联系时间” `nextTime`。
- 修复:复用现有 `parseTime(new Date())` 生成本地时间。编辑已有记录的跟进时间与下次联系时间不变。
## 修复回归
- `pnpm exec tsx --test scripts/crm-existing-issues.test.ts`:6 项通过。修复前 4 项失败,覆盖两个联系人入口、失败清空、全部日志字段的 HTML 转义、本地时间跨日、编辑时间不覆盖。
- 测试直接执行组件中的真实处理函数;网络/弹窗仅在边界替换,不写入真实业务库。日志使用真实 Vue SSR 渲染验证,不执行恶意标签。
- 连同移动客户模块测试共 11 项通过,`pnpm build` 通过;全量 `vue-tsc` 与修复前 675 条错误基线对比无新增。已同步代码图谱,尚未发布生产。
## 验证环境的原有限制
- 全量 `vue-tsc --noEmit` 基线已有 675 条错误(输出 1514 行);本次新增客户模块的最终检查必须与基线比较,不能将全项目类型检查描述为通过。
- AGENTS 要求的 `scripts/check_field_consistency.py` 在仓库中不存在;使用可用 Python 执行得到文件不存在错误。此次未新增/修改后端 CRUD、Pydantic 或业务表字段,因此通过逐项核对既有 schema 与新移动 payload 补充检查,但不宣称该脚本通过。
- 本次浏览器回归使用隔离模拟 API,不写真实业务库。微信真机的 WebView 文件选择、预览/下载、业务域名、登录票据与真实角色权限仍需在发布环境验收。
@@ -0,0 +1,53 @@
# 小程序 CRM 六界面标准化交付核验
状态:六个工作台 CRM WebView 入口已按 `docs/miniprogram-ui-spec-sanqiang.md` §12、OpenSpec UI Conventions 和 `bak/mobile-ui/shanxia-mp-crm-pl-cert-style-align-demo.html` 完成标准化;危化品证上传同步完成同一轮校准。未部署、未写真实业务数据,微信真机验收仍需在测试环境执行。
## 交付入口
| 工作台入口 | H5 路径 | 交付结果 |
| --- | --- | --- |
| 客户管理 | `/m/crm/customer` | 四视图、搜索、状态 Tab、三栏筛选、More、expense-card、新增/批量 Speed Dial、七 Tab 详情、72×72 证件附件与完整客户业务功能 |
| 产品管理 | `/m/crm/product` | 全部/上架/下架、分类/型号/More、产品卡、新增/分类双 Speed Dial、独立详情和移动表单;分类全路径、上下架及创建者删除限制保留 |
| 报价单管理 | `/m/crm/quote` | 管理列表、金额卡、筛选、卡片动作、独立详情、版本/提交/作废等既有功能 |
| 报价单审批 | `/m/crm/quote/approval` | 当前用户待审/已审、最早步骤与前置驳回门禁、通过/驳回、时间轴与附件 |
| 合同管理 | `/m/crm/contract` | 管理列表、金额卡、单选/批量、提交审批、独立详情、移动附件宫格 |
| 合同审批 | `/m/crm/contract/approval` | 当前用户待审/已审、单条/批量审批及部分成功、审批人限制、时间轴与附件 |
小程序入口 code、H5 路由和六组权限映射已逐项核对;详情直达和刷新、列表返回、403 门禁与管理/审批入口差异均纳入浏览器回归。
## 视觉与交互核验
- 列表统一为“搜索 → 状态 Tab → 三栏筛选 → 卡片”,更多筛选按字段类型提供 Chip、模糊检索、日期和区间控件。
- 列表卡片使用备用金式六层 `expense-card`,无可用动作时不渲染空 footer;状态为浅底语义标签。
- 详情使用固定普通标题,不使用票根、Hero 或动态摘要;业务内容通过文字 Tab 展示。
- 附件使用发票夹式 72×72 宫格;预览、添加和删除触控区域至少 44×44,删除角标与预览区域不重叠。
- 375px 与 414px 视口均有横向溢出断言和截图;小程序品牌色使用 `#0a9f6d`,目标范围未混入 Web Fresh Mint 主色 `#10b981`。
共享实现位于 `web-admin/src/components/mp/` 与 `web-admin/src/styles/mp-crm-primitives.scss`;页面实现位于 `web-admin/src/views/m/crm/{customer,product,quote,contract}/`。产品列表只读补充了后端 `createBy` 返回字段,用于保持 Web 端既有创建者删除限制,未新增 CRM 写接口或改变租户/数据权限规则。
## 已执行验证
- CRM 纯逻辑与既有问题守卫:42/42 通过;共享移动原语契约:4/4 通过。
- DOM 契约:客户、产品、报价、合同与危化品证入口全部通过。
- 浏览器回归:客户 113 次 fixture API 调用通过;报价 85 次通过;合同 40 次通过;产品通过;危化品证 152 次通过。网络只在浏览器边界替换,不写真实业务库。
- 客户 403 场景额外断言状态 Tab、筛选、计数、Speed Dial、批量栏和弹层均不暴露;详情 403 隐藏正文动作。
- 产品后端字段/facet 测试:8 项通过。
- 目标 ESLint、Stylelint、Prettier 和 `git diff --check` 通过;生产构建通过。
- 全量 `vue-tsc --noEmit` 仍被仓库既有 673 条诊断阻断;过滤结果未发现本轮 CRM、危化品证、共享移动原语或产品 API/表单目标路径诊断,不能表述为全项目类型检查通过。
- frontend、miniprogram 与根目录知识图谱均已增量更新;节点数量超过 HTML 可视化上限,工具按设计更新 JSON/报告并跳过 HTML 重建。
浏览器截图:
- 客户:`/tmp/shanxia-customer-e2e/`
- 产品:`/tmp/shanxia-product-e2e/`
- 报价:`/tmp/shanxia-quote-e2e/`
- 合同:`/tmp/shanxia-contract-e2e/`
- 危化品证:`/tmp/shanxia-plan-cert-e2e/`
## 范围边界与后续验收
- 实施中发现的其他界面/基线问题只记录在 `2026-10-17-mp-crm-style-align-existing-issues.md`,未借机修改全量 SQL、旧迁移或旧人工回归文档。
- 本轮没有执行生产发布或真实数据库写入;菜单 SQL 与已有授权状态不在本次代码改造中重复操作。
- 发布前仍需微信真机核验:角色/租户数据范围、ticket 过期、返回栈、软键盘、弱网、文件选择及图片/PDF 预览。
- 浏览器脚本已完成全部业务断言;本机 Chrome 在个别长脚本关闭阶段偶发残留进程,拿到 PASS 输出后人工终止,不影响页面断言结果,但可作为后续测试基础设施清理项。
@@ -0,0 +1,52 @@
# 小程序 CRM 六界面统一样式设计
日期:2026-10-17
## 1. 范围
改造工作台「CRM 管理」六个入口及其直接详情页,不改变业务 API、权限、审批状态机或数据字段:
1. 客户管理 `/m/crm/customer`
2. 产品管理 `/m/crm/product`
3. 报价单管理 `/m/crm/quote`
4. 报价单审批 `/m/crm/quote/approval`
5. 合同管理 `/m/crm/contract`
6. 合同审批 `/m/crm/contract/approval`
新增/编辑表单与深层业务 Sheet 只处理由共用组件引起的必要适配,不做全面重设计。发现其他界面问题记录到问题文档,不顺手修改。
## 2. 视觉权威
以 OpenSpec「工作台 WebView 业务页」、`docs/miniprogram-ui-spec-sanqiang.md` §12 和 style-align Demo 为准,并应用用户最新覆盖要求:详情必须是独立路由页面;顶部只保留普通绿顶栏,不使用票根、Hero、动态记录标题、副标题或顶栏状态标签。编号、状态、金额等记录信息下沉到正文。
色板只使用三墙绿 `#0a9f6d` / `#087f57` / `#e9f9f2`,禁止把 Fresh Mint `#10b981` 用作移动主色。状态标签采用浅底语义色。正文14px,页面标题17px,辅助文字11–12px;可见按钮按标准尺寸展示,同时命中区域不少于44×44px。
## 3. 共用结构
### 3.1 列表
六页固定顺序为:标准绿顶栏与胶囊搜索 → 横滑状态 Tab → 固定三栏筛选(两个快捷下拉 + 更多)→ `expense-card` 列表 → Speed Dial 或选择后批量底栏。
`expense-card` 采用标题/浅底状态、可选金额区、副信息、灰底 KV、时间 meta、虚线操作栏;每卡至多一个实心主色按钮。客户、产品等无金额业务跳过金额区,但其他层级不省略。管理页新增使用 Speed Dial;审批页不显示新增。
「更多」Sheet 收纳全部筛选,至少支持 Chip、模糊检索、日期/日期区间、日期时间和数值区间;底栏重置/确定居中适宽,不粘连、不伸满。
### 3.2 详情
现有独立详情路由继续保留。页面只显示普通固定标题绿顶栏,后接业务 Tab、信息卡和底部操作栏。客户沿用业务 Tab;产品、报价、合同补齐与字段分组对应的 Tab。报价/合同审批流程使用时间轴;附件使用约72×72宫格。详情底栏实际触达不少于44px,主操作至多一个实心按钮。
### 3.3 共用组件边界
新增小而专一的展示组件:状态 Tab、三栏筛选、备用金式卡片、附件宫格、审批时间轴和普通详情头。Quote/Contract 继续复用各自现有共享列表组件;API、权限判断、审批提交、批量部分成功统计留在业务模块,避免形成巨型共用 composable。不得直接全局改旧 `.mp-wv-filter-bar` 的列数,以免影响非 CRM 页面。
## 4. 页面映射
- 报价管理/审批:同一 `MpQuoteList` 根据 mode 提供不同 Tab/快捷筛选和动作;金额使用独立大金额区。
- 合同管理/审批:同一 `MpContractList`,保留多选、批量通过/驳回和部分成功反馈;合同附件改为宫格。
- 产品管理:保留新增/分类双项 Speed Dial;状态 Tab 为全部/上架/下架,卡片无金额区。
- 客户管理:保留四种业务视图和现有完整筛选能力;移除两行工具条,将新增迁入 Speed Dial,将筛选接入三栏/更多;附件改为宫格。
## 5. 验证
先建立共用 DOM 契约和每组页面失败测试,再分组迁移。每组验证查询参数、权限/审批动作、分页、失败重试、独立详情直达/返回、375px/414px 无横向溢出、按钮命中和视觉截图。现有四组纯逻辑测试必须继续通过;客户 Playwright 测试同步新骨架。菜单 SQL 基线缺失和旧迁移幂等问题记录到问题文档,未经授权不写数据库。
@@ -0,0 +1,32 @@
# 小程序 CRM 六界面标准化:范围外问题记录
记录日期:2026-10-17。
本文件只记录实施过程中发现的既有问题。当前授权是改造工作台 CRM 六个 WebView 界面,不据此修改全量 SQL、旧迁移或其他界面功能。
## 1. 全量 SQL 基线缺少六个 CRM 工作台菜单
- 位置:`docs/sql/shanxia2.0.sql`。
- 依据:客户、产品、报价管理、报价审批、合同管理、合同审批菜单只存在于 `docs/sql/migrations/2026-10-15-mini-crm-*-webview-menus.sql` 与 `2026-10-17-mini-crm-customer-webview-menus.sql`,全量基线没有 `MINI_CRM_*` 记录。
- 影响:只导入全量 SQL 的新环境不会出现这六个工作台入口,还需额外执行增量脚本。
- 状态:仅记录;本轮不修改数据库基线或真实数据库。
## 2. 2026-10-15 CRM 菜单迁移对软删除记录不完全幂等
- 位置:产品、报价、合同三个 `2026-10-15-mini-crm-*-webview-menus.sql`。
- 依据:菜单插入使用固定主键,而 `NOT EXISTS` 只查询 `del_flag = 0`;若同主键菜单已经软删除,重跑脚本可能发生主键冲突。2026-10-17 的客户脚本已使用 `ON DUPLICATE KEY UPDATE del_flag = 0`,两批脚本行为不一致。
- 影响:曾软删除相关菜单的环境可能无法直接重跑旧迁移,祖先授权也可能无法恢复。
- 状态:仅记录;本轮不修改历史迁移。
## 3. 旧小程序 CRM 人工回归说明与当前入口状态不一致
- 位置:`bak/miniprogram-e2e/cases/01-crm.md`。
- 依据:文档仍把部分 CRM 菜单描述为灰卡或未开放,但当前 `miniprogram/config/workbench-menus.js` 已启用六个 WebView 入口并按权限过滤。
- 影响:人工验收可能误跳过已启用页面,或把正常入口误报为异常。
- 状态:仅记录;本轮不修改旧备份说明。
## 4. 微信真机行为尚未在本地浏览器回归中覆盖
- 范围:微信 WebView 返回行为、软键盘顶起、文件选择/预览、弱网、登录票据过期与真实角色数据范围。
- 依据:本轮自动化使用真实前端页面与本地 API 边界替身,不写真实业务库;Chromium 无法等价替代微信内核和原生桥。
- 状态:发布前验证项,不属于代码缺陷;需在测试环境由对应角色做真机验收。
@@ -0,0 +1,65 @@
# 危化品证上传移动 H5 交付核验
状态:用户第三次界面校准已在源码中完成并通过审查修复后的浏览器回归;当前总目标还包含 CRM 六页,图谱与全量门禁在总目标收尾统一执行。菜单 SQL 已按用户后续授权在测试库执行;未部署、未进行微信真机验收,生产库未执行。
## 范围
设计:`2026-10-17-mp-webview-pl-plan-cert-design.md`。
计划:`../plans/2026-10-17-mp-webview-pl-plan-cert.md`。
独立移动模块 `/m/pl/plan-cert`,小程序运输管理工作台 code `pl-plan-cert-webview`,入口权限 `mini:pl:plan-cert:list`。Web 页面与既有后端不修改;移除导出,保留已确认设计中的全部移动功能。
## 需求核验表
| 要求 | 当前证据 / 状态 |
| --- | --- |
| 复用 H5 WebView、运输工作台入口 | 入口配置及路由测试 3/3 通过;独立规格审查通过 |
| 五个文字 Tab 及默认查询 | model 与浏览器交互通过 |
| 筛选、搜索、日期、分页与完整卡片字段 | 搜索/日期/动态候选项上下文/文本、日期、数字、枚举、合同侧别、排序参数/加载更多失败重试/乱序请求通过;字段完整性由独立规格审查核对 |
| 独立详情、关联合同、运输复制与链接 | `/m/pl/plan-cert/:id` 直达/刷新/路由 ID 切换通过;普通固定绿顶栏、三 Tab、无票根/Hero;只读合同商机/报价关联及别名补全、金额独立权限、关联计划打开、司机/现场复制、磅单二维码预览均通过 |
| 产品分组证书上传、预览、下载、移除 | 3种证书槽位、最新详情、上传中保存/关闭拦截、失败保留重试、载荷白名单、预览/下载/移除及取消不写入通过 |
| 单条/批量齐备与取消、权限、状态限制 | model 及浏览器批量1成功1失败1跳过、单条取消、权限失败关闭写入口通过 |
| 375/414px 移动样式、附件卡片 | 顶栏、状态 Tab、三栏筛选和列表卡片横向铺满视口;列表/筛选/详情/上传截图及横向溢出断言通过;72×72 缩略图及编辑态下载/删除 44px 命中区互不重叠 |
| 不修改 Web 及共享后端行为 | 8 个关键文件 SHA256 与实施前全部一致 |
| 其他问题只记录 | 见 `2026-10-17-mp-pl-plan-cert-existing-issues.md`,未修改其中的既有问题 |
| 构建、目标 lint、类型检查、图谱更新 | 本阶段生产构建、目标 ESLint/Stylelint 通过;全量类型检查仍被仓库既有错误阻断且目标路径无诊断;图谱在 CRM 六页完成后统一更新 |
## 已执行的验证
- `node --test scripts/mp-pl-plan-cert-entry.test.mjs`:先3项失败(缺入口/路由/SQL),补齐后3项通过。
- `pnpm exec tsx --test src/views/m/pl/plan-cert/model.test.ts src/views/m/crm/customer/model.test.ts scripts/crm-existing-issues.test.ts`:19项通过,其中8项为本次危化证规则、11项为前序客户功能回归。补充的日期上下文、品牌别名及合同补全测试均先失败后通过。
- `node --check miniprogram/config/api.js`、`node --check miniprogram/config/workbench-menus.js`:通过。
- 开发阶段仅只读确认菜单链 `1 → MINI_PL_ROOT`,并检查 SQL 两段 SELECT 的执行计划;当时未执行写语句。后续获用户授权后的实际执行与提交验证见下方记录。
- `node scripts/test-mp-pl-plan-cert.mjs`:最终通过并自然退出,152次模拟API调用,无未知接口、无页面脚本异常。覆盖携带 `mpTicket` 直达详情和刷新、列表跳转、路由 ID 切换、详情内上传与齐备/取消齐备原位刷新;页面与共享组件使用真实代码,仅网络边界替身,不写真实数据库。
- 浏览器验证边界:筛选断言验证真实前端序列化及配对展示,替身没有重做后端全部列筛选算法;PDF校验请求、弹层和尺寸,不代表微信内核原生PDF渲染已验收;二维码图片使用本地替身,不访问真实二维码服务。
- 截图:`/tmp/shanxia-plan-cert-e2e/` 下 `list-375/414.png`、`filters-375/414.png`、`detail-375/414.png`、`upload-375/414.png`、`attachment-preview-375.png`、`feedback-preview-414.png`。已人工检查,预览和下载互不覆盖。
- `pnpm build`:本次独立详情阶段 fresh 通过(4943 modules,38.13秒);仍有既有组件重名和包体积警告。
- `pnpm exec eslint src/views/m/pl/plan-cert scripts/mp-pl-plan-cert-entry.test.mjs scripts/mp-pl-plan-cert-fixture.mjs scripts/test-mp-pl-plan-cert.mjs`、目标 Stylelint 与 Prettier:通过。
- `pnpm exec vue-tsc --noEmit`:仍被仓库大量既有错误阻断;过滤输出未发现本目标路径诊断。总目标收尾将重新生成统一基线证据。
- 图谱更新延后到 CRM 六页代码全部完成后统一执行,避免共享脏工作区并发覆盖。
## 变更隔离与审查
- 规格审查发现并修复本模块的快捷日期facet上下文、筛选选中文字遮挡、产品品牌别名、合同关联补全及反馈附件触达区域问题;复审通过。
- 本轮独立详情审查继续发现并修复 Teleport 弹层未命中详情页约束、编辑态附件操作命中区重叠、只读权限空底栏、旧票根/旧自定义顶部样式残留、顶部 Tab/筛选未 full-bleed、E2E 强制退出等问题;永久断言先复现后转绿。
- 最终复审继续校准附件:保持 72×72 发票夹式卡片,删除角标与下载入口各自保留 44×44 且互不重叠,不再出现可见操作底栏;权限失败与只读提示移至详情 Tab 后,且无写权限时不渲染底部操作区。
- 入口/菜单SQL独立规格和质量审查通过;菜单权限测试改为精确检查 Web `m.perms = 'pl:plan-cert:list'`,避免误匹配移动权限字符串。
- 以下8个文件SHA256与本次实施前一致:Web危化品列表及列筛定义、Web证书上传弹窗、共享危化品检查、后端计划路由及计划服务、既有移动运输详情、共享 `mp-webview.scss`。
- 继续使用用户确认的当前 `V1.5` 工作区,保留全部原有修改;未提交、推送、合并或清理用户文件。
## 数据库与发布
- 无业务表结构或模型迁移。
- 菜单脚本:`docs/sql/migrations/2026-10-17-mini-pl-plan-cert-webview-menus.sql`。
- 菜单 SQL 已在测试库执行;生产库未执行。角色仍需保留对应 Web 上传/齐备业务权限,本脚本不扩大这些写权限。
- 首次发布仍需要部署 H5、发布带新入口的小程序包。生产库 SQL 需另行授权,执行前核对父菜单并备份菜单及授权表。
- 尚未进行微信真机验证、部署或发布;模拟浏览器验证不能替代这些步骤。
### 测试库执行记录(2026-10-17,用户后续授权)
- 目标:`backend/.env` 对应的 `47.99.164.66:3306 / shanxia2.0`,MySQL 5.7.36;未连接生产库写入。
- 执行前确认移动父链 `1 → MINI_PL_ROOT` 唯一、目标菜单尚不存在、两张目标表均为 InnoDB。
- 执行前完整备份 `sys_menu` 255行、`sys_role_menu` 1375行至本机临时文件 `/var/folders/v5/ylmpvkjd4zxcf20pzbys3stc0000gn/T/shanxia-plan-cert-menu-backup-zu0_vlar/before.json`,目录权限700、文件权限600。备份不包含数据库密码。
- 事务新增1个“危化品证上传”移动菜单(ID `329823eb-e3af-49b2-b656-08f2c2a713f3`,权限 `mini:pl:plan-cert:list`),新增5条角色菜单绑定:管理员、销售主管、销售计划员、销售实录员、销售办证员。父级授权原已完整,无需新增。
- 提交前核对所有已有菜单行未改、授权变化仅为上述5条移动叶子绑定;无SQL警告,未修改业务表或Web权限。
- 事务提交后通过新数据库连接复核:菜单启用且可见、5角色各有唯一有效叶子绑定、移动根和运输父级授权链完整,验证通过。
@@ -0,0 +1,70 @@
# 危化品证上传迁移期间的既有问题记录
原迁移范围:只记录迁移范围外的问题,不修改 Web 或其他既有页面。2026-10-17 用户追加授权修复问题 1、4,问题 2、3 按要求忽略,保留记录但不修改对应功能。
## 1. Web 上传证书时,保存未等待附件上传结束(已修复)
- 位置:`web-admin/src/components/business/pl-plan-cert-upload-dialog/index.vue` 的 `handleSubmit` 和保存按钮。
- 原因:原保存按钮仅按 `hazardProducts.length` 禁用;提交函数直接读取三个证书 URL。`AttachmentUploadField` 在上传完成后才同步 URL,虽暴露 `hasUploading()`,但原弹窗没有引用或检查它。
- 可能影响:慢网下选中文件后立即保存,可能提交旧 URL 并关闭弹窗,刚选文件未绑定到计划。
- 建议复现:浏览器网络限速,选择一个证书文件,在上传返回前点击保存,对比 PATCH 请求载荷与上传返回后的 URL。
- 修复:收集所有证书附件组件引用,提交前检查 `hasUploading()`;上传中提示“附件正在上传,请稍候再保存”且不提交、不关闭。保存请求期间禁用附件编辑,加载中及重复提交也被拦截。
- 验证:真实 Web 弹窗和附件组件的 Playwright 测试(API 边界替身)先复现旧代码上传中仍发送 1 次 PATCH,再确认修复后发送 0 次;上传完成后保存包含新 URL,关闭销毁后重开也能正常保存。未写入真实业务数据。
- 状态:已按追加授权修复。测试脚本:`web-admin/scripts/test-pl-plan-cert-upload-dialog.mjs`。
## 2. H5 模块接入文档引用的运输计划菜单 SQL 不存在
- 位置:`docs/web-admin-mp-webview-modules.md` 的双权限说明引用 `docs/sql/migrations/2026-09-14-mini-pl-plan-webview-menus.sql`。
- 依据:当前工作区读取该路径返回不存在,`docs/sql/` 文件检索也未找到同名脚本。
- 影响:按旧文档发布运输计划时无法直接定位该菜单初始化脚本;不代表数据库现有菜单缺失。
- 状态:按用户要求忽略,本次未修改旧文档该引用。本模块已有独立菜单脚本,不代表旧运输计划文档问题已修复。
## 3. 证书写接口未接入对应按钮权限校验(代码审查发现,未越权实测)
- 位置:`backend/app/api/pl/plan.py` 的 `patch_plan_product_certs`、`patch_plan_hazard_cert_ready`;权限工具在 `backend/app/services/pl_plan_cert_perm.py`。
- 依据:两个接口检查登录、租户、计划数据范围后调用业务服务,路由聚合未配置相应菜单权限依赖;`user_can_upload_plan_cert`、`user_can_manage_plan_cert_ready` 在当前后端仅有定义、没有调用。Web 页显示按钮的检查不能代替服务端权限检查。
- 可能影响:有计划数据访问范围但没有对应按钮权限的登录用户,可能仍可直接调用证书写接口。
- 状态:按用户要求忽略,本次未修改权限校验;未使用真实账号实施越权请求。新移动页保留独立的按钮及提交前权限检查,但不能声称已修复服务端权限问题。
## 4. 证书附件保存服务未拒绝已取消计划(已修复)
- 位置:`backend/app/services/pl_plan_service.py` 的 `update_plan_product_certs`。
- 原因:原函数拒绝已归档计划,但没有与 `set_hazard_cert_ready` 一样检查 `follow_status == 'cancelled'`;前端 Web 的 `canUploadCert` 则禁止已取消计划操作。
- 可能影响:页面打开后计划被另一用户取消,仍可能保存证书附件,前后端状态限制不一致。
- 修复:读取计划后、查询及修改产品证书前,检查已作废状态并返回 HTTP 400:“已作废的计划不能修改危化品证书”。保留原租户、软删除和归档检查;不改变按钮权限。
- 验证:服务回归测试先确认旧代码对已作废计划不抛错,修复后确认拒绝且不修改 URL、修改人、齐备状态,不提交事务;另覆盖已归档、不存在及正常计划保存。测试使用数据库边界替身,不写入真实业务库;不声明已解决数据库事务之间的并发锁定问题。
- 状态:已按追加授权修复。测试文件:`backend/tests/test_pl_plan_product_certs.py`。
## 5. CRM 六个移动菜单未回灌全量 SQL 基线(仅记录)
- 位置:`docs/sql/shanxia2.0.sql` 与 `docs/sql/migrations/2026-10-15-mini-crm-*-webview-menus.sql`、`2026-10-17-mini-crm-customer-webview-menus.sql`。
- 依据:当前全量基线未包含 `MINI_CRM_*` 菜单;客户、产品、报价、报价审批、合同、合同审批只存在于增量迁移。
- 可能影响:仅使用全量基线初始化的新环境不会出现六个工作台入口,必须额外执行对应迁移。
- 状态:本轮先记录。用户当前授权是界面标准化,不据此写数据库;在 CRM 六页收尾时再评估是否同步基线 SQL。
## 6. 旧 CRM 菜单迁移对软删除记录的幂等性不足(仅记录)
- 位置:2026-10-15 的产品、报价和合同工作台菜单迁移。
- 依据:脚本的 `NOT EXISTS` 仅检查 `del_flag=0`,但插入使用固定主键;若同主键记录已软删除,重新执行会触发主键冲突。客户迁移已使用 `ON DUPLICATE KEY UPDATE del_flag=0`,行为更稳健。
- 可能影响:环境曾软删除菜单后重跑迁移可能失败,祖先角色绑定也可能无法恢复。
- 状态:本轮先记录,不修改数据库迁移或真实数据。
## 7. 旧小程序 CRM 回归说明仍标记已启用入口为“未开放”(仅记录)
- 位置:`bak/miniprogram-e2e/cases/01-crm.md`。
- 依据:文档仍把产品管理、合同审批描述为灰卡/未开放;当前 `workbench-menus.js` 中六个 CRM WebView 入口均已启用并受权限过滤。
- 可能影响:人工验收人员可能按过期说明跳过页面或误报入口状态。
- 状态:本轮只记录;CRM 六页交付时统一修订回归说明。
## 8. Demo 原“证书上传时间”没有可对应的 Web/API 筛选字段(已校准文档)
- 位置:`bak/mobile-ui/shanxia-mp-crm-pl-cert-style-align-demo.html` 的危化品证「更多」示意字段。
- 依据:Web 端 `columnFilterDefs.ts` 与计划列表 API 只有计划日期、送货时间、证书齐备状态等已生效条件,没有独立的证书上传时刻字段。
- 处理:按用户“功能完全参考 Web 端”的要求,删除 Demo 中无法生效的示意字段,并在 OpenSpec/UI 规范中明确单页不得为凑控件类型伪造查询条件;未改动后端或其他页面功能。
## 本次验证
- 后端:`venv/bin/python -m pytest tests/test_pl_plan_product_certs.py tests/test_pl_plan_cert_list_filters.py tests/test_pl_menu_badge_plan_cert.py tests/test_pl_plan_product_upsert.py tests/test_pl_plan_draft_save.py -q`(在 `backend/`):17 项通过。
- Web:`node scripts/test-pl-plan-cert-upload-dialog.mjs`(在 `web-admin/`,本地 Vite 默认端口 3006):通过。
- 构建:`pnpm build` 通过;保留原有组件重名和包体积警告。`pnpm exec vue-tsc --noEmit` 仍未通过,其输出与修复前基线完全一致,无本次新增类型错误。
- 本次没有新增字段、数据库结构或菜单权限变更,无需执行 SQL。
@@ -0,0 +1,35 @@
# 小程序 H5 客户管理迁移需求
日期:2026-10-17。用户已授权整理需求后直接开发。
## 目标与边界
以当前 Web「CRM管理 / 客户管理」为业务基准,在小程序中通过已有 WebView 加载独立移动 H5 页面。仅排除导出。Web 页面、共享业务 API、其他模块的行为保持不变;已有未提交改动保留。其他页面发现的问题只记录在本次问题文档。
## 功能清单
- 全部客户、我的客户、待跟进、重点客户四个视图;关键词搜索、字段筛选、排序、分页刷新。
- 客户新增、编辑、详情、单条/批量删除、单条/批量放入公海。保留创建人删除限制与操作确认。
- 全量客户可写字段、名称查重、拼音首字母重新生成、负责人默认当前用户。
- 营业执照(5)、经营许可证(5)、其他证件(10):图片/PDF,即时上传、预览、下载、删除、上传期间禁止提交。
- 跟进记录查看、新增、删除,联系人选择、跟进方式、内容、下次联系时间、附件(15)、备注。
- 联系人查看、新增、编辑、删除、设置主要联系人;校验与 Web 相同。
- 关联商机、合同列表及查看;合同使用已有移动详情;商机在本模块内提供移动详情,并保留详情中的修改、推进阶段、删除及跟进维护入口,不新增独立商机菜单。
- 开票资料新增、编辑、删除、默认设置、粘贴识别;新增客户可同时填写第一条开票资料。
- 操作日志分页及详情。
- Web 源码中未暴露的转移/成交处理函数不作为新增入口;以当前实际界面可操作功能为准。
- 桌面列显隐改为卡片字段显隐;列头筛选改为移动筛选面板,保留查询含义。
## 方案
沿用现有 `MpWebviewShell`、一次性 ticket、`mp-webview.scss` 与工作台路由映射。新增 `/m/crm/customer`、`/new`、`/:id`、`/:id/edit`。不新增第二套登录及 WebView 壳。
采用独立移动组件复用现有 API 与附件组件。相比修改 Web 客户弹窗增加移动分支,这样能满足本次 Web 功能完全不改的边界;相比独立 H5 工程,继续复用现有构建、鉴权和附件基础设施。
视觉采用三墙绿、手机卡片、分区表单、横向标签、底部动作栏及 Bottom Sheet;参考 `views/m/crm/product/index.vue`、`views/m/crm/contract/detail.vue` 和现有附件卡片。模式 P08(视图列表)、P12(分区表单)、P25(独立详情),移动样式以现有 H5 为准。
菜单使用 `crm-customer-webview` / `mini:crm:customer:list`,SQL 补齐移动父菜单与已有 Web 客户角色绑定,业务继续使用原接口权限。
## 验收
检查字段及上述功能逐项覆盖;单测验证 payload、列表参数、响应归一化及部分失败重试;手机宽度浏览器交互回归;构建与类型检查(已有问题分别记录);核对 Web 源码未被本次修改;执行字段一致性检查和图谱更新。数据库及真机环境无法直接验证的部分如实记录,不宣称已发布。
@@ -0,0 +1,130 @@
# 小程序危化品证上传 H5 迁移设计
日期:2026-10-17
状态:2026-10-17 按用户最新反馈进行第三次校准;详情改为独立页面并删除票根/顶部特殊区。此前“详情面板 + 票根”的验证结论已失效,完成证据以本轮更新后的 `2026-10-17-mp-pl-plan-cert-delivery.md` 为准。
## 1. 目标与边界
在小程序「运输管理」下新增「危化品证上传」,通过已有 WebView 容器访问独立移动端 H5。迁移 Web 同名功能,唯一明确删除的业务功能是导出;桌面表格、列头筛选及弹窗按移动端交互转换。
- 不修改 Web 页面、Web 上传弹窗、共享业务规则和既有后端行为。
- 不修改其他页面发现的问题,只在独立问题文档记录与交付时报告。
- 不增加业务表或字段。新增移动菜单及其角色祖先绑定 SQL 放在 `docs/sql/`,不因前一项客户菜单的授权而自动执行本次 SQL。
- 保留现有工作区的未提交改动;开始编码前确定工作区选择,不能用重置或覆盖方式清理。
## 2. 已核对的功能来源
| 项目 | 依据 |
| --- | --- |
| 列表、五个 Tab、批量操作、权限、详情入口 | `web-admin/src/views/pl/plan-cert/index.vue` |
| 全部列头筛选字段 | `web-admin/src/views/pl/plan-cert/columnFilterDefs.ts` |
| 证书上传及保存载荷 | `web-admin/src/components/business/pl-plan-cert-upload-dialog/index.vue` |
| 危化属性、证书槽位、齐备确认提示 | `web-admin/src/composables/usePlToxicProductCertCheck.ts` |
| Tab 参数和状态枚举 | `web-admin/src/utils/pl-display.ts` |
| 列表、详情、证书 PATCH 接口 | `web-admin/src/api/pl/index.ts` |
| 移动布局、运输卡片参考 | `web-admin/src/views/m/pl/plan/index.vue`、`web-admin/src/views/m/pl/plan/detail.vue` |
| 最近迁移经验、附件引用及上传中检查 | `web-admin/src/views/m/crm/customer/` |
| 入口鉴权与 WebView 接入约定 | `docs/web-admin-mp-webview-modules.md` |
模式选型:工作台 WebView style-align 列表(标准绿顶栏 + 状态 Tab + 三栏筛选 + expense-card)+独立路由 Tab 详情+发票夹附件。详情仅保留普通绿顶栏,不使用票根、Hero 或记录摘要等顶部特殊区。移动视觉以 `docs/miniprogram-ui-spec-sanqiang.md` §12、OpenSpec「工作台 WebView 业务页」和 `bak/mobile-ui/shanxia-mp-crm-pl-cert-style-align-demo.html` 为唯一权威,不照搬桌面宽表,也不沿用旧 `cert-*` 密集工具条。
## 3. 方案选择
推荐列表 `/m/pl/plan-cert` + 独立详情 `/m/pl/plan-cert/:id`,复用 API、纯业务规则及现有移动基础组件。列表只能通过路由进入详情;详情必须支持直达、刷新、浏览器/小程序返回与路由参数切换。
另两种方案不采用:直接嵌入 Web 页会保留宽表和桌面弹窗;修改共享 Web 页面适配移动会扩大回归范围并违反不修改 Web 的要求。
## 4. 页面和交互
### 4.1 列表与 Tab
顶部复用 `mp-wv-header`:返回工作台、17px 固定标题,以及标题下方的五个胶囊状态 Tab(仍在绿顶栏内);不显示副标题、独立“搜索”按钮或检索框。Tab 顺序:
1. 未上传证书(默认)。
2. 全部。
3. 入库单。
4. 出库单。
5. 直销单。
Tab 不能改成下拉选择。窄屏允许 Tab 区域横滑;绿顶栏内选中态为白底绿字,未选为半透明白边白字;页面整体不能横向溢出。点击后重置页码与选中项,保留无冲突筛选;过期请求不能覆盖新 Tab 的结果。绿顶栏下方为单个胶囊检索框(提交键触发搜索),再紧贴三栏筛选。
查询语义必须与当前 Web 一致:
- 所有 Tab 均附带 `hasHazardProduct=1`。
- 未上传证书:`certUploadStatus=incomplete` 且 `hazardCertReady=0`,不是简单地检查是否存在任意附件。
- 全部:不增加单据类型和证书状态限定。
- 其他三个 Tab 分别使用 `planType=inbound/outbound/direct`。
- 默认跟进状态集合不含 `cancelled`,允许用户在筛选中明确选择已取消。
- Tab 优先于冲突的单据类型、齐备状态筛选;保持后端的默认排序。
列表使用备用金式 `expense-card` 和可展开段次/产品信息。卡片与左右边界保留约 12px 间距;「共 N 条计划 · 已加载 M 条」汇总放在卡片列表之后。卡片须具备 `ec-top`、副信息、灰底 KV、时间 meta、虚线操作底栏;勾选框在标题左侧,浅底语义状态标签在右侧,每卡最多一个实心主色按钮(上传证书)。列表展开按段次分区:装货段次(1 段)与送货段次(可多段),各段展示客户、时间、地址、合同与产品摘要;不展示计划附件(附件改在详情查看)。合同可进入移动只读详情。
### 4.2 搜索、筛选与加载
列表自上而下固定为「绿顶栏(标题 + 状态 Tab)→ 检索框 → 三栏筛选 → 卡片 → 条数汇总」,不得在 Tab 下方堆叠日期 Chip 和“筛选/刷新/重置”工具条。三栏固定为:
1. 日期:全部日期、今天、明天、昨天、本周,可直接下拉选择。
2. 齐备状态:全部、未齐备、已齐备,可直接下拉选择;与默认“未上传证书”Tab 冲突时 Tab 语义优先。
3. 更多:打开底部筛选 Sheet;存在额外条件时显示红点。
“更多”集中承载 Web 列头筛选,并同时提供 Chip、模糊检索、日期/日期时间、日期区间和数值区间;底栏仅“重置/确定”。保留关键词查询、自定义日期、筛选重置和分页加载;刷新通过重新提交搜索或重试错误完成,不保留独立密集工具条:
- 计划编号、计划日期、送货时间、单据类型、归属公司、跟进状态、证书齐备。
- 车牌、人员、跟进人、装货客户、送货客户、装送地址/时间。
- 合同、产品名称、规格、单位、计划量、实际量。
- 保留对应枚举多选、动态可选值、日期范围、数值比较及可用排序能力。动态可选值请求保留当前查询上下文。
- 装送客户合并查询、合同侧别和配对行过滤遵循 Web 工具函数,不能仅查询到计划后无差别展示不相关的匹配段次。
空结果、加载中、加载失败、无权限状态分别提示;加载更多失败不跳过页码;重复点击操作期间禁止再次提交。
### 4.3 计划详情
采用独立页面 `/m/pl/plan-cert/:id`,由页面自己从 `route.params.id` 读取计划、权限和关联数据。顶部只使用普通 `mp-wv-header`(返回 + 固定标题“危化品证详情”),不显示票根、Hero、计划号副标题或状态等顶部特殊区;计划号、状态和齐备信息下沉到正文信息卡。顶栏后直接展示“基础信息 / 运输与段次 / 证书与附件”Tab,每个 Tab 下使用字段上标签下值的信息卡。详情范围对齐 Web `PlPlanViewDialog`,包括完整装送段次、产品/数量、运输、证书、附件、运费、备注及其原有可查看关联信息。
详情必须支持链接直达和刷新;非法或无权 ID 展示独立页面错误态;关联计划跳转更新当前路由参数并重新加载。上传、齐备、取消齐备成功后原位刷新详情,返回列表后列表重新请求,不能依赖列表父组件的 ref 或弹层开关。
复用已有移动展示工具,但不能直接导航到具有新增计划、复制计划、作废、安排运输等额外业务操作的通用运输详情而改变本菜单的操作边界。详情主要操作提供本菜单有权限且当前状态允许的上传、齐备和取消齐备。
进一步核对 `PlPlanViewDialog` 后确认:Web 本菜单的详情还会根据 `usePlPlanArrangeActions` 的结果提供「复制给司机」「复制给现场」「复制磅单二维码」及司机/现场链接。这些属于现有详情功能,移动迁移必须保留并沿用相同显示条件,不能误当成导出删除;可放入详情“更多”操作区。列表与详情操作成功后均重新读取服务器状态。
### 4.4 上传证书
- 打开时重新获取计划详情,按现有逻辑读取 CRM 产品危化属性。
- 易制毒展示购买证、运输证;易制爆展示合法证明;按共享 `getRequiredCertSlots` 规则处理已有数据,不自行覆盖历史属性。
- 每个产品一张分组卡,字段标题在上,附件宫格在下。
- 继续使用已有 `AttachmentUploadField` 的上传、预览、下载与删除能力,但在本模块内将触发项和文件项覆盖为发票夹式 72×72 宫格:图片缩略填充,非图片显示扩展名与文件名,删除为右上角角标,末位虚线“+”添加;不得保留管理端 140×96 主形态。
- 支持预览、下载、移除,与既有移动附件交互一致;只读状态不显示编辑入口。
- 文件选中即上传到既有附件服务;业务保存仅发送产品 ID 和三个逗号分隔 URL 字段,不发送 Base64、不修改计划其他字段。
- 有上传进行中时阻止保存。保存失败保留已填写内容,可重试;取消不提交业务记录。
- 沿用 Web 允许稍后补附件的规则,不把所有槽位非空作为保存或人工齐备的强制条件。
### 4.5 齐备操作与权限
- 单条及多选批量「证书齐备」「取消齐备」均保留;批量确认说明实际可操作条数,跳过不符合状态的项目,并报告成功、失败及跳过数量。
- 已归档(`archiveStatus=archived`)或已取消(`followStatus=cancelled`)不能上传或改齐备。
- 只有含危化产品的计划可操作齐备;已齐备只允许取消,未齐备只允许标记。
- 使用既有 `pl:plan-cert:upload`、`pl:plan-cert:ready` 权限,不因有移动入口就赋予写权限。权限加载失败按无权限处理。
- 人工齐备需二次确认,提示易制毒/易制爆的证书要求及之后仍可补附件;取消齐备提示其对运输安排的影响。
- 调用现有 `PATCH /transport/plan/{id}/product-certs`、`PATCH /transport/plan/{id}/hazard-cert-ready`,不增加替代接口或绕过已有租户/数据范围保护。
## 5. 入口和样式
- 工作台「运输管理」新增 code `pl-plan-cert-webview`。
- 路径映射到已有 `mpWebview` 容器,目标 `/m/pl/plan-cert`;沿用票据换取登录、返回工作台及安全区处理。
- 新移动权限建议 `mini:pl:plan-cert:list`;迁移 SQL 通过 Web `pl:plan-cert:list` 匹配既有角色,补齐移动端根及运输管理父级绑定,不改 Web 菜单、不自动扩大上传或齐备权限。
- 当前 SQL 目录没有可直接复用的运输计划移动菜单脚本。新增脚本前须只读核对已有运输管理父菜单 ID、路径和租户归属,不猜测父级 ID,也不新增第二个同名运输根菜单。
- 使用 `MpWebviewShell`、`MpBottomSheet` 和本模块样式。结构 class 对齐 Demo 的 `mp-wv-header`、`status-tabs/filter-tag`、`filter-bar/filter-trig`、`expense-card`、`detail-tabs`、`att-grid/att-img` 语义;详情禁止 `ticket-*`。品牌绿 `#0a9f6d`,不得出现 Fresh Mint `#10b981`,375/414px 验证,交互触达至少 44px,状态使用浅底语义标签,弹层无背景模糊。
- 本模块筛选和上传 Sheet 底栏操作水平居中、按钮间距 12px;按钮宽度约 96px、不伸满、不粘连,视觉字号 14px,实际触达高度不少于 44px。该规则只作用于本模块,不能修改共享 Sheet 导致其他界面变化。
- 首次新增本地工作台入口需要发布含入口配置的小程序包及 H5,并执行本模块菜单 SQL;不能误用“以后只更新 H5 无需提审”的说明代替首次发布步骤。
## 6. 验证和交付
- 先写失败测试覆盖五个 Tab 查询、默认状态、冲突筛选、操作限制、批量目标、证书载荷及上传中保护,再实现。
- 浏览器模拟 API 验证列表、搜索、筛选、全部 Tab、分页、详情、上传增删预览、保存、单条/批量齐备与取消、无权限和接口失败。模拟测试不向真实业务数据库写入。
- 375px 和 414px 截图检查列表、详情、附件和底部操作栏,不以编译通过替代界面验收。
- 运行新增测试、目标文件 lint、构建及类型检查;把已有类型错误与新增错误区分。核对 Web 原文件内容没有因本次迁移变化。
- 更新模块接入文档、SQL、交付和问题记录,按仓库要求更新相关知识图谱。真机和部署未实际执行时必须明确标注,不能宣称通过。
## 7. 当前状态
用户已明确给出第三次校准要求:独立详情、删除详情顶部特殊区、列表横向无额外间距、统一按钮/字号、Sheet 底栏按钮居中适宽、列表顶部对齐 OpenSpec。当前分支为 `V1.5`,在现有工作区实施并保留前序任务及用户的未提交改动。数据库写入与发布不属于此次开发默认授权,另行确认。
@@ -0,0 +1,184 @@
# 客户详情 Tab:商机 / 合同 / 开票 / 操作日志 UI 对齐
日期:2026-10-18
状态:已确认(方案 A)
范围:`web-admin` 小程序 WebView 客户详情 `/m/crm/customer/:id` 内四个业务 Tab
## 1. 目标与边界
### 1.1 目标
将客户详情中的 **商机、合同、开票信息、操作日志** 四个 Tab,在列表交互上对齐已定稿的 **联系人** 范式;在详情展示上对齐 **资料** Tab 的只读字段呈现。业务能力与现有 API 契约不变。
### 1.2 范式权威
| 维度 | 权威参考 | 路径 |
|------|----------|------|
| 列表骨架 | 联系人 `ContactPanel` | `web-admin/src/views/m/crm/customer/ContactPanel.vue` |
| 卡片 / 展开 | `mp-customer-detail-card` | `customer.scss` + 跟进 `FollowPanel` |
| 详情字段 | 资料 Tab | `detail.vue`:`section.mp-wv-section` + `h3` + `CustomerFields`(透明底只读) |
| 视觉 Token | 三墙绿 | `#0a9f6d` / `#087f57` / `#e9f9f2`;禁止 Fresh Mint `#10b981` |
### 1.3 不做
- 不改后端 API、权限、分页语义(商机/合同/日志仍服务端分页 + 客户端关键词/筛选;开票仍按客户一次拉全量)。
- 不新建商机/合同独立菜单或新路由;合同模块既有 `/m/crm/contract/:id` **保留**,但本客户详情 Tab 内「查看」**不再跳转该路由**(见 §4.2)。
- 不改「资料 / 跟进 / 联系人」三个 Tab 的既有行为(除本会话已修的 sticky Tab 顶栏遮挡)。
- 详情页底栏 FAB(编辑 / 公海 / 删除)仍仅在 **资料** Tab 显示。
- 不顺手改 PC 端 `crm/customer` 弹窗。
## 2. 现状与差距
| Tab | 现状组件 | 现状形态 | 目标差距 |
|-----|----------|----------|----------|
| 商机 | `RelatedPanel` `kind=opportunity` | `van-field` 查找 + `LocalFilters` + `van-cell-group` inset;点行进底部弹层 | 换联系人式检索/三栏/扁平卡/收叠;弹层皮肤改资料风 |
| 合同 | `RelatedPanel` `kind=contract` | 同上列表;**点行 `router.push` 合同详情页** | 同列表范式;查看改为资料风底部弹层,不跳转 |
| 开票 | `InvoicePanel` | `van-field` + `LocalFilters` + inset 卡 + 行内全字段;增删改弹层 | 换联系人式列表/筛选;展开后操作;表单弹层 `teleport` |
| 日志 | `RelatedPanel` `kind=log` | 同商机列表 + 弹层字段/`pre` | 同列表范式;弹层资料风 + 请求参数区 |
共用依赖(保留逻辑、可换皮):
- API:`listOpportunity` / `getOpportunity` / `listFollowByOpportunity` / `getStageLogList`;`listContract`;`list`(operlog);`getInvoiceInfoByCustomerId` / `add|update|delInvoiceInfo`
- 筛选定义:`dialogColumnFilterDefs.ts` 中 `buildDetail*ColumnFilterDefs` / `buildInvoiceInfoColumnFilterDefs`(「更多」Sheet 字段来源)
- 商机动作:`OpportunityActions.vue`(修改 / 推进阶段 / 跟进 / 删除)
## 3. 共用列表骨架(对齐联系人)
四个 Tab 统一结构(自上而下):
1. **顶栏操作**:`刷新`;开票额外主色「+ 新增开票信息」;商机/合同/日志可保留「共 N 条」弱文案(可选)。
2. **胶囊检索**:`input.mp-wv-search`(占位见 §4),非带 label 的 `van-field`。
3. **三栏筛选**:`van-dropdown-menu` + 两个 `van-dropdown-item` 快捷枚举 + 「更多」打开底部 Sheet;`teleport="body"`、`z-index ≥ 3000`,避免被 sticky Tab / 卡片遮挡。
4. **扁平白底卡**:`article.mp-wv-card.mp-customer-detail-card`
- 头:标题 + 一行 meta 摘要 + 右侧 `status-tag`(语义浅底)
- 中:「展开 / 收起」,**默认收叠**
- 展开展:全部列表字段行(`mp-customer-detail-card__row`)+ 操作区
5. **去掉**:`LocalFilters` 折叠块;`van-cell-group.inset.mp-customer-panel-card` 列表堆叠;列表态整页 `CustomerFields` / `InvoiceFields` 展开。
分页:商机/合同/日志保留「加载更多」;筛选与关键词作用在**已加载**集合上(与现状一致)。开票无分页。
样式优先复用现有 `.mp-customer-detail-card` / `.mp-customer-contact-panel` 节奏;可抽轻量 panel 根类(如 `.mp-customer-related-panel`)避免复制粘贴 CSS,但**不强制**新建巨型共用组件——允许四面板各自落地、结构对齐即可。
## 4. 分 Tab 规格
### 4.1 商机
| 项 | 规格 |
|----|------|
| 快捷1 | 商机阶段 `crm_opportunity_stage`(含「全部」) |
| 快捷2 | 商机状态 `crm_opportunity_status`(含「全部」) |
| 更多 | 沿用 `buildDetailOpportunityColumnFilterDefs`:名称、负责人、预计成交日、创建时间等 |
| 检索占位 | `商机名称 / 编号 / 负责人` |
| 卡标题 | `opportunityName` |
| Tag | 阶段(或状态,二选一主 Tag;建议阶段) |
| 摘要 | `金额 · 负责人`(缺省用 —) |
| 展开字段 | 现有 `opportunityFields`(编号、客户、联系人、金额、日期、负责人、进度、赢率、阶段、状态、类型、来源) |
| 展开操作 | 「查看」→ 详情弹层 |
**详情弹层(82%,teleport body)**
- 结构对齐资料:`section` + `h3`(商机名称)+ 透明底 `CustomerFields`(`detailFields`,只读)
- 保留 `OpportunityActions`(修改 / 推进 / 新增跟进 / 删除)
- 「跟进记录」「阶段记录」分组:子项用扁平 `mp-customer-detail-card`,默认可收叠;跟进编辑/删除能力与现状一致
- 删除/变更后刷新列表并视情况关闭弹层(现状行为保留)
### 4.2 合同
| 项 | 规格 |
|----|------|
| 快捷1 | 合同类型 `crm_contract_type` |
| 快捷2 | 合同状态 `crm_contract_status` |
| 更多 | `buildDetailContractColumnFilterDefs`:编号、名称、负责人、签订日、金额等 |
| 检索占位 | `合同名称 / 编号 / 负责人` |
| 卡标题 | `contractName` 或回退 `contractNo` |
| Tag | 状态 |
| 摘要 | `金额 · 签订日` |
| 展开字段 | 现有列表字段(编号、金额、签订日、负责人、类型、状态) |
| 展开操作 | 「查看」→ 详情弹层 |
**详情弹层**
- 资料风:`h3` + `CustomerFields` 只读(以列表行字段为准;若后续有按 id 拉合同详情 API 再加深,本次不强制新接口)
- **行为变更**:取消本 Tab 内 `router.push('/m/crm/contract/:id')`;完整合同模块仍从合同菜单进入独立详情页
### 4.3 开票信息
| 项 | 规格 |
|----|------|
| 快捷1 | 是否默认:全部 / 默认 / 非默认 |
| 快捷2 | 有税号:全部 / 有税号 / 无税号(客户端根据 `taxNo` 是否非空) |
| 更多 | `buildInvoiceInfoColumnFilterDefs` 其余文本字段(单位名、税号、地址、法人、银行等) |
| 检索占位 | `单位名称 / 税号 / 银行` |
| 卡标题 | `companyName` |
| Tag | 默认 / 非默认 |
| 摘要 | `税号 · 开户银行` |
| 展开字段 | 现有 `invoiceFields` / `InvoiceFields` 只读等价行 |
| 展开操作 | 设为默认(非默认时)/ 编辑 / 删除(创建人删除限制不变) |
**无独立「查看」弹层**;新增/编辑仍底部表单弹层(`van-popup` + `van-nav-bar` + `InvoiceFields` + 是否默认),须 `teleport="body"`,避免 sticky Tab 遮挡。粘贴识别布局保持已修样式。
### 4.4 操作日志
| 项 | 规格 |
|----|------|
| 快捷1 | 操作类型 `sys_oper_type` |
| 快捷2 | 成功 / 失败(`status` 0/1) |
| 更多 | `buildDetailOperLogColumnFilterDefs`:时间、模块、人员、IP、耗时等 |
| 检索占位 | `模块 / 人员 / IP` |
| 卡标题 | `title`(操作模块) |
| Tag | 成功 / 失败 |
| 摘要 | `人员 · 时间` |
| 展开字段 | 现有 `logFields` |
| 展开操作 | 「查看」→ 详情弹层(只读) |
**详情弹层**
- 资料风字段(含请求地址、错误信息等 `detailFields`)
- 「请求参数」独立 `section` + `<pre class="mp-customer-pre">`
- 无编辑/删除
## 5. 资料风详情弹层 DOM 契约
对齐 `detail.vue` 资料 Tab:
```text
van-popup (teleport=body, round, ~82%)
van-nav-bar (标题 + 关闭)
body
section.mp-wv-section
h3 主标题
CustomerFields(readonly) // 透明底,勿再用 inset 白组包一层
(商机)OpportunityActions
(商机)section 跟进 / 阶段 …
(日志)section 请求参数 + pre
```
弹层内禁止再套一层「大白板」冲掉灰底页节奏;子列表卡用 `mp-customer-detail-card`。
## 6. 实现切分建议
1. **InvoicePanel**:先对齐列表骨架(改动面独立、无 RelatedPanel 分支)。
2. **RelatedPanel**:按 `kind` 统一列表卡 + 三栏;合同去掉路由跳转改弹层;商机/日志弹层换资料风皮肤;商机子列表扁平化。
3. **样式**:扩展 `customer.scss`;下拉/更多复用联系人 `teleport`/`z-index` 约定。
4. **清理**:四 Tab 不再依赖列表态 `LocalFilters`(组件文件可保留供他处,确认无引用后再删)。
建议顺序可按「开票 → 日志 → 合同 → 商机」降低联调风险(商机弹层最重)。
## 7. 验收
- 375 / 414 宽:无横向溢出;三栏下拉与更多 Sheet 不被 Tab/顶栏挡住。
- 四 Tab:检索 + 两快捷 + 更多可用;卡片默认收叠,展开见全字段与操作。
- 商机:弹层资料风 + 推进/跟进/删除仍可用;跟进/阶段可展示。
- 合同:查看弹层资料风;本 Tab 不跳合同独立页。
- 开票:增删改、设默认、粘贴识别仍可用;表单弹层可完整操作。
- 日志:只读详情含请求参数。
- sticky 主 Tab 不下钻绿头(已有 `offset-top` + z-index 约束保持)。
- 不改 Web PC 客户页;API 请求参数与权限行为与改造前一致。
- 现有相关单测若有则保持通过;无则手工回归上述路径即可。
## 8. 自检(写文档时)
- [x] 无 TBD/占位符遗漏关键决策(开票快捷2 定为「有税号」;合同改为弹层)
- [x] 与「完全对齐联系人 + 详情对齐资料」及方案 A 一致
- [x] 范围不含后端/新路由/资料·跟进·联系人重做
- [x] 合同「不跳转」与「不做新路由」无矛盾:改的是客户 Tab 内入口,非删除合同模块页
@@ -0,0 +1,32 @@
# 小程序 WebView · 客户管理列表卡(全量 Web 列 + 产品卡范式)
日期:2026-10-18
状态:已确认
范围:`web-admin` 小程序 WebView 客户管理列表 `/m/crm/customer`
## 1. 目标
1. 列表卡视觉/交互对齐产品管理列表卡:扁平无色块、预览 +「更多字段」、展开按钮上方无虚线。
2. 卡片字段固定为 Web「客户管理」列表全部列(含默认可隐藏列),不随「显示字段」勾选变化。
3. 「更多」筛选去掉「显示字段」;其它筛选条件不变。
## 2. 字段(对齐 PC `crm/customer` 列表)
始终标题:客户名称。右上 Tag:客户状态。
| 区域 | 字段 |
|------|------|
| meta | `客户状态 · 客户编号 · 创建时间` |
| 预览 | 客户简称、手机号 |
| 展开 | 客户编号、客户类型、固定电话、邮箱、客户网站、客户级别、客户来源、所属行业、所在园区、企业规模、省、市、区、详细地址、客户状态、成交状态、负责人、跟进次数、最后跟进时间、下次联系时间、成交时间、成交金额、联系人数量、商机数量、合同数量、进入公海时间、领取时间、保护到期时间、创建部门、创建人、创建时间、更新人、更新时间、备注 |
空值 `—`。字典字段用现有 dict 展示。客户名称仅作标题,不进预览/展开。
## 3. 筛选
- 保留:视图 Tab、类型/级别下拉、「更多」内其它条件。
- 删除:`CustomerFilters`「显示字段」及其 picker;列表页不再维护 `visible` / `selectableFields` 驱动卡片。
## 4. 不做
- 不改详情、表单、公海页、PC 列显隐。
@@ -0,0 +1,56 @@
# 小程序产品管理对齐客户管理样式
日期:2026-10-18
状态:已确认(方案 A)
范围:`/m/crm/product` 列表、详情、新增/编辑外壳、分类页
## 1. 目标
视觉与交互对齐已落地的客户管理 H5(Vant + 三墙绿 + 绿头内 Tab/筛选 + 扁平卡片),**不改**产品 API、权限、字段与业务规则。
## 2. 权威参考
| 维度 | 参考 |
|------|------|
| 列表骨架 | `views/m/crm/customer/index.vue` + `customer.scss` |
| 详情 Tab / 字段 | 客户详情资料风 + sticky `van-tabs` |
| Token | `#0a9f6d` / `#087f57` / `#e9f9f2`;禁止 `#10b981` |
| 表单外壳 | 客户 `form.vue`(绿头 + loading + 底栏);内层仍用 `ProductFormDialog` |
## 3. 分页面
### 3.1 列表 `index.vue`
- 根容器:`mp-product-page mp-product-list`(样式可复用/镜像 customer-list)
- 绿头内:搜索前放 `van-tabs`(全部/上架/下架);搜索后放三栏 `van-dropdown-menu`(分类、型号、更多);`teleport` + 高 z-index
- 去掉:`MpStatusTabs`、`MpThreeColumnFilters`、`MpExpenseCard`、`MpBottomSheet`
- 卡片:对齐 `mp-customer-list-card`(标题/状态 Tag/强调行编号/KV 分类·规格·危化 Tag/操作:编辑·上架下架·删除·查看主按钮)
- 更多筛选:`van-popup` + 字段表单(状态、关键词、分类、型号、标准价区间)
- Speed Dial 保留:产品分类 + 新增产品;确认框改 `showConfirmDialog` / Toast 改 Vant
- 加载/空态:`van-loading` / `van-empty`
### 3.2 详情 `detail.vue`
- sticky `van-tabs`:基础信息 / 品牌与别名 / 产品图片;`offset-top` 避绿头
- 分区:`section.mp-wv-section` + `h3` + KV 行;Tag/`van-image` 替代 el-*
- 底栏:`van-button` 编辑 / 上架下架 / 删除
### 3.3 表单 `form.vue`
- 外壳:`van-loading` / 错误空态对齐客户;底栏若 Dialog 已有则只统一绿头与页背景
- **不**拆写整套 Vant 字段表(方案 A 边界)
### 3.4 分类 `categories.vue`
- `MpBusinessHeader`;列表行保留树缩进
- `el-tag` → `status-tag` 或 `van-tag`;操作/表单 `van-popup` + `van-field`/`van-picker` 等,去掉 `MpBottomSheet` + `el-form`
## 4. 不做
- 不改后端与 PC 产品管理
- 不新建产品字段模型大重构
- 不顺手改报价/合同页
## 5. 验收
375/414 无横溢;列表筛选/分页/上下架删除可用;详情三 Tab 与底栏可用;分类增删改可用;表单保存路径可用;与客户页绿头/卡片节奏一致。
@@ -0,0 +1,109 @@
# 小程序 WebView · 产品管理列表卡(去色块 + 折叠)
日期:2026-10-18
状态:已确认(方案 A)
范围:`web-admin` 小程序 WebView 产品管理列表 `/m/crm/product`
## 1. 目标与边界
### 1.1 目标
1. 去掉顶栏「全部 / 上架 / 下架」状态 Tab:本页与 PC「产品管理」一致,**列表仅展示上架产品**。
2. 去掉列表卡上两处背景色信息区(绿底编号强调条、灰底 KV 块),降低视觉噪音。
3. 字段集对齐 PC「产品管理」**默认可见列**;字段较多时采用与客户列表一致的 **扁平摘要 + 预览 +「更多字段」展开**。
### 1.2 范式权威
| 维度 | 权威参考 | 路径 |
|------|----------|------|
| 折叠交互 / 无色块卡 | 客户列表方案 B | `web-admin/src/views/m/crm/customer/index.vue` + `mp-webview.scss` 客户卡段 |
| PC 字段集 | 产品管理列表默认可见列 | `web-admin/src/views/crm/product/index.vue` `listColumnDefs` |
| 视觉 Token | 三墙绿 | `#0a9f6d` / `#087f57` / `#e9f9f2`;禁止 Fresh Mint `#10b981` |
| 交互示意 Demo | HTML 对照页 | `bak/mobile-ui/shanxia-mp-crm-product-list-card-demo.html` |
### 1.3 不做
- 不改产品详情页、新增/编辑表单、产品分类页。
- 不改 PC 端 `crm/product` 列表行为(含列显隐持久化)。
- 不在本列表展示「成本价」(PC 默认 `defaultVisible: false`)。
- 不新增「字段显隐」配置能力;移动端字段集固定为 PC 默认可见列映射。
- 不顺手改报价/合同等其它 CRM 列表卡。
## 2. 现状与差距
| 项 | 现状 | 目标 |
|----|------|------|
| 状态 Tab | 顶栏 `全部 / 上架 / 下架`,默认 `all` | 移除 Tab;查询固定 `statusIn=on` |
| 编号区 | `.mp-product-list-card__emphasis` 绿底 | 取消色块;编号进 meta / 展开 |
| KV 区 | `.mp-wv-card__kv` 灰底块 | 取消色块;扁平预览行 + 展开行 |
| 字段 | 分类、型号、危化标识 | 对齐 PC 默认可见列(见 §3) |
| 折叠 | 无 | 与客户列表相同的「更多字段 / 收起」 |
## 3. 字段映射(PC 默认可见列 → 移动卡)
PC `listColumnDefs` 默认可见:`productNo`、`categoryName`、`brands`、`aliases`、`model`、`price`、`status`;`productName` 始终展示;`cost` 默认隐藏 → **本页不展示**。
| PC 列 | 移动卡位置 |
|-------|------------|
| 产品名称 | 标题(主文案) |
| 易制毒 / 易制爆 | 标题旁小标「毒」「爆」(有则显示;对齐 PC 名称列 Tag) |
| 状态 | 右上角浅底 Tag「上架」;meta 行亦含「上架」文案(已确认保留) |
| 产品编号 | meta:`上架 · {编号} · {创建时间}`;展开区再列「产品编号」一行 |
| 创建时间 | 仅 meta(PC 列表无此列,但移动卡保留时间辅助信息) |
| 产品分类 | 预览第 1 行 |
| 规格型号 | 预览第 2 行 |
| 品牌 | 展开区(格式化与 PC `formatProductBrands` 一致) |
| 别名 | 展开区(格式化与 PC `formatProductAliases` 一致) |
| 标准价(元) | 展开区(格式化与 PC `formatProductPrice` 一致) |
| 状态(文案行) | 展开区「状态」一行 |
| 成本价 | **不展示** |
展示规则:
- 空值显示 `—`(与现有移动 CRM 习惯一致)。
- 分类优先全路径(现有 `categoryPathMap`),无路径时回退 `categoryName`。
- 展开区固定五行:产品编号、品牌、别名、标准价(元)、状态;因此「更多字段」按钮**始终展示**(与 Demo 一致)。
## 4. 页面结构
自上而下:
1. **绿顶栏**:返回 + 标题「产品管理」+ 胶囊搜索(无状态 Tab)。
2. **三栏筛选**:全部分类 / 全部型号 / 更多(价格区间等现有能力保留)。
3. **列表卡**(§3)+ 分页加载更多。
4. **Speed Dial**:产品分类 / 新增产品(不变)。
操作栏仍为:编辑、下架、删除、查看。下架成功后刷新列表,该卡因 `status=on` 过滤而消失。
## 5. 实现要点(落地时)
### 5.1 前端
- `web-admin/src/views/m/crm/product/index.vue`:移除 `van-tabs` 状态筛选;`buildProductListQuery` 固定上架;卡片 DOM 改客户式 preview / toggle / extra。
- `web-admin/src/styles/mp-webview.scss`:删除或停用产品列表「强调条 + KV 色块」专属样式;产品卡复用/对齐客户卡扁平类名(可加 `mp-product-list-card__*` 镜像客户结构,避免误伤其它页)。
- `web-admin/src/utils/mp-crm-product-card.ts`:扩展卡片模型(brands、aliases、price、展示串);`statusFilter` 可收敛为固定 `on` 或删除 Tab 常量;更新单元测试。
- 列表行需带出 `brands` / `aliases` / `price`(后端列表 `_row_to_dict` 已含则直接用;若前端类型未声明则补齐)。
### 5.2 查询
- `statusIn`(或等价参数)固定为 `on`;不再传 `all` / `off`。
- 其它筛选(keyword、categoryId、model、priceMin/Max)行为不变。
### 5.3 验证
- 无状态 Tab;默认列表仅上架。
- 卡无绿底编号条、无灰底 KV 块。
- 预览仅分类、型号;点「更多字段」可见编号/品牌/别名/标准价/状态。
- 毒/爆小标与右上「上架」Tag 正确。
- 下架成功后卡消失;375 / 414 无横向溢出。
- 相关 `mp-crm-product-card` 单测更新并通过。
## 6. 已确认决策摘要
| 决策点 | 结论 |
|--------|------|
| 实现路径 | 方案 A:对齐客户列表扁平 + 折叠 |
| 右上状态 Tag | 保留「上架」 |
| meta 行 | `上架 · 编号 · 时间` |
| 成本价 | 不展示(跟 PC 默认可见列) |
| 状态 Tab | 移除;固定只查上架 |
@@ -0,0 +1,26 @@
# 小程序 WebView · 报价单管理/审批扁平卡对齐
日期:2026-10-18
状态:已确认(用户授权直接落地)
范围:`/m/crm/quote`、`/m/crm/quote/approval`(共用 `MpQuoteList`)、`detail.vue`、`form.vue` 外壳与确认弹窗
## 决策摘要
| 项 | 结论 |
|----|------|
| 范围 | D:列表+详情+表单外壳+审批共用列表 |
| 字段集 | A:PC 默认可见列 → 预览/更多字段 |
| Tab | C:管理/审批均保留状态 Tab,换皮进绿顶栏 |
| 确认弹窗 | A:横排;普通绿 / 删除·作废危险红 |
| 路径 | 方案 1:对齐客户/产品(Vant + 扁平卡) |
## 列表卡字段
标题:`quoteTitle`;右上 Tag:状态。
meta:`状态 · 单号 · 报价日期`。
预览:客户名称、最终金额。
展开:报价类型、产品信息、产品总金额、运费、桶押金、状态、负责人、报价日期、创建时间、报价单号。
## 不做
不改 API/权限;不做分享/转合同/PDF/导出/批量;不改合同模块。
@@ -0,0 +1,161 @@
# 工作台 WebView 列表卡标准升级(扁平折叠卡)
日期:2026-10-18
状态:已确认(方案 B)
范围:文档标准——所有小程序工作台 H5 列表页(`web-admin` 路由 `/m/**`);本次**只改规范文档**,不改业务实现代码
## 1. 目标与权威
### 1.1 目标
1. 将已验收的 **客户管理 / 产品管理** 扁平折叠列表卡,提升为全部 `/m/**` WebView 列表的**新标准**。
2. 将旧 `expense-card`(灰底 KV、绿底强调条、大号金额色块区、style-align 备用金式卡)标为 **过渡 / 待改造**。
3. 同步更新 OpenSpec 与 `miniprogram-ui-spec-sanqiang.md` §12,避免后续研发继续按旧骨架落地。
### 1.2 决策
| 决策点 | 结论 |
|--------|------|
| 权威范围 | **B**:扁平卡成为所有 `/m/**` WebView 列表新标准 |
| 旧范式 | expense-card / style-align 标过渡;新需求与改造 PR SHALL NOT 再新建旧形态 |
| 文档路径 | 先本 design,再改 OpenSpec + §12(内容对齐) |
| 代码 | 本次不改报价/合同/危化证等存量实现 |
### 1.3 权威参考
| 维度 | 权威 |
|------|------|
| 参考实现 | `web-admin/src/views/m/crm/customer/index.vue`、`.../product/index.vue` + `mp-webview.scss` 扁平卡段 |
| 列表卡 Demo | `bak/mobile-ui/shanxia-mp-crm-customer-list-card-demo.html`、`shanxia-mp-crm-product-list-card-demo.html` |
| 确认弹窗 Demo | `bak/mobile-ui/shanxia-mp-crm-product-delete-confirm-demo.html` |
| Token | 三墙绿 `#0a9f6d` / `#087f57` / `#e9f9f2`;禁止 Fresh Mint `#10b981` |
| 过渡对照(非新标) | `bak/mobile-ui/shanxia-mp-crm-pl-cert-style-align-demo.html`、原生备用金 `expense-card` |
### 1.4 不做
- 不改业务 Vue/SCSS(报价、合同、危化证等存量页可暂留旧卡,仅文档催迁移)。
- 不改 PC Fresh Mint、不改后端 API。
- 不新建独立 OpenSpec change 名;条款挂在现有 `zkxw-engineering-standards` → `ui-conventions`。
## 2. 列表骨架与扁平卡规则
### 2.1 列表页纵向顺序
| 顺序 | 区块 | 规则 |
|------|------|------|
| 1 | 绿顶栏 | 返回 + 固定标题;胶囊搜索可放在顶栏内(对齐 `MpBusinessHeader`) |
| 2 | 业务 Tab(可选) | 仅业务有多视图/多状态时出现(如客户视图);**不是**每页必有。无业务需要时 SHALL NOT 硬塞状态 Tab(产品列表已省略) |
| 3 | 三栏筛选 | 固定 3 格:前 2 快捷下拉 +「更多」;紧贴搜索下方 |
| 4 | 列表区 | 扁平卡 + 条数/加载提示 + 加载更多;卡片与左右边界留白约 12px |
| 5 | 新增 / 批量 | 管理类用右下角 Speed Dial;批量底栏仅勾选后出现 |
禁止:顶栏下再堆多行「新增 / 筛选 / 刷新」工具条。
### 2.2 「更多」筛选
底部 Sheet 收纳该页全部筛选条件。控件能力集合:单选 Chip、模糊检索、日期/日期时间、日期区间、数值区间。单个业务页只呈现桌面端/后端已支持且可生效的字段,不得为凑控件类型伪造无效条件。
底栏:重置、确定。两钮水平居中、间距清晰、适中固定宽度、触达高度 ≥ 44px;不粘连、不伸满整行。确定后回写前两栏文案;「更多」内有额外已填条件时,顶栏「更多」须有已筛提示(如「更多·已筛」或红点)。
### 2.3 扁平列表卡结构(新标准,取代 expense-card)
自上而下 SHALL:
1. **顶行**:主标题(单行省略)+ 右侧浅底 `status-tag`;可勾选业务勾选在标题左侧。
2. **meta 行**:弱文案摘要(如 `状态 · 编号 · 时间`),无色块底。
3. **预览区**:少量关键字段,左右 label/value;**无灰底 / 绿底色块**。
4. **「更多字段 / 收起」**:展开按钮上方无虚线;字段较多时提供展开。
5. **展开区**:其余列表字段扁平 KV 行;空值显示 `—`。
6. **虚线操作底栏**:允许多钮;同一卡片 **至多一个** 实心主色按钮(通常「查看」);危险操作用浅底危险描边,不用第二主色实心。
有金额的业务(报价/合同等迁移时):金额进入 meta / 预览 / 展开之一;SHALL NOT 再强制独立「大号金额色块区」作为默认层。若需强调金额,用字号/字重,不用灰底/绿底容器。
建议 class(H5):`mp-wv-card` + 业务修饰(如 `mp-customer-list-card` / `mp-product-list-card`)及 `__meta` / `__preview` / `__toggle` / `__extra` / `__row`;结构不得缺层,类名可等价映射。
### 2.4 字段集原则
- 移动卡字段对齐对应 PC 列表的约定可见列(或该业务已确认的固定全集)。
- SHALL NOT 提供移动端「显示字段」勾选驱动卡片内容(客户管理已删除该能力)。
- 字段集由产品定稿后固定。
### 2.5 旧 expense-card(过渡)
规范中保留简短结构说明,仅作存量对照:
- 旧结构:顶行标题+Tag → 可选金额大区 → 副信息 → 灰底 KV → 时间 → 虚线底栏。
- 权威 Demo(过渡):`shanxia-mp-crm-pl-cert-style-align-demo.html`;原生参考:备用金列表。
- **新增与改造** SHALL 使用 §2.3 扁平卡;审查 SHALL 拒绝新建灰底 KV / 绿底强调条列表卡。
### 2.6 存量映射
| 模块 | 相对新标准 | 文档表述 |
|------|------------|----------|
| 客户管理 / 产品管理列表 | 已达标 | 参考实现 |
| 仍使用 expense-card / 色块卡的报价、合同、危化证等 | 过渡 | 待改造;新 PR 勿再加深旧形态 |
## 3. 详情、确认弹窗、附件与 Token
### 3.1 独立详情页
- 详情 SHALL 使用可直达、可刷新、可返回的独立路由;SHALL NOT 把主详情放入 Bottom Sheet 或桌面 `el-dialog`。
- 绿顶栏只展示返回与固定页面标题;编号、状态、金额等业务数据下沉正文;SHALL NOT 再设票根、Hero、动态记录标题、副标题或顶栏状态等顶部特殊区。
- 顶栏后:业务 Tab(如有)→ 分区信息卡(上标签下值或 `section` + `h3` + KV)→ 底栏主操作。
- 审批流(有审批业务时)使用时间轴头像步骤。
- 底栏触达 ≥ 44×44;同一层级至多一个实心主色按钮。
### 3.2 确认弹窗(新纳入规范)
权威示意:`bak/mobile-ui/shanxia-mp-crm-product-delete-confirm-demo.html`。
| 类型 | 规格 |
|------|------|
| 公共外壳 | 白底圆角约 14–16px;遮罩半透明深色、无 backdrop-blur;底部**横排**取消 \| 确认,等宽、高约 40px、圆角约 10px;禁止纵向堆叠双钮 |
| 普通确认 | 确认键三墙绿实心 `#0a9f6d` + 白字;取消白底 + `#dcebe4` 描边 |
| 删除 / 不可恢复 | 标题区危险警示视觉;确认键危险红实心 `#dc2626` + 白字;确认文案如「确认删除」 |
规范约束视觉结果;实现可采用 Vant `showConfirmDialog` + 业务 class 覆盖,不绑死具体 API。
### 3.3 附件上传与回显
对齐「我的发票夹」宫格:单项约 72×72、圆角 8px;图片缩略 `aspectFill`;非图片扩展名 + 文件名;编辑态角标删除 + 虚线「+」添加入口。色板走三墙 Token;SHALL NOT 使用管理端 140×96 附件卡作为 H5 主形态。
### 3.4 Token 与触控
- 主色链:`#0a9f6d` / `#087f57` / `#e9f9f2`;边框参考 `#dcebe4`;正文约 `#1f2a26`,辅助约 `#6b7b74`。
- 正文约 14px,页面标题约 17px,辅助 11–12px。
- 可见按钮可按视觉略矮,实际命中区域 SHALL ≥ 44×44。
## 4. 文档落地与审查
### 4.1 落地文件
| 文件 | 动作 |
|------|------|
| 本文件 | 设计定稿 |
| `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md` | 更新「工作台 WebView 业务页」Requirement、Scenario,及文首 WebView Demo 引用 |
| `docs/miniprogram-ui-spec-sanqiang.md` | 更新文首权威 Demo 列表与 §12 全文骨架/卡片/清单 |
| `docs/web-admin-mp-webview-modules.md` | 必要时补一句:列表卡标准见 §12 / OpenSpec |
### 4.2 Code review 检查清单(写入规范后)
- [ ] Token 为三墙绿,无 `#10b981` 主色
- [ ] 列表卡为扁平折叠范式,无新建灰底 KV / 绿底强调条
- [ ] 权威 Demo 对照客户/产品 list-card(非仅 style-align)
- [ ] 状态/视图 Tab 仅业务需要时出现
- [ ] 顺序:绿顶栏(含搜索)→ 可选业务 Tab → 三栏筛选 → 扁平卡;无顶栏下多行工具条
- [ ] 「更多」含业务实际需要的非 Chip 条件;已筛有提示
- [ ] 确认弹窗横排;删除为危险红语义
- [ ] 详情为独立路由、普通绿顶栏且无票根/顶部特殊区;附件为 72 宫格
- [ ] 拒绝桌面 `el-dialog` 硬套窄屏作为主详情
### 4.3 验证(文档变更)
- OpenSpec 与 §12 对权威 Demo、卡片结构、Tab 可选、确认弹窗、过渡说明表述一致。
- 文首/引用不再把 style-align 写成唯一 WebView 列表权威。
- 存量过渡页在规范中可定位(§2.6)。
## 5. 已确认对话摘要
1. 以客户管理、产品管理已验收样式为标准,回写 OpenSpec / UI 规范。
2. 权威范围选 **B**:扁平卡覆盖全部 `/m/**` 列表;expense-card 标过渡。
3. 文档更新路径:design → OpenSpec + §12;本次不改业务代码。
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_env_js", "label": "env.js", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L1"}, {"id": "config_env_env_presets", "label": "ENV_PRESETS", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L28"}, {"id": "config_env_legacy_minio_base_prefixes", "label": "LEGACY_MINIO_BASE_PREFIXES", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L78"}, {"id": "config_env_resolveminiourl", "label": "resolveMinioUrl()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L90"}, {"id": "config_env_resolveapibase", "label": "resolveApiBase()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L121"}, {"id": "config_env_resolveweborigin", "label": "resolveWebOrigin()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L125"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_env_js", "target": "config_env_env_presets", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L28", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_env_js", "target": "config_env_legacy_minio_base_prefixes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L78", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_env_js", "target": "config_env_resolveminiourl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L90", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_env_js", "target": "config_env_resolveapibase", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L121", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_env_js", "target": "config_env_resolveweborigin", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L125", "weight": 1.0}], "raw_calls": [{"caller_nid": "config_env_resolveminiourl", "callee": "trim", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L91"}, {"caller_nid": "config_env_resolveminiourl", "callee": "String", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L91"}, {"caller_nid": "config_env_resolveminiourl", "callee": "replace", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L93"}, {"caller_nid": "config_env_resolveminiourl", "callee": "String", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L93"}, {"caller_nid": "config_env_resolveminiourl", "callee": "test", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L97"}, {"caller_nid": "config_env_resolveminiourl", "callee": "startsWith", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L98"}, {"caller_nid": "config_env_resolveminiourl", "callee": "toLowerCase", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L98"}, {"caller_nid": "config_env_resolveminiourl", "callee": "toLowerCase", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L98"}, {"caller_nid": "config_env_resolveminiourl", "callee": "startsWith", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L102"}, {"caller_nid": "config_env_resolveminiourl", "callee": "toLowerCase", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L102"}, {"caller_nid": "config_env_resolveminiourl", "callee": "toLowerCase", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/env.js", "source_location": "L102"}, {"caller_nid": "config_env_resolveminiourl", "callee": "replace", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprograLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_workbench_menus_js", "label": "workbench-menus.js", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/workbench-menus.js", "source_location": "L1"}, {"id": "config_workbench_menus_workbench_modules", "label": "WORKBENCH_MODULES", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/workbench-menus.js", "source_location": "L6"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_workbench_menus_js", "target": "config_workbench_menus_workbench_modules", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/workbench-menus.js", "source_location": "L6", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_utils_plvehiclerechargecopy_js", "label": "plVehicleRechargeCopy.js", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L1"}, {"id": "utils_plvehiclerechargecopy_type_labels", "label": "TYPE_LABELS", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L7"}, {"id": "utils_plvehiclerechargecopy_getplrechargetypelabel", "label": "getPlRechargeTypeLabel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L12"}, {"id": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "label": "buildVehicleRechargeApplyCopyText()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L16"}, {"id": "utils_plvehiclerechargecopy_copyvehiclerechargeapplytoclipboard", "label": "copyVehicleRechargeApplyToClipboard()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L37"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_utils_plvehiclerechargecopy_js", "target": "plfuelregistercopy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_utils_plvehiclerechargecopy_js", "target": "utils_plvehiclerechargecopy_type_labels", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_utils_plvehiclerechargecopy_js", "target": "utils_plvehiclerechargecopy_getplrechargetypelabel", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_utils_plvehiclerechargecopy_js", "target": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L16", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_utils_plvehiclerechargecopy_js", "target": "utils_plvehiclerechargecopy_copyvehiclerechargeapplytoclipboard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L37", "weight": 1.0}, {"source": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "target": "utils_plvehiclerechargecopy_getplrechargetypelabel", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L26", "weight": 1.0}, {"source": "utils_plvehiclerechargecopy_copyvehiclerechargeapplytoclipboard", "target": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L38", "weight": 1.0}], "raw_calls": [{"caller_nid": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "callee": "trim", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L18"}, {"caller_nid": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "callee": "String", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L18"}, {"caller_nid": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "callee": "isFinite", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L20"}, {"caller_nid": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "callee": "Number", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L20"}, {"caller_nid": "utils_plvehiclerechargecopy_buildvehiclerechargeapplycopytext", "callee": "toFixed", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/utils/plVehicleRechargeCopy.js", "source_location": "L21"}, {"caller_nid": "utils_plvehiclerechargeLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_pages_pl_vehicle_recharge_apply_list_vue", "label": "list.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L1"}, {"id": "vehicle_recharge_apply_list_safeareastyle", "label": "safeAreaStyle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L131"}, {"id": "vehicle_recharge_apply_list_userstore", "label": "userStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L132"}, {"id": "vehicle_recharge_apply_list_tabs", "label": "tabs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L134"}, {"id": "vehicle_recharge_apply_list_keyword", "label": "keyword", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L143"}, {"id": "vehicle_recharge_apply_list_activetab", "label": "activeTab", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L144"}, {"id": "vehicle_recharge_apply_list_list", "label": "list", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L145"}, {"id": "vehicle_recharge_apply_list_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L146"}, {"id": "vehicle_recharge_apply_list_finished", "label": "finished", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L147"}, {"id": "vehicle_recharge_apply_list_refreshing", "label": "refreshing", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L148"}, {"id": "vehicle_recharge_apply_list_currentpage", "label": "currentPage", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L149"}, {"id": "vehicle_recharge_apply_list_listheight", "label": "listHeight", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L151"}, {"id": "vehicle_recharge_apply_list_currentuserid", "label": "currentUserId()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L153"}, {"id": "vehicle_recharge_apply_list_canwrite", "label": "canWrite()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L158"}, {"id": "vehicle_recharge_apply_list_canwithdraw", "label": "canWithdraw()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L164"}, {"id": "vehicle_recharge_apply_list_typelabel", "label": "typeLabel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L170"}, {"id": "vehicle_recharge_apply_list_statuslabel", "label": "statusLabel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L176"}, {"id": "vehicle_recharge_apply_list_statustagclass", "label": "statusTagClass()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L188"}, {"id": "vehicle_recharge_apply_list_computelistheight", "label": "computeListHeight()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L197"}, {"id": "vehicle_recharge_apply_list_fetchpage", "label": "fetchPage()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L207"}, {"id": "vehicle_recharge_apply_list_onrefresh", "label": "onRefresh()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/list.vue", "source_location": "L233"}, {"id": "vehicle_recharge_apply_list_onpullrefresh", "label": "onPullRefresh()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_workbench_menus_js", "label": "workbench-menus.js", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/workbench-menus.js", "source_location": "L1"}, {"id": "config_workbench_menus_workbench_modules", "label": "WORKBENCH_MODULES", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/workbench-menus.js", "source_location": "L6"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_workbench_menus_js", "target": "config_workbench_menus_workbench_modules", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/workbench-menus.js", "source_location": "L6", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "label": "api.js", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L1"}, {"id": "config_api_auth_api", "label": "AUTH_API", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L36"}, {"id": "config_api_upload_api", "label": "UPLOAD_API", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L54"}, {"id": "config_api_page_paths", "label": "PAGE_PATHS", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L59"}, {"id": "config_api_workbench_route_map", "label": "WORKBENCH_ROUTE_MAP", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L98"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "env", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_auth_api", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L36", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_upload_api", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L54", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_page_paths", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L59", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_workbench_route_map", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L98", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_pages_pl_vehicle_recharge_apply_form_vue", "label": "form.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/pages/pl/vehicle-recharge-apply/form.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "label": "api.js", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L1"}, {"id": "config_api_auth_api", "label": "AUTH_API", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L36"}, {"id": "config_api_upload_api", "label": "UPLOAD_API", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L54"}, {"id": "config_api_page_paths", "label": "PAGE_PATHS", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L59"}, {"id": "config_api_workbench_route_map", "label": "WORKBENCH_ROUTE_MAP", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L98"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "env", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_auth_api", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L36", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_upload_api", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L54", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_page_paths", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L59", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_miniprogram_config_api_js", "target": "config_api_workbench_route_map", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/miniprogram/config/api.js", "source_location": "L98", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/detail.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_business_pl_plan_form_dialog_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1"}, {"id": "pl_plan_form_dialog_index_frominfo", "label": "fromInfo", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L663"}, {"id": "pl_plan_form_dialog_index_b64", "label": "b64", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L669"}, {"id": "pl_plan_form_dialog_index_pad", "label": "pad", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L670"}, {"id": "pl_plan_form_dialog_index_obj", "label": "obj", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L671"}, {"id": "pl_plan_form_dialog_index_d", "label": "d", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L684"}, {"id": "pl_plan_form_dialog_index_y", "label": "y", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L685"}, {"id": "pl_plan_form_dialog_index_m", "label": "m", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L686"}, {"id": "pl_plan_form_dialog_index_day", "label": "day", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L687"}, {"id": "pl_plan_form_dialog_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L864"}, {"id": "pl_plan_form_dialog_index_cids", "label": "cids", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L920"}, {"id": "pl_plan_form_dialog_index_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L990"}, {"id": "pl_plan_form_dialog_index_productrows", "label": "productRows", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L998"}, {"id": "pl_plan_form_dialog_index_allowedids", "label": "allowedIds", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L999"}, {"id": "pl_plan_form_dialog_index", "label": "[]", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1001"}, {"id": "pl_plan_form_dialog_index_parts", "label": "parts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1119"}, {"id": "pl_plan_form_dialog_index_total", "label": "total", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1123"}, {"id": "pl_plan_form_dialog_index_fromlist", "label": "fromList", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1164"}, {"id": "pl_plan_form_dialog_index_loaddefaults", "label": "loadDefaults", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1172"}, {"id": "pl_plan_form_dialog_index_defaults", "label": "defaults", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1183"}, {"id": "pl_plan_form_dialog_index_carrier", "label": "carrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1193"}, {"id": "pl_plan_form_dialog_index_alng", "label": "aLng", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plaLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan/detail.vue", "source_location": "L1"}, {"id": "plan_detail_el", "label": "el", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan/detail.vue", "source_location": "L663"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_detail_vue", "target": "plan_detail_el", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan/detail.vue", "source_location": "L663", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "label": "CustomerPoolSheet.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L1"}, {"id": "customer_customerpoolsheet_reason", "label": "reason", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L51"}, {"id": "customer_customerpoolsheet_error", "label": "error", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L52"}, {"id": "customer_customerpoolsheet_saving", "label": "saving", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L53"}, {"id": "customer_customerpoolsheet_onshow", "label": "onShow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L65"}, {"id": "customer_customerpoolsheet_close", "label": "close()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L68"}, {"id": "customer_customerpoolsheet_submit", "label": "submit()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L72"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_confirm_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L46", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "utils_mp_crm_confirm_showcrmconfirmdialog", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L46", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_api_crm_customer_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L47", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "crm_customer_fetchintopool", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L47", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "customer_customerpoolsheet_reason", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L51", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "customer_customerpoolsheet_error", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L52", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "customer_customerpoolsheet_saving", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L53", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "customer_customerpoolsheet_onshow", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L65", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customerpoolsheet_vue", "target": "customer_customerpoolsheet_close", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerPoolSheet.vue", "source_location": "L68", "weight": 1.0}, {Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "label": "InvoicePanel.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L1"}, {"id": "customer_invoicepanel_key", "label": "key", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L248"}, {"id": "customer_invoicepanel_kw", "label": "kw", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L262"}, {"id": "customer_invoicepanel_tip", "label": "tip", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L269"}, {"id": "customer_invoicepanel_data", "label": "data", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L343"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_key", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L248", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_kw", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L262", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_tip", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L269", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_data", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L343", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_categories_vue", "label": "categories.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L1"}, {"id": "product_categories_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L418"}, {"id": "product_categories_payload", "label": "payload", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L475"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_categories_vue", "target": "product_categories_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L418", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_categories_vue", "target": "product_categories_payload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L475", "weight": 1.0}], "raw_calls": [{"caller_nid": "product_categories_if", "callee": "showFailToast", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L419"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L1"}, {"id": "quote_detail_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L695"}, {"id": "quote_detail_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L786"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_detail_vue", "target": "quote_detail_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L695", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_detail_vue", "target": "quote_detail_id", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L786", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_contract_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L1"}, {"id": "contract_detail_route", "label": "route", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L480"}, {"id": "contract_detail_router", "label": "router", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L481"}, {"id": "contract_detail_userstore", "label": "userStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L482"}, {"id": "contract_detail_dictstore", "label": "dictStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L483"}, {"id": "contract_detail_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L485"}, {"id": "contract_detail_acting", "label": "acting", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L486"}, {"id": "contract_detail_errortext", "label": "errorText", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L487"}, {"id": "contract_detail_approverpickeropen", "label": "approverPickerOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L491"}, {"id": "contract_detail_voidopen", "label": "voidOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L492"}, {"id": "contract_detail_voidreason", "label": "voidReason", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L493"}, {"id": "contract_detail_stickyoffset", "label": "stickyOffset", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L494"}, {"id": "contract_detail_scrollmargin", "label": "scrollMargin", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L496"}, {"id": "contract_detail_moreopen", "label": "moreOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L502"}, {"id": "contract_detail_submitopen", "label": "submitOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L503"}, {"id": "contract_detail_approveopen", "label": "approveOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L504"}, {"id": "contract_detail_approveopinion", "label": "approveOpinion", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L506"}, {"id": "contract_detail_isapproveentry", "label": "isApproveEntry", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L514"}, {"id": "contract_detail_resolvecurrentuserid", "label": "resolveCurrentUserId()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L516"}, {"id": "contract_detail_detailtabs", "label": "detailTabs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L533"}, {"id": "contract_detail_statuslabel", "label": "statusLabel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L542"}, {"id": "contract_detail_statustone", "label": "statusTone", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L547"}, {"id": "contract_detail_contracttypelabel", "label": "contractTypeLabel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L555"}, {"id": "contract_detail_templatelabel", "label": "templateLabel", "file_type": "code", "source_file": "/Users/zlwu/workLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "label": "InvoicePanel.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L1"}, {"id": "customer_invoicepanel_key", "label": "key", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L236"}, {"id": "customer_invoicepanel_kw", "label": "kw", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L250"}, {"id": "customer_invoicepanel_tip", "label": "tip", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L257"}, {"id": "customer_invoicepanel_data", "label": "data", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L331"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_key", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L236", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_kw", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L250", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_tip", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L257", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_invoicepanel_vue", "target": "customer_invoicepanel_data", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/InvoicePanel.vue", "source_location": "L331", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_mpquotelist_vue", "label": "MpQuoteList.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L1"}, {"id": "quote_mpquotelist_text", "label": "text", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L620"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_mpquotelist_vue", "target": "quote_mpquotelist_text", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L620", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_business_pl_plan_form_dialog_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1"}, {"id": "pl_plan_form_dialog_index_frominfo", "label": "fromInfo", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L676"}, {"id": "pl_plan_form_dialog_index_b64", "label": "b64", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L682"}, {"id": "pl_plan_form_dialog_index_pad", "label": "pad", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L683"}, {"id": "pl_plan_form_dialog_index_obj", "label": "obj", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L684"}, {"id": "pl_plan_form_dialog_index_d", "label": "d", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L697"}, {"id": "pl_plan_form_dialog_index_y", "label": "y", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L698"}, {"id": "pl_plan_form_dialog_index_m", "label": "m", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L699"}, {"id": "pl_plan_form_dialog_index_day", "label": "day", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L700"}, {"id": "pl_plan_form_dialog_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L877"}, {"id": "pl_plan_form_dialog_index_cids", "label": "cids", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L933"}, {"id": "pl_plan_form_dialog_index_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1003"}, {"id": "pl_plan_form_dialog_index_productrows", "label": "productRows", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1011"}, {"id": "pl_plan_form_dialog_index_allowedids", "label": "allowedIds", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1012"}, {"id": "pl_plan_form_dialog_index", "label": "[]", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1014"}, {"id": "pl_plan_form_dialog_index_parts", "label": "parts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1132"}, {"id": "pl_plan_form_dialog_index_total", "label": "total", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1136"}, {"id": "pl_plan_form_dialog_index_fromlist", "label": "fromList", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1177"}, {"id": "pl_plan_form_dialog_index_loaddefaults", "label": "loadDefaults", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1185"}, {"id": "pl_plan_form_dialog_index_defaults", "label": "defaults", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1196"}, {"id": "pl_plan_form_dialog_index_carrier", "label": "carrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue", "source_location": "L1206"}, {"id": "pl_plan_form_dialog_index_alng", "label": "aLng", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "label": "MpProductSelectSheet.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L1"}, {"id": "mp_mpproductselectsheet_raw", "label": "raw", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L174"}, {"id": "mp_mpproductselectsheet_names", "label": "names", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L180"}, {"id": "mp_mpproductselectsheet_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L189"}, {"id": "mp_mpproductselectsheet_idx", "label": "idx", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L190"}, {"id": "mp_mpproductselectsheet_kw", "label": "kw", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L199"}, {"id": "mp_mpproductselectsheet_name", "label": "name", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L200"}, {"id": "mp_mpproductselectsheet_model", "label": "model", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L201"}, {"id": "mp_mpproductselectsheet_fetched", "label": "fetched", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L218"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_raw", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L174", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_names", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L180", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_id", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L189", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_idx", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L190", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_kw", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L199", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_name", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L200", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_model", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L201", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_fetched", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpProductSelectSheet.vue", "source_location": "L218", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpproductselectsheet_vue", "target": "mp_mpproductselectsheet_kw", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/compLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "label": "PlanCard.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L1"}, {"id": "plan_cert_plancard_loadname", "label": "loadName", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L198"}, {"id": "plan_cert_plancard_deliveryname", "label": "deliveryName", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L200"}, {"id": "plan_cert_plancard_missing", "label": "missing", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L207"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_pl_display_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L151", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_display_getplplancompanylabel", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L151", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_display_getplplanfollowstatuslabel", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L151", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_display_getplplantypelabel", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L151", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_display_formatplvehicleplatedisplay", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L151", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_display_normalizeplpersonnelsummarydisplay", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L151", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_pl_plan_list_display_rows_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L158", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_plan_list_display_rows_getdeliverycustomernamefromrow", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L158", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_plan_list_display_rows_getloadcustomernamefromrow", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L158", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "utils_pl_plan_list_display_rows_plplanlistdisplayrow", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L158", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_composables_usepltoxicproductcertcheck_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_fileLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_ts", "label": "mp-crm-product-confirm.ts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_mpquotelist_vue", "label": "MpQuoteList.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L1"}, {"id": "quote_mpquotelist_text", "label": "text", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L633"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_mpquotelist_vue", "target": "quote_mpquotelist_text", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L633", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_categories_vue", "label": "categories.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L1"}, {"id": "product_categories_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L394"}, {"id": "product_categories_payload", "label": "payload", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L440"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_categories_vue", "target": "product_categories_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L394", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_categories_vue", "target": "product_categories_payload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L440", "weight": 1.0}], "raw_calls": [{"caller_nid": "product_categories_if", "callee": "showFailToast", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/categories.vue", "source_location": "L395"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_pl_plan_cert_mjs", "label": "test-mp-pl-plan-cert.mjs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L1"}, {"id": "scripts_test_mp_pl_plan_cert_start", "label": "start()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L12"}, {"id": "scripts_test_mp_pl_plan_cert_pageerrors", "label": "pageErrors", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L27"}, {"id": "scripts_test_mp_pl_plan_cert_button", "label": "button()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L31"}, {"id": "scripts_test_mp_pl_plan_cert_card", "label": "card()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L32"}, {"id": "scripts_test_mp_pl_plan_cert_sheet", "label": "sheet()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L33"}, {"id": "scripts_test_mp_pl_plan_cert_nooverflow", "label": "noOverflow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L38"}, {"id": "scripts_test_mp_pl_plan_cert_assertcenteredsheetactions", "label": "assertCenteredSheetActions()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L40"}, {"id": "scripts_test_mp_pl_plan_cert_waitpatch", "label": "waitPatch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L53"}, {"id": "scripts_test_mp_pl_plan_cert_cleanscreenshot", "label": "cleanScreenshot()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L58"}, {"id": "scripts_test_mp_pl_plan_cert_latestlist", "label": "latestList()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L62"}, {"id": "scripts_test_mp_pl_plan_cert_quickdate", "label": "quickDate()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L63"}, {"id": "scripts_test_mp_pl_plan_cert_quickready", "label": "quickReady()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L64"}, {"id": "scripts_test_mp_pl_plan_cert_quickmore", "label": "quickMore()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L65"}, {"id": "scripts_test_mp_pl_plan_cert_quickoption", "label": "quickOption()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L66"}, {"id": "scripts_test_mp_pl_plan_cert_waitlist", "label": "waitList()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L68"}, {"id": "scripts_test_mp_pl_plan_cert_directdetail", "label": "directDetail", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L75"}, {"id": "scripts_test_mp_pl_plan_cert_tabs", "label": "tabs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L85"}, {"id": "scripts_test_mp_pl_plan_cert_header", "label": "header", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L87"}, {"id": "scripts_test_mp_pl_plan_cert_summary", "label": "summary", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L141"}, {"id": "scripts_test_mp_pl_plan_cert_line", "label": "line", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L159"}, {"id": "scripts_test_mp_pl_plan_cert_textbox", "label": "textBox", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mjs", "source_location": "L160"}, {"id": "scripts_test_mp_pl_plan_cert_linebox", "label": "lineBox", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-pl-plan-cert.mLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mppageloading_vue", "label": "MpPageLoading.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpPageLoading.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "label": "CertUpload.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L1"}, {"id": "plan_cert_certupload_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L73"}, {"id": "plan_cert_certupload_saving", "label": "saving", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L74"}, {"id": "plan_cert_certupload_error", "label": "error", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L75"}, {"id": "plan_cert_certupload_setuploadref", "label": "setUploadRef()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L79"}, {"id": "plan_cert_certupload_hasuploading", "label": "hasUploading()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L83"}, {"id": "plan_cert_certupload_close", "label": "close()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L86"}, {"id": "plan_cert_certupload_load", "label": "load()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L93"}, {"id": "plan_cert_certupload_save", "label": "save()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L118"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_styles_mp_webview_scss", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L59", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_customer_scss", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L60", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_composables_usepltoxicproductcertcheck_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L61", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "composables_usepltoxicproductcertcheck_enrichplanproductshazardfromcrm", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L61", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "composables_usepltoxicproductcertcheck_getrequiredcertslots", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L61", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "composables_usepltoxicproductcertcheck_ishazardproduct", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L61", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "composables_usepltoxicproductcertcheck_hazard_cert_ready_confirm_hint", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertUpload.vue", "source_location": "L61", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certupload_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certattachments_vue", "relation": "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_crm_quote_components_quoteformpanel_vue", "label": "QuoteFormPanel.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1"}, {"id": "components_quoteformpanel_props", "label": "props", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1154"}, {"id": "components_quoteformpanel_ispagevariant", "label": "isPageVariant", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1168"}, {"id": "components_quoteformpanel_userstore", "label": "userStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1176"}, {"id": "components_quoteformpanel_router", "label": "router", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1200"}, {"id": "components_quoteformpanel_route", "label": "route", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1201"}, {"id": "components_quoteformpanel_quotetypeoptions", "label": "quoteTypeOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1203"}, {"id": "components_quoteformpanel_createdefaultform", "label": "createDefaultForm()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1211"}, {"id": "components_quoteformpanel_formtitle", "label": "formTitle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1234"}, {"id": "components_quoteformpanel_basiclabels", "label": "basicLabels", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1238"}, {"id": "components_quoteformpanel_customershortname", "label": "customerShortName", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1257"}, {"id": "components_quoteformpanel_rules", "label": "rules", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1260"}, {"id": "components_quoteformpanel_opportunitydisplayname", "label": "opportunityDisplayName", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1305"}, {"id": "components_quoteformpanel_saveloading", "label": "saveLoading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1307"}, {"id": "components_quoteformpanel_productselectorvisible", "label": "productSelectorVisible", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1310"}, {"id": "components_quoteformpanel_customerselectorvisible", "label": "customerSelectorVisible", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1314"}, {"id": "components_quoteformpanel_customeroptionsloading", "label": "customerOptionsLoading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1316"}, {"id": "components_quoteformpanel_shortnamedialogvisible", "label": "shortNameDialogVisible", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1319"}, {"id": "components_quoteformpanel_shortnamesaving", "label": "shortNameSaving", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1320"}, {"id": "components_quoteformpanel_shortnameform", "label": "shortNameForm", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue", "source_location": "L1322"}, {"id": "components_quoteformpanel_shortnamesavedsuccessfully", "label": "shortNameSavedSuccessfully", "file_type": "coLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_contract_mjs", "label": "test-mp-crm-contract.mjs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L1"}, {"id": "scripts_test_mp_crm_contract_start", "label": "start()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L17"}, {"id": "scripts_test_mp_crm_contract_pageerrors", "label": "pageErrors", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L32"}, {"id": "scripts_test_mp_crm_contract_calls", "label": "calls", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L33"}, {"id": "scripts_test_mp_crm_contract_basecontract", "label": "baseContract", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L36"}, {"id": "scripts_test_mp_crm_contract_contracts", "label": "contracts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L81"}, {"id": "scripts_test_mp_crm_contract_approvals", "label": "approvals", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L101"}, {"id": "scripts_test_mp_crm_contract_dict", "label": "dict", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L126"}, {"id": "scripts_test_mp_crm_contract_navigateback", "label": "navigateBack()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L142"}, {"id": "scripts_test_mp_crm_contract_relaunch", "label": "reLaunch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L143"}, {"id": "scripts_test_mp_crm_contract_getenv", "label": "getEnv()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L144"}, {"id": "scripts_test_mp_crm_contract_request", "label": "request", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L152"}, {"id": "scripts_test_mp_crm_contract_url", "label": "url", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L153"}, {"id": "scripts_test_mp_crm_contract_path", "label": "path", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L155"}, {"id": "scripts_test_mp_crm_contract_body", "label": "body", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L156"}, {"id": "scripts_test_mp_crm_contract_data", "label": "data", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L164"}, {"id": "scripts_test_mp_crm_contract_ticket", "label": "ticket", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L168"}, {"id": "scripts_test_mp_crm_contract_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L210"}, {"id": "scripts_test_mp_crm_contract_click", "label": "click()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L255"}, {"id": "scripts_test_mp_crm_contract_lastcall", "label": "lastCall()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L262"}, {"id": "scripts_test_mp_crm_contract_assertnooverflow", "label": "assertNoOverflow()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L264"}, {"id": "scripts_test_mp_crm_contract_boxesoverlap", "label": "boxesOverlap()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L273"}, {"id": "scripts_test_mp_crm_contract_managecards", "label": "manageCards", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract.mjs", "source_location": "L306"}, {"id": "scripts_test_mp_crm_coLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L1"}, {"id": "product_index_path", "label": "path", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L323"}, {"id": "product_index_categorydropdownoptions", "label": "categoryDropdownOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L329"}, {"id": "product_index_modeldropdownoptions", "label": "modelDropdownOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L333"}, {"id": "product_index_moreactive", "label": "moreActive", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L334"}, {"id": "product_index_cards", "label": "cards", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L335"}, {"id": "product_index_hasmore", "label": "hasMore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L342"}, {"id": "product_index_ensurecategorypathmap", "label": "ensureCategoryPathMap()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L344"}, {"id": "product_index_ensuremodeloptions", "label": "ensureModelOptions()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L366"}, {"id": "product_index_requestsnapshot", "label": "requestSnapshot()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L394"}, {"id": "product_index_isexpanded", "label": "isExpanded()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L404"}, {"id": "product_index_snapshot", "label": "snapshot", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L419"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L246", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "utils_mp_crm_product_confirm_showproductconfirmdialog", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L246", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_layouts_mpwebviewshell_vue", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L247", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpbusinessheader_vue", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L248", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_api_crm_product_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L249", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "crm_product_fetchproductlist", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue", "source_location": "L249", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_product_index_vue", "target": "crLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_business_attachment_upload_field_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L1"}, {"id": "attachment_upload_field_index_usetiles", "label": "useTiles", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L346"}, {"id": "attachment_upload_field_index_ismppreviewtoolbar", "label": "isMpPreviewToolbar", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L347"}, {"id": "attachment_upload_field_index_mpbtnsize", "label": "mpBtnSize", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L350"}, {"id": "attachment_upload_field_index_previewvisible", "label": "previewVisible", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L355"}, {"id": "attachment_upload_field_index_previewpdfsrc", "label": "previewPdfSrc", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L357"}, {"id": "attachment_upload_field_index_previewloading", "label": "previewLoading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L358"}, {"id": "attachment_upload_field_index_previewzoom", "label": "previewZoom", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L362"}, {"id": "attachment_upload_field_index_previewoffsetx", "label": "previewOffsetX", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L363"}, {"id": "attachment_upload_field_index_previewoffsety", "label": "previewOffsetY", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L364"}, {"id": "attachment_upload_field_index_previewdragging", "label": "previewDragging", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L365"}, {"id": "attachment_upload_field_index_previewfitbase", "label": "previewFitBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L366"}, {"id": "attachment_upload_field_index_ispointerinside", "label": "isPointerInside", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L367"}, {"id": "attachment_upload_field_index_isdragover", "label": "isDragOver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L368"}, {"id": "attachment_upload_field_index_previewiconsize", "label": "previewIconSize", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L375"}, {"id": "attachment_upload_field_index_previewtransformcss", "label": "previewTransformCss", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L380"}, {"id": "attachment_upload_field_index_previewstatustext", "label": "previewStatusText", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L389"}, {"id": "attachment_upload_field_index_resolvefileurl", "label": "resolveFileUrl()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L394"}, {"id": "attachment_upload_field_index_next", "label": "next", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/attachment-upload-field/index.vue", "source_location": "L425"}, {"id": "attachment_upload_field_index_current", "label": "current", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/atLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_relatedpanel_vue", "label": "RelatedPanel.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L1"}, {"id": "customer_relatedpanel_dict", "label": "dict", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L332"}, {"id": "customer_relatedpanel_page", "label": "page", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L335"}, {"id": "customer_relatedpanel_total", "label": "total", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L336"}, {"id": "customer_relatedpanel_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L337"}, {"id": "customer_relatedpanel_error", "label": "error", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L338"}, {"id": "customer_relatedpanel_keyword", "label": "keyword", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L339"}, {"id": "customer_relatedpanel_quick1", "label": "quick1", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L340"}, {"id": "customer_relatedpanel_quick2", "label": "quick2", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L341"}, {"id": "customer_relatedpanel_moreopen", "label": "moreOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L342"}, {"id": "customer_relatedpanel_detailopen", "label": "detailOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L346"}, {"id": "customer_relatedpanel_detailloading", "label": "detailLoading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L347"}, {"id": "customer_relatedpanel_detailerror", "label": "detailError", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L348"}, {"id": "customer_relatedpanel_searchplaceholder", "label": "searchPlaceholder", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L387"}, {"id": "customer_relatedpanel_quick1options", "label": "quick1Options", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L393"}, {"id": "customer_relatedpanel_quick2options", "label": "quick2Options", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L418"}, {"id": "customer_relatedpanel_moreactive", "label": "moreActive", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L444"}, {"id": "customer_relatedpanel_key", "label": "key", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L569"}, {"id": "customer_relatedpanel_issubexpanded", "label": "isSubExpanded()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L572"}, {"id": "customer_relatedpanel_togglesubexpand", "label": "toggleSubExpand()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L575"}, {"id": "customer_relatedpanel_fieldtext", "label": "fieldText()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L580"}, {"id": "customer_relatedpanel_cardtitle", "label": "cardTitle()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/RelatedPanel.vue", "source_location": "L590"}, {"id": "customer_relatedpanel_cardmeta", "label": "cardMeta()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_mpquotelist_vue", "label": "MpQuoteList.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L1"}, {"id": "quote_mpquotelist_userstore", "label": "userStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L317"}, {"id": "quote_mpquotelist_dictstore", "label": "dictStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L318"}, {"id": "quote_mpquotelist_keyword", "label": "keyword", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L320"}, {"id": "quote_mpquotelist_quotetype", "label": "quoteType", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L322"}, {"id": "quote_mpquotelist_amountrange", "label": "amountRange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L323"}, {"id": "quote_mpquotelist_quotedatebegin", "label": "quoteDateBegin", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L324"}, {"id": "quote_mpquotelist_quotedateend", "label": "quoteDateEnd", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L325"}, {"id": "quote_mpquotelist_minamount", "label": "minAmount", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L326"}, {"id": "quote_mpquotelist_maxamount", "label": "maxAmount", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L327"}, {"id": "quote_mpquotelist_moreopen", "label": "moreOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L328"}, {"id": "quote_mpquotelist_datepickeropen", "label": "datePickerOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L337"}, {"id": "quote_mpquotelist_speedopen", "label": "speedOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L340"}, {"id": "quote_mpquotelist_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L341"}, {"id": "quote_mpquotelist_forbidden", "label": "forbidden", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L342"}, {"id": "quote_mpquotelist_total", "label": "total", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L344"}, {"id": "quote_mpquotelist_pagenum", "label": "pageNum", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L345"}, {"id": "quote_mpquotelist_quotetypeoptions", "label": "quoteTypeOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L351"}, {"id": "quote_mpquotelist_amountrangeoptions", "label": "amountRangeOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L356"}, {"id": "quote_mpquotelist_headertitle", "label": "headerTitle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L363"}, {"id": "quote_mpquotelist_viewtabs", "label": "viewTabs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L364"}, {"id": "quote_mpquotelist_quotetypedropdownoptions", "label": "quoteTypeDropdownOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L367"}, {"id": "quote_mpquotelist_moreactive", "label": "moreActive", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue", "source_location": "L368"}, {"id": "quote_mpquotelist_Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_form_vue", "label": "form.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/form.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_certdetail_vue", "label": "CertDetail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/CertDetail.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_contract_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L1"}, {"id": "contract_detail_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L663"}, {"id": "contract_detail_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L746"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_contract_detail_vue", "target": "contract_detail_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L663", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_contract_detail_vue", "target": "contract_detail_id", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L746", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_versions_vue", "label": "versions.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/versions.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "label": "ContactPanel.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L1"}, {"id": "customer_contactpanel_key", "label": "key", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L224"}, {"id": "customer_contactpanel_hit", "label": "hit", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L252"}, {"id": "customer_contactpanel_kw", "label": "kw", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L257"}, {"id": "customer_contactpanel_onmoreopen", "label": "onMoreOpen()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L331"}, {"id": "customer_contactpanel_resetmore", "label": "resetMore()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L336"}, {"id": "customer_contactpanel_applymore", "label": "applyMore()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L339"}, {"id": "customer_contactpanel_data", "label": "data", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L391"}, {"id": "customer_contactpanel_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L418"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_key", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L224", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_hit", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L252", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_kw", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L257", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_onmoreopen", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L331", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_resetmore", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L336", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_applymore", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L339", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_data", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L391", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_contactpanel_vue", "target": "customer_contactpanel_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L418", "weight": 1.0}], "raw_calls": [{"caller_nid": "customer_contactpanel_onmoreopen", "callee": "nextTick", "is_member_call": false, "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/ContactPanel.vue", "source_location": "L334"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_components_mp_mpstructskeleton_vue", "label": "MpStructSkeleton.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpStructSkeleton.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_contract_form_vue", "label": "form.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/form.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_form_vue", "label": "form.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L1"}, {"id": "quote_form_loadingcopy", "label": "loadingCopy", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L85"}, {"id": "quote_form_onloadingchange", "label": "onLoadingChange()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L99"}, {"id": "quote_form_golist", "label": "goList()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L103"}, {"id": "quote_form_onsuccess", "label": "onSuccess()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L107"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_form_vue", "target": "quote_form_loadingcopy", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L85", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_form_vue", "target": "quote_form_onloadingchange", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L99", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_form_vue", "target": "quote_form_golist", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L103", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_form_vue", "target": "quote_form_onsuccess", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L107", "weight": 1.0}, {"source": "quote_form_onsuccess", "target": "quote_form_golist", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L112", "weight": 1.0}], "raw_calls": [{"caller_nid": "quote_form_golist", "callee": "replace", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L104"}, {"caller_nid": "quote_form_onsuccess", "callee": "replace", "is_member_call": true, "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/form.vue", "source_location": "L109"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_customer_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L1"}, {"id": "customer_index_router", "label": "router", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L244"}, {"id": "customer_index_dict", "label": "dict", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L245"}, {"id": "customer_index_views", "label": "views", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L246"}, {"id": "customer_index_keyword", "label": "keyword", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L253"}, {"id": "customer_index_filtersopen", "label": "filtersOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L255"}, {"id": "customer_index_poolopen", "label": "poolOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L256"}, {"id": "customer_index_speedopen", "label": "speedOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L257"}, {"id": "customer_index_selectmode", "label": "selectMode", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L258"}, {"id": "customer_index_total", "label": "total", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L261"}, {"id": "customer_index_page", "label": "page", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L262"}, {"id": "customer_index_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L263"}, {"id": "customer_index_deleting", "label": "deleting", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L264"}, {"id": "customer_index_forbidden", "label": "forbidden", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L265"}, {"id": "customer_index_error", "label": "error", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L266"}, {"id": "customer_index_listfieldsplit", "label": "listFieldSplit", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L269"}, {"id": "customer_index_selectedrows", "label": "selectedRows", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L272"}, {"id": "customer_index_candelete", "label": "canDelete", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L275"}, {"id": "customer_index_allloadedselected", "label": "allLoadedSelected", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L278"}, {"id": "customer_index_hasmore", "label": "hasMore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L282"}, {"id": "customer_index_loadhinttext", "label": "loadHintText", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L283"}, {"id": "customer_index_typefilter", "label": "typeFilter", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L297"}, {"id": "customer_index_levelfilter", "label": "levelFilter", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L298"}, {"id": "customer_index_typefilteroptions", "label": "typeFilterOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue", "source_location": "L299"}, {"id": "customer_index_levelfilteroptionLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_crm_contract_components_contractformdialog_vue", "label": "ContractFormDialog.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1"}, {"id": "components_contractformdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1092"}, {"id": "components_contractformdialog_exists", "label": "exists", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1175"}, {"id": "components_contractformdialog_ensurecustomeroption", "label": "ensureCustomerOption()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1243"}, {"id": "components_contractformdialog_ensurecontactoption", "label": "ensureContactOption()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1307"}, {"id": "components_contractformdialog_isfw", "label": "isFw", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1554"}, {"id": "components_contractformdialog_customer", "label": "customer", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L1718"}, {"id": "components_contractformdialog_fwdefaults", "label": "fwDefaults", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2059"}, {"id": "components_contractformdialog_catch", "label": "catch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2122"}, {"id": "components_contractformdialog_supplier", "label": "supplier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2178"}, {"id": "components_contractformdialog_allowtoxic", "label": "allowToxic", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2201"}, {"id": "components_contractformdialog_allowexplosive", "label": "allowExplosive", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2202"}, {"id": "components_contractformdialog_kept_removednames", "label": "{ kept, removedNames }", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2203"}, {"id": "components_contractformdialog_partyashort", "label": "partyAShort", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2277"}, {"id": "components_contractformdialog_partybshort", "label": "partyBShort", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2278"}, {"id": "components_contractformdialog_namebody", "label": "nameBody", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2279"}, {"id": "components_contractformdialog_contractno", "label": "contractNo", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2282"}, {"id": "components_contractformdialog_nextpartya_nextpartyb", "label": "{ nextPartyA, nextPartyB }", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2324"}, {"id": "components_contractformdialog_brandmeta", "label": "brandMeta", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2439"}, {"id": "components_contractformdialog_meta", "label": "meta", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue", "source_location": "L2501"}, {"id": Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L1"}, {"id": "quote_detail_if", "label": "if()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L684"}, {"id": "quote_detail_id", "label": "id", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L775"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_detail_vue", "target": "quote_detail_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L684", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_detail_vue", "target": "quote_detail_id", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/detail.vue", "source_location": "L775", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_contract_detail_vue", "label": "detail.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L1"}, {"id": "contract_detail_route", "label": "route", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L478"}, {"id": "contract_detail_router", "label": "router", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L479"}, {"id": "contract_detail_userstore", "label": "userStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L480"}, {"id": "contract_detail_dictstore", "label": "dictStore", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L481"}, {"id": "contract_detail_loading", "label": "loading", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L483"}, {"id": "contract_detail_acting", "label": "acting", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L484"}, {"id": "contract_detail_errortext", "label": "errorText", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L485"}, {"id": "contract_detail_approverpickeropen", "label": "approverPickerOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L489"}, {"id": "contract_detail_voidopen", "label": "voidOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L490"}, {"id": "contract_detail_voidreason", "label": "voidReason", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L491"}, {"id": "contract_detail_stickyoffset", "label": "stickyOffset", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L492"}, {"id": "contract_detail_scrollmargin", "label": "scrollMargin", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L494"}, {"id": "contract_detail_moreopen", "label": "moreOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L500"}, {"id": "contract_detail_submitopen", "label": "submitOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L501"}, {"id": "contract_detail_approveopen", "label": "approveOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L502"}, {"id": "contract_detail_approveopinion", "label": "approveOpinion", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L504"}, {"id": "contract_detail_isapproveentry", "label": "isApproveEntry", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L512"}, {"id": "contract_detail_resolvecurrentuserid", "label": "resolveCurrentUserId()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L514"}, {"id": "contract_detail_detailtabs", "label": "detailTabs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L531"}, {"id": "contract_detail_statuslabel", "label": "statusLabel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L540"}, {"id": "contract_detail_statustone", "label": "statusTone", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L545"}, {"id": "contract_detail_contracttypelabel", "label": "contractTypeLabel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue", "source_location": "L553"}, {"id": "contract_detail_templatelabel", "label": "templateLabel", "file_type": "code", "source_file": "/Users/zlwu/workLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_product_confirm_dom_mjs", "label": "test-mp-crm-product-confirm-dom.mjs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-product-confirm-dom.mjs", "source_location": "L1"}, {"id": "scripts_test_mp_crm_product_confirm_dom_root", "label": "root", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-product-confirm-dom.mjs", "source_location": "L4"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_product_confirm_dom_mjs", "target": "strict", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-product-confirm-dom.mjs", "source_location": "L1", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_product_confirm_dom_mjs", "target": "promises", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-product-confirm-dom.mjs", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_product_confirm_dom_mjs", "target": "scripts_test_mp_crm_product_confirm_dom_root", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-product-confirm-dom.mjs", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_plancard_vue", "label": "PlanCard.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/PlanCard.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_contract_dom_mjs", "label": "test-mp-crm-contract-dom.mjs", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract-dom.mjs", "source_location": "L1"}, {"id": "scripts_test_mp_crm_contract_dom_root", "label": "root", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract-dom.mjs", "source_location": "L4"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_contract_dom_mjs", "target": "strict", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract-dom.mjs", "source_location": "L1", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_contract_dom_mjs", "target": "promises", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract-dom.mjs", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_scripts_test_mp_crm_contract_dom_mjs", "target": "scripts_test_mp_crm_contract_dom_root", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/scripts/test-mp-crm-contract-dom.mjs", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "label": "mp-crm-product-confirm.test.ts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L1"}, {"id": "utils_mp_crm_product_confirm_test_opts", "label": "opts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L7"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "strict", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "node_test", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_buildproductconfirmoptions", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_test_opts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_test_opts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L22", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_test_opts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L39", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_test_opts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L48", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "label": "mp-crm-product-confirm.test.ts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L1"}, {"id": "utils_mp_crm_product_confirm_test_opts", "label": "opts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L7"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "strict", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "node_test", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L2", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_buildproductconfirmoptions", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_test_opts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_web_admin_src_utils_mp_crm_product_confirm_test_ts", "target": "utils_mp_crm_product_confirm_test_opts", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/mp-crm-product-confirm.test.ts", "source_location": "L18", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_pl_plan_cert_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L1"}, {"id": "plan_cert_index_tab", "label": "tab", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L141"}, {"id": "plan_cert_index_keyword", "label": "keyword", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L142"}, {"id": "plan_cert_index_appliedkeyword", "label": "appliedKeyword", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L143"}, {"id": "plan_cert_index_datekind", "label": "dateKind", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L145"}, {"id": "plan_cert_index_dateoptions", "label": "dateOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L146"}, {"id": "plan_cert_index_readyoptions", "label": "readyOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L153"}, {"id": "plan_cert_index_filters_toqueryparams", "label": "{ filters, toQueryParams }", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L158"}, {"id": "plan_cert_index_sort", "label": "sort", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L161"}, {"id": "plan_cert_index_quickpanel", "label": "quickPanel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L162"}, {"id": "plan_cert_index_readykind", "label": "readyKind", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L163"}, {"id": "plan_cert_index_datelabel", "label": "dateLabel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L164"}, {"id": "plan_cert_index_readylabel", "label": "readyLabel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L167"}, {"id": "plan_cert_index_datedropdownoptions", "label": "dateDropdownOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L170"}, {"id": "plan_cert_index_readydropdownoptions", "label": "readyDropdownOptions", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L173"}, {"id": "plan_cert_index_extrafiltercount", "label": "extraFilterCount", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L178"}, {"id": "plan_cert_index_filtersopen", "label": "filtersOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L193"}, {"id": "plan_cert_index_uploadopen", "label": "uploadOpen", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L194"}, {"id": "plan_cert_index_uploadid", "label": "uploadId", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L195"}, {"id": "plan_cert_index_canupload", "label": "canUpload", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L196"}, {"id": "plan_cert_index_canready", "label": "canReady", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L197"}, {"id": "plan_cert_index_canprice", "label": "canPrice", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L198"}, {"id": "plan_cert_index_permissionerror", "label": "permissionError", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/pl/plan-cert/index.vue", "source_location": "L199"}, {"id": "plan_cert_index_permissionsloaded", "label": "permissionsLoaded", "file_type": "code", "source_file": "/Users/zlwu/work/0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_web_admin_src_views_m_crm_quote_versions_vue", "label": "versions.vue", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/versions.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff. Show more