ud: replace a4-index with a5
This commit is contained in:
parent
99c188a26b
commit
d43072372e
|
|
@ -1,4 +1,2 @@
|
|||
TARO_APP_API_ORIGIN=https://corp-test.batiao8.com
|
||||
TARO_APP_API_A5=https://ag-test.batiao8.com
|
||||
TARO_APP_API_HOST=nb.batiao8.com
|
||||
TARO_APP_TIANDITU_KEY=ad8b251afd667ea8b8bc6f33df2ce198
|
||||
|
|
|
|||
|
|
@ -1,4 +1,2 @@
|
|||
TARO_APP_API_ORIGIN=https://corp.batiao8.com
|
||||
TARO_APP_API_A5=https://ag.batiao8.com
|
||||
TARO_APP_API_HOST=nb.zuom8.cn
|
||||
TARO_APP_TIANDITU_KEY=ad8b251afd667ea8b8bc6f33df2ce198
|
||||
|
|
|
|||
29
AGENTS.md
29
AGENTS.md
|
|
@ -19,24 +19,27 @@ Other targets follow `pnpm dev:<platform>` / `pnpm build:<platform>` (`alipay`,
|
|||
|
||||
**Running the mini-program:** builds output to `./dist`. Open `./dist` in WeChat DevTools (`project.config.json` already points `miniprogramRoot` there; appId `wxf8a8961f30396c19`).
|
||||
|
||||
**There is no test runner and no lint script in `package.json`.** ESLint (`taro/react`) and Stylelint are configured but run manually. The only enforced gate is a Husky `commit-msg` hook running **commitlint** (Conventional Commits) — non-conforming commit messages are rejected.
|
||||
**Tests:** `pnpm test` runs Vitest. There is no lint script; ESLint (`taro/react`) and Stylelint run manually. The Husky `commit-msg` hook runs **commitlint** (Conventional Commits).
|
||||
|
||||
## Architecture
|
||||
|
||||
Native mini-program for map-label progress query. Pages in `src/app.config.ts`:
|
||||
Native A5 mini-program for map-label progress query. Pages in `src/app.config.ts`:
|
||||
|
||||
**`pages/index/index` — 进度查询.** Accepts H5 jump query `token` / `host` / `package` / `scene` (see `corp-h5` `weixinMiniProgram.ts`). Queries `GET /api/h5/order/phone`, resubmits with `PUT /api/h5/order`, uploads via `GET /api/presign` + Qiniu.
|
||||
- `pages/index/index`: A5 progress query, order details and customer service. Accepts `phone`, `link_id` / `linkId`, or `/h/<id>` in `scene`.
|
||||
- `pages/resubmit/index`: A5 business information and document uploads.
|
||||
- `pages/complaint/index`: A5 order complaints.
|
||||
- `pages/map/index`: native map selection returning GCJ-02 `lng,lat`.
|
||||
- `pages/agreement/index`: service and privacy agreements.
|
||||
|
||||
**`pages/map/index` — 微信原生地图选点.** Tap / locate / `chooseLocation` search, returns GCJ-02 `lng,lat`.
|
||||
### Request and configuration
|
||||
|
||||
### Session and request
|
||||
|
||||
- API origin comes from `TARO_APP_API_ORIGIN`: `.env.development` → `http://corp-test.batiao8.com`, `.env.production` → `https://nb.zuom8.cn`.
|
||||
- `x-host` prefers the H5 jump `host`, then `TARO_APP_API_HOST`.
|
||||
- Map reverse geocode uses `TARO_APP_TIANDITU_KEY` against `api.tianditu.gov.cn` (add this request domain in WeChat admin).
|
||||
- Missing launch query fields fall back to `DEFAULT_LAUNCH_SCHEME` in `src/lib/launch.ts` (`scene=/h/KZ9Q`, `host=nb.batiao8.com`, `package=10044`).
|
||||
- No token: `GET /api/user/config` then keep the returned guest token. Then `GET /api/h5/corp` for package/config.
|
||||
- Shared client: `src/lib/request.ts` (same MD5 sign + AES decrypt as H5). Platform header is `wx-mp`.
|
||||
- A5 client and draft storage: `src/lib/a5/api.ts`; data adapters: `src/lib/a5/data.ts`.
|
||||
- `TARO_APP_API_A5`: development `https://ag-test.batiao8.com`, production `https://ag.batiao8.com`.
|
||||
- `TARO_APP_TIANDITU_KEY`: reverse geocoding against `api.tianditu.gov.cn`.
|
||||
- Orders use `GET/PUT /h5/order`, configuration uses `GET /h5/link`, files use `/h5/file`.
|
||||
- No corp token bootstrap, x-host, default launch scheme, signing/decryption or presigned Qiniu upload.
|
||||
- Only A5 business behavior is supported. The former `pages/a5/*` routes are removed; external entries must use `pages/index/index`.
|
||||
- See `docs/a5-integration.md` for API details, legal domains and the existing complaint HTTP 404 fallback.
|
||||
|
||||
Do not reintroduce a business H5 WebView unless explicitly requested.
|
||||
|
||||
|
|
@ -48,5 +51,3 @@ Do not reintroduce a business H5 WebView unless explicitly requested.
|
|||
- **Styling:** SCSS with BEM (`block__element--modifier`); CSS Modules are disabled. Design width is `750`, so author dimensions in `px` — Taro converts to `rpx`/`rem` at compile time.
|
||||
- **Imports:** `@/*` is aliased to `src/*` (see `tsconfig.json`).
|
||||
- `strictNullChecks`, `noUnusedLocals`, and `noUnusedParameters` are on; `noImplicitAny` is off.
|
||||
|
||||
A more verbose Chinese-language version of these notes lives in `GEMINI.md`.
|
||||
|
|
|
|||
182
DESIGN.md
182
DESIGN.md
|
|
@ -1,182 +0,0 @@
|
|||
---
|
||||
name: 门店管理服务
|
||||
description: 面向线下门店经营负责人的蓝色科技感业务落地页
|
||||
colors:
|
||||
midnight-navy: "#05256E"
|
||||
service-blue: "#0B57D0"
|
||||
action-blue: "#1677FF"
|
||||
signal-cyan: "#5FC8FF"
|
||||
canvas-blue: "#F3F7FF"
|
||||
pure-white: "#FFFFFF"
|
||||
trust-ink: "#102A56"
|
||||
supporting-ink: "#425F8C"
|
||||
soft-border: "#D7E4FF"
|
||||
typography:
|
||||
display:
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
|
||||
fontSize: "56px"
|
||||
fontWeight: 700
|
||||
lineHeight: 1.18
|
||||
letterSpacing: "-1px"
|
||||
title:
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
|
||||
fontSize: "32px"
|
||||
fontWeight: 700
|
||||
lineHeight: 1.4
|
||||
body:
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
|
||||
fontSize: "28px"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.65
|
||||
label:
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei, sans-serif"
|
||||
fontSize: "24px"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.4
|
||||
rounded:
|
||||
sm: "12px"
|
||||
md: "24px"
|
||||
lg: "36px"
|
||||
pill: "999px"
|
||||
spacing:
|
||||
xs: "8px"
|
||||
sm: "16px"
|
||||
md: "24px"
|
||||
lg: "32px"
|
||||
xl: "48px"
|
||||
section: "72px"
|
||||
components:
|
||||
call-button:
|
||||
backgroundColor: "{colors.action-blue}"
|
||||
textColor: "{colors.pure-white}"
|
||||
rounded: "{rounded.pill}"
|
||||
height: "88px"
|
||||
padding: "0 36px"
|
||||
industry-mark:
|
||||
backgroundColor: "{colors.canvas-blue}"
|
||||
textColor: "{colors.service-blue}"
|
||||
rounded: "{rounded.md}"
|
||||
size: "112px"
|
||||
service-item:
|
||||
backgroundColor: "{colors.pure-white}"
|
||||
textColor: "{colors.trust-ink}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "28px"
|
||||
---
|
||||
|
||||
# Design System: 门店管理服务
|
||||
|
||||
## Overview
|
||||
|
||||
**Creative North Star: "清晨开店前的蓝色灯箱"**
|
||||
|
||||
页面像门店营业前刚亮起的蓝色灯箱:清晰、稳定、有秩序。深蓝主视觉承担品牌识别,纯白内容面保证业务说明快速可读,青蓝只用于光线和细节提示。
|
||||
|
||||
整个系统拒绝地图工具、表单后台和政务门户的视觉语言,也拒绝用大量发光、玻璃面板或彩虹图标堆叠科技感。布局应让经营负责人在短时间内认出自己的行业、理解服务内容并完成电话咨询。
|
||||
|
||||
**Key Characteristics:**
|
||||
|
||||
- 深蓝主视觉与浅色信息面的明确分区,不用面板覆盖主视觉。
|
||||
- 门店剪影完整呈现,真实业务文案始终由原生组件渲染。
|
||||
- 行业标识收纳在统一横向信息带中,服务说明舒展,唯一主操作固定在底部。
|
||||
- 轻量入场动效有静态降级,不阻塞内容显示。
|
||||
|
||||
## Colors
|
||||
|
||||
以深蓝建立信任,以亮蓝承载行动,以青蓝提供有限的光感;内容区保持接近纯白,正文使用带蓝相的深色文字。
|
||||
|
||||
### Primary
|
||||
|
||||
- **午夜门楣蓝:** 用于原生导航栏、首屏深色背景与最深层光影。
|
||||
- **服务蓝:** 用于主视觉过渡、行业标识和服务序号。
|
||||
- **行动蓝:** 仅用于拨号按钮和关键交互状态。
|
||||
|
||||
### Secondary
|
||||
|
||||
- **信号青:** 只用于插画轮廓、光带和小面积高光,不承载正文。
|
||||
|
||||
### Neutral
|
||||
|
||||
- **清晨画布:** 页面内容背景,带极轻蓝相。
|
||||
- **纯白:** 服务内容与固定电话栏的主要表面。
|
||||
- **信任墨蓝:** 标题和正文主色。
|
||||
- **辅助墨蓝:** 次要说明文字,仍需满足 AA 对比度。
|
||||
- **柔和分隔蓝:** 仅用于完整边框和分隔线。
|
||||
|
||||
**The Blue Commitment Rule.** 蓝色覆盖首屏主视觉,但内容区必须回到高对比浅色表面;禁止用第三种高饱和色争夺注意力。
|
||||
|
||||
## Typography
|
||||
|
||||
**Display Font:** 系统无衬线字体(优先 PingFang SC)
|
||||
**Body Font:** 系统无衬线字体(优先 PingFang SC)
|
||||
|
||||
**Character:** 单一系统字体通过字号和字重建立层级,加载稳定,适合微信小程序和中文快速扫读。
|
||||
|
||||
### Hierarchy
|
||||
|
||||
- **Display**(700,56px,1.18):仅用于首屏“门店管理服务”。
|
||||
- **Title**(700,32px,1.4):用于“服务行业”“核心服务”和服务名称。
|
||||
- **Body**(400,28px,1.65):用于业务说明,单行长度受移动端容器约束。
|
||||
- **Label**(600,24px,1.4):用于行业名称、电话提示和辅助标签。
|
||||
|
||||
**The Plain Language Rule.** 标题直接命名业务,按钮使用“立即拨打”等动词加对象表达;禁止口号式套话和全大写正文。
|
||||
|
||||
## Elevation
|
||||
|
||||
系统以色块、完整边框和有限的环境阴影建立层次。内容表面默认平坦,固定电话栏可使用向上的柔和阴影与页面分离,主视觉光晕只存在于蓝色背景内部。
|
||||
|
||||
### Shadow Vocabulary
|
||||
|
||||
- **内容环境影:** 低透明度、大模糊半径,只用于主内容容器与蓝色主视觉交界处。
|
||||
- **固定栏上浮影:** 从底部栏向上扩散,说明其固定层级,不模拟厚重卡片。
|
||||
|
||||
**The Flat Content Rule.** 服务信息依靠留白与分隔,不给每个条目添加独立厚重阴影。
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
|
||||
- **Shape:** 44px 视觉高度的胶囊按钮(88px 设计稿高度)。
|
||||
- **Primary:** 行动蓝底、纯白粗体文字,左右内边距 36px。
|
||||
- **Active / Focus:** 按下时轻微降低亮度;焦点通过完整轮廓表达,不移动布局。
|
||||
|
||||
### Chips
|
||||
|
||||
- **Style:** 三个行业标识共用一个横向信息带,以 1px 分隔线划分;每项由浅蓝圆形字符标识和行业名称组成。
|
||||
- **State:** 全部为静态信息,不提供选中、悬停或点击状态。
|
||||
|
||||
### Cards / Containers
|
||||
|
||||
- **Corner Style:** 主内容容器 36px,服务条目 24px。
|
||||
- **Background:** 纯白或清晨画布。
|
||||
- **Shadow Strategy:** 主容器允许环境影,服务条目使用完整边框或分隔线。
|
||||
- **Internal Padding:** 24px 至 40px,保持移动端舒适密度。
|
||||
|
||||
### Navigation
|
||||
|
||||
- 原生导航栏使用午夜门楣蓝背景、纯白标题“门店管理服务”,不放 Logo 或自定义返回控件。
|
||||
|
||||
### Fixed Consultation Bar
|
||||
|
||||
- 白色固定底栏包含“业务咨询”、电话号码和“立即拨打”按钮。
|
||||
- 底部使用安全区内边距,页面正文预留等高空间,任何屏幕都不得遮挡内容。
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do:
|
||||
|
||||
- **Do** 让百货、超市、便利店在首屏直接可见。
|
||||
- **Do** 使用原生文字承载业务内容,插画加载失败时信息仍完整。
|
||||
- **Do** 按图片宽度展示门店主视觉,只裁去上方空蓝区域,完整保留门店建筑与地面基线。
|
||||
- **Do** 保持正文至少 4.5:1 对比度、按钮约 44px 触控高度。
|
||||
- **Do** 为所有入场动画提供 `prefers-reduced-motion` 静态降级。
|
||||
- **Do** 使用完整边框、留白和色块建立层级。
|
||||
|
||||
### Don't:
|
||||
|
||||
- **Don't** 做地图、标注、定位、坐标或地址管理界面。
|
||||
- **Don't** 做表单页、政务门户或业务办理后台的视觉复刻。
|
||||
- **Don't** 使用 Logo、彩虹分类图标、夸张赛博霓虹或装饰性玻璃拟态。
|
||||
- **Don't** 把行业与服务包装成可点击但没有目标的假入口。
|
||||
- **Don't** 使用负 margin 内容面板覆盖门店图片,或用深色渐变压暗门店底部。
|
||||
- **Don't** 使用渐变文字、侧边粗色条、嵌套卡片或重复的图标卡片网格。
|
||||
36
PRODUCT.md
36
PRODUCT.md
|
|
@ -1,36 +0,0 @@
|
|||
# Product
|
||||
|
||||
## Register
|
||||
|
||||
brand
|
||||
|
||||
## Users
|
||||
|
||||
面向百货、超市与便利店的经营负责人和运营负责人。他们通常在移动端快速了解服务范围,并希望用最短路径联系业务人员。
|
||||
|
||||
## Product Purpose
|
||||
|
||||
落地页用于清楚介绍门店管理服务覆盖的行业与能力,并将没有 `scene` 的自然访问流量转化为电话咨询。成功标准是访问者能在首屏理解业务对象,在一次滚动内看到完整服务范围,并随时拨打咨询电话。
|
||||
|
||||
## Brand Personality
|
||||
|
||||
可靠、利落、专业。文案直接说明业务,不夸大结果,不使用营销套话;视觉有明确科技感,但不牺牲可读性和可信度。
|
||||
|
||||
## Anti-references
|
||||
|
||||
- 不做地图、标注、定位、坐标或地址管理界面。
|
||||
- 不做表单页、政务门户或业务办理后台的视觉复刻。
|
||||
- 不使用 Logo、彩虹分类图标、夸张赛博霓虹或装饰性玻璃拟态。
|
||||
- 不把行业与服务包装成可点击但没有目标的假入口。
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **先讲清服务对象。** 首屏必须直接出现百货、超市与便利店。
|
||||
- **唯一行动清晰。** 电话咨询是页面唯一主操作,并始终可见。
|
||||
- **科技感服务于信任。** 蓝色、光线和门店插画建立专业氛围,不制造地图或数据监控联想。
|
||||
- **信息先于装饰。** 即使插画未加载,标题、服务范围和联系电话仍完整可用。
|
||||
- **移动端快速扫读。** 使用短文案、强层级和足够触控尺寸。
|
||||
|
||||
## Accessibility & Inclusion
|
||||
|
||||
以移动端 WCAG AA 为基线:正文对比度至少 4.5:1,大字号至少 3:1,交互区域视觉尺寸不小于约 44px,不仅依赖颜色传达信息,并为减少动态效果提供静态降级。
|
||||
33
design-qa.md
33
design-qa.md
|
|
@ -1,33 +0,0 @@
|
|||
# Design QA
|
||||
|
||||
- source visual truth path: `C:\Users\29169\AppData\Local\Temp\codex-clipboard-5202e314-49da-48b8-a154-05e5d4a96bcc.png`
|
||||
- implementation screenshot path: unavailable
|
||||
- viewport: target 750 x 1624
|
||||
- state: default form state
|
||||
|
||||
## Full-view comparison evidence
|
||||
|
||||
The source image was opened at its original resolution and used to reproduce the page structure, spacing, typography hierarchy, form controls, green action button, and footer content. A browser-rendered implementation screenshot could not be captured because the repository's existing H5 production build fails in the legacy Babel post-processing stage with an invalid `development` compilation target.
|
||||
|
||||
## Focused region comparison evidence
|
||||
|
||||
Blocked for the same reason. Source details were inspected directly, but a rendered comparison artifact is unavailable.
|
||||
|
||||
## Findings
|
||||
|
||||
- No code-level P0/P1 issue was found by the WeChat production build or stylesheet validation.
|
||||
- Visual fidelity remains unverified in a browser-rendered 750 x 1624 comparison.
|
||||
|
||||
## Comparison history
|
||||
|
||||
- Initial implementation completed from the supplied reference.
|
||||
- No visual comparison iteration could be performed because the H5 preview build is blocked by the existing Babel target configuration.
|
||||
|
||||
## Verification
|
||||
|
||||
- Primary interactions implemented: service type selection, all text inputs, agreement selection, policy dialogs, validation feedback, and success dialog.
|
||||
- WeChat production build: passed.
|
||||
- Stylesheet validation: passed.
|
||||
- Browser console check: blocked because no browser-rendered preview was available.
|
||||
|
||||
final result: blocked
|
||||
|
|
@ -2,20 +2,21 @@
|
|||
|
||||
## Launch Contract
|
||||
|
||||
`pages/a5/index?phone=13800138000&link_id=42`
|
||||
`pages/index/index?phone=13800138000&link_id=42`
|
||||
|
||||
- `phone`: auto-query phone; manual query remains available without it.
|
||||
- `link_id`: A5 link ID for configuration, business documents and completion files.
|
||||
- `link_id`: A5 link ID for configuration, business documents and completion files. Defaults to `2x6` when neither `link_id` / `linkId` nor a valid `/h/<id>` scene supplies a link ID.
|
||||
- Legacy `/h/<id>` scenes and URL Link encoded separators are accepted.
|
||||
- No corp token, host, package or default short link is used by A5.
|
||||
- agent-h5 resolves the link from `agentLink.linkId`, `linkId`, then `/h/<id>`.
|
||||
|
||||
## Environments
|
||||
|
||||
- Development: `TARO_APP_API_A5=http://ag-test.batiao8.com`
|
||||
- Production: `TARO_APP_API_A5=http://ag.batiao8.com`
|
||||
- Development: `TARO_APP_API_A5=https://ag-test.batiao8.com`
|
||||
- Production: `TARO_APP_API_A5=https://ag.batiao8.com`
|
||||
- Map reverse geocoding: `TARO_APP_TIANDITU_KEY`.
|
||||
|
||||
HTTP is configured as requested for regular requests. File uploads always use HTTPS on the same A5 host because WeChat uploadFile requires it. Before a WeChat release, verify the HTTPS gateway and certificate and update the environment value for all requests. Configure request/upload/download legal domains, including returned file CDN domains. HTTP is not a release-ready WeChat endpoint.
|
||||
Requests and uploads use HTTPS. Configure request/upload/download legal domains, including returned file CDN domains and `api.tianditu.gov.cn`. The former corp API origin and x-host environment variables have been removed.
|
||||
|
||||
## API Contract
|
||||
|
||||
|
|
@ -42,6 +43,8 @@ WeChat uses `chooseMedia` with `mediaType: ['image']`; other targets retain `cho
|
|||
- In agent-h5: `pnpm exec vitest run src/utils/weixinMiniProgram.test.ts src/ui/map_label/utils/miniProgramEntry.test.ts`
|
||||
- In agent-h5: `pnpm build`
|
||||
|
||||
Open the mini-program project in WeChat DevTools, using `dist` as the mini-program root. Set the compile page to `pages/a5/index` and use an authorized test phone and matching A5 link ID. Check auto-query, pending-submission status, three required storefront slots, optional license/identity slots, example previews, map return, deletion, resubmission refresh, platform screenshots, PDF opening and complaints. Revisit the original index to verify it still uses the corp service.
|
||||
Open the mini-program project in WeChat DevTools, using `dist` as the mini-program root. Set the compile page to `pages/index/index` and use an authorized test phone and matching A5 link ID. Check auto-query, pending-submission status, three required storefront slots, optional license/identity slots, example previews, map return, deletion, resubmission refresh, platform screenshots, PDF opening and complaints.
|
||||
|
||||
A5 is the only business flow. Resubmission and complaints use `pages/resubmit/index` and `pages/complaint/index`; map selection and agreements remain shared pages. The former `pages/a5/*` routes have been removed, so external entry links must use `pages/index/index`.
|
||||
|
||||
No real customer orders were submitted or modified during implementation verification.
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@
|
|||
"@tarojs/runtime": "4.2.0",
|
||||
"@tarojs/shared": "4.2.0",
|
||||
"@tarojs/taro": "4.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
|
|
@ -75,9 +73,7 @@
|
|||
"@tarojs/cli": "4.2.0",
|
||||
"@tarojs/plugin-generator": "4.2.0",
|
||||
"@tarojs/vite-runner": "4.2.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/minimatch": "^5",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"babel-preset-taro": "4.2.0",
|
||||
|
|
|
|||
159
pnpm-lock.yaml
159
pnpm-lock.yaml
|
|
@ -56,12 +56,6 @@ importers:
|
|||
'@tarojs/taro':
|
||||
specifier: 4.2.0
|
||||
version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.31)(postcss@8.5.15)(rollup@3.30.0)
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
qrcode:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
react:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1
|
||||
|
|
@ -93,15 +87,9 @@ importers:
|
|||
'@tarojs/vite-runner':
|
||||
specifier: 4.2.0
|
||||
version: 4.2.0(@tarojs/runtime@4.2.0)(@types/babel__core@7.20.5)(jiti@2.6.1)(postcss@8.5.15)(rollup@3.30.0)(terser@5.48.0)(typescript@5.9.3)(vite@4.5.14(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0))
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/minimatch':
|
||||
specifier: ^5
|
||||
version: 5.1.2
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.6
|
||||
version: 1.5.6
|
||||
'@types/react':
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.31
|
||||
|
|
@ -2087,9 +2075,6 @@ packages:
|
|||
'@types/conventional-commits-parser@5.0.2':
|
||||
resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
||||
|
||||
|
|
@ -2132,9 +2117,6 @@ packages:
|
|||
'@types/prop-types@15.7.15':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
'@types/qrcode@1.5.6':
|
||||
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
|
||||
|
||||
'@types/react@18.3.31':
|
||||
resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==}
|
||||
|
||||
|
|
@ -2561,10 +2543,6 @@ packages:
|
|||
camel-case@4.1.2:
|
||||
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001797:
|
||||
resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==}
|
||||
|
||||
|
|
@ -2641,9 +2619,6 @@ packages:
|
|||
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
||||
|
|
@ -2765,10 +2740,6 @@ packages:
|
|||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
|
||||
|
||||
css-functions-list@3.3.3:
|
||||
resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -2818,10 +2789,6 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decode-uri-component@0.2.2:
|
||||
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
|
@ -2895,9 +2862,6 @@ packages:
|
|||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dijkstrajs@1.0.3:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
dingtalk-jsapi@2.15.6:
|
||||
resolution: {integrity: sha512-804mFz2AFV/H9ysmo7dLqMjSGOQgREsgQIuep+Xg+yNQeQtnUOYntElEzlB798Sj/691e4mMKz9mtQ7v9qdjuA==}
|
||||
|
||||
|
|
@ -3244,10 +3208,6 @@ packages:
|
|||
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-up@5.0.0:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -3926,10 +3886,6 @@ packages:
|
|||
resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -4237,10 +4193,6 @@ packages:
|
|||
resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -4249,10 +4201,6 @@ packages:
|
|||
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
p-locate@4.1.0:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-locate@5.0.0:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -4265,10 +4213,6 @@ packages:
|
|||
resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
package-json@6.5.0:
|
||||
resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -4379,10 +4323,6 @@ packages:
|
|||
platform@1.3.6:
|
||||
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -4522,11 +4462,6 @@ packages:
|
|||
resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
query-string@5.1.1:
|
||||
resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -4641,9 +4576,6 @@ packages:
|
|||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -4776,9 +4708,6 @@ packages:
|
|||
resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -5361,9 +5290,6 @@ packages:
|
|||
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which-typed-array@1.1.22:
|
||||
resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -5416,9 +5342,6 @@ packages:
|
|||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -5431,10 +5354,6 @@ packages:
|
|||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs-parser@20.2.9:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -5443,10 +5362,6 @@ packages:
|
|||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yargs@15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
yargs@16.2.0:
|
||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -7506,8 +7421,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 25.9.2
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
|
@ -7552,10 +7465,6 @@ snapshots:
|
|||
|
||||
'@types/prop-types@15.7.15': {}
|
||||
|
||||
'@types/qrcode@1.5.6':
|
||||
dependencies:
|
||||
'@types/node': 25.9.2
|
||||
|
||||
'@types/react@18.3.31':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.15
|
||||
|
|
@ -8114,8 +8023,6 @@ snapshots:
|
|||
pascal-case: 3.1.2
|
||||
tslib: 2.8.1
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001797: {}
|
||||
|
||||
capital-case@1.0.4:
|
||||
|
|
@ -8218,12 +8125,6 @@ snapshots:
|
|||
|
||||
cli-width@3.0.0: {}
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
cliui@7.0.4:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
|
@ -8347,8 +8248,6 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
css-functions-list@3.3.3: {}
|
||||
|
||||
css-tree@3.2.1:
|
||||
|
|
@ -8388,8 +8287,6 @@ snapshots:
|
|||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decode-uri-component@0.2.2: {}
|
||||
|
||||
decode-uri-component@0.4.1: {}
|
||||
|
|
@ -8467,8 +8364,6 @@ snapshots:
|
|||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
dijkstrajs@1.0.3: {}
|
||||
|
||||
dingtalk-jsapi@2.15.6:
|
||||
dependencies:
|
||||
promise-polyfill: 7.1.2
|
||||
|
|
@ -9007,11 +8902,6 @@ snapshots:
|
|||
|
||||
filter-obj@5.1.0: {}
|
||||
|
||||
find-up@4.1.0:
|
||||
dependencies:
|
||||
locate-path: 5.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
find-up@5.0.0:
|
||||
dependencies:
|
||||
locate-path: 6.0.0
|
||||
|
|
@ -9726,10 +9616,6 @@ snapshots:
|
|||
|
||||
loader-utils@3.3.1: {}
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
|
@ -10013,10 +9899,6 @@ snapshots:
|
|||
|
||||
p-is-promise@1.1.0: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-limit@3.1.0:
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
|
|
@ -10025,10 +9907,6 @@ snapshots:
|
|||
dependencies:
|
||||
yocto-queue: 1.2.2
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-locate@5.0.0:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
|
@ -10041,8 +9919,6 @@ snapshots:
|
|||
dependencies:
|
||||
p-finally: 1.0.0
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json@6.5.0:
|
||||
dependencies:
|
||||
got: 9.6.0
|
||||
|
|
@ -10135,8 +10011,6 @@ snapshots:
|
|||
|
||||
platform@1.3.6: {}
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-css-variables@0.19.0(postcss@8.5.15):
|
||||
|
|
@ -10258,12 +10132,6 @@ snapshots:
|
|||
dependencies:
|
||||
hookified: 2.2.0
|
||||
|
||||
qrcode@1.5.4:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
query-string@5.1.1:
|
||||
dependencies:
|
||||
decode-uri-component: 0.2.2
|
||||
|
|
@ -10396,8 +10264,6 @@ snapshots:
|
|||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
|
@ -10574,8 +10440,6 @@ snapshots:
|
|||
|
||||
seroval@1.5.4: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
|
|
@ -11225,8 +11089,6 @@ snapshots:
|
|||
is-weakmap: 2.0.2
|
||||
is-weakset: 2.0.4
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which-typed-array@1.1.22:
|
||||
dependencies:
|
||||
available-typed-arrays: 1.0.7
|
||||
|
|
@ -11287,37 +11149,16 @@ snapshots:
|
|||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml@2.9.0: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs-parser@20.2.9: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@15.4.1:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
yargs@16.2.0:
|
||||
dependencies:
|
||||
cliui: 7.0.4
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { request } from '@/lib/request'
|
||||
|
||||
export function getOrdersByPhone (phone: string) {
|
||||
return request({
|
||||
url: '/api/h5/order/phone',
|
||||
method: 'GET',
|
||||
params: { phone },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateOrderExtra (data: Record<string, any>) {
|
||||
return request({
|
||||
url: '/api/h5/order',
|
||||
method: 'PUT',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
import { request } from '@/lib/request'
|
||||
import { applyCorpInit } from '@/lib/session'
|
||||
import { buildFileUrl, getFileExtension, pickPresignResult, QINIU_UPLOAD_URL } from '@/lib/qiniu'
|
||||
|
||||
export function getUserConfig (params: Record<string, any> = {}) {
|
||||
return request({
|
||||
url: '/api/user/config',
|
||||
method: 'GET',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function initCorp (params: Record<string, any> = {}) {
|
||||
return request({
|
||||
url: '/api/h5/corp',
|
||||
method: 'GET',
|
||||
params,
|
||||
}).then((res) => {
|
||||
applyCorpInit(res)
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
export function getCompanies (keyword: string) {
|
||||
return request({
|
||||
url: '/api/h5/company',
|
||||
method: 'GET',
|
||||
params: { keyword },
|
||||
})
|
||||
}
|
||||
|
||||
export function report (data: {
|
||||
contact: string
|
||||
content: string
|
||||
images?: string[]
|
||||
type: string
|
||||
[property: string]: any
|
||||
}) {
|
||||
return request({
|
||||
url: '/api/user/feedback',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function presign (ext = '') {
|
||||
return request({
|
||||
url: '/api/presign',
|
||||
method: 'GET',
|
||||
params: {
|
||||
ext,
|
||||
scene: 'process',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function uploadImage (filePath: string, onProgress?: (percent: number) => void) {
|
||||
const ext = getFileExtension(filePath)
|
||||
onProgress?.(0)
|
||||
const presignResponse = await presign(ext)
|
||||
const { id, token, key, domain } = pickPresignResult(presignResponse)
|
||||
if (!id || !token || !key || !domain) {
|
||||
throw new Error('获取上传凭证失败')
|
||||
}
|
||||
|
||||
const uploaded = await new Promise<{ key?: string }>((resolve, reject) => {
|
||||
const task = Taro.uploadFile({
|
||||
url: QINIU_UPLOAD_URL,
|
||||
filePath,
|
||||
name: 'file',
|
||||
formData: {
|
||||
token,
|
||||
key,
|
||||
},
|
||||
success (res) {
|
||||
try {
|
||||
const body = typeof res.data === 'string' ? JSON.parse(res.data || '{}') : res.data
|
||||
resolve(body || {})
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
},
|
||||
fail (error) {
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
task.onProgressUpdate?.((event) => {
|
||||
onProgress?.(Math.min(100, Math.max(0, event.progress)))
|
||||
})
|
||||
})
|
||||
|
||||
onProgress?.(100)
|
||||
return {
|
||||
id,
|
||||
url: buildFileUrl(domain, uploaded.key || key),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
describe('A5 application entry', () => {
|
||||
it('registers the A5 home and its supporting pages at canonical paths', async () => {
|
||||
vi.stubGlobal('defineAppConfig', (config: unknown) => config)
|
||||
const { default: config } = await import('./app.config')
|
||||
|
||||
expect(config.pages).toEqual([
|
||||
'pages/index/index',
|
||||
'pages/resubmit/index',
|
||||
'pages/complaint/index',
|
||||
'pages/map/index',
|
||||
'pages/agreement/index',
|
||||
])
|
||||
config.pages?.forEach((page) => {
|
||||
expect(existsSync(resolve('src', `${page}.tsx`))).toBe(true)
|
||||
expect(existsSync(resolve('src', `${page}.config.ts`))).toBe(true)
|
||||
})
|
||||
expect(existsSync(resolve('src/pages/a5'))).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['development', 'production'])('keeps only A5 business environment variables in %s', (mode) => {
|
||||
const env = readFileSync(resolve(`.env.${mode}`), 'utf8')
|
||||
const keys = env.split(/\r?\n/).filter((line) => line.startsWith('TARO_APP_')).map((line) => line.split('=')[0])
|
||||
expect(keys.sort()).toEqual(['TARO_APP_API_A5', 'TARO_APP_TIANDITU_KEY'])
|
||||
})
|
||||
})
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/a5/index',
|
||||
'pages/a5/resubmit/index',
|
||||
'pages/a5/complaint/index',
|
||||
'pages/resubmit/index',
|
||||
'pages/complaint/index',
|
||||
'pages/map/index',
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 965 B |
|
|
@ -4,14 +4,17 @@ import { A5_STATUSES, A5_TABS, adaptA5Files, adaptA5Order, buildA5Update, parseA
|
|||
describe('A5 launch', () => {
|
||||
it('accepts the H5 contract without injecting corp defaults', () => {
|
||||
expect(parseA5Launch({ phone: '13800138000', link_id: '42', token: 'corp-token' })).toEqual({ phone: '13800138000', linkId: '42' })
|
||||
expect(parseA5Launch({})).toEqual({ phone: '', linkId: '' })
|
||||
expect(parseA5Launch({})).toEqual({ phone: '', linkId: '2x6' })
|
||||
expect(parseA5Launch({ phone: '13800138000' })).toEqual({ phone: '13800138000', linkId: '2x6' })
|
||||
expect(parseA5Launch({ link_id: '' }).linkId).toBe('2x6')
|
||||
expect(parseA5Launch({ linkId: 'custom' }).linkId).toBe('custom')
|
||||
})
|
||||
it('supports legacy encoded scenes and URL Link separators', () => {
|
||||
expect(parseA5Launch({ scene: '%2Fh%2F42%26phone%3D13800138000' })).toEqual({ phone: '13800138000', linkId: '42' })
|
||||
expect(parseA5Launch({ phone: '13800138000%26link_id=42' })).toEqual({ phone: '13800138000', linkId: '42' })
|
||||
expect(parseA5Launch({ phone: '13800138000&link_id=old', link_id: 'new' }).linkId).toBe('new')
|
||||
expect(parseA5Launch({ scene: '/h/old', link_id: 'new' }).linkId).toBe('new')
|
||||
expect(parseA5Launch({ scene: '/p/corp' }).linkId).toBe('')
|
||||
expect(parseA5Launch({ scene: '/p/corp' }).linkId).toBe('2x6')
|
||||
})
|
||||
})
|
||||
describe('A5 responses', () => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function parseA5Launch (raw: Record<string, string | undefined>) {
|
|||
const scene = text(expanded.scene)
|
||||
return {
|
||||
phone: text(expanded.phone),
|
||||
linkId: text(expanded.link_id || expanded.linkId) || scene.match(/^\/h\/([^/?#&]+)/)?.[1] || '',
|
||||
linkId: text(expanded.link_id || expanded.linkId) || scene.match(/^\/h\/([^/?#&]+)/)?.[1] || '2x6',
|
||||
}
|
||||
}
|
||||
export function unwrapA5Response (status: number, raw: any) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const storage = vi.hoisted(() => ({ getStorageSync: vi.fn(), setStorageSync: vi.fn(), removeStorageSync: vi.fn() }))
|
||||
vi.mock('@tarojs/taro', () => ({ default: storage }))
|
||||
import { clearQueryPhone, getQueryPhone, saveQueryPhone } from './queryPhone'
|
||||
|
||||
beforeEach(() => vi.resetAllMocks())
|
||||
|
||||
describe('A5 query phone persistence', () => {
|
||||
it('restores the saved phone on direct entry', () => {
|
||||
storage.getStorageSync.mockReturnValue('13800138000')
|
||||
expect(getQueryPhone()).toBe('13800138000')
|
||||
expect(storage.getStorageSync).toHaveBeenCalledWith('a5QueryPhone')
|
||||
})
|
||||
|
||||
it('gives the launch phone priority over storage', () => {
|
||||
storage.getStorageSync.mockReturnValue('13800138000')
|
||||
expect(getQueryPhone('13900139000')).toBe('13900139000')
|
||||
expect(storage.getStorageSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([undefined, null, '', '138', 13800138000, { phone: '13800138000' }])('ignores invalid cached data: %j', (value) => {
|
||||
storage.getStorageSync.mockReturnValue(value)
|
||||
expect(getQueryPhone()).toBe('')
|
||||
})
|
||||
|
||||
it('persists valid query phones across subsequent reads', () => {
|
||||
const values = new Map<string, string>()
|
||||
storage.setStorageSync.mockImplementation((key, value) => values.set(key, value))
|
||||
storage.getStorageSync.mockImplementation((key) => values.get(key))
|
||||
saveQueryPhone('13800138000')
|
||||
expect(getQueryPhone()).toBe('13800138000')
|
||||
saveQueryPhone('13900139000')
|
||||
expect(getQueryPhone()).toBe('13900139000')
|
||||
saveQueryPhone('139')
|
||||
expect(getQueryPhone()).toBe('13900139000')
|
||||
})
|
||||
|
||||
it('removes the saved phone when explicitly cleared', () => {
|
||||
clearQueryPhone()
|
||||
expect(storage.removeStorageSync).toHaveBeenCalledWith('a5QueryPhone')
|
||||
})
|
||||
|
||||
it('tolerates storage errors', () => {
|
||||
const fail = () => { throw new Error('Storage unavailable') }
|
||||
storage.getStorageSync.mockImplementation(fail)
|
||||
storage.setStorageSync.mockImplementation(fail)
|
||||
storage.removeStorageSync.mockImplementation(fail)
|
||||
expect(getQueryPhone()).toBe('')
|
||||
expect(() => saveQueryPhone('13800138000')).not.toThrow()
|
||||
expect(() => clearQueryPhone()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
import { isValidPhone } from '../phone'
|
||||
|
||||
const PHONE_KEY = 'a5QueryPhone'
|
||||
|
||||
export function getQueryPhone (launchPhone = ''): string {
|
||||
if (launchPhone) return launchPhone
|
||||
try {
|
||||
const stored = Taro.getStorageSync(PHONE_KEY)
|
||||
return typeof stored === 'string' && isValidPhone(stored) ? stored : ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function saveQueryPhone (phone: string) {
|
||||
if (!isValidPhone(phone)) return
|
||||
try {
|
||||
Taro.setStorageSync(PHONE_KEY, phone)
|
||||
} catch {
|
||||
// Storage failure must not prevent the current query.
|
||||
}
|
||||
}
|
||||
|
||||
export function clearQueryPhone () {
|
||||
try {
|
||||
Taro.removeStorageSync(PHONE_KEY)
|
||||
} catch {
|
||||
// The input can still be cleared when storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import iconComplaintTimeoutActive from '@/assets/images/icon-complaint-timeout-a
|
|||
import iconKefu from '@/assets/images/icon-kefu.png'
|
||||
import iconInputClear from '@/assets/images/icon-input-clear.png'
|
||||
import iconLogo from '@/assets/images/icon-logo.png'
|
||||
import iconPen from '@/assets/images/icon-pen.png'
|
||||
import iconSearch from '@/assets/images/icon-search.png'
|
||||
|
||||
export const LOCAL_IMAGES = {
|
||||
|
|
@ -42,6 +41,5 @@ export const LOCAL_IMAGES = {
|
|||
kefu: iconKefu,
|
||||
clear: iconInputClear,
|
||||
logo: iconLogo,
|
||||
pen: iconPen,
|
||||
search: iconSearch,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { ensureSessionToken, pickToken } from './auth'
|
||||
|
||||
describe('pickToken', () => {
|
||||
it('reads token from the common config payload', () => {
|
||||
expect(pickToken({ data: { token: 'tmp-token' } })).toBe('tmp-token')
|
||||
})
|
||||
|
||||
it('reads a nested data.token', () => {
|
||||
expect(pickToken({ data: { data: { token: 'nested-token' } } })).toBe('nested-token')
|
||||
})
|
||||
|
||||
it('returns empty when missing', () => {
|
||||
expect(pickToken({ data: { config: {} } })).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureSessionToken', () => {
|
||||
it('keeps an existing jump token', async () => {
|
||||
const token = await ensureSessionToken({
|
||||
token: 'h5-token',
|
||||
fetchUserConfig: async () => {
|
||||
throw new Error('should not request config when token exists')
|
||||
},
|
||||
})
|
||||
|
||||
expect(token).toBe('h5-token')
|
||||
})
|
||||
|
||||
it('loads a temporary token from /api/user/config when missing', async () => {
|
||||
const token = await ensureSessionToken({
|
||||
token: '',
|
||||
fetchUserConfig: async () => ({ data: { token: 'guest-token' } }),
|
||||
})
|
||||
|
||||
expect(token).toBe('guest-token')
|
||||
})
|
||||
|
||||
it('fails when config does not return a token', async () => {
|
||||
await expect(
|
||||
ensureSessionToken({
|
||||
token: '',
|
||||
fetchUserConfig: async () => ({ data: {} }),
|
||||
})
|
||||
).rejects.toThrow('获取临时登录态失败')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
export function pickToken (payload: any): string {
|
||||
const first = payload?.data ?? payload
|
||||
const nested = first?.data && typeof first.data === 'object' && ('token' in first.data || !first.token)
|
||||
? first.data
|
||||
: first
|
||||
const token = nested?.token ?? first?.token ?? payload?.token
|
||||
return typeof token === 'string' ? token.trim() : ''
|
||||
}
|
||||
|
||||
export async function ensureSessionToken (input: {
|
||||
token?: string
|
||||
fetchUserConfig: () => Promise<unknown>
|
||||
}) {
|
||||
const existing = String(input.token || '').trim()
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const token = pickToken(await input.fetchUserConfig())
|
||||
if (!token) {
|
||||
throw new Error('获取临时登录态失败')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { getUserConfig, initCorp } from '@/api/user'
|
||||
import { ensureSessionToken } from './auth'
|
||||
import { applyLaunchParams, applyServiceConfig, getSession, setSession } from './session'
|
||||
|
||||
export async function bootstrapSession (raw: Record<string, string | undefined> = {}) {
|
||||
applyLaunchParams(raw)
|
||||
try {
|
||||
applyServiceConfig(await getUserConfig())
|
||||
} catch {
|
||||
// 配置拉取失败时仍可继续查询
|
||||
}
|
||||
const token = await ensureSessionToken({
|
||||
token: getSession().token,
|
||||
fetchUserConfig: () => getUserConfig(),
|
||||
})
|
||||
setSession({ token })
|
||||
try {
|
||||
await initCorp()
|
||||
} catch {
|
||||
// 已有临时 token 时,初始化失败不阻断查询
|
||||
}
|
||||
return getSession()
|
||||
}
|
||||
|
|
@ -2,12 +2,7 @@ export const STATIC_CDN = 'https://cdn.u8t.cn/frontend-static/corp-h5/static'
|
|||
export const CORP_ASSET_BASE = `${STATIC_CDN}/package-v3`
|
||||
export const MAP_ASSET_BASE = `${STATIC_CDN}/package-v4`
|
||||
export const HOME_ICON_BASE = `${MAP_ASSET_BASE}/images-pc/home`
|
||||
export const MOBILE_ASSET_BASE = `${MAP_ASSET_BASE}/images-mobile`
|
||||
export const BANNER_BG = `${MOBILE_ASSET_BASE}/bg-banner.png`
|
||||
export const CARD_HEADER = `${MOBILE_ASSET_BASE}/card_header.png`
|
||||
export const PAGE_TITLE = '敌无双企业服务'
|
||||
export const RECEIPT_STAMP_DONE = `${STATIC_CDN}/images/oderDetailCallBackSuccess.png`
|
||||
export const RECEIPT_STAMP_PENDING = `${STATIC_CDN}/images/oderDetailCallBack.png`
|
||||
|
||||
export const ENTITY_LABELS = {
|
||||
entityName: '门店名称',
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { buildCorpResubmitPayload, getCorpResubmitValues, validateIDCard } from './company'
|
||||
|
||||
describe('corp resubmit', () => {
|
||||
const order = {
|
||||
order_id: '12',
|
||||
entity_name: '测试有限公司',
|
||||
entity_id: '91330100MA2TEST001',
|
||||
entity_legal: '张三',
|
||||
entity_idno: '11010519491231002X',
|
||||
entity_phone: '13800138000',
|
||||
}
|
||||
|
||||
it('initializes the company form from the queried order', () => {
|
||||
expect(getCorpResubmitValues(order)).toEqual({
|
||||
entity_name: '测试有限公司',
|
||||
entity_id: '91330100MA2TEST001',
|
||||
entity_legal: '张三',
|
||||
entity_idno: '11010519491231002X',
|
||||
entity_phone: '13800138000',
|
||||
})
|
||||
})
|
||||
|
||||
it('matches the H5 update-order payload', () => {
|
||||
expect(buildCorpResubmitPayload(order, getCorpResubmitValues(order))).toEqual({
|
||||
id: '12',
|
||||
entity_extra: {},
|
||||
entity_id: '91330100MA2TEST001',
|
||||
entity_idno: '11010519491231002X',
|
||||
entity_legal: '张三',
|
||||
entity_name: '测试有限公司',
|
||||
entity_phone: '13800138000',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateIDCard', () => {
|
||||
it('validates the 18 digit checksum and accepts a lowercase x', () => {
|
||||
expect(validateIDCard('11010519491231002X')).toBe(true)
|
||||
expect(validateIDCard('11010519491231002x')).toBe(true)
|
||||
expect(validateIDCard('110105194912310021')).toBe(false)
|
||||
expect(validateIDCard('not-an-id')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import type { OrderLike } from './order'
|
||||
|
||||
export type CompanyItem = {
|
||||
name: string
|
||||
merge_name?: string
|
||||
color_name?: string
|
||||
credit_code: string
|
||||
oper_name: string
|
||||
start_date?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export type CorpResubmitValues = {
|
||||
entity_name: string
|
||||
entity_id: string
|
||||
entity_legal: string
|
||||
entity_idno: string
|
||||
entity_phone: string
|
||||
}
|
||||
|
||||
export function getCorpResubmitValues (order: OrderLike): CorpResubmitValues {
|
||||
return {
|
||||
entity_name: String(order.entity_name || ''),
|
||||
entity_id: String(order.entity_id || ''),
|
||||
entity_legal: String(order.entity_legal || ''),
|
||||
entity_idno: String(order.entity_idno || ''),
|
||||
entity_phone: String(order.entity_phone || ''),
|
||||
}
|
||||
}
|
||||
|
||||
export function validateIDCard (id: string) {
|
||||
const normalized = String(id || '').trim().toUpperCase()
|
||||
if (!/^\d{17}[\dX]$/.test(normalized)) return false
|
||||
|
||||
const coefficients = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
||||
const sum = coefficients.reduce((total, coefficient, index) => {
|
||||
return total + Number(normalized[index]) * coefficient
|
||||
}, 0)
|
||||
|
||||
return checkCodes[sum % 11] === normalized[17]
|
||||
}
|
||||
|
||||
export function buildCorpResubmitPayload (order: OrderLike, values: CorpResubmitValues) {
|
||||
return {
|
||||
id: String(order.order_id || ''),
|
||||
entity_extra: {},
|
||||
entity_id: values.entity_id,
|
||||
entity_idno: values.entity_idno,
|
||||
entity_legal: values.entity_legal,
|
||||
entity_name: values.entity_name,
|
||||
entity_phone: values.entity_phone,
|
||||
}
|
||||
}
|
||||
|
|
@ -48,16 +48,3 @@ export function hasServiceContact (config?: Record<string, any> | null) {
|
|||
const contact = pickServiceContact(config)
|
||||
return Boolean(contact.online || contact.phone || contact.weixin)
|
||||
}
|
||||
|
||||
export function mergeServiceConfig (current: Record<string, any> | undefined, payload: any) {
|
||||
const data = payload?.data ?? payload
|
||||
const incoming = data?.config && typeof data.config === 'object' ? data.config : {}
|
||||
const merged = {
|
||||
...(current || {}),
|
||||
...incoming,
|
||||
}
|
||||
if (merged['client.service.phone']) merged.service_phone = merged['client.service.phone']
|
||||
if (merged['client.service.weixin']) merged.service_weixin = merged['client.service.weixin']
|
||||
if (merged['client.service.online.mobile']) merged.service_online = merged['client.service.online.mobile']
|
||||
return merged
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import CryptoJS from 'crypto-js'
|
||||
import Utf8 from 'crypto-js/enc-utf8'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decryptResponse } from './crypto'
|
||||
|
||||
describe('decryptResponse', () => {
|
||||
it('decrypts the AES payload used by H5', () => {
|
||||
const key = Utf8.parse('JphN7MRfPSsaydwMeYk6YhBquRz5Ck5K')
|
||||
const iv = key.clone()
|
||||
iv.sigBytes = 16
|
||||
iv.clamp()
|
||||
const payload = { code: 0, data: { token: 'tmp-token' } }
|
||||
const encrypted = CryptoJS.AES.encrypt(JSON.stringify(payload), key, {
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
}).toString()
|
||||
|
||||
expect(decryptResponse(encrypted)).toEqual(payload)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import CryptoJS from 'crypto-js'
|
||||
import Utf8 from 'crypto-js/enc-utf8'
|
||||
|
||||
const AES_KEY = 'JphN7MRfPSsaydwMeYk6YhBquRz5Ck5K'
|
||||
|
||||
export function decryptResponse (encrypted: string) {
|
||||
const key = Utf8.parse(AES_KEY)
|
||||
const iv = key.clone()
|
||||
iv.sigBytes = 16
|
||||
iv.clamp()
|
||||
const decrypted = CryptoJS.AES.decrypt(encrypted, key, {
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
}).toString(Utf8)
|
||||
|
||||
return JSON.parse(decrypted)
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getReceiptMeta, numberToUpperCase, pickGoodsType, shouldShowReceipt, splitOrderFiles } from './detail'
|
||||
|
||||
describe('splitOrderFiles', () => {
|
||||
it('separates screenshot images from pdf reports', () => {
|
||||
expect(splitOrderFiles([
|
||||
{ id: '1', url: 'https://cdn.example/a.jpg' },
|
||||
{ id: '2', url: 'http://cdn.example/report.pdf' },
|
||||
{ id: '3', url: 'https://cdn.example/b.png?ver=1' },
|
||||
{ id: '4', url: 'https://cdn.example/note.PDF' },
|
||||
])).toEqual({
|
||||
images: [
|
||||
{ id: '1', url: 'https://cdn.example/a.jpg' },
|
||||
{ id: '3', url: 'https://cdn.example/b.png?ver=1' },
|
||||
],
|
||||
pdfs: [
|
||||
{ id: '2', url: 'https://cdn.example/report.pdf' },
|
||||
{ id: '4', url: 'https://cdn.example/note.PDF' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('returns empty groups when files are missing', () => {
|
||||
expect(splitOrderFiles()).toEqual({ images: [], pdfs: [] })
|
||||
expect(splitOrderFiles([])).toEqual({ images: [], pdfs: [] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldShowReceipt', () => {
|
||||
it('shows receipt for corp goods types and hides it for map_label', () => {
|
||||
expect(shouldShowReceipt('license_year')).toBe(true)
|
||||
expect(shouldShowReceipt('license_destory')).toBe(true)
|
||||
expect(shouldShowReceipt('credit_repair')).toBe(true)
|
||||
expect(shouldShowReceipt('map_label')).toBe(false)
|
||||
expect(shouldShowReceipt('unknown')).toBe(false)
|
||||
expect(shouldShowReceipt()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getReceiptMeta', () => {
|
||||
it('returns title and remark for each corp goods type', () => {
|
||||
expect(getReceiptMeta('license_year').title).toBe('信息申报回执单')
|
||||
expect(getReceiptMeta('license_year').description).toContain('年报国家公示官网')
|
||||
expect(getReceiptMeta('license_destory').title).toBe('工商信息注销回执单')
|
||||
expect(getReceiptMeta('license_destory').description).toContain('登记状态')
|
||||
expect(getReceiptMeta('credit_repair').title).toBe('工商信息修复回执单')
|
||||
expect(getReceiptMeta('credit_repair').description).toContain('creditchina.gov.cn')
|
||||
})
|
||||
|
||||
it('falls back to goods name when type is unknown', () => {
|
||||
expect(getReceiptMeta('unknown', '自定义业务')).toEqual({
|
||||
title: '自定义业务回执单',
|
||||
description: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('numberToUpperCase', () => {
|
||||
it('converts receipt amounts the same way as H5', () => {
|
||||
expect(numberToUpperCase('')).toBe('')
|
||||
expect(numberToUpperCase('0')).toBe('零元整')
|
||||
expect(numberToUpperCase('1')).toBe('壹元整')
|
||||
expect(numberToUpperCase('10')).toBe('壹拾元整')
|
||||
expect(numberToUpperCase('100.5')).toBe('壹佰元伍角')
|
||||
expect(numberToUpperCase('123.45')).toBe('壹佰贰拾叁元肆角伍分')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickGoodsType', () => {
|
||||
it('prefers prev_info goods_type then first goods item', () => {
|
||||
expect(pickGoodsType({
|
||||
data: {
|
||||
prev_info: { goods_type: 'license_year' },
|
||||
goods: [{ goods_type: 'map_label' }],
|
||||
},
|
||||
})).toBe('license_year')
|
||||
expect(pickGoodsType({
|
||||
prevInfo: { type: 'credit_repair' },
|
||||
})).toBe('credit_repair')
|
||||
expect(pickGoodsType({
|
||||
data: { goods: [{ goods_type: 'map_label' }] },
|
||||
})).toBe('map_label')
|
||||
expect(pickGoodsType({})).toBe('')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
export const CORP_GOODS_TYPES = ['license_year', 'license_destory', 'credit_repair'] as const
|
||||
|
||||
export type CorpGoodsType = (typeof CORP_GOODS_TYPES)[number]
|
||||
|
||||
const RECEIPT_META: Record<CorpGoodsType, { title: string, description: string }> = {
|
||||
credit_repair: {
|
||||
title: '工商信息修复回执单',
|
||||
description:
|
||||
'信用网站官网 https://www.creditchina.gov.cn/\n' +
|
||||
'搜索企业: 在首页搜索框输入公司全称或统一社会信用代码进行查询\n' +
|
||||
'查看信用信息: 进入企业信用信息详情页;\n' +
|
||||
'重点关注:\n' +
|
||||
'1."行政处罚"栏目: 查看该条被修复的行政处罚信息状态。如果修复成功,通常会显示"已修复"、"已信用修复"、"已撤下"等状态标识,或者该条信息已不再显示在公示列表中;\n' +
|
||||
'2."信用修复"栏目(如果有): 有些平台会单独列出企业的信用修复申请记录及状态(如:申请中、审核通过/修复成功、审核不通过);\n' +
|
||||
'3.查看"失信信息"或"重点关注名单": 如果该处罚曾导致企业被列入失信名单或重点关注名单,确认这些名单中是否已移除该公司;',
|
||||
},
|
||||
license_destory: {
|
||||
title: '工商信息注销回执单',
|
||||
description:
|
||||
'访问官网(http://www.gsxt.gov.cn),输入企业名称或统一社会信用代码进行搜索。\n' +
|
||||
'在企业详情页面查看“登记状态”栏是否标注为“注销”\n' +
|
||||
'注意:注销信息通常需1-3天完成公示更新,建议完成注销流程后次日查询。',
|
||||
},
|
||||
license_year: {
|
||||
title: '信息申报回执单',
|
||||
description:
|
||||
'年报国家公示官网:https://www.gsxt.gov.cn\n' +
|
||||
'1.年报公示日期为7个工作日,到期后可自行登陆国家企业信用信息官网查询;\n' +
|
||||
'2.您的个人信息(姓名、电话、身份证),只用于您办理业务时使用;\n' +
|
||||
'3.企业一站式服务平台为第三方企业服务公司,办理业务方便快捷,办理不成功退全部费用;',
|
||||
},
|
||||
}
|
||||
|
||||
export function isCorpGoodsType (goodsType?: string): goodsType is CorpGoodsType {
|
||||
return (CORP_GOODS_TYPES as readonly string[]).includes(goodsType || '')
|
||||
}
|
||||
|
||||
export function shouldShowReceipt (goodsType?: string) {
|
||||
return isCorpGoodsType(goodsType)
|
||||
}
|
||||
|
||||
export function pickGoodsType (payload: any): string {
|
||||
const data = payload?.data ?? payload ?? {}
|
||||
const prev = data.prev_info ?? data.prevInfo ?? payload?.prevInfo ?? {}
|
||||
const fromPrev = String(prev.goods_type || prev.type || '').trim()
|
||||
if (fromPrev) return fromPrev
|
||||
|
||||
const goods = Array.isArray(data.goods) ? data.goods : []
|
||||
const first = goods[0] || {}
|
||||
return String(first.goods_type || first.type || '').trim()
|
||||
}
|
||||
|
||||
export function getReceiptMeta (goodsType?: string, goodsName?: string) {
|
||||
if (isCorpGoodsType(goodsType)) {
|
||||
return RECEIPT_META[goodsType]
|
||||
}
|
||||
const name = String(goodsName || '').trim()
|
||||
return {
|
||||
title: name ? `${name}回执单` : '回执单',
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
|
||||
export type OrderFile = {
|
||||
id?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
function isPdfUrl (url: string) {
|
||||
return /\.pdf(?:$|[?#])/i.test(url)
|
||||
}
|
||||
|
||||
export function splitOrderFiles (files?: OrderFile[] | null) {
|
||||
const images: OrderFile[] = []
|
||||
const pdfs: OrderFile[] = []
|
||||
|
||||
for (const file of files || []) {
|
||||
const url = typeof file?.url === 'string' ? file.url.replace(/^http:\/\//i, 'https://') : ''
|
||||
if (!url) continue
|
||||
const nextFile = url === file.url ? file : { ...file, url }
|
||||
if (isPdfUrl(url)) pdfs.push(nextFile)
|
||||
else images.push(nextFile)
|
||||
}
|
||||
|
||||
return { images, pdfs }
|
||||
}
|
||||
|
||||
export function numberToUpperCase (money: unknown): string {
|
||||
const cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
|
||||
const cnIntRadice = ['', '拾', '佰', '仟']
|
||||
const cnIntUnits = ['', '万', '亿', '兆']
|
||||
const cnDecUnits = ['角', '分']
|
||||
const cnInteger = '整'
|
||||
const cnIntLast = '元'
|
||||
|
||||
if (money === '' || money === undefined || money === null) return ''
|
||||
|
||||
const text = String(money)
|
||||
const cashText = text.charAt(0) === '-' ? text.slice(1) : text
|
||||
let cash = parseFloat(cashText)
|
||||
if (cash === 0) return `${cnNums[0]}${cnIntLast}${cnInteger}`
|
||||
if (!Number.isFinite(cash)) return ''
|
||||
|
||||
const [integerNum, rawDecimal = ''] = cash.toString().split('.')
|
||||
const decimalNum = rawDecimal.substr(0, 2)
|
||||
let chineseStr = ''
|
||||
|
||||
if (integerNum !== '-' && parseInt(integerNum, 10) > 0) {
|
||||
const intLen = integerNum.length
|
||||
let zero = 0
|
||||
for (let i = 0; i < intLen; i++) {
|
||||
const intChar = integerNum.substr(i, 1)
|
||||
const intSlen = intLen - i - 1
|
||||
const divided = intSlen / 4
|
||||
const remain = intSlen % 4
|
||||
|
||||
if (intChar === '0') {
|
||||
zero += 1
|
||||
} else {
|
||||
if (zero > 0) chineseStr += cnNums[0]
|
||||
zero = 0
|
||||
chineseStr += cnNums[parseInt(intChar, 10)] + cnIntRadice[remain]
|
||||
}
|
||||
if (remain === 0 && divided > 0) {
|
||||
chineseStr += cnIntUnits[divided]
|
||||
}
|
||||
}
|
||||
chineseStr += cnIntLast
|
||||
}
|
||||
|
||||
if (decimalNum) {
|
||||
for (let i = 0; i < decimalNum.length; i++) {
|
||||
const decChar = decimalNum.substr(i, 1)
|
||||
if (decChar !== '0') {
|
||||
chineseStr += cnNums[parseInt(decChar, 10)] + cnDecUnits[i]
|
||||
}
|
||||
if (decChar === '0' && parseInt(integerNum, 10) > 0) {
|
||||
chineseStr += cnNums[0] + cnDecUnits[i]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chineseStr += cnInteger
|
||||
}
|
||||
|
||||
return chineseStr
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_LAUNCH_SCHEME,
|
||||
parseLaunchQuery,
|
||||
parseMiniProgramScheme,
|
||||
resolveLaunchQuery,
|
||||
} from './launch'
|
||||
|
||||
describe('parseMiniProgramScheme', () => {
|
||||
it('reads the default weixin business scheme query', () => {
|
||||
expect(parseMiniProgramScheme(DEFAULT_LAUNCH_SCHEME)).toEqual({
|
||||
appId: 'wxf8a8961f30396c19',
|
||||
path: 'pages/index/index',
|
||||
scene: '/h/KZ9Q',
|
||||
token: '',
|
||||
host: 'nb.batiao8.com',
|
||||
package: '10044',
|
||||
phone: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseLaunchQuery', () => {
|
||||
it('keeps token host and package from the H5 jump', () => {
|
||||
expect(
|
||||
parseLaunchQuery({
|
||||
token: 'h5-token',
|
||||
host: 'h5test.batiao8.com',
|
||||
package: '10044',
|
||||
scene: '%2Fh%2FKZ9Q',
|
||||
})
|
||||
).toEqual({
|
||||
token: 'h5-token',
|
||||
host: 'h5test.batiao8.com',
|
||||
packageId: '10044',
|
||||
scene: '/h/KZ9Q',
|
||||
linkId: 'KZ9Q',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('reads share and preview short links', () => {
|
||||
expect(parseLaunchQuery({ scene: '/s/SHARE1' }).shareId).toBe('SHARE1')
|
||||
expect(parseLaunchQuery({ scene: '/p/PREV9' }).prevId).toBe('PREV9')
|
||||
})
|
||||
|
||||
it('unpacks jump params stuffed into scene when & is encoded as %26', () => {
|
||||
expect(
|
||||
parseLaunchQuery({
|
||||
scene:
|
||||
'%2Fh%2FKZ9Q%26token=67738e89-4f74-4d9b-bdcc-1d30dc11f0fd%26host=nb.batiao8.com%26package=10044%26phone=17316703138',
|
||||
})
|
||||
).toEqual({
|
||||
token: '67738e89-4f74-4d9b-bdcc-1d30dc11f0fd',
|
||||
host: 'nb.batiao8.com',
|
||||
packageId: '10044',
|
||||
scene: '/h/KZ9Q',
|
||||
linkId: 'KZ9Q',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '17316703138',
|
||||
})
|
||||
})
|
||||
|
||||
it('unpacks jump params after WeChat decodes %26 into &', () => {
|
||||
expect(
|
||||
parseLaunchQuery({
|
||||
scene:
|
||||
'/h/KZ9Q&token=67738e89-4f74-4d9b-bdcc-1d30dc11f0fd&host=nb.batiao8.com&package=10044&phone=17316703138',
|
||||
})
|
||||
).toEqual({
|
||||
token: '67738e89-4f74-4d9b-bdcc-1d30dc11f0fd',
|
||||
host: 'nb.batiao8.com',
|
||||
packageId: '10044',
|
||||
scene: '/h/KZ9Q',
|
||||
linkId: 'KZ9Q',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '17316703138',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps explicit query fields over values stuffed into scene', () => {
|
||||
expect(
|
||||
parseLaunchQuery({
|
||||
scene: '/h/KZ9Q&token=from-scene&phone=13800138000',
|
||||
token: 'from-query',
|
||||
}).token
|
||||
).toBe('from-query')
|
||||
})
|
||||
|
||||
it('ignores empty values', () => {
|
||||
expect(parseLaunchQuery({})).toEqual({
|
||||
token: '',
|
||||
host: '',
|
||||
packageId: '',
|
||||
scene: '',
|
||||
linkId: '',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
expect(parseLaunchQuery({ phone: '13900001111' }).phone).toBe('13900001111')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveLaunchQuery', () => {
|
||||
it('fills missing fields from the default scheme', () => {
|
||||
expect(resolveLaunchQuery({})).toEqual({
|
||||
token: '',
|
||||
host: 'nb.batiao8.com',
|
||||
packageId: '10044',
|
||||
scene: '/h/KZ9Q',
|
||||
linkId: 'KZ9Q',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the launched phone when present', () => {
|
||||
expect(resolveLaunchQuery({ phone: '13900001111' }).phone).toBe('13900001111')
|
||||
})
|
||||
|
||||
it('keeps explicit jump params over defaults', () => {
|
||||
expect(
|
||||
resolveLaunchQuery({
|
||||
token: 'h5-token',
|
||||
host: 'h5test.batiao8.com',
|
||||
package: '20001',
|
||||
scene: '/s/SHARE1',
|
||||
})
|
||||
).toEqual({
|
||||
token: 'h5-token',
|
||||
host: 'h5test.batiao8.com',
|
||||
packageId: '20001',
|
||||
scene: '/s/SHARE1',
|
||||
linkId: '',
|
||||
shareId: 'SHARE1',
|
||||
prevId: '',
|
||||
phone: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('uses stuffed scene params instead of scheme defaults', () => {
|
||||
expect(
|
||||
resolveLaunchQuery({
|
||||
scene:
|
||||
'/h/KZ9Q&token=h5-token&host=h5test.batiao8.com&package=20001&phone=17316703138',
|
||||
})
|
||||
).toEqual({
|
||||
token: 'h5-token',
|
||||
host: 'h5test.batiao8.com',
|
||||
packageId: '20001',
|
||||
scene: '/h/KZ9Q',
|
||||
linkId: 'KZ9Q',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '17316703138',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
export type LaunchQuery = {
|
||||
token: string
|
||||
host: string
|
||||
packageId: string
|
||||
scene: string
|
||||
linkId: string
|
||||
shareId: string
|
||||
prevId: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
export const DEFAULT_LAUNCH_SCHEME =
|
||||
'weixin://dl/business/?appid=wxf8a8961f30396c19&path=pages/index/index&query=scene%3D%2Fh%2FKZ9Q%26host%3Dnb.batiao8.com%26package%3D10044'
|
||||
|
||||
export function parseMiniProgramScheme (scheme: string) {
|
||||
const search = scheme.split('?')[1] || ''
|
||||
const params = new URLSearchParams(search)
|
||||
const inner = new URLSearchParams(params.get('query') || '')
|
||||
return {
|
||||
appId: params.get('appid') || '',
|
||||
path: params.get('path') || '',
|
||||
scene: inner.get('scene') || '',
|
||||
token: inner.get('token') || '',
|
||||
host: inner.get('host') || '',
|
||||
package: inner.get('package') || '',
|
||||
phone: inner.get('phone') || '',
|
||||
}
|
||||
}
|
||||
|
||||
function asText (value?: string) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function safeDecode (value: string) {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function parseSceneIds (scene: string) {
|
||||
const decoded = safeDecode(scene).trim()
|
||||
const matched = decoded.match(/\/(h|s|p)\/([^/?#&]+)/)
|
||||
if (!matched) {
|
||||
return { linkId: '', shareId: '', prevId: '' }
|
||||
}
|
||||
|
||||
const id = matched[2]
|
||||
if (matched[1] === 'h') return { linkId: id, shareId: '', prevId: '' }
|
||||
if (matched[1] === 's') return { linkId: '', shareId: id, prevId: '' }
|
||||
return { linkId: '', shareId: '', prevId: id }
|
||||
}
|
||||
|
||||
function expandLaunchRaw (raw: Record<string, string | undefined> = {}) {
|
||||
const scene = asText(raw.scene)
|
||||
if (!scene) return raw
|
||||
|
||||
const decoded = safeDecode(scene).replace(/%26/gi, '&')
|
||||
const amp = decoded.indexOf('&')
|
||||
if (amp < 0) return raw
|
||||
|
||||
const extra: Record<string, string> = {}
|
||||
new URLSearchParams(decoded.slice(amp + 1)).forEach((value, key) => {
|
||||
extra[key] = value
|
||||
})
|
||||
|
||||
return {
|
||||
...extra,
|
||||
...raw,
|
||||
scene: decoded.slice(0, amp),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLaunchQuery (raw: Record<string, string | undefined> = {}): LaunchQuery {
|
||||
const expanded = expandLaunchRaw(raw)
|
||||
const scene = safeDecode(asText(expanded.scene))
|
||||
return {
|
||||
token: asText(expanded.token),
|
||||
host: asText(expanded.host),
|
||||
packageId: asText(expanded.package),
|
||||
scene,
|
||||
phone: asText(expanded.phone),
|
||||
...parseSceneIds(scene),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveLaunchQuery (raw: Record<string, string | undefined> = {}): LaunchQuery {
|
||||
const parsed = parseLaunchQuery(raw)
|
||||
const defaults = parseLaunchQuery(parseMiniProgramScheme(DEFAULT_LAUNCH_SCHEME))
|
||||
const scene = parsed.scene || defaults.scene
|
||||
const sceneIds = parsed.scene
|
||||
? { linkId: parsed.linkId, shareId: parsed.shareId, prevId: parsed.prevId }
|
||||
: { linkId: defaults.linkId, shareId: defaults.shareId, prevId: defaults.prevId }
|
||||
|
||||
return {
|
||||
token: parsed.token || defaults.token,
|
||||
host: parsed.host || defaults.host,
|
||||
packageId: parsed.packageId || defaults.packageId,
|
||||
scene,
|
||||
phone: parsed.phone || defaults.phone,
|
||||
...sceneIds,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { adaptA5Order } from './a5/data'
|
||||
import {
|
||||
buildResubmitPayload,
|
||||
filterMapLabelOrders,
|
||||
filterOrdersForProduct,
|
||||
formatMapCoordinate,
|
||||
formatMapCoordinateDisplay,
|
||||
getOrderAddressName,
|
||||
|
|
@ -56,47 +53,6 @@ describe('order display name', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('filterMapLabelOrders', () => {
|
||||
it('keeps only map_label orders and parses goods_param', () => {
|
||||
const orders = filterMapLabelOrders([
|
||||
mapOrder,
|
||||
{ ...mapOrder, order_id: '5', goods_type: 'license_year' },
|
||||
] as any)
|
||||
|
||||
expect(orders).toHaveLength(1)
|
||||
expect(orders[0].extra.goods_param).toEqual({
|
||||
'client.goods.icon': ['https://cdn.example/i.png'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('filterOrdersForProduct', () => {
|
||||
it('keeps all three corp products together when querying from a corp entry', () => {
|
||||
const orders = filterOrdersForProduct('license_year', [
|
||||
mapOrder,
|
||||
{ ...mapOrder, order_id: '5', goods_type: 'license_year' },
|
||||
{ ...mapOrder, order_id: '6', goods_type: 'license_destory' },
|
||||
{ ...mapOrder, order_id: '7', goods_type: 'credit_repair' },
|
||||
] as any)
|
||||
|
||||
expect(orders.map((item) => item.order_id)).toEqual(['4', '5', '6', '7'])
|
||||
expect(orders[0].extra.goods_param).toEqual({
|
||||
'client.goods.icon': ['https://cdn.example/i.png'],
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps only matching orders for map_label and unknown types', () => {
|
||||
const mixed = [
|
||||
mapOrder,
|
||||
{ ...mapOrder, order_id: '5', goods_type: 'license_year' },
|
||||
] as any
|
||||
|
||||
expect(filterOrdersForProduct('map_label', mixed).map((item) => item.order_id)).toEqual(['4'])
|
||||
expect(filterOrdersForProduct('', mixed).map((item) => item.order_id)).toEqual(['4'])
|
||||
expect(filterOrdersForProduct('unknown', mixed)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('order address helpers', () => {
|
||||
it('splits named address and coordinate address', () => {
|
||||
expect(getOrderAddressName(mapOrder as any)).toBe('南京路1号')
|
||||
|
|
@ -158,39 +114,3 @@ describe('map coordinate display', () => {
|
|||
expect(formatMapCoordinateDisplay({ lng: 120.156892, lat: 30.278956 })).toBe('N30.278956°, E120.156892°')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildResubmitPayload', () => {
|
||||
it('updates extra while dropping upload-only keys and keeping storefront 0 placeholders', () => {
|
||||
expect(
|
||||
buildResubmitPayload({
|
||||
order: mapOrder as any,
|
||||
values: {
|
||||
entity_name: '新店',
|
||||
entity_address_name: '淮海路2号',
|
||||
entity_address: '121.480000,31.240000',
|
||||
entity_phone: '13900139000',
|
||||
entity_phone2: '13700137000',
|
||||
},
|
||||
storefrontSlots: [
|
||||
{ id: '123', url: 'https://cdn.example/c.jpg', preview: 'p' },
|
||||
{ id: '45', url: 'https://cdn.example/d.jpg', preview: 'p' },
|
||||
null,
|
||||
],
|
||||
licenseImages: [{ id: '31', url: 'https://cdn.example/e.jpg', preview: 'p' }],
|
||||
})
|
||||
).toEqual({
|
||||
id: '4',
|
||||
corp_id: 0,
|
||||
entity_name: '新店',
|
||||
entity_phone: '13900139000',
|
||||
entity_extra: { keep: 1 },
|
||||
extra: {
|
||||
entity_address_name: '淮海路2号',
|
||||
entity_address: '121.480000,31.240000',
|
||||
entity_phone2: '13700137000',
|
||||
entity_storefront_image: '123,45,0',
|
||||
entity_business_license_image: '31',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { isCorpGoodsType } from './detail'
|
||||
|
||||
export type UploadedImage = {
|
||||
id: string
|
||||
url: string
|
||||
|
|
@ -25,18 +23,6 @@ export type ResubmitValues = {
|
|||
entity_phone2?: string
|
||||
}
|
||||
|
||||
const OMIT_EXTRA_KEYS = new Set([
|
||||
'origin_price',
|
||||
'qrCodeImgUrl',
|
||||
'remoteIp',
|
||||
'weixinAppId',
|
||||
'entity_storefront_image',
|
||||
'entity_business_license_image',
|
||||
'entity_storefront_image_url',
|
||||
'entity_business_license_image_url',
|
||||
'goods_param',
|
||||
])
|
||||
|
||||
function splitValues (value: unknown) {
|
||||
if (value === undefined || value === null) return []
|
||||
return String(value)
|
||||
|
|
@ -122,41 +108,10 @@ export function getInitialPhone2 (order: OrderLike) {
|
|||
return String(extra.entity_phone2 || extra.entity_phone_2 || extra.phone2 || '')
|
||||
}
|
||||
|
||||
export function getGoodsIcons (order: OrderLike) {
|
||||
const icons = order.extra?.goods_param?.['client.goods.icon']
|
||||
return Array.isArray(icons) ? icons.filter((item) => typeof item === 'string') : []
|
||||
}
|
||||
|
||||
export function normalizeOrders<T extends OrderLike> (orders: T[] = []) {
|
||||
return orders.map((item) => {
|
||||
const nextItem = { ...item, extra: { ...(item.extra || {}) } }
|
||||
const goodsParam = nextItem.extra.goods_param
|
||||
if (typeof goodsParam === 'string') {
|
||||
try {
|
||||
nextItem.extra.goods_param = JSON.parse(goodsParam)
|
||||
} catch {
|
||||
nextItem.extra.goods_param = null
|
||||
}
|
||||
}
|
||||
return nextItem
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrderDisplayName (order?: OrderLike | null) {
|
||||
return String(order?.goods_name || '').trim() || '-'
|
||||
}
|
||||
|
||||
export function filterOrdersForProduct<T extends OrderLike> (goodsType: string, orders: T[] = []) {
|
||||
const normalized = normalizeOrders(orders)
|
||||
if (isCorpGoodsType(goodsType)) return normalized
|
||||
const target = goodsType || 'map_label'
|
||||
return normalized.filter((item) => item.goods_type === target)
|
||||
}
|
||||
|
||||
export function filterMapLabelOrders<T extends OrderLike> (orders: T[] = []) {
|
||||
return filterOrdersForProduct('map_label', orders)
|
||||
}
|
||||
|
||||
export function getOrderExtraImages (order: OrderLike | null | undefined, keys: string[]): UploadedImage[] {
|
||||
const extra = order?.extra || {}
|
||||
|
||||
|
|
@ -189,33 +144,3 @@ export function joinUploadedImageIds (images: UploadedImage[]) {
|
|||
.filter(Boolean)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
export function buildResubmitPayload (input: {
|
||||
order: OrderLike
|
||||
values: ResubmitValues
|
||||
storefrontSlots: StorefrontSlot[]
|
||||
licenseImages: UploadedImage[]
|
||||
}) {
|
||||
const existingExtra = Object.entries(input.order.extra || {}).reduce<Record<string, unknown>>((result, [key, value]) => {
|
||||
if (!OMIT_EXTRA_KEYS.has(key)) {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
|
||||
return {
|
||||
id: String(input.order.order_id || ''),
|
||||
corp_id: 0,
|
||||
entity_name: input.values.entity_name || input.order.entity_name || '',
|
||||
entity_phone: input.values.entity_phone || '',
|
||||
entity_extra: input.order.entity_extra || {},
|
||||
extra: {
|
||||
...existingExtra,
|
||||
entity_address_name: input.values.entity_address_name || '',
|
||||
entity_address: input.values.entity_address || '',
|
||||
entity_phone2: input.values.entity_phone2 || '',
|
||||
entity_storefront_image: joinStorefrontSlots(input.storefrontSlots),
|
||||
entity_business_license_image: joinUploadedImageIds(input.licenseImages),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
import type { OrderLike } from './order'
|
||||
|
||||
const ORDER_DRAFT_KEY = 'orderDraft'
|
||||
|
||||
let memoryDraft: OrderLike | null = null
|
||||
|
||||
export function setOrderDraft (order: OrderLike) {
|
||||
memoryDraft = order
|
||||
try {
|
||||
Taro.setStorageSync(ORDER_DRAFT_KEY, order)
|
||||
} catch {
|
||||
// ignore quota / private-mode failures; in-memory draft still works
|
||||
}
|
||||
}
|
||||
|
||||
export function getOrderDraft (): OrderLike | null {
|
||||
if (memoryDraft) return memoryDraft
|
||||
try {
|
||||
const stored = Taro.getStorageSync(ORDER_DRAFT_KEY)
|
||||
if (stored && typeof stored === 'object') {
|
||||
memoryDraft = stored
|
||||
return stored
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { hostFromOrigin, resolveApiOrigin, resolveRequestHost } from './origin'
|
||||
|
||||
describe('resolveApiOrigin', () => {
|
||||
const original = process.env.TARO_APP_API_ORIGIN
|
||||
|
||||
afterEach(() => {
|
||||
process.env.TARO_APP_API_ORIGIN = original
|
||||
})
|
||||
|
||||
it('uses the configured local test origin', () => {
|
||||
expect(resolveApiOrigin('http://corp-test.batiao8.com')).toBe('http://corp-test.batiao8.com')
|
||||
})
|
||||
|
||||
it('uses the configured production origin', () => {
|
||||
expect(resolveApiOrigin('https://nb.zuom8.cn')).toBe('https://nb.zuom8.cn')
|
||||
})
|
||||
|
||||
it('strips a trailing slash', () => {
|
||||
expect(resolveApiOrigin('https://nb.zuom8.cn/')).toBe('https://nb.zuom8.cn')
|
||||
})
|
||||
|
||||
it('reads TARO_APP_API_ORIGIN when no argument is passed', () => {
|
||||
process.env.TARO_APP_API_ORIGIN = 'http://corp-test.batiao8.com'
|
||||
expect(resolveApiOrigin()).toBe('http://corp-test.batiao8.com')
|
||||
})
|
||||
|
||||
it('throws when the env origin is missing', () => {
|
||||
delete process.env.TARO_APP_API_ORIGIN
|
||||
expect(() => resolveApiOrigin()).toThrow('缺少接口域名')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hostFromOrigin', () => {
|
||||
it('extracts the hostname from a local test origin', () => {
|
||||
expect(hostFromOrigin('http://corp-test.batiao8.com')).toBe('corp-test.batiao8.com')
|
||||
})
|
||||
|
||||
it('extracts the hostname from the official origin', () => {
|
||||
expect(hostFromOrigin('https://nb.zuom8.cn')).toBe('nb.zuom8.cn')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveRequestHost', () => {
|
||||
it('prefers the launched host for x-host', () => {
|
||||
expect(resolveRequestHost({
|
||||
host: 'h5test.batiao8.com',
|
||||
fallbackHost: 'nb.zuom8.cn',
|
||||
})).toBe('h5test.batiao8.com')
|
||||
})
|
||||
|
||||
it('falls back to the env host when no host is launched', () => {
|
||||
expect(resolveRequestHost({ fallbackHost: 'nb.zuom8.cn' })).toBe('nb.zuom8.cn')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
export function normalizeHost (host?: string) {
|
||||
return String(host || '')
|
||||
.trim()
|
||||
.replace(/^https?:\/\//i, '')
|
||||
.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
export function normalizeOrigin (origin?: string) {
|
||||
return String(origin || '').trim().replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
export function hostFromOrigin (origin?: string) {
|
||||
return normalizeHost(normalizeOrigin(origin).replace(/^https?:\/\//i, ''))
|
||||
}
|
||||
|
||||
export function resolveApiOrigin (origin = process.env.TARO_APP_API_ORIGIN) {
|
||||
const value = normalizeOrigin(origin)
|
||||
if (!value) {
|
||||
throw new Error('缺少接口域名')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function resolveRequestHost (input: { host?: string, fallbackHost?: string }) {
|
||||
return normalizeHost(input.host) || normalizeHost(input.fallbackHost)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { buildFileUrl, pickPresignResult } from './qiniu'
|
||||
|
||||
describe('pickPresignResult', () => {
|
||||
it('normalizes the presign payload used by H5', () => {
|
||||
expect(
|
||||
pickPresignResult({
|
||||
code: 0,
|
||||
data: {
|
||||
id: 33013,
|
||||
token: 'qiniu-upload-token',
|
||||
key: 'upload/10034/a.jpg',
|
||||
domain: 'https://cdn2.batiao8.com',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
id: '33013',
|
||||
token: 'qiniu-upload-token',
|
||||
key: 'upload/10034/a.jpg',
|
||||
domain: 'https://cdn2.batiao8.com',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildFileUrl', () => {
|
||||
it('joins domain and key without duplicate slashes', () => {
|
||||
expect(buildFileUrl('https://cdn2.batiao8.com/', '/upload/a.jpg')).toBe(
|
||||
'https://cdn2.batiao8.com/upload/a.jpg'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
export const QINIU_UPLOAD_URL = 'https://up-cn-east-2.qiniup.com'
|
||||
|
||||
export type PresignResult = {
|
||||
id: string
|
||||
token: string
|
||||
key: string
|
||||
domain: string
|
||||
}
|
||||
|
||||
export function pickPresignResult (response: any): PresignResult {
|
||||
const payload = response?.data ?? response
|
||||
const data = payload?.data && typeof payload.data === 'object' ? payload.data : payload
|
||||
return {
|
||||
id: data?.id === undefined || data?.id === null ? '' : String(data.id),
|
||||
token: typeof data?.token === 'string' ? data.token : '',
|
||||
key: typeof data?.key === 'string' ? data.key : '',
|
||||
domain: typeof data?.domain === 'string' ? data.domain : '',
|
||||
}
|
||||
}
|
||||
|
||||
export function buildFileUrl (domain: string, key: string) {
|
||||
if (!domain) return key
|
||||
const normalizedDomain = domain.endsWith('/') ? domain.slice(0, -1) : domain
|
||||
const normalizedKey = key.startsWith('/') ? key.slice(1) : key
|
||||
return `${normalizedDomain}/${normalizedKey}`
|
||||
}
|
||||
|
||||
export function getFileExtension (filePath: string) {
|
||||
const name = filePath.split('?')[0]
|
||||
const slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'))
|
||||
const fileName = slash >= 0 ? name.slice(slash + 1) : name
|
||||
const dotIndex = fileName.lastIndexOf('.')
|
||||
if (dotIndex >= 0 && dotIndex < fileName.length - 1) {
|
||||
return fileName.slice(dotIndex + 1).toLowerCase()
|
||||
}
|
||||
return 'jpg'
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { bytesToBase64, encodeQrPng } from './qrPng'
|
||||
|
||||
describe('encodeQrPng', () => {
|
||||
it('encodes a wechat personal link into a png buffer', () => {
|
||||
const png = encodeQrPng('https://u.wechat.com/ELkCkT_hVVAz8wNghBA3hMc?s=2')
|
||||
expect(Array.from(png.slice(0, 8))).toEqual([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||
expect(bytesToBase64(png).startsWith('iVBOR')).toBe(true)
|
||||
})
|
||||
})
|
||||
129
src/lib/qrPng.ts
129
src/lib/qrPng.ts
|
|
@ -1,129 +0,0 @@
|
|||
import QRCode from 'qrcode'
|
||||
|
||||
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
||||
const CRC_TABLE = makeCrcTable()
|
||||
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
export function encodeQrPng (text: string, scale = 6, margin = 4) {
|
||||
const qr = QRCode.create(text, { errorCorrectionLevel: 'M' })
|
||||
const moduleCount = qr.modules.size
|
||||
const dim = (moduleCount + margin * 2) * scale
|
||||
const raw = new Uint8Array((dim * 3 + 1) * dim)
|
||||
let offset = 0
|
||||
|
||||
for (let y = 0; y < dim; y += 1) {
|
||||
raw[offset] = 0
|
||||
offset += 1
|
||||
const moduleY = Math.floor(y / scale) - margin
|
||||
for (let x = 0; x < dim; x += 1) {
|
||||
const moduleX = Math.floor(x / scale) - margin
|
||||
const dark = moduleX >= 0 && moduleX < moduleCount && moduleY >= 0 && moduleY < moduleCount
|
||||
? Boolean(qr.modules.get(moduleX, moduleY))
|
||||
: false
|
||||
const value = dark ? 0 : 255
|
||||
raw[offset] = value
|
||||
raw[offset + 1] = value
|
||||
raw[offset + 2] = value
|
||||
offset += 3
|
||||
}
|
||||
}
|
||||
|
||||
const ihdr = [
|
||||
...u32(dim),
|
||||
...u32(dim),
|
||||
8,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
const idat = zlibStore(raw)
|
||||
const bytes = new Uint8Array([
|
||||
...PNG_SIGNATURE,
|
||||
...chunk('IHDR', ihdr),
|
||||
...chunk('IDAT', idat),
|
||||
...chunk('IEND', []),
|
||||
])
|
||||
return bytes
|
||||
}
|
||||
|
||||
export function bytesToBase64 (bytes: Uint8Array) {
|
||||
let result = ''
|
||||
for (let i = 0; i < bytes.length; i += 3) {
|
||||
const a = bytes[i]
|
||||
const b = i + 1 < bytes.length ? bytes[i + 1] : 0
|
||||
const c = i + 2 < bytes.length ? bytes[i + 2] : 0
|
||||
const triple = (a << 16) | (b << 8) | c
|
||||
result += BASE64_CHARS[(triple >> 18) & 63]
|
||||
result += BASE64_CHARS[(triple >> 12) & 63]
|
||||
result += i + 1 < bytes.length ? BASE64_CHARS[(triple >> 6) & 63] : '='
|
||||
result += i + 2 < bytes.length ? BASE64_CHARS[triple & 63] : '='
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function zlibStore (raw: Uint8Array) {
|
||||
const blocks: number[] = [0x78, 0x01]
|
||||
const max = 65535
|
||||
for (let i = 0; i < raw.length; i += max) {
|
||||
const end = Math.min(i + max, raw.length)
|
||||
const chunkData = raw.subarray(i, end)
|
||||
const last = end === raw.length ? 1 : 0
|
||||
const len = chunkData.length
|
||||
blocks.push(last, len & 0xff, (len >> 8) & 0xff, (~len) & 0xff, ((~len) >> 8) & 0xff)
|
||||
for (let j = 0; j < chunkData.length; j += 1) {
|
||||
blocks.push(chunkData[j])
|
||||
}
|
||||
}
|
||||
blocks.push(...u32(adler32(raw)))
|
||||
return blocks
|
||||
}
|
||||
|
||||
function chunk (type: string, data: number[]) {
|
||||
const typeBytes = [type.charCodeAt(0), type.charCodeAt(1), type.charCodeAt(2), type.charCodeAt(3)]
|
||||
return [
|
||||
...u32(data.length),
|
||||
...typeBytes,
|
||||
...data,
|
||||
...u32(crc32([...typeBytes, ...data])),
|
||||
]
|
||||
}
|
||||
|
||||
function u32 (value: number) {
|
||||
return [
|
||||
(value >>> 24) & 0xff,
|
||||
(value >>> 16) & 0xff,
|
||||
(value >>> 8) & 0xff,
|
||||
value & 0xff,
|
||||
]
|
||||
}
|
||||
|
||||
function crc32 (bytes: number[]) {
|
||||
let crc = 0xffffffff
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8)
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0
|
||||
}
|
||||
|
||||
function adler32 (bytes: Uint8Array) {
|
||||
let a = 1
|
||||
let b = 0
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
a = (a + bytes[i]) % 65521
|
||||
b = (b + a) % 65521
|
||||
}
|
||||
return ((b << 16) | a) >>> 0
|
||||
}
|
||||
|
||||
function makeCrcTable () {
|
||||
const table = new Uint32Array(256)
|
||||
for (let n = 0; n < 256; n += 1) {
|
||||
let c = n
|
||||
for (let k = 0; k < 8; k += 1) {
|
||||
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1)
|
||||
}
|
||||
table[n] = c >>> 0
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
import { decryptResponse } from './crypto'
|
||||
import { resolveApiOrigin, resolveRequestHost } from './origin'
|
||||
import { signRequest } from './sign'
|
||||
import { getSession, getSessionHost, setSession } from './session'
|
||||
|
||||
export const REQUEST_TIMEOUT = 60000
|
||||
export const APP_VERSION = '3.0.0'
|
||||
export const CORP_ID = '10006'
|
||||
|
||||
type RequestOptions = {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
params?: Record<string, any>
|
||||
data?: any
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
export type BusinessResponse<T = any> = {
|
||||
code: number
|
||||
data: T
|
||||
message?: string
|
||||
msg?: string
|
||||
encrypt?: boolean
|
||||
}
|
||||
|
||||
let waitGroup = 0
|
||||
|
||||
function showLoading () {
|
||||
waitGroup += 1
|
||||
if (waitGroup === 1) {
|
||||
Taro.showLoading({ title: '加载中...', mask: true })
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading () {
|
||||
waitGroup = Math.max(0, waitGroup - 1)
|
||||
if (waitGroup === 0) {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function getResponseMessage (data: any) {
|
||||
const message = data?.message ?? data?.msg ?? '请求失败'
|
||||
return String(message).split(',')[0] || '请求失败'
|
||||
}
|
||||
|
||||
function parseResponseBody (data: any) {
|
||||
if (typeof data !== 'string') return data
|
||||
const text = data.trim()
|
||||
if (!text) return data
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
function toQuery (params: Record<string, any>) {
|
||||
return Object.keys(params)
|
||||
.map((key) => `${key}=${encodeURIComponent(params[key] ?? '')}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
function buildHeaders () {
|
||||
const session = getSession()
|
||||
const headers: Record<string, string> = {
|
||||
'content-type': 'application/json',
|
||||
'x-device-id': session.deviceId,
|
||||
'x-platform': 'wx-mp',
|
||||
'x-channel': 'wx-mp',
|
||||
'x-version': APP_VERSION,
|
||||
'x-host': resolveRequestHost({
|
||||
host: getSessionHost(),
|
||||
fallbackHost: process.env.TARO_APP_API_HOST,
|
||||
}),
|
||||
'b-corp-id': CORP_ID,
|
||||
}
|
||||
if (session.linkId) headers['b-link-id'] = session.linkId
|
||||
if (session.shareId) headers['b-share-id'] = session.shareId
|
||||
if (session.prevId) headers['b-prev-id'] = session.prevId
|
||||
if (session.token) headers['x-token'] = session.token
|
||||
if (session.packageId) headers['x-package'] = session.packageId
|
||||
return headers
|
||||
}
|
||||
|
||||
export async function request<T = any> (options: RequestOptions): Promise<BusinessResponse<T>> {
|
||||
const method = options.method || 'GET'
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
const nonce = `${timestamp}-${Math.random().toString(16).slice(2)}`
|
||||
const signed = signRequest({
|
||||
method,
|
||||
params: {
|
||||
...(options.params || {}),
|
||||
timestamp,
|
||||
nonce,
|
||||
},
|
||||
data: options.data,
|
||||
})
|
||||
const origin = resolveApiOrigin()
|
||||
const url = `${origin}${options.url}?${toQuery(signed.params)}`
|
||||
|
||||
if (!options.silent) showLoading()
|
||||
|
||||
try {
|
||||
const response = await Taro.request({
|
||||
url,
|
||||
method,
|
||||
data: method === 'GET' || method === 'DELETE' ? undefined : signed.data,
|
||||
header: buildHeaders(),
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
})
|
||||
|
||||
let payload = parseResponseBody(response.data) as BusinessResponse<T>
|
||||
if (payload && typeof payload === 'object' && payload.encrypt) {
|
||||
payload = decryptResponse((payload as any).data)
|
||||
}
|
||||
payload = parseResponseBody(payload)
|
||||
|
||||
if (!payload || payload.code !== 0) {
|
||||
const message = getResponseMessage(payload)
|
||||
if (!options.silent) {
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
if (payload?.code === 11022 || payload?.code === 1001003 || payload?.code === 1001004) {
|
||||
setSession({ token: '' })
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return payload
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message && error.message !== 'request:fail') {
|
||||
throw error
|
||||
}
|
||||
const message = '当前无法连接服务,请稍后再试'
|
||||
if (!options.silent) {
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
throw new Error(message)
|
||||
} finally {
|
||||
if (!options.silent) hideLoading()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
import { mergeServiceConfig } from './contact'
|
||||
import { pickGoodsType } from './detail'
|
||||
import { resolveLaunchQuery, type LaunchQuery } from './launch'
|
||||
import { hostFromOrigin, resolveApiOrigin } from './origin'
|
||||
import { createDeviceId } from './uuid'
|
||||
|
||||
const SESSION_KEY = 'corpMpSession'
|
||||
|
||||
export type SessionState = LaunchQuery & {
|
||||
deviceId: string
|
||||
config: Record<string, any>
|
||||
goodsType: string
|
||||
}
|
||||
|
||||
const EMPTY_SESSION: SessionState = {
|
||||
token: '',
|
||||
host: '',
|
||||
packageId: '',
|
||||
scene: '',
|
||||
linkId: '',
|
||||
shareId: '',
|
||||
prevId: '',
|
||||
phone: '',
|
||||
deviceId: '',
|
||||
config: {},
|
||||
goodsType: '',
|
||||
}
|
||||
|
||||
function readStorage (): Partial<SessionState> {
|
||||
try {
|
||||
const value = Taro.getStorageSync(SESSION_KEY)
|
||||
return value && typeof value === 'object' ? value : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function writeStorage (session: SessionState) {
|
||||
try {
|
||||
Taro.setStorageSync(SESSION_KEY, session)
|
||||
} catch {
|
||||
// ignore quota / private mode
|
||||
}
|
||||
}
|
||||
|
||||
let memorySession: SessionState | null = null
|
||||
|
||||
export function getSession (): SessionState {
|
||||
if (!memorySession) {
|
||||
const stored = readStorage()
|
||||
memorySession = {
|
||||
...EMPTY_SESSION,
|
||||
...stored,
|
||||
deviceId: stored.deviceId || createDeviceId(),
|
||||
}
|
||||
writeStorage(memorySession)
|
||||
}
|
||||
return memorySession
|
||||
}
|
||||
|
||||
export function setSession (patch: Partial<SessionState>) {
|
||||
memorySession = {
|
||||
...getSession(),
|
||||
...patch,
|
||||
}
|
||||
writeStorage(memorySession)
|
||||
return memorySession
|
||||
}
|
||||
|
||||
export function applyLaunchParams (raw: Record<string, string | undefined> = {}) {
|
||||
const launch = resolveLaunchQuery(raw)
|
||||
const current = getSession()
|
||||
return setSession({
|
||||
token: launch.token || current.token,
|
||||
host: launch.host || current.host,
|
||||
packageId: launch.packageId || current.packageId,
|
||||
scene: launch.scene || current.scene,
|
||||
linkId: launch.linkId || current.linkId,
|
||||
shareId: launch.shareId || current.shareId,
|
||||
prevId: launch.prevId || current.prevId,
|
||||
phone: launch.phone || current.phone,
|
||||
})
|
||||
}
|
||||
|
||||
export function getSessionHost () {
|
||||
const session = getSession()
|
||||
if (session.host) return session.host
|
||||
return process.env.TARO_APP_API_HOST || hostFromOrigin(resolveApiOrigin())
|
||||
}
|
||||
|
||||
export function applyServiceConfig (payload: any) {
|
||||
return setSession({
|
||||
config: mergeServiceConfig(getSession().config, payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function applyCorpInit (payload: any) {
|
||||
const data = payload?.data ?? payload
|
||||
const token = typeof data?.token === 'string' ? data.token.trim() : ''
|
||||
const packageId = data?.package === undefined || data?.package === null ? '' : String(data.package)
|
||||
const goodsType = pickGoodsType(payload)
|
||||
const patch: Partial<SessionState> = {
|
||||
config: mergeServiceConfig(getSession().config, payload),
|
||||
}
|
||||
if (token) patch.token = token
|
||||
if (packageId) patch.packageId = packageId
|
||||
if (goodsType) patch.goodsType = goodsType
|
||||
return setSession(patch)
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { signRequest } from './sign'
|
||||
|
||||
describe('signRequest', () => {
|
||||
it('signs GET query params the same way as H5', () => {
|
||||
const signed = signRequest({
|
||||
method: 'GET',
|
||||
params: {
|
||||
phone: '13800138000',
|
||||
timestamp: 1700000000,
|
||||
nonce: 'abc-nonce',
|
||||
},
|
||||
})
|
||||
|
||||
expect(signed.params.signature).toBe('170c9063021f3863d00c32b5ddffeb01')
|
||||
})
|
||||
|
||||
it('signs POST with query params plus JSON body', () => {
|
||||
const signed = signRequest({
|
||||
method: 'POST',
|
||||
params: {
|
||||
phone: '13800138000',
|
||||
timestamp: 1700000000,
|
||||
nonce: 'abc-nonce',
|
||||
},
|
||||
data: {
|
||||
id: '12',
|
||||
entity_name: '店',
|
||||
},
|
||||
})
|
||||
|
||||
expect(signed.params.signature).toBe('0b7bcf9a71df45452fda85a3a45b9a8a')
|
||||
})
|
||||
|
||||
it('treats PUT like POST', () => {
|
||||
const signed = signRequest({
|
||||
method: 'PUT',
|
||||
params: {
|
||||
timestamp: 1700000000,
|
||||
nonce: 'abc-nonce',
|
||||
},
|
||||
data: {
|
||||
id: '12',
|
||||
},
|
||||
})
|
||||
|
||||
expect(typeof signed.params.signature).toBe('string')
|
||||
expect(signed.params.signature).toHaveLength(32)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import MD5 from 'crypto-js/md5'
|
||||
|
||||
export const SIGN_KEY = 'MsvqoWCMxJsozicF5K4EFVSoVf8rEHfn'
|
||||
|
||||
type SignInput = {
|
||||
method?: string
|
||||
params?: Record<string, any>
|
||||
data?: any
|
||||
}
|
||||
|
||||
export function signRequest (input: SignInput) {
|
||||
const method = (input.method || 'GET').toUpperCase()
|
||||
const params = { ...(input.params || {}) }
|
||||
const data = input.data && typeof input.data === 'object' ? input.data : {}
|
||||
const keys = Object.keys(params).sort()
|
||||
const query = keys
|
||||
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
const secret = MD5(SIGN_KEY).toString()
|
||||
const source = method === 'POST' || method === 'PUT'
|
||||
? `${query}&${JSON.stringify(data)}&${secret}`
|
||||
: `${query}&${secret}`
|
||||
|
||||
return {
|
||||
params: {
|
||||
...params,
|
||||
signature: MD5(source).toString(),
|
||||
},
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
filterOrdersByTab,
|
||||
getOrderCardActions,
|
||||
getStatusMeta,
|
||||
ORDER_TABS,
|
||||
} from './status'
|
||||
|
||||
describe('order status labels', () => {
|
||||
it('uses the redesigned tab labels for known process statuses', () => {
|
||||
expect(getStatusMeta('4').result).toBe('办理中')
|
||||
expect(getStatusMeta('6').result).toBe('待处理')
|
||||
expect(getStatusMeta('1').result).toBe('待办理')
|
||||
expect(getStatusMeta('3').result).toBe('待办理')
|
||||
expect(getStatusMeta('2').result).toBe('已完成')
|
||||
expect(ORDER_TABS.map((tab) => tab.label)).toEqual(['全部', '办理中', '待处理', '待办理', '已完成'])
|
||||
})
|
||||
|
||||
it('uses filled badge colors from the order card design', () => {
|
||||
expect(getStatusMeta('4')).toMatchObject({ color: '#0261FC', bg: '#E5EFFE' })
|
||||
expect(getStatusMeta('6')).toMatchObject({ color: '#FC6202', bg: '#FEEFE5' })
|
||||
expect(getStatusMeta('1')).toMatchObject({ color: '#FF2E2E', bg: '#FFEAEA' })
|
||||
expect(getStatusMeta('2')).toMatchObject({ color: '#07C160', bg: '#E6F8EF' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('filterOrdersByTab', () => {
|
||||
const orders = [
|
||||
{ order_id: 'doing', process_status: '4' },
|
||||
{ order_id: 'rejected', process_status: '6' },
|
||||
{ order_id: 'waiting', process_status: '1' },
|
||||
{ order_id: 'waiting-3', process_status: '3' },
|
||||
{ order_id: 'done', process_status: '2' },
|
||||
]
|
||||
|
||||
it('filters list orders into the five redesigned tabs', () => {
|
||||
expect(filterOrdersByTab(orders, 'all').map((item) => item.order_id)).toEqual([
|
||||
'doing', 'rejected', 'waiting', 'waiting-3', 'done',
|
||||
])
|
||||
expect(filterOrdersByTab(orders, 'doing').map((item) => item.order_id)).toEqual(['doing'])
|
||||
expect(filterOrdersByTab(orders, 'rejected').map((item) => item.order_id)).toEqual(['rejected'])
|
||||
expect(filterOrdersByTab(orders, 'waiting').map((item) => item.order_id)).toEqual(['waiting', 'waiting-3'])
|
||||
expect(filterOrdersByTab(orders, 'done').map((item) => item.order_id)).toEqual(['done'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOrderCardActions', () => {
|
||||
it('shows complaint plus submit on 办理中 and 待处理', () => {
|
||||
expect(getOrderCardActions({ process_status: '4', goods_type: 'map_label' })).toEqual({
|
||||
complaint: true,
|
||||
submit: true,
|
||||
detail: false,
|
||||
})
|
||||
expect(getOrderCardActions({ process_status: '6', goods_type: 'map_label' })).toEqual({
|
||||
complaint: true,
|
||||
submit: true,
|
||||
detail: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps submit available on 待办理', () => {
|
||||
expect(getOrderCardActions({ process_status: '1', goods_type: 'map_label' })).toEqual({
|
||||
complaint: true,
|
||||
submit: true,
|
||||
detail: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps submit available on completed orders and hides complaint', () => {
|
||||
expect(getOrderCardActions({ process_status: '2', goods_type: 'map_label' })).toEqual({
|
||||
complaint: false,
|
||||
submit: true,
|
||||
detail: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('matches H5 actions for all three corp goods types', () => {
|
||||
for (const goodsType of ['license_year', 'license_destory', 'credit_repair']) {
|
||||
expect(getOrderCardActions({ process_status: '6', goods_type: goodsType })).toEqual({
|
||||
complaint: true,
|
||||
submit: true,
|
||||
detail: false,
|
||||
})
|
||||
expect(getOrderCardActions({ process_status: '4', goods_type: goodsType })).toEqual({
|
||||
complaint: false,
|
||||
submit: true,
|
||||
detail: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps submit available for every known and unknown status', () => {
|
||||
for (const status of ['', '1', '2', '3', '4', '5', '6', '7', '8', 'unknown']) {
|
||||
expect(getOrderCardActions({ process_status: status, goods_type: 'map_label' }).submit).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { isCorpGoodsType } from './detail'
|
||||
|
||||
export const STATUS_CONFIG: Record<string, { color: string, bg: string, result: string }> = {
|
||||
'4': { color: '#0261FC', bg: '#E5EFFE', result: '办理中' },
|
||||
'1': { color: '#FF2E2E', bg: '#FFEAEA', result: '待办理' },
|
||||
'3': { color: '#FF2E2E', bg: '#FFEAEA', result: '待办理' },
|
||||
'2': { color: '#07C160', bg: '#E6F8EF', result: '已完成' },
|
||||
'5': { color: '#07C160', bg: '#E6F8EF', result: '已完成' },
|
||||
'7': { color: '#07C160', bg: '#E6F8EF', result: '已完成' },
|
||||
'8': { color: '#07C160', bg: '#E6F8EF', result: '已完成' },
|
||||
'6': { color: '#FC6202', bg: '#FEEFE5', result: '待处理' },
|
||||
}
|
||||
|
||||
export const PAY_ICON: Record<string, string> = {
|
||||
alipay: 'https://cdn.u8t.cn/frontend-static/corp-h5/static/images/alipay.png',
|
||||
weixin: 'https://cdn.u8t.cn/frontend-static/corp-h5/static/images/weixin.png',
|
||||
}
|
||||
|
||||
export const ORDER_TABS = [
|
||||
{ key: 'all', label: '全部', statuses: null },
|
||||
{ key: 'doing', label: '办理中', statuses: ['4'] },
|
||||
{ key: 'rejected', label: '待处理', statuses: ['6'] },
|
||||
{ key: 'waiting', label: '待办理', statuses: ['1', '3'] },
|
||||
{ key: 'done', label: '已完成', statuses: ['2', '5', '7', '8'] },
|
||||
] as const
|
||||
|
||||
export type OrderTabKey = (typeof ORDER_TABS)[number]['key']
|
||||
|
||||
const DONE_STATUSES = new Set(['2', '5', '7', '8'])
|
||||
|
||||
export function getStatusMeta (status?: string, name?: string) {
|
||||
const config = STATUS_CONFIG[String(status || '')]
|
||||
if (config) return { ...config }
|
||||
return {
|
||||
color: '#666666',
|
||||
bg: '#F5F5F5',
|
||||
result: name || '未知',
|
||||
}
|
||||
}
|
||||
|
||||
export function filterOrdersByTab<T extends Record<string, any>> (orders: T[] = [], tab: OrderTabKey) {
|
||||
const current = ORDER_TABS.find((item) => item.key === tab)
|
||||
if (!current || !current.statuses) return orders
|
||||
const statuses = new Set<string>(current.statuses)
|
||||
return orders.filter((order) => statuses.has(String(order.process_status || '')))
|
||||
}
|
||||
|
||||
export function getOrderCardActions (order: { process_status?: string, goods_type?: string }) {
|
||||
const status = String(order.process_status || '')
|
||||
const corpGoods = isCorpGoodsType(order.goods_type)
|
||||
const detail = corpGoods ? status !== '6' : DONE_STATUSES.has(status)
|
||||
return {
|
||||
complaint: !detail,
|
||||
submit: true,
|
||||
detail,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export function createDeviceId () {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
||||
const random = Math.floor(Math.random() * 16)
|
||||
const value = char === 'x' ? random : (random & 0x3) | 0x8
|
||||
return value.toString(16)
|
||||
})
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export default definePageConfig({ navigationBarTitleText: '投诉' })
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import ComplaintPage, { type ComplaintServices } from '@/pages/complaint/index'
|
||||
import { a5Request, getA5Draft, uploadA5Image } from '@/lib/a5/api'
|
||||
|
||||
const services: ComplaintServices = {
|
||||
getDraft: () => getA5Draft()?.order || null,
|
||||
upload: (path, onProgress) => {
|
||||
const draft = getA5Draft()
|
||||
return uploadA5Image(path, draft?.linkId || '', String(draft?.order.order_id || ''), 'process', undefined, onProgress)
|
||||
},
|
||||
submit: (data) => a5Request('/api/user/feedback', { method: 'POST', data }),
|
||||
}
|
||||
export default function A5Complaint () { return <ComplaintPage services={services} /> }
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import { Image, Text, View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getA5Files } from '@/lib/a5/api'
|
||||
import type { A5File } from '@/lib/a5/data'
|
||||
import { LOCAL_IMAGES } from '@/lib/assets'
|
||||
import type { OrderLike } from '@/lib/order'
|
||||
|
||||
const PLATFORMS = [
|
||||
{ scene: 'order_process_gd', name: '高德地图' },
|
||||
{ scene: 'order_process_bd', name: '百度地图' },
|
||||
{ scene: 'order_process_tx', name: '腾讯地图' },
|
||||
]
|
||||
export default function A5Detail ({ order, linkId, onCancel }: { order: OrderLike, linkId: string, onCancel: () => void }) {
|
||||
return (
|
||||
<View className='detail-mask' onClick={onCancel}>
|
||||
<View className='detail-drawer' onClick={(event) => event.stopPropagation()}>
|
||||
<View className='detail-dialog__head'>
|
||||
<Text className='detail-dialog__title'>详情</Text>
|
||||
<View className='detail-dialog__close' onClick={onCancel}><Image className='detail-dialog__close-img' src={LOCAL_IMAGES.close} /></View>
|
||||
</View>
|
||||
<View className='detail-dialog__body'>
|
||||
<Text className='detail-dialog__section-title'>完成截图</Text>
|
||||
{order.process_status === '2' ? PLATFORMS.map((platform) => (
|
||||
<PlatformFiles key={`${order.order_id}-${platform.scene}`} orderId={String(order.order_id)} linkId={linkId} {...platform} />
|
||||
)) : <Text className='detail-dialog__empty'>暂无完成截图</Text>}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
function PlatformFiles ({ orderId, linkId, scene, name }: { orderId: string, linkId: string, scene: string, name: string }) {
|
||||
const [files, setFiles] = useState<A5File[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [attempt, setAttempt] = useState(0)
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
setLoading(true)
|
||||
setError('')
|
||||
void getA5Files(linkId, orderId, scene).then((result) => {
|
||||
if (active) setFiles(result)
|
||||
}).catch((reason) => {
|
||||
if (active) setError(reason instanceof Error ? reason.message : '截图加载失败')
|
||||
}).finally(() => { if (active) setLoading(false) })
|
||||
return () => { active = false }
|
||||
}, [linkId, orderId, scene, attempt])
|
||||
const isDocument = (file: A5File) => file.fileType === 'document' || /\.pdf(?:\?|$)/i.test(file.url)
|
||||
const images = files.filter((file) => !isDocument(file))
|
||||
return (
|
||||
<View className='detail-dialog__section'>
|
||||
<Text className='detail-dialog__section-title'>{name}</Text>
|
||||
{loading ? <Text>截图加载中...</Text> : error ? (
|
||||
<View onClick={() => setAttempt((value) => value + 1)}><Text>{error},点击重试</Text></View>
|
||||
) : !files.length ? <Text className='detail-dialog__empty'>暂无完成截图</Text> : <>
|
||||
{images.map((file) => <Image key={file.id || file.url} className='detail-dialog__shot' src={file.url} mode='widthFix' onClick={() => Taro.previewImage({ current: file.url, urls: images.map((image) => image.url) })} />)}
|
||||
{files.filter(isDocument).map((file) => <View key={file.id || file.url} className='detail-dialog__pdf' onClick={() => void openDocument(file)}><Text>{file.name || '报告文件'}</Text></View>)}
|
||||
</>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
async function openDocument (file: A5File) {
|
||||
try {
|
||||
if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) {
|
||||
await Taro.setClipboardData({ data: file.url })
|
||||
return
|
||||
}
|
||||
Taro.showLoading({ title: '打开中...' })
|
||||
const response = await Taro.downloadFile({ url: file.url })
|
||||
if (response.statusCode !== 200) throw new Error('下载失败')
|
||||
await Taro.openDocument({ filePath: response.tempFilePath, showMenu: true })
|
||||
} catch { Taro.showToast({ title: '报告打开失败', icon: 'none' }) } finally { Taro.hideLoading() }
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export default definePageConfig({ navigationStyle: 'custom', backgroundColor: '#F6F8FC' })
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import Index from '@/pages/index/index'
|
||||
|
||||
export default function A5Page () { return <Index a5 /> }
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export default definePageConfig({
|
||||
navigationBarTitleText: '重新提交',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#F7F8FA',
|
||||
})
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
.resubmit-page--a5 {
|
||||
.resubmit-card { width: 100%; }
|
||||
.resubmit__control-value, .resubmit__error { overflow-wrap: anywhere; }
|
||||
}
|
||||
.a5-doc {
|
||||
margin-bottom: 28px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
&__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
|
||||
&__header .resubmit__label { margin-bottom: 0; }
|
||||
&__example { flex-shrink: 0; font-size: 24px; color: #165dff; }
|
||||
&__loading { display: block; margin-bottom: 16px; font-size: 24px; color: #86909c; }
|
||||
&__grid { display: flex; gap: 20px; }
|
||||
&__item { flex: 1; min-width: 0; max-width: calc((100% - 40px) / 3); text-align: center; }
|
||||
&__media { width: 100%; aspect-ratio: 1; box-sizing: border-box; background: #fff; border: 2px solid #e5e6eb; border-radius: 16px; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; }
|
||||
&__image { width: 100%; height: 100%; }
|
||||
&__upload { width: 100%; height: 100%; margin: 0; padding: 12px 0; display: flex; flex-direction: column; justify-content: center; align-items: center; background: transparent; font-size: 22px; line-height: 1.5; color: #c9cdd4; }
|
||||
&__upload[disabled] { background: transparent; color: #c9cdd4; }
|
||||
&__upload::after, &__remove::after { border: 0; }
|
||||
&__camera { width: 56px; height: 56px; margin-bottom: 8px; }
|
||||
&__remove { position: absolute; top: 0; right: 0; width: 48px; height: 48px; padding: 0; line-height: 44px; border-radius: 0 16px 0 16px; background: rgba(29, 33, 41, .55); color: #fff; font-size: 32px; }
|
||||
&__caption { display: block; font-size: 24px; color: #1d2129; margin-top: 12px; }
|
||||
&__progress { position: absolute; bottom: 0; left: 0; right: 0; background: #165dff; color: white; text-align: center; font-size: 22px; }
|
||||
}
|
||||
.a5-example {
|
||||
position: fixed; inset: 0; z-index: 1200; background: rgba(0, 0, 0, .65); display: flex; align-items: center; justify-content: center; padding: 28px; box-sizing: border-box;
|
||||
&__dialog { width: 100%; max-height: 85vh; overflow-y: auto; padding: 28px; background: white; border-radius: 16px; box-sizing: border-box; }
|
||||
&__title { display: block; text-align: center; font-size: 32px; font-weight: 600; margin: 8px 0 28px; }
|
||||
&__image { width: 100%; height: 180px; }
|
||||
&__subtitle { display: block; font-size: 28px; font-weight: 600; margin: 28px 0 16px; }
|
||||
&__requirements { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; font-size: 24px; color: #666; }
|
||||
.a5-doc__item { max-width: none; }
|
||||
.resubmit__submit { margin-top: 28px; }
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
import { Button, Text, View } from '@tarojs/components'
|
||||
import Taro, { useDidShow, useLoad } from '@tarojs/taro'
|
||||
import { useRef, useState } from 'react'
|
||||
import { a5Request, deleteA5File, getA5Draft, getA5Files, uploadA5Image, type A5Draft } from '@/lib/a5/api'
|
||||
import { buildA5Update, STOREFRONT_LOCS, storefrontFromFiles } from '@/lib/a5/data'
|
||||
import { formatMapCoordinate, getInitialPhone2, getOrderAddressName, getOrderExtraImages, getOrderMapAddress, parseCoordinate, parseStorefrontSlots, type ResubmitValues, type StorefrontSlot } from '@/lib/order'
|
||||
import { isValidPhone } from '@/lib/phone'
|
||||
import { consumeMapPickerResult } from '@/pages/map/picker'
|
||||
import PrivacyConsent from '@/components/privacy-consent'
|
||||
import { chooseA5Image, imageErrorMessage } from '@/lib/a5/image'
|
||||
import { hasPrivacyConsent, savePrivacyConsent } from '@/lib/consent'
|
||||
import { MapResubmitFields, ResubmitNotes } from '@/pages/resubmit/fields'
|
||||
import '@/pages/resubmit/index.scss'
|
||||
import Documents from './documents'
|
||||
import './index.scss'
|
||||
|
||||
export default function A5Resubmit () {
|
||||
const [draft, setDraft] = useState<A5Draft | null>(null)
|
||||
const [values, setValues] = useState<ResubmitValues>({})
|
||||
const [slots, setSlots] = useState<StorefrontSlot[]>(Array(6).fill(null))
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filesReady, setFilesReady] = useState(false)
|
||||
const [busy, setBusy] = useState<number | null>(null)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loadError, setLoadError] = useState('')
|
||||
const [agreed, setAgreed] = useState(hasPrivacyConsent)
|
||||
const lock = useRef(false)
|
||||
const returningMap = useRef(false)
|
||||
|
||||
async function loadFiles (current: A5Draft) {
|
||||
setLoading(true)
|
||||
setFilesReady(false)
|
||||
setLoadError('')
|
||||
try {
|
||||
const [storefront, license, identity] = await Promise.all(['order_biz2', 'order_biz1', 'order_biz3'].map((scene) => getA5Files(current.linkId, String(current.order.order_id), scene)))
|
||||
setSlots((previous) => [
|
||||
...(storefront.length ? storefrontFromFiles(storefront) : previous.slice(0, 3)),
|
||||
...(license.length ? [license[license.length - 1]] : previous.slice(3, 4)),
|
||||
...(identity.length ? [identity.slice(-2)[0] || null, identity.slice(-2)[1] || null] : previous.slice(4, 6)),
|
||||
])
|
||||
setFilesReady(true)
|
||||
} catch (reason) {
|
||||
setLoadError(imageErrorMessage(reason, '图片加载失败'))
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
useLoad(() => {
|
||||
const current = getA5Draft()
|
||||
if (!current) {
|
||||
setLoading(false)
|
||||
setError('订单不存在,请返回查询页重新进入')
|
||||
return
|
||||
}
|
||||
consumeMapPickerResult()
|
||||
setDraft(current)
|
||||
const order = current.order
|
||||
setValues({ entity_name: order.entity_name || '', entity_phone: order.entity_phone || '', entity_phone2: getInitialPhone2(order), entity_address_name: getOrderAddressName(order), entity_address: getOrderMapAddress(order) })
|
||||
const license = getOrderExtraImages(order, ['entity_business_license_image', 'business_license_image'])
|
||||
const identity = getOrderExtraImages(order, ['entity_identity_card_image', 'identity_card_image'])
|
||||
setSlots([...parseStorefrontSlots(order), license[0] || null, identity[0] || null, identity[1] || null])
|
||||
void loadFiles(current)
|
||||
})
|
||||
useDidShow(() => {
|
||||
if (!returningMap.current) return
|
||||
returningMap.current = false
|
||||
const point = consumeMapPickerResult()
|
||||
if (point) setValues((current) => ({ ...current, entity_address: formatMapCoordinate(point), entity_address_name: point.address || point.name || current.entity_address_name }))
|
||||
})
|
||||
const disabled = loading || busy !== null || submitting
|
||||
const update = (field: keyof ResubmitValues, value: string) => setValues((current) => ({ ...current, [field]: value }))
|
||||
async function upload (index: number) {
|
||||
if (!draft || disabled || !filesReady || lock.current) return
|
||||
lock.current = true
|
||||
try {
|
||||
setError('')
|
||||
const path = await chooseA5Image()
|
||||
if (!path) return
|
||||
setBusy(index)
|
||||
setProgress(0)
|
||||
setError('')
|
||||
const uploaded = await uploadA5Image(path, draft.linkId, String(draft.order.order_id), index < 3 ? 'order_biz2' : index === 3 ? 'order_biz1' : 'order_biz3', index < 3 ? { loc: STOREFRONT_LOCS[index] } : undefined, setProgress)
|
||||
setSlots((current) => current.map((slot, position) => position === index ? { ...uploaded, preview: path } : slot))
|
||||
} catch (reason) {
|
||||
setError(imageErrorMessage(reason))
|
||||
} finally { setBusy(null); lock.current = false }
|
||||
}
|
||||
async function remove (index: number) {
|
||||
if (disabled || !filesReady || lock.current) return
|
||||
lock.current = true
|
||||
try {
|
||||
const confirmation = await Taro.showModal({ title: '删除照片', content: '确定删除这张照片?' })
|
||||
if (!confirmation.confirm) return
|
||||
setBusy(index)
|
||||
setProgress(0)
|
||||
if (slots[index]?.id) await deleteA5File(slots[index]!.id)
|
||||
setSlots((current) => current.map((slot, position) => position === index ? null : slot))
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : '删除失败') } finally { setBusy(null); lock.current = false }
|
||||
}
|
||||
async function submit () {
|
||||
if (!draft || disabled || !filesReady || lock.current) return
|
||||
const invalid = !agreed ? '请先阅读并同意协议'
|
||||
: !values.entity_name?.trim() ? '请输入门店名称'
|
||||
: !values.entity_address_name?.trim() ? '请输入实际经营地址'
|
||||
: !values.entity_address ? '请选择门店位置'
|
||||
: !isValidPhone(values.entity_phone || '') ? '请填写正确的门店电话'
|
||||
: !isValidPhone(values.entity_phone2 || '') ? '请填写正确的联系人电话'
|
||||
: slots.slice(0, 3).some((slot) => !slot?.id) ? '请上传门头正面、左面和右面照片' : ''
|
||||
if (invalid) { Taro.showToast({ title: invalid, icon: 'none' }); return }
|
||||
lock.current = true
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await a5Request('/h5/order', { method: 'PUT', data: buildA5Update(draft.order, values, slots) })
|
||||
Taro.showToast({ title: '重新提交成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 500)
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : '重新提交失败') } finally { setSubmitting(false); lock.current = false }
|
||||
}
|
||||
function chooseMap () {
|
||||
const point = parseCoordinate(values.entity_address)
|
||||
consumeMapPickerResult()
|
||||
returningMap.current = true
|
||||
void Taro.navigateTo({ url: `/pages/map/index?name=${encodeURIComponent(values.entity_address_name || '')}${point ? `&lng=${point.lng}&lat=${point.lat}` : ''}` })
|
||||
}
|
||||
return <View className='resubmit-page resubmit-page--a5'>
|
||||
{draft?.order.reject_reason ? <View className='resubmit-page__reject'>驳回理由:{draft.order.reject_reason}</View> : null}
|
||||
<View className='resubmit-page__body resubmit-page__body--consent'>
|
||||
<View className='resubmit-card'>
|
||||
<MapResubmitFields values={values} onChange={update} onChooseMap={chooseMap} />
|
||||
{loading ? <Text className='a5-doc__loading'>图片加载中...</Text> : null}
|
||||
<Documents slots={slots} busy={busy} progress={progress} disabled={disabled || !filesReady} onUpload={(index) => void upload(index)} onRemove={(index) => void remove(index)} />
|
||||
</View>
|
||||
<ResubmitNotes />
|
||||
{loadError ? <View className='resubmit__error'>{loadError}{draft ? <Text onClick={() => !disabled && void loadFiles(draft)}> 重新加载资料</Text> : null}</View> : null}
|
||||
{error ? <View className='resubmit__error'>{error}</View> : null}
|
||||
</View>
|
||||
<View className='resubmit-page__footer resubmit-page__footer--consent'>
|
||||
<PrivacyConsent className='resubmit__privacy' agreed={agreed} onChange={(value) => { setAgreed(value); savePrivacyConsent(value) }} />
|
||||
<Button className='resubmit__submit' loading={submitting} disabled={disabled || !filesReady || !draft} onClick={() => void submit()}>立即提交</Button>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import { Button, Image, Input, Text, Textarea, View } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { report, uploadImage } from '@/api/user'
|
||||
import { a5Request, getA5Draft, uploadA5Image } from '@/lib/a5/api'
|
||||
import { LOCAL_IMAGES } from '@/lib/assets'
|
||||
import { buildFeedbackPayload, COMPLAINT_TYPES, getFeedbackImageSrc, type ComplaintType } from '@/lib/feedback'
|
||||
import { getOrderDraft } from '@/lib/orderDraft'
|
||||
import { getOrderDisplayName, type OrderLike, type UploadedImage } from '@/lib/order'
|
||||
import './index.scss'
|
||||
|
||||
|
|
@ -31,13 +30,7 @@ const TYPE_ICONS: Record<ComplaintType, { idle: string, active: string }> = {
|
|||
},
|
||||
}
|
||||
|
||||
export type ComplaintServices = {
|
||||
getDraft: () => OrderLike | null
|
||||
upload: typeof uploadImage
|
||||
submit: typeof report
|
||||
}
|
||||
|
||||
export default function ComplaintPage ({ services }: { services?: ComplaintServices }) {
|
||||
export default function ComplaintPage () {
|
||||
const [order, setOrder] = useState<OrderLike | null>(null)
|
||||
const [type, setType] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
|
|
@ -47,7 +40,7 @@ export default function ComplaintPage ({ services }: { services?: ComplaintServi
|
|||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useLoad(() => {
|
||||
const draft = services ? services.getDraft() : getOrderDraft()
|
||||
const draft = getA5Draft()?.order
|
||||
if (!draft) {
|
||||
Taro.showToast({ title: '订单不存在', icon: 'none' })
|
||||
setTimeout(() => Taro.navigateBack(), 400)
|
||||
|
|
@ -73,7 +66,8 @@ export default function ComplaintPage ({ services }: { services?: ComplaintServi
|
|||
].slice(0, 5))
|
||||
setUploading(true)
|
||||
try {
|
||||
const result = await (services?.upload || uploadImage)(filePath)
|
||||
const draft = getA5Draft()
|
||||
const result = await uploadA5Image(filePath, draft?.linkId || '', String(draft?.order.order_id || ''), 'process')
|
||||
setImages((current) => current.map((image) => (
|
||||
image.preview === filePath
|
||||
? { id: result.id, url: result.url, preview: filePath }
|
||||
|
|
@ -107,13 +101,13 @@ export default function ComplaintPage ({ services }: { services?: ComplaintServi
|
|||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await (services?.submit || report)(buildFeedbackPayload({
|
||||
await a5Request('/api/user/feedback', { method: 'POST', data: buildFeedbackPayload({
|
||||
contact: contact.trim(),
|
||||
content: content.trim(),
|
||||
images: images.map((image) => image.url),
|
||||
type,
|
||||
order,
|
||||
}))
|
||||
}) })
|
||||
Taro.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 500)
|
||||
} catch (error) {
|
||||
|
|
@ -132,8 +126,8 @@ export default function ComplaintPage ({ services }: { services?: ComplaintServi
|
|||
</View>
|
||||
<View className='complaint-hero__meta'>
|
||||
<View className='complaint-hero__meta-row'>
|
||||
<Text className='complaint-hero__meta-label'>{services ? '订单编号' : '交易ID'}</Text>
|
||||
<Text className='complaint-hero__meta-value'>{(services ? order?.order_id : order?.out_trade_no) || '-'}</Text>
|
||||
<Text className='complaint-hero__meta-label'>订单编号</Text>
|
||||
<Text className='complaint-hero__meta-value'>{order?.order_id || '-'}</Text>
|
||||
</View>
|
||||
<View className='complaint-hero__meta-row'>
|
||||
<Text className='complaint-hero__meta-label'>交易时间</Text>
|
||||
|
|
|
|||
|
|
@ -1,202 +1,73 @@
|
|||
import { Image, Text, View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getA5Files } from '@/lib/a5/api'
|
||||
import type { A5File } from '@/lib/a5/data'
|
||||
import { LOCAL_IMAGES } from '@/lib/assets'
|
||||
import { RECEIPT_STAMP_DONE, RECEIPT_STAMP_PENDING } from '@/lib/cdn'
|
||||
import {
|
||||
getReceiptMeta,
|
||||
numberToUpperCase,
|
||||
shouldShowReceipt,
|
||||
splitOrderFiles,
|
||||
type OrderFile,
|
||||
} from '@/lib/detail'
|
||||
import type { OrderLike } from '@/lib/order'
|
||||
|
||||
const DRAWER_ANIMATION_MS = 300
|
||||
|
||||
type Props = {
|
||||
order: OrderLike
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function DetailDrawer ({ order, onCancel }: Props) {
|
||||
const [leaving, setLeaving] = useState(false)
|
||||
const onCancelRef = useRef(onCancel)
|
||||
onCancelRef.current = onCancel
|
||||
|
||||
const showReceipt = shouldShowReceipt(order.goods_type)
|
||||
const receipt = getReceiptMeta(order.goods_type, order.goods_name)
|
||||
const files = splitOrderFiles(order.files)
|
||||
const empty = !showReceipt && files.images.length === 0 && files.pdfs.length === 0
|
||||
const imageUrls = files.images.map((file) => String(file.url))
|
||||
|
||||
const handleClose = () => {
|
||||
if (leaving) return
|
||||
setLeaving(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!leaving) return
|
||||
const timer = setTimeout(() => onCancelRef.current(), DRAWER_ANIMATION_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}, [leaving])
|
||||
|
||||
const PLATFORMS = [
|
||||
{ scene: 'order_process_gd', name: '高德地图' },
|
||||
{ scene: 'order_process_bd', name: '百度地图' },
|
||||
{ scene: 'order_process_tx', name: '腾讯地图' },
|
||||
]
|
||||
export default function A5Detail ({ order, linkId, onCancel }: { order: OrderLike, linkId: string, onCancel: () => void }) {
|
||||
return (
|
||||
<View
|
||||
className={`detail-mask${leaving ? ' detail-mask--leave' : ''}`}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<View
|
||||
className={`detail-drawer${leaving ? ' detail-drawer--leave' : ''}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<View className='detail-drawer__handle' />
|
||||
<View className='detail-mask' onClick={onCancel}>
|
||||
<View className='detail-drawer' onClick={(event) => event.stopPropagation()}>
|
||||
<View className='detail-dialog__head'>
|
||||
<Text className='detail-dialog__title'>详情</Text>
|
||||
<View className='detail-dialog__close' onClick={handleClose}>
|
||||
<Image className='detail-dialog__close-img' src={LOCAL_IMAGES.close} />
|
||||
</View>
|
||||
<View className='detail-dialog__close' onClick={onCancel}><Image className='detail-dialog__close-img' src={LOCAL_IMAGES.close} /></View>
|
||||
</View>
|
||||
|
||||
<View className='detail-dialog__body'>
|
||||
{showReceipt ? <ReceiptCard order={order} title={receipt.title} description={receipt.description} /> : null}
|
||||
|
||||
{files.images.length ? (
|
||||
<View className='detail-dialog__section'>
|
||||
<Text className='detail-dialog__section-title'>完成截图:</Text>
|
||||
{files.images.map((file, index) => (
|
||||
<Image
|
||||
key={file.id || file.url || index}
|
||||
className='detail-dialog__shot'
|
||||
src={String(file.url)}
|
||||
mode='widthFix'
|
||||
onClick={() => previewImages(imageUrls, String(file.url))}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{files.pdfs.length ? (
|
||||
<View className='detail-dialog__section'>
|
||||
<Text className='detail-dialog__section-title'>报告文件:</Text>
|
||||
{files.pdfs.map((file, index) => (
|
||||
<View
|
||||
key={file.id || file.url || index}
|
||||
className='detail-dialog__pdf'
|
||||
onClick={() => void openPdf(file)}
|
||||
>
|
||||
<Text className='detail-dialog__pdf-name'>查看报告文件{files.pdfs.length > 1 ? index + 1 : ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{empty ? (
|
||||
<Text className='detail-dialog__empty'>暂无完成截图</Text>
|
||||
) : null}
|
||||
<Text className='detail-dialog__section-title'>完成截图</Text>
|
||||
{order.process_status === '2' ? PLATFORMS.map((platform) => (
|
||||
<PlatformFiles key={`${order.order_id}-${platform.scene}`} orderId={String(order.order_id)} linkId={linkId} {...platform} />
|
||||
)) : <Text className='detail-dialog__empty'>暂无完成截图</Text>}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ReceiptCard ({
|
||||
order,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
order: OrderLike
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
const stamp = order.process_status === '2' ? RECEIPT_STAMP_DONE : RECEIPT_STAMP_PENDING
|
||||
const remarkLines = description.split('\n').filter(Boolean)
|
||||
|
||||
function PlatformFiles ({ orderId, linkId, scene, name }: { orderId: string, linkId: string, scene: string, name: string }) {
|
||||
const [files, setFiles] = useState<A5File[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [attempt, setAttempt] = useState(0)
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
setLoading(true)
|
||||
setError('')
|
||||
void getA5Files(linkId, orderId, scene).then((result) => {
|
||||
if (active) setFiles(result)
|
||||
}).catch((reason) => {
|
||||
if (active) setError(reason instanceof Error ? reason.message : '截图加载失败')
|
||||
}).finally(() => { if (active) setLoading(false) })
|
||||
return () => { active = false }
|
||||
}, [linkId, orderId, scene, attempt])
|
||||
const isDocument = (file: A5File) => file.fileType === 'document' || /\.pdf(?:\?|$)/i.test(file.url)
|
||||
const images = files.filter((file) => !isDocument(file))
|
||||
return (
|
||||
<View className='detail-dialog__section'>
|
||||
<Text className='detail-dialog__section-title'>回执单:</Text>
|
||||
<View className='detail-receipt'>
|
||||
<Text className='detail-receipt__title'>{title}</Text>
|
||||
<View className='detail-receipt__rule' />
|
||||
<View className='detail-receipt__rule detail-receipt__rule--thin' />
|
||||
|
||||
<View className='detail-receipt__box'>
|
||||
<View className='detail-receipt__inner'>
|
||||
<View className='detail-receipt__fields'>
|
||||
<Image className='detail-receipt__stamp' src={stamp} />
|
||||
<ReceiptLine label='名称:' value={order.entity_name || ''} />
|
||||
<ReceiptLine
|
||||
label='法人:'
|
||||
value={`${order.entity_legal || ''}${order.entity_phone ? ` (${order.entity_phone})` : ''}`}
|
||||
/>
|
||||
<ReceiptLine label='统一社会信用代码:' value={order.entity_id || ''} />
|
||||
<ReceiptLine label='已受理时间:' value={order.pay_time || ''} />
|
||||
<ReceiptLine label='申报内容:' value={order.goods_name || ''} />
|
||||
<ReceiptLine label='数量:' value='1' />
|
||||
<ReceiptLine label='金额(小写):' value={formatAmount(order.total_fee)} />
|
||||
<ReceiptLine label='金额(大写):' value={numberToUpperCase(order.total_fee)} />
|
||||
<ReceiptLine label='支付时间:' value={order.pay_time || ''} />
|
||||
<ReceiptLine label='订单号码:' value={order.out_trade_no || ''} />
|
||||
</View>
|
||||
<View className='detail-receipt__remark'>
|
||||
<View className='detail-receipt__remark-label'>
|
||||
<Text className='detail-receipt__remark-label-text'>备注</Text>
|
||||
</View>
|
||||
<View className='detail-receipt__remark-body'>
|
||||
{remarkLines.map((line, index) => (
|
||||
<Text key={`${index}-${line}`} className='detail-receipt__remark-line'>{line}</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='detail-dialog__section-title'>{name}</Text>
|
||||
{loading ? <Text>截图加载中...</Text> : error ? (
|
||||
<View onClick={() => setAttempt((value) => value + 1)}><Text>{error},点击重试</Text></View>
|
||||
) : !files.length ? <Text className='detail-dialog__empty'>暂无完成截图</Text> : <>
|
||||
{images.map((file) => <Image key={file.id || file.url} className='detail-dialog__shot' src={file.url} mode='widthFix' onClick={() => Taro.previewImage({ current: file.url, urls: images.map((image) => image.url) })} />)}
|
||||
{files.filter(isDocument).map((file) => <View key={file.id || file.url} className='detail-dialog__pdf' onClick={() => void openDocument(file)}><Text>{file.name || '报告文件'}</Text></View>)}
|
||||
</>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function formatAmount (amount: unknown) {
|
||||
if (amount === '' || amount === undefined || amount === null) return ''
|
||||
const value = Number(amount)
|
||||
return Number.isFinite(value) ? `¥${value.toFixed(2)}` : `¥${String(amount)}`
|
||||
}
|
||||
|
||||
function ReceiptLine ({ label, value }: { label: string, value: string }) {
|
||||
return (
|
||||
<View className='detail-receipt__row'>
|
||||
<Text className='detail-receipt__label'>{label}</Text>
|
||||
<Text className='detail-receipt__value'>{value}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function previewImages (urls: string[], current: string) {
|
||||
if (!urls.length) return
|
||||
Taro.previewImage({ current, urls })
|
||||
}
|
||||
|
||||
async function openPdf (file: OrderFile) {
|
||||
const url = String(file.url || '')
|
||||
if (!url) return
|
||||
|
||||
async function openDocument (file: A5File) {
|
||||
try {
|
||||
Taro.showLoading({ title: '打开中...', mask: true })
|
||||
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) {
|
||||
const downloaded = await Taro.downloadFile({ url })
|
||||
if (downloaded.statusCode !== 200 || !downloaded.tempFilePath) {
|
||||
throw new Error('download failed')
|
||||
}
|
||||
await Taro.openDocument({
|
||||
filePath: downloaded.tempFilePath,
|
||||
fileType: 'pdf',
|
||||
showMenu: true,
|
||||
})
|
||||
if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) {
|
||||
await Taro.setClipboardData({ data: file.url })
|
||||
return
|
||||
}
|
||||
Taro.setClipboardData({ data: url })
|
||||
} catch {
|
||||
Taro.showToast({ title: '报告打开失败', icon: 'none' })
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
Taro.showLoading({ title: '打开中...' })
|
||||
const response = await Taro.downloadFile({ url: file.url })
|
||||
if (response.statusCode !== 200) throw new Error('下载失败')
|
||||
await Taro.openDocument({ filePath: response.tempFilePath, showMenu: true })
|
||||
} catch { Taro.showToast({ title: '报告打开失败', icon: 'none' }) } finally { Taro.hideLoading() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1 @@
|
|||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#F6F8FC',
|
||||
backgroundColor: '#F6F8FC',
|
||||
backgroundColorTop: '#F6F8FC',
|
||||
})
|
||||
export default definePageConfig({ navigationStyle: 'custom', backgroundColor: '#F6F8FC' })
|
||||
|
|
|
|||
|
|
@ -415,112 +415,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-receipt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-receipt__title {
|
||||
color: #1a1a1a;
|
||||
font-size: 36px;
|
||||
line-height: 52px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-receipt__rule {
|
||||
width: 280px;
|
||||
height: 4px;
|
||||
margin-top: 8px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.detail-receipt__rule--thin {
|
||||
height: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.detail-receipt__box {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
padding: 6px;
|
||||
border: 2px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.detail-receipt__inner {
|
||||
border: 2px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.detail-receipt__fields {
|
||||
position: relative;
|
||||
padding: 28px 20px 20px;
|
||||
}
|
||||
|
||||
.detail-receipt__stamp {
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
right: 8px;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.detail-receipt__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-receipt__label {
|
||||
flex-shrink: 0;
|
||||
color: #1a1a1a;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.detail-receipt__value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #1a1a1a;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-receipt__remark {
|
||||
display: flex;
|
||||
border-top: 2px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.detail-receipt__remark-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
border-right: 2px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.detail-receipt__remark-label-text {
|
||||
color: #1a1a1a;
|
||||
font-size: 22px;
|
||||
writing-mode: vertical-lr;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.detail-receipt__remark-body {
|
||||
flex: 1;
|
||||
padding: 16px 16px 20px;
|
||||
}
|
||||
|
||||
.detail-receipt__remark-line {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 22px;
|
||||
line-height: 32px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.contact-fab {
|
||||
position: fixed;
|
||||
right: 32px;
|
||||
|
|
|
|||
|
|
@ -3,29 +3,18 @@ import Taro, { useDidShow, useLoad } from '@tarojs/taro'
|
|||
import { useMemo, useRef, useState } from 'react'
|
||||
import { getA5Config, getA5Orders, setA5Draft } from '@/lib/a5/api'
|
||||
import { A5_STATUSES, A5_TABS, parseA5Launch } from '@/lib/a5/data'
|
||||
import A5Detail from '@/pages/a5/detail'
|
||||
import { getOrdersByPhone } from '@/api/pay'
|
||||
import { clearQueryPhone, getQueryPhone, saveQueryPhone } from '@/lib/a5/queryPhone'
|
||||
import A5Detail from './detail'
|
||||
import { LOCAL_IMAGES } from '@/lib/assets'
|
||||
import { bootstrapSession } from '@/lib/bootstrap'
|
||||
import { COPY_ICON, PAGE_TITLE } from '@/lib/cdn'
|
||||
import { getNavMetrics } from '@/lib/nav'
|
||||
import { pickServiceContact, type ServiceContact } from '@/lib/contact'
|
||||
import { setOrderDraft } from '@/lib/orderDraft'
|
||||
import { filterOrdersForProduct, getOrderDisplayName, type OrderLike } from '@/lib/order'
|
||||
import { getOrderDisplayName, type OrderLike } from '@/lib/order'
|
||||
import { isValidPhone } from '@/lib/phone'
|
||||
import { getSession } from '@/lib/session'
|
||||
import {
|
||||
filterOrdersByTab,
|
||||
getOrderCardActions,
|
||||
getStatusMeta,
|
||||
ORDER_TABS,
|
||||
type OrderTabKey,
|
||||
} from '@/lib/status'
|
||||
import ContactModal, { ContactFab } from './contact'
|
||||
import DetailDrawer from './detail'
|
||||
import './index.scss'
|
||||
|
||||
export default function Index ({ a5 = false }: { a5?: boolean }) {
|
||||
export default function Index () {
|
||||
const linkIdRef = useRef('')
|
||||
const queryIdRef = useRef(0)
|
||||
const nav = useMemo(() => getNavMetrics(), [])
|
||||
|
|
@ -33,7 +22,7 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
const [inputPhone, setInputPhone] = useState('')
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [searched, setSearched] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<OrderTabKey>('all')
|
||||
const [activeTab, setActiveTab] = useState<(typeof A5_TABS)[number]['key']>('all')
|
||||
const [orders, setOrders] = useState<OrderLike[]>([])
|
||||
const [detailOrder, setDetailOrder] = useState<OrderLike | null>(null)
|
||||
const [showContact, setShowContact] = useState(false)
|
||||
|
|
@ -45,24 +34,15 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
|
||||
useLoad(async (options) => {
|
||||
try {
|
||||
if (a5) {
|
||||
const launch = parseA5Launch(options || {})
|
||||
linkIdRef.current = launch.linkId
|
||||
setInputPhone(launch.phone)
|
||||
setReady(true)
|
||||
void getA5Config(launch.linkId).then((config) => setContact(pickServiceContact(config))).catch(() => {
|
||||
Taro.showToast({ title: '客服配置加载失败', icon: 'none' })
|
||||
})
|
||||
if (launch.phone) await queryOrders(launch.phone)
|
||||
return
|
||||
}
|
||||
const session = await bootstrapSession(options || {})
|
||||
setInputPhone(session.phone)
|
||||
setContact(pickServiceContact(session.config))
|
||||
const launch = parseA5Launch(options || {})
|
||||
linkIdRef.current = launch.linkId
|
||||
const phone = getQueryPhone(launch.phone)
|
||||
setInputPhone(phone)
|
||||
setReady(true)
|
||||
if (session.phone) {
|
||||
await queryOrders(session.phone)
|
||||
}
|
||||
void getA5Config(launch.linkId).then((config) => setContact(pickServiceContact(config))).catch(() => {
|
||||
Taro.showToast({ title: '客服配置加载失败', icon: 'none' })
|
||||
})
|
||||
if (phone) await queryOrders(phone)
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: error instanceof Error ? error.message : '初始化失败',
|
||||
|
|
@ -81,12 +61,13 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
return
|
||||
}
|
||||
|
||||
saveQueryPhone(phone)
|
||||
const queryId = ++queryIdRef.current
|
||||
setChecking(true)
|
||||
try {
|
||||
const res = a5 ? await getA5Orders(phone) : await getOrdersByPhone(phone)
|
||||
const res = await getA5Orders(phone)
|
||||
if (queryId !== queryIdRef.current) return
|
||||
const nextOrders = a5 ? res.data : filterOrdersForProduct(getSession().goodsType, res.data || [])
|
||||
const nextOrders = res.data
|
||||
setOrders(nextOrders)
|
||||
setSearched(true)
|
||||
} catch {
|
||||
|
|
@ -102,22 +83,15 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
}
|
||||
})
|
||||
|
||||
const tabs = a5 ? A5_TABS : ORDER_TABS
|
||||
const tabs = A5_TABS
|
||||
const a5Statuses: readonly string[] = A5_TABS.find((tab) => tab.key === activeTab)?.statuses || []
|
||||
const visibleOrders = a5
|
||||
? orders.filter((order) => !a5Statuses.length || a5Statuses.includes(order.process_status))
|
||||
: filterOrdersByTab(orders, activeTab)
|
||||
const visibleOrders = orders.filter((order) => !a5Statuses.length || a5Statuses.includes(order.process_status))
|
||||
const emptyText = searched && orders.length === 0
|
||||
? '该手机号未查询到订单,请联系客服处理'
|
||||
: '暂无订单列表'
|
||||
|
||||
const openOrderPage = (path: string, order: OrderLike) => {
|
||||
if (a5) {
|
||||
setA5Draft({ order, linkId: linkIdRef.current })
|
||||
Taro.navigateTo({ url: path.replace('/pages/', '/pages/a5/') })
|
||||
return
|
||||
}
|
||||
setOrderDraft(order)
|
||||
setA5Draft({ order, linkId: linkIdRef.current })
|
||||
Taro.navigateTo({ url: path })
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +118,10 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
<Image
|
||||
className='home-search__clear'
|
||||
src={LOCAL_IMAGES.clear}
|
||||
onClick={() => setInputPhone('')}
|
||||
onClick={() => {
|
||||
setInputPhone('')
|
||||
clearQueryPhone()
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
|
@ -176,10 +153,9 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
{visibleOrders.map((order, index) => (
|
||||
<OrderCard
|
||||
key={order.order_id || order.transaction_id || index}
|
||||
a5={a5}
|
||||
order={order}
|
||||
onCopy={() => {
|
||||
const number = a5 ? order.order_id : order.transaction_id
|
||||
const number = order.order_id
|
||||
if (!number) return
|
||||
Taro.setClipboardData({ data: String(number) })
|
||||
}}
|
||||
|
|
@ -199,8 +175,7 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
<ContactFab onClick={() => setShowContact(true)} />
|
||||
|
||||
{detailOrder ? (
|
||||
a5 ? <A5Detail order={detailOrder} linkId={linkIdRef.current} onCancel={() => setDetailOrder(null)} />
|
||||
: <DetailDrawer order={detailOrder} onCancel={() => setDetailOrder(null)} />
|
||||
<A5Detail order={detailOrder} linkId={linkIdRef.current} onCancel={() => setDetailOrder(null)} />
|
||||
) : null}
|
||||
|
||||
<ContactModal
|
||||
|
|
@ -213,14 +188,12 @@ export default function Index ({ a5 = false }: { a5?: boolean }) {
|
|||
}
|
||||
|
||||
function OrderCard ({
|
||||
a5,
|
||||
order,
|
||||
onCopy,
|
||||
onViewDetail,
|
||||
onResubmit,
|
||||
onComplaint,
|
||||
}: {
|
||||
a5?: boolean
|
||||
order: OrderLike
|
||||
onCopy: () => void
|
||||
onViewDetail: () => void
|
||||
|
|
@ -228,10 +201,10 @@ function OrderCard ({
|
|||
onComplaint: () => void
|
||||
}) {
|
||||
const isMapLabel = order.goods_type === 'map_label'
|
||||
const number = a5 ? order.order_id : order.transaction_id
|
||||
const status = (a5 && A5_STATUSES[order.process_status]) || getStatusMeta(order.process_status, order.process_status_name)
|
||||
const actions = a5 ? { complaint: true, submit: true, detail: order.process_status === '2' } : getOrderCardActions(order)
|
||||
const canSubmit = isMapLabel && actions.submit
|
||||
const number = order.order_id
|
||||
const status = A5_STATUSES[order.process_status] || { result: order.process_status_name || '未知', color: '#666666', bg: '#F5F5F5' }
|
||||
const canViewDetail = order.process_status === '2'
|
||||
const canSubmit = isMapLabel
|
||||
|
||||
return (
|
||||
<View className='order-card'>
|
||||
|
|
@ -246,7 +219,7 @@ function OrderCard ({
|
|||
<InfoLine label='订单时间:' value={order.create_time || order.pay_time || '-'} />
|
||||
|
||||
<View className='order-card__row'>
|
||||
<Text className='order-card__label'>{a5 ? '订单编号:' : '交易单号:'}</Text>
|
||||
<Text className='order-card__label'>订单编号:</Text>
|
||||
<Text className='order-card__value order-card__value--id'>{number || '-'}</Text>
|
||||
{number ? (
|
||||
<Image className='order-card__copy' src={COPY_ICON} onClick={onCopy} />
|
||||
|
|
@ -262,24 +235,18 @@ function OrderCard ({
|
|||
<View className='order-card__reject'>驳回理由:{order.reject_reason}</View>
|
||||
) : null}
|
||||
|
||||
{actions.complaint || canSubmit || actions.detail ? (
|
||||
<>
|
||||
<View className='order-card__divider' />
|
||||
<View className='order-card__actions'>
|
||||
{actions.complaint ? (
|
||||
<View className='order-card__action' onClick={onComplaint}>投诉</View>
|
||||
) : null}
|
||||
{actions.detail ? (
|
||||
<View className='order-card__action' onClick={onViewDetail}>查看详情</View>
|
||||
) : null}
|
||||
{canSubmit ? (
|
||||
<View className='order-card__action order-card__action--primary' onClick={onResubmit}>
|
||||
补充资料
|
||||
</View>
|
||||
) : null}
|
||||
<View className='order-card__divider' />
|
||||
<View className='order-card__actions'>
|
||||
<View className='order-card__action' onClick={onComplaint}>投诉</View>
|
||||
{canViewDetail ? (
|
||||
<View className='order-card__action' onClick={onViewDetail}>查看详情</View>
|
||||
) : null}
|
||||
{canSubmit ? (
|
||||
<View className='order-card__action order-card__action--primary' onClick={onResubmit}>
|
||||
补充资料
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
import { Button, Input, Text, View } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { updateOrderExtra } from '@/api/pay'
|
||||
import { getCompanies } from '@/api/user'
|
||||
import {
|
||||
buildCorpResubmitPayload,
|
||||
getCorpResubmitValues,
|
||||
validateIDCard,
|
||||
type CompanyItem,
|
||||
type CorpResubmitValues,
|
||||
} from '@/lib/company'
|
||||
import { isValidMobilePhone } from '@/lib/phone'
|
||||
import type { OrderLike } from '@/lib/order'
|
||||
|
||||
type Props = {
|
||||
order: OrderLike
|
||||
}
|
||||
|
||||
export default function CorpResubmit ({ order }: Props) {
|
||||
const [values, setValues] = useState<CorpResubmitValues>(() => getCorpResubmitValues(order))
|
||||
const [companies, setCompanies] = useState<CompanyItem[]>([])
|
||||
const [querying, setQuerying] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const updateField = <K extends keyof CorpResubmitValues>(field: K, value: CorpResubmitValues[K]) => {
|
||||
setValues((current) => ({ ...current, [field]: value }))
|
||||
}
|
||||
|
||||
const handleCompanyNameChange = (value: string) => {
|
||||
setValues((current) => ({
|
||||
...current,
|
||||
entity_name: value,
|
||||
entity_id: '',
|
||||
entity_legal: '',
|
||||
}))
|
||||
setCompanies([])
|
||||
}
|
||||
|
||||
const handleQueryCompany = async () => {
|
||||
const keyword = values.entity_name.replace(/\s/g, '')
|
||||
if (!keyword) {
|
||||
Taro.showToast({ title: '查询关键字不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (querying) return
|
||||
|
||||
setQuerying(true)
|
||||
try {
|
||||
const res = await getCompanies(keyword)
|
||||
const list = (Array.isArray(res.data) ? res.data : [])
|
||||
.filter((item: CompanyItem) => item?.credit_code)
|
||||
setCompanies(list)
|
||||
if (!list.length) {
|
||||
Taro.showToast({ title: '未查询到公司', icon: 'none' })
|
||||
}
|
||||
} catch {
|
||||
setCompanies([])
|
||||
Taro.showToast({ title: '查询公司失败,请稍后再试', icon: 'none' })
|
||||
} finally {
|
||||
setQuerying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectCompany = (company: CompanyItem) => {
|
||||
setValues((current) => ({
|
||||
...current,
|
||||
entity_name: company.name,
|
||||
entity_id: company.credit_code,
|
||||
entity_legal: company.oper_name,
|
||||
}))
|
||||
setCompanies([])
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (submitting) return
|
||||
if (!values.entity_name || !values.entity_id || !values.entity_legal) {
|
||||
Taro.showToast({ title: '请查询并选择公司', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!values.entity_idno || !values.entity_phone) {
|
||||
Taro.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!validateIDCard(values.entity_idno)) {
|
||||
Taro.showToast({ title: '请填写正确身份证号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!isValidMobilePhone(values.entity_phone)) {
|
||||
Taro.showToast({ title: '请填写正确手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await updateOrderExtra(buildCorpResubmitPayload(order, values))
|
||||
Taro.showToast({ title: '重新提交成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 500)
|
||||
} catch {
|
||||
Taro.showToast({ title: '重新提交失败,请稍后再试', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='resubmit-page resubmit-page--corp'>
|
||||
{order.reject_reason ? (
|
||||
<View className='resubmit-page__reject'>驳回理由:{order.reject_reason}</View>
|
||||
) : null}
|
||||
|
||||
<View className='resubmit-page__body'>
|
||||
<View className='resubmit-card'>
|
||||
<View className='resubmit__field resubmit__field--company'>
|
||||
<CorpFieldLabel text='公司名称' />
|
||||
<View className='resubmit__control resubmit__control--company'>
|
||||
<Input
|
||||
className='resubmit__input'
|
||||
value={values.entity_name}
|
||||
placeholder='请输入营业执照名称'
|
||||
placeholderClass='resubmit__placeholder'
|
||||
onInput={(event) => handleCompanyNameChange(event.detail.value)}
|
||||
/>
|
||||
<Button
|
||||
className='resubmit__query-company'
|
||||
loading={querying}
|
||||
disabled={querying}
|
||||
onClick={() => void handleQueryCompany()}
|
||||
>
|
||||
{querying ? '查询中' : '查询'}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{companies.length ? (
|
||||
<View className='resubmit__company-list'>
|
||||
{companies.map((company, index) => (
|
||||
<View
|
||||
className='resubmit__company-item'
|
||||
key={`${company.credit_code}-${index}`}
|
||||
onClick={() => handleSelectCompany(company)}
|
||||
>
|
||||
<Text className='resubmit__company-name'>{company.name}</Text>
|
||||
<Text className='resubmit__company-legal'>{company.oper_name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{values.entity_id ? (
|
||||
<View className='resubmit__selected-company'>
|
||||
<View className='resubmit__selected-company-main'>
|
||||
<Text className='resubmit__selected-company-name'>{values.entity_name}</Text>
|
||||
<Text className='resubmit__selected-company-code'>({values.entity_id})</Text>
|
||||
</View>
|
||||
<Text className='resubmit__selected-company-legal'>{values.entity_legal}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View className='resubmit__field'>
|
||||
<CorpFieldLabel text='身份证号' />
|
||||
<View className='resubmit__control'>
|
||||
<Input
|
||||
className='resubmit__input'
|
||||
maxlength={18}
|
||||
value={values.entity_idno}
|
||||
placeholder='请输入法人身份证号'
|
||||
placeholderClass='resubmit__placeholder'
|
||||
onInput={(event) => {
|
||||
const value = event.detail.value.toUpperCase().replace(/[^\dX]/g, '').slice(0, 18)
|
||||
updateField('entity_idno', value)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='resubmit__field'>
|
||||
<CorpFieldLabel text='手机号码' />
|
||||
<View className='resubmit__control'>
|
||||
<Input
|
||||
className='resubmit__input'
|
||||
type='number'
|
||||
maxlength={11}
|
||||
value={values.entity_phone}
|
||||
placeholder='请输入法人手机号'
|
||||
placeholderClass='resubmit__placeholder'
|
||||
onInput={(event) => updateField('entity_phone', event.detail.value.replace(/\D/g, '').slice(0, 11))}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='resubmit-page__footer'>
|
||||
<Button
|
||||
className='resubmit__submit resubmit__submit--corp'
|
||||
loading={submitting}
|
||||
disabled={submitting}
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
立即申报
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function CorpFieldLabel ({ text }: { text: string }) {
|
||||
return (
|
||||
<View className='resubmit__label'>
|
||||
<Text className='resubmit__required resubmit__required--leading'>*</Text>
|
||||
<Text>{text}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -110,88 +110,6 @@
|
|||
line-height: 36px;
|
||||
}
|
||||
|
||||
.resubmit__shots {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.resubmit__shot,
|
||||
.resubmit__example {
|
||||
position: relative;
|
||||
width: 292px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.resubmit__example {
|
||||
margin-left: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resubmit__example-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resubmit__example-caption {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 48px;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.resubmit__shot--empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.resubmit__shot-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resubmit__shot-camera {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.resubmit__shot-text {
|
||||
margin-top: 8px;
|
||||
color: #c9cdd4;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.resubmit__shot-edit {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 0 16px 0 16px;
|
||||
background: rgba(29, 33, 41, 0.55);
|
||||
}
|
||||
|
||||
.resubmit__shot-edit-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.resubmit__disclaimer,
|
||||
.resubmit__online {
|
||||
display: block;
|
||||
|
|
@ -261,137 +179,36 @@
|
|||
border: 0;
|
||||
}
|
||||
|
||||
.resubmit-page--corp .resubmit-card {
|
||||
padding-bottom: 12px;
|
||||
.resubmit-page--a5 {
|
||||
.resubmit-card { width: 100%; }
|
||||
.resubmit__control-value, .resubmit__error { overflow-wrap: anywhere; }
|
||||
}
|
||||
|
||||
.resubmit-page--corp .resubmit__field {
|
||||
margin-bottom: 36px;
|
||||
.a5-doc {
|
||||
margin-bottom: 28px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
&__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
|
||||
&__header .resubmit__label { margin-bottom: 0; }
|
||||
&__example { flex-shrink: 0; font-size: 24px; color: #165dff; }
|
||||
&__loading { display: block; margin-bottom: 16px; font-size: 24px; color: #86909c; }
|
||||
&__grid { display: flex; gap: 20px; }
|
||||
&__item { flex: 1; min-width: 0; max-width: calc((100% - 40px) / 3); text-align: center; }
|
||||
&__media { width: 100%; aspect-ratio: 1; box-sizing: border-box; background: #fff; border: 2px solid #e5e6eb; border-radius: 16px; overflow: hidden; position: relative; display: flex; align-items: center; justify-content: center; }
|
||||
&__image { width: 100%; height: 100%; }
|
||||
&__upload { width: 100%; height: 100%; margin: 0; padding: 12px 0; display: flex; flex-direction: column; justify-content: center; align-items: center; background: transparent; font-size: 22px; line-height: 1.5; color: #c9cdd4; }
|
||||
&__upload[disabled] { background: transparent; color: #c9cdd4; }
|
||||
&__upload::after, &__remove::after { border: 0; }
|
||||
&__camera { width: 56px; height: 56px; margin-bottom: 8px; }
|
||||
&__remove { position: absolute; top: 0; right: 0; width: 48px; height: 48px; padding: 0; line-height: 44px; border-radius: 0 16px 0 16px; background: rgba(29, 33, 41, .55); color: #fff; font-size: 32px; }
|
||||
&__caption { display: block; font-size: 24px; color: #1d2129; margin-top: 12px; }
|
||||
&__progress { position: absolute; bottom: 0; left: 0; right: 0; background: #165dff; color: white; text-align: center; font-size: 22px; }
|
||||
}
|
||||
|
||||
.resubmit__required--leading {
|
||||
margin-right: 6px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.resubmit__control--company {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.resubmit__query-company {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
height: 64px;
|
||||
margin: 0 0 0 12px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #fc6c00;
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
line-height: 64px;
|
||||
}
|
||||
|
||||
.resubmit__query-company::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.resubmit__company-list {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-height: 420px;
|
||||
margin-top: 8px;
|
||||
overflow-y: auto;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.resubmit__company-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 96px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.resubmit__company-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.resubmit__company-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #1a1a1a;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.resubmit__company-legal {
|
||||
flex-shrink: 0;
|
||||
max-width: 160px;
|
||||
margin-left: 20px;
|
||||
overflow: hidden;
|
||||
color: #fc6c00;
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resubmit__selected-company {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
min-height: 128px;
|
||||
margin: -16px 0 36px;
|
||||
padding: 20px 24px;
|
||||
border-radius: 12px;
|
||||
background: rgba(252, 108, 0, 0.06);
|
||||
}
|
||||
|
||||
.resubmit__selected-company-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resubmit__selected-company-name,
|
||||
.resubmit__selected-company-code,
|
||||
.resubmit__selected-company-legal {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resubmit__selected-company-name {
|
||||
color: #af5715;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.resubmit__selected-company-code {
|
||||
margin-top: 4px;
|
||||
color: #fc6c00;
|
||||
font-size: 24px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.resubmit__selected-company-legal {
|
||||
flex-shrink: 0;
|
||||
max-width: 180px;
|
||||
margin-left: 20px;
|
||||
color: #8b7c6f;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.resubmit__submit--corp {
|
||||
background: #df0000;
|
||||
.a5-example {
|
||||
position: fixed; inset: 0; z-index: 1200; background: rgba(0, 0, 0, .65); display: flex; align-items: center; justify-content: center; padding: 28px; box-sizing: border-box;
|
||||
&__dialog { width: 100%; max-height: 85vh; overflow-y: auto; padding: 28px; background: white; border-radius: 16px; box-sizing: border-box; }
|
||||
&__title { display: block; text-align: center; font-size: 32px; font-weight: 600; margin: 8px 0 28px; }
|
||||
&__image { width: 100%; height: 180px; }
|
||||
&__subtitle { display: block; font-size: 28px; font-weight: 600; margin: 28px 0 16px; }
|
||||
&__requirements { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; font-size: 24px; color: #666; }
|
||||
.a5-doc__item { max-width: none; }
|
||||
.resubmit__submit { margin-top: 28px; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,307 +1,140 @@
|
|||
import { Button, Image, Text, View } from '@tarojs/components'
|
||||
import { Button, Text, View } from '@tarojs/components'
|
||||
import Taro, { useDidShow, useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import PrivacyConsent from '@/components/privacy-consent'
|
||||
import { updateOrderExtra } from '@/api/pay'
|
||||
import { uploadImage } from '@/api/user'
|
||||
import { LOCAL_IMAGES } from '@/lib/assets'
|
||||
import { ENTITY_LABELS, MAP_ASSET_BASE } from '@/lib/cdn'
|
||||
import { hasPrivacyConsent, savePrivacyConsent } from '@/lib/consent'
|
||||
import { isCorpGoodsType } from '@/lib/detail'
|
||||
import { getOrderDraft } from '@/lib/orderDraft'
|
||||
import { useRef, useState } from 'react'
|
||||
import { a5Request, deleteA5File, getA5Draft, getA5Files, uploadA5Image, type A5Draft } from '@/lib/a5/api'
|
||||
import { buildA5Update, STOREFRONT_LOCS, storefrontFromFiles } from '@/lib/a5/data'
|
||||
import { formatMapCoordinate, getInitialPhone2, getOrderAddressName, getOrderExtraImages, getOrderMapAddress, parseCoordinate, parseStorefrontSlots, type ResubmitValues, type StorefrontSlot } from '@/lib/order'
|
||||
import { isValidPhone } from '@/lib/phone'
|
||||
import {
|
||||
buildResubmitPayload,
|
||||
formatMapCoordinate,
|
||||
getInitialPhone2,
|
||||
getOrderAddressName,
|
||||
getOrderExtraImages,
|
||||
getOrderMapAddress,
|
||||
parseCoordinate,
|
||||
parseStorefrontSlots,
|
||||
type OrderLike,
|
||||
type ResubmitValues,
|
||||
type StorefrontSlot,
|
||||
type UploadedImage,
|
||||
} from '@/lib/order'
|
||||
import { consumeMapPickerResult } from '@/pages/map/picker'
|
||||
import { FieldLabel, MapResubmitFields, ResubmitNotes } from './fields'
|
||||
import CorpResubmit from './corp'
|
||||
import PrivacyConsent from '@/components/privacy-consent'
|
||||
import { chooseA5Image, imageErrorMessage } from '@/lib/a5/image'
|
||||
import { hasPrivacyConsent, savePrivacyConsent } from '@/lib/consent'
|
||||
import { MapResubmitFields, ResubmitNotes } from './fields'
|
||||
import Documents from './documents'
|
||||
import './index.scss'
|
||||
|
||||
const STOREFRONT_LABELS = ['门头正面', '门头左面', '门头右面']
|
||||
const STOREFRONT_EXAMPLES = [
|
||||
LOCAL_IMAGES.exampleStorefrontFront,
|
||||
LOCAL_IMAGES.exampleStorefrontRight,
|
||||
LOCAL_IMAGES.exampleStorefrontLeft,
|
||||
]
|
||||
|
||||
function ImageSlot ({
|
||||
value,
|
||||
uploading,
|
||||
progress,
|
||||
onChange,
|
||||
}: {
|
||||
value: StorefrontSlot
|
||||
uploading: boolean
|
||||
progress: number
|
||||
onChange: (filePath: string) => void
|
||||
}) {
|
||||
const handleChoose = async () => {
|
||||
const res = await Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
})
|
||||
const filePath = res.tempFilePaths?.[0]
|
||||
if (filePath) onChange(filePath)
|
||||
}
|
||||
|
||||
if (value) {
|
||||
return (
|
||||
<View className='resubmit__shot'>
|
||||
<Image
|
||||
className='resubmit__shot-image'
|
||||
src={value.preview || value.url}
|
||||
mode='aspectFill'
|
||||
onClick={() => Taro.previewImage({ current: value.preview || value.url, urls: [value.preview || value.url] })}
|
||||
/>
|
||||
<View className='resubmit__shot-edit' onClick={() => void handleChoose()}>
|
||||
<Image className='resubmit__shot-edit-icon' src={LOCAL_IMAGES.pen} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='resubmit__shot resubmit__shot--empty' onClick={() => void handleChoose()}>
|
||||
<Image className='resubmit__shot-camera' src={LOCAL_IMAGES.camera} />
|
||||
<Text className='resubmit__shot-text'>
|
||||
{uploading ? `上传中 ${Math.round(progress)}%` : '请上传图片'}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ResubmitPage () {
|
||||
const [order, setOrder] = useState<OrderLike | null>(null)
|
||||
export default function A5Resubmit () {
|
||||
const [draft, setDraft] = useState<A5Draft | null>(null)
|
||||
const [values, setValues] = useState<ResubmitValues>({})
|
||||
const [storefrontSlots, setStorefrontSlots] = useState<StorefrontSlot[]>([null, null, null])
|
||||
const [licenseImages, setLicenseImages] = useState<UploadedImage[]>([])
|
||||
const [uploadingField, setUploadingField] = useState<'storefront0' | 'storefront1' | 'storefront2' | 'license' | null>(null)
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
const [formError, setFormError] = useState('')
|
||||
const [slots, setSlots] = useState<StorefrontSlot[]>(Array(6).fill(null))
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filesReady, setFilesReady] = useState(false)
|
||||
const [busy, setBusy] = useState<number | null>(null)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loadError, setLoadError] = useState('')
|
||||
const [agreed, setAgreed] = useState(hasPrivacyConsent)
|
||||
const lock = useRef(false)
|
||||
const returningMap = useRef(false)
|
||||
|
||||
async function loadFiles (current: A5Draft) {
|
||||
setLoading(true)
|
||||
setFilesReady(false)
|
||||
setLoadError('')
|
||||
try {
|
||||
const [storefront, license, identity] = await Promise.all(['order_biz2', 'order_biz1', 'order_biz3'].map((scene) => getA5Files(current.linkId, String(current.order.order_id), scene)))
|
||||
setSlots((previous) => [
|
||||
...(storefront.length ? storefrontFromFiles(storefront) : previous.slice(0, 3)),
|
||||
...(license.length ? [license[license.length - 1]] : previous.slice(3, 4)),
|
||||
...(identity.length ? [identity.slice(-2)[0] || null, identity.slice(-2)[1] || null] : previous.slice(4, 6)),
|
||||
])
|
||||
setFilesReady(true)
|
||||
} catch (reason) {
|
||||
setLoadError(imageErrorMessage(reason, '图片加载失败'))
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
useLoad(() => {
|
||||
const draft = getOrderDraft()
|
||||
if (!draft) {
|
||||
Taro.showToast({ title: '订单不存在', icon: 'none' })
|
||||
setTimeout(() => Taro.navigateBack(), 400)
|
||||
const current = getA5Draft()
|
||||
if (!current) {
|
||||
setLoading(false)
|
||||
setError('订单不存在,请返回查询页重新进入')
|
||||
return
|
||||
}
|
||||
setOrder(draft)
|
||||
setValues({
|
||||
entity_name: draft.entity_name || '',
|
||||
entity_address_name: getOrderAddressName(draft),
|
||||
entity_address: getOrderMapAddress(draft),
|
||||
entity_phone: draft.entity_phone || '',
|
||||
entity_phone2: getInitialPhone2(draft),
|
||||
})
|
||||
setStorefrontSlots(parseStorefrontSlots(draft))
|
||||
setLicenseImages(getOrderExtraImages(draft, ['entity_business_license_image', 'business_license_image']).slice(0, 1))
|
||||
consumeMapPickerResult()
|
||||
setDraft(current)
|
||||
const order = current.order
|
||||
setValues({ entity_name: order.entity_name || '', entity_phone: order.entity_phone || '', entity_phone2: getInitialPhone2(order), entity_address_name: getOrderAddressName(order), entity_address: getOrderMapAddress(order) })
|
||||
const license = getOrderExtraImages(order, ['entity_business_license_image', 'business_license_image'])
|
||||
const identity = getOrderExtraImages(order, ['entity_identity_card_image', 'identity_card_image'])
|
||||
setSlots([...parseStorefrontSlots(order), license[0] || null, identity[0] || null, identity[1] || null])
|
||||
void loadFiles(current)
|
||||
})
|
||||
|
||||
useDidShow(() => {
|
||||
const result = consumeMapPickerResult()
|
||||
if (!result) return
|
||||
const mapAddress = formatMapCoordinate(result)
|
||||
const mapAddressName = result.address || result.name || ''
|
||||
setValues((current) => ({
|
||||
...current,
|
||||
entity_address: mapAddress,
|
||||
entity_address_name: mapAddressName || current.entity_address_name,
|
||||
}))
|
||||
if (!returningMap.current) return
|
||||
returningMap.current = false
|
||||
const point = consumeMapPickerResult()
|
||||
if (point) setValues((current) => ({ ...current, entity_address: formatMapCoordinate(point), entity_address_name: point.address || point.name || current.entity_address_name }))
|
||||
})
|
||||
|
||||
const updateField = <K extends keyof ResubmitValues>(field: K, value: ResubmitValues[K]) => {
|
||||
setValues((current) => ({ ...current, [field]: value }))
|
||||
}
|
||||
|
||||
const requireConsent = () => {
|
||||
if (agreed) return true
|
||||
Taro.showToast({ title: '请先阅读并同意服务协议和隐私政策', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
const handleConsentChange = (nextAgreed: boolean) => {
|
||||
setAgreed(nextAgreed)
|
||||
savePrivacyConsent(nextAgreed)
|
||||
}
|
||||
|
||||
const handleUpload = async (field: 'storefront0' | 'storefront1' | 'storefront2' | 'license', filePath: string) => {
|
||||
setUploadingField(field)
|
||||
setUploadProgress(0)
|
||||
setFormError('')
|
||||
const disabled = loading || busy !== null || submitting
|
||||
const update = (field: keyof ResubmitValues, value: string) => setValues((current) => ({ ...current, [field]: value }))
|
||||
async function upload (index: number) {
|
||||
if (!draft || disabled || !filesReady || lock.current) return
|
||||
lock.current = true
|
||||
try {
|
||||
const uploaded = await uploadImage(filePath, (percent) => setUploadProgress(percent))
|
||||
const image: UploadedImage = { id: uploaded.id, url: uploaded.url, preview: filePath }
|
||||
if (field === 'license') {
|
||||
setLicenseImages([image])
|
||||
return
|
||||
}
|
||||
const index = Number(field.replace('storefront', ''))
|
||||
setStorefrontSlots((current) => {
|
||||
const next = [...current]
|
||||
next[index] = image
|
||||
return next
|
||||
})
|
||||
} catch (error) {
|
||||
setFormError(error instanceof Error ? error.message : '图片上传失败,请稍后重试')
|
||||
} finally {
|
||||
setUploadingField(null)
|
||||
setUploadProgress(0)
|
||||
}
|
||||
setError('')
|
||||
const path = await chooseA5Image()
|
||||
if (!path) return
|
||||
setBusy(index)
|
||||
setProgress(0)
|
||||
setError('')
|
||||
const uploaded = await uploadA5Image(path, draft.linkId, String(draft.order.order_id), index < 3 ? 'order_biz2' : index === 3 ? 'order_biz1' : 'order_biz3', index < 3 ? { loc: STOREFRONT_LOCS[index] } : undefined, setProgress)
|
||||
setSlots((current) => current.map((slot, position) => position === index ? { ...uploaded, preview: path } : slot))
|
||||
} catch (reason) {
|
||||
setError(imageErrorMessage(reason))
|
||||
} finally { setBusy(null); lock.current = false }
|
||||
}
|
||||
|
||||
const handleChooseMap = () => {
|
||||
const point = parseCoordinate(values.entity_address)
|
||||
const query = [
|
||||
point ? `lng=${point.lng}` : '',
|
||||
point ? `lat=${point.lat}` : '',
|
||||
values.entity_address_name ? `name=${encodeURIComponent(values.entity_address_name)}` : '',
|
||||
].filter(Boolean).join('&')
|
||||
Taro.navigateTo({
|
||||
url: query ? `/pages/map/index?${query}` : '/pages/map/index',
|
||||
})
|
||||
async function remove (index: number) {
|
||||
if (disabled || !filesReady || lock.current) return
|
||||
lock.current = true
|
||||
try {
|
||||
const confirmation = await Taro.showModal({ title: '删除照片', content: '确定删除这张照片?' })
|
||||
if (!confirmation.confirm) return
|
||||
setBusy(index)
|
||||
setProgress(0)
|
||||
if (slots[index]?.id) await deleteA5File(slots[index]!.id)
|
||||
setSlots((current) => current.map((slot, position) => position === index ? null : slot))
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : '删除失败') } finally { setBusy(null); lock.current = false }
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!order || submitting) return
|
||||
if (!requireConsent()) return
|
||||
if (uploadingField) {
|
||||
setFormError('图片正在上传,请稍候')
|
||||
return
|
||||
}
|
||||
if (!values.entity_name?.trim()) {
|
||||
Taro.showToast({ title: `请输入${ENTITY_LABELS.entityName}`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!values.entity_address_name?.trim()) {
|
||||
Taro.showToast({ title: `请输入${ENTITY_LABELS.entityAddress}`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!values.entity_address?.trim()) {
|
||||
Taro.showToast({ title: `请选择${ENTITY_LABELS.entityMapLocation}`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!isValidPhone(values.entity_phone || '')) {
|
||||
Taro.showToast({ title: '请填写正确的门店电话', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!isValidPhone(values.entity_phone2 || '')) {
|
||||
Taro.showToast({ title: '请填写正确的联系人电话', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
async function submit () {
|
||||
if (!draft || disabled || !filesReady || lock.current) return
|
||||
const invalid = !agreed ? '请先阅读并同意协议'
|
||||
: !values.entity_name?.trim() ? '请输入门店名称'
|
||||
: !values.entity_address_name?.trim() ? '请输入实际经营地址'
|
||||
: !values.entity_address ? '请选择门店位置'
|
||||
: !isValidPhone(values.entity_phone || '') ? '请填写正确的门店电话'
|
||||
: !isValidPhone(values.entity_phone2 || '') ? '请填写正确的联系人电话'
|
||||
: slots.slice(0, 3).some((slot) => !slot?.id) ? '请上传门头正面、左面和右面照片' : ''
|
||||
if (invalid) { Taro.showToast({ title: invalid, icon: 'none' }); return }
|
||||
lock.current = true
|
||||
setSubmitting(true)
|
||||
setFormError('')
|
||||
try {
|
||||
await updateOrderExtra(buildResubmitPayload({
|
||||
order,
|
||||
values,
|
||||
storefrontSlots,
|
||||
licenseImages,
|
||||
}))
|
||||
await a5Request('/h5/order', { method: 'PUT', data: buildA5Update(draft.order, values, slots) })
|
||||
Taro.showToast({ title: '重新提交成功', icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 500)
|
||||
} catch {
|
||||
Taro.showToast({ title: '重新提交失败,请稍后再试', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
} catch (reason) { setError(reason instanceof Error ? reason.message : '重新提交失败') } finally { setSubmitting(false); lock.current = false }
|
||||
}
|
||||
|
||||
if (order && isCorpGoodsType(order.goods_type)) {
|
||||
return <CorpResubmit order={order} />
|
||||
function chooseMap () {
|
||||
const point = parseCoordinate(values.entity_address)
|
||||
consumeMapPickerResult()
|
||||
returningMap.current = true
|
||||
void Taro.navigateTo({ url: `/pages/map/index?name=${encodeURIComponent(values.entity_address_name || '')}${point ? `&lng=${point.lng}&lat=${point.lat}` : ''}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='resubmit-page'>
|
||||
{order?.reject_reason ? (
|
||||
<View className='resubmit-page__reject'>驳回理由:{order.reject_reason}</View>
|
||||
) : null}
|
||||
|
||||
<View className='resubmit-page__body resubmit-page__body--consent'>
|
||||
<View className='resubmit-card'>
|
||||
<MapResubmitFields values={values} onChange={updateField} onChooseMap={handleChooseMap} />
|
||||
|
||||
{STOREFRONT_LABELS.map((label, index) => (
|
||||
<View className='resubmit__field' key={label}>
|
||||
<FieldLabel icon={`${MAP_ASSET_BASE}/pay-success-storefront.png`} text={label} />
|
||||
<View className='resubmit__shots'>
|
||||
<ImageSlot
|
||||
value={storefrontSlots[index] || null}
|
||||
uploading={uploadingField === `storefront${index}`}
|
||||
progress={uploadProgress}
|
||||
onChange={(filePath) => void handleUpload(`storefront${index}` as 'storefront0' | 'storefront1' | 'storefront2', filePath)}
|
||||
/>
|
||||
{storefrontSlots[index] ? null : (
|
||||
<View className='resubmit__example'>
|
||||
<Image
|
||||
className='resubmit__example-image'
|
||||
src={STOREFRONT_EXAMPLES[index]}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='resubmit__example-caption'>
|
||||
<Text>示例</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View className='resubmit__field'>
|
||||
<FieldLabel icon={`${MAP_ASSET_BASE}/pay-success-license.png`} text='营业执照' />
|
||||
<View className='resubmit__shots'>
|
||||
<ImageSlot
|
||||
value={licenseImages[0] || null}
|
||||
uploading={uploadingField === 'license'}
|
||||
progress={uploadProgress}
|
||||
onChange={(filePath) => void handleUpload('license', filePath)}
|
||||
/>
|
||||
{licenseImages[0] ? null : (
|
||||
<Image
|
||||
className='resubmit__example'
|
||||
src={LOCAL_IMAGES.exampleLicense}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ResubmitNotes />
|
||||
{formError ? <Text className='resubmit__error'>{formError}</Text> : null}
|
||||
</View>
|
||||
|
||||
<View className='resubmit-page__footer resubmit-page__footer--consent'>
|
||||
<PrivacyConsent
|
||||
className='resubmit__privacy'
|
||||
agreed={agreed}
|
||||
onChange={handleConsentChange}
|
||||
/>
|
||||
<Button
|
||||
className='resubmit__submit'
|
||||
loading={submitting}
|
||||
disabled={Boolean(uploadingField)}
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
立即提交
|
||||
</Button>
|
||||
return <View className='resubmit-page resubmit-page--a5'>
|
||||
{draft?.order.reject_reason ? <View className='resubmit-page__reject'>驳回理由:{draft.order.reject_reason}</View> : null}
|
||||
<View className='resubmit-page__body resubmit-page__body--consent'>
|
||||
<View className='resubmit-card'>
|
||||
<MapResubmitFields values={values} onChange={update} onChooseMap={chooseMap} />
|
||||
{loading ? <Text className='a5-doc__loading'>图片加载中...</Text> : null}
|
||||
<Documents slots={slots} busy={busy} progress={progress} disabled={disabled || !filesReady} onUpload={(index) => void upload(index)} onRemove={(index) => void remove(index)} />
|
||||
</View>
|
||||
<ResubmitNotes />
|
||||
{loadError ? <View className='resubmit__error'>{loadError}{draft ? <Text onClick={() => !disabled && void loadFiles(draft)}> 重新加载资料</Text> : null}</View> : null}
|
||||
{error ? <View className='resubmit__error'>{error}</View> : null}
|
||||
</View>
|
||||
)
|
||||
<View className='resubmit-page__footer resubmit-page__footer--consent'>
|
||||
<PrivacyConsent className='resubmit__privacy' agreed={agreed} onChange={(value) => { setAgreed(value); savePrivacyConsent(value) }} />
|
||||
<Button className='resubmit__submit' loading={submitting} disabled={disabled || !filesReady || !draft} onClick={() => void submit()}>立即提交</Button>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,8 @@ declare namespace NodeJS {
|
|||
* @see https://taro-docs.jd.com/docs/next/env-mode-config#特殊环境变量-taro_app_id
|
||||
*/
|
||||
TARO_APP_ID: string
|
||||
/** 接口域名,开发/生产分别来自 .env.development / .env.production */
|
||||
TARO_APP_API_ORIGIN: string
|
||||
/** A5 独立接口域名 */
|
||||
/** A5 接口域名,开发/生产分别来自 .env.development / .env.production */
|
||||
TARO_APP_API_A5: string
|
||||
/** x-host 回退值 */
|
||||
TARO_APP_API_HOST: string
|
||||
/** 天地图逆地理编码 key,用于地图选点回填地址名称 */
|
||||
TARO_APP_TIANDITU_KEY: string
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue