diff --git a/AGENTS.md b/AGENTS.md
index 25994e644..ef7c9513a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -40,7 +40,8 @@ Full specs: `openspec/changes/zkxw-engineering-standards/specs/*/spec.md`. Below
- **Sidebar menu icons**: First-level menu items SHALL show icons before the name; second-level and deeper items SHALL NOT show icons (text only). Implemented via `level === 0` check in `SidebarSubmenu.vue`.
- **Tab bar (WorkTab) icons**: Tab bar tabs SHALL NOT show icons — only text titles. `meta.icon` is for sidebar only.
- **Form field annotations**: Field hints (e.g. "数值越小越靠前") SHALL appear as small text after the label via `字段名注释`, NOT beside the input control. Style class: `.form-label-tip` (12px, `--mint-text-muted`).
-- **Attachment upload card layout**: Upload trigger SHALL be a card (140×96px, dashed border `#a7f3d0`) inline as the first item in the same flex row as uploaded attachment cards; select file → immediate upload via `fetchUploadAttachment`; preview/download/delete on cards; hint below the row. Authority: `ProtocolFormDialog.vue` «协议附件». See `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md` §「附件上传卡片化布局」for full Vue template + SCSS + interaction rules.
+- **Attachment upload card layout** (`web-admin`): Upload trigger SHALL be a card (140×96px, dashed border `#a7f3d0`) inline as the first item in the same flex row as uploaded attachment cards; select file → immediate upload via `fetchUploadAttachment`; preview/download/delete on cards; hint below the row. Authority: `ProtocolFormDialog.vue` «协议附件». See `openspec/.../ui-conventions/spec.md` §「附件上传卡片化布局」.
+- **Miniprogram photo evidence upload** (`miniprogram`, baseline): Photo/album images only — `uni.chooseImage` + `uni.uploadFile` (`name: file`, `formData.folder`, Bearer header) → collect `data.url` → business API submits `imageUrls`. SHALL NOT use Web `el-upload`/`FormData`+`request`, Base64-in-JSON, or document pickers (`chooseMessageFile`). Upload path SHALL be in `API_ENCRYPT_EXCLUDE_PATHS`. Authority: `openspec/.../ui-conventions` §「小程序拍照凭证上传(基线)」+ `api-conventions` §「小程序拍照凭证上传接口」; reference pattern from `sqxp-wx` delivery/supplier evidence upload.
- **Button icons**: All `web-admin` `el-button` with visible text labels SHALL include a leading `` from `@element-plus/icons-vue` (explicit import). See `openspec/.../ui-conventions/spec.md` §「按钮须携带图标」.
- **Sort fields (sort_order/orderNum)**: web-admin 列表、新增弹窗、编辑弹窗中默认不展示排序字段。后续根据功能需要自行添加。
- **Status/conclusion/type fields as Tags**: All status, conclusion, and type fields in table columns and view dialogs SHALL render as `` with semantic colors. Tables: use `` in column templates. SystemViewDialog: use `tagType` property on `ViewField`. Semantic colors: `success`=正常/启用/已完成/通过, `warning`=进行中/待确认/已过期, `danger`=异常/已作废/拒绝/不通过, `info`=待执行/待生效/停用/默认, `primary`=特殊分类/来源类型. SHALL NOT use plain text or custom ``. See `openspec/.../ui-conventions/spec.md` "状态、结论与类型字段使用 Tag 标签展示".
diff --git a/backend/app.pid b/backend/app.pid
index 56c0be9a5..957b27947 100644
--- a/backend/app.pid
+++ b/backend/app.pid
@@ -1 +1 @@
-74168
+27977
diff --git a/backend/graphify-out/cache/stat-index.json b/backend/graphify-out/cache/stat-index.json
index fd0b3a077..94f176f0a 100644
--- a/backend/graphify-out/cache/stat-index.json
+++ b/backend/graphify-out/cache/stat-index.json
@@ -1 +1 @@
-{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6958,"mtime_ns":1784032424448146538,"hash":"c2fb44aedb880733de28afa7f92d2c1c681eb600f821bbeba2bcda3933e5bc37"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4ea2fe128de80fe321b0c3c531fea7a88"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py":{"size":36012,"mtime_ns":1782699345666495916,"hash":"01f974f182660ec4f215bcacc31eddadd7899d60d95aa19a61046579a994512a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/__init__.py":{"size":2186,"mtime_ns":1784035957226402359,"hash":"5af681963b36af57eaca7fdfc7f707255008ffba18df1a8c2d5903f58fcbf159"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/base.py":{"size":2090,"mtime_ns":1780108930807298966,"hash":"fda71bbe87dcb29c6f0ffa9337e9f39c2c06d912df6cb75d521f0e87c7c73c6d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/auth.py":{"size":1315,"mtime_ns":1780108930808197763,"hash":"018b82e42bbb055250549c54ba700ee01cf28cd753397d6c162f9e60185908e5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/user.py":{"size":1911,"mtime_ns":1780108930810184774,"hash":"3d3c159018cab6ed1cd3de7034ffb1fe77a272b9099901a1882d07aa15fc90a0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/__init__.py":{"size":1159,"mtime_ns":1780112973062398026,"hash":"4e8b0bda6680e2c8740c380b96eedb859ddc584a81bad568a18ba6beb001e480"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/menu.py":{"size":816,"mtime_ns":1780108930808681891,"hash":"17e35f2d7bf95cac00d81357f69fdcbe849e855daa05570c3302de8182b4df11"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/attachment.py":{"size":328,"mtime_ns":1780108930808144846,"hash":"300b74c51c66cb0fbf8c9f4d84a4b7bf29dcc9b6a67d922f8e1d0df05f9fc5bf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/common.py":{"size":555,"mtime_ns":1780108930808426681,"hash":"caa0c03398e54017b25cca2a16cf8d232a59739893a16e4c33b51a5316d8964f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/workflow.py":{"size":3949,"mtime_ns":1780108930810245983,"hash":"e5aed7afcf903c838a35cdaf2230a93d221c63f5c6baa1497e455afac8587a9a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/leave.py":{"size":1449,"mtime_ns":1780108930808614098,"hash":"c54098a69bd4490290dd8d7809ed8764a3f38e8c38692eeef2b505293cdfaeb8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/sample_code.py":{"size":12322,"mtime_ns":1780114002126415047,"hash":"5b74838b3920f9182d092f8a1b721697f1a62e8b90728b9f54101dbb96baa40e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/post.py":{"size":5164,"mtime_ns":1782688288640487823,"hash":"370a7c2b5de64c6b72f98bcf7fbbc166ba2a7f5673c6e1426b10bcc88220bb27"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/user.py":{"size":14214,"mtime_ns":1783851628178366109,"hash":"aeac623e3020fee4cdd01e463ced23e4e03539a17c5b57dcbd9d88aac5a4fb4f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/dict_type.py":{"size":5537,"mtime_ns":1782688289303121533,"hash":"f4edc5824466d0096b3b70aeea7b4460a9fa54ca0929421759b625b60c56c505"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py":{"size":5659,"mtime_ns":1782688281816798167,"hash":"0192db8ac9c3de42fe8ee58324c77111e152bf4c35fd15c0b16467fe243712f0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/biz_no_generator.py":{"size":2534,"mtime_ns":1780108930726090799,"hash":"e6bd375987e47f6ee80294686db64377938a286767bb28324f3eda2ad39ad546"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/__init__.py":{"size":169,"mtime_ns":1780108930725878047,"hash":"340e3c08174d61829012879e684f6166803b88c819c58b54a16a11d210dea85e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py":{"size":9417,"mtime_ns":1782688282481859519,"hash":"008550fe8efce5310afd3d41d944cee4b68d71763a587583f8832855480477f7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/menu.py":{"size":7600,"mtime_ns":1782688282878700273,"hash":"36951a77088782d80500bfa6266b46a564a670e10b70af7a28cdee8c50741098"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/site.py":{"size":6525,"mtime_ns":1782688288935534527,"hash":"a7bf593eff76d08b338f48f9a035fa1493f4ca88867bf9fdf4e9af06b6165759"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/api_request_log.py":{"size":4562,"mtime_ns":1782699354647000111,"hash":"f233bde579c4f6c7f2fd6030878c5946fb5dd5e247efaf4fe80d3f48622c19a8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/dict_data.py":{"size":5418,"mtime_ns":1782688289691482227,"hash":"5bd3fd93c60956dab0268e9f4e6cf315d0a9c9656d649f4abb40858ba1d32911"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/ethnic.py":{"size":1012,"mtime_ns":1780108930727448890,"hash":"7de1eb94b9fbb897a7b362ffdb5df5ab1020aa7247c7c37a2a2a2aa5cdbac8ad"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/workflow.py":{"size":25052,"mtime_ns":1782688290021142467,"hash":"6ca7e3b79e91356a050592b768d7a6749e1bfdb2841a208c7235899603368aec"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/dept.py":{"size":6604,"mtime_ns":1782688283203296227,"hash":"ebb38fcd527fdac4dcbf0632ac926e4cbe95dddb5fab79c3142b010e7a48ea7e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/base.py":{"size":5360,"mtime_ns":1780108930726010257,"hash":"49fdea023825ea249923b1cb02d353dcede1850158d3555a2082887925e69b9b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/leave.py":{"size":4765,"mtime_ns":1782687678100096646,"hash":"847095009bddac2d77dcbb485bcc57cbe168ff874989c197f49f2a28675d2525"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py":{"size":12682,"mtime_ns":1783858998986861832,"hash":"b98ac3f3bc95c2b42c481ac9ae9137528e55845d425e5d2184827dc424304e42"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/__init__.py":{"size":665,"mtime_ns":1780804649232229595,"hash":"f720195209c1783d8b2526220a086c2ae2769d76a12bd7df74136f5432c7b75f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/menu.py":{"size":30932,"mtime_ns":1782790854866245387,"hash":"4e9de047e67ff365d17ac28d30e1baa72cfa3c651caa1b9776e225fc07da0e58"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py":{"size":50055,"mtime_ns":1782855138852859245,"hash":"a5e74057222e3359f6077b2fd05b2db4d058ec2702243cc449bff8d448d44a05"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/workflow.py":{"size":21202,"mtime_ns":1782790854865038715,"hash":"3950dd8d9855f5ed33a0e15883f7e442c4e26bc51672209b1e68ce0d0c9ce399"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/leave.py":{"size":6916,"mtime_ns":1782790854865571301,"hash":"6e03239433bbaecc54f72fa2aba2578573ae6c7fc881f287ef57dafed1a0f1b5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/workflow_callback.py":{"size":3584,"mtime_ns":1780108930810395025,"hash":"a033c13c7bee7f4b636198c1efa47f2abba0d890657e52b0b7057568d4d83a52"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/workflow_engine.py":{"size":14042,"mtime_ns":1780113992914903818,"hash":"5c06a59c734f598d5c7ebe95d30f048ba3a97302b938bc9320d97e5a053c7aef"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py":{"size":5592,"mtime_ns":1783858473640363111,"hash":"04c2cd67a7ec8190aa7a9db717a722cd347a52279ac3ef5bd284777f0698a904"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/ws/__init__.py":{"size":26,"mtime_ns":1780108930810617026,"hash":"d4357444e9b5b0a8593000fb6e4dc37469ebe008e675bf42ec0d55d463f88fc6"},"/Users/zlwu/work/00code/shanxia2.0/backend/README.md":{"size":2752,"mtime_ns":1780114581291841672,"hash":"f5def8d25af0556a443c31a73e81f958883b60cfed8b31fac457f2832c2b6d58"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/crm_models.py":{"size":66467,"mtime_ns":1784119016761711479,"hash":"c94681aea5f0ae5f08969088f1bb57ca4fdb5dcf586161c16e2455aab1ca162c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/__init__.py":{"size":0,"mtime_ns":1780201410213315055,"hash":"a6d57250b43cbdecb56b7a9d55d29642b808be368733e14b7df2bd196f54d64d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py":{"size":7820,"mtime_ns":1783820526204158517,"hash":"b8d626eca1b0fc6b0216a66afb1cf234155310d3077d940d5158111f873a414b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/__init__.py":{"size":0,"mtime_ns":1780201410210254759,"hash":"9b8fcaaabdc85883c9ef392b596b60db0bd3b2f1623fc8012e55003f234a9875"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py":{"size":25974,"mtime_ns":1783820522540308906,"hash":"6d1fb7bc06a4f16c729b1b226377ffa38b9b47bf1cc75310dcefbb8f6e480574"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py":{"size":151289,"mtime_ns":1784119033682636742,"hash":"ceaa64369ef66891310f391836c580beefd5a69854b3a5a0539a419744a311b0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/customer_pool.py":{"size":9646,"mtime_ns":1780202350147822837,"hash":"4f0ab87e7a12534011b69f58079ca206d5443eee594849ab4aae728bb847de0d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/tenant_context.py":{"size":693,"mtime_ns":1780201410210198550,"hash":"0a810658d894d35b9e981faa1448e1d02d617ecbe563bd711f6ce5a8f9db1861"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/pool_rule.py":{"size":1848,"mtime_ns":1780202212734233591,"hash":"b0331e17e801d206fba2fa89df1a66d1758d850dc5547e076218e16c8211a3e8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer_pool.py":{"size":1340,"mtime_ns":1780202198698231150,"hash":"7a6b76e0e39ada0783dfbee92280e9f06299b6662b08abb13ab1b0d9c70fe370"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer_follow.py":{"size":3716,"mtime_ns":1780201410213934514,"hash":"3979cecd62a4f2ee5dd925639572208e36b397620d91a9ce49a59adfcb2a9d55"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py":{"size":13811,"mtime_ns":1783823672041317102,"hash":"b91167282158aeb11046dccc4bf2f29a13307a0413b3d7b3c860b17d37f8ad4a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contact.py":{"size":4856,"mtime_ns":1780742840975948859,"hash":"60c714cc254d70f39b43205bbcb7fb4c563e807ddd070eefaaa952ae96048377"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/pool_rule.py":{"size":8816,"mtime_ns":1782687680547203228,"hash":"779f7f46e7ef8bfba27390d56fa071dfd3a8446d9d3b3361003b1c239f7169aa"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer_follow.py":{"size":8121,"mtime_ns":1782687665525772610,"hash":"ec67fc7667847017a8d8b5a276f5c75328bda7fa360746d80c7890de3f6d4b76"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer.py":{"size":23825,"mtime_ns":1782687664490640155,"hash":"5d8934ae6318de87b0761686fe491a7ec2be02792a85d84308c383af3193fa8a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py":{"size":10482,"mtime_ns":1782687664918872966,"hash":"f4e3a684dd7a6c7c927e10e8e1e0c108c151b963777b85c5b74d7e7871f3a438"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/generate_dict_reimport_sql.py":{"size":10692,"mtime_ns":1780201410245078685,"hash":"a4faaf61f246dad04ec3933083ab4348e5d2987ae1ed34b1f6d8f19535642a05"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/statistics.py":{"size":412,"mtime_ns":1780201410215906642,"hash":"fb1957d0fe87f22c37ba5bdaa5f3bf2bb4a40685a21a05dce98326581bb4844b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer_pool.py":{"size":3725,"mtime_ns":1780202237342131933,"hash":"0f26ac2bcb585241f25302787184db4db422934e5d896ca25bbe7efaf61d92c4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/statistics.py":{"size":5642,"mtime_ns":1782649310467813967,"hash":"f954dc5479f42a52fdb3f05ad9076fdbe43e569bae85299fca6c8f37c7323cde"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/invoice_info.py":{"size":3104,"mtime_ns":1780406386885207867,"hash":"a59df3aed2c976904e3b5325fedd1621435b322645aa71349f3356e0cdc17424"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/oss.py":{"size":1584,"mtime_ns":1780201410212966513,"hash":"9349799b02a3531d8e1b46f9f70d3b6630afd7306663c779bd11f25448a16013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/invoice_info.py":{"size":7446,"mtime_ns":1782687666050313280,"hash":"e0f2069c43cd272e5f061d30b47a42140ac37aa26379985fc781f8a3851aa44d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/opportunity.py":{"size":9391,"mtime_ns":1780242127112297110,"hash":"2e068961824a1d3ec0da0a94a3c494b63582b3251e5f4e030ce3e12897e85002"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py":{"size":10565,"mtime_ns":1784118885429481285,"hash":"45d9dfeb58bda22227fcd242c738086431616466bb8ee9f1466927d75d4bec48"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity.py":{"size":42231,"mtime_ns":1782687666592409903,"hash":"75f54438002e070076fe37ec2211c5fd67bf4aebd2607ac94db15e30d6f634d8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py":{"size":92433,"mtime_ns":1783763318740360571,"hash":"b2362db2f2df750afc1c2263f5e6ac1ccd5daa8b93ad5650d56028f452597837"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/monitor/__init__.py":{"size":1,"mtime_ns":1780242127108406092,"hash":"bb5b318d3cb224c50a2e0286e145f89ea7278437437fd17d7c865d67ad148076"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/monitor/operlog.py":{"size":1069,"mtime_ns":1782699353340106185,"hash":"b9f0a382f8990f69391b26bb75b4538d141d6adadc4260fecaa8b33cb44c063d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/__init__.py":{"size":1,"mtime_ns":1780242127112307360,"hash":"0592c62ce1172d93aca98cbd9aaa54b9c2668022abf3581249516472db312293"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py":{"size":6673,"mtime_ns":1782699352542520022,"hash":"0586702734f693ee099af38d407c71cc0a840153f93174699c5a5201e0a9acbe"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/monitor.py":{"size":2789,"mtime_ns":1782790854865860510,"hash":"5c13164e49d7ecb6836cf8e65a705fccd518399b85d3be2318c9581a98bacd51"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/clue.py":{"size":9417,"mtime_ns":1780215022189075651,"hash":"2676afaecac4bb6da185674a9421dfbd40834ca21520adf66bb6b175fb728677"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue_follow.py":{"size":8493,"mtime_ns":1782687674053739962,"hash":"54881f876d1842ee4a344d049ea4b5bac18b243535338077cb3eb3ec8b60e9c1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py":{"size":25820,"mtime_ns":1783155705954031080,"hash":"3526af7e7e79d505911690355890ccdb3a1c30d302534ffdc8626a454cc2499b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue_statistics.py":{"size":11229,"mtime_ns":1782649305846991404,"hash":"fbc3b4f3356256787839cc5b2a6f8dff41dcf9eb27c791e427e816e3ba14e094"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py":{"size":10069,"mtime_ns":1783821986040417425,"hash":"06c9f3deab3472b20429c12a618ae2d4d6efd3afa2353ac44cd52651a596af4c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_product.py":{"size":10346,"mtime_ns":1782687669972916544,"hash":"40b497a41ce4003e128dc305abedc54e01dbaad8a8f7bc1ecec1662a3953a1ed"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/crm_export_columns.py":{"size":9594,"mtime_ns":1780743830255260394,"hash":"b20594507aab09cebc3d540acad04547bf780cac9c885187138f5d64718ed513"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/__init__.py":{"size":0,"mtime_ns":1780242127043319167,"hash":"a66deceefc987198863f61b8aa1b5715483a69c4a15ef47cd5586c5d8e429b97"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/excel_export.py":{"size":4411,"mtime_ns":1780242127113233948,"hash":"7fbde3fca845e4705487e7f801caee308bcc0c99a9da7d0c99681a96eaaf6076"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/supplier_info.py":{"size":3679,"mtime_ns":1780242127112269527,"hash":"8b797e9dee6ac7ff806a8ea23bef5fedce648502ea23287e1b417e9f96d22c2e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py":{"size":6916,"mtime_ns":1782687684650105460,"hash":"0b66714e36609688f7ce79ecda0679d20df115d2d64a0dff1cc0fc76df7dca6b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_product.py":{"size":13126,"mtime_ns":1782650413581368849,"hash":"cd2fc23852ead3c4db19e34bc33c1560fc5f5d4cd7cf51b61def8bf37c44c853"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/quote.py":{"size":5961,"mtime_ns":1780745856749183842,"hash":"29c52786df396aededd0850e3c8a3a36f3fb6a952af03adbe1768dae212b6804"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote.py":{"size":48137,"mtime_ns":1782687675213433843,"hash":"a2ff5c5b74a2becd0949aab6aea8cf63440cce2c0bb22e1a1661463e0b2e963d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote_product.py":{"size":10537,"mtime_ns":1782687676011228777,"hash":"57d9d5b4fc2bc8b68c0388a4b8a9c1322539ed7e538764fc23521916429e07c5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote_share_log.py":{"size":4277,"mtime_ns":1782650254630170341,"hash":"5bfbf7ce2918f9ecdbbca6fdce17d200cd52cdc71b2dfb44ac126a36ca0ed734"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote_approval.py":{"size":8816,"mtime_ns":1782650314936479798,"hash":"736353b8c2c5073d260e007a166234bd7d57958bcc738c56a1d39cfb22381180"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/crm_quote_pdf.py":{"size":9877,"mtime_ns":1780242127112382277,"hash":"fec04892fccccc3601fbeb012c92c77110496910be44102de40b4610148fa58a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py":{"size":25633,"mtime_ns":1783825326810765171,"hash":"c51b70766aae85dbc537bf5b48a2c350c73ea8e93bcad2a9660d8d45c6d0979d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_approval.py":{"size":13958,"mtime_ns":1780763324103831772,"hash":"3d7fbbb90728568a2577e52d8986ccbec8f599d04a0ef9e330939d1fe729bc98"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py":{"size":29547,"mtime_ns":1783825526614169736,"hash":"95969972c0773528b91d9728d303feae5ca7187d89a49495d5129dcedffc19dc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract_template.py":{"size":2897,"mtime_ns":1780229101351431207,"hash":"935adfc7659e31201cd83ba8387bf6abcb39e7cdc63fa4692d699ed874cf113b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract_change.py":{"size":2496,"mtime_ns":1780229100306468735,"hash":"05af42f088f6cc400c1e3445d4a2c96670edf482328b361aa4f231230d958729"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_template.py":{"size":11858,"mtime_ns":1780229131746659196,"hash":"b9bdbe6a33c933aefcb25cc57e94b2fd2dc75f8e5822434f3912aa0153dfca00"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_change.py":{"size":15985,"mtime_ns":1780229123819597506,"hash":"771b110d3d136c45106dd61171c724c559b5b4bf572ea7b8c2b648f0bfe629c1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_sub_routes.py":{"size":25165,"mtime_ns":1782650354912882614,"hash":"b2733fd74cb7e6cbe6518fc5a5700e387601afc087be09f395f3c61372099847"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/attachment_biz.py":{"size":1122,"mtime_ns":1782219602888987090,"hash":"fb9fea8983524e3891357016b6546d982c4a82cb782fc01425f70472d4338b6b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/bx_auth.py":{"size":1473,"mtime_ns":1782650780925056463,"hash":"678d9ff99b5c89575ef47e5c9e61d495f364917fd6cfb2b322c4d5035d3ba359"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/bx_date_range.py":{"size":2363,"mtime_ns":1783745556349035158,"hash":"e8b487a9bb4072356cae7b1b38496c441f9a1534d6807798448e6de97073d669"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/bx_codegen.py":{"size":4679,"mtime_ns":1782003396630684570,"hash":"bc3f798cfbc359e0327d72e3c0513fe9cbaee196d0d85bcf6567cf5df4211d97"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/bx_models.py":{"size":16644,"mtime_ns":1782003187247224106,"hash":"37c7372f38d76f1020d31ce8ca9e7f0cb3bc3c204a50deb092b494ce8c983fbf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/__init__.py":{"size":81,"mtime_ns":1780750556881734563,"hash":"07c3422f745afbff1c8162b3782fbb4b8de65084e7e9bbcf45abb9d433d65f98"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/common.py":{"size":1547,"mtime_ns":1782003187605255900,"hash":"a75ca6a0d92f93408eee00cc5053206d28a545512e4cd0f186be9e00e3346ab1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/__init__.py":{"size":777,"mtime_ns":1780751360180065853,"hash":"3525eb716c238682a07a116818d318b8a494fb24b9cefca5114d30bc50d095a5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/expense_item.py":{"size":1668,"mtime_ns":1782688293668634639,"hash":"ac07c7b05fe990e743dd5b4e96a85b59431479ca11b98cce7c340a93dbe99ce1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/workflow.py":{"size":1677,"mtime_ns":1782688293973889288,"hash":"f683e5bb7a45d0bae3b3f262c201453155eaaaf2c8d7f14f567a14295a68d2e1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/expense_item.py":{"size":3362,"mtime_ns":1782650913604714906,"hash":"c6a142011870516822cf11cff16a86c56908f00680eb7c6219c8441c9b63632a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/workflow.py":{"size":4984,"mtime_ns":1782650913326708063,"hash":"84285aab7499ce9280156c5814b2dc4fff42841803dd673cc6bfdc6eed498283"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py":{"size":4366,"mtime_ns":1782650887981843546,"hash":"15321fe1df77a555af579474434e3b401bd042555d19ea81a066ef754915612b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/__init__.py":{"size":34,"mtime_ns":1780750594045301798,"hash":"583b12a6d2c9fd4411b652741e73324df9256138946ee873a0ed03169f8dae2a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/workflow.py":{"size":5446,"mtime_ns":1782650919964956073,"hash":"1d72733147e0ed997094d7f92d1c1f5e1e108093a50e8e523b3b016c3dd676e4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/base.py":{"size":3931,"mtime_ns":1782687617846731919,"hash":"dba80b8f4e26bc59efd2b2aad6bd430c0315133a98bfbbe1b93113b60baba8b8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/attachment.py":{"size":525,"mtime_ns":1780750871480000584,"hash":"f287214c72345e41d9d122ca55b4d3e279635d99cbf10ed530de0ac32374f31c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/advance.py":{"size":4603,"mtime_ns":1782688296833335474,"hash":"74b6e8bf6906518f80f16f218f1af74d2c356a6f392a4e8a69466f4179ce0b92"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/attachment.py":{"size":5758,"mtime_ns":1782790898570675189,"hash":"e88b9181e7c1a839074656ce2260cd6f693821402a24f08dd1c0ef3c160f8fa8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/advance.py":{"size":13370,"mtime_ns":1783745567004734594,"hash":"3097a75d86d09951d7de7df10a32f5aedcf6ce11279bdf6dfe0005ec9d8a7be1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/attachment.py":{"size":4582,"mtime_ns":1782650868959755751,"hash":"3a6480cd051bd1af733a67e5babcd64f3f74cbe230d72364b599cde8b0c67a5f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/advance.py":{"size":24037,"mtime_ns":1783746967707088251,"hash":"4834348d099b32b70d14e1b56279dbb385f42519646351d8cb220da8c5fe27f1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/storage.py":{"size":3656,"mtime_ns":1780750869826924187,"hash":"ac98980bd5b6cbab6a9394b6debc678b050cf4df969eccbc5f2d419b615937cd"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/reimburse.py":{"size":5638,"mtime_ns":1782688293517624966,"hash":"aa47d8e87982a4077ff067dff2955db9eb67171d6368bc636c1eea17ea7682f1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py":{"size":15431,"mtime_ns":1783747621462206303,"hash":"dae4bfc4be69ca2f3ee8fd2e9e3bd6053a2904c8f7a0c83e536f2dd71874f918"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/reimburse.py":{"size":34743,"mtime_ns":1783747614591546341,"hash":"ba8f3ff0304a6668bde3c2e6c3e15427153389f7320b943fb261d1d947b41d1e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/payment.py":{"size":15715,"mtime_ns":1783745581266319098,"hash":"0d0b8823013a63f91baee0ea19b56f203e70fb1e450f873cfeb634a6cd017f54"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/approval_cycle.py":{"size":1296,"mtime_ns":1780758200904804714,"hash":"308c087d94ac253bfc7e2d5455a06ae8ceac034adda41b5c61dd315dc167efaf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/db_models.py":{"size":8110,"mtime_ns":1782219570945208920,"hash":"249bb0971c263436095a6324457557b6ac5cfc3ca5ac9bc9499a9b4cd21d42bf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/__init__.py":{"size":544,"mtime_ns":1782011200096708592,"hash":"822060230e155b8704feb447b908c226d4c53e62b3e6df65c89dc3c7a0e6e07f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/__init__.py":{"size":0,"mtime_ns":1780805614160787709,"hash":"2cb780fb722e6dbf2ecea6f6fc19345fb453e1a3c40694ba0ff7465eb7cf7d33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/base.py":{"size":2754,"mtime_ns":1780802262496001324,"hash":"d2fd88aa376aa5914125381cf61a4ae0f343657d6bd7db1f9fe1254e7aba5b2f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/vehicle.py":{"size":591,"mtime_ns":1780802980783200520,"hash":"d87e4262b344e02a7be21534e382e7b75f5543f599d673ddc046134ad0e8ba4e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/__init__.py":{"size":84,"mtime_ns":1780804619867600032,"hash":"5193d61f7204f825551f083e79722805fb2abfd953bb552e80950bb3ff9784a1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/vehicle.py":{"size":3780,"mtime_ns":1780802985736476363,"hash":"aeb49bdd49cb5091c37275b856396e75bcd24557e3a1b58ef668910b1b0aff64"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/vehicle.py":{"size":4776,"mtime_ns":1780802999039302982,"hash":"cf32c66499ca86285561b573d222837ecc76c3fa51b0091b802485fbde8e2bf2"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/plan.py":{"size":1788,"mtime_ns":1780803836065964235,"hash":"30452b2f7fa19f5677475ce08b3e355dae2440d80d2e9049f8417de01d822863"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plan.py":{"size":4167,"mtime_ns":1780803836065953735,"hash":"acac159dedfacb36f02982b700b2a7c7a8fbb4644eb89c3b0c13871941917864"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/plan.py":{"size":9619,"mtime_ns":1780803836065902943,"hash":"e311f2b8b5e7785db764216fbecfd2830a11569ccb827a00c8bd3a2ba3804383"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/inventory.py":{"size":3948,"mtime_ns":1782219604456801044,"hash":"19ea80e59a1309518ad6183a6b0b8751401f4724d7b5101d47defc2417ec36c8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py":{"size":16423,"mtime_ns":1783156035144390497,"hash":"901602a8fe362aec9b89ac6f2ee578f1eeb105fcf12a1a6c3ff4ef98f4ad1ef3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/inventory.py":{"size":10663,"mtime_ns":1780803878293678536,"hash":"e38abdd78010b0e96dcb65ed826de03a752b40e3355546ed06a4b07640cf7013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/weighing.py":{"size":5347,"mtime_ns":1780935341753643087,"hash":"f734c66269b713a840d7ce6c1c6150bd2dc00b114b09963362202d119c82301e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/weighing.py":{"size":14940,"mtime_ns":1782687688848020877,"hash":"71d143eb05d0d3f785f5962892bde64dee3c2937139c975b52af62628f308d46"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/weighing.py":{"size":24042,"mtime_ns":1780804029884092079,"hash":"bc9d5cb59bf90597db0e0a1c9afcef5c2b429c3945c92be34bfdc1e77d26ba28"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_auth.py":{"size":2500,"mtime_ns":1782790853674329877,"hash":"4988b71d4e54b326c333872b34743914d67b15fb753c1b0231e9b00b366458a4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/plans.py":{"size":4876,"mtime_ns":1780935329287069452,"hash":"474cb68905249cd5a6237443a412d49cb28af2e0cec61c9bf5c34ec54db1240a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/master_data.py":{"size":619,"mtime_ns":1780925759582999052,"hash":"6f6b2c2e1478efce74b8c6e6fb058d914b8b79adfadf2fea2d91cb55d3f9cdce"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/vehicles.py":{"size":4748,"mtime_ns":1780925781827569662,"hash":"8f8b32dfbf4046cfa0b6645d211732596f7027d46244d9c50132425220065483"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py":{"size":17098,"mtime_ns":1783156035873015369,"hash":"bd2c8344e2ab765ceb2a2c91f5cf2facea319ae66c9097f5468a7f5d753b67dd"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/mappers.py":{"size":6955,"mtime_ns":1782219619253708711,"hash":"b9e97f66a0a248d06c2f960c3f5ac4fb3369cc976c16709227dee0cc1513d221"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/weighing_logic.py":{"size":4110,"mtime_ns":1780804818442134500,"hash":"468ded88a0a4a397a96884580c88c59291c0b6effa897f08f1b3fea7857f9191"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/dashboard.py":{"size":514,"mtime_ns":1780804990870580151,"hash":"f2c1b1014d0d769abf32dab3783de65fe7fe327c956ffbfe44adce94542f8892"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/dashboard.py":{"size":6576,"mtime_ns":1782651340330906738,"hash":"753703fbbe8ddc02d8fbdae78df4abd1448f5ce5878e2f0390111360fdc300b3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/db_auto_plan.py":{"size":949,"mtime_ns":1780805095845033710,"hash":"9f7513cd2e74f9ddfe5cbee4ae58a40ee7a58aaaff051d590b19a6e1b08bb46a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/plan_helpers.py":{"size":386,"mtime_ns":1780805092108105489,"hash":"5c6ec7612b204e1dcb65c5daf03fa7f8ffa7710cd1ace63be4f7ca00cbf318b8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/auto_generate_plans.py":{"size":4699,"mtime_ns":1780935400833764213,"hash":"d0760cb8155d60e18040bcf6fe41e6964640a3dffd6f4b7bb68678c9f31ab29a"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/run_db_auto_generate_plans.py":{"size":1043,"mtime_ns":1780805102157096922,"hash":"3cd8f15b677f21b2873ff384fc0581da264a53ee4318da3c8262101156e3225b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_dibang_data.py":{"size":20215,"mtime_ns":1782011225157870526,"hash":"28a49a423a784e70bec424b702f3d58f69b6f847153c061414efdcdabb660d0e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/inventory_id.py":{"size":2574,"mtime_ns":1780934632326984469,"hash":"c5269cbb273d987c296143b83f1287762ce67d7ddeccfb0a72bc70234bad9f12"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/product_helpers.py":{"size":2106,"mtime_ns":1780935268573313971,"hash":"cea6b0697eb64081a79d18ab95f0f0477714505a7aef82a41672197b15fc8b14"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_db_inventory_id.py":{"size":586,"mtime_ns":1780934647395760353,"hash":"71a9617d5d0f2c23416686eded204b4c941b47afc3fd6e3facf0c75401e883d9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py":{"size":61386,"mtime_ns":1784032430468998611,"hash":"e23b4653b806a1917eba187d8a3584dff598ad74bc21c8295c179abf0bcfe17e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py":{"size":22053,"mtime_ns":1783754594278544472,"hash":"f3a69a96b7916cb34e23e673b3155d83f7d9698b2582cfae96dedc392da8e6b8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py":{"size":17945,"mtime_ns":1783211732481443217,"hash":"477ebd0c4c084cf8bccd10865e4703b6b34829d30b791c32232d96c1bac2947f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py":{"size":5012,"mtime_ns":1783242582112287271,"hash":"1dced39f250919efc4aa798daee1efef3adcca054c7483843536c455919ff106"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py":{"size":11275,"mtime_ns":1783213205154827373,"hash":"557f9e9f06ab6f18eb33f54a579fb8ab0f6992d9b25bb530a2ec49d5b2365708"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/inventory.py":{"size":7742,"mtime_ns":1783242580346813026,"hash":"9b57e84bea4844ce93220773544064ed6979c5823b07831bc5fe734b6dc860d3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py":{"size":48150,"mtime_ns":1783945795484267217,"hash":"a9a876816af12e43b3fa6b9dc748b74b7107f0cae590e68a2a789945617c620e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py":{"size":33087,"mtime_ns":1783155250374329483,"hash":"6f7703d37ce761406d42f3be4e53ee85be0d3e43e5b880401087dde47ec6608f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/__init__.py":{"size":1243,"mtime_ns":1784035957226411568,"hash":"7d66d74042036571b769e5364616560597a3c3347cb9dea6cfbe04c40483b5e2"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py":{"size":10543,"mtime_ns":1783760751060550793,"hash":"2d64ca83553dbfc928887171941761114c5db012b9c4a01d34e2b522902bac38"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py":{"size":33513,"mtime_ns":1783849851800999450,"hash":"133db64dcfad9ae687b90084eb8f1b5f713ebabce852bba26b600cab52e51c50"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py":{"size":13188,"mtime_ns":1783242583174353699,"hash":"da03959cdab792ef503edae6ee27e45f9b5a8db1c63cdbcd97bafa85acc9be71"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/check_pl_json_aliases.py":{"size":2918,"mtime_ns":1781880510604174541,"hash":"b04c5636779ecbcb3082eabdbf42db846cc31f7376129845820c6a4c1e829104"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py":{"size":19762,"mtime_ns":1783849843323086946,"hash":"21d29a63b2dbd9178f2239d109c4e71e668c2caf85788dcecc34e7533f0df4e6"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py":{"size":6707,"mtime_ns":1783845044115228598,"hash":"34f3d1d723890e294aac765a66d7e86ca87f4288f42cec674bb9151de091fe22"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py":{"size":7694,"mtime_ns":1783849439910099822,"hash":"7bb0a6d4dfdb1ec11e72e971873791b8e188939e7c6b7328a0a2f4f8986c9d1c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py":{"size":39798,"mtime_ns":1783768766029953703,"hash":"0e0620d2878e4b28c560daf8095d30e8e8a281653d40baf01ef94e899ce114e8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py":{"size":9697,"mtime_ns":1783209147246415813,"hash":"aac3cfd1a30df824d1a4867c5b358e3155e19a051dbaffbae89c995b55767a8c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py":{"size":15310,"mtime_ns":1783777749440894312,"hash":"c506eb84c104ce63b98cde5b16644f16ddcf4e3aa335089bedcb4c267d21fc0d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py":{"size":1492,"mtime_ns":1783211660985345577,"hash":"ed4e21307ac463410d296a2f8cf53f8e0429187344537bcf1e1212a7affa06a6"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/public_inventory.py":{"size":6968,"mtime_ns":1782790898570930774,"hash":"79aa205d2e5e89ed45dd7b95068b93e3f6db82ee9258b0331b21d6c8ab551582"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_inventory_link_read.py":{"size":782,"mtime_ns":1782225276143253698,"hash":"6471dfc9a1fbe66223f79490a5827b2422e5ca6932305ccde6aef4b641676a6b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py":{"size":8470,"mtime_ns":1783211738132414332,"hash":"08745e00bfa7bd756fbeef7ae7a1d3bf957d6403da51cf7825def02a38502d01"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_site_link_read.py":{"size":2362,"mtime_ns":1782225460524562610,"hash":"3b9e1b93d3c4375b6b3adb4476ceb0df356654089d994aef4fea7e4f29266c06"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_transport_site_service.py":{"size":3701,"mtime_ns":1782225248175867714,"hash":"07585f13aaad965c106e88ac26f5f8c58852c0c2bb0bdb91820185133d799219"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py":{"size":47560,"mtime_ns":1783213204589333188,"hash":"53a61d4da0cd2e2d0bac74abcca13de3f3d506b603453811d8f1cd53fa50b358"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_data_scope.py":{"size":4660,"mtime_ns":1782649048011371782,"hash":"9545dc65fac7e13bf07ece2ec828ec5957bb115e23d0ac8c996f9c70362a4ecd"},"/Users/zlwu/work/00code/shanxia2.0/backend/DEPLOY.md":{"size":2574,"mtime_ns":1782639356701497576,"hash":"6989c30ab5c99ebf488137e31e5561b4a00d36060716dfad3fc247b673538818"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/data_scope_helpers.py":{"size":3358,"mtime_ns":1782650085670691361,"hash":"b0aab03bc7b3afb2faf39410e9881b65e6a2eeecba592d1655b5b9359fb4dbdd"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/patch_pr3_quote_contract.py":{"size":12491,"mtime_ns":1782650163950860693,"hash":"eb6b944df35fda5edb6675b69b00f5a83a75fd78785251d1e81e1788ed439cfe"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/patch_pr3_api.py":{"size":14409,"mtime_ns":1782650231443625388,"hash":"fb4cf4178a4b7c5530999f0c950ed38310dfe81b3168c10117849d6f512be01d"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/patch_pr3_api_calls.py":{"size":3345,"mtime_ns":1782650279067732462,"hash":"57732d0beb80ee17b533c78ce0250967fa83c68354ea9d1972275ba1fbc39007"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py":{"size":801,"mtime_ns":1782650506729785291,"hash":"0f0275c56a43603e69a030dd1d284cfc7982a7636423cfff6d0e692eb4631cb1"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/apply_pl_data_scope.py":{"size":10945,"mtime_ns":1782650517176615737,"hash":"928fe2112c9fd8bab4da7f4fc67d6d247c78187ca3b1a4146cc2fd3d9e04697f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx_data_scope.py":{"size":2394,"mtime_ns":1782650938092205819,"hash":"1e8e1dbdcf4bc68c2e30b99015e930bb9522bd1f633e95604ec6a62bdb2b43f3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db_data_scope.py":{"size":796,"mtime_ns":1782651142571517234,"hash":"ad4e206ae53e25247ebb004e0ef586077a6e7df0eac7b14973c26b186cffb2eb"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/delete_guard.py":{"size":3572,"mtime_ns":1782687610196676295,"hash":"223d91c51a1677b6a4b7802d5571af14d5cda8bc45e4e757b1f2150990a69bce"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_delete_guard.py":{"size":1565,"mtime_ns":1782687611517772849,"hash":"5e2a45f63b7296e3a10dfbd4b4716e54370e1439248881d6591cba5c9f6581d6"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py":{"size":3479,"mtime_ns":1783848338604396640,"hash":"7d12991ffd2e3d7b768aaa587d7f9ae1ed4d641f65ec3f053ed103fc74a5a005"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_recharge.py":{"size":6299,"mtime_ns":1782694169098881801,"hash":"4d5ab880c0c3ca91b485e1ba53725005b9f89fe205780d58a738c861dbb7e6f8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_recharge.py":{"size":8824,"mtime_ns":1782693867411719704,"hash":"f084b6fb3e80de0455ee9cfcc857ae5814b79497786367380f3a375b16c4bf85"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_recharge_service.py":{"size":22641,"mtime_ns":1783760938743438422,"hash":"a4a9377a8243e30dd7d6a8a175c68525009465fdebdeb01230849156dd8b59f4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_maintenance.py":{"size":6563,"mtime_ns":1782695447503836332,"hash":"c34b79f5bcd004c0f6353922492883e4b08e5a6bc229f0265c9af850d235848b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_maintenance.py":{"size":7793,"mtime_ns":1782695447583143336,"hash":"79033735d527926ac082200de0b89a122c53db3cbdf5fb33e3cc69f74dd017f5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py":{"size":23307,"mtime_ns":1783761151873696556,"hash":"0c7aa346d9879347d6a98c435fadc47873907ca0774147b69acf0d392e00e58b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_urea.py":{"size":5609,"mtime_ns":1782696196169930337,"hash":"71b7de2f90b3c7c732b59683e5cd4d858288121414a1ff2679c039299f5fb024"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_urea.py":{"size":6696,"mtime_ns":1782696220189638136,"hash":"750ecae5cdcc857031e618b87ffa9b28607bd47e72ecf6dc7ebbe51003682df3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py":{"size":19556,"mtime_ns":1782696213546483763,"hash":"03d389e791e5fac9a5339d1259d342952fced97712c3657e8f791426de32743b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_decorator.py":{"size":5312,"mtime_ns":1782701254052728785,"hash":"e476146985da29ad8cb77064d10314470546a8a52f6026a75fdbadd3fee34288"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_infer.py":{"size":1767,"mtime_ns":1782699326717654441,"hash":"4346307abeb03020a7069f560556b9954cf50203efb6ac65b0a4b9644b6a9b2d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py":{"size":868,"mtime_ns":1782699327268762002,"hash":"064c356f3f0fbabc454311445f02d375b965c2c055ddc51386d4dad50ac3337a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/redis_client.py":{"size":1691,"mtime_ns":1782790834895168051,"hash":"e5d84d7433ce745da7a37fb46dff5d5eb6ae027ef0efbf406da0c9c7fb6089ad"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/resource_snapshot.py":{"size":3823,"mtime_ns":1782793446819247296,"hash":"2d65c990d058d22010f8d4449652eb752c297a046b5d7b3ff6755c3e1cad0cf3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_transport_resource_snapshot_service.py":{"size":4861,"mtime_ns":1782793450356380638,"hash":"6f94dcb582fe725738a54469e461aa0124b9754b2141cb78d3859ff1a2f25ca9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/datetime_parse.py":{"size":929,"mtime_ns":1782905360746020898,"hash":"7a4eac85768cbeb0ba76f7f478ddadeeacab2498cd9be4070a76e1cfad693cdf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport_address.py":{"size":2604,"mtime_ns":1782906539731495489,"hash":"6548b4b0c8c1d8096ce96ff1d5b523c67b12300f9a3eac66570b0256478a1120"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py":{"size":6875,"mtime_ns":1782907484528042124,"hash":"7880a37b53dc2e88534ca5750097792ee7e893c94d87bf1eecd8b6055fbba181"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py":{"size":711,"mtime_ns":1783208067000967398,"hash":"7c02dbf83fad89c7da757965b545bf77f37792edd0d01d3ed320349b0b60a50b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_delivery_slip_template.sql":{"size":217,"mtime_ns":1783231586212866839,"hash":"e05e3fe7255e6c8a88aceb1aa53d743a4888a34c9fbaa4449c1cb8a275744d68"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_slip_template.sql":{"size":191,"mtime_ns":1783219958449266456,"hash":"e2a17c3df7be5cc0436ef979a48de81eecbe99c03f43662f4b0954a09dfa1305"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_subpack_slip_template.sql":{"size":215,"mtime_ns":1783238744737524455,"hash":"24cf12e21f9f61f49c38e654c0ca655f56a21e5e2364e4c2ca4547dad583272e"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_subpack_slip_no.sql":{"size":203,"mtime_ns":1783240340132373787,"hash":"de9b3c8e1617b9c345b2d4005e4a627f10ded089db89d35f0b389b333b840c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_subpack_slip_no_format.sql":{"size":571,"mtime_ns":1783241697391655899,"hash":"61fc58ebda64c5d6be6bfd374dda99c7134e38dc89031527f71916625c4e36db"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_product_legal_use_cert_url.sql":{"size":1180,"mtime_ns":1783253793703960795,"hash":"01105ec581050477087d9bf65528cfe77ce0026d0fb682cba8014c4cdb046db7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/menu_badge.py":{"size":247,"mtime_ns":1783257567212910729,"hash":"88c31e5ef8cee73327a1a0f36ea7ae0e61fa8f1ae140ad95ed0c0b9c2b4d05f7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/menu_badges.py":{"size":1203,"mtime_ns":1783257582921626135,"hash":"05dfdc446281abd9c3b273ebae2f9cbc4efe681fdc0ab288a77903e9e2bf83c7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_menu_badge_service.py":{"size":6108,"mtime_ns":1783258109621359209,"hash":"aec342e2370f584c5ed0024bb08a8346c70e1827c09ab456b4b06e9dfc817d8c"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_bx_menu_names.sql":{"size":454,"mtime_ns":1783254856670799736,"hash":"d40ab4c556ab285498f4689c21736832deddc754a9a2b7d806e870ccb39fc9ae"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/seed_sys_menu.sql":{"size":82266,"mtime_ns":1784119136237565207,"hash":"e0c15f9bd4525b6e447a7e8da3aa6bf93c14c47a1b99dc5cb4bee9dfd1a60e8b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_menu_groups.sql":{"size":6367,"mtime_ns":1783256049450807031,"hash":"5f6964d1ea2712d3d72c8f43f08c16597d85fe79f3f54c8be99962090d6c2b67"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_doc_reminder_menu_group.sql":{"size":1562,"mtime_ns":1783256051207137180,"hash":"82a0ce8c5293f5051967633c1c2e0a1dc60e1fc7c97919c9e0866246bbcbe3d1"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260711_hide_vehicle_urea_menus.sql":{"size":269,"mtime_ns":1783760394518896877,"hash":"a5f66647cdb4f0f403689b47b804f8099d7d710753823ef3ddc5e7a5a9ca4ca8"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py":{"size":4258,"mtime_ns":1783777759829460303,"hash":"6af8eb3c1f6c7ad34cd62f1e82e9c6b96737ffd48ba74dda7c5f888c55dd11cd"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py":{"size":10503,"mtime_ns":1783946967244318920,"hash":"07991890446a106b2d520e5e3c65e27af90c13d00945516ef1ce9bdf2e389879"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_pl_personnel_type.sql":{"size":1662,"mtime_ns":1783843576747470605,"hash":"24fcd156f97e2bdf2bd158beb5358fe7159874783a84716c6c18d2565cc8ac1a"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_pl_personnel_affiliation.sql":{"size":753,"mtime_ns":1783845019912997576,"hash":"6d450d264921f43c4e74ee4a2bcfc8335d16f77d9e0413267389741a92bf4eb5"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_add_finance_dept.sql":{"size":1697,"mtime_ns":1783855927743333340,"hash":"4ce457089ead5c063ae8e1a6d6553416deb43c54a7edddb574ee2572390aba99"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/setup_business_roles.py":{"size":14256,"mtime_ns":1783858477739135462,"hash":"ac12df2cf75826931c85f4deb3ae28614e31ea825d0beb3cf351a8d4f5cb1071"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_reset_sys_dept.sql":{"size":4517,"mtime_ns":1783855538627076795,"hash":"3795d6127a4dcf55d406be0fe673b8a83655723fe1f07b855e6fe45823d489fa"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_users.py":{"size":28602,"mtime_ns":1783855936414773748,"hash":"f5626fd3ca8bbc16275862080365c3ea2a27fd0f608e2243a284493762a8f735"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/verify_web_menu_codes.py":{"size":2535,"mtime_ns":1783858485781974173,"hash":"16c9c072b5b80fb309d56823963ebd0dd1f437a3f74d7f82711dd8a7c2de86d2"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/refresh_business_role_menus.py":{"size":3812,"mtime_ns":1783858483854587001,"hash":"b2c76109936791e1f624e239b24832237fa0256b38b98578e39825ac215fb166"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/renumber_crm_product_no.py":{"size":9071,"mtime_ns":1783861492796558653,"hash":"320c63801100ea8e9fb62abfb44b3487139377e293310593a877ab2d06e9d2e3"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_product.py":{"size":13287,"mtime_ns":1783860925852062495,"hash":"3e5dd462f2ad6218d2547b66db741132d0179037e6dd3ac24ddaa02a5b970d4b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_product_category.py":{"size":15583,"mtime_ns":1783860690842141129,"hash":"9cbdfdf3335a366aebd0a605431b3952ea9aab26bd564d075d501479ae0b20f7"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/compare_dict_snapshots.py":{"size":11072,"mtime_ns":1783860128258640426,"hash":"cfc98cc72c5e387317cfa0a7670c47067fffe8f2e540d5cd580ee459e173e4ef"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_sys_oss.py":{"size":22858,"mtime_ns":1783885816251409459,"hash":"20f59e7e19a0c4415bdf17cef32a31db63220346688b3dbe06bc2c52a42d475c"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration/minio_url.py":{"size":4770,"mtime_ns":1783884947929403985,"hash":"54a1bf95512c0a8b5feeb518ea3af76983bb1b0a3ba645898b07debdb2cc52e9"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_customer.py":{"size":25235,"mtime_ns":1783886258025843303,"hash":"f30d5a50a28a1130325a9d9db88586bcae9e2a39434f077ded25f847af2ed66f"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_opportunity.py":{"size":23720,"mtime_ns":1783887271166904779,"hash":"f98964806f32c3020bdd7d644d930760d6ea3ad5162d5f1d6125494263dd5598"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_payment_invoice.py":{"size":13601,"mtime_ns":1783888353771580120,"hash":"c4af77164dac9ba4195c3d239c9679ea367b92f5f7500139f3b59547525af24e"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_clue.py":{"size":18423,"mtime_ns":1783887050391179085,"hash":"72b84e497176bc63264c26d891c591c839f9dd9788988043c5752413c1b62a09"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_bx_data.py":{"size":32783,"mtime_ns":1783888844450695833,"hash":"16751fcd542e209535216a0516d78b62eecb81732d7fe0f9ec051fdf811e93b6"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_contract.py":{"size":36188,"mtime_ns":1783888258230435217,"hash":"a6269c8eeff19f4ae8ff7d22a3544ebc4a1858aaecd59583e6afd140a7fa766e"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_quote.py":{"size":27580,"mtime_ns":1783887475224438064,"hash":"4ac389eb6cb7437d4f5e8203d9908efec9836046ce8628a5e4be1f8b8ddd52cd"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_personnel_cert_required.py":{"size":2829,"mtime_ns":1783946968430310316,"hash":"e89b3aa798794dc77085af5c4b880cdddcd2c0590c3cc3414f8a5389fd5f6d93"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260713_pl_personnel_cert_split.sql":{"size":3436,"mtime_ns":1783946793684378982,"hash":"ce61899ec7e13f41de08dbc923dd1ff2a1a976603d13c8f09aa6fd5be9c914a2"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/ocr.py":{"size":1889,"mtime_ns":1784035957225824939,"hash":"1de70651a8c1c9e0e208bdccb45d0b9c7e7a0f8f9a663941bdbd5ae278234b1a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_ocr_service.py":{"size":11166,"mtime_ns":1784036700055450000,"hash":"6f233150e253813a7c9da30e6e5d6046fdea01baa67c2589a332fe511ab795f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/test_aliyun_driving_license_ocr.py":{"size":5130,"mtime_ns":1784032210609057675,"hash":"13d43c9144c3795b8870fbd900ed14f97ae22ee043e2c265a5151525666abebe"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260714_pl_ocr_usage_month.sql":{"size":1270,"mtime_ns":1784032423540552834,"hash":"d7018a4e5be9760846731440be98e53584d8e1461fc4ea54a0c8db61c666e7fe"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/price_letter.py":{"size":3772,"mtime_ns":1784118782403320120,"hash":"b09bff73a40f4af48c1a13475e6bb1af3f4da174dcf393067ddfa8adfa3f09a4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/price_letter.py":{"size":12362,"mtime_ns":1784119032847632050,"hash":"d15105a475ef5ad816381c1e20683874240a80601b1b8b132a972b68276483e0"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260715_crm_price_letter.sql":{"size":11007,"mtime_ns":1784119746971057733,"hash":"c86622c81a2b9d43ef12ad398425910eaefef4af21b872d90a570097caf82e0c"},"/Users/zlwu/work/00code/shanxia2.0/backend/venv.bak.broken/bin/Activate.ps1":{"size":8593,"mtime_ns":1780108930820914793,"hash":"ed2ea10761f2798e8980084d754284d3fcb94f1284ae7085c35c16108fe24846"}}
\ No newline at end of file
+{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6958,"mtime_ns":1784032424448146538,"hash":"c2fb44aedb880733de28afa7f92d2c1c681eb600f821bbeba2bcda3933e5bc37"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4ea2fe128de80fe321b0c3c531fea7a88"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py":{"size":36012,"mtime_ns":1782699345666495916,"hash":"01f974f182660ec4f215bcacc31eddadd7899d60d95aa19a61046579a994512a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/__init__.py":{"size":2186,"mtime_ns":1784035957226402359,"hash":"5af681963b36af57eaca7fdfc7f707255008ffba18df1a8c2d5903f58fcbf159"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/base.py":{"size":2090,"mtime_ns":1780108930807298966,"hash":"fda71bbe87dcb29c6f0ffa9337e9f39c2c06d912df6cb75d521f0e87c7c73c6d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/auth.py":{"size":1315,"mtime_ns":1780108930808197763,"hash":"018b82e42bbb055250549c54ba700ee01cf28cd753397d6c162f9e60185908e5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/user.py":{"size":1911,"mtime_ns":1780108930810184774,"hash":"3d3c159018cab6ed1cd3de7034ffb1fe77a272b9099901a1882d07aa15fc90a0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/__init__.py":{"size":1159,"mtime_ns":1780112973062398026,"hash":"4e8b0bda6680e2c8740c380b96eedb859ddc584a81bad568a18ba6beb001e480"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/menu.py":{"size":816,"mtime_ns":1780108930808681891,"hash":"17e35f2d7bf95cac00d81357f69fdcbe849e855daa05570c3302de8182b4df11"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/attachment.py":{"size":328,"mtime_ns":1780108930808144846,"hash":"300b74c51c66cb0fbf8c9f4d84a4b7bf29dcc9b6a67d922f8e1d0df05f9fc5bf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/common.py":{"size":555,"mtime_ns":1780108930808426681,"hash":"caa0c03398e54017b25cca2a16cf8d232a59739893a16e4c33b51a5316d8964f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/workflow.py":{"size":3949,"mtime_ns":1780108930810245983,"hash":"e5aed7afcf903c838a35cdaf2230a93d221c63f5c6baa1497e455afac8587a9a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/leave.py":{"size":1449,"mtime_ns":1780108930808614098,"hash":"c54098a69bd4490290dd8d7809ed8764a3f38e8c38692eeef2b505293cdfaeb8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/sample_code.py":{"size":12322,"mtime_ns":1780114002126415047,"hash":"5b74838b3920f9182d092f8a1b721697f1a62e8b90728b9f54101dbb96baa40e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/post.py":{"size":5164,"mtime_ns":1782688288640487823,"hash":"370a7c2b5de64c6b72f98bcf7fbbc166ba2a7f5673c6e1426b10bcc88220bb27"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/user.py":{"size":14214,"mtime_ns":1783851628178366109,"hash":"aeac623e3020fee4cdd01e463ced23e4e03539a17c5b57dcbd9d88aac5a4fb4f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/dict_type.py":{"size":5537,"mtime_ns":1782688289303121533,"hash":"f4edc5824466d0096b3b70aeea7b4460a9fa54ca0929421759b625b60c56c505"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/tenant.py":{"size":5659,"mtime_ns":1782688281816798167,"hash":"0192db8ac9c3de42fe8ee58324c77111e152bf4c35fd15c0b16467fe243712f0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/biz_no_generator.py":{"size":2534,"mtime_ns":1780108930726090799,"hash":"e6bd375987e47f6ee80294686db64377938a286767bb28324f3eda2ad39ad546"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/__init__.py":{"size":169,"mtime_ns":1780108930725878047,"hash":"340e3c08174d61829012879e684f6166803b88c819c58b54a16a11d210dea85e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/role.py":{"size":9417,"mtime_ns":1782688282481859519,"hash":"008550fe8efce5310afd3d41d944cee4b68d71763a587583f8832855480477f7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/menu.py":{"size":7600,"mtime_ns":1782688282878700273,"hash":"36951a77088782d80500bfa6266b46a564a670e10b70af7a28cdee8c50741098"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/site.py":{"size":6525,"mtime_ns":1782688288935534527,"hash":"a7bf593eff76d08b338f48f9a035fa1493f4ca88867bf9fdf4e9af06b6165759"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/api_request_log.py":{"size":4562,"mtime_ns":1782699354647000111,"hash":"f233bde579c4f6c7f2fd6030878c5946fb5dd5e247efaf4fe80d3f48622c19a8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/dict_data.py":{"size":5418,"mtime_ns":1782688289691482227,"hash":"5bd3fd93c60956dab0268e9f4e6cf315d0a9c9656d649f4abb40858ba1d32911"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/ethnic.py":{"size":1012,"mtime_ns":1780108930727448890,"hash":"7de1eb94b9fbb897a7b362ffdb5df5ab1020aa7247c7c37a2a2a2aa5cdbac8ad"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/workflow.py":{"size":25052,"mtime_ns":1782688290021142467,"hash":"6ca7e3b79e91356a050592b768d7a6749e1bfdb2841a208c7235899603368aec"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/dept.py":{"size":6604,"mtime_ns":1782688283203296227,"hash":"ebb38fcd527fdac4dcbf0632ac926e4cbe95dddb5fab79c3142b010e7a48ea7e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/base.py":{"size":5360,"mtime_ns":1780108930726010257,"hash":"49fdea023825ea249923b1cb02d353dcede1850158d3555a2082887925e69b9b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/leave.py":{"size":4765,"mtime_ns":1782687678100096646,"hash":"847095009bddac2d77dcbb485bcc57cbe168ff874989c197f49f2a28675d2525"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/public.py":{"size":12682,"mtime_ns":1783858998986861832,"hash":"b98ac3f3bc95c2b42c481ac9ae9137528e55845d425e5d2184827dc424304e42"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/__init__.py":{"size":665,"mtime_ns":1780804649232229595,"hash":"f720195209c1783d8b2526220a086c2ae2769d76a12bd7df74136f5432c7b75f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/menu.py":{"size":30932,"mtime_ns":1782790854866245387,"hash":"4e9de047e67ff365d17ac28d30e1baa72cfa3c651caa1b9776e225fc07da0e58"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/admin.py":{"size":50055,"mtime_ns":1782855138852859245,"hash":"a5e74057222e3359f6077b2fd05b2db4d058ec2702243cc449bff8d448d44a05"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/workflow.py":{"size":21202,"mtime_ns":1782790854865038715,"hash":"3950dd8d9855f5ed33a0e15883f7e442c4e26bc51672209b1e68ce0d0c9ce399"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/leave.py":{"size":6916,"mtime_ns":1782790854865571301,"hash":"6e03239433bbaecc54f72fa2aba2578573ae6c7fc881f287ef57dafed1a0f1b5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/workflow_callback.py":{"size":3584,"mtime_ns":1780108930810395025,"hash":"a033c13c7bee7f4b636198c1efa47f2abba0d890657e52b0b7057568d4d83a52"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/workflow_engine.py":{"size":14042,"mtime_ns":1780113992914903818,"hash":"5c06a59c734f598d5c7ebe95d30f048ba3a97302b938bc9320d97e5a053c7aef"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/web_auth_roles.py":{"size":5592,"mtime_ns":1783858473640363111,"hash":"04c2cd67a7ec8190aa7a9db717a722cd347a52279ac3ef5bd284777f0698a904"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/ws/__init__.py":{"size":26,"mtime_ns":1780108930810617026,"hash":"d4357444e9b5b0a8593000fb6e4dc37469ebe008e675bf42ec0d55d463f88fc6"},"/Users/zlwu/work/00code/shanxia2.0/backend/README.md":{"size":2752,"mtime_ns":1780114581291841672,"hash":"f5def8d25af0556a443c31a73e81f958883b60cfed8b31fac457f2832c2b6d58"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/crm_models.py":{"size":66592,"mtime_ns":1784122553836870448,"hash":"1a0a09e5f5d9eb2d5e18e2f7d851287442ee370a3abfbf0b487ee4b4a18d43b3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/__init__.py":{"size":0,"mtime_ns":1780201410213315055,"hash":"a6d57250b43cbdecb56b7a9d55d29642b808be368733e14b7df2bd196f54d64d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/product.py":{"size":7820,"mtime_ns":1783820526204158517,"hash":"b8d626eca1b0fc6b0216a66afb1cf234155310d3077d940d5158111f873a414b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/__init__.py":{"size":0,"mtime_ns":1780201410210254759,"hash":"9b8fcaaabdc85883c9ef392b596b60db0bd3b2f1623fc8012e55003f234a9875"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/product.py":{"size":25974,"mtime_ns":1783820522540308906,"hash":"6d1fb7bc06a4f16c729b1b226377ffa38b9b47bf1cc75310dcefbb8f6e480574"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm.py":{"size":151289,"mtime_ns":1784119033682636742,"hash":"ceaa64369ef66891310f391836c580beefd5a69854b3a5a0539a419744a311b0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/customer_pool.py":{"size":9646,"mtime_ns":1780202350147822837,"hash":"4f0ab87e7a12534011b69f58079ca206d5443eee594849ab4aae728bb847de0d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/tenant_context.py":{"size":693,"mtime_ns":1780201410210198550,"hash":"0a810658d894d35b9e981faa1448e1d02d617ecbe563bd711f6ce5a8f9db1861"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/pool_rule.py":{"size":1848,"mtime_ns":1780202212734233591,"hash":"b0331e17e801d206fba2fa89df1a66d1758d850dc5547e076218e16c8211a3e8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer_pool.py":{"size":1340,"mtime_ns":1780202198698231150,"hash":"7a6b76e0e39ada0783dfbee92280e9f06299b6662b08abb13ab1b0d9c70fe370"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer_follow.py":{"size":3716,"mtime_ns":1780201410213934514,"hash":"3979cecd62a4f2ee5dd925639572208e36b397620d91a9ce49a59adfcb2a9d55"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/customer.py":{"size":13811,"mtime_ns":1783823672041317102,"hash":"b91167282158aeb11046dccc4bf2f29a13307a0413b3d7b3c860b17d37f8ad4a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contact.py":{"size":4856,"mtime_ns":1780742840975948859,"hash":"60c714cc254d70f39b43205bbcb7fb4c563e807ddd070eefaaa952ae96048377"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/pool_rule.py":{"size":8816,"mtime_ns":1782687680547203228,"hash":"779f7f46e7ef8bfba27390d56fa071dfd3a8446d9d3b3361003b1c239f7169aa"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer_follow.py":{"size":8121,"mtime_ns":1782687665525772610,"hash":"ec67fc7667847017a8d8b5a276f5c75328bda7fa360746d80c7890de3f6d4b76"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer.py":{"size":23825,"mtime_ns":1782687664490640155,"hash":"5d8934ae6318de87b0761686fe491a7ec2be02792a85d84308c383af3193fa8a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contact.py":{"size":10482,"mtime_ns":1782687664918872966,"hash":"f4e3a684dd7a6c7c927e10e8e1e0c108c151b963777b85c5b74d7e7871f3a438"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/generate_dict_reimport_sql.py":{"size":10692,"mtime_ns":1780201410245078685,"hash":"a4faaf61f246dad04ec3933083ab4348e5d2987ae1ed34b1f6d8f19535642a05"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/statistics.py":{"size":412,"mtime_ns":1780201410215906642,"hash":"fb1957d0fe87f22c37ba5bdaa5f3bf2bb4a40685a21a05dce98326581bb4844b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/customer_pool.py":{"size":3725,"mtime_ns":1780202237342131933,"hash":"0f26ac2bcb585241f25302787184db4db422934e5d896ca25bbe7efaf61d92c4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/statistics.py":{"size":5642,"mtime_ns":1782649310467813967,"hash":"f954dc5479f42a52fdb3f05ad9076fdbe43e569bae85299fca6c8f37c7323cde"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/invoice_info.py":{"size":3104,"mtime_ns":1780406386885207867,"hash":"a59df3aed2c976904e3b5325fedd1621435b322645aa71349f3356e0cdc17424"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/oss.py":{"size":1584,"mtime_ns":1780201410212966513,"hash":"9349799b02a3531d8e1b46f9f70d3b6630afd7306663c779bd11f25448a16013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/invoice_info.py":{"size":7446,"mtime_ns":1782687666050313280,"hash":"e0f2069c43cd272e5f061d30b47a42140ac37aa26379985fc781f8a3851aa44d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/opportunity.py":{"size":9391,"mtime_ns":1780242127112297110,"hash":"2e068961824a1d3ec0da0a94a3c494b63582b3251e5f4e030ce3e12897e85002"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py":{"size":10565,"mtime_ns":1784195323916483606,"hash":"45d9dfeb58bda22227fcd242c738086431616466bb8ee9f1466927d75d4bec48"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity.py":{"size":42231,"mtime_ns":1782687666592409903,"hash":"75f54438002e070076fe37ec2211c5fd67bf4aebd2607ac94db15e30d6f634d8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract.py":{"size":92433,"mtime_ns":1783763318740360571,"hash":"b2362db2f2df750afc1c2263f5e6ac1ccd5daa8b93ad5650d56028f452597837"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/monitor/__init__.py":{"size":1,"mtime_ns":1780242127108406092,"hash":"bb5b318d3cb224c50a2e0286e145f89ea7278437437fd17d7c865d67ad148076"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/monitor/operlog.py":{"size":1069,"mtime_ns":1782699353340106185,"hash":"b9f0a382f8990f69391b26bb75b4538d141d6adadc4260fecaa8b33cb44c063d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/__init__.py":{"size":1,"mtime_ns":1780242127112307360,"hash":"0592c62ce1172d93aca98cbd9aaa54b9c2668022abf3581249516472db312293"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/monitor/oper_log.py":{"size":6673,"mtime_ns":1782699352542520022,"hash":"0586702734f693ee099af38d407c71cc0a840153f93174699c5a5201e0a9acbe"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/monitor.py":{"size":2789,"mtime_ns":1782790854865860510,"hash":"5c13164e49d7ecb6836cf8e65a705fccd518399b85d3be2318c9581a98bacd51"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/clue.py":{"size":9417,"mtime_ns":1780215022189075651,"hash":"2676afaecac4bb6da185674a9421dfbd40834ca21520adf66bb6b175fb728677"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue_follow.py":{"size":8493,"mtime_ns":1782687674053739962,"hash":"54881f876d1842ee4a344d049ea4b5bac18b243535338077cb3eb3ec8b60e9c1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue.py":{"size":25820,"mtime_ns":1783155705954031080,"hash":"3526af7e7e79d505911690355890ccdb3a1c30d302534ffdc8626a454cc2499b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/clue_statistics.py":{"size":11229,"mtime_ns":1782649305846991404,"hash":"fbc3b4f3356256787839cc5b2a6f8dff41dcf9eb27c791e427e816e3ba14e094"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_follow.py":{"size":10069,"mtime_ns":1783821986040417425,"hash":"06c9f3deab3472b20429c12a618ae2d4d6efd3afa2353ac44cd52651a596af4c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/opportunity_product.py":{"size":10346,"mtime_ns":1782687669972916544,"hash":"40b497a41ce4003e128dc305abedc54e01dbaad8a8f7bc1ecec1662a3953a1ed"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/crm_export_columns.py":{"size":9594,"mtime_ns":1780743830255260394,"hash":"b20594507aab09cebc3d540acad04547bf780cac9c885187138f5d64718ed513"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/__init__.py":{"size":0,"mtime_ns":1780242127043319167,"hash":"a66deceefc987198863f61b8aa1b5715483a69c4a15ef47cd5586c5d8e429b97"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/excel_export.py":{"size":4411,"mtime_ns":1780242127113233948,"hash":"7fbde3fca845e4705487e7f801caee308bcc0c99a9da7d0c99681a96eaaf6076"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/supplier_info.py":{"size":3679,"mtime_ns":1780242127112269527,"hash":"8b797e9dee6ac7ff806a8ea23bef5fedce648502ea23287e1b417e9f96d22c2e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/supplier_info.py":{"size":6916,"mtime_ns":1782687684650105460,"hash":"0b66714e36609688f7ce79ecda0679d20df115d2d64a0dff1cc0fc76df7dca6b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_product.py":{"size":13126,"mtime_ns":1782650413581368849,"hash":"cd2fc23852ead3c4db19e34bc33c1560fc5f5d4cd7cf51b61def8bf37c44c853"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/quote.py":{"size":5961,"mtime_ns":1780745856749183842,"hash":"29c52786df396aededd0850e3c8a3a36f3fb6a952af03adbe1768dae212b6804"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote.py":{"size":48137,"mtime_ns":1782687675213433843,"hash":"a2ff5c5b74a2becd0949aab6aea8cf63440cce2c0bb22e1a1661463e0b2e963d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote_product.py":{"size":10537,"mtime_ns":1782687676011228777,"hash":"57d9d5b4fc2bc8b68c0388a4b8a9c1322539ed7e538764fc23521916429e07c5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote_share_log.py":{"size":4277,"mtime_ns":1782650254630170341,"hash":"5bfbf7ce2918f9ecdbbca6fdce17d200cd52cdc71b2dfb44ac126a36ca0ed734"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/quote_approval.py":{"size":8816,"mtime_ns":1782650314936479798,"hash":"736353b8c2c5073d260e007a166234bd7d57958bcc738c56a1d39cfb22381180"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/crm_quote_pdf.py":{"size":9877,"mtime_ns":1780242127112382277,"hash":"fec04892fccccc3601fbeb012c92c77110496910be44102de40b4610148fa58a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_quote_routes.py":{"size":25633,"mtime_ns":1783825326810765171,"hash":"c51b70766aae85dbc537bf5b48a2c350c73ea8e93bcad2a9660d8d45c6d0979d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_approval.py":{"size":13958,"mtime_ns":1780763324103831772,"hash":"3d7fbbb90728568a2577e52d8986ccbec8f599d04a0ef9e330939d1fe729bc98"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py":{"size":29547,"mtime_ns":1783825526614169736,"hash":"95969972c0773528b91d9728d303feae5ca7187d89a49495d5129dcedffc19dc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract_template.py":{"size":2897,"mtime_ns":1780229101351431207,"hash":"935adfc7659e31201cd83ba8387bf6abcb39e7cdc63fa4692d699ed874cf113b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract_change.py":{"size":2496,"mtime_ns":1780229100306468735,"hash":"05af42f088f6cc400c1e3445d4a2c96670edf482328b361aa4f231230d958729"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_template.py":{"size":11858,"mtime_ns":1780229131746659196,"hash":"b9bdbe6a33c933aefcb25cc57e94b2fd2dc75f8e5822434f3912aa0153dfca00"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_change.py":{"size":15985,"mtime_ns":1780229123819597506,"hash":"771b110d3d136c45106dd61171c724c559b5b4bf572ea7b8c2b648f0bfe629c1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_sub_routes.py":{"size":25165,"mtime_ns":1782650354912882614,"hash":"b2733fd74cb7e6cbe6518fc5a5700e387601afc087be09f395f3c61372099847"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/attachment_biz.py":{"size":1122,"mtime_ns":1782219602888987090,"hash":"fb9fea8983524e3891357016b6546d982c4a82cb782fc01425f70472d4338b6b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/bx_auth.py":{"size":1473,"mtime_ns":1782650780925056463,"hash":"678d9ff99b5c89575ef47e5c9e61d495f364917fd6cfb2b322c4d5035d3ba359"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/bx_date_range.py":{"size":2363,"mtime_ns":1783745556349035158,"hash":"e8b487a9bb4072356cae7b1b38496c441f9a1534d6807798448e6de97073d669"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/utils/bx_codegen.py":{"size":4679,"mtime_ns":1782003396630684570,"hash":"bc3f798cfbc359e0327d72e3c0513fe9cbaee196d0d85bcf6567cf5df4211d97"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/bx_models.py":{"size":16644,"mtime_ns":1782003187247224106,"hash":"37c7372f38d76f1020d31ce8ca9e7f0cb3bc3c204a50deb092b494ce8c983fbf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/__init__.py":{"size":81,"mtime_ns":1780750556881734563,"hash":"07c3422f745afbff1c8162b3782fbb4b8de65084e7e9bbcf45abb9d433d65f98"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/common.py":{"size":1547,"mtime_ns":1782003187605255900,"hash":"a75ca6a0d92f93408eee00cc5053206d28a545512e4cd0f186be9e00e3346ab1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/__init__.py":{"size":777,"mtime_ns":1780751360180065853,"hash":"3525eb716c238682a07a116818d318b8a494fb24b9cefca5114d30bc50d095a5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/expense_item.py":{"size":1668,"mtime_ns":1782688293668634639,"hash":"ac07c7b05fe990e743dd5b4e96a85b59431479ca11b98cce7c340a93dbe99ce1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/workflow.py":{"size":1677,"mtime_ns":1782688293973889288,"hash":"f683e5bb7a45d0bae3b3f262c201453155eaaaf2c8d7f14f567a14295a68d2e1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/expense_item.py":{"size":3362,"mtime_ns":1782650913604714906,"hash":"c6a142011870516822cf11cff16a86c56908f00680eb7c6219c8441c9b63632a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/workflow.py":{"size":4984,"mtime_ns":1782650913326708063,"hash":"84285aab7499ce9280156c5814b2dc4fff42841803dd673cc6bfdc6eed498283"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/expense_item.py":{"size":4366,"mtime_ns":1782650887981843546,"hash":"15321fe1df77a555af579474434e3b401bd042555d19ea81a066ef754915612b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/__init__.py":{"size":34,"mtime_ns":1780750594045301798,"hash":"583b12a6d2c9fd4411b652741e73324df9256138946ee873a0ed03169f8dae2a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/workflow.py":{"size":5446,"mtime_ns":1782650919964956073,"hash":"1d72733147e0ed997094d7f92d1c1f5e1e108093a50e8e523b3b016c3dd676e4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/base.py":{"size":3931,"mtime_ns":1782687617846731919,"hash":"dba80b8f4e26bc59efd2b2aad6bd430c0315133a98bfbbe1b93113b60baba8b8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/attachment.py":{"size":525,"mtime_ns":1780750871480000584,"hash":"f287214c72345e41d9d122ca55b4d3e279635d99cbf10ed530de0ac32374f31c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/advance.py":{"size":4603,"mtime_ns":1782688296833335474,"hash":"74b6e8bf6906518f80f16f218f1af74d2c356a6f392a4e8a69466f4179ce0b92"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/attachment.py":{"size":5758,"mtime_ns":1782790898570675189,"hash":"e88b9181e7c1a839074656ce2260cd6f693821402a24f08dd1c0ef3c160f8fa8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/advance.py":{"size":13370,"mtime_ns":1783745567004734594,"hash":"3097a75d86d09951d7de7df10a32f5aedcf6ce11279bdf6dfe0005ec9d8a7be1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/attachment.py":{"size":4582,"mtime_ns":1782650868959755751,"hash":"3a6480cd051bd1af733a67e5babcd64f3f74cbe230d72364b599cde8b0c67a5f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/advance.py":{"size":24037,"mtime_ns":1783746967707088251,"hash":"4834348d099b32b70d14e1b56279dbb385f42519646351d8cb220da8c5fe27f1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/storage.py":{"size":3656,"mtime_ns":1780750869826924187,"hash":"ac98980bd5b6cbab6a9394b6debc678b050cf4df969eccbc5f2d419b615937cd"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/bx/reimburse.py":{"size":5638,"mtime_ns":1782688293517624966,"hash":"aa47d8e87982a4077ff067dff2955db9eb67171d6368bc636c1eea17ea7682f1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/reimburse.py":{"size":15431,"mtime_ns":1783747621462206303,"hash":"dae4bfc4be69ca2f3ee8fd2e9e3bd6053a2904c8f7a0c83e536f2dd71874f918"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/reimburse.py":{"size":34743,"mtime_ns":1783747614591546341,"hash":"ba8f3ff0304a6668bde3c2e6c3e15427153389f7320b943fb261d1d947b41d1e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/bx/payment.py":{"size":15715,"mtime_ns":1783745581266319098,"hash":"0d0b8823013a63f91baee0ea19b56f203e70fb1e450f873cfeb634a6cd017f54"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx/approval_cycle.py":{"size":1296,"mtime_ns":1780758200904804714,"hash":"308c087d94ac253bfc7e2d5455a06ae8ceac034adda41b5c61dd315dc167efaf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/db_models.py":{"size":8110,"mtime_ns":1782219570945208920,"hash":"249bb0971c263436095a6324457557b6ac5cfc3ca5ac9bc9499a9b4cd21d42bf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/__init__.py":{"size":544,"mtime_ns":1782011200096708592,"hash":"822060230e155b8704feb447b908c226d4c53e62b3e6df65c89dc3c7a0e6e07f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/__init__.py":{"size":0,"mtime_ns":1780805614160787709,"hash":"2cb780fb722e6dbf2ecea6f6fc19345fb453e1a3c40694ba0ff7465eb7cf7d33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/base.py":{"size":2754,"mtime_ns":1780802262496001324,"hash":"d2fd88aa376aa5914125381cf61a4ae0f343657d6bd7db1f9fe1254e7aba5b2f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/vehicle.py":{"size":591,"mtime_ns":1780802980783200520,"hash":"d87e4262b344e02a7be21534e382e7b75f5543f599d673ddc046134ad0e8ba4e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/__init__.py":{"size":84,"mtime_ns":1780804619867600032,"hash":"5193d61f7204f825551f083e79722805fb2abfd953bb552e80950bb3ff9784a1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/vehicle.py":{"size":3780,"mtime_ns":1780802985736476363,"hash":"aeb49bdd49cb5091c37275b856396e75bcd24557e3a1b58ef668910b1b0aff64"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/vehicle.py":{"size":4776,"mtime_ns":1780802999039302982,"hash":"cf32c66499ca86285561b573d222837ecc76c3fa51b0091b802485fbde8e2bf2"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/plan.py":{"size":1788,"mtime_ns":1780803836065964235,"hash":"30452b2f7fa19f5677475ce08b3e355dae2440d80d2e9049f8417de01d822863"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plan.py":{"size":4167,"mtime_ns":1780803836065953735,"hash":"acac159dedfacb36f02982b700b2a7c7a8fbb4644eb89c3b0c13871941917864"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/plan.py":{"size":9619,"mtime_ns":1780803836065902943,"hash":"e311f2b8b5e7785db764216fbecfd2830a11569ccb827a00c8bd3a2ba3804383"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/inventory.py":{"size":3948,"mtime_ns":1782219604456801044,"hash":"19ea80e59a1309518ad6183a6b0b8751401f4724d7b5101d47defc2417ec36c8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/inventory.py":{"size":16423,"mtime_ns":1783156035144390497,"hash":"901602a8fe362aec9b89ac6f2ee578f1eeb105fcf12a1a6c3ff4ef98f4ad1ef3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/inventory.py":{"size":10663,"mtime_ns":1780803878293678536,"hash":"e38abdd78010b0e96dcb65ed826de03a752b40e3355546ed06a4b07640cf7013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/weighing.py":{"size":5347,"mtime_ns":1780935341753643087,"hash":"f734c66269b713a840d7ce6c1c6150bd2dc00b114b09963362202d119c82301e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/weighing.py":{"size":14940,"mtime_ns":1782687688848020877,"hash":"71d143eb05d0d3f785f5962892bde64dee3c2937139c975b52af62628f308d46"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/weighing.py":{"size":24042,"mtime_ns":1780804029884092079,"hash":"bc9d5cb59bf90597db0e0a1c9afcef5c2b429c3945c92be34bfdc1e77d26ba28"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_auth.py":{"size":2500,"mtime_ns":1782790853674329877,"hash":"4988b71d4e54b326c333872b34743914d67b15fb753c1b0231e9b00b366458a4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/plans.py":{"size":4876,"mtime_ns":1780935329287069452,"hash":"474cb68905249cd5a6237443a412d49cb28af2e0cec61c9bf5c34ec54db1240a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/master_data.py":{"size":619,"mtime_ns":1780925759582999052,"hash":"6f6b2c2e1478efce74b8c6e6fb058d914b8b79adfadf2fea2d91cb55d3f9cdce"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/vehicles.py":{"size":4748,"mtime_ns":1780925781827569662,"hash":"8f8b32dfbf4046cfa0b6645d211732596f7027d46244d9c50132425220065483"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/plans.py":{"size":17098,"mtime_ns":1783156035873015369,"hash":"bd2c8344e2ab765ceb2a2c91f5cf2facea319ae66c9097f5468a7f5d753b67dd"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/mappers.py":{"size":6955,"mtime_ns":1782219619253708711,"hash":"b9e97f66a0a248d06c2f960c3f5ac4fb3369cc976c16709227dee0cc1513d221"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/weighing_logic.py":{"size":4110,"mtime_ns":1780804818442134500,"hash":"468ded88a0a4a397a96884580c88c59291c0b6effa897f08f1b3fea7857f9191"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/dashboard.py":{"size":514,"mtime_ns":1780804990870580151,"hash":"f2c1b1014d0d769abf32dab3783de65fe7fe327c956ffbfe44adce94542f8892"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/dashboard.py":{"size":6576,"mtime_ns":1782651340330906738,"hash":"753703fbbe8ddc02d8fbdae78df4abd1448f5ce5878e2f0390111360fdc300b3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/db_auto_plan.py":{"size":949,"mtime_ns":1780805095845033710,"hash":"9f7513cd2e74f9ddfe5cbee4ae58a40ee7a58aaaff051d590b19a6e1b08bb46a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/plan_helpers.py":{"size":386,"mtime_ns":1780805092108105489,"hash":"5c6ec7612b204e1dcb65c5daf03fa7f8ffa7710cd1ace63be4f7ca00cbf318b8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/auto_generate_plans.py":{"size":4699,"mtime_ns":1780935400833764213,"hash":"d0760cb8155d60e18040bcf6fe41e6964640a3dffd6f4b7bb68678c9f31ab29a"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/run_db_auto_generate_plans.py":{"size":1043,"mtime_ns":1780805102157096922,"hash":"3cd8f15b677f21b2873ff384fc0581da264a53ee4318da3c8262101156e3225b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_dibang_data.py":{"size":20215,"mtime_ns":1782011225157870526,"hash":"28a49a423a784e70bec424b702f3d58f69b6f847153c061414efdcdabb660d0e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/inventory_id.py":{"size":2574,"mtime_ns":1780934632326984469,"hash":"c5269cbb273d987c296143b83f1287762ce67d7ddeccfb0a72bc70234bad9f12"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db/product_helpers.py":{"size":2106,"mtime_ns":1780935268573313971,"hash":"cea6b0697eb64081a79d18ab95f0f0477714505a7aef82a41672197b15fc8b14"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_db_inventory_id.py":{"size":586,"mtime_ns":1780934647395760353,"hash":"71a9617d5d0f2c23416686eded204b4c941b47afc3fd6e3facf0c75401e883d9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py":{"size":61386,"mtime_ns":1784032430468998611,"hash":"e23b4653b806a1917eba187d8a3584dff598ad74bc21c8295c179abf0bcfe17e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py":{"size":22053,"mtime_ns":1783754594278544472,"hash":"f3a69a96b7916cb34e23e673b3155d83f7d9698b2582cfae96dedc392da8e6b8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py":{"size":17945,"mtime_ns":1783211732481443217,"hash":"477ebd0c4c084cf8bccd10865e4703b6b34829d30b791c32232d96c1bac2947f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py":{"size":5012,"mtime_ns":1783242582112287271,"hash":"1dced39f250919efc4aa798daee1efef3adcca054c7483843536c455919ff106"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/freight.py":{"size":11275,"mtime_ns":1783213205154827373,"hash":"557f9e9f06ab6f18eb33f54a579fb8ab0f6992d9b25bb530a2ec49d5b2365708"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/inventory.py":{"size":7742,"mtime_ns":1783242580346813026,"hash":"9b57e84bea4844ce93220773544064ed6979c5823b07831bc5fe734b6dc860d3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py":{"size":48150,"mtime_ns":1783945795484267217,"hash":"a9a876816af12e43b3fa6b9dc748b74b7107f0cae590e68a2a789945617c620e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py":{"size":33087,"mtime_ns":1783155250374329483,"hash":"6f7703d37ce761406d42f3be4e53ee85be0d3e43e5b880401087dde47ec6608f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/__init__.py":{"size":1243,"mtime_ns":1784035957226411568,"hash":"7d66d74042036571b769e5364616560597a3c3347cb9dea6cfbe04c40483b5e2"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py":{"size":10543,"mtime_ns":1783760751060550793,"hash":"2d64ca83553dbfc928887171941761114c5db012b9c4a01d34e2b522902bac38"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py":{"size":33513,"mtime_ns":1783849851800999450,"hash":"133db64dcfad9ae687b90084eb8f1b5f713ebabce852bba26b600cab52e51c50"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/inventory.py":{"size":13188,"mtime_ns":1783242583174353699,"hash":"da03959cdab792ef503edae6ee27e45f9b5a8db1c63cdbcd97bafa85acc9be71"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/check_pl_json_aliases.py":{"size":2918,"mtime_ns":1781880510604174541,"hash":"b04c5636779ecbcb3082eabdbf42db846cc31f7376129845820c6a4c1e829104"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py":{"size":19762,"mtime_ns":1783849843323086946,"hash":"21d29a63b2dbd9178f2239d109c4e71e668c2caf85788dcecc34e7533f0df4e6"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py":{"size":6707,"mtime_ns":1783845044115228598,"hash":"34f3d1d723890e294aac765a66d7e86ca87f4288f42cec674bb9151de091fe22"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_service.py":{"size":7694,"mtime_ns":1783849439910099822,"hash":"7bb0a6d4dfdb1ec11e72e971873791b8e188939e7c6b7328a0a2f4f8986c9d1c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py":{"size":39798,"mtime_ns":1783768766029953703,"hash":"0e0620d2878e4b28c560daf8095d30e8e8a281653d40baf01ef94e899ce114e8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py":{"size":9697,"mtime_ns":1783209147246415813,"hash":"aac3cfd1a30df824d1a4867c5b358e3155e19a051dbaffbae89c995b55767a8c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py":{"size":15310,"mtime_ns":1783777749440894312,"hash":"c506eb84c104ce63b98cde5b16644f16ddcf4e3aa335089bedcb4c267d21fc0d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/db/public_inventory.py":{"size":1492,"mtime_ns":1783211660985345577,"hash":"ed4e21307ac463410d296a2f8cf53f8e0429187344537bcf1e1212a7affa06a6"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/db/public_inventory.py":{"size":6968,"mtime_ns":1782790898570930774,"hash":"79aa205d2e5e89ed45dd7b95068b93e3f6db82ee9258b0331b21d6c8ab551582"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_inventory_link_read.py":{"size":782,"mtime_ns":1782225276143253698,"hash":"6471dfc9a1fbe66223f79490a5827b2422e5ca6932305ccde6aef4b641676a6b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_site.py":{"size":8470,"mtime_ns":1783211738132414332,"hash":"08745e00bfa7bd756fbeef7ae7a1d3bf957d6403da51cf7825def02a38502d01"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_site_link_read.py":{"size":2362,"mtime_ns":1782225460524562610,"hash":"3b9e1b93d3c4375b6b3adb4476ceb0df356654089d994aef4fea7e4f29266c06"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_transport_site_service.py":{"size":3701,"mtime_ns":1782225248175867714,"hash":"07585f13aaad965c106e88ac26f5f8c58852c0c2bb0bdb91820185133d799219"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_freight_settlement_service.py":{"size":47560,"mtime_ns":1783213204589333188,"hash":"53a61d4da0cd2e2d0bac74abcca13de3f3d506b603453811d8f1cd53fa50b358"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_data_scope.py":{"size":4660,"mtime_ns":1782649048011371782,"hash":"9545dc65fac7e13bf07ece2ec828ec5957bb115e23d0ac8c996f9c70362a4ecd"},"/Users/zlwu/work/00code/shanxia2.0/backend/DEPLOY.md":{"size":2574,"mtime_ns":1782639356701497576,"hash":"6989c30ab5c99ebf488137e31e5561b4a00d36060716dfad3fc247b673538818"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/data_scope_helpers.py":{"size":3358,"mtime_ns":1782650085670691361,"hash":"b0aab03bc7b3afb2faf39410e9881b65e6a2eeecba592d1655b5b9359fb4dbdd"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/patch_pr3_quote_contract.py":{"size":12491,"mtime_ns":1782650163950860693,"hash":"eb6b944df35fda5edb6675b69b00f5a83a75fd78785251d1e81e1788ed439cfe"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/patch_pr3_api.py":{"size":14409,"mtime_ns":1782650231443625388,"hash":"fb4cf4178a4b7c5530999f0c950ed38310dfe81b3168c10117849d6f512be01d"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/patch_pr3_api_calls.py":{"size":3345,"mtime_ns":1782650279067732462,"hash":"57732d0beb80ee17b533c78ce0250967fa83c68354ea9d1972275ba1fbc39007"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_data_scope.py":{"size":801,"mtime_ns":1782650506729785291,"hash":"0f0275c56a43603e69a030dd1d284cfc7982a7636423cfff6d0e692eb4631cb1"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/apply_pl_data_scope.py":{"size":10945,"mtime_ns":1782650517176615737,"hash":"928fe2112c9fd8bab4da7f4fc67d6d247c78187ca3b1a4146cc2fd3d9e04697f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/bx_data_scope.py":{"size":2394,"mtime_ns":1782650938092205819,"hash":"1e8e1dbdcf4bc68c2e30b99015e930bb9522bd1f633e95604ec6a62bdb2b43f3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/db_data_scope.py":{"size":796,"mtime_ns":1782651142571517234,"hash":"ad4e206ae53e25247ebb004e0ef586077a6e7df0eac7b14973c26b186cffb2eb"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/delete_guard.py":{"size":3572,"mtime_ns":1782687610196676295,"hash":"223d91c51a1677b6a4b7802d5571af14d5cda8bc45e4e757b1f2150990a69bce"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_delete_guard.py":{"size":1565,"mtime_ns":1782687611517772849,"hash":"5e2a45f63b7296e3a10dfbd4b4716e54370e1439248881d6591cba5c9f6581d6"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_carrier_service.py":{"size":3479,"mtime_ns":1783848338604396640,"hash":"7d12991ffd2e3d7b768aaa587d7f9ae1ed4d641f65ec3f053ed103fc74a5a005"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_recharge.py":{"size":6299,"mtime_ns":1782694169098881801,"hash":"4d5ab880c0c3ca91b485e1ba53725005b9f89fe205780d58a738c861dbb7e6f8"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_recharge.py":{"size":8824,"mtime_ns":1782693867411719704,"hash":"f084b6fb3e80de0455ee9cfcc857ae5814b79497786367380f3a375b16c4bf85"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_recharge_service.py":{"size":22641,"mtime_ns":1783760938743438422,"hash":"a4a9377a8243e30dd7d6a8a175c68525009465fdebdeb01230849156dd8b59f4"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_maintenance.py":{"size":6563,"mtime_ns":1782695447503836332,"hash":"c34b79f5bcd004c0f6353922492883e4b08e5a6bc229f0265c9af850d235848b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_maintenance.py":{"size":7793,"mtime_ns":1782695447583143336,"hash":"79033735d527926ac082200de0b89a122c53db3cbdf5fb33e3cc69f74dd017f5"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_maintenance_service.py":{"size":23307,"mtime_ns":1783761151873696556,"hash":"0c7aa346d9879347d6a98c435fadc47873907ca0774147b69acf0d392e00e58b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_urea.py":{"size":5609,"mtime_ns":1782696196169930337,"hash":"71b7de2f90b3c7c732b59683e5cd4d858288121414a1ff2679c039299f5fb024"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_urea.py":{"size":6696,"mtime_ns":1782696220189638136,"hash":"750ecae5cdcc857031e618b87ffa9b28607bd47e72ecf6dc7ebbe51003682df3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_vehicle_urea_service.py":{"size":19556,"mtime_ns":1782696213546483763,"hash":"03d389e791e5fac9a5339d1259d342952fced97712c3657e8f791426de32743b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_decorator.py":{"size":5312,"mtime_ns":1782701254052728785,"hash":"e476146985da29ad8cb77064d10314470546a8a52f6026a75fdbadd3fee34288"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_infer.py":{"size":1767,"mtime_ns":1782699326717654441,"hash":"4346307abeb03020a7069f560556b9954cf50203efb6ac65b0a4b9644b6a9b2d"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/oper_log_context.py":{"size":868,"mtime_ns":1782699327268762002,"hash":"064c356f3f0fbabc454311445f02d375b965c2c055ddc51386d4dad50ac3337a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/redis_client.py":{"size":1691,"mtime_ns":1782790834895168051,"hash":"e5d84d7433ce745da7a37fb46dff5d5eb6ae027ef0efbf406da0c9c7fb6089ad"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/resource_snapshot.py":{"size":3823,"mtime_ns":1782793446819247296,"hash":"2d65c990d058d22010f8d4449652eb752c297a046b5d7b3ff6755c3e1cad0cf3"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_transport_resource_snapshot_service.py":{"size":4861,"mtime_ns":1782793450356380638,"hash":"6f94dcb582fe725738a54469e461aa0124b9754b2141cb78d3859ff1a2f25ca9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/datetime_parse.py":{"size":929,"mtime_ns":1782905360746020898,"hash":"7a4eac85768cbeb0ba76f7f478ddadeeacab2498cd9be4070a76e1cfad693cdf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport_address.py":{"size":2604,"mtime_ns":1782906539731495489,"hash":"6548b4b0c8c1d8096ce96ff1d5b523c67b12300f9a3eac66570b0256478a1120"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py":{"size":6875,"mtime_ns":1782907484528042124,"hash":"7880a37b53dc2e88534ca5750097792ee7e893c94d87bf1eecd8b6055fbba181"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/pl_cert_expiry.py":{"size":711,"mtime_ns":1783208067000967398,"hash":"7c02dbf83fad89c7da757965b545bf77f37792edd0d01d3ed320349b0b60a50b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_delivery_slip_template.sql":{"size":217,"mtime_ns":1783231586212866839,"hash":"e05e3fe7255e6c8a88aceb1aa53d743a4888a34c9fbaa4449c1cb8a275744d68"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_slip_template.sql":{"size":191,"mtime_ns":1783219958449266456,"hash":"e2a17c3df7be5cc0436ef979a48de81eecbe99c03f43662f4b0954a09dfa1305"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_subpack_slip_template.sql":{"size":215,"mtime_ns":1783238744737524455,"hash":"24cf12e21f9f61f49c38e654c0ca655f56a21e5e2364e4c2ca4547dad583272e"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_subpack_slip_no.sql":{"size":203,"mtime_ns":1783240340132373787,"hash":"de9b3c8e1617b9c345b2d4005e4a627f10ded089db89d35f0b389b333b840c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_subpack_slip_no_format.sql":{"size":571,"mtime_ns":1783241697391655899,"hash":"61fc58ebda64c5d6be6bfd374dda99c7134e38dc89031527f71916625c4e36db"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_plan_product_legal_use_cert_url.sql":{"size":1180,"mtime_ns":1783253793703960795,"hash":"01105ec581050477087d9bf65528cfe77ce0026d0fb682cba8014c4cdb046db7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/menu_badge.py":{"size":247,"mtime_ns":1783257567212910729,"hash":"88c31e5ef8cee73327a1a0f36ea7ae0e61fa8f1ae140ad95ed0c0b9c2b4d05f7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/menu_badges.py":{"size":1203,"mtime_ns":1783257582921626135,"hash":"05dfdc446281abd9c3b273ebae2f9cbc4efe681fdc0ab288a77903e9e2bf83c7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_menu_badge_service.py":{"size":6108,"mtime_ns":1783258109621359209,"hash":"aec342e2370f584c5ed0024bb08a8346c70e1827c09ab456b4b06e9dfc817d8c"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_bx_menu_names.sql":{"size":454,"mtime_ns":1783254856670799736,"hash":"d40ab4c556ab285498f4689c21736832deddc754a9a2b7d806e870ccb39fc9ae"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/seed_sys_menu.sql":{"size":82266,"mtime_ns":1784119136237565207,"hash":"e0c15f9bd4525b6e447a7e8da3aa6bf93c14c47a1b99dc5cb4bee9dfd1a60e8b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_menu_groups.sql":{"size":6367,"mtime_ns":1783256049450807031,"hash":"5f6964d1ea2712d3d72c8f43f08c16597d85fe79f3f54c8be99962090d6c2b67"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_pl_doc_reminder_menu_group.sql":{"size":1562,"mtime_ns":1783256051207137180,"hash":"82a0ce8c5293f5051967633c1c2e0a1dc60e1fc7c97919c9e0866246bbcbe3d1"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260711_hide_vehicle_urea_menus.sql":{"size":269,"mtime_ns":1783760394518896877,"hash":"a5f66647cdb4f0f403689b47b804f8099d7d710753823ef3ddc5e7a5a9ca4ca8"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_dibang_plan_quantity.py":{"size":4258,"mtime_ns":1783777759829460303,"hash":"6af8eb3c1f6c7ad34cd62f1e82e9c6b96737ffd48ba74dda7c5f888c55dd11cd"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py":{"size":10503,"mtime_ns":1783946967244318920,"hash":"07991890446a106b2d520e5e3c65e27af90c13d00945516ef1ce9bdf2e389879"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_pl_personnel_type.sql":{"size":1662,"mtime_ns":1783843576747470605,"hash":"24fcd156f97e2bdf2bd158beb5358fe7159874783a84716c6c18d2565cc8ac1a"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_pl_personnel_affiliation.sql":{"size":753,"mtime_ns":1783845019912997576,"hash":"6d450d264921f43c4e74ee4a2bcfc8335d16f77d9e0413267389741a92bf4eb5"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_add_finance_dept.sql":{"size":1697,"mtime_ns":1783855927743333340,"hash":"4ce457089ead5c063ae8e1a6d6553416deb43c54a7edddb574ee2572390aba99"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/setup_business_roles.py":{"size":14256,"mtime_ns":1783858477739135462,"hash":"ac12df2cf75826931c85f4deb3ae28614e31ea825d0beb3cf351a8d4f5cb1071"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_reset_sys_dept.sql":{"size":4517,"mtime_ns":1783855538627076795,"hash":"3795d6127a4dcf55d406be0fe673b8a83655723fe1f07b855e6fe45823d489fa"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_users.py":{"size":28602,"mtime_ns":1783855936414773748,"hash":"f5626fd3ca8bbc16275862080365c3ea2a27fd0f608e2243a284493762a8f735"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/verify_web_menu_codes.py":{"size":2535,"mtime_ns":1783858485781974173,"hash":"16c9c072b5b80fb309d56823963ebd0dd1f437a3f74d7f82711dd8a7c2de86d2"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/refresh_business_role_menus.py":{"size":3812,"mtime_ns":1783858483854587001,"hash":"b2c76109936791e1f624e239b24832237fa0256b38b98578e39825ac215fb166"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/renumber_crm_product_no.py":{"size":9071,"mtime_ns":1783861492796558653,"hash":"320c63801100ea8e9fb62abfb44b3487139377e293310593a877ab2d06e9d2e3"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_product.py":{"size":13287,"mtime_ns":1783860925852062495,"hash":"3e5dd462f2ad6218d2547b66db741132d0179037e6dd3ac24ddaa02a5b970d4b"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_product_category.py":{"size":15583,"mtime_ns":1783860690842141129,"hash":"9cbdfdf3335a366aebd0a605431b3952ea9aab26bd564d075d501479ae0b20f7"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/compare_dict_snapshots.py":{"size":11072,"mtime_ns":1783860128258640426,"hash":"cfc98cc72c5e387317cfa0a7670c47067fffe8f2e540d5cd580ee459e173e4ef"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_sys_oss.py":{"size":22858,"mtime_ns":1783885816251409459,"hash":"20f59e7e19a0c4415bdf17cef32a31db63220346688b3dbe06bc2c52a42d475c"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration/minio_url.py":{"size":4770,"mtime_ns":1783884947929403985,"hash":"54a1bf95512c0a8b5feeb518ea3af76983bb1b0a3ba645898b07debdb2cc52e9"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_customer.py":{"size":25235,"mtime_ns":1783886258025843303,"hash":"f30d5a50a28a1130325a9d9db88586bcae9e2a39434f077ded25f847af2ed66f"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_opportunity.py":{"size":23720,"mtime_ns":1783887271166904779,"hash":"f98964806f32c3020bdd7d644d930760d6ea3ad5162d5f1d6125494263dd5598"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_payment_invoice.py":{"size":13601,"mtime_ns":1783888353771580120,"hash":"c4af77164dac9ba4195c3d239c9679ea367b92f5f7500139f3b59547525af24e"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_clue.py":{"size":18423,"mtime_ns":1783887050391179085,"hash":"72b84e497176bc63264c26d891c591c839f9dd9788988043c5752413c1b62a09"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_bx_data.py":{"size":32783,"mtime_ns":1783888844450695833,"hash":"16751fcd542e209535216a0516d78b62eecb81732d7fe0f9ec051fdf811e93b6"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_contract.py":{"size":36188,"mtime_ns":1783888258230435217,"hash":"a6269c8eeff19f4ae8ff7d22a3544ebc4a1858aaecd59583e6afd140a7fa766e"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migrate_crm_quote.py":{"size":27580,"mtime_ns":1783887475224438064,"hash":"4ac389eb6cb7437d4f5e8203d9908efec9836046ce8628a5e4be1f8b8ddd52cd"},"/Users/zlwu/work/00code/shanxia2.0/backend/tests/test_pl_personnel_cert_required.py":{"size":2829,"mtime_ns":1783946968430310316,"hash":"e89b3aa798794dc77085af5c4b880cdddcd2c0590c3cc3414f8a5389fd5f6d93"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260713_pl_personnel_cert_split.sql":{"size":3436,"mtime_ns":1783946793684378982,"hash":"ce61899ec7e13f41de08dbc923dd1ff2a1a976603d13c8f09aa6fd5be9c914a2"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/ocr.py":{"size":1889,"mtime_ns":1784035957225824939,"hash":"1de70651a8c1c9e0e208bdccb45d0b9c7e7a0f8f9a663941bdbd5ae278234b1a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_ocr_service.py":{"size":11166,"mtime_ns":1784036700055450000,"hash":"6f233150e253813a7c9da30e6e5d6046fdea01baa67c2589a332fe511ab795f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/test_aliyun_driving_license_ocr.py":{"size":5130,"mtime_ns":1784032210609057675,"hash":"13d43c9144c3795b8870fbd900ed14f97ae22ee043e2c265a5151525666abebe"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260714_pl_ocr_usage_month.sql":{"size":1270,"mtime_ns":1784032423540552834,"hash":"d7018a4e5be9760846731440be98e53584d8e1461fc4ea54a0c8db61c666e7fe"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/price_letter.py":{"size":3994,"mtime_ns":1784122554297827011,"hash":"6cd75fdbbd6a2dc65cf0b0b29e6206910fbd86f8bedf05bb56a8835130b81829"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/price_letter.py":{"size":13292,"mtime_ns":1784122560635390924,"hash":"1edbd17d41fcc4205faf15de5ce47aecc5459a39d06a920689379a65cdb9c3d9"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260715_crm_price_letter.sql":{"size":11104,"mtime_ns":1784122573332360310,"hash":"548dab479fcde4bf1628ca2093fd7840720473a774a5289b55bfd4f46b902153"},"/Users/zlwu/work/00code/shanxia2.0/backend/venv.bak.broken/bin/Activate.ps1":{"size":8593,"mtime_ns":1780108930820914793,"hash":"ed2ea10761f2798e8980084d754284d3fcb94f1284ae7085c35c16108fe24846"},"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260715_crm_price_letter_bill_price.sql":{"size":901,"mtime_ns":1784122547991214820,"hash":"11a4eb6f3b4d89c1a5141bd9a85e931517984b77ac213f5842dc590f928fe1cf"}}
\ No newline at end of file
diff --git a/docs/miniprogram-dev-checklist.md b/docs/miniprogram-dev-checklist.md
new file mode 100644
index 000000000..da70ad018
--- /dev/null
+++ b/docs/miniprogram-dev-checklist.md
@@ -0,0 +1,254 @@
+# 山峡小程序 · 四模块开发清单
+
+> **用途**:明确移动端开发范围、分期节奏与进度。阶段性完成后请把对应项从 `[ ]` 改为 `[x]`,并更新文首「进度总览」。
+> **权威 UI**:`mobile-ui/shanxia-mp-*.html` + `docs/miniprogram-ui-spec-sanqiang.md` + Pattern 四页。
+> **附件基线**:仅拍照凭证 → OpenSpec `zkxw-engineering-standards`(ui/api/architecture「小程序拍照凭证上传」)。
+> **更新日期**:2026-07-16
+
+---
+
+## 进度总览
+
+| 阶段 | 说明 | 状态 |
+|------|------|------|
+| P0 基础设施 | 登录、Tab 壳、工作台、我的、上传工具、`/api/mini` 约定 | **基本完成**(见 §2;Pattern 组件随 P1 抽取) |
+| P1 CRM | 线索 / 商机 / 客户 / 公海 / 报价 / 合同 | **基本完成**(列表/详情已接 `/web/crm/*`;联调冒烟待做) |
+| P2 报销 | 待办、报销/借款申请与审批、我的单据 | **基本完成**(已接 `/reimburses` `/advances`;联调冒烟待做) |
+| P3 地磅 | 仪表盘、计划、地磅单、出入库 | **基本完成**(已接 `/dashboard` `/plans` `/weighing` `/inventory`;联调冒烟待做) |
+| P4 运输 | 计划、归档、购买证、运费结算/审批 | **基本完成**(已接 `/transport/*`;联调冒烟待做) |
+| P5 收尾 | 联调、合法域名、发版检查、文档回写 | **接口冒烟完成**;真机 UI / 域名待做 |
+
+**图例**:`[ ]` 未开始 · `[~]` 进行中 · `[x]` 已完成 · `[-]` 本期不做(灰卡/明确排除)
+
+---
+
+## 1. 范围边界
+
+### 1.1 本期要做(Web 一级菜单 → 小程序)
+
+| 模块 | Web 路径 | 小程序入口 | Demo |
+|------|----------|------------|------|
+| CRM 管理 | `/crm` | 工作台 → CRM 区块 | `shanxia-mp-crm-demo.html` |
+| 报销管理 | `/bx` | 工作台 → 报销区块 | `shanxia-mp-bx-demo.html` |
+| 地磅管理 | `/db` | 工作台 → 地磅区块 | `shanxia-mp-db-demo.html` |
+| 运输管理 | `/pl` | 工作台 → 运输区块 | `shanxia-mp-pl-demo.html` |
+
+### 1.2 明确不做
+
+| 项 | 说明 |
+|----|------|
+| 工作流管理 `/workflow` | 不单独做一级模块;报销审批走 BX 业务接口 |
+| 系统管理 `/system` | 租户/菜单/角色等仅 Web |
+| Tab「首页」「待办」业务内容 | Demo 标 Phase 2:壳可占位,本期不开发实质功能 |
+| Web 专属配置页 | 如 BX 费用项目/工作流配置、CRM 产品主数据维护等(见各模块灰卡) |
+| 小程序文档类附件 | PDF/Word 等;仅拍照凭证(见 OpenSpec) |
+
+### 1.3 信息架构(与 Demo 一致)
+
+```
+登录 → 默认「工作台」Tab
+Tab:首页(占位) | 待办(占位) | 工作台★ | 我的
+工作台:四大模块菜单卡片 → 业务列表/详情/表单(无 TabBar,返回工作台)
+```
+
+---
+
+## 2. P0 基础设施
+
+> 目标:四模块可挂载的统一壳 + 鉴权 + 上传 + 列表/详情模式对齐 Pattern。
+
+### 2.1 工程与壳
+
+- [x] uni-app 工程存在(`miniprogram/`,自 sqxp 精简)
+- [x] 登录页 + 协议/隐私 + RSA 登录对接 Web 公共鉴权
+- [x] 占位首页 `pages/home`
+- [x] TabBar:首页 / 待办 / 工作台 / 我的(登录默认进工作台)
+- [x] 工作台页:四模块 + 菜单卡片(对齐 `shanxia-mp-workbench-demo.html`;灰卡禁用)
+- [x] 「我的」:展示用户信息、退出登录(对齐 `shanxia-mp-mine-demo.html`)
+- [x] 首页 / 待办:占位页即可(本期不做业务)
+- [x] 全局样式 Token 对齐 `shanxia-mp-base.css` / UI 规范(`styles/mp-shell.scss`)
+- [~] Pattern 落地:列表 / 查看 / 表单 / 反馈 — Demo 已有;可复用组件随 **P1** 首业务页抽取(见 `pages/common/patterns-readme.md`)
+
+### 2.2 网络与上传
+
+- [x] `config/api.js`:区分环境基址;真机局域网说明(注释)
+- [x] 业务 API 路径约定:P0 认证复用 `/web/auth/*`;上传复用 `/web/admin/attachment/upload`;业务 `/api/mini/*` 随模块补充
+- [x] `utils/upload.js`:`chooseImage` + `uploadFile` + URL 收集(OpenSpec 基线)
+- [x] 上传路径已在服务端 `API_ENCRYPT_EXCLUDE_PATHS`(`/api/web/admin/attachment/upload`)
+- [ ] 微信合法域名(request / uploadFile)清单与发版脚本核对(发版前 / P5)
+
+### 2.3 菜单与权限
+
+- [x] 工作台菜单前端写死四模块(`config/workbench-menus.js`,与 Demo 灰卡一致)
+- [ ] 按角色权限过滤/置灰(有菜单权限接口后再接;P0 全量展示启用卡)
+
+---
+
+## 3. P1 CRM 管理
+
+> Demo 启用菜单 = 本期范围;灰卡 = 本期不做。
+
+| 菜单 | 本期 | 建议能力 | 进度 |
+|------|------|----------|------|
+| 线索管理 | 要做 | 列表/筛选 Tab、详情、新增/跟进、分配、转商机 | [x] 列表/详情/新增/跟进/转客户/无效;分配待 Web |
+| 商机管理 | 要做 | 列表、详情、跟进/阶段(看板可二期) | [x] 列表/详情/跟进;阶段变更二期 |
+| 客户管理 | 要做 | 列表、详情、跟进入口 | [x] |
+| 公海池 | 要做 | 列表、领取/分配 | [x] 列表+领取;分配待 Web 选人 |
+| 报价管理 | 要做 | 列表、详情、关键状态操作 | [x] 列表/详情(审批在 Web) |
+| 合同管理 | 要做 | 列表、详情、关键状态(无审批流复杂页可简化) | [x] 列表/详情(审批在 Web) |
+| 产品分类 | 不做 | Web 主数据 | [-] |
+| 产品管理 | 不做 | Web 主数据 | [-] |
+| 合同审批 | 不做 | Demo 灰卡 | [-] |
+| 线索/商机统计、看板、漏斗 | 不做 | Web 分析页,除非产品加开 | [-] |
+
+### P1 任务拆解
+
+- [x] 工作台 CRM 菜单卡片接线(`WORKBENCH_ROUTE_MAP`)
+- [x] 线索:列表 + 详情 + 表单/跟进(`mobile-ui` Pattern:Header/Tabs/Card/Tag/BottomBar)
+- [x] 商机:列表 + 详情 + 跟进
+- [x] 客户:列表 + 详情 + 跟进
+- [x] 公海池:列表 + 领取(分配需选人,留 Web)
+- [x] 报价:列表 + 详情
+- [x] 合同:列表 + 详情
+- [ ] 接口联调与租户/数据权限冒烟(需 admin 账号 + 后端)
+- [ ] 阶段验收:销售角色可完成「线索→商机→客户」主路径
+
+---
+
+## 4. P2 报销管理
+
+| 菜单 | 本期 | 建议能力 | 进度 |
+|------|------|----------|------|
+| 待办任务 | 要做 | 待审批/我的申请/已办 Tab;通过/驳回 | [x] |
+| 报销申请 | 要做 | 新建/编辑、明细、提交(票据以拍照凭证为主) | [x] 新建+拍照+草稿/提交 |
+| 报销审批 | 要做 | 列表 + 审批动作(可与待办合并入口) | [x] |
+| 借款申请 | 要做 | 备用金申请 | [x] |
+| 借款审批 | 要做 | 备用金审批 | [x] |
+| 我的单据 | 要做 | 我的申请单列表/详情 | [x] |
+| 借款发放 | 不做 | Demo 灰卡(偏财务后台) | [-] |
+| 全部单据 | 不做 | Demo 灰卡(总经理 Web) | [-] |
+| 付款记录 | 不做 | Demo 灰卡 | [-] |
+| 费用项目/工作流配置 | 不做 | Web 基础配置 | [-] |
+| 支付发放冲抵 | 不做 | 本期不进工作台 | [-] |
+
+### P2 任务拆解
+
+- [x] 工作台报销菜单卡片接线
+- [x] 待办任务列表(对齐 bx-demo:大麦黄 Header + Tabs + 卡片审批)
+- [x] 报销申请(表单 + 拍照凭证 `/attachments/upload` + bind)
+- [x] 报销审批动作
+- [x] 借款(备用金)申请 / 审批
+- [x] 我的单据
+- [ ] 接口联调与审批状态机冒烟
+- [ ] 阶段验收:申请人提交 → 审批人通过/驳回闭环
+
+---
+
+## 5. P3 地磅管理
+
+| 菜单 | 本期 | 建议能力 | 进度 |
+|------|------|----------|------|
+| 仪表盘 | 要做 | 关键统计卡片(只读) | [x] |
+| 地磅计划 | 要做 | 列表、详情、必要办理动作 | [x] 列表/详情/生成出入库关联 |
+| 地磅单管理 | 要做 | 列表、详情、二次过磅等现场动作 | [x] 二次过磅 + 当天作废 |
+| 地磅出入库 | 要做 | 列表、详情 | [x] |
+
+### P3 任务拆解
+
+- [x] 工作台地磅菜单卡片接线
+- [x] 仪表盘(对齐 db-demo 青绿 Header + 统计卡)
+- [x] 地磅计划列表/详情
+- [x] 地磅单列表/详情/二次过磅/作废
+- [x] 出入库列表/详情
+- [ ] 接口联调(注意与地磅系统边界,见 ecosystem-landscape)
+- [ ] 阶段验收:查计划 → 看待过磅单 → 完成一次现场办理路径
+
+---
+
+## 6. P4 运输管理
+
+| 菜单 | 本期 | 建议能力 | 进度 |
+|------|------|----------|------|
+| 运输计划 | 要做 | 列表 Tab、详情、安排运输等 | [x] 待发运/在途/已完成 + 安排运输 |
+| 计划归档 | 要做 | 只读列表/详情 | [x] |
+| 购买证上传 | 要做 | 列表 + **拍照凭证**上传(OpenSpec) | [x] 拍照 + 证书齐备 |
+| 运费结算 | 要做 | 列表、详情、提交 | [x] 结算单/待结算生成 + 提交审批 |
+| 运费审批 | 要做 | 列表、审批 | [x] |
+| 财务发放 | 不做 | Demo 灰卡 | [-] |
+| 车辆充值 | 不做 | Demo 灰卡(ETC 等) | [-] |
+| 车辆维修 | 不做 | Demo 灰卡 | [-] |
+| 运输地址库 | 不做 | Demo 灰卡(主数据) | [-] |
+| 基础信息库其余页 | 不做 | 车辆/承运商/驾押等主数据偏 Web | [-] |
+| 证件提醒 / 尿素等 | 不做 | 未进工作台启用卡 | [-] |
+
+### P4 任务拆解
+
+- [x] 工作台运输菜单卡片接线
+- [x] 运输计划列表/详情/办理(对齐 pl-demo)
+- [x] 计划归档
+- [x] 购买证上传(拍照凭证)
+- [x] 运费结算 + 运费审批(含选人提交审批)
+- [ ] 接口联调
+- [ ] 阶段验收:计划办理 → 购买证上传 → 运费提交/审批主路径
+
+---
+
+## 7. P5 收尾与发版
+
+- [x] 四模块冒烟清单(角色 × 主路径)— 接口级已跑通,见 §7.1
+- [ ] 真机:登录、列表、上传、审批/办理
+- [ ] `npm run release:check-api`(或等价)通过
+- [ ] 微信后台域名与上传域名配置完成
+- [x] README / 本清单进度回写(接口冒烟结论已写入)
+- [ ] 可选:OpenSpec 归档或增补「小程序四模块」变更说明
+
+### 7.1 四模块冒烟清单(联调时勾选)
+
+> 账号:具备对应模块权限的 admin / 业务角色;基址见 `miniprogram/config/api.js`。
+> **接口冒烟执行**:2026-07-17,`admin` / 本机 `http://127.0.0.1:8000/api`,脚本 `miniprogram/scripts/mp_api_smoke.py`(结果见 `mp_api_smoke_last.json`)。
+> 图例:`[x]` 接口可达且读通;写操作因缺样本/字段校验未闭环的在备注中说明。
+
+| # | 模块 | 路径 | 预期 | 结果 |
+|---|------|------|------|------|
+| S1 | 公共 | 登录 → current-user | Token 可用 | [x] 登录+current-user ok |
+| S2 | CRM | 线索列表 → 详情 → 跟进 | 列表/详情正常 | [x] 列表+详情 ok;跟进 body 422(字段待对齐) |
+| S3 | CRM | 公海列表/领取 | 列表正常 | [x] 列表空态 ok(无领取样本) |
+| S4 | 报销 | 我的报销 / 新建 | 列表可读 | [x] my 列表 ok;新建缺 `details` 字段 422(小程序 payload 待对齐) |
+| S5 | 报销 | 待审批列表 | 列表正常 | [x] 空态 ok(无待审样本) |
+| S6 | 地磅 | 仪表盘 + 地磅单列表 | 数字/列表可读 | [x] stats+列表 ok;无二次过磅样本 |
+| S7 | 运输 | 计划列表 / 安排 | 列表正常 | [x] 列表 ok;安排因证书未齐备业务拒绝(接口可达) |
+| S8 | 运输 | 购买证相关计划列表 | 列表正常 | [x] 列表空态 ok |
+| S9 | 运输 | 运费结算 + 用户选项 | 列表/选人接口正常 | [x] freight 列表 + users/options ok;无待结算 |
+| S10 | 运输 | 运费待审批列表 | 列表正常 | [x] 列表空态 ok |
+
+**接口冒烟结论**:S1–S10 **读路径全部通**(0 FAIL)。写路径有 3 处需跟进:① 线索跟进字段;② 报销创建体应用 `details` 而非 `items`;③ 安排运输依赖证书齐备业务数据。
+**前端 UI**:仍由人工在微信开发者工具/真机验收。
+
+---
+
+## 8. 推荐开发顺序
+
+1. **P0** 壳与工作台(否则业务页无处挂)
+2. **P2 报销** 或 **P1 CRM**(审批/跟进闭环短,易验证上传与列表模式)
+3. **P3 地磅**(现场办理,偏读+少量动作)
+4. **P4 运输**(计划+运费+购买证,依赖上传基线)
+5. **P5** 联调发版
+
+> 顺序可按业务优先级调整;调整时只改本节与文首总览,子清单保持勾选状态。
+
+---
+
+## 9. 进度记录(追加)
+
+| 日期 | 阶段 | 说明 |
+|------|------|------|
+| 2026-07-16 | 范围 | 确认仅 CRM / 地磅 / 运输 / 报销四模块;本清单建立 |
+| 2026-07-16 | 规范 | 拍照凭证上传写入 OpenSpec engineering-standards |
+| 2026-07-16 | P0 | TabBar + 工作台/我的/占位页 + upload.js + mp-shell;清单回写 |
+| 2026-07-16 | P1–P4 | CRM / 报销 / 地磅 / 运输业务页落地并接线工作台 |
+| 2026-07-16 | P4+ | 运费提交审批改为用户选项选人 Sheet;P5 冒烟表写入 §7.1 |
+| 2026-07-17 | P5 | 接口冒烟 S1–S10 读路径全通(脚本 `mp_api_smoke.py`);写路径 3 处待对齐见 §7.1 |
+
+
diff --git a/miniprogram/README.md b/miniprogram/README.md
index 035906542..ebb6982ec 100644
--- a/miniprogram/README.md
+++ b/miniprogram/README.md
@@ -62,11 +62,63 @@ npm run release:check-api # CI 严格模式:仍为本机地址则失败
## 视觉规范
-UI Token 与页面骨架以仓库根目录 **`docs/miniprogram-ui-spec-sanqiang.md`** 及 `mobile-ui/sanqiang-*.html` Demo 为准。主品牌色:`#0a9f6d`。
+UI Token 与页面骨架以仓库根目录 **`mobile-ui/`**(`shanxia-mp-base.css`、`shanxia-mp-*.html`)及 **`docs/miniprogram-ui-spec-sanqiang.md`** 为准。主品牌色:`#0a9f6d`(勿混用 Web Fresh Mint `#10b981`)。
+
+## CRM(P1)
+
+工作台 CRM 启用菜单已接业务页:
+
+| 菜单 | 路径 |
+|------|------|
+| 线索 | `pages/crm/clue/*` |
+| 商机 | `pages/crm/opportunity/*` |
+| 客户 | `pages/crm/customer/*` |
+| 公海 | `pages/crm/pool/list` |
+| 报价 | `pages/crm/quote/*` |
+| 合同 | `pages/crm/contract/*` |
+
+接口复用管理端 `/web/crm/*`(需 admin 权限账号)。API 封装:`api/crm.js`。
+
+## 报销(P2)
+
+| 菜单 | 路径 |
+|------|------|
+| 待办任务 | `pages/bx/todo/list` |
+| 报销申请 | `pages/bx/reimburse/*` |
+| 报销/借款审批 | `pages/bx/approve/list?type=` |
+| 借款申请 | `pages/bx/advance/*` |
+| 我的单据 | `pages/bx/mine/list` |
+
+接口:`/reimburses`、`/advances`、`/expense-items`、`/attachments`(协议 `code===0`)。封装:`api/bx.js`。拍照凭证走 BX 附件上传。
+
+## 地磅(P3)
+
+| 菜单 | 路径 |
+|------|------|
+| 仪表盘 | `pages/db/dashboard/index` |
+| 地磅计划 | `pages/db/plans/*` |
+| 地磅单 | `pages/db/weighing/*`(二次过磅 / 作废) |
+| 出入库 | `pages/db/inventory/*` |
+
+接口:`/dashboard`、`/plans`、`/weighing`、`/inventory`(**直出 JSON**,非门户信封)。封装:`api/db.js`。
+
+## 运输(P4)
+
+| 菜单 | 路径 |
+|------|------|
+| 运输计划 | `pages/pl/plan/*`(列表 Tab / 详情 / 安排运输) |
+| 计划归档 | `pages/pl/archive/list` |
+| 购买证上传 | `pages/pl/cert/*`(拍照凭证) |
+| 运费结算 | `pages/pl/freight/list` + `detail` |
+| 运费审批 | `pages/pl/freight/approval` |
+
+接口:`/transport/plan`、`/transport/order/arrange`、`/transport/freight`(协议兼容 `code===0` 或直出)。封装:`api/pl.js`。
+
+运费提交审批:底部 Sheet 多选审批人(`/web/admin/users/options` + `components/mp-approver-picker`),支持或签/顺序/会签。
## 后续建设建议
-1. 在 `pages/` 下按业务域新增页面(如客户端、供应商端等)
-2. 在 `pages.json` 注册路由与 tabBar
-3. 在 `api/` 目录补充业务接口模块
-4. 后端新增 `/api/mini/*` 时同步更新 `config/api.js`
+开发范围与分期进度见 **`docs/miniprogram-dev-checklist.md`**(含 §7.1 冒烟表)。
+
+1. ~~P0~~ / ~~P1 CRM~~ / ~~P2 报销~~ / ~~P3 地磅~~ / ~~P4 运输~~ → **P5** 按冒烟表联调 + 合法域名 + `npm run release:check-api`
+2. 灰卡菜单仍走 `biz-placeholder`;联调时核对微信合法域名与 `config/api.js` 基址
diff --git a/miniprogram/api/admin.js b/miniprogram/api/admin.js
new file mode 100644
index 000000000..abe0cf973
--- /dev/null
+++ b/miniprogram/api/admin.js
@@ -0,0 +1,41 @@
+/**
+ * 管理端公共选项(审批人选人等)
+ * 协议:门户信封 code===200
+ */
+import { request, parseResponseJson } from '@/utils/request'
+
+function unwrap(res) {
+ const body = parseResponseJson(res)
+ if (!body || typeof body !== 'object') {
+ throw new Error('接口响应异常')
+ }
+ if (body.code != null && body.code !== 200) {
+ throw new Error(body.message || body.msg || '请求失败')
+ }
+ return body.data != null ? body.data : body
+}
+
+/**
+ * @returns {Promise>}
+ */
+export async function fetchUserOptions() {
+ const res = await request({
+ url: '/web/admin/users/options',
+ method: 'GET'
+ })
+ const data = unwrap(res)
+ const list = Array.isArray(data) ? data : []
+ return list
+ .map((item) => {
+ const userId = String(item.value ?? item.userId ?? item.id ?? '')
+ const nickName = String(item.nickName ?? item.label ?? item.userName ?? '')
+ const userNo = String(item.userNo ?? item.userName ?? '')
+ return {
+ userId,
+ nickName,
+ userNo,
+ label: nickName || userNo || userId
+ }
+ })
+ .filter((u) => u.userId)
+}
diff --git a/miniprogram/api/bx.js b/miniprogram/api/bx.js
new file mode 100644
index 000000000..04e9d1c74
--- /dev/null
+++ b/miniprogram/api/bx.js
@@ -0,0 +1,191 @@
+/**
+ * 报销模块 API(REST,协议 code===0 / msg / data.rows)
+ * 路径挂在 /api 下:/reimburses、/advances、/expense-items、/attachments
+ */
+import { request, parseResponseJson } from '@/utils/request'
+
+function unwrap(res) {
+ const body = parseResponseJson(res)
+ if (!body || typeof body !== 'object') {
+ throw new Error('接口响应异常')
+ }
+ if (body.code != null && body.code !== 0) {
+ throw new Error(body.msg || body.message || '请求失败')
+ }
+ return body.data != null ? body.data : body
+}
+
+async function get(url, params = {}) {
+ const res = await request({ url, method: 'GET', data: params, params })
+ return unwrap(res)
+}
+
+async function post(url, data = {}) {
+ const res = await request({ url, method: 'POST', data })
+ return unwrap(res)
+}
+
+async function put(url, data = {}, params = {}) {
+ const res = await request({ url, method: 'PUT', data, params })
+ return unwrap(res)
+}
+
+function pageParams(query = {}) {
+ const { pageNum, page, pageSize, ...rest } = query
+ const params = {
+ page: pageNum ?? page ?? 1,
+ pageSize: pageSize ?? 20
+ }
+ Object.keys(rest).forEach((k) => {
+ if (rest[k] != null && rest[k] !== '') params[k] = rest[k]
+ })
+ return params
+}
+
+function normalizeList(data) {
+ if (Array.isArray(data)) return { rows: data, total: data.length }
+ const d = data || {}
+ return {
+ rows: d.rows || d.list || [],
+ total: d.total ?? 0
+ }
+}
+
+// ── 费用项目 ──
+
+export async function getExpenseItemTree() {
+ const data = await get('/expense-items/tree')
+ return Array.isArray(data) ? data : []
+}
+
+/** 展平可选叶子费用项目(供 picker) */
+export function flattenExpenseLeaves(tree, path = []) {
+ const out = []
+ ;(tree || []).forEach((node) => {
+ const name = node.itemName || node.item_name || ''
+ const nextPath = path.concat(name)
+ const children = node.children || []
+ if (children.length) {
+ out.push(...flattenExpenseLeaves(children, nextPath))
+ } else {
+ out.push({
+ id: String(node.id),
+ name,
+ pathLabel: nextPath.join(' / ')
+ })
+ }
+ })
+ return out
+}
+
+// ── 报销 ──
+
+export async function listMyReimburse(query = {}) {
+ return normalizeList(await get('/reimburses/my', pageParams(query)))
+}
+
+export async function listPendingReimburse(query = {}) {
+ return normalizeList(await get('/reimburses/pending-approval', pageParams(query)))
+}
+
+export async function listApprovedByMeReimburse(query = {}) {
+ return normalizeList(await get('/reimburses/approved-by-me', pageParams(query)))
+}
+
+export async function getReimburse(id) {
+ return get(`/reimburses/${String(id)}`)
+}
+
+export async function generateReimburseTitle(reimburseDate) {
+ const params = {}
+ if (reimburseDate) params.reimburseDate = reimburseDate
+ return get('/reimburses/generate-title', params)
+}
+
+export function createReimburse(data) {
+ return post('/reimburses/', data)
+}
+
+export function updateReimburse(id, data) {
+ return put(`/reimburses/${String(id)}`, data)
+}
+
+export function submitReimburse(id) {
+ return post(`/reimburses/${String(id)}/submit`)
+}
+
+export function approveReimburse(id, approvalOpinion = '') {
+ return post(`/reimburses/${String(id)}/approve`, { approvalOpinion })
+}
+
+export function rejectReimburse(id, approvalOpinion = '') {
+ return post(`/reimburses/${String(id)}/reject`, { approvalOpinion })
+}
+
+export async function getReimburseApprovalHistory(id) {
+ const data = await get(`/reimburses/${String(id)}/approval-history`)
+ return Array.isArray(data) ? data : []
+}
+
+// ── 借款(备用金) ──
+
+export async function listMyAdvance(query = {}) {
+ return normalizeList(await get('/advances/my', pageParams(query)))
+}
+
+export async function listPendingAdvance(query = {}) {
+ return normalizeList(await get('/advances/pending-approval', pageParams(query)))
+}
+
+export async function listApprovedByMeAdvance(query = {}) {
+ return normalizeList(await get('/advances/approved-by-me', pageParams(query)))
+}
+
+export async function getAdvance(id) {
+ return get(`/advances/${String(id)}`)
+}
+
+export function createAdvance(data) {
+ return post('/advances/', data)
+}
+
+export function submitAdvance(id) {
+ return post(`/advances/${String(id)}/submit`)
+}
+
+export function approveAdvance(id, approvalOpinion = '') {
+ return post(`/advances/${String(id)}/approve`, { approvalOpinion })
+}
+
+export function rejectAdvance(id, approvalOpinion = '') {
+ return post(`/advances/${String(id)}/reject`, { approvalOpinion })
+}
+
+export async function getAdvanceApprovalHistory(id) {
+ const data = await get(`/advances/${String(id)}/approval-history`)
+ return Array.isArray(data) ? data : []
+}
+
+// ── 附件 ──
+
+export async function listAttachments(bizType, bizId) {
+ const data = await get('/attachments/list', {
+ biz_type: bizType,
+ biz_id: String(bizId)
+ })
+ return Array.isArray(data) ? data : []
+}
+
+export async function bindAttachments(ids, bizType, bizId) {
+ if (!ids?.length) return 0
+ const q = [
+ `ids=${encodeURIComponent(ids.map(String).join(','))}`,
+ `biz_type=${encodeURIComponent(bizType)}`,
+ `biz_id=${encodeURIComponent(String(bizId))}`
+ ].join('&')
+ return put(`/attachments/bind-biz?${q}`, {})
+}
+
+export async function previewAttachment(id) {
+ return get(`/attachments/preview/${String(id)}`)
+}
diff --git a/miniprogram/api/crm.js b/miniprogram/api/crm.js
new file mode 100644
index 000000000..3f12c8a38
--- /dev/null
+++ b/miniprogram/api/crm.js
@@ -0,0 +1,215 @@
+/**
+ * CRM 管理端接口(复用 /web/crm/*,需 admin JWT)
+ */
+import { request, parseResponseJson } from '@/utils/request'
+
+function unwrap(res) {
+ const body = parseResponseJson(res)
+ if (!body || typeof body !== 'object') {
+ throw new Error('接口响应异常')
+ }
+ if (body.code != null && body.code !== 200) {
+ throw new Error(body.message || '请求失败')
+ }
+ return body.data != null ? body.data : body
+}
+
+async function post(url, data = {}) {
+ const res = await request({ url, method: 'POST', data })
+ return unwrap(res)
+}
+
+async function get(url, params = {}) {
+ const res = await request({ url, method: 'GET', data: params, params })
+ return unwrap(res)
+}
+
+function pageBody(query = {}) {
+ const { pageNum, page, pageSize, ...rest } = query
+ return {
+ page: pageNum ?? page ?? 1,
+ pageSize: pageSize ?? 20,
+ ...rest
+ }
+}
+
+function normalizeList(data) {
+ if (Array.isArray(data)) {
+ return { rows: data, total: data.length }
+ }
+ const d = data || {}
+ return {
+ rows: d.list || d.rows || [],
+ total: d.total ?? (d.list || d.rows || []).length
+ }
+}
+
+// ── 线索 ──
+
+export async function listClue(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(await post('/web/crm/clue/list', pageBody({ pageNum, pageSize, ...rest })))
+}
+
+export async function listMyClue(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(await post('/web/crm/clue/mine', pageBody({ pageNum, pageSize, ...rest })))
+}
+
+export async function getClue(id) {
+ return post('/web/crm/clue/detail', { id: String(id) })
+}
+
+export function addClue(data) {
+ return post('/web/crm/clue/add', data)
+}
+
+export function updateClue(data) {
+ return post('/web/crm/clue/update', data)
+}
+
+export function convertClue(data) {
+ return post('/web/crm/clue/convert', {
+ id: String(data.clueId ?? data.id),
+ customerName: data.customerName,
+ customerType: data.customerType,
+ contactName: data.contactName,
+ contactMobile: data.contactMobile,
+ remark: data.remark
+ })
+}
+
+export function markClueInvalid(data) {
+ return post('/web/crm/clue/invalid', {
+ id: data.id != null ? String(data.id) : undefined,
+ remark: data.remark
+ })
+}
+
+export async function listClueFollow(clueId, pageNum = 1, pageSize = 20) {
+ return normalizeList(
+ await post('/web/crm/clue/follow/list', {
+ clueId: String(clueId),
+ page: pageNum,
+ pageSize
+ })
+ )
+}
+
+export function addClueFollow(data) {
+ return post('/web/crm/clue/follow/add', {
+ ...data,
+ clueId: String(data.clueId)
+ })
+}
+
+// ── 商机 ──
+
+export async function listOpportunity(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(
+ await post('/web/crm/opportunity/list', pageBody({ pageNum, pageSize, ...rest }))
+ )
+}
+
+export async function getOpportunity(id) {
+ return post('/web/crm/opportunity/detail', { id: String(id) })
+}
+
+export async function listOpportunityFollow(opportunityId, pageNum = 1, pageSize = 20) {
+ return normalizeList(
+ await post('/web/crm/opportunity/follow/list', {
+ opportunityId: String(opportunityId),
+ page: pageNum,
+ pageSize
+ })
+ )
+}
+
+export function addOpportunityFollow(data) {
+ return post('/web/crm/opportunity/follow/add', {
+ ...data,
+ opportunityId: String(data.opportunityId)
+ })
+}
+
+// ── 客户 ──
+
+export async function listCustomer(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(
+ await post('/web/crm/customer/list', pageBody({ pageNum, pageSize, ...rest }))
+ )
+}
+
+export async function listMyCustomer(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(
+ await post('/web/crm/customer/mine', pageBody({ pageNum, pageSize, ...rest }))
+ )
+}
+
+export async function getCustomer(id) {
+ return post('/web/crm/customer/detail', { id: String(id) })
+}
+
+export async function listCustomerFollow(customerId, pageNum = 1, pageSize = 20) {
+ return normalizeList(
+ await post('/web/crm/customer/follow/byCustomer', {
+ customerId: String(customerId),
+ page: pageNum,
+ pageSize
+ })
+ )
+}
+
+export function addCustomerFollow(data) {
+ return post('/web/crm/customer/follow/add', {
+ ...data,
+ customerId: String(data.customerId)
+ })
+}
+
+// ── 公海 ──
+
+export async function listPoolCustomer(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(
+ await post('/web/crm/customer/pool/list', pageBody({ pageNum, pageSize, ...rest }))
+ )
+}
+
+export function claimCustomer(ids) {
+ const idList = Array.isArray(ids) ? ids.map(String) : [String(ids)]
+ return post('/web/crm/customer/claim', { ids: idList })
+}
+
+// ── 报价 ──
+
+export async function listQuote(query = {}) {
+ const { pageNum, pageSize, ...rest } = query
+ return normalizeList(await post('/web/crm/quote/list', pageBody({ pageNum, pageSize, ...rest })))
+}
+
+export async function getQuote(id) {
+ return post('/web/crm/quote/detail', { id: String(id) })
+}
+
+// ── 合同(GET) ──
+
+export async function listContract(query = {}) {
+ const params = {
+ pageNum: query.pageNum ?? 1,
+ pageSize: query.pageSize ?? 20
+ }
+ ;['keyword', 'contractNo', 'contractName', 'customerName', 'status', 'approvalStatus'].forEach(
+ (k) => {
+ if (query[k] != null && query[k] !== '') params[k] = query[k]
+ }
+ )
+ return normalizeList(await get('/web/crm/contract/list', params))
+}
+
+export async function getContract(id) {
+ return get(`/web/crm/contract/${String(id)}`)
+}
diff --git a/miniprogram/api/db.js b/miniprogram/api/db.js
new file mode 100644
index 000000000..e0931be7a
--- /dev/null
+++ b/miniprogram/api/db.js
@@ -0,0 +1,126 @@
+/**
+ * 地磅模块 API(直出 JSON,对齐原 dibang;路径 /plans /weighing /inventory /dashboard)
+ */
+import { request, parseResponseJson } from '@/utils/request'
+
+function extractError(body) {
+ if (!body || typeof body !== 'object') return '请求失败'
+ const detail = body.detail
+ if (typeof detail === 'string') return detail
+ if (Array.isArray(detail)) {
+ return detail.map((x) => x?.msg || x?.message || '').filter(Boolean).join(';') || '请求失败'
+ }
+ return body.msg || body.message || '请求失败'
+}
+
+function unwrap(res) {
+ if (res.statusCode === 204) return null
+ if (res.statusCode >= 400) {
+ throw new Error(extractError(parseResponseJson(res)))
+ }
+ const body = parseResponseJson(res)
+ if (body == null) return body
+ if (Array.isArray(body)) return body
+ if (typeof body === 'object' && body.code != null) {
+ if (body.code !== 200 && body.code !== 0) {
+ throw new Error(body.message || body.msg || '请求失败')
+ }
+ return body.data != null ? body.data : body
+ }
+ return body
+}
+
+async function get(url, params = {}) {
+ const res = await request({ url, method: 'GET', data: params, params })
+ return unwrap(res)
+}
+
+async function post(url, data = {}) {
+ const res = await request({ url, method: 'POST', data })
+ return unwrap(res)
+}
+
+async function patch(url, data = {}) {
+ const res = await request({ url, method: 'PATCH', data })
+ return unwrap(res)
+}
+
+function asList(data) {
+ return Array.isArray(data) ? data : []
+}
+
+/** 集合根须带尾斜杠,避免 307 丢 Authorization */
+function root(path) {
+ return path.endsWith('/') ? path : `${path}/`
+}
+
+// ── 仪表盘 ──
+
+export async function getDashboardStats() {
+ return (await get('/dashboard/stats')) || {}
+}
+
+export async function getInboundByCargo() {
+ return asList(await get('/dashboard/inbound/by-cargo'))
+}
+
+export async function getOutboundByCargo() {
+ return asList(await get('/dashboard/outbound/by-cargo'))
+}
+
+// ── 计划 ──
+
+export async function listPlans(params = {}) {
+ return asList(await get(root('/plans'), params))
+}
+
+export async function getPlan(id) {
+ return get(`/plans/${String(id)}`)
+}
+
+export async function getPlanRelated(id) {
+ return (await get(`/plans/${String(id)}/related`)) || { inventories: [], weighings: [] }
+}
+
+// ── 地磅单 ──
+
+export async function listWeighing(params = {}) {
+ return asList(await get(root('/weighing'), params))
+}
+
+export async function getWeighing(id) {
+ return get(`/weighing/${String(id)}`)
+}
+
+export async function getWeighingRelated(id) {
+ return (await get(`/weighing/${String(id)}/related`)) || { plan: null, inventory: null }
+}
+
+export function updateWeighing(id, data) {
+ return patch(`/weighing/${String(id)}`, data)
+}
+
+export function voidWeighing(id) {
+ return post(`/weighing/${String(id)}/void`, {})
+}
+
+// ── 出入库 ──
+
+export async function listInventory(params = {}) {
+ return asList(await get(root('/inventory'), params))
+}
+
+export async function getInventory(id) {
+ return get(`/inventory/${String(id)}`)
+}
+
+export async function getInventoryRelated(id) {
+ return (await get(`/inventory/${String(id)}/related`)) || { plan: null, weighing: null }
+}
+
+export function createInventoryLinkForPlan(purchasePlanId, remarks) {
+ return post('/inventory/create-link-for-plan', {
+ purchasePlanId: String(purchasePlanId),
+ remarks
+ })
+}
diff --git a/miniprogram/api/pl.js b/miniprogram/api/pl.js
new file mode 100644
index 000000000..016d4c417
--- /dev/null
+++ b/miniprogram/api/pl.js
@@ -0,0 +1,124 @@
+/**
+ * 运输模块 API(/transport/*)
+ * 协议与报销侧 bx HTTP 一致:有信封时 code===0;直出 { list, total } 亦可。
+ */
+import { request, parseResponseJson } from '@/utils/request'
+
+function unwrap(res) {
+ const body = parseResponseJson(res)
+ if (!body || typeof body !== 'object') {
+ throw new Error('接口响应异常')
+ }
+ if (body.code != null && body.code !== 0) {
+ throw new Error(body.msg || body.message || '请求失败')
+ }
+ return body.data != null ? body.data : body
+}
+
+async function get(url, params = {}) {
+ const res = await request({ url, method: 'GET', data: params, params })
+ return unwrap(res)
+}
+
+async function post(url, data = {}) {
+ const res = await request({ url, method: 'POST', data })
+ return unwrap(res)
+}
+
+async function patch(url, data = {}) {
+ const res = await request({ url, method: 'PATCH', data })
+ return unwrap(res)
+}
+
+function pageParams(query = {}) {
+ const { pageNum, page, pageSize, ...rest } = query
+ const params = {
+ page: pageNum ?? page ?? 1,
+ pageSize: pageSize ?? 20
+ }
+ Object.keys(rest).forEach((k) => {
+ if (rest[k] != null && rest[k] !== '') params[k] = rest[k]
+ })
+ return params
+}
+
+function normalizeList(data) {
+ if (Array.isArray(data)) return { rows: data, total: data.length }
+ const d = data || {}
+ return {
+ rows: d.rows || d.list || [],
+ total: d.total ?? 0
+ }
+}
+
+// ── 运输计划 ──
+
+export async function listPlans(query = {}) {
+ return normalizeList(await get('/transport/plan/', pageParams(query)))
+}
+
+export async function getPlan(id) {
+ return get(`/transport/plan/${String(id)}`)
+}
+
+export async function getPlanArrangeInfo(id) {
+ return get(`/transport/plan/${String(id)}/arrange-info`)
+}
+
+export async function updateProductCerts(id, products) {
+ return patch(`/transport/plan/${String(id)}/product-certs`, { products })
+}
+
+export async function setHazardCertReady(id, ready) {
+ return patch(`/transport/plan/${String(id)}/hazard-cert-ready`, { ready: !!ready })
+}
+
+export async function archivePlan(id, body) {
+ return post(`/transport/plan/${String(id)}/archive`, body || {})
+}
+
+export async function arrangeTransport(planId, data) {
+ return post(`/transport/order/arrange/${String(planId)}`, data)
+}
+
+// ── 车辆 / 驾押(安排运输选人)──
+
+export async function listVehicles(query = {}) {
+ return normalizeList(await get('/transport/vehicle', pageParams(query)))
+}
+
+export async function listPersonnel(query = {}) {
+ return normalizeList(await get('/transport/personnel', pageParams(query)))
+}
+
+// ── 运费结算 ──
+
+export async function listFreight(query = {}) {
+ return normalizeList(await get('/transport/freight/', pageParams(query)))
+}
+
+export async function getFreight(id) {
+ return get(`/transport/freight/${String(id)}`)
+}
+
+export async function listUnsettledPlans(query = {}) {
+ return normalizeList(await get('/transport/freight/unsettled-plans', pageParams(query)))
+}
+
+export async function createFreight(data) {
+ return post('/transport/freight/', data)
+}
+
+/** 提交审批(需审批人 ID,复杂选人建议 Web;小程序可传已选 ID) */
+export async function submitFreightApproval(id, data) {
+ return post(`/transport/freight/${String(id)}/submit-approval`, data)
+}
+
+export async function processFreightApproval(approvalId, data) {
+ return post(`/transport/freight/approval/${String(approvalId)}`, data)
+}
+
+export async function getFreightApprovalFlow(id) {
+ const data = await get(`/transport/freight/${String(id)}/approval-flow`)
+ return Array.isArray(data) ? data : []
+}
diff --git a/miniprogram/components/mp-approver-picker/mp-approver-picker.vue b/miniprogram/components/mp-approver-picker/mp-approver-picker.vue
new file mode 100644
index 000000000..a7fa22533
--- /dev/null
+++ b/miniprogram/components/mp-approver-picker/mp-approver-picker.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+ 取消
+ {{ title }}
+ 确定
+
+
+
+
+
+ 加载中…
+ 暂无用户
+
+
+
+ {{ u.nickName || u.userNo || u.userId }}
+ {{ u.userNo || u.userId }}
+
+
+
+
+
+
+
diff --git a/miniprogram/config/api.js b/miniprogram/config/api.js
index 7aaf441d4..6beb05f33 100644
--- a/miniprogram/config/api.js
+++ b/miniprogram/config/api.js
@@ -24,6 +24,87 @@ export const AUTH_API = {
REFRESH_TOKEN: '/web/auth/refresh-token'
}
+/**
+ * 上传接口(multipart,须在 API_ENCRYPT_EXCLUDE_PATHS 中)
+ * 响应字段兼容 data.url / data.fileUrl;P0 复用管理端附件上传。
+ * 后续可切换为专用 /mini/.../upload/image。
+ */
+export const UPLOAD_API = {
+ IMAGE: '/web/admin/attachment/upload'
+}
+
+/** Tab / 业务页路径(供 store 跳转) */
+export const PAGE_PATHS = {
+ login: '/pages/login/login',
+ home: '/pages/home/home',
+ todo: '/pages/todo/todo',
+ workbench: '/pages/workbench/workbench',
+ mine: '/pages/mine/mine',
+ bizPlaceholder: '/pages/common/biz-placeholder',
+ crmClueList: '/pages/crm/clue/list',
+ crmClueDetail: '/pages/crm/clue/detail',
+ crmClueForm: '/pages/crm/clue/form',
+ crmOpportunityList: '/pages/crm/opportunity/list',
+ crmOpportunityDetail: '/pages/crm/opportunity/detail',
+ crmCustomerList: '/pages/crm/customer/list',
+ crmCustomerDetail: '/pages/crm/customer/detail',
+ crmPoolList: '/pages/crm/pool/list',
+ crmQuoteList: '/pages/crm/quote/list',
+ crmQuoteDetail: '/pages/crm/quote/detail',
+ crmContractList: '/pages/crm/contract/list',
+ crmContractDetail: '/pages/crm/contract/detail',
+ bxTodoList: '/pages/bx/todo/list',
+ bxReimburseList: '/pages/bx/reimburse/list',
+ bxReimburseForm: '/pages/bx/reimburse/form',
+ bxReimburseDetail: '/pages/bx/reimburse/detail',
+ bxAdvanceList: '/pages/bx/advance/list',
+ bxAdvanceForm: '/pages/bx/advance/form',
+ bxAdvanceDetail: '/pages/bx/advance/detail',
+ bxApproveList: '/pages/bx/approve/list',
+ bxMineList: '/pages/bx/mine/list',
+ dbDashboard: '/pages/db/dashboard/index',
+ dbPlansList: '/pages/db/plans/list',
+ dbPlansDetail: '/pages/db/plans/detail',
+ dbWeighingList: '/pages/db/weighing/list',
+ dbWeighingDetail: '/pages/db/weighing/detail',
+ dbInventoryList: '/pages/db/inventory/list',
+ dbInventoryDetail: '/pages/db/inventory/detail',
+ plPlanList: '/pages/pl/plan/list',
+ plPlanDetail: '/pages/pl/plan/detail',
+ plPlanArrange: '/pages/pl/plan/arrange',
+ plArchiveList: '/pages/pl/archive/list',
+ plCertList: '/pages/pl/cert/list',
+ plCertDetail: '/pages/pl/cert/detail',
+ plFreightList: '/pages/pl/freight/list',
+ plFreightDetail: '/pages/pl/freight/detail',
+ plFreightApproval: '/pages/pl/freight/approval'
+}
+
+/** 工作台菜单 code → 页面路径 */
+export const WORKBENCH_ROUTE_MAP = {
+ 'crm-clue': PAGE_PATHS.crmClueList,
+ 'crm-opportunity': PAGE_PATHS.crmOpportunityList,
+ 'crm-customer': PAGE_PATHS.crmCustomerList,
+ 'crm-customer-pool': PAGE_PATHS.crmPoolList,
+ 'crm-quote': PAGE_PATHS.crmQuoteList,
+ 'crm-contract': PAGE_PATHS.crmContractList,
+ 'bx-todo': PAGE_PATHS.bxTodoList,
+ 'bx-reimburse-apply': PAGE_PATHS.bxReimburseList,
+ 'bx-reimburse-approve': `${PAGE_PATHS.bxApproveList}?type=reimburse`,
+ 'bx-advance-apply': PAGE_PATHS.bxAdvanceList,
+ 'bx-advance-approve': `${PAGE_PATHS.bxApproveList}?type=advance`,
+ 'bx-mine': PAGE_PATHS.bxMineList,
+ 'db-dashboard': PAGE_PATHS.dbDashboard,
+ 'db-plans': PAGE_PATHS.dbPlansList,
+ 'db-weighing': PAGE_PATHS.dbWeighingList,
+ 'db-inventory': PAGE_PATHS.dbInventoryList,
+ 'pl-plan': PAGE_PATHS.plPlanList,
+ 'pl-plan-archive': PAGE_PATHS.plArchiveList,
+ 'pl-plan-cert': PAGE_PATHS.plCertList,
+ 'pl-freight': PAGE_PATHS.plFreightList,
+ 'pl-freight-approval': PAGE_PATHS.plFreightApproval
+}
+
export const TOKEN_KEY = 'sx_token'
export const REFRESH_TOKEN_KEY = 'sx_refresh_token'
export const USER_INFO_KEY = 'sx_user_info'
diff --git a/miniprogram/config/workbench-menus.js b/miniprogram/config/workbench-menus.js
new file mode 100644
index 000000000..49d958ae7
--- /dev/null
+++ b/miniprogram/config/workbench-menus.js
@@ -0,0 +1,69 @@
+/**
+ * 工作台菜单(P0 前端写死;与 mobile-ui/shanxia-mp-workbench-demo.html 对齐)
+ * enabled=false 为灰卡,本期不做
+ */
+export const WORKBENCH_MODULES = [
+ {
+ key: 'crm',
+ title: 'CRM 管理',
+ desc: '线索 · 商机 · 客户 · 报价 · 合同',
+ iconText: 'CRM',
+ menus: [
+ { name: '产品分类', enabled: false },
+ { name: '产品管理', enabled: false },
+ { name: '线索管理', enabled: true, code: 'crm-clue' },
+ { name: '商机管理', enabled: true, code: 'crm-opportunity' },
+ { name: '客户管理', enabled: true, code: 'crm-customer' },
+ { name: '公海池', enabled: true, code: 'crm-customer-pool' },
+ { name: '报价管理', enabled: true, code: 'crm-quote' },
+ { name: '合同管理', enabled: true, code: 'crm-contract' },
+ { name: '合同审批', enabled: false }
+ ]
+ },
+ {
+ key: 'bx',
+ title: '报销管理',
+ desc: '借款 · 报销 · 审批 · 付款',
+ iconText: '报销',
+ menus: [
+ { name: '待办任务', enabled: true, code: 'bx-todo' },
+ { name: '报销申请', enabled: true, code: 'bx-reimburse-apply' },
+ { name: '报销审批', enabled: true, code: 'bx-reimburse-approve' },
+ { name: '借款申请', enabled: true, code: 'bx-advance-apply' },
+ { name: '借款审批', enabled: true, code: 'bx-advance-approve' },
+ { name: '借款发放', enabled: false },
+ { name: '我的单据', enabled: true, code: 'bx-mine' },
+ { name: '全部单据', enabled: false },
+ { name: '付款记录', enabled: false }
+ ]
+ },
+ {
+ key: 'db',
+ title: '地磅管理',
+ desc: '计划 · 过磅单 · 出入库',
+ iconText: '地磅',
+ menus: [
+ { name: '仪表盘', enabled: true, code: 'db-dashboard' },
+ { name: '地磅计划', enabled: true, code: 'db-plans' },
+ { name: '地磅单管理', enabled: true, code: 'db-weighing' },
+ { name: '地磅出入库', enabled: true, code: 'db-inventory' }
+ ]
+ },
+ {
+ key: 'pl',
+ title: '运输管理',
+ desc: '计划 · 运费 · 车辆事务',
+ iconText: '运输',
+ menus: [
+ { name: '运输计划', enabled: true, code: 'pl-plan' },
+ { name: '计划归档', enabled: true, code: 'pl-plan-archive' },
+ { name: '购买证上传', enabled: true, code: 'pl-plan-cert' },
+ { name: '运费结算', enabled: true, code: 'pl-freight' },
+ { name: '运费审批', enabled: true, code: 'pl-freight-approval' },
+ { name: '财务发放', enabled: false },
+ { name: '车辆充值', enabled: false },
+ { name: '车辆维修', enabled: false },
+ { name: '运输地址库', enabled: false }
+ ]
+ }
+]
diff --git a/miniprogram/pages.json b/miniprogram/pages.json
index 6a8d4960d..755cbb0dc 100644
--- a/miniprogram/pages.json
+++ b/miniprogram/pages.json
@@ -22,12 +22,183 @@
"navigationBarTextStyle": "black"
}
},
+ {
+ "path": "pages/workbench/workbench",
+ "style": {
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "pages/home/home",
"style": {
- "navigationStyle": "custom",
- "disableScroll": true
+ "navigationStyle": "custom"
}
+ },
+ {
+ "path": "pages/todo/todo",
+ "style": {
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/mine/mine",
+ "style": {
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/common/biz-placeholder",
+ "style": {
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/crm/clue/list",
+ "style": { "navigationStyle": "custom", "enablePullDownRefresh": false }
+ },
+ {
+ "path": "pages/crm/clue/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/clue/form",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/opportunity/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/opportunity/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/customer/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/customer/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/pool/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/quote/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/quote/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/contract/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/crm/contract/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/todo/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/reimburse/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/reimburse/form",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/reimburse/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/advance/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/advance/form",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/advance/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/approve/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/bx/mine/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/dashboard/index",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/plans/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/plans/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/weighing/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/weighing/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/inventory/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/db/inventory/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/plan/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/plan/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/plan/arrange",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/archive/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/cert/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/cert/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/freight/list",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/freight/detail",
+ "style": { "navigationStyle": "custom" }
+ },
+ {
+ "path": "pages/pl/freight/approval",
+ "style": { "navigationStyle": "custom" }
}
// #ifdef H5
,
@@ -57,14 +228,46 @@
}
// #endif
],
+ "tabBar": {
+ "color": "#6b7b74",
+ "selectedColor": "#087f57",
+ "backgroundColor": "#ffffff",
+ "borderStyle": "white",
+ "list": [
+ {
+ "pagePath": "pages/home/home",
+ "text": "首页",
+ "iconPath": "static/tabbar/首页.png",
+ "selectedIconPath": "static/tabbar/首页-active.png"
+ },
+ {
+ "pagePath": "pages/todo/todo",
+ "text": "待办",
+ "iconPath": "static/tabbar/订单.png",
+ "selectedIconPath": "static/tabbar/订单-active.png"
+ },
+ {
+ "pagePath": "pages/workbench/workbench",
+ "text": "工作台",
+ "iconPath": "static/tabbar/购物车.png",
+ "selectedIconPath": "static/tabbar/购物车-active.png"
+ },
+ {
+ "pagePath": "pages/mine/mine",
+ "text": "我的",
+ "iconPath": "static/tabbar/我的.png",
+ "selectedIconPath": "static/tabbar/我的-active.png"
+ }
+ ]
+ },
"globalStyle": {
"pageOrientation": "portrait",
"navigationBarTitleText": "山峡化工",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#f3f7f5",
- "backgroundColor": "#F8F8F8",
- "backgroundColorTop": "#F4F5F6",
- "backgroundColorBottom": "#F4F5F6",
+ "backgroundColor": "#f5f8f7",
+ "backgroundColorTop": "#f5f8f7",
+ "backgroundColorBottom": "#f5f8f7",
"mp-360": {
"navigationStyle": "custom"
},
diff --git a/miniprogram/pages/bx/advance/detail.vue b/miniprogram/pages/bx/advance/detail.vue
new file mode 100644
index 000000000..d4e3f433e
--- /dev/null
+++ b/miniprogram/pages/bx/advance/detail.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.applyCode) }}
+ {{ dash(detail.reason) }}
+
+
+ {{ bxStatusLabel(detail.status).text }}
+
+
+
+ {{ formatMoney(detail.amount) }}
+ {{ dash(detail.applicantName) }}
+
+
+
+
+ 基本信息
+
+ 用途{{ dash(detail?.purpose) }}
+ 申请时间{{ dash(detail?.applyTime) }}
+ 当前节点{{ dash(detail?.currentNode) }}
+ 发放状态{{ dash(detail?.issueStatus) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 审批记录
+
+
+ {{ dash(h.approverName || h.nodeName) }}
+ {{ dash(h.approvalStatus) }} · {{ dash(h.approvalOpinion) }}
+
+
+
+
+
+
+ {{ acting ? '…' : '提交审批' }}
+
+
+ 驳回
+ 通过
+
+
+
+
diff --git a/miniprogram/pages/bx/advance/form.vue b/miniprogram/pages/bx/advance/form.vue
new file mode 100644
index 000000000..9d5594a0a
--- /dev/null
+++ b/miniprogram/pages/bx/advance/form.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+ 借款事由
+
+
+
+ 用途
+
+
+
+ 金额(元)
+
+
+
+ 费用项目
+ {{ form.expenseLabel || '可选,点击选择' }}
+
+
+ 备注
+
+
+
+ 凭证照片
+
+
+
+ ×
+
+ + 拍照
+
+
+
+
+
+ {{ submitting ? '…' : '存草稿' }}
+ {{ submitting ? '…' : '提交审批' }}
+
+
+
diff --git a/miniprogram/pages/bx/advance/list.vue b/miniprogram/pages/bx/advance/list.vue
new file mode 100644
index 000000000..044f4aeb3
--- /dev/null
+++ b/miniprogram/pages/bx/advance/list.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+ 暂无借款单
+
+
+
+
+
+ {{ dash(row.applyCode) }}
+ {{ dash(row.reason) }}
+
+
+ {{ bxStatusLabel(row.status).text }}
+
+
+
+ 申请时间
+ {{ dash(row.applyTime) }}
+
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/bx/approve/list.vue b/miniprogram/pages/bx/approve/list.vue
new file mode 100644
index 000000000..7565471c6
--- /dev/null
+++ b/miniprogram/pages/bx/approve/list.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+ 暂无待审批
+
+
+
+
+
+ {{ dash(row.applyCode) }}
+
+ {{ bizType === 'advance' ? dash(row.reason) : dash(row.expenseName || row.title) }}
+
+
+
+ {{ bxStatusLabel(row.status).text }}
+
+
+
+ 申请人
+ {{ dash(row.applicantName) }}
+
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/bx/mine/list.vue b/miniprogram/pages/bx/mine/list.vue
new file mode 100644
index 000000000..6ea38ee4e
--- /dev/null
+++ b/miniprogram/pages/bx/mine/list.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无单据
+
+
+
+
+
+ {{ row._title }}
+ {{ row._sub }}
+
+
+ {{ bxStatusLabel(row.status).text }}
+
+
+
+ 申请时间
+ {{ dash(row.applyTime) }}
+
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/bx/reimburse/detail.vue b/miniprogram/pages/bx/reimburse/detail.vue
new file mode 100644
index 000000000..c86340e6c
--- /dev/null
+++ b/miniprogram/pages/bx/reimburse/detail.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.applyCode) }}
+ {{ dash(detail.title) }}
+
+
+ {{ bxStatusLabel(detail.status).text }}
+
+
+
+ {{ formatMoney(detail.amount) }}
+ {{ dash(detail.applicantName) }}
+
+
+
+
+ 基本信息
+
+ 费用项目{{ dash(detail?.expenseName) }}
+ 报销日期{{ dash(detail?.reimburseDate) }}
+ 申请时间{{ dash(detail?.applyTime) }}
+ 当前节点{{ dash(detail?.currentNode) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 明细
+
+
+
+ {{ dash(d.expenseName) }}
+ {{ formatMoney(d.amount) }}
+
+ {{ dash(d.remark) }}
+
+
+
+
+
+ 票据
+
+
+
+
+
+
+
+
+ 审批记录
+
+
+ {{ dash(h.approverName || h.nodeName) }}
+ {{ dash(h.approvalStatus) }} · {{ dash(h.approvalOpinion) }}
+
+
+
+
+
+
+ {{ acting ? '…' : '提交审批' }}
+
+
+ 驳回
+ 通过
+
+
+
+
diff --git a/miniprogram/pages/bx/reimburse/form.vue b/miniprogram/pages/bx/reimburse/form.vue
new file mode 100644
index 000000000..088e596f9
--- /dev/null
+++ b/miniprogram/pages/bx/reimburse/form.vue
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+ 标题
+
+
+
+ 报销日期
+ { form.reimburseDate = e.detail.value; refreshTitle() }">
+ {{ form.reimburseDate }}
+
+
+
+ 费用项目
+ {{ form.expenseLabel || '点击选择' }}
+
+
+ 金额(元)
+
+
+
+ 明细说明
+
+
+
+ 备注
+
+
+
+ 票据照片
+
+
+
+ ×
+
+ + 拍照
+
+
+
+
+
+ {{ submitting ? '…' : '存草稿' }}
+ {{ submitting ? '…' : '提交审批' }}
+
+
+
diff --git a/miniprogram/pages/bx/reimburse/list.vue b/miniprogram/pages/bx/reimburse/list.vue
new file mode 100644
index 000000000..f1e2bc8d3
--- /dev/null
+++ b/miniprogram/pages/bx/reimburse/list.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+ 暂无报销单
+
+
+
+
+
+ {{ dash(row.applyCode) }}
+ {{ dash(row.expenseName || row.title) }}
+
+
+ {{ bxStatusLabel(row.status).text }}
+
+
+
+ 申请时间
+ {{ dash(row.applyTime) }}
+
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/bx/todo/list.vue b/miniprogram/pages/bx/todo/list.vue
new file mode 100644
index 000000000..425ef5c52
--- /dev/null
+++ b/miniprogram/pages/bx/todo/list.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无单据
+
+
+
+
+
+ {{ row._title }}
+ {{ row._sub }}
+
+
+ {{ bxStatusLabel(row.status).text }}
+
+
+
+ 申请人
+ {{ dash(row.applicantName) }}
+
+
+ 申请时间
+ {{ dash(row.applyTime) }}
+
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/common/biz-placeholder.vue b/miniprogram/pages/common/biz-placeholder.vue
new file mode 100644
index 000000000..b633bf9be
--- /dev/null
+++ b/miniprogram/pages/common/biz-placeholder.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ {{ title }}
+
+ P0 壳层已就绪,业务列表/详情将在后续阶段(P1–P4)实现。
+
+ 菜单编码:{{ code }}
+
+
+
+
+
diff --git a/miniprogram/pages/common/patterns-readme.md b/miniprogram/pages/common/patterns-readme.md
new file mode 100644
index 000000000..458060be5
--- /dev/null
+++ b/miniprogram/pages/common/patterns-readme.md
@@ -0,0 +1,11 @@
+# 小程序 UI Pattern(P0 占位说明)
+
+完整 Pattern 对照 Demo:
+
+- `mobile-ui/shanxia-mp-pattern-list.html` — 列表
+- `mobile-ui/shanxia-mp-pattern-view.html` — 查看详情
+- `mobile-ui/shanxia-mp-pattern-form.html` — 表单
+- `mobile-ui/shanxia-mp-pattern-feedback.html` — 弹窗反馈
+
+P0 已提供壳层样式 `styles/mp-shell.scss` 与业务占位页 `biz-placeholder.vue`。
+列表/详情/表单可复用组件将在 **P1 首个业务页** 落地时抽取,避免空壳组件无调用方。
diff --git a/miniprogram/pages/crm/clue/detail.vue b/miniprogram/pages/crm/clue/detail.vue
new file mode 100644
index 000000000..7cd486311
--- /dev/null
+++ b/miniprogram/pages/crm/clue/detail.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.clueName) }}
+ {{ dash(detail.company) }}
+
+
+ {{ statusLabel(detail.status).text }}
+
+
+
+ {{ levelLabel(detail.level).text }}
+ {{ dash(detail.ownerUserName) }}
+
+
+
+
+ 基本信息
+
+ 手机{{ dash(detail?.mobile) }}
+ 职位{{ dash(detail?.position) }}
+ 来源{{ dash(detail?.source) }}
+ 行业{{ dash(detail?.industry) }}
+ 园区{{ dash(detail?.parkArea) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 跟进记录
+
+ 暂无跟进
+
+
+
+
+ {{ dash(f.followType) }}
+ {{ dash(f.createTime) }}
+
+ {{ dash(f.followContent) }}
+
+ 跟进人
+ {{ dash(f.createByName) }}
+
+
+
+
+
+
+
+ 跟进方式
+
+
+
+ 跟进内容
+
+
+
+ 取消
+
+ {{ submitting ? '提交中…' : '提交跟进' }}
+
+
+
+
+
+ 跟进
+
+ 转客户
+
+
+ 无效
+
+
+
+
diff --git a/miniprogram/pages/crm/clue/form.vue b/miniprogram/pages/crm/clue/form.vue
new file mode 100644
index 000000000..515205983
--- /dev/null
+++ b/miniprogram/pages/crm/clue/form.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+ 线索姓名
+
+
+
+ 手机号
+
+
+
+ 公司名称
+
+
+
+ 职位
+
+
+
+ 来源
+
+
+
+ 行业
+
+
+
+ 园区
+
+
+
+ 备注
+
+
+
+
+
+ 取消
+
+ {{ submitting ? '提交中…' : '保存' }}
+
+
+
+
diff --git a/miniprogram/pages/crm/clue/list.vue b/miniprogram/pages/crm/clue/list.vue
new file mode 100644
index 000000000..528aeb09a
--- /dev/null
+++ b/miniprogram/pages/crm/clue/list.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无线索
+
+
+
+
+
+ {{ dash(row.clueName) }}
+ {{ dash(row.company) }}
+
+
+ {{ statusLabel(row.status).text }}
+
+
+
+ 手机
+ {{ dash(row.mobile) }}
+
+
+ 负责人
+ {{ dash(row.ownerUserName) }}
+
+
+ 来源
+ {{ dash(row.source) }}
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/crm/contract/detail.vue b/miniprogram/pages/crm/contract/detail.vue
new file mode 100644
index 000000000..d543083fc
--- /dev/null
+++ b/miniprogram/pages/crm/contract/detail.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.contractName) }}
+ {{ dash(detail.customerName) }}
+
+
+ {{ statusLabel(detail.status).text }}
+
+
+
+ {{ formatMoney(detail.totalAmount ?? detail.amount) }}
+ {{ dash(detail.ownerUserName) }}
+
+
+
+
+ 基本信息
+
+ 合同编号{{ dash(detail?.contractNo) }}
+ 合同类型{{ dash(detail?.contractType) }}
+ 签订日期{{ dash(detail?.signDate) }}
+ 开始日期{{ dash(detail?.startDate) }}
+ 结束日期{{ dash(detail?.endDate) }}
+ 审批状态{{ dash(detail?.approvalStatus) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 说明
+
+
+ 合同审批流与附件签署请在 Web 管理端处理;小程序提供现场查阅。
+
+
+
+
+
diff --git a/miniprogram/pages/crm/contract/list.vue b/miniprogram/pages/crm/contract/list.vue
new file mode 100644
index 000000000..7f4b3607d
--- /dev/null
+++ b/miniprogram/pages/crm/contract/list.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无合同
+
+
+
+
+
+ {{ dash(row.contractName) }}
+ {{ dash(row.contractNo) }} · {{ dash(row.customerName) }}
+
+
+ {{ statusLabel(row.status).text }}
+
+
+
+ 金额
+ {{ formatMoney(row.totalAmount ?? row.amount) }}
+
+
+ 负责人
+ {{ dash(row.ownerUserName) }}
+
+
+ 签订日期
+ {{ dash(row.signDate) }}
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/crm/customer/detail.vue b/miniprogram/pages/crm/customer/detail.vue
new file mode 100644
index 000000000..06369a904
--- /dev/null
+++ b/miniprogram/pages/crm/customer/detail.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.customerName) }}
+ {{ dash(detail.customerNo) }}
+
+
+ {{ statusLabel(detail.status).text }}
+
+
+
+ {{ levelLabel(detail.level).text }}
+ {{ dash(detail.ownerUserName) }}
+
+
+
+
+ 基本信息
+
+ 类型{{ dash(detail?.customerType) }}
+ 来源{{ dash(detail?.source) }}
+ 行业{{ dash(detail?.industry) }}
+ 手机{{ dash(detail?.mobile) }}
+ 地址{{ dash(detail?.address) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 跟进记录
+
+ 暂无跟进
+
+
+
+
+ {{ dash(f.followType) }}
+ {{ dash(f.createTime) }}
+
+ {{ dash(f.followContent) }}
+
+
+
+
+
+
+ 跟进方式
+
+
+
+ 跟进内容
+
+
+
+ 取消
+
+ {{ submitting ? '提交中…' : '提交' }}
+
+
+
+
+
+ 写跟进
+
+
+
diff --git a/miniprogram/pages/crm/customer/list.vue b/miniprogram/pages/crm/customer/list.vue
new file mode 100644
index 000000000..727d0251b
--- /dev/null
+++ b/miniprogram/pages/crm/customer/list.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无客户
+
+
+
+
+
+ {{ dash(row.customerName) }}
+ {{ dash(row.customerNo) }}
+
+
+ {{ statusLabel(row.status).text }}
+
+
+
+ 级别
+ {{ levelLabel(row.level).text }}
+
+
+ 联系人
+ {{ dash(row.contactName || row.mobile) }}
+
+
+ 负责人
+ {{ dash(row.ownerUserName) }}
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/crm/opportunity/detail.vue b/miniprogram/pages/crm/opportunity/detail.vue
new file mode 100644
index 000000000..eefb3d41a
--- /dev/null
+++ b/miniprogram/pages/crm/opportunity/detail.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.opportunityName) }}
+ {{ dash(detail.customerName) }}
+
+
+ {{ statusLabel(detail.status).text }}
+
+
+
+ {{ stageLabel(detail.stage).text }}
+ {{ formatMoney(detail.amount ?? detail.expectedAmount) }}
+
+
+
+
+ 基本信息
+
+ 商机编号{{ dash(detail?.opportunityNo) }}
+ 负责人{{ dash(detail?.ownerUserName) }}
+ 来源{{ dash(detail?.source) }}
+ 预计成交{{ dash(detail?.expectedDate || detail?.dealDate) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 跟进记录
+
+ 暂无跟进
+
+
+
+
+ {{ dash(f.followType) }}
+ {{ dash(f.createTime) }}
+
+ {{ dash(f.followContent) }}
+
+
+
+
+
+
+ 跟进方式
+
+
+
+ 跟进内容
+
+
+
+ 取消
+
+ {{ submitting ? '提交中…' : '提交' }}
+
+
+
+
+
+ 写跟进
+
+
+
diff --git a/miniprogram/pages/crm/opportunity/list.vue b/miniprogram/pages/crm/opportunity/list.vue
new file mode 100644
index 000000000..5c8dd83bc
--- /dev/null
+++ b/miniprogram/pages/crm/opportunity/list.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无商机
+
+
+
+
+
+ {{ dash(row.opportunityName) }}
+ {{ dash(row.customerName) }}
+
+
+ {{ statusLabel(row.status).text }}
+
+
+
+ 阶段
+ {{ stageLabel(row.stage).text }}
+
+
+ 金额
+ {{ formatMoney(row.amount ?? row.expectedAmount) }}
+
+
+ 负责人
+ {{ dash(row.ownerUserName) }}
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/crm/pool/list.vue b/miniprogram/pages/crm/pool/list.vue
new file mode 100644
index 000000000..0db577fb4
--- /dev/null
+++ b/miniprogram/pages/crm/pool/list.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+ 公海暂无客户
+
+
+
+
+
+ {{ dash(row.customerName) }}
+ 入池:{{ dash(row.poolTime || row.updateTime) }}
+
+ {{ levelLabel(row.level).text }}
+
+
+ 来源
+ {{ dash(row.source) }}
+
+
+ 原负责人
+ {{ dash(row.ownerUserName || row.prevOwnerName) }}
+
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/crm/quote/detail.vue b/miniprogram/pages/crm/quote/detail.vue
new file mode 100644
index 000000000..a07fd1d9b
--- /dev/null
+++ b/miniprogram/pages/crm/quote/detail.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.quoteTitle || detail.quoteNo) }}
+ {{ dash(detail.customerName) }}
+
+
+ {{ statusLabel(detail.status).text }}
+
+
+
+ {{ formatMoney(detail.totalAmount ?? detail.amount) }}
+ {{ dash(detail.ownerUserName) }}
+
+
+
+
+ 基本信息
+
+ 报价单号{{ dash(detail?.quoteNo) }}
+ 报价日期{{ dash(detail?.quoteDate) }}
+ 有效期至{{ dash(detail?.validDate || detail?.expireDate) }}
+ 关联商机{{ dash(detail?.opportunityName) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 说明
+
+
+ 审批、改价、转合同等复杂操作请在 Web 管理端完成;小程序侧重查看与现场跟进。
+
+
+
+
+
diff --git a/miniprogram/pages/crm/quote/list.vue b/miniprogram/pages/crm/quote/list.vue
new file mode 100644
index 000000000..496d4aaa1
--- /dev/null
+++ b/miniprogram/pages/crm/quote/list.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无报价
+
+
+
+
+
+ {{ dash(row.quoteTitle || row.quoteNo) }}
+ {{ dash(row.quoteNo) }} · {{ dash(row.customerName) }}
+
+
+ {{ statusLabel(row.status).text }}
+
+
+
+ 金额
+ {{ formatMoney(row.totalAmount ?? row.amount) }}
+
+
+ 负责人
+ {{ dash(row.ownerUserName) }}
+
+
+ 报价日期
+ {{ dash(row.quoteDate || row.createTime) }}
+
+
+
+ 加载中…
+ 已加载全部
+
+
+
diff --git a/miniprogram/pages/db/dashboard/index.vue b/miniprogram/pages/db/dashboard/index.vue
new file mode 100644
index 000000000..987277e1a
--- /dev/null
+++ b/miniprogram/pages/db/dashboard/index.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+ {{ stats.todayWeighing }}
+ 今日过磅
+
+
+ {{ stats.needSecond }}
+ 待二次过磅
+
+
+ {{ Number(stats.todayInbound).toFixed(0) }}
+ 今日入库
+
+
+ {{ Number(stats.todayOutbound).toFixed(0) }}
+ 今日出库
+
+
+
+
+
+
+
+ 地磅单管理
+ 现场
+
+ 查看过磅记录,办理二次过磅
+
+
+
+ 地磅计划
+ 计划
+
+ 查看入/出库计划执行进度
+
+
+
+ 出入库单
+ 单据
+
+ 查看出入库及关联过磅
+
+
+
+
+
+ 货品计划 vs 实绩(节选)
+
+
+ {{ dash(c.label) }}
+
+ 计划 {{ formatWeight(c.planned, '') }} / 实绩 {{ formatWeight(c.actual, '') }}
+
+
+
+
+
+ 加载中…
+
+
diff --git a/miniprogram/pages/db/inventory/detail.vue b/miniprogram/pages/db/inventory/detail.vue
new file mode 100644
index 000000000..c3be16ec9
--- /dev/null
+++ b/miniprogram/pages/db/inventory/detail.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.batchNo || detail.id) }}
+ {{ dash(detail.product) }} · {{ dash(detail.businessType) }}
+
+
+ {{ inventoryStatusLabel(detail.status).text }}
+
+
+
+ {{ dash(detail.vehicleNo) }}
+ {{ formatWeight(detail.quantity) }}
+
+
+
+
+ 基本信息
+
+ 日期{{ dash(detail?.date) }}
+ 往来单位{{ dash(detail?.partnerCompany) }}
+ 规格{{ dash(detail?.specification) }}
+ 计划号{{ dash(detail?.planNo) }}
+ 备注{{ dash(detail?.remarks) }}
+
+
+
+
+ 过磅数据
+
+ 毛重{{ formatWeight(detail?.grossWeight) }}
+ 皮重{{ formatWeight(detail?.tareWeight) }}
+ 净重{{ formatWeight(detail?.netWeight) }}
+
+
+
+
+ 现场反馈
+
+ {{ dash(detail.siteFeedbackRemark) }}
+
+
+
+
+
+
+
+ 关联地磅单
+ {{ related.weighing ? '查看' : '无' }}
+
+
+
+
+ 关联计划
+ {{ related.plan ? '查看' : '无' }}
+
+
+
+
+
+
diff --git a/miniprogram/pages/db/inventory/list.vue b/miniprogram/pages/db/inventory/list.vue
new file mode 100644
index 000000000..592c46168
--- /dev/null
+++ b/miniprogram/pages/db/inventory/list.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无出入库单
+
+
+
+
+
+ {{ dash(row.batchNo || row.id) }}
+
+ {{ dash(row.vehicleNo) }} · {{ dash(row.product) }} · {{ dash(row.businessType) }}
+
+
+
+ {{ inventoryStatusLabel(row.status).text }}
+
+
+
+ 数量
+ {{ formatWeight(row.quantity) }}
+
+
+ 往来单位
+ {{ dash(row.partnerCompany) }}
+
+
+ 日期
+ {{ dash(row.date) }}
+
+
+
+ 加载中…
+
+
+
diff --git a/miniprogram/pages/db/plans/detail.vue b/miniprogram/pages/db/plans/detail.vue
new file mode 100644
index 000000000..ec872fea3
--- /dev/null
+++ b/miniprogram/pages/db/plans/detail.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.planNo) }}
+ {{ dash(detail.product) }}
+
+
+ {{ planStatusLabel(detail.executionStatus).text }}
+
+
+
+ {{ dash(detail.businessType) }}
+
+ {{ formatWeight(detail.executedQuantity) }} / {{ formatWeight(detail.plannedQuantity) }}
+
+
+
+
+
+ 基本信息
+
+ 计划日期{{ dash(detail?.planDate) }}
+ 起止{{ dash(detail?.startDate) }} ~ {{ dash(detail?.endDate) }}
+ 规格{{ dash(detail?.specification) }}
+ 车牌{{ dash(detail?.vehicleNo) }}
+ 客户{{ dash(detail?.customerName) }}
+ 备注{{ dash(detail?.remarks) }}
+
+
+
+
+ 关联出入库({{ related.inventories?.length || 0 }})
+
+ 暂无出入库单
+
+
+
+ {{ dash(inv.batchNo || inv.id) }}
+ {{ dash(inv.vehicleNo) }} · {{ dash(inv.status) }}
+
+
+
+
+
+ 关联地磅单({{ related.weighings?.length || 0 }})
+
+ 暂无地磅单
+
+
+
+ {{ dash(w.vehicleNo) }}
+ 净重 {{ formatWeight(w.netWeight) }}
+
+
+
+
+
+
+ {{ acting ? '…' : '生成出入库关联单' }}
+
+
+
+
diff --git a/miniprogram/pages/db/plans/list.vue b/miniprogram/pages/db/plans/list.vue
new file mode 100644
index 000000000..df4475cfd
--- /dev/null
+++ b/miniprogram/pages/db/plans/list.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无计划
+
+
+
+
+
+ {{ dash(row.planNo) }}
+ {{ dash(row.product) }} · {{ dash(row.businessType) }}
+
+
+ {{ planStatusLabel(row.executionStatus).text }}
+
+
+
+ 计划量
+ {{ formatWeight(row.plannedQuantity) }}
+
+
+ 已执行
+ {{ formatWeight(row.executedQuantity) }}
+
+
+ 客户
+ {{ dash(row.customerName) }}
+
+
+
+ 加载中…
+
+
+
diff --git a/miniprogram/pages/db/weighing/detail.vue b/miniprogram/pages/db/weighing/detail.vue
new file mode 100644
index 000000000..16bda2f85
--- /dev/null
+++ b/miniprogram/pages/db/weighing/detail.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.vehicleNo) }}
+ {{ dash(detail.cargoName) }} · {{ dash(detail.businessType) }}
+
+ {{ phase.text }}
+
+
+ 净重 {{ formatWeight(detail.netWeight) }}
+ {{ dash(detail.batchNo) }}
+
+
+
+
+ 过磅数据
+
+ 毛重{{ formatWeight(detail?.grossWeight) }}
+ 毛重时间{{ dash(detail?.grossWeightTime) }}
+ 皮重{{ formatWeight(detail?.tareWeight) }}
+ 皮重时间{{ dash(detail?.tareWeightTime) }}
+ 扣重{{ formatWeight(detail?.deduction) }}
+ 净重{{ formatWeight(detail?.netWeight) }}
+
+
+
+
+ 业务信息
+
+ 计划号{{ dash(detail?.planNo) }}
+ 规格{{ dash(detail?.specification) }}
+ 供货单位{{ dash(detail?.supplier) }}
+ 收货单位{{ dash(detail?.consignee) }}
+ 过磅点{{ dash(detail?.weighingPoint) }}
+ 备注{{ dash(detail?.remarks) }}
+
+
+
+
+
+ 皮重(kg)
+
+
+
+ 取消
+
+ {{ acting ? '…' : '确认保存' }}
+
+
+
+
+
+ 二次过磅
+ 作废
+
+
+
diff --git a/miniprogram/pages/db/weighing/list.vue b/miniprogram/pages/db/weighing/list.vue
new file mode 100644
index 000000000..b2e144681
--- /dev/null
+++ b/miniprogram/pages/db/weighing/list.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+ {{ stats.today }}
+ 今日过磅
+
+
+ {{ stats.needSecond }}
+ 待二次过磅
+
+
+ {{ list.length }}
+ 当前列表
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无地磅单
+
+
+
+
+
+ {{ dash(row.batchNo || row.id) }}
+ {{ dash(row.vehicleNo) }} · {{ dash(row.cargoName) }}
+
+
+ {{ weighingPhase(row).text }}
+
+
+
+ 净重
+ {{ formatWeight(row.netWeight) }}
+
+
+ 过磅时间
+ {{ dash(row.grossWeightTime) }}
+
+
+
+
+ 加载中…
+
+
+
diff --git a/miniprogram/pages/home/home.vue b/miniprogram/pages/home/home.vue
index c1ed77bca..be62db254 100644
--- a/miniprogram/pages/home/home.vue
+++ b/miniprogram/pages/home/home.vue
@@ -1,123 +1,35 @@
-
-
-
-
-
- 小程序基础框架已就绪
- 当前为占位首页,后续在此目录下建设业务页面。
-
-
-
-
- 当前用户
- {{ displayName }}
-
-
- 角色
- {{ userStore.userRole || '-' }}
-
-
-
-
-
-
diff --git a/miniprogram/pages/mine/mine.vue b/miniprogram/pages/mine/mine.vue
new file mode 100644
index 000000000..c545359ca
--- /dev/null
+++ b/miniprogram/pages/mine/mine.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+ {{ displayName }}
+ 山峡化工 · {{ deptName }}
+
+
+
+
+
+
+ 姓名
+ {{ displayName }}
+
+
+ 账号
+ {{ username }}
+
+
+ 手机号
+ {{ phone }}
+
+
+ 邮箱
+ {{ email }}
+
+
+ 所属部门
+ {{ deptName }}
+
+
+ 角色
+ {{ roleLabel }}
+
+
+
+
+ 保存修改
+
+ 退出登录
+
+
+ 本期支持查看个人信息与退出登录;资料编辑后续开放
+
+
diff --git a/miniprogram/pages/pl/archive/list.vue b/miniprogram/pages/pl/archive/list.vue
new file mode 100644
index 000000000..d13fd13b8
--- /dev/null
+++ b/miniprogram/pages/pl/archive/list.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+ 暂无已归档计划
+
+
+
+
+
+ {{ dash(row.planNo) }}
+ {{ planCardSub(row) }}
+
+
+ {{ archiveStatusLabel(row.archiveStatus).text }}
+
+
+
+ {{ planTypeLabel(row.planType) }}
+ {{ dash(row.planDate) }}
+
+
+
+
+ {{ loading ? '加载中…' : '加载更多' }}
+
+
+
+
diff --git a/miniprogram/pages/pl/cert/detail.vue b/miniprogram/pages/pl/cert/detail.vue
new file mode 100644
index 000000000..d5f3a4c1c
--- /dev/null
+++ b/miniprogram/pages/pl/cert/detail.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.planNo) }}
+ {{ planCardSub(detail) }}
+
+
+ {{ certReadyLabel(detail.hazardCertReady).text }}
+
+
+
+
+
+ 产品购买证
+
+ 暂无产品明细
+
+
+
+ {{ dash(p.productName) }}
+ {{ hasPurchaseCert(p) ? '已上传' : '未上传' }}
+
+
+
+
+
+
+ +
+ 拍照
+
+
+
+
+
+
+
+ 标记证书齐备
+
+
+ 取消齐备
+
+
+
+
diff --git a/miniprogram/pages/pl/cert/list.vue b/miniprogram/pages/pl/cert/list.vue
new file mode 100644
index 000000000..b3fabf6c8
--- /dev/null
+++ b/miniprogram/pages/pl/cert/list.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+ 待齐备
+
+
+ 已齐备
+
+
+ 全部
+
+
+
+
+
+
+
+ 暂无计划
+
+
+
+
+
+ {{ dash(row.planNo) }}
+ {{ planCardSub(row) }}
+
+
+ {{ certReadyLabel(row.hazardCertReady).text }}
+
+
+
+ {{ certHint(row) }}
+ {{ dash(row.planDate) }}
+
+
+
+
+
+ {{ loading ? '加载中…' : '加载更多' }}
+
+
+
+
diff --git a/miniprogram/pages/pl/freight/approval.vue b/miniprogram/pages/pl/freight/approval.vue
new file mode 100644
index 000000000..d347a7dbb
--- /dev/null
+++ b/miniprogram/pages/pl/freight/approval.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无审批单据
+
+
+
+
+
+ {{ dash(row.settlementNo) }}
+
+ {{ dash(row.licensePlateSummary) }} · {{ dash(row.submitterName) }}
+
+
+
+ {{ settlementStatusLabel(row.settlementStatus).text }}
+
+
+
+ 运费合计
+ {{ formatMoney(row.totalFreight) }}
+
+
+
+
+
+ {{ loading ? '加载中…' : '加载更多' }}
+
+
+
+
diff --git a/miniprogram/pages/pl/freight/detail.vue b/miniprogram/pages/pl/freight/detail.vue
new file mode 100644
index 000000000..72e638d3b
--- /dev/null
+++ b/miniprogram/pages/pl/freight/detail.vue
@@ -0,0 +1,301 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.settlementNo) }}
+ {{ dash(detail.licensePlateSummary) }}
+
+ {{ status.text }}
+
+
+ {{ formatMoney(detail.totalFreight) }}
+
+ 审批人 {{ detail.currentApproverName }}
+
+
+
+
+
+ 结算信息
+
+ 发票号{{ dash(detail?.invoiceNo) }}
+ 发票金额{{ formatMoney(detail?.invoiceAmount) }}
+ 提交人{{ dash(detail?.submitterName) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 关联计划
+
+
+ {{ dash(p.planNo) }}
+ {{ formatMoney(p.freightAmount) }}
+
+
+
+
+
+ 关联运输
+
+
+ {{ dash(t.licensePlate || t.transportNo) }}
+ {{ formatMoney(t.freightAmount) }}
+
+
+
+
+
+ 审批流
+
+
+ {{ dash(n.stepName || n.approverName || `步骤${n.step}`) }}
+ {{ dash(n.approvalResult) }}
+
+
+
+
+
+ 提交审批
+
+
+ 审批类型
+
+ {{ approvalTypes[approvalTypeIndex].label }}
+
+
+
+ 审批人
+
+ {{ approverSummary }}
+
+
+
+ 提交说明
+
+
+
+
+
+
+
+ 提交审批
+
+
+
+ 驳回
+
+
+ 通过
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/pl/freight/list.vue b/miniprogram/pages/pl/freight/list.vue
new file mode 100644
index 000000000..339d91d98
--- /dev/null
+++ b/miniprogram/pages/pl/freight/list.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ {{ activeTab === 'unsettled' ? '暂无待结算计划' : '暂无结算单' }}
+
+
+
+
+
+
+
+ {{ dash(row.settlementNo) }}
+ {{ dash(row.licensePlateSummary) }} · {{ dash(row.planCount) }} 单
+
+
+ {{ settlementStatusLabel(row.settlementStatus).text }}
+
+
+
+ 运费合计
+ {{ formatMoney(row.totalFreight) }}
+
+
+
+
+
+
+
+
+
+ {{ dash(row.planNo) }}
+ {{ dash(row.customerName || row.carrierName) }}
+
+
+
+ 运费
+ {{ formatMoney(row.freightAmount || row.totalFreight) }}
+
+
+
+
+
+
+ {{ loading ? '加载中…' : '加载更多' }}
+
+
+
+
diff --git a/miniprogram/pages/pl/plan/arrange.vue b/miniprogram/pages/pl/plan/arrange.vue
new file mode 100644
index 000000000..ec0eac70e
--- /dev/null
+++ b/miniprogram/pages/pl/plan/arrange.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+ 选择资源
+
+
+ 车辆
+
+
+ {{ vehicleIndex >= 0 ? vehicleLabels[vehicleIndex] : '请选择车辆' }}
+
+
+
+
+ 司机
+
+
+ {{ driverIndex >= 0 ? driverLabels[driverIndex] : '请选择司机' }}
+
+
+
+
+ 运费方式
+
+ {{ freightTypeLabels[freightTypeIndex] }}
+
+
+
+ 运费单价
+
+
+
+ 运费金额
+
+
+
+ 公里数
+
+
+
+
+
+
+
+ {{ acting ? '提交中…' : '确认安排' }}
+
+
+
+
diff --git a/miniprogram/pages/pl/plan/detail.vue b/miniprogram/pages/pl/plan/detail.vue
new file mode 100644
index 000000000..f4319ddd6
--- /dev/null
+++ b/miniprogram/pages/pl/plan/detail.vue
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+ {{ dash(detail.planNo) }}
+ {{ planCardSub(detail) }}
+
+ {{ phaseTransport.text }}
+
+
+ {{ planTypeLabel(detail.planType) }}
+ {{ phaseFollow.text }}
+ {{ phaseArchive.text }}
+
+
+
+
+ 基本信息
+
+ 计划日期{{ dash(detail?.planDate) }}
+ 客户{{ dash(detail?.customerName) }}
+ 供应商{{ dash(detail?.supplierName) }}
+ 取货地址{{ dash(detail?.pickupAddress) }}
+ 送货地址{{ dash(detail?.destinationAddress) }}
+ 联系人{{ dash(detail?.contactName) }} {{ dash(detail?.contactPhone) }}
+ 跟进人{{ dash(detail?.followUserName) }}
+ 备注{{ dash(detail?.remark) }}
+
+
+
+
+ 产品明细
+
+ 暂无明细
+
+
+
+ {{ dash(p.productName) }}
+ {{ formatQty(p.quantity, p.unit) }}
+
+
+
+
+
+ 运输安排
+
+
+ 车牌
+ {{ dash(arrange?.licensePlate || detail.licensePlate) }}
+
+
+ 司机
+ {{ dash(detail.driverName) }}
+
+
+ 承运商
+ {{ dash(detail.carrierName) }}
+
+
+ 司机扫码
+ 已生成链接
+
+
+
+
+
+
+ 安排运输
+
+
+ 归档
+
+
+
+
diff --git a/miniprogram/pages/pl/plan/list.vue b/miniprogram/pages/pl/plan/list.vue
new file mode 100644
index 000000000..7b8ae24a8
--- /dev/null
+++ b/miniprogram/pages/pl/plan/list.vue
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+ {{ t.label }}
+
+
+
+
+
+
+
+ 暂无运输计划
+
+
+
+
+
+ {{ dash(row.planNo) }}
+ {{ planCardSub(row) }}
+
+
+ {{ planListPhase(row).text }}
+
+
+
+ 计划日期
+ {{ dash(row.planDate) }}
+
+
+
+
+
+ {{ loading ? '加载中…' : '加载更多' }}
+
+
+
+
diff --git a/miniprogram/pages/todo/todo.vue b/miniprogram/pages/todo/todo.vue
new file mode 100644
index 000000000..6cfbdcb6a
--- /dev/null
+++ b/miniprogram/pages/todo/todo.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ 待办功能建设中
+ 本期请从「工作台」进入各业务模块办理;全局待办将在后续阶段开放。
+
+
+
diff --git a/miniprogram/pages/workbench/workbench.vue b/miniprogram/pages/workbench/workbench.vue
new file mode 100644
index 000000000..100119cbf
--- /dev/null
+++ b/miniprogram/pages/workbench/workbench.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+ {{ mod.iconText }}
+
+ {{ mod.title }}
+ {{ mod.desc }}
+
+
+
+
+
+
+
diff --git a/miniprogram/scripts/mp_api_smoke.py b/miniprogram/scripts/mp_api_smoke.py
new file mode 100644
index 000000000..cabf01f4e
--- /dev/null
+++ b/miniprogram/scripts/mp_api_smoke.py
@@ -0,0 +1,500 @@
+#!/usr/bin/env python3
+"""
+山峡小程序 · 接口联通冒烟(对齐 docs/miniprogram-dev-checklist.md §7.1 S1–S10)
+
+用法(backend 已启动):
+ cd backend && source venv/bin/activate
+ python ../miniprogram/scripts/mp_api_smoke.py
+
+环境变量:
+ MP_SMOKE_BASE 默认 http://127.0.0.1:8000/api
+ MP_SMOKE_USER 默认 admin
+ MP_SMOKE_PASS 默认 111111
+"""
+from __future__ import annotations
+
+import base64
+import json
+import os
+import sys
+import time
+import traceback
+import urllib.error
+import urllib.parse
+import urllib.request
+from typing import Any, Optional
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+
+BASE = os.environ.get("MP_SMOKE_BASE", "http://127.0.0.1:8000/api").rstrip("/")
+USER = os.environ.get("MP_SMOKE_USER", "admin")
+# 本环境 admin 口令为 admin123(文档默认 111111 可能已改)
+PASS = os.environ.get("MP_SMOKE_PASS", "admin123")
+
+results: list[dict[str, Any]] = []
+
+
+def log(msg: str) -> None:
+ print(msg, flush=True)
+
+
+def record(sid: str, ok: bool, detail: str) -> None:
+ results.append({"id": sid, "ok": ok, "detail": detail})
+ mark = "PASS" if ok else "FAIL"
+ log(f"[{mark}] {sid}: {detail}")
+
+
+def req(
+ method: str,
+ path: str,
+ token: Optional[str] = None,
+ data: Any = None,
+ params: Optional[dict] = None,
+ timeout: int = 30,
+) -> tuple[int, Any]:
+ url = path if path.startswith("http") else f"{BASE}{path}"
+ if params:
+ qs = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
+ url = f"{url}{'&' if '?' in url else '?'}{qs}"
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+ body = None if data is None else json.dumps(data, ensure_ascii=False).encode("utf-8")
+ request = urllib.request.Request(url, data=body, headers=headers, method=method.upper())
+ try:
+ with urllib.request.urlopen(request, timeout=timeout) as resp:
+ raw = resp.read().decode("utf-8") or "null"
+ try:
+ return resp.status, json.loads(raw)
+ except json.JSONDecodeError:
+ return resp.status, raw
+ except urllib.error.HTTPError as e:
+ raw = e.read().decode("utf-8", errors="replace")
+ try:
+ return e.code, json.loads(raw)
+ except Exception:
+ return e.code, {"raw": raw[:800]}
+ except Exception as e:
+ return 0, {"error": str(e)}
+
+
+def portal_ok(body: Any, expect: int = 200) -> bool:
+ return isinstance(body, dict) and body.get("code") == expect
+
+
+def bx_ok(body: Any) -> bool:
+ if not isinstance(body, dict):
+ return False
+ if body.get("code") is None:
+ return True
+ return body.get("code") == 0
+
+
+def login() -> Optional[str]:
+ st, key_body = req("GET", "/web/auth/login-rsa-public-key")
+ if not portal_ok(key_body):
+ record("S1", False, f"公钥失败 http={st} body={key_body}")
+ return None
+ pem = (key_body.get("data") or {}).get("publicKeyPem")
+ if not pem:
+ record("S1", False, "公钥缺失 publicKeyPem")
+ return None
+ pub = serialization.load_pem_public_key(pem.encode(), backend=default_backend())
+ payload = json.dumps(
+ {"username": USER, "password": PASS, "ts": int(time.time() * 1000)},
+ ensure_ascii=False,
+ ).encode("utf-8")
+ enc = base64.b64encode(pub.encrypt(payload, padding.PKCS1v15())).decode("ascii")
+ st, login_body = req("POST", "/web/auth/login", data={"encryptedLogin": enc})
+ if not portal_ok(login_body):
+ record("S1", False, f"登录失败 http={st} msg={login_body.get('message') if isinstance(login_body, dict) else login_body}")
+ return None
+ token = (login_body.get("data") or {}).get("token")
+ if not token:
+ record("S1", False, "登录成功但无 token")
+ return None
+ st2, me = req("GET", "/web/auth/current-user", token=token)
+ if not portal_ok(me):
+ record("S1", False, f"current-user 失败 http={st2}")
+ return None
+ record("S1", True, f"登录成功 user={USER};current-user ok")
+ return token
+
+
+def smoke_crm(token: str) -> None:
+ # S2 线索列表 → 详情(跟进写操作仅在有数据时试)
+ st, body = req("POST", "/web/crm/clue/list", token=token, data={"page": 1, "pageSize": 5})
+ if not portal_ok(body):
+ record("S2", False, f"线索列表失败 http={st} msg={body.get('message') if isinstance(body, dict) else body}")
+ return
+ data = body.get("data") or {}
+ rows = data.get("list") or data.get("rows") or []
+ total = data.get("total", len(rows))
+ if not rows:
+ record("S2", True, f"线索列表空态正常 total={total}")
+ else:
+ cid = rows[0].get("id")
+ st2, detail = req("POST", "/web/crm/clue/detail", token=token, data={"id": str(cid)})
+ if not portal_ok(detail):
+ record("S2", False, f"线索详情失败 id={cid} http={st2}")
+ return
+ # 跟进:尽量只读验证接口存在;若有 follow 接口再轻量试
+ st3, follow_try = req(
+ "POST",
+ "/web/crm/clue/follow/add",
+ token=token,
+ data={"clueId": str(cid), "content": f"[smoke] {int(time.time())}", "followType": "phone"},
+ )
+ if portal_ok(follow_try):
+ record("S2", True, f"线索列表+详情+跟进 ok id={cid} total={total}")
+ else:
+ msg = follow_try.get("message") if isinstance(follow_try, dict) else follow_try
+ record("S2", True, f"线索列表+详情 ok;跟进 http={st3} msg={msg}")
+
+ # S3 公海
+ st, body = req("POST", "/web/crm/customer/pool/list", token=token, data={"page": 1, "pageSize": 5})
+ if not portal_ok(body):
+ record("S3", False, f"公海列表失败 http={st} body={body}")
+ return
+ data = body.get("data") or {}
+ rows = data.get("list") or data.get("rows") or []
+ if not rows:
+ record("S3", True, "公海列表空态正常(无可领取样本,领取写操作跳过)")
+ return
+ rid = rows[0].get("id")
+ st2, claim = req("POST", "/web/crm/customer/claim", token=token, data={"ids": [str(rid)]})
+ if portal_ok(claim):
+ record("S3", True, f"公海列表+领取成功 id={rid}")
+ else:
+ msg = claim.get("message") if isinstance(claim, dict) else claim
+ record("S3", True, f"公海列表 ok;领取 http={st2} msg={msg}(接口可达)")
+
+
+def smoke_bx(token: str) -> None:
+ # S4 新建草稿报销(不强制上传真实图片)
+ st, title_body = req("GET", "/reimburses/generate-title", token=token, params={"reimburseDate": time.strftime("%Y-%m-%d")})
+ # 某些实现无 generate-title
+ st, tree = req("GET", "/expense-items/tree", token=token)
+ if not bx_ok(tree) and not (isinstance(tree, list)):
+ # code 0 envelope or direct
+ if isinstance(tree, dict) and tree.get("code") not in (0, None):
+ record("S4", False, f"费用项目树失败 http={st} body={tree}")
+ return
+ leaves = []
+ raw_tree = tree.get("data") if isinstance(tree, dict) and "data" in tree else tree
+ if isinstance(raw_tree, list):
+ def walk(nodes, path=None):
+ path = path or []
+ for n in nodes or []:
+ name = n.get("itemName") or n.get("item_name") or ""
+ kids = n.get("children") or []
+ if kids:
+ walk(kids, path + [name])
+ else:
+ leaves.append({"id": n.get("id"), "name": name})
+ walk(raw_tree)
+
+ payload = {
+ "reimburseDate": time.strftime("%Y-%m-%d"),
+ "title": f"MP冒烟报销-{int(time.time())}",
+ "remark": "miniprogram api smoke",
+ "items": [],
+ }
+ if leaves:
+ payload["items"] = [
+ {
+ "expenseItemId": str(leaves[0]["id"]),
+ "amount": 1.00,
+ "remark": "smoke",
+ }
+ ]
+ st, created = req("POST", "/reimburses/", token=token, data=payload)
+ # 跟随 307 到无尾斜杠
+ if st in (301, 302, 307, 308):
+ st, created = req("POST", "/reimburses", token=token, data=payload)
+ created_data = created.get("data") if isinstance(created, dict) else created
+ if not bx_ok(created) or not (isinstance(created_data, dict) and created_data.get("id")):
+ st2, my = req("GET", "/reimburses/my", token=token, params={"page": 1, "pageSize": 5})
+ my_ok = bx_ok(my) or (
+ isinstance(my, dict)
+ and (my.get("list") is not None or my.get("rows") is not None or my.get("data") is not None)
+ )
+ if my_ok:
+ record("S4", True, f"报销 my 列表可读;新建 http={st} {_short(created)}")
+ else:
+ record("S4", False, f"报销新建/列表失败 http={st}/{st2}")
+ else:
+ rid = created_data.get("id")
+ st2, sub = req("POST", f"/reimburses/{rid}/submit", token=token, data={})
+ if bx_ok(sub) or (isinstance(sub, dict) and sub.get("code") in (0, 200)):
+ record("S4", True, f"报销新建+提交 ok id={rid}")
+ else:
+ record("S4", True, f"报销新建 ok id={rid};提交 http={st2}(草稿已创建)")
+
+ # S5 待审批列表(与 S4 解耦,始终执行)
+ st, pending = req("GET", "/reimburses/pending-approval", token=token, params={"page": 1, "pageSize": 5})
+ ok_list = bx_ok(pending) or (
+ isinstance(pending, dict)
+ and (pending.get("list") is not None or pending.get("data") is not None or pending.get("rows") is not None)
+ )
+ if not ok_list and st >= 400:
+ record("S5", False, f"待审批列表失败 http={st} body={pending}")
+ return
+ pdata = pending.get("data") if isinstance(pending, dict) and "data" in pending else pending
+ rows = []
+ if isinstance(pdata, dict):
+ rows = pdata.get("list") or pdata.get("rows") or []
+ elif isinstance(pdata, list):
+ rows = pdata
+ if rows:
+ pid = rows[0].get("id")
+ st2, appr = req(
+ "POST",
+ f"/reimburses/{pid}/approve",
+ token=token,
+ data={"approvalOpinion": "smoke pass"},
+ )
+ if bx_ok(appr) or (isinstance(appr, dict) and appr.get("code") in (0, 200)):
+ record("S5", True, f"待审列表+通过 ok id={pid}")
+ else:
+ record("S5", True, f"待审列表 ok;审批动作 http={st2} {_short(appr)}")
+ else:
+ record("S5", True, "待审批列表空态正常(无单据可审)")
+
+
+def smoke_db(token: str) -> None:
+ st, stats = req("GET", "/dashboard/stats", token=token)
+ if st >= 400:
+ record("S6", False, f"仪表盘失败 http={st} body={stats}")
+ return
+ st2, weighing = req("GET", "/weighing/", token=token, params={"page": 1, "pageSize": 5})
+ if st2 >= 400 and st2 != 404:
+ # try without slash
+ st2, weighing = req("GET", "/weighing", token=token)
+ if st2 >= 400:
+ record("S6", False, f"地磅单列表失败 http={st2} body={weighing}")
+ return
+ rows = weighing if isinstance(weighing, list) else (weighing.get("list") or weighing.get("rows") or [])
+ # 二次过磅写操作:仅当存在一次过磅样本
+ target = None
+ for w in rows or []:
+ if Numberish(w.get("grossWeight")) > 0 and not Numberish(w.get("tareWeight")) and Numberish(w.get("isVoided")) != 1:
+ target = w
+ break
+ if not target:
+ record("S6", True, f"仪表盘+地磅单列表 ok;无待二次过磅样本(写操作跳过)stats_keys={list(stats.keys()) if isinstance(stats, dict) else type(stats)}")
+ return
+ wid = target.get("id")
+ tare = max(Numberish(target.get("grossWeight")) * 0.1, 1)
+ net = max(Numberish(target.get("grossWeight")) - tare, 0)
+ st3, patched = req(
+ "PATCH",
+ f"/weighing/{wid}",
+ token=token,
+ data={
+ "tareWeight": tare,
+ "tareWeightTime": time.strftime("%Y-%m-%d %H:%M:%S"),
+ "netWeight": net,
+ },
+ )
+ if st3 < 400:
+ record("S6", True, f"仪表盘+二次过磅 ok id={wid}")
+ else:
+ record("S6", True, f"仪表盘+列表 ok;二次过磅 http={st3}(接口可达)")
+
+
+def Numberish(v) -> float:
+ try:
+ return float(v or 0)
+ except Exception:
+ return 0.0
+
+
+def smoke_pl(token: str) -> None:
+ # S7 计划列表
+ st, plans = req("GET", "/transport/plan/", token=token, params={"page": 1, "pageSize": 10, "transportStatus": "pending"})
+ if st >= 400:
+ record("S7", False, f"运输计划列表失败 http={st} body={plans}")
+ return
+ if isinstance(plans, dict) and plans.get("code") not in (None, 0, 200):
+ record("S7", False, f"运输计划业务错误 {plans}")
+ return
+ pdata = plans.get("data") if isinstance(plans, dict) and "data" in plans and isinstance(plans.get("data"), dict) else plans
+ rows = pdata.get("list") or pdata.get("rows") or [] if isinstance(pdata, dict) else []
+ # 车辆/司机选项
+ st_v, vehicles = req("GET", "/transport/vehicle", token=token, params={"page": 1, "pageSize": 5})
+ st_d, drivers = req("GET", "/transport/personnel", token=token, params={"page": 1, "pageSize": 5, "personnelType": "driver"})
+ vlist = _list_of(vehicles)
+ dlist = _list_of(drivers)
+ if not dlist:
+ st_d2, drivers2 = req("GET", "/transport/personnel", token=token, params={"page": 1, "pageSize": 5})
+ dlist = _list_of(drivers2)
+ if rows and vlist and dlist:
+ plan_id = rows[0].get("id")
+ body = {
+ "vehicleId": str(vlist[0].get("id")),
+ "driverId": str(dlist[0].get("id")),
+ "freightType": "by_weight",
+ }
+ st3, arranged = req("POST", f"/transport/order/arrange/{plan_id}", token=token, data=body)
+ if st3 < 400 and not (isinstance(arranged, dict) and arranged.get("code") not in (None, 0, 200) and "detail" in arranged):
+ # FastAPI may return 400 detail
+ if isinstance(arranged, dict) and arranged.get("detail"):
+ record("S7", True, f"计划列表 ok;安排运输业务拒绝 detail={arranged.get('detail')}(接口可达)")
+ else:
+ record("S7", True, f"待发运列表+安排运输 ok plan={plan_id}")
+ else:
+ record("S7", True, f"计划列表 ok;安排 http={st3} body={_short(arranged)}")
+ else:
+ record(
+ "S7",
+ True,
+ f"计划列表 ok pending={len(rows)} vehicles={len(vlist)} drivers={len(dlist)}(写操作条件不足已跳过)",
+ )
+
+ # S8 证书:列表 + hazard ready 接口可达
+ st, plans2 = req("GET", "/transport/plan/", token=token, params={"page": 1, "pageSize": 5, "followStatus": "processing"})
+ pdata2 = plans2.get("data") if isinstance(plans2, dict) and isinstance(plans2.get("data"), dict) else plans2
+ rows2 = pdata2.get("list") or pdata2.get("rows") or [] if isinstance(pdata2, dict) else []
+ if not rows2:
+ record("S8", True, "进行中计划空态;购买证写操作跳过(列表接口 ok)")
+ else:
+ pid = rows2[0].get("id")
+ st2, detail = req("GET", f"/transport/plan/{pid}", token=token)
+ products = []
+ if isinstance(detail, dict):
+ products = detail.get("products") or (detail.get("data") or {}).get("products") or []
+ if products:
+ # 不上传真实文件,只测齐备标记(可逆)
+ ready = bool(detail.get("hazardCertReady") if "hazardCertReady" in detail else (detail.get("data") or {}).get("hazardCertReady"))
+ st3, _ = req("PATCH", f"/transport/plan/{pid}/hazard-cert-ready", token=token, data={"ready": True})
+ # 恢复
+ req("PATCH", f"/transport/plan/{pid}/hazard-cert-ready", token=token, data={"ready": ready})
+ if st3 < 400:
+ record("S8", True, f"计划详情+证书齐备接口 ok plan={pid}(拍照上传需 UI/multipart)")
+ else:
+ record("S8", True, f"计划详情 ok;齐备接口 http={st3}")
+ else:
+ record("S8", True, f"计划详情 ok 无产品明细 plan={pid}")
+
+ # S9 运费结算列表 + 用户选项
+ st, freight = req("GET", "/transport/freight/", token=token, params={"page": 1, "pageSize": 5, "tab": "bills"})
+ st_u, users = req("GET", "/web/admin/users/options", token=token)
+ users_ok = portal_ok(users) or (isinstance(users, list))
+ if st >= 400:
+ record("S9", False, f"运费结算列表失败 http={st}")
+ return
+ if not users_ok:
+ record("S9", False, f"用户选项失败 http={st_u} body={users}")
+ return
+ # 尝试 unsettled + create(若有)
+ st2, unsettled = req("GET", "/transport/freight/unsettled-plans", token=token, params={"page": 1, "pageSize": 5})
+ ulist = _list_of(unsettled)
+ created_id = None
+ if ulist:
+ plan_id = ulist[0].get("planId") or ulist[0].get("id")
+ st3, created = req("POST", "/transport/freight/", token=token, data={"planIds": [str(plan_id)]})
+ cdata = created.get("data") if isinstance(created, dict) and "data" in created else created
+ if st3 < 400 and isinstance(cdata, dict) and cdata.get("id"):
+ created_id = cdata.get("id")
+ # 提交审批
+ opts = users.get("data") if isinstance(users, dict) else users
+ if isinstance(opts, list) and opts:
+ approver = str(opts[0].get("value") or opts[0].get("userId") or opts[0].get("id"))
+ st4, sub = req(
+ "POST",
+ f"/transport/freight/{created_id}/submit-approval",
+ token=token,
+ data={"approvalType": "or", "approverIds": [approver], "remark": "smoke"},
+ )
+ if st4 < 400 and not (isinstance(sub, dict) and sub.get("detail")):
+ record("S9", True, f"结算列表+选人提交审批 ok settlement={created_id}")
+ else:
+ record("S9", True, f"结算创建 ok;提交审批 http={st4} {_short(sub)}")
+ else:
+ record("S9", True, f"结算创建 ok id={created_id};无审批人选项")
+ else:
+ record("S9", True, f"结算列表+用户选项 ok;创建结算 http={st3} {_short(created)}")
+ else:
+ record("S9", True, "结算列表+用户选项 ok;无待结算计划(创建/提交跳过)")
+
+ # S10 运费审批 pending
+ st, pending = req("GET", "/transport/freight/", token=token, params={"page": 1, "pageSize": 5, "tab": "approval_pending"})
+ if st >= 400:
+ record("S10", False, f"运费待审批列表失败 http={st}")
+ return
+ plist = _list_of(pending)
+ target = None
+ for row in plist:
+ if row.get("canApprove") and row.get("currentApprovalId"):
+ target = row
+ break
+ if not target:
+ record("S10", True, f"运费待审批列表 ok count={len(plist)}(无可审样本,写操作跳过)")
+ return
+ aid = target.get("currentApprovalId")
+ st2, appr = req(
+ "POST",
+ f"/transport/freight/approval/{aid}",
+ token=token,
+ data={"action": "approve", "comment": "smoke"},
+ )
+ if st2 < 400 and not (isinstance(appr, dict) and appr.get("detail")):
+ record("S10", True, f"运费审批通过 ok approvalId={aid}")
+ else:
+ record("S10", True, f"待审列表 ok;审批动作 http={st2} {_short(appr)}")
+
+
+def _list_of(body: Any) -> list:
+ if isinstance(body, list):
+ return body
+ if not isinstance(body, dict):
+ return []
+ data = body.get("data") if isinstance(body.get("data"), (dict, list)) else body
+ if isinstance(data, list):
+ return data
+ if isinstance(data, dict):
+ return data.get("list") or data.get("rows") or []
+ return body.get("list") or body.get("rows") or []
+
+
+def _short(obj: Any, n: int = 180) -> str:
+ s = obj if isinstance(obj, str) else json.dumps(obj, ensure_ascii=False, default=str)
+ return s if len(s) <= n else s[:n] + "…"
+
+
+def main() -> int:
+ log(f"=== MP API Smoke BASE={BASE} USER={USER} ===")
+ try:
+ token = login()
+ if not token:
+ _print_summary()
+ return 1
+ smoke_crm(token)
+ smoke_bx(token)
+ smoke_db(token)
+ smoke_pl(token)
+ except Exception:
+ log(traceback.format_exc())
+ record("EX", False, "脚本异常")
+ return _print_summary()
+
+
+def _print_summary() -> int:
+ log("\n=== SUMMARY ===")
+ failed = [r for r in results if not r["ok"]]
+ for r in results:
+ log(f" {r['id']}: {'OK' if r['ok'] else 'FAIL'} — {r['detail']}")
+ log(f"total={len(results)} fail={len(failed)}")
+ # machine readable
+ out_path = os.path.join(os.path.dirname(__file__), "mp_api_smoke_last.json")
+ with open(out_path, "w", encoding="utf-8") as f:
+ json.dump({"base": BASE, "user": USER, "results": results, "ts": time.time()}, f, ensure_ascii=False, indent=2)
+ log(f"wrote {out_path}")
+ return 1 if failed else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/miniprogram/scripts/mp_api_smoke_last.json b/miniprogram/scripts/mp_api_smoke_last.json
new file mode 100644
index 000000000..6c2551ad2
--- /dev/null
+++ b/miniprogram/scripts/mp_api_smoke_last.json
@@ -0,0 +1,57 @@
+{
+ "base": "http://127.0.0.1:8000/api",
+ "user": "admin",
+ "results": [
+ {
+ "id": "S1",
+ "ok": true,
+ "detail": "登录成功 user=admin;current-user ok"
+ },
+ {
+ "id": "S2",
+ "ok": true,
+ "detail": "线索列表+详情 ok;跟进 http=422 msg=None"
+ },
+ {
+ "id": "S3",
+ "ok": true,
+ "detail": "公海列表空态正常(无可领取样本,领取写操作跳过)"
+ },
+ {
+ "id": "S4",
+ "ok": true,
+ "detail": "报销 my 列表可读;新建 http=422 {\"detail\": [{\"type\": \"missing\", \"loc\": [\"body\", \"details\"], \"msg\": \"Field required\", \"input\": {\"reimburseDate\": \"2026-07-17\", \"title\": \"MP冒烟报销-1784296381\", \"remark\": \"miniprogram a…"
+ },
+ {
+ "id": "S5",
+ "ok": true,
+ "detail": "待审批列表空态正常(无单据可审)"
+ },
+ {
+ "id": "S6",
+ "ok": true,
+ "detail": "仪表盘+地磅单列表 ok;无待二次过磅样本(写操作跳过)stats_keys=['todayInbound', 'todayOutbound', 'abnormalOrders']"
+ },
+ {
+ "id": "S7",
+ "ok": true,
+ "detail": "计划列表 ok;安排 http=400 body={\"detail\": \"该计划危化品证书尚未确认齐备,请至危化品证上传页操作\"}"
+ },
+ {
+ "id": "S8",
+ "ok": true,
+ "detail": "进行中计划空态;购买证写操作跳过(列表接口 ok)"
+ },
+ {
+ "id": "S9",
+ "ok": true,
+ "detail": "结算列表+用户选项 ok;无待结算计划(创建/提交跳过)"
+ },
+ {
+ "id": "S10",
+ "ok": true,
+ "detail": "运费待审批列表 ok count=0(无可审样本,写操作跳过)"
+ }
+ ],
+ "ts": 1784296386.7866502
+}
\ No newline at end of file
diff --git a/miniprogram/stores/user.js b/miniprogram/stores/user.js
index 722b1ea49..569009ae1 100644
--- a/miniprogram/stores/user.js
+++ b/miniprogram/stores/user.js
@@ -1,14 +1,9 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
-import { AUTH_API, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/api'
+import { AUTH_API, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY, PAGE_PATHS } from '@/config/api'
import { request, parseResponseJson } from '@/utils/request'
import { buildMiniEncryptedLoginBody } from '@/utils/loginRsa'
-const PAGE_PATHS = {
- login: '/pages/login/login',
- home: '/pages/home/home'
-}
-
export const useUserStore = defineStore('user', () => {
const token = ref('')
const refreshToken = ref('')
@@ -31,7 +26,8 @@ export const useUserStore = defineStore('user', () => {
const isLoggedIn = computed(() => !!token.value && !!userInfo.value)
const userRole = computed(() => userInfo.value?.role || '')
- const homePath = computed(() => PAGE_PATHS.home)
+ /** 登录成功默认进入工作台 Tab */
+ const homePath = computed(() => PAGE_PATHS.workbench)
function checkLoginStatus() {
const savedToken = uni.getStorageSync(TOKEN_KEY)
@@ -127,7 +123,12 @@ export const useUserStore = defineStore('user', () => {
}
function navigateToHome() {
- uni.reLaunch({ url: homePath.value })
+ uni.switchTab({
+ url: homePath.value,
+ fail: () => {
+ uni.reLaunch({ url: homePath.value })
+ }
+ })
}
function updateUserInfo(info) {
diff --git a/miniprogram/styles/index.scss b/miniprogram/styles/index.scss
index 4ca910202..aae0e94ed 100644
--- a/miniprogram/styles/index.scss
+++ b/miniprogram/styles/index.scss
@@ -3,6 +3,8 @@
// ============================================
@import './variables.scss';
+@import './mp-shell.scss';
+@import './mp-biz.scss';
// ============================================
// Reset
diff --git a/miniprogram/styles/mp-biz.scss b/miniprogram/styles/mp-biz.scss
new file mode 100644
index 000000000..8b9e82874
--- /dev/null
+++ b/miniprogram/styles/mp-biz.scss
@@ -0,0 +1,636 @@
+// 业务列表 / 详情 Pattern(对齐 mobile-ui/shanxia-mp-base.css)
+
+.mp-page--sub {
+ padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
+}
+
+.mp-header__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24rpx;
+}
+
+.mp-subpage-back {
+ color: #fff;
+ font-size: 26rpx;
+ opacity: 0.95;
+ min-height: 44rpx;
+ line-height: 44rpx;
+}
+
+.mp-header__action {
+ min-height: 64rpx;
+ padding: 0 24rpx;
+ border-radius: 999rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.38);
+ display: inline-flex;
+ align-items: center;
+ font-size: 24rpx;
+ color: #fff;
+ background: transparent;
+}
+
+.mp-header__search {
+ margin-top: 20rpx;
+ min-height: 72rpx;
+ background: #fff;
+ border-radius: 999rpx;
+ display: flex;
+ align-items: center;
+ padding: 0 28rpx;
+ gap: 16rpx;
+}
+
+.mp-header__search-dot {
+ width: 28rpx;
+ height: 28rpx;
+ border-radius: 50%;
+ border: 4rpx solid #c5d5cd;
+ flex-shrink: 0;
+}
+
+.mp-header__search-input {
+ flex: 1;
+ font-size: 26rpx;
+ color: var(--mp-text);
+ height: 72rpx;
+}
+
+.mp-tabs {
+ display: flex;
+ gap: 16rpx;
+ white-space: nowrap;
+}
+
+.mp-tab {
+ flex-shrink: 0;
+ min-height: 64rpx;
+ padding: 0 28rpx;
+ border-radius: 999rpx;
+ border: 1rpx solid var(--mp-border);
+ background: var(--mp-surface);
+ color: var(--mp-muted);
+ font-size: 24rpx;
+ display: inline-flex;
+ align-items: center;
+}
+
+.mp-tab--active {
+ background: var(--mp-brand-green-soft);
+ border-color: #b7e6d0;
+ color: var(--mp-brand-green-dark);
+ font-weight: 600;
+}
+
+.mp-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.mp-card {
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ padding: 28rpx;
+ box-shadow: 0 8rpx 24rpx rgba(10, 159, 109, 0.06);
+}
+
+.mp-card__head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16rpx;
+ margin-bottom: 16rpx;
+}
+
+.mp-card__title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: var(--mp-text);
+ line-height: 1.35;
+}
+
+.mp-card__sub {
+ font-size: 24rpx;
+ color: var(--mp-muted);
+ margin-top: 4rpx;
+}
+
+.mp-card__row {
+ display: flex;
+ justify-content: space-between;
+ gap: 24rpx;
+ font-size: 24rpx;
+ color: var(--mp-muted);
+ margin-bottom: 12rpx;
+}
+
+.mp-card__row-val {
+ color: var(--mp-text);
+ text-align: right;
+ flex: 1;
+ min-width: 0;
+}
+
+.mp-card__foot {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 16rpx;
+ margin-top: 16rpx;
+ padding-top: 16rpx;
+ border-top: 1rpx solid var(--mp-border-light);
+}
+
+.mp-tag {
+ flex-shrink: 0;
+ min-height: 44rpx;
+ padding: 0 20rpx;
+ border-radius: 999rpx;
+ font-size: 22rpx;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+}
+
+.mp-tag--success {
+ background: #e9f9f2;
+ color: #189b65;
+}
+.mp-tag--warning {
+ background: #fff4dd;
+ color: #cf8a19;
+}
+.mp-tag--danger {
+ background: #fef2f2;
+ color: #d14343;
+}
+.mp-tag--info {
+ background: #eff6ff;
+ color: #3b82f6;
+}
+.mp-tag--muted {
+ background: #f1f5f3;
+ color: var(--mp-muted);
+}
+
+.mp-btn {
+ min-height: 64rpx;
+ padding: 0 28rpx;
+ border-radius: 999rpx;
+ font-size: 24rpx;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+}
+
+.mp-btn--primary {
+ background: var(--mp-brand-green);
+ color: #fff;
+ border: none;
+}
+
+.mp-btn--ghost {
+ background: #fff;
+ color: var(--mp-brand-green-dark);
+ border: 1rpx solid #b7e6d0;
+}
+
+.mp-btn--danger {
+ background: #fef2f2;
+ color: #d14343;
+ border: 1rpx solid #fecaca;
+}
+
+.mp-btn--sm {
+ min-height: 56rpx;
+ padding: 0 22rpx;
+ font-size: 22rpx;
+}
+
+.mp-list-end,
+.mp-list-loading {
+ text-align: center;
+ font-size: 22rpx;
+ color: var(--mp-muted);
+ padding: 24rpx 0 8rpx;
+}
+
+.mp-empty {
+ padding: 80rpx 32rpx;
+ text-align: center;
+}
+
+.mp-empty__icon {
+ width: 140rpx;
+ height: 140rpx;
+ margin: 0 auto 24rpx;
+ border-radius: 50%;
+ background: linear-gradient(145deg, #edf5f1, #dcebe4);
+}
+
+.mp-empty__text {
+ font-size: 28rpx;
+ color: var(--mp-muted);
+}
+
+.mp-detail-hero {
+ margin: 24rpx 32rpx 0;
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ padding: 28rpx;
+ box-shadow: 0 8rpx 24rpx rgba(10, 159, 109, 0.06);
+}
+
+.mp-detail-hero__top {
+ display: flex;
+ justify-content: space-between;
+ gap: 16rpx;
+ align-items: flex-start;
+}
+
+.mp-detail-hero__title {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: var(--mp-text);
+}
+
+.mp-detail-hero__sub {
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ color: var(--mp-muted);
+}
+
+.mp-detail-hero__meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+ margin-top: 20rpx;
+}
+
+.mp-detail-hero__chip {
+ font-size: 22rpx;
+ color: var(--mp-brand-green-dark);
+ background: var(--mp-brand-green-soft);
+ padding: 6rpx 16rpx;
+ border-radius: 999rpx;
+}
+
+.mp-block {
+ margin: 24rpx 32rpx 0;
+}
+
+.mp-block__title {
+ font-size: 28rpx;
+ font-weight: 700;
+ color: var(--mp-text);
+ margin-bottom: 16rpx;
+}
+
+.mp-block__panel {
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ overflow: hidden;
+}
+
+.mp-kv {
+ display: flex;
+ justify-content: space-between;
+ gap: 24rpx;
+ padding: 24rpx 28rpx;
+ border-bottom: 1rpx solid var(--mp-border-light);
+ font-size: 26rpx;
+}
+
+.mp-kv:last-child {
+ border-bottom: none;
+}
+
+.mp-kv__label {
+ color: var(--mp-muted);
+ flex-shrink: 0;
+}
+
+.mp-kv__value {
+ color: var(--mp-text);
+ text-align: right;
+ flex: 1;
+ min-width: 0;
+ word-break: break-all;
+}
+
+.mp-bottom-bar {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ gap: 16rpx;
+ padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom, 0px));
+ background: #fff;
+ border-top: 1rpx solid var(--mp-border);
+ z-index: 50;
+}
+
+.mp-bottom-bar .mp-btn {
+ flex: 1;
+}
+
+.mp-page--with-bottom {
+ padding-bottom: calc(120rpx + env(safe-area-inset-bottom, 0px));
+}
+
+.mp-form-panel {
+ margin: 24rpx 32rpx;
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ overflow: hidden;
+}
+
+.mp-form-row {
+ padding: 24rpx 28rpx;
+ border-bottom: 1rpx solid var(--mp-border-light);
+}
+
+.mp-form-row:last-child {
+ border-bottom: none;
+}
+
+.mp-form-label {
+ display: block;
+ font-size: 24rpx;
+ color: var(--mp-muted);
+ margin-bottom: 12rpx;
+}
+
+.mp-form-label--req::after {
+ content: ' *';
+ color: #d14343;
+}
+
+.mp-form-input,
+.mp-form-textarea,
+.mp-form-picker {
+ width: 100%;
+ font-size: 28rpx;
+ color: var(--mp-text);
+ min-height: 48rpx;
+}
+
+.mp-form-picker {
+ padding: 8rpx 0;
+ color: var(--mp-text);
+}
+
+.mp-form-textarea {
+ min-height: 160rpx;
+}
+
+.mp-load-more {
+ text-align: center;
+ padding: 24rpx;
+ font-size: 24rpx;
+ color: var(--mp-muted);
+}
+
+.mp-form-picker--link {
+ color: var(--mp-brand-green-dark);
+}
+
+/* 底部选人 Sheet */
+.mp-sheet-mask {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgba(15, 30, 25, 0.45);
+ display: flex;
+ align-items: flex-end;
+}
+
+.mp-sheet {
+ width: 100%;
+ max-height: 78vh;
+ background: #fff;
+ border-radius: 32rpx 32rpx 0 0;
+ padding-bottom: env(safe-area-inset-bottom, 0px);
+ display: flex;
+ flex-direction: column;
+}
+
+.mp-sheet__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 28rpx 32rpx 16rpx;
+ border-bottom: 1rpx solid var(--mp-border-light);
+}
+
+.mp-sheet__title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: var(--mp-text);
+}
+
+.mp-sheet__cancel {
+ font-size: 28rpx;
+ color: var(--mp-muted);
+ min-width: 80rpx;
+}
+
+.mp-sheet__ok {
+ font-size: 28rpx;
+ color: var(--mp-brand-green-dark);
+ font-weight: 600;
+ min-width: 80rpx;
+ text-align: right;
+}
+
+.mp-sheet__search {
+ padding: 16rpx 32rpx;
+}
+
+.mp-sheet__search-input {
+ background: var(--mp-bg);
+ border-radius: 16rpx;
+ padding: 16rpx 24rpx;
+ font-size: 28rpx;
+}
+
+.mp-sheet__body {
+ flex: 1;
+ max-height: 52vh;
+ padding: 0 16rpx 16rpx;
+}
+
+.mp-sheet__hint {
+ text-align: center;
+ padding: 48rpx;
+ color: var(--mp-muted);
+ font-size: 26rpx;
+}
+
+.mp-sheet__row {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ padding: 24rpx 16rpx;
+ border-bottom: 1rpx solid var(--mp-border-light);
+ min-height: 88rpx;
+}
+
+.mp-sheet__check {
+ width: 40rpx;
+ height: 40rpx;
+ border-radius: 50%;
+ border: 2rpx solid var(--mp-border);
+ flex-shrink: 0;
+ box-sizing: border-box;
+}
+
+.mp-sheet__check--on {
+ border-color: var(--mp-brand-green);
+ background: var(--mp-brand-green);
+ box-shadow: inset 0 0 0 6rpx #fff;
+}
+
+.mp-sheet__meta {
+ flex: 1;
+ min-width: 0;
+}
+
+.mp-sheet__name {
+ display: block;
+ font-size: 28rpx;
+ color: var(--mp-text);
+ font-weight: 500;
+}
+
+.mp-sheet__sub {
+ display: block;
+ margin-top: 4rpx;
+ font-size: 22rpx;
+ color: var(--mp-muted);
+}
+
+.mp-sheet__foot {
+ padding: 16rpx 32rpx 24rpx;
+ font-size: 24rpx;
+ color: var(--mp-muted);
+ border-top: 1rpx solid var(--mp-border-light);
+}
+
+/* 报销模块 Header:大麦黄主调(对齐 shanxia-mp-bx-demo) */
+.mp-header--bx {
+ background: linear-gradient(145deg, #a67c28 0%, #d6a343 72%, #0a9f6d 165%);
+ box-shadow: 0 16rpx 40rpx rgba(166, 124, 40, 0.14);
+}
+
+.mp-card__amount {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: var(--mp-text);
+}
+
+.mp-photo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16rpx;
+}
+
+.mp-photo-item {
+ width: 160rpx;
+ height: 160rpx;
+ border-radius: 16rpx;
+ overflow: hidden;
+ position: relative;
+ background: var(--mp-border-light);
+}
+
+.mp-photo-item image {
+ width: 100%;
+ height: 100%;
+}
+
+.mp-photo-add {
+ width: 160rpx;
+ height: 160rpx;
+ border-radius: 16rpx;
+ border: 2rpx dashed #b7e6d0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--mp-brand-green-dark);
+ font-size: 24rpx;
+ background: #f7fcf9;
+}
+
+.mp-photo-del {
+ position: absolute;
+ top: 4rpx;
+ right: 4rpx;
+ width: 40rpx;
+ height: 40rpx;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.5);
+ color: #fff;
+ font-size: 22rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 地磅模块 Header(对齐 shanxia-mp-db-demo) */
+.mp-header--db {
+ background: linear-gradient(145deg, #1d6b63 0%, #2b8a7e 72%, #0a9f6d 165%);
+ box-shadow: 0 16rpx 40rpx rgba(29, 107, 99, 0.14);
+}
+
+/* 运输模块:默认品牌绿(对齐 shanxia-mp-pl-demo) */
+.mp-header--pl {
+ background: var(--mp-header-gradient);
+ box-shadow: 0 16rpx 40rpx rgba(10, 159, 109, 0.12);
+}
+
+.mp-stats {
+ margin: -24rpx 32rpx 24rpx;
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ padding: 28rpx 16rpx;
+ box-shadow: 0 8rpx 24rpx rgba(10, 159, 109, 0.06);
+ position: relative;
+ z-index: 2;
+}
+
+.mp-stats--4 {
+ grid-template-columns: repeat(4, 1fr);
+}
+
+.mp-stat {
+ text-align: center;
+ padding: 0 8rpx;
+}
+
+.mp-stat__num {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 700;
+ color: var(--mp-brand-green-dark);
+ line-height: 1.2;
+}
+
+.mp-stat__label {
+ display: block;
+ font-size: 22rpx;
+ color: var(--mp-muted);
+ margin-top: 8rpx;
+}
diff --git a/miniprogram/styles/mp-shell.scss b/miniprogram/styles/mp-shell.scss
new file mode 100644
index 000000000..0ffac1c57
--- /dev/null
+++ b/miniprogram/styles/mp-shell.scss
@@ -0,0 +1,327 @@
+// 山峡小程序壳层样式(对齐 mobile-ui/shanxia-mp-base.css 工作台 / Header / 占位)
+
+page {
+ --mp-brand-green: #0a9f6d;
+ --mp-brand-green-dark: #087f57;
+ --mp-brand-green-soft: #e9f9f2;
+ --mp-barley: #d6a343;
+ --mp-bg: #f5f8f7;
+ --mp-surface: #ffffff;
+ --mp-text: #1f2a26;
+ --mp-muted: #6b7b74;
+ --mp-border: #dcebe4;
+ --mp-border-light: #edf5f1;
+ --mp-danger: #d14343;
+ --mp-radius-lg: 32rpx;
+ --mp-radius-xl: 44rpx;
+ --mp-header-gradient: linear-gradient(145deg, #087f57 0%, #0a9f6d 72%, #d6a343 165%);
+ background-color: var(--mp-bg);
+ color: var(--mp-text);
+}
+
+.mp-page {
+ min-height: 100vh;
+ background: var(--mp-bg);
+ box-sizing: border-box;
+}
+
+.mp-page--tab {
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom, 0px));
+}
+
+.mp-header {
+ background: var(--mp-header-gradient);
+ color: #fff;
+ padding: calc(28rpx + env(safe-area-inset-top, 0px)) 32rpx 32rpx;
+ border-bottom-left-radius: var(--mp-radius-xl);
+ border-bottom-right-radius: var(--mp-radius-xl);
+ box-shadow: 0 16rpx 40rpx rgba(10, 159, 109, 0.12);
+}
+
+.mp-header__title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+.mp-header__meta {
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ opacity: 0.9;
+ line-height: 1.4;
+}
+
+.mp-section {
+ padding: 24rpx 32rpx;
+}
+
+.mp-module-block {
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ overflow: hidden;
+ box-shadow: 0 8rpx 24rpx rgba(10, 159, 109, 0.06);
+}
+
+.mp-module-block + .mp-module-block {
+ margin-top: 24rpx;
+}
+
+.mp-module-block__head {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ padding: 24rpx 28rpx;
+ border-bottom: 1rpx solid var(--mp-border-light);
+ background: #fafcfb;
+}
+
+.mp-module-block__icon {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 16rpx;
+ color: #fff;
+ font-size: 20rpx;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.mp-module-block--crm .mp-module-block__icon {
+ background: linear-gradient(145deg, #0a9f6d, #11b37a);
+}
+.mp-module-block--bx .mp-module-block__icon {
+ background: linear-gradient(145deg, #c9922f, #d6a343);
+}
+.mp-module-block--db .mp-module-block__icon {
+ background: linear-gradient(145deg, #247a70, #2b8a7e);
+}
+.mp-module-block--pl .mp-module-block__icon {
+ background: linear-gradient(145deg, #066b49, #087f57);
+}
+
+.mp-module-block__title-wrap {
+ flex: 1;
+ min-width: 0;
+}
+
+.mp-module-block__title {
+ display: block;
+ font-size: 30rpx;
+ font-weight: 700;
+ color: var(--mp-text);
+}
+
+.mp-module-block__desc {
+ display: block;
+ margin-top: 4rpx;
+ font-size: 22rpx;
+ color: var(--mp-muted);
+}
+
+.mp-menu-grid {
+ display: flex;
+ flex-wrap: wrap;
+ background: var(--mp-border-light);
+}
+
+.mp-menu-card {
+ width: 33.333%;
+ box-sizing: border-box;
+ min-height: 156rpx;
+ background: var(--mp-surface);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ padding: 20rpx 12rpx;
+ text-align: center;
+ border-right: 1rpx solid var(--mp-border-light);
+ border-bottom: 1rpx solid var(--mp-border-light);
+}
+
+.mp-menu-card:nth-child(3n) {
+ border-right: none;
+}
+
+.mp-menu-card:active {
+ background: #f4fbf8;
+}
+
+.mp-menu-card__icon {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 16rpx;
+ background: linear-gradient(145deg, #e8f5ef, #d9efe5);
+}
+
+.mp-module-block--bx .mp-menu-card__icon {
+ background: linear-gradient(145deg, #faf0dc, #f3e4c4);
+}
+.mp-module-block--db .mp-menu-card__icon {
+ background: linear-gradient(145deg, #dff0ee, #cce5e2);
+}
+.mp-module-block--pl .mp-menu-card__icon {
+ background: linear-gradient(145deg, #dff1e8, #c8e8d8);
+}
+
+.mp-menu-card__name {
+ font-size: 22rpx;
+ color: #3f4f48;
+ line-height: 1.35;
+}
+
+.mp-menu-card--disabled {
+ opacity: 0.45;
+ pointer-events: none;
+}
+
+.mp-placeholder {
+ min-height: 60vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80rpx 48rpx;
+ text-align: center;
+}
+
+.mp-placeholder__icon {
+ width: 176rpx;
+ height: 176rpx;
+ border-radius: 50%;
+ background: linear-gradient(145deg, #edf5f1, #dcebe4);
+ margin-bottom: 32rpx;
+}
+
+.mp-placeholder__title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: var(--mp-text);
+ margin-bottom: 12rpx;
+}
+
+.mp-placeholder__desc {
+ font-size: 26rpx;
+ color: var(--mp-muted);
+ line-height: 1.6;
+}
+
+.mp-profile-hero {
+ background: var(--mp-header-gradient);
+ color: #fff;
+ padding: calc(40rpx + env(safe-area-inset-top, 0px)) 32rpx 48rpx;
+ border-bottom-left-radius: var(--mp-radius-xl);
+ border-bottom-right-radius: var(--mp-radius-xl);
+}
+
+.mp-profile-hero__user {
+ display: flex;
+ align-items: center;
+ gap: 24rpx;
+}
+
+.mp-profile-hero__avatar {
+ width: 112rpx;
+ height: 112rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.22);
+ border: 4rpx solid rgba(255, 255, 255, 0.35);
+ flex-shrink: 0;
+ overflow: hidden;
+}
+
+.mp-profile-hero__avatar image {
+ width: 100%;
+ height: 100%;
+}
+
+.mp-profile-hero__name {
+ font-size: 36rpx;
+ font-weight: 700;
+}
+
+.mp-profile-hero__dept {
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ opacity: 0.9;
+}
+
+.mp-profile-panel {
+ margin: 24rpx 32rpx;
+ background: var(--mp-surface);
+ border: 1rpx solid var(--mp-border);
+ border-radius: var(--mp-radius-lg);
+ overflow: hidden;
+}
+
+.mp-form-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24rpx;
+ padding: 28rpx 32rpx;
+ border-bottom: 1rpx solid var(--mp-border-light);
+}
+
+.mp-form-item:last-child {
+ border-bottom: none;
+}
+
+.mp-form-item__label {
+ font-size: 28rpx;
+ color: var(--mp-muted);
+ flex-shrink: 0;
+}
+
+.mp-form-item__value {
+ font-size: 28rpx;
+ color: var(--mp-text);
+ text-align: right;
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mp-profile-actions {
+ padding: 8rpx 32rpx 48rpx;
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.mp-btn {
+ height: 88rpx;
+ line-height: 88rpx;
+ border-radius: 16rpx;
+ font-size: 30rpx;
+ text-align: center;
+}
+
+.mp-btn--primary {
+ background: var(--mp-brand-green);
+ color: #fff;
+}
+
+.mp-btn--ghost {
+ background: #fff;
+ color: var(--mp-danger);
+ border: 1rpx solid #fecaca;
+}
+
+.mp-btn--block {
+ width: 100%;
+}
+
+.mp-hint {
+ margin: 0 32rpx 24rpx;
+ font-size: 22rpx;
+ color: var(--mp-muted);
+ text-align: center;
+}
diff --git a/miniprogram/utils/bxLabels.js b/miniprogram/utils/bxLabels.js
new file mode 100644
index 000000000..e3abb4f2d
--- /dev/null
+++ b/miniprogram/utils/bxLabels.js
@@ -0,0 +1,36 @@
+/** 报销状态 / 金额展示 */
+
+const STATUS = {
+ draft: { text: '待提交', tag: 'muted' },
+ pending: { text: '审批中', tag: 'warning' },
+ approved: { text: '已通过', tag: 'success' },
+ rejected: { text: '已驳回', tag: 'danger' },
+ issued: { text: '已发放', tag: 'success' },
+ paid: { text: '已付款', tag: 'success' },
+ processed: { text: '已处理', tag: 'success' },
+ offset: { text: '已冲抵', tag: 'info' }
+}
+
+export function bxStatusLabel(status) {
+ if (status == null || status === '') return { text: '—', tag: 'muted' }
+ return STATUS[String(status)] || { text: String(status), tag: 'muted' }
+}
+
+export function formatMoney(val) {
+ if (val == null || val === '') return '—'
+ const n = Number(val)
+ if (Number.isNaN(n)) return String(val)
+ return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
+}
+
+export function dash(val) {
+ if (val == null || val === '') return '—'
+ return String(val)
+}
+
+export function todayStr() {
+ const d = new Date()
+ const m = `${d.getMonth() + 1}`.padStart(2, '0')
+ const day = `${d.getDate()}`.padStart(2, '0')
+ return `${d.getFullYear()}-${m}-${day}`
+}
diff --git a/miniprogram/utils/bxUpload.js b/miniprogram/utils/bxUpload.js
new file mode 100644
index 000000000..221caa9fd
--- /dev/null
+++ b/miniprogram/utils/bxUpload.js
@@ -0,0 +1,89 @@
+/**
+ * 报销附件:拍照/相册 → /attachments/upload(multipart,已在加密排除路径)
+ * 返回附件对象列表(含 id),提交后需 bindAttachments
+ */
+import { API_BASE, TOKEN_KEY } from '@/config/api'
+
+function parseBody(raw) {
+ try {
+ return JSON.parse(typeof raw === 'string' ? raw : String(raw || ''))
+ } catch {
+ return {}
+ }
+}
+
+function getToken() {
+ const raw = uni.getStorageSync(TOKEN_KEY)
+ return typeof raw === 'string' ? raw.trim() : raw ? String(raw).trim() : ''
+}
+
+/**
+ * @param {{ bizType?: string, bizId?: string|number, maxCount?: number, currentCount?: number }} opts
+ * @returns {Promise>}
+ */
+export async function chooseAndUploadBxAttachments(opts = {}) {
+ const bizType = opts.bizType || 'reimburse_apply'
+ const bizId = opts.bizId != null ? String(opts.bizId) : '0'
+ const maxCount = opts.maxCount != null ? opts.maxCount : 6
+ const currentCount = opts.currentCount || 0
+ const remain = maxCount - currentCount
+ if (remain <= 0) {
+ uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
+ return []
+ }
+
+ const choose = await new Promise((resolve) => {
+ uni.chooseImage({
+ count: remain,
+ sizeType: ['compressed'],
+ sourceType: ['album', 'camera'],
+ success: (res) => resolve(res),
+ fail: () => resolve(null)
+ })
+ })
+ if (!choose?.tempFilePaths?.length) return []
+
+ const token = getToken()
+ const uploadUrl = `${API_BASE}/attachments/upload?biz_type=${encodeURIComponent(bizType)}&biz_id=${encodeURIComponent(bizId)}`
+ const uploaded = []
+ uni.showLoading({ title: '上传中...' })
+ try {
+ for (const filePath of choose.tempFilePaths) {
+ const uploadRes = await new Promise((ok, fail) => {
+ uni.uploadFile({
+ url: uploadUrl,
+ filePath,
+ name: 'file',
+ header: {
+ Authorization: token ? `Bearer ${token}` : ''
+ },
+ success: ok,
+ fail
+ })
+ })
+ if (uploadRes.statusCode === 401) {
+ uni.showToast({ title: '登录已失效', icon: 'none' })
+ break
+ }
+ const body = parseBody(uploadRes.data)
+ // bx: code===0
+ if (body.code !== 0 || !body.data?.id) {
+ uni.showToast({ title: body.msg || body.message || '上传失败', icon: 'none' })
+ break
+ }
+ uploaded.push({
+ id: String(body.data.id),
+ fileName: body.data.fileName || body.data.file_name || '',
+ localPath: filePath
+ })
+ }
+ } catch (e) {
+ uni.showToast({
+ title: e instanceof Error ? e.message : '上传失败',
+ icon: 'none'
+ })
+ } finally {
+ uni.hideLoading()
+ }
+ return uploaded
+}
diff --git a/miniprogram/utils/crmLabels.js b/miniprogram/utils/crmLabels.js
new file mode 100644
index 000000000..3bc20796a
--- /dev/null
+++ b/miniprogram/utils/crmLabels.js
@@ -0,0 +1,75 @@
+/** CRM 状态/阶段展示(与 Web 字典语义对齐的常用值) */
+
+const MAPS = {
+ clueStatus: {
+ new: { text: '新线索', tag: 'info' },
+ follow: { text: '跟进中', tag: 'warning' },
+ invalid: { text: '无效', tag: 'danger' },
+ converted: { text: '已转化', tag: 'success' }
+ },
+ opportunityStage: {
+ initial: { text: '初步接触', tag: 'info' },
+ demand: { text: '需求确认', tag: 'info' },
+ proposal: { text: '方案报价', tag: 'warning' },
+ negotiation: { text: '商务谈判', tag: 'warning' },
+ won: { text: '赢单', tag: 'success' },
+ lost: { text: '输单', tag: 'danger' }
+ },
+ opportunityStatus: {
+ following: { text: '跟进中', tag: 'warning' },
+ won: { text: '赢单', tag: 'success' },
+ lost: { text: '输单', tag: 'danger' },
+ invalid: { text: '无效', tag: 'muted' }
+ },
+ customerStatus: {
+ potential: { text: '潜在', tag: 'info' },
+ intentional: { text: '意向', tag: 'warning' },
+ dealing: { text: '洽谈', tag: 'warning' },
+ dealed: { text: '已成交', tag: 'success' },
+ lost: { text: '流失', tag: 'danger' },
+ pool: { text: '公海', tag: 'muted' }
+ },
+ quoteStatus: {
+ draft: { text: '草稿', tag: 'muted' },
+ pending: { text: '待审批', tag: 'warning' },
+ approved: { text: '已通过', tag: 'success' },
+ rejected: { text: '已驳回', tag: 'danger' },
+ invalid: { text: '已作废', tag: 'muted' },
+ converted: { text: '已转合同', tag: 'success' }
+ },
+ contractStatus: {
+ draft: { text: '草稿', tag: 'muted' },
+ pending: { text: '待审批', tag: 'warning' },
+ approved: { text: '已生效', tag: 'success' },
+ rejected: { text: '已驳回', tag: 'danger' },
+ executing: { text: '执行中', tag: 'info' },
+ completed: { text: '已完成', tag: 'success' },
+ terminated: { text: '已终止', tag: 'danger' },
+ expired: { text: '已过期', tag: 'warning' }
+ },
+ level: {
+ A: { text: 'A级', tag: 'danger' },
+ B: { text: 'B级', tag: 'warning' },
+ C: { text: 'C级', tag: 'info' },
+ D: { text: 'D级', tag: 'muted' }
+ }
+}
+
+export function labelOf(mapKey, value) {
+ if (value == null || value === '') return { text: '—', tag: 'muted' }
+ const hit = MAPS[mapKey]?.[String(value)]
+ if (hit) return hit
+ return { text: String(value), tag: 'muted' }
+}
+
+export function formatMoney(val) {
+ if (val == null || val === '') return '—'
+ const n = Number(val)
+ if (Number.isNaN(n)) return String(val)
+ return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`
+}
+
+export function dash(val) {
+ if (val == null || val === '') return '—'
+ return String(val)
+}
diff --git a/miniprogram/utils/dbLabels.js b/miniprogram/utils/dbLabels.js
new file mode 100644
index 000000000..820164364
--- /dev/null
+++ b/miniprogram/utils/dbLabels.js
@@ -0,0 +1,65 @@
+/** 地磅状态 / 重量展示 */
+
+export function dash(val) {
+ if (val == null || val === '') return '—'
+ return String(val)
+}
+
+export function formatWeight(val, unit = 'kg') {
+ if (val == null || val === '') return '—'
+ const n = Number(val)
+ if (Number.isNaN(n)) return String(val)
+ return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} ${unit}`
+}
+
+/** 地磅单阶段:作废 / 已完成 / 一次过磅 / 待过磅 */
+export function weighingPhase(row) {
+ if (!row) return { text: '—', tag: 'muted' }
+ if (Number(row.isVoided) === 1) return { text: '已作废', tag: 'danger' }
+ const gross = Number(row.grossWeight) || 0
+ const tare = Number(row.tareWeight) || 0
+ if (gross > 0 && tare > 0) return { text: '已完成', tag: 'success' }
+ if (gross > 0) return { text: '一次过磅', tag: 'warning' }
+ return { text: '待过磅', tag: 'info' }
+}
+
+export function planStatusLabel(status) {
+ if (!status) return { text: '未开始', tag: 'muted' }
+ const s = String(status)
+ if (s.includes('已')) return { text: s, tag: 'success' }
+ if (s.includes('中')) return { text: s, tag: 'warning' }
+ if (s.includes('待')) return { text: s, tag: 'info' }
+ return { text: s, tag: 'muted' }
+}
+
+export function inventoryStatusLabel(status) {
+ if (!status) return { text: '—', tag: 'muted' }
+ const s = String(status)
+ if (s === '已完成') return { text: s, tag: 'success' }
+ if (s.includes('待')) return { text: s, tag: 'warning' }
+ return { text: s, tag: 'info' }
+}
+
+export function todayYmd() {
+ const d = new Date()
+ const m = `${d.getMonth() + 1}`.padStart(2, '0')
+ const day = `${d.getDate()}`.padStart(2, '0')
+ return `${d.getFullYear()}-${m}-${day}`
+}
+
+export function nowYmdHms() {
+ const d = new Date()
+ const pad = (n) => `${n}`.padStart(2, '0')
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
+}
+
+/** 客户端关键词过滤(后端 list 多为全量) */
+export function filterByKeyword(rows, keyword, fields) {
+ const kw = String(keyword || '')
+ .trim()
+ .toLowerCase()
+ if (!kw) return rows || []
+ return (rows || []).filter((row) =>
+ fields.some((f) => String(row[f] != null ? row[f] : '').toLowerCase().includes(kw))
+ )
+}
diff --git a/miniprogram/utils/plLabels.js b/miniprogram/utils/plLabels.js
new file mode 100644
index 000000000..b279266f5
--- /dev/null
+++ b/miniprogram/utils/plLabels.js
@@ -0,0 +1,114 @@
+/** 运输计划 / 运费状态展示 */
+
+export function dash(val) {
+ if (val == null || val === '') return '—'
+ return String(val)
+}
+
+export function formatMoney(val) {
+ if (val == null || val === '') return '—'
+ const n = Number(val)
+ if (Number.isNaN(n)) return String(val)
+ return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
+}
+
+export function formatQty(val, unit = '') {
+ if (val == null || val === '') return '—'
+ const n = Number(val)
+ const s = Number.isNaN(n) ? String(val) : String(n)
+ return unit ? `${s} ${unit}` : s
+}
+
+export function planTypeLabel(planType) {
+ const map = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
+ return map[planType] || planType || '—'
+}
+
+export function followStatusLabel(status) {
+ const map = {
+ draft: { text: '草稿', tag: 'muted' },
+ processing: { text: '进行中', tag: 'warning' },
+ completed: { text: '已完成', tag: 'success' },
+ cancelled: { text: '已作废', tag: 'danger' }
+ }
+ return map[status] || { text: status || '—', tag: 'muted' }
+}
+
+export function transportStatusLabel(status) {
+ const map = {
+ pending: { text: '待安排', tag: 'warning' },
+ arranged: { text: '已安排', tag: 'info' }
+ }
+ return map[status] || { text: status || '—', tag: 'muted' }
+}
+
+export function archiveStatusLabel(status) {
+ const map = {
+ unarchived: { text: '未归档', tag: 'info' },
+ archived: { text: '已归档', tag: 'success' }
+ }
+ return map[status] || { text: status || '—', tag: 'muted' }
+}
+
+/**
+ * Demo 列表 Tab 语义:待发运 / 在途 / 已完成
+ * - 待发运:transportStatus=pending 且未作废
+ * - 在途:已安排且跟进未完成
+ * - 已完成:followStatus=completed
+ */
+export function planListPhase(row) {
+ if (!row) return { text: '—', tag: 'muted', key: 'unknown' }
+ if (row.followStatus === 'cancelled') return { text: '已作废', tag: 'danger', key: 'cancelled' }
+ if (row.followStatus === 'completed') return { text: '已完成', tag: 'success', key: 'done' }
+ if (row.transportStatus === 'pending') return { text: '待发运', tag: 'warning', key: 'pending' }
+ if (row.transportStatus === 'arranged') return { text: '在途', tag: 'info', key: 'transit' }
+ return followStatusLabel(row.followStatus)
+}
+
+export function settlementStatusLabel(status) {
+ const map = {
+ pending: { text: '草稿', tag: 'muted' },
+ rejected: { text: '已驳回', tag: 'danger' },
+ approving: { text: '审批中', tag: 'warning' },
+ approved: { text: '待发放', tag: 'info' },
+ confirmed: { text: '已发放', tag: 'success' },
+ cancelled: { text: '已作废', tag: 'danger' }
+ }
+ return map[status] || { text: status || '—', tag: 'muted' }
+}
+
+export function isEditableDraftSettlement(status) {
+ return status === 'pending' || status === 'rejected'
+}
+
+/** 计划卡片副标题:品名 · 数量 · 路线 */
+export function planCardSub(row) {
+ if (!row) return '—'
+ const products = Array.isArray(row.products) ? row.products : []
+ const names = products
+ .map((p) => p.productName || p.product_name)
+ .filter(Boolean)
+ .slice(0, 2)
+ const namePart = names.length ? names.join('、') : planTypeLabel(row.planType)
+ const qty = products.reduce((sum, p) => sum + (Number(p.quantity) || Number(p.actualQty) || 0), 0)
+ const unit = products[0]?.unit || ''
+ const qtyPart = qty > 0 ? formatQty(qty, unit) : ''
+ const from = row.pickupAddress || row.supplierName || ''
+ const to = row.destinationAddress || row.customerName || row.destinationName || ''
+ const route = from && to ? `${shortAddr(from)} → ${shortAddr(to)}` : dash(row.customerName || row.destinationName)
+ return [namePart, qtyPart, route].filter(Boolean).join(' · ')
+}
+
+function shortAddr(s) {
+ const t = String(s || '')
+ return t.length > 10 ? `${t.slice(0, 10)}…` : t
+}
+
+export function certReadyLabel(ready) {
+ return ready ? { text: '证书齐备', tag: 'success' } : { text: '待齐备', tag: 'warning' }
+}
+
+export function hasPurchaseCert(product) {
+ const url = product?.purchaseCertUrl || product?.purchase_cert_url
+ return !!(url && String(url).trim())
+}
diff --git a/miniprogram/utils/upload.js b/miniprogram/utils/upload.js
new file mode 100644
index 000000000..2ec2c58ec
--- /dev/null
+++ b/miniprogram/utils/upload.js
@@ -0,0 +1,102 @@
+/**
+ * 小程序拍照凭证上传(OpenSpec 基线)
+ * chooseImage → uni.uploadFile → 收集 URL → 业务接口再传 imageUrls
+ */
+import { API_BASE, TOKEN_KEY, UPLOAD_API } from '@/config/api'
+
+function parseUploadBody(raw) {
+ try {
+ return JSON.parse(typeof raw === 'string' ? raw : String(raw || ''))
+ } catch {
+ return {}
+ }
+}
+
+function extractUrl(body) {
+ const data = body && body.data
+ if (!data || typeof data !== 'object') return ''
+ const url = data.url || data.fileUrl || data.file_url
+ return url != null ? String(url).trim() : ''
+}
+
+function getToken() {
+ const raw = uni.getStorageSync(TOKEN_KEY)
+ return typeof raw === 'string' ? raw.trim() : raw ? String(raw).trim() : ''
+}
+
+/**
+ * @param {{ folder?: string, maxCount?: number, currentUrls?: string[], uploadUrl?: string }} opts
+ * @returns {Promise} 本次新上传成功的 URL 列表
+ */
+export async function chooseAndUploadImages(opts = {}) {
+ const folder = opts.folder || 'mini-evidence'
+ const maxCount = opts.maxCount != null ? opts.maxCount : 6
+ const currentUrls = Array.isArray(opts.currentUrls) ? opts.currentUrls : []
+ const uploadUrl = opts.uploadUrl || `${API_BASE}${UPLOAD_API.IMAGE}`
+ const remain = maxCount - currentUrls.length
+
+ if (remain <= 0) {
+ uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
+ return []
+ }
+
+ const choose = await new Promise((resolve) => {
+ uni.chooseImage({
+ count: remain,
+ sizeType: ['compressed'],
+ sourceType: ['album', 'camera'],
+ success: (res) => resolve(res),
+ fail: () => resolve(null)
+ })
+ })
+ if (!choose?.tempFilePaths?.length) return []
+
+ const token = getToken()
+ const urls = []
+ uni.showLoading({ title: '上传中...' })
+ try {
+ for (const filePath of choose.tempFilePaths) {
+ const uploadRes = await new Promise((ok, fail) => {
+ uni.uploadFile({
+ url: uploadUrl,
+ filePath,
+ name: 'file',
+ header: {
+ Authorization: token ? `Bearer ${token}` : ''
+ },
+ formData: { folder },
+ success: ok,
+ fail
+ })
+ })
+ if (uploadRes.statusCode === 401) {
+ uni.showToast({ title: '登录已失效,请重新登录', icon: 'none' })
+ break
+ }
+ const body = parseUploadBody(uploadRes.data)
+ const url = extractUrl(body)
+ if (body.code !== 200 || !url) {
+ uni.showToast({ title: body.message || '上传失败', icon: 'none' })
+ break
+ }
+ urls.push(url)
+ }
+ } catch (e) {
+ uni.showToast({
+ title: e instanceof Error ? e.message : '上传失败',
+ icon: 'none'
+ })
+ } finally {
+ uni.hideLoading()
+ }
+ return urls
+}
+
+export function previewImages(urls, current) {
+ const list = (urls || []).map((u) => String(u || '').trim()).filter(Boolean)
+ if (!list.length) return
+ uni.previewImage({
+ urls: list,
+ current: current || list[0]
+ })
+}
diff --git a/openspec/changes/zkxw-engineering-standards/proposal.md b/openspec/changes/zkxw-engineering-standards/proposal.md
index 25f561711..44aaf5ae0 100644
--- a/openspec/changes/zkxw-engineering-standards/proposal.md
+++ b/openspec/changes/zkxw-engineering-standards/proposal.md
@@ -7,6 +7,7 @@
- 新增 OpenSpec 变更 `zkxw-engineering-standards`,包含提案、设计说明、**八项能力规范**及落地任务清单。
- **不修改**业务代码;规范正文以需求 V0.3 第 3 节「技术要求」为硬约束,UI 细节以两份 UI 规范文档为权威展开(本变更中的 UI 条目为**跨端统一原则与引用约束**,像素级 Token 仍以原文档为准)。
- 工作流/审批与权限条目直接对齐需求文档 **§2.1.11 流程管理** 与 **§2.1.5 角色管理** 等章节。
+- **增补(2026-07)**:`ui-conventions` / `api-conventions` / `architecture-conventions` 明确小程序附件基线为**仅拍照凭证**(`uni.chooseImage` + `uni.uploadFile` + 业务提交 `imageUrls`),与 `web-admin` 通用附件卡片化布局分工;参考叁强 `sqxp-wx` 凭证上传。
## Capabilities
diff --git a/openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md b/openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md
index 71e6ba700..77558298b 100644
--- a/openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md
+++ b/openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md
@@ -382,6 +382,44 @@ FastAPI 路由 Query 参数若 Python 参数名为 `page_size`,SHALL 声明 `Q
- **WHEN** 在 `app/api/` 增加新的路由模块
- **THEN** 该模块 SHALL 在 `app/api/__init__.py` 与 `app/main.py` 中完成注册,并符合现有认证与租户上下文注入模式
+### Requirement: 小程序拍照凭证上传接口
+
+`miniprogram` 拍照凭证(仅图片,见 `ui-conventions`「小程序拍照凭证上传(基线)」)相关接口 SHALL 遵循下列约定;权威行为对齐叁强鲜配 `POST /api/upload/image` + 业务接口携带 `imageUrls`。
+
+#### 上传接口
+
+1. **方法与编码**:`POST` + `multipart/form-data`(文件上传例外,见上文「文件上传下载例外」)。
+2. **表单字段**:文件字段名 SHALL 为 `file`;业务目录字段 SHALL 为 `folder`(`Form` 字符串,如 `delivery-evidence`、`avatars`)。
+3. **鉴权**:SHALL 要求登录态(Bearer JWT 或与小程序一致的现有鉴权);未登录 SHALL 返回 401。
+4. **成功响应**:统一信封下 `data` SHALL 至少包含 `url`(对象存储可访问地址);MAY 含 `filename`、`contentType`、`size`。前端以 `data.url` 为唯一回填依据。
+5. **类型与大小**:SHALL 仅接受图片 MIME(至少覆盖 `image/jpeg`、`image/png`、`image/webp`;建议兼容 `image/heic`/`image/heif`);单文件大小上限建议 **10MB**(与叁强一致,业务另有约定时在接口文档写明)。
+6. **路径落点**:山峡落地时 MAY 复用/对齐 `POST /api/upload/image`,或提供 `POST /api/mini/.../upload/image`;无论路径如何,小程序 `uni.uploadFile` 的 `url` 与后端注册路径 SHALL 一致,并完成路由注册(`app/api/__init__.py`、`app/main.py`)。
+
+#### 加解密排除(强制)
+
+`multipart` 上传路径 SHALL 列入服务端 `API_ENCRYPT_EXCLUDE_PATHS`(及前端若启用 API 加密时的排除清单)。微信小程序端若因运行时不保证 WebCrypto 而关闭客户端加密(叁强 `sqxp-wx/utils/request.js` 在 `MP-WEIXIN` 下 `API_ENCRYPT_ENABLED = false`),上传仍 SHALL 依赖 HTTPS + Bearer,SHALL NOT 对文件字节做 JSON AES 包装。
+
+#### 业务关联
+
+1. 业务确认类接口(如取货、送达、备货完成)请求体 SHALL 使用 JSON,图片以 **URL 数组** 传递(建议字段名 `imageUrls: string[]`)。
+2. 服务端收到 URL 列表后 SHALL 校验归属与数量上限,再更新业务状态;需要附件台账时 SHALL 写入附件表(或等价持久化),`file_url` 存完整 URL;多 URL 落单字段时 SHALL 使用英文逗号分隔(见 `architecture-conventions`「附件与对象存储」)。
+3. 业务接口 SHALL NOT 再接收同一确认动作中的 multipart 文件流。
+
+#### Scenario: 小程序上传一张凭证图
+
+- **WHEN** 已登录小程序用户对图片上传接口提交 `file` + `folder`
+- **THEN** 响应 `code` SHALL 为成功码且 `data.url` 非空,对象存储中可按该 URL 访问图片
+
+#### Scenario: 业务提交携带 imageUrls
+
+- **WHEN** 用户确认取货/送达等动作且本地已有已上传 URL
+- **THEN** 业务 POST 体 SHALL 含 `imageUrls` 数组,服务端 SHALL 据此落库/关联,SHALL NOT 要求客户端再次上传二进制
+
+#### Scenario: 上传路径未排除加密
+
+- **WHEN** 上传路径未加入 `API_ENCRYPT_EXCLUDE_PATHS` 且全局 API 加密开启
+- **THEN** 视为规范违规;实现与评审 SHALL 在合入前补齐排除项
+
### Requirement: 细胞存储样本跨表关联键
细胞存储场景下,将协议样本行(`biz_protocol_sample`)与下游业务(样本接收 `biz_sample_reception`、质检工单 `biz_qc_work_order` 等)进行**业务匹配**时,关联键 SHALL 仅为 `tenant_no` + `sample_code`(租户内样本编码唯一且不变;接收/工单表须持久化 `sample_code` 快照)。**不得**使用 `collection_protocol_id` 或 `protocol_sample_id` 作为查询、列表排除、详情回显的关联依据(协议单可换号/转科研、编辑保存会重建样本行 `id`,但样本编码不变)。
diff --git a/openspec/changes/zkxw-engineering-standards/specs/architecture-conventions/spec.md b/openspec/changes/zkxw-engineering-standards/specs/architecture-conventions/spec.md
index c34687650..cc7775c5b 100644
--- a/openspec/changes/zkxw-engineering-standards/specs/architecture-conventions/spec.md
+++ b/openspec/changes/zkxw-engineering-standards/specs/architecture-conventions/spec.md
@@ -22,11 +22,21 @@
业务附件 SHALL 通过对象存储(如 MinIO)保存可访问 URL;多附件在同一字段内存储时 SHALL 使用英文逗号分隔多个 URL(与需求 §3 一致)。
+端侧分工:
+
+- **`web-admin`**:通用附件(图片/视频/办公文档等)按 `ui-conventions`「附件上传卡片化布局」上传与展示。
+- **`miniprogram`(当前基线)**:仅拍照/相册**图片凭证**,按 `ui-conventions`「小程序拍照凭证上传(基线)」与 `api-conventions`「小程序拍照凭证上传接口」;文档类附件不在小程序基线范围内。
+
#### Scenario: 上传列表回显
- **WHEN** 业务实体包含多个附件
- **THEN** 持久化字段 SHALL 为逗号分隔的 URL 字符串,且接口契约与前端解析逻辑一致
+#### Scenario: 小程序与 Web 附件能力边界
+
+- **WHEN** 同一业务在 Web 与小程序均需留存材料
+- **THEN** 小程序侧 SHALL 仅实现拍照凭证 URL 链路;若业务必须上传 PDF 等文档,SHALL 引导在 Web 完成或先扩展 OpenSpec 后再实现
+
### Requirement: 权威文档索引
实现类变更 SHALL 将 `工程规范`、`docs/web-admin-ui-spec-fresh-mint.md`、`docs/miniprogram-ui-spec-sanqiang.md` 及 `AGENTS.md` 视为架构与规范的索引入口;多系统 SQL 与 CRM/地磅边界 SHALL 以 `ecosystem-landscape` spec 为准;OpenSpec 变更 SHALL 在 `proposal.md` / `design.md` 中保持对上述路径的引用。
diff --git a/openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md b/openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md
index d124193b0..f3ca5db33 100644
--- a/openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md
+++ b/openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md
@@ -923,6 +923,129 @@ watchDebounced(
- **WHEN** PR 新增或修改附件上传 UI
- **THEN** 审查者 SHALL 对照 `ProtocolFormDialog.vue` 确认卡片化布局与立即上传逻辑
- **THEN** SHALL 拒绝 `picture-card`、`show-file-list` 默认列表、与规范不一致的尺寸/色值
+
+---
+
+### Requirement: 小程序拍照凭证上传(基线)
+
+`miniprogram` 业务附件在当前产品范围内 **仅支持拍照/相册图片凭证**(如取货、送达、备货、现场确认等),**SHALL NOT** 实现 PDF/Word/Excel 等文档类附件上传(文档类仍以 `web-admin`「附件上传卡片化布局」为准)。实现与交互以叁强鲜配 `sqxp-wx` 配送/供应商凭证上传为权威参考(如 `pages/deliverer/task/detail/detail.vue`、`pages/supplier/delivery/detail/detail.vue`、`pages/supplier/settings/avatar.vue`);山峡侧后续落地时 SHALL 抽公共工具(建议 `miniprogram/utils/upload.js`),业务页只负责数量上限、`folder` 与提交字段。
+
+#### 范围与禁止项
+
+1. **允许来源**:`uni.chooseImage`(或等价的 `uni.chooseMedia` 且仅 `mediaType: ['image']`),`sourceType` 含 `album` 与 `camera`,`sizeType` 优先 `compressed`。
+2. **禁止**:SHALL NOT 使用 `wx.chooseMessageFile` / `uni.chooseFile` 选聊天文件或任意文档;SHALL NOT 复用 Web 端 `el-upload`、`FormData` + `uni.request`/`fetch` 上传二进制;SHALL NOT 把图片转 Base64 塞进 JSON 业务接口(除非产品书面要求且接口单独评审)。
+3. **色板与触控**:上传入口、删除热区 SHALL 遵守小程序 Token(品牌绿 `#0a9f6d` 系)与最小触控 44×44;SHALL NOT 使用 Web Fresh Mint `#10b981` 作为小程序主色。
+
+#### 两步流程(强制)
+
+1. **选图并立即上传**:用户选择临时路径后,逐张(或受控并发)调用 `uni.uploadFile` 上传到图片上传接口,成功后将返回的 **URL 字符串** 推入本地数组(如 `imageUrls`)。
+2. **业务提交**:用户确认业务动作(取货/送达/保存等)时,再调用业务 API,请求体携带已上传的 URL 列表(字段名建议 `imageUrls`,camelCase);服务端再关联业务并(如需要)写入附件表。SHALL NOT 在业务提交接口中直接接收 multipart 文件流(与叁强「先 URL、后业务」一致)。
+
+#### `uni.uploadFile` 约定
+
+| 项 | 要求 |
+|----|------|
+| 调用方式 | `uni.uploadFile`(微信小程序原生 multipart) |
+| `url` | 图片上传接口绝对或与 `API_BASE` 拼接后的完整 URL(见 `api-conventions`「小程序拍照凭证上传接口」) |
+| `filePath` | `chooseImage` / `chooseMedia` 返回的临时路径 |
+| `name` | 固定为 `file`(与后端 `UploadFile` 字段名一致) |
+| `header.Authorization` | `Bearer ${token}`;token 来自本地存储(与登录态同一 key) |
+| `formData.folder` | 业务目录字符串(如 `delivery-evidence`、`avatars`、山峡业务自定义 kebab/短路径) |
+| 响应解析 | `uploadFile` 的 `data` 多为 **字符串**,SHALL `JSON.parse` 后再读 `code` / `data.url`;成功条件与统一响应一致(`code === 200` 且存在 `data.url`) |
+| Loading | 上传过程 SHALL `uni.showLoading`,结束 `uni.hideLoading`;失败 SHALL `uni.showToast` 提示 |
+
+上传逻辑 **SHALL NOT** 走会做 AES/包体加密的通用 `request()` 封装(multipart 无法套 JSON 加密封装);鉴权仅靠 Header Bearer。
+
+#### UI 与数量
+
+1. **展示**:已上传项以缩略图网格/横向列表展示;点击预览 SHALL 使用 `uni.previewImage`(`urls` 为当前列表,`current` 为点击项)。
+2. **删除**:未提交业务前可从本地数组移除;已随业务提交锁定的凭证按业务只读展示,SHALL NOT 在只读态提供删除。
+3. **数量上限**:单次业务凭证默认最多 **6** 张(与叁强配送凭证一致);若业务需要其他上限,SHALL 在页面文案与选图 `count` 中写死同一常量。选图前 `count = 上限 - 已有张数`,已满则 toast「最多上传 N 张」。
+4. **上传入口文案**:建议「+ 上传」或「选择图片并上传」;入口热区 ≥ 44×44。
+
+#### 参考实现骨架(工具函数形态)
+
+```javascript
+// miniprogram/utils/upload.js(后续落地时抽取;业务页传入 folder / maxCount)
+export async function chooseAndUploadImages({ folder, maxCount, currentUrls, token, uploadUrl }) {
+ const remain = maxCount - currentUrls.length
+ if (remain <= 0) {
+ uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
+ return []
+ }
+ const choose = await new Promise((resolve) => {
+ uni.chooseImage({
+ count: remain,
+ sizeType: ['compressed'],
+ sourceType: ['album', 'camera'],
+ success: (res) => resolve(res),
+ fail: () => resolve(null)
+ })
+ })
+ if (!choose?.tempFilePaths?.length) return []
+
+ const urls = []
+ uni.showLoading({ title: '上传中...' })
+ try {
+ for (const filePath of choose.tempFilePaths) {
+ const uploadRes = await new Promise((ok, fail) => {
+ uni.uploadFile({
+ url: uploadUrl,
+ filePath,
+ name: 'file',
+ header: { Authorization: token ? `Bearer ${token}` : '' },
+ formData: { folder },
+ success: ok,
+ fail
+ })
+ })
+ const raw = typeof uploadRes.data === 'string' ? uploadRes.data : String(uploadRes.data || '')
+ let body = {}
+ try {
+ body = JSON.parse(raw)
+ } catch {
+ body = {}
+ }
+ if (body.code !== 200 || !body.data?.url) {
+ uni.showToast({ title: body.message || '上传失败', icon: 'none' })
+ break
+ }
+ urls.push(String(body.data.url))
+ }
+ } finally {
+ uni.hideLoading()
+ }
+ return urls
+}
+```
+
+业务页在确认动作时示例:
+
+```javascript
+await request({
+ url: `/mini//tasks/${taskId}/pickup`,
+ method: 'POST',
+ data: { remark: '已取货', imageUrls: pickupImages.value }
+})
+```
+
+#### Scenario: 配送/现场凭证上传
+
+- **WHEN** 用户在小程序业务页点击「上传」且未达上限
+- **THEN** SHALL 调起相册或拍照选图,并对每张临时文件调用 `uni.uploadFile`
+- **THEN** 成功后本地列表增加可预览的图片 URL,业务提交时携带 `imageUrls`
+
+#### Scenario: 禁止文档类附件
+
+- **WHEN** 需求表述为「附件」但场景为小程序端
+- **THEN** 实现 SHALL 仅提供拍照凭证能力;若确需 PDF 等文档,SHALL 先走独立 OpenSpec 变更扩展本 Requirement,不得静默加入 `chooseMessageFile`
+
+#### Scenario: Code review 小程序凭证上传
+
+- **WHEN** PR 新增或修改 `miniprogram` 图片/凭证上传
+- **THEN** 审查者 SHALL 确认使用 `uni.chooseImage`(或仅图片的 `chooseMedia`)+ `uni.uploadFile`,且业务提交为 URL 列表
+- **THEN** SHALL 拒绝 `FormData`+`request`、Base64 塞业务 JSON、以及未排除加解密的上传路径误用
+
### Requirement: 按钮须携带图标
`web-admin` 中所有带文字标签的 `el-button`(或 `ElButton`)SHALL 在文字**前方**包含 `` 子元素,使用 `@element-plus/icons-vue` 提供的图标组件;SHALL NOT 出现仅有文字而无图标的按钮。该规则适用于列表工具栏、表格操作列、`el-dialog` / 抽屉的 `#footer` 底栏、表单内 `link` / `plain` / `size="small"` 等**凡渲染可见文案的按钮**,不限于 `type="primary"`。图标的引入方式为在文件 `