diff --git a/.env.development b/.env.development index 5c9d3ce..dc30a27 100644 --- a/.env.development +++ b/.env.development @@ -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 diff --git a/.env.production b/.env.production index 23d0c5d..3761668 100644 --- a/.env.production +++ b/.env.production @@ -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 diff --git a/.env.test b/.env.test deleted file mode 100644 index 194e829..0000000 --- a/.env.test +++ /dev/null @@ -1 +0,0 @@ -# TARO_APP_ID="测试环境下的小程序 AppID" diff --git a/AGENTS.md b/AGENTS.md index 6ad37da..07e4c4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,24 +19,27 @@ Other targets follow `pnpm dev:` / `pnpm build:` (`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/` 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`. diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index afef593..0000000 --- a/DESIGN.md +++ /dev/null @@ -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** 使用渐变文字、侧边粗色条、嵌套卡片或重复的图标卡片网格。 diff --git a/PRODUCT.md b/PRODUCT.md deleted file mode 100644 index b9c59b0..0000000 --- a/PRODUCT.md +++ /dev/null @@ -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,不仅依赖颜色传达信息,并为减少动态效果提供静态降级。 diff --git a/design-qa.md b/design-qa.md deleted file mode 100644 index a71c0ad..0000000 --- a/design-qa.md +++ /dev/null @@ -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 diff --git a/docs/a5-integration.md b/docs/a5-integration.md index 05380d0..1ae6c57 100644 --- a/docs/a5-integration.md +++ b/docs/a5-integration.md @@ -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/` scene supplies a link ID. - Legacy `/h/` 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/`. ## 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. diff --git a/package.json b/package.json index e9f978a..b0fd0c7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e691922..6a62252 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/src/api/pay.ts b/src/api/pay.ts deleted file mode 100644 index 8c379bd..0000000 --- a/src/api/pay.ts +++ /dev/null @@ -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) { - return request({ - url: '/api/h5/order', - method: 'PUT', - data, - }) -} diff --git a/src/api/user.ts b/src/api/user.ts deleted file mode 100644 index ab62926..0000000 --- a/src/api/user.ts +++ /dev/null @@ -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 = {}) { - return request({ - url: '/api/user/config', - method: 'GET', - params, - }) -} - -export function initCorp (params: Record = {}) { - 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), - } -} diff --git a/src/app.config.test.ts b/src/app.config.test.ts new file mode 100644 index 0000000..8a4fb7d --- /dev/null +++ b/src/app.config.test.ts @@ -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']) + }) +}) diff --git a/src/app.config.ts b/src/app.config.ts index 1c5ccf9..95b6a0c 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -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', diff --git a/src/assets/images/icon-pen.png b/src/assets/images/icon-pen.png deleted file mode 100644 index eda6135..0000000 Binary files a/src/assets/images/icon-pen.png and /dev/null differ diff --git a/src/lib/a5/data.test.ts b/src/lib/a5/data.test.ts index e04aecf..8964a5c 100644 --- a/src/lib/a5/data.test.ts +++ b/src/lib/a5/data.test.ts @@ -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', () => { diff --git a/src/lib/a5/data.ts b/src/lib/a5/data.ts index 303b128..745bb99 100644 --- a/src/lib/a5/data.ts +++ b/src/lib/a5/data.ts @@ -27,7 +27,7 @@ export function parseA5Launch (raw: Record) { 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) { diff --git a/src/lib/a5/queryPhone.test.ts b/src/lib/a5/queryPhone.test.ts new file mode 100644 index 0000000..f7d7a40 --- /dev/null +++ b/src/lib/a5/queryPhone.test.ts @@ -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() + 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() + }) +}) diff --git a/src/lib/a5/queryPhone.ts b/src/lib/a5/queryPhone.ts new file mode 100644 index 0000000..bde27e3 --- /dev/null +++ b/src/lib/a5/queryPhone.ts @@ -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. + } +} diff --git a/src/lib/assets.ts b/src/lib/assets.ts index 975ee15..595816f 100644 --- a/src/lib/assets.ts +++ b/src/lib/assets.ts @@ -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, } diff --git a/src/lib/auth.test.ts b/src/lib/auth.test.ts deleted file mode 100644 index 57de3e3..0000000 --- a/src/lib/auth.test.ts +++ /dev/null @@ -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('获取临时登录态失败') - }) -}) diff --git a/src/lib/auth.ts b/src/lib/auth.ts deleted file mode 100644 index 0361dfc..0000000 --- a/src/lib/auth.ts +++ /dev/null @@ -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 -}) { - const existing = String(input.token || '').trim() - if (existing) { - return existing - } - - const token = pickToken(await input.fetchUserConfig()) - if (!token) { - throw new Error('获取临时登录态失败') - } - - return token -} diff --git a/src/lib/bootstrap.ts b/src/lib/bootstrap.ts deleted file mode 100644 index a0e3d49..0000000 --- a/src/lib/bootstrap.ts +++ /dev/null @@ -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 = {}) { - 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() -} diff --git a/src/lib/cdn.ts b/src/lib/cdn.ts index d1d59ee..887fa2f 100644 --- a/src/lib/cdn.ts +++ b/src/lib/cdn.ts @@ -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: '门店名称', diff --git a/src/lib/company.test.ts b/src/lib/company.test.ts deleted file mode 100644 index b3daad4..0000000 --- a/src/lib/company.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/lib/company.ts b/src/lib/company.ts deleted file mode 100644 index dc284a3..0000000 --- a/src/lib/company.ts +++ /dev/null @@ -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, - } -} diff --git a/src/lib/contact.ts b/src/lib/contact.ts index d72d36b..1bbfc23 100644 --- a/src/lib/contact.ts +++ b/src/lib/contact.ts @@ -48,16 +48,3 @@ export function hasServiceContact (config?: Record | null) { const contact = pickServiceContact(config) return Boolean(contact.online || contact.phone || contact.weixin) } - -export function mergeServiceConfig (current: Record | 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 -} diff --git a/src/lib/crypto.test.ts b/src/lib/crypto.test.ts deleted file mode 100644 index 658a1ae..0000000 --- a/src/lib/crypto.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts deleted file mode 100644 index 08440ec..0000000 --- a/src/lib/crypto.ts +++ /dev/null @@ -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) -} diff --git a/src/lib/detail.test.ts b/src/lib/detail.test.ts deleted file mode 100644 index 4662a7e..0000000 --- a/src/lib/detail.test.ts +++ /dev/null @@ -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('') - }) -}) diff --git a/src/lib/detail.ts b/src/lib/detail.ts deleted file mode 100644 index 4cb034b..0000000 --- a/src/lib/detail.ts +++ /dev/null @@ -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 = { - 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 -} diff --git a/src/lib/launch.test.ts b/src/lib/launch.test.ts deleted file mode 100644 index ea270ca..0000000 --- a/src/lib/launch.test.ts +++ /dev/null @@ -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', - }) - }) -}) - diff --git a/src/lib/launch.ts b/src/lib/launch.ts deleted file mode 100644 index ad8be2d..0000000 --- a/src/lib/launch.ts +++ /dev/null @@ -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 = {}) { - 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 = {} - 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 = {}): 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 = {}): 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, - } -} diff --git a/src/lib/order.test.ts b/src/lib/order.test.ts index 10d0580..585a55b 100644 --- a/src/lib/order.test.ts +++ b/src/lib/order.test.ts @@ -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', - }, - }) - }) -}) diff --git a/src/lib/order.ts b/src/lib/order.ts index c01b434..a2882f2 100644 --- a/src/lib/order.ts +++ b/src/lib/order.ts @@ -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 (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 (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 (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>((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), - }, - } -} diff --git a/src/lib/orderDraft.ts b/src/lib/orderDraft.ts deleted file mode 100644 index 54c3332..0000000 --- a/src/lib/orderDraft.ts +++ /dev/null @@ -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 -} diff --git a/src/lib/origin.test.ts b/src/lib/origin.test.ts deleted file mode 100644 index c9cc73a..0000000 --- a/src/lib/origin.test.ts +++ /dev/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') - }) -}) - diff --git a/src/lib/origin.ts b/src/lib/origin.ts deleted file mode 100644 index 61c6e90..0000000 --- a/src/lib/origin.ts +++ /dev/null @@ -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) -} diff --git a/src/lib/qiniu.test.ts b/src/lib/qiniu.test.ts deleted file mode 100644 index 13d2f4c..0000000 --- a/src/lib/qiniu.test.ts +++ /dev/null @@ -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' - ) - }) -}) diff --git a/src/lib/qiniu.ts b/src/lib/qiniu.ts deleted file mode 100644 index 5e72a44..0000000 --- a/src/lib/qiniu.ts +++ /dev/null @@ -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' -} diff --git a/src/lib/qrPng.test.ts b/src/lib/qrPng.test.ts deleted file mode 100644 index 2c5ab25..0000000 --- a/src/lib/qrPng.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/lib/qrPng.ts b/src/lib/qrPng.ts deleted file mode 100644 index 5f17400..0000000 --- a/src/lib/qrPng.ts +++ /dev/null @@ -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 -} diff --git a/src/lib/request.ts b/src/lib/request.ts deleted file mode 100644 index 70fd1ae..0000000 --- a/src/lib/request.ts +++ /dev/null @@ -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 - data?: any - silent?: boolean -} - -export type BusinessResponse = { - 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) { - return Object.keys(params) - .map((key) => `${key}=${encodeURIComponent(params[key] ?? '')}`) - .join('&') -} - -function buildHeaders () { - const session = getSession() - const headers: Record = { - '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 (options: RequestOptions): Promise> { - 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 - 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() - } -} diff --git a/src/lib/session.ts b/src/lib/session.ts deleted file mode 100644 index 73ddeaf..0000000 --- a/src/lib/session.ts +++ /dev/null @@ -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 - goodsType: string -} - -const EMPTY_SESSION: SessionState = { - token: '', - host: '', - packageId: '', - scene: '', - linkId: '', - shareId: '', - prevId: '', - phone: '', - deviceId: '', - config: {}, - goodsType: '', -} - -function readStorage (): Partial { - 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) { - memorySession = { - ...getSession(), - ...patch, - } - writeStorage(memorySession) - return memorySession -} - -export function applyLaunchParams (raw: Record = {}) { - 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 = { - config: mergeServiceConfig(getSession().config, payload), - } - if (token) patch.token = token - if (packageId) patch.packageId = packageId - if (goodsType) patch.goodsType = goodsType - return setSession(patch) -} diff --git a/src/lib/sign.test.ts b/src/lib/sign.test.ts deleted file mode 100644 index f2ad392..0000000 --- a/src/lib/sign.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/lib/sign.ts b/src/lib/sign.ts deleted file mode 100644 index e9fbe61..0000000 --- a/src/lib/sign.ts +++ /dev/null @@ -1,31 +0,0 @@ -import MD5 from 'crypto-js/md5' - -export const SIGN_KEY = 'MsvqoWCMxJsozicF5K4EFVSoVf8rEHfn' - -type SignInput = { - method?: string - params?: Record - 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, - } -} diff --git a/src/lib/status.test.ts b/src/lib/status.test.ts deleted file mode 100644 index 562aa30..0000000 --- a/src/lib/status.test.ts +++ /dev/null @@ -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) - } - }) -}) diff --git a/src/lib/status.ts b/src/lib/status.ts deleted file mode 100644 index 6d36d67..0000000 --- a/src/lib/status.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { isCorpGoodsType } from './detail' - -export const STATUS_CONFIG: Record = { - '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 = { - 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> (orders: T[] = [], tab: OrderTabKey) { - const current = ORDER_TABS.find((item) => item.key === tab) - if (!current || !current.statuses) return orders - const statuses = new Set(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, - } -} diff --git a/src/lib/uuid.ts b/src/lib/uuid.ts deleted file mode 100644 index 5f01178..0000000 --- a/src/lib/uuid.ts +++ /dev/null @@ -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) - }) -} diff --git a/src/pages/a5/complaint/index.config.ts b/src/pages/a5/complaint/index.config.ts deleted file mode 100644 index d9ba842..0000000 --- a/src/pages/a5/complaint/index.config.ts +++ /dev/null @@ -1 +0,0 @@ -export default definePageConfig({ navigationBarTitleText: '投诉' }) diff --git a/src/pages/a5/complaint/index.tsx b/src/pages/a5/complaint/index.tsx deleted file mode 100644 index c77688e..0000000 --- a/src/pages/a5/complaint/index.tsx +++ /dev/null @@ -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 } diff --git a/src/pages/a5/detail.tsx b/src/pages/a5/detail.tsx deleted file mode 100644 index 67af0bc..0000000 --- a/src/pages/a5/detail.tsx +++ /dev/null @@ -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 ( - - event.stopPropagation()}> - - 详情 - - - - 完成截图 - {order.process_status === '2' ? PLATFORMS.map((platform) => ( - - )) : 暂无完成截图} - - - - ) -} -function PlatformFiles ({ orderId, linkId, scene, name }: { orderId: string, linkId: string, scene: string, name: string }) { - const [files, setFiles] = useState([]) - 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 ( - - {name} - {loading ? 截图加载中... : error ? ( - setAttempt((value) => value + 1)}>{error},点击重试 - ) : !files.length ? 暂无完成截图 : <> - {images.map((file) => Taro.previewImage({ current: file.url, urls: images.map((image) => image.url) })} />)} - {files.filter(isDocument).map((file) => void openDocument(file)}>{file.name || '报告文件'})} - } - - ) -} -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() } -} diff --git a/src/pages/a5/index.config.ts b/src/pages/a5/index.config.ts deleted file mode 100644 index a35ff31..0000000 --- a/src/pages/a5/index.config.ts +++ /dev/null @@ -1 +0,0 @@ -export default definePageConfig({ navigationStyle: 'custom', backgroundColor: '#F6F8FC' }) diff --git a/src/pages/a5/index.tsx b/src/pages/a5/index.tsx deleted file mode 100644 index 8bf1387..0000000 --- a/src/pages/a5/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import Index from '@/pages/index/index' - -export default function A5Page () { return } diff --git a/src/pages/a5/resubmit/index.config.ts b/src/pages/a5/resubmit/index.config.ts deleted file mode 100644 index e6b679f..0000000 --- a/src/pages/a5/resubmit/index.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default definePageConfig({ - navigationBarTitleText: '重新提交', - navigationBarBackgroundColor: '#ffffff', - navigationBarTextStyle: 'black', - backgroundColor: '#F7F8FA', -}) diff --git a/src/pages/a5/resubmit/index.scss b/src/pages/a5/resubmit/index.scss deleted file mode 100644 index 2e3c11a..0000000 --- a/src/pages/a5/resubmit/index.scss +++ /dev/null @@ -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; } -} diff --git a/src/pages/a5/resubmit/index.tsx b/src/pages/a5/resubmit/index.tsx deleted file mode 100644 index 5a7525a..0000000 --- a/src/pages/a5/resubmit/index.tsx +++ /dev/null @@ -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(null) - const [values, setValues] = useState({}) - const [slots, setSlots] = useState(Array(6).fill(null)) - const [loading, setLoading] = useState(true) - const [filesReady, setFilesReady] = useState(false) - const [busy, setBusy] = useState(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 - {draft?.order.reject_reason ? 驳回理由:{draft.order.reject_reason} : null} - - - - {loading ? 图片加载中... : null} - void upload(index)} onRemove={(index) => void remove(index)} /> - - - {loadError ? {loadError}{draft ? !disabled && void loadFiles(draft)}> 重新加载资料 : null} : null} - {error ? {error} : null} - - - { setAgreed(value); savePrivacyConsent(value) }} /> - - - -} diff --git a/src/pages/complaint/index.tsx b/src/pages/complaint/index.tsx index 47929d7..86864d3 100644 --- a/src/pages/complaint/index.tsx +++ b/src/pages/complaint/index.tsx @@ -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 = { }, } -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(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 - {services ? '订单编号' : '交易ID'} - {(services ? order?.order_id : order?.out_trade_no) || '-'} + 订单编号 + {order?.order_id || '-'} 交易时间 diff --git a/src/pages/index/detail.tsx b/src/pages/index/detail.tsx index 62fc41e..67af0bc 100644 --- a/src/pages/index/detail.tsx +++ b/src/pages/index/detail.tsx @@ -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 ( - - event.stopPropagation()} - > - + + event.stopPropagation()}> 详情 - - - + - - {showReceipt ? : null} - - {files.images.length ? ( - - 完成截图: - {files.images.map((file, index) => ( - previewImages(imageUrls, String(file.url))} - /> - ))} - - ) : null} - - {files.pdfs.length ? ( - - 报告文件: - {files.pdfs.map((file, index) => ( - void openPdf(file)} - > - 查看报告文件{files.pdfs.length > 1 ? index + 1 : ''} - - ))} - - ) : null} - - {empty ? ( - 暂无完成截图 - ) : null} + 完成截图 + {order.process_status === '2' ? PLATFORMS.map((platform) => ( + + )) : 暂无完成截图} ) } - -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([]) + 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 ( - 回执单: - - {title} - - - - - - - - - - - - - - - - - - - - - 备注 - - - {remarkLines.map((line, index) => ( - {line} - ))} - - - - - + {name} + {loading ? 截图加载中... : error ? ( + setAttempt((value) => value + 1)}>{error},点击重试 + ) : !files.length ? 暂无完成截图 : <> + {images.map((file) => Taro.previewImage({ current: file.url, urls: images.map((image) => image.url) })} />)} + {files.filter(isDocument).map((file) => void openDocument(file)}>{file.name || '报告文件'})} + } ) } - -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 ( - - {label} - {value} - - ) -} - -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() } } diff --git a/src/pages/index/index.config.ts b/src/pages/index/index.config.ts index cd68a8a..a35ff31 100644 --- a/src/pages/index/index.config.ts +++ b/src/pages/index/index.config.ts @@ -1,7 +1 @@ -export default definePageConfig({ - navigationStyle: 'custom', - navigationBarTextStyle: 'black', - navigationBarBackgroundColor: '#F6F8FC', - backgroundColor: '#F6F8FC', - backgroundColorTop: '#F6F8FC', -}) +export default definePageConfig({ navigationStyle: 'custom', backgroundColor: '#F6F8FC' }) diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss index bf5a72e..59b8d8a 100644 --- a/src/pages/index/index.scss +++ b/src/pages/index/index.scss @@ -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; diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 83f8a96..99bf063 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -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('all') + const [activeTab, setActiveTab] = useState<(typeof A5_TABS)[number]['key']>('all') const [orders, setOrders] = useState([]) const [detailOrder, setDetailOrder] = useState(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 }) { setInputPhone('')} + onClick={() => { + setInputPhone('') + clearQueryPhone() + }} /> ) : null} @@ -176,10 +153,9 @@ export default function Index ({ a5 = false }: { a5?: boolean }) { {visibleOrders.map((order, index) => ( { - 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 }) { setShowContact(true)} /> {detailOrder ? ( - a5 ? setDetailOrder(null)} /> - : setDetailOrder(null)} /> + setDetailOrder(null)} /> ) : null} 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 ( @@ -246,7 +219,7 @@ function OrderCard ({ - {a5 ? '订单编号:' : '交易单号:'} + 订单编号: {number || '-'} {number ? ( @@ -262,24 +235,18 @@ function OrderCard ({ 驳回理由:{order.reject_reason} ) : null} - {actions.complaint || canSubmit || actions.detail ? ( - <> - - - {actions.complaint ? ( - 投诉 - ) : null} - {actions.detail ? ( - 查看详情 - ) : null} - {canSubmit ? ( - - 补充资料 - - ) : null} + + + 投诉 + {canViewDetail ? ( + 查看详情 + ) : null} + {canSubmit ? ( + + 补充资料 - - ) : null} + ) : null} + ) } diff --git a/src/pages/resubmit/corp.tsx b/src/pages/resubmit/corp.tsx deleted file mode 100644 index 3c6e293..0000000 --- a/src/pages/resubmit/corp.tsx +++ /dev/null @@ -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(() => getCorpResubmitValues(order)) - const [companies, setCompanies] = useState([]) - const [querying, setQuerying] = useState(false) - const [submitting, setSubmitting] = useState(false) - - const updateField = (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 ( - - {order.reject_reason ? ( - 驳回理由:{order.reject_reason} - ) : null} - - - - - - - handleCompanyNameChange(event.detail.value)} - /> - - - - {companies.length ? ( - - {companies.map((company, index) => ( - handleSelectCompany(company)} - > - {company.name} - {company.oper_name} - - ))} - - ) : null} - - - {values.entity_id ? ( - - - {values.entity_name} - ({values.entity_id}) - - {values.entity_legal} - - ) : null} - - - - - { - const value = event.detail.value.toUpperCase().replace(/[^\dX]/g, '').slice(0, 18) - updateField('entity_idno', value) - }} - /> - - - - - - - updateField('entity_phone', event.detail.value.replace(/\D/g, '').slice(0, 11))} - /> - - - - - - - - - - ) -} - -function CorpFieldLabel ({ text }: { text: string }) { - return ( - - * - {text} - - ) -} diff --git a/src/pages/a5/resubmit/documents.tsx b/src/pages/resubmit/documents.tsx similarity index 100% rename from src/pages/a5/resubmit/documents.tsx rename to src/pages/resubmit/documents.tsx diff --git a/src/pages/resubmit/index.scss b/src/pages/resubmit/index.scss index 3228037..760a965 100644 --- a/src/pages/resubmit/index.scss +++ b/src/pages/resubmit/index.scss @@ -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; } } diff --git a/src/pages/resubmit/index.tsx b/src/pages/resubmit/index.tsx index a60151e..779f3af 100644 --- a/src/pages/resubmit/index.tsx +++ b/src/pages/resubmit/index.tsx @@ -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 ( - - Taro.previewImage({ current: value.preview || value.url, urls: [value.preview || value.url] })} - /> - void handleChoose()}> - - - - ) - } - - return ( - void handleChoose()}> - - - {uploading ? `上传中 ${Math.round(progress)}%` : '请上传图片'} - - - ) -} - -export default function ResubmitPage () { - const [order, setOrder] = useState(null) +export default function A5Resubmit () { + const [draft, setDraft] = useState(null) const [values, setValues] = useState({}) - const [storefrontSlots, setStorefrontSlots] = useState([null, null, null]) - const [licenseImages, setLicenseImages] = useState([]) - const [uploadingField, setUploadingField] = useState<'storefront0' | 'storefront1' | 'storefront2' | 'license' | null>(null) - const [uploadProgress, setUploadProgress] = useState(0) - const [formError, setFormError] = useState('') + const [slots, setSlots] = useState(Array(6).fill(null)) + const [loading, setLoading] = useState(true) + const [filesReady, setFilesReady] = useState(false) + const [busy, setBusy] = useState(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 = (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 + 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 ( - - {order?.reject_reason ? ( - 驳回理由:{order.reject_reason} - ) : null} - - - - - - {STOREFRONT_LABELS.map((label, index) => ( - - - - void handleUpload(`storefront${index}` as 'storefront0' | 'storefront1' | 'storefront2', filePath)} - /> - {storefrontSlots[index] ? null : ( - - - - 示例 - - - )} - - - ))} - - - - - void handleUpload('license', filePath)} - /> - {licenseImages[0] ? null : ( - - )} - - - - - - {formError ? {formError} : null} - - - - - + return + {draft?.order.reject_reason ? 驳回理由:{draft.order.reject_reason} : null} + + + + {loading ? 图片加载中... : null} + void upload(index)} onRemove={(index) => void remove(index)} /> + + {loadError ? {loadError}{draft ? !disabled && void loadFiles(draft)}> 重新加载资料 : null} : null} + {error ? {error} : null} - ) + + { setAgreed(value); savePrivacyConsent(value) }} /> + + + } diff --git a/types/global.d.ts b/types/global.d.ts index 3b73b0e..2698ffd 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -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 }