feat: native progress query, resubmit and map picker
Replace the WebView shell with a native check page that accepts H5 launch params, queries orders, resubmits materials with Qiniu uploads, and picks map locations on a WeChat Map with reverse geocoding.
This commit is contained in:
parent
15b047dfdd
commit
3a42f65c5a
|
|
@ -1,2 +1,3 @@
|
||||||
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config
|
TARO_APP_API_ORIGIN=https://corp-test.batiao8.com
|
||||||
TARO_APP_ID="wx6d4f6f29c41aff93"
|
TARO_APP_API_HOST=nb.batiao8.com
|
||||||
|
TARO_APP_TIANDITU_KEY=ad8b251afd667ea8b8bc6f33df2ce198
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
# TARO_APP_ID="生产环境下的小程序 AppID"
|
TARO_APP_API_ORIGIN=https://nb.zuom8.cn
|
||||||
TARO_APP_ID="wx6d4f6f29c41aff93"
|
TARO_APP_API_HOST=nb.zuom8.cn
|
||||||
|
TARO_APP_TIANDITU_KEY=ad8b251afd667ea8b8bc6f33df2ce198
|
||||||
|
|
|
||||||
21
AGENTS.md
21
AGENTS.md
|
|
@ -23,17 +23,22 @@ Other targets follow `pnpm dev:<platform>` / `pnpm build:<platform>` (`alipay`,
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The app is a thin native shell around a business H5, with a native payment bridge. Two pages, registered in `src/app.config.ts`:
|
Native mini-program for map-label progress query. Pages in `src/app.config.ts`:
|
||||||
|
|
||||||
**1. `pages/index/index.tsx` — auth + WebView host.** On load it calls `Taro.login()` to get the WeChat `code`, then renders a full-screen `<WebView>` pointing at the business H5 (`http://niubsw.com/...?mpCode=<code>`). All business UI lives in that remote H5, not in this repo.
|
**`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.
|
||||||
|
|
||||||
**2. `pages/pay/index.tsx` — native payment bridge.** The H5 cannot invoke WeChat JSAPI pay while running inside the mini-program WebView, so it hands off to this native page. The handoff contract:
|
**`pages/map/index` — 微信原生地图选点.** Tap / locate / `chooseLocation` search, returns GCJ-02 `lng,lat`.
|
||||||
- The H5 detects it's inside the mini-program and calls `wxSdk.miniProgram.navigateTo({ url: '/pages/pay/index?order=<b64>&formInfo=<b64>' })`.
|
|
||||||
- Params are **base64-encoded JSON**, produced H5-side by `utf8ToBase64(v) = btoa(unescape(encodeURIComponent(JSON.stringify(v))))`.
|
|
||||||
- `order` already contains the complete WeChat JSAPI fields (`timeStamp`/`nonceStr`/`package`/`signType`/`paySign`). The page decodes it, auto-fires `Taro.requestPayment`, then `navigateBack()`s to the H5 on success. No backend call is made here.
|
|
||||||
- `temp.txt` (repo root) holds the H5-side snippet documenting this contract — keep the two sides in sync.
|
|
||||||
|
|
||||||
**`src/utils/base64.ts`** is the strict inverse of the H5 encoder. It hand-rolls a base64 decoder because **weapp has no global `atob`**, and normalizes `+`→space mangling from URL routing. Reuse `decodeOrderParam<T>(raw)` for any future base64 route params.
|
### 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`, `phone=13800138000`).
|
||||||
|
- 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`.
|
||||||
|
|
||||||
|
Do not reintroduce a business H5 WebView unless explicitly requested.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|
|
||||||
56
package.json
56
package.json
|
|
@ -29,7 +29,8 @@
|
||||||
"dev:rn": "npm run build:rn -- --watch",
|
"dev:rn": "npm run build:rn -- --watch",
|
||||||
"dev:qq": "npm run build:qq -- --watch",
|
"dev:qq": "npm run build:qq -- --watch",
|
||||||
"dev:jd": "npm run build:jd -- --watch",
|
"dev:jd": "npm run build:jd -- --watch",
|
||||||
"dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch"
|
"dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"development": [
|
"development": [
|
||||||
|
|
@ -47,48 +48,51 @@
|
||||||
"@babel/runtime": "^7.24.4",
|
"@babel/runtime": "^7.24.4",
|
||||||
"@tarojs/components": "4.2.0",
|
"@tarojs/components": "4.2.0",
|
||||||
"@tarojs/helper": "4.2.0",
|
"@tarojs/helper": "4.2.0",
|
||||||
"@tarojs/plugin-platform-weapp": "4.2.0",
|
"@tarojs/plugin-framework-react": "4.2.0",
|
||||||
"@tarojs/plugin-platform-alipay": "4.2.0",
|
"@tarojs/plugin-platform-alipay": "4.2.0",
|
||||||
"@tarojs/plugin-platform-tt": "4.2.0",
|
|
||||||
"@tarojs/plugin-platform-swan": "4.2.0",
|
|
||||||
"@tarojs/plugin-platform-jd": "4.2.0",
|
|
||||||
"@tarojs/plugin-platform-qq": "4.2.0",
|
|
||||||
"@tarojs/plugin-platform-h5": "4.2.0",
|
"@tarojs/plugin-platform-h5": "4.2.0",
|
||||||
"@tarojs/plugin-platform-harmony-hybrid": "4.2.0",
|
"@tarojs/plugin-platform-harmony-hybrid": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-jd": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-qq": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-swan": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-tt": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-weapp": "4.2.0",
|
||||||
|
"@tarojs/react": "4.2.0",
|
||||||
"@tarojs/runtime": "4.2.0",
|
"@tarojs/runtime": "4.2.0",
|
||||||
"@tarojs/shared": "4.2.0",
|
"@tarojs/shared": "4.2.0",
|
||||||
"@tarojs/taro": "4.2.0",
|
"@tarojs/taro": "4.2.0",
|
||||||
"@tarojs/plugin-framework-react": "4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"@tarojs/react": "4.2.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0"
|
||||||
"react": "^18.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tarojs/plugin-generator": "4.2.0",
|
|
||||||
"@commitlint/cli": "^19.8.1",
|
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
|
||||||
"lint-staged": "^16.1.2",
|
|
||||||
"husky": "^9.1.7",
|
|
||||||
"stylelint-config-standard": "^38.0.0",
|
|
||||||
"@babel/core": "^7.24.4",
|
"@babel/core": "^7.24.4",
|
||||||
"@babel/plugin-transform-class-properties": "7.25.9",
|
"@babel/plugin-transform-class-properties": "7.25.9",
|
||||||
"@tarojs/cli": "4.2.0",
|
|
||||||
"@tarojs/vite-runner": "4.2.0",
|
|
||||||
"babel-preset-taro": "4.2.0",
|
|
||||||
"eslint-config-taro": "4.2.0",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"stylelint": "^16.4.0",
|
|
||||||
"terser": "^5.30.4",
|
|
||||||
"vite": "^4.2.0",
|
|
||||||
"@babel/preset-react": "^7.24.1",
|
"@babel/preset-react": "^7.24.1",
|
||||||
|
"@commitlint/cli": "^19.8.1",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
|
"@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/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"babel-preset-taro": "4.2.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-taro": "4.2.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^4.4.0",
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^16.1.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"sass": "^1.75.0",
|
"sass": "^1.75.0",
|
||||||
|
"stylelint": "^16.4.0",
|
||||||
|
"stylelint-config-standard": "^38.0.0",
|
||||||
|
"terser": "^5.30.4",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"postcss": "^8.5.6",
|
"vite": "^4.2.0",
|
||||||
"@types/minimatch": "^5"
|
"vitest": "^2.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
597
pnpm-lock.yaml
597
pnpm-lock.yaml
|
|
@ -56,6 +56,9 @@ importers:
|
||||||
'@tarojs/taro':
|
'@tarojs/taro':
|
||||||
specifier: 4.2.0
|
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)
|
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
|
||||||
react:
|
react:
|
||||||
specifier: ^18.0.0
|
specifier: ^18.0.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
|
|
@ -87,6 +90,9 @@ importers:
|
||||||
'@tarojs/vite-runner':
|
'@tarojs/vite-runner':
|
||||||
specifier: 4.2.0
|
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))
|
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':
|
'@types/minimatch':
|
||||||
specifier: ^5
|
specifier: ^5
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
|
|
@ -141,6 +147,9 @@ importers:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.5.14(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
version: 4.5.14(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
||||||
|
vitest:
|
||||||
|
specifier: ^2.1.9
|
||||||
|
version: 2.1.9(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|
@ -1253,6 +1262,13 @@ packages:
|
||||||
'@keyv/serialize@1.1.1':
|
'@keyv/serialize@1.1.1':
|
||||||
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
|
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
|
||||||
|
|
||||||
|
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||||
|
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
|
||||||
|
engines: {node: ^22.20 || ^24.12 || >=25}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/triples@1.2.0':
|
'@napi-rs/triples@1.2.0':
|
||||||
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
|
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
|
||||||
|
|
||||||
|
|
@ -1425,6 +1441,144 @@ packages:
|
||||||
rollup:
|
rollup:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm-eabi@4.62.4':
|
||||||
|
resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-arm64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-x64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-arm64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-x64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-gnueabihf@4.62.4':
|
||||||
|
resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-musleabihf@4.62.4':
|
||||||
|
resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-musl@4.62.4':
|
||||||
|
resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-loong64-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-loong64-musl@4.62.4':
|
||||||
|
resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-ppc64-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-ppc64-musl@4.62.4':
|
||||||
|
resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-riscv64-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-riscv64-musl@4.62.4':
|
||||||
|
resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-s390x-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-musl@4.62.4':
|
||||||
|
resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rollup/rollup-openbsd-x64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@rollup/rollup-openharmony-arm64@4.62.4':
|
||||||
|
resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-arm64-msvc@4.62.4':
|
||||||
|
resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-ia32-msvc@4.62.4':
|
||||||
|
resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-x64-gnu@4.62.4':
|
||||||
|
resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-x64-msvc@4.62.4':
|
||||||
|
resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
|
|
@ -1927,6 +2081,9 @@ packages:
|
||||||
'@types/conventional-commits-parser@5.0.2':
|
'@types/conventional-commits-parser@5.0.2':
|
||||||
resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==}
|
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':
|
'@types/debug@4.1.13':
|
||||||
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
||||||
|
|
||||||
|
|
@ -2056,6 +2213,35 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@vitest/expect@2.1.9':
|
||||||
|
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||||
|
|
||||||
|
'@vitest/mocker@2.1.9':
|
||||||
|
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@2.1.9':
|
||||||
|
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
|
||||||
|
|
||||||
|
'@vitest/runner@2.1.9':
|
||||||
|
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@2.1.9':
|
||||||
|
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
|
||||||
|
|
||||||
|
'@vitest/spy@2.1.9':
|
||||||
|
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
||||||
|
|
||||||
|
'@vitest/utils@2.1.9':
|
||||||
|
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
||||||
|
|
||||||
JSONStream@1.3.5:
|
JSONStream@1.3.5:
|
||||||
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -2171,6 +2357,10 @@ packages:
|
||||||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
astral-regex@2.0.0:
|
astral-regex@2.0.0:
|
||||||
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2326,6 +2516,10 @@ packages:
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
|
cac@6.7.14:
|
||||||
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
cacheable-request@2.1.4:
|
cacheable-request@2.1.4:
|
||||||
resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==}
|
resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==}
|
||||||
|
|
||||||
|
|
@ -2368,6 +2562,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==}
|
resolution: {integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
chai@5.3.3:
|
||||||
|
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
chalk@3.0.0:
|
chalk@3.0.0:
|
||||||
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
|
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2386,6 +2584,10 @@ packages:
|
||||||
chardet@2.1.1:
|
chardet@2.1.1:
|
||||||
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
|
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
|
||||||
|
|
||||||
|
check-error@2.1.3:
|
||||||
|
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
@ -2547,6 +2749,10 @@ packages:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
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:
|
css-functions-list@3.3.3:
|
||||||
resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==}
|
resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -2636,6 +2842,10 @@ packages:
|
||||||
babel-plugin-macros:
|
babel-plugin-macros:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
deep-eql@5.0.2:
|
||||||
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
deep-extend@0.6.0:
|
||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
@ -2759,6 +2969,9 @@ packages:
|
||||||
resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==}
|
resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-module-lexer@1.7.0:
|
||||||
|
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||||
|
|
||||||
es-object-atoms@1.1.2:
|
es-object-atoms@1.1.2:
|
||||||
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -2910,6 +3123,9 @@ packages:
|
||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
esutils@2.0.3:
|
esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -2917,6 +3133,10 @@ packages:
|
||||||
eventemitter3@5.0.4:
|
eventemitter3@5.0.4:
|
||||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
|
expect-type@1.4.0:
|
||||||
|
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
ext-list@2.2.2:
|
ext-list@2.2.2:
|
||||||
resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==}
|
resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -3748,6 +3968,9 @@ packages:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
loupe@3.2.1:
|
||||||
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
|
||||||
lower-case@1.1.4:
|
lower-case@1.1.4:
|
||||||
resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==}
|
resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==}
|
||||||
|
|
||||||
|
|
@ -4069,6 +4292,13 @@ packages:
|
||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
pathe@1.1.2:
|
||||||
|
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||||
|
|
||||||
|
pathval@2.0.1:
|
||||||
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
pend@1.2.0:
|
pend@1.2.0:
|
||||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||||
|
|
||||||
|
|
@ -4408,6 +4638,11 @@ packages:
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rollup@4.62.4:
|
||||||
|
resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
run-async@2.4.1:
|
run-async@2.4.1:
|
||||||
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
@ -4526,6 +4761,9 @@ packages:
|
||||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
signal-exit@3.0.7:
|
signal-exit@3.0.7:
|
||||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
|
|
||||||
|
|
@ -4590,6 +4828,12 @@ packages:
|
||||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
engines: {node: '>= 10.x'}
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
|
std-env@3.10.0:
|
||||||
|
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -4753,10 +4997,28 @@ packages:
|
||||||
tiny-case@1.0.3:
|
tiny-case@1.0.3:
|
||||||
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||||
|
|
||||||
|
tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@0.3.2:
|
||||||
|
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||||
|
|
||||||
tinyexec@1.2.4:
|
tinyexec@1.2.4:
|
||||||
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tinypool@1.1.1:
|
||||||
|
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
||||||
|
tinyrainbow@1.2.0:
|
||||||
|
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tinyspy@3.0.2:
|
||||||
|
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
to-buffer@1.2.2:
|
to-buffer@1.2.2:
|
||||||
resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
|
resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -4920,6 +5182,11 @@ packages:
|
||||||
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
|
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
vite-node@2.1.9:
|
||||||
|
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vite-plugin-static-copy@0.17.1:
|
vite-plugin-static-copy@0.17.1:
|
||||||
resolution: {integrity: sha512-9h3iaVs0bqnqZOM5YHJXGHqdC5VAVlTZ2ARYsuNpzhEJUHmFqXY7dAK4ZFpjEQ4WLFKcaN8yWbczr81n01U4sQ==}
|
resolution: {integrity: sha512-9h3iaVs0bqnqZOM5YHJXGHqdC5VAVlTZ2ARYsuNpzhEJUHmFqXY7dAK4ZFpjEQ4WLFKcaN8yWbczr81n01U4sQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
@ -4954,6 +5221,62 @@ packages:
|
||||||
terser:
|
terser:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vite@5.4.21:
|
||||||
|
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^18.0.0 || >=20.0.0
|
||||||
|
less: '*'
|
||||||
|
lightningcss: ^1.21.0
|
||||||
|
sass: '*'
|
||||||
|
sass-embedded: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
lightningcss:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
vitest@2.1.9:
|
||||||
|
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@types/node': ^18.0.0 || >=20.0.0
|
||||||
|
'@vitest/browser': 2.1.9
|
||||||
|
'@vitest/ui': 2.1.9
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||||
|
|
||||||
|
|
@ -4993,6 +5316,11 @@ packages:
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
wildcard@2.0.1:
|
wildcard@2.0.1:
|
||||||
resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
|
resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==}
|
||||||
|
|
||||||
|
|
@ -6303,6 +6631,9 @@ snapshots:
|
||||||
|
|
||||||
'@keyv/serialize@1.1.1': {}
|
'@keyv/serialize@1.1.1': {}
|
||||||
|
|
||||||
|
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/triples@1.2.0': {}
|
'@napi-rs/triples@1.2.0': {}
|
||||||
|
|
||||||
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
|
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
|
||||||
|
|
@ -6435,6 +6766,81 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
rollup: 3.30.0
|
rollup: 3.30.0
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm-eabi@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-android-arm64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-arm64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-darwin-x64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-arm64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-freebsd-x64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-gnueabihf@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm-musleabihf@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-arm64-musl@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-loong64-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-loong64-musl@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-ppc64-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-ppc64-musl@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-riscv64-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-riscv64-musl@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-s390x-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-linux-x64-musl@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-openbsd-x64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-openharmony-arm64@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-arm64-msvc@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-ia32-msvc@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-x64-gnu@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/rollup-win32-x64-msvc@4.62.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@sideway/address@4.1.5':
|
'@sideway/address@4.1.5':
|
||||||
|
|
@ -7028,6 +7434,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.9.2
|
'@types/node': 25.9.2
|
||||||
|
|
||||||
|
'@types/crypto-js@4.2.2': {}
|
||||||
|
|
||||||
'@types/debug@4.1.13':
|
'@types/debug@4.1.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
@ -7201,6 +7609,46 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vitest/expect@2.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 2.1.9
|
||||||
|
'@vitest/utils': 2.1.9
|
||||||
|
chai: 5.3.3
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
|
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 2.1.9
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 5.4.21(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
||||||
|
|
||||||
|
'@vitest/pretty-format@2.1.9':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
|
'@vitest/runner@2.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 2.1.9
|
||||||
|
pathe: 1.1.2
|
||||||
|
|
||||||
|
'@vitest/snapshot@2.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 2.1.9
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 1.1.2
|
||||||
|
|
||||||
|
'@vitest/spy@2.1.9':
|
||||||
|
dependencies:
|
||||||
|
tinyspy: 3.0.2
|
||||||
|
|
||||||
|
'@vitest/utils@2.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 2.1.9
|
||||||
|
loupe: 3.2.1
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
JSONStream@1.3.5:
|
JSONStream@1.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
jsonparse: 1.3.1
|
jsonparse: 1.3.1
|
||||||
|
|
@ -7344,6 +7792,8 @@ snapshots:
|
||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
is-array-buffer: 3.0.5
|
is-array-buffer: 3.0.5
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
astral-regex@2.0.0: {}
|
astral-regex@2.0.0: {}
|
||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
@ -7529,6 +7979,8 @@ snapshots:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
cac@6.7.14: {}
|
||||||
|
|
||||||
cacheable-request@2.1.4:
|
cacheable-request@2.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
clone-response: 1.0.2
|
clone-response: 1.0.2
|
||||||
|
|
@ -7601,6 +8053,14 @@ snapshots:
|
||||||
tunnel-agent: 0.6.0
|
tunnel-agent: 0.6.0
|
||||||
url-to-options: 1.0.1
|
url-to-options: 1.0.1
|
||||||
|
|
||||||
|
chai@5.3.3:
|
||||||
|
dependencies:
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
check-error: 2.1.3
|
||||||
|
deep-eql: 5.0.2
|
||||||
|
loupe: 3.2.1
|
||||||
|
pathval: 2.0.1
|
||||||
|
|
||||||
chalk@3.0.0:
|
chalk@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
|
|
@ -7630,6 +8090,8 @@ snapshots:
|
||||||
|
|
||||||
chardet@2.1.1: {}
|
chardet@2.1.1: {}
|
||||||
|
|
||||||
|
check-error@2.1.3: {}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
|
|
@ -7801,6 +8263,8 @@ snapshots:
|
||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
crypto-js@4.2.0: {}
|
||||||
|
|
||||||
css-functions-list@3.3.3: {}
|
css-functions-list@3.3.3: {}
|
||||||
|
|
||||||
css-tree@3.2.1:
|
css-tree@3.2.1:
|
||||||
|
|
@ -7888,6 +8352,8 @@ snapshots:
|
||||||
|
|
||||||
dedent@1.7.2: {}
|
dedent@1.7.2: {}
|
||||||
|
|
||||||
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
deep-extend@0.6.0: {}
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
|
@ -8079,6 +8545,8 @@ snapshots:
|
||||||
iterator.prototype: 1.1.5
|
iterator.prototype: 1.1.5
|
||||||
math-intrinsics: 1.1.0
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
es-module-lexer@1.7.0: {}
|
||||||
|
|
||||||
es-object-atoms@1.1.2:
|
es-object-atoms@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
@ -8368,10 +8836,16 @@ snapshots:
|
||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
eventemitter3@5.0.4: {}
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
|
expect-type@1.4.0: {}
|
||||||
|
|
||||||
ext-list@2.2.2:
|
ext-list@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.54.0
|
mime-db: 1.54.0
|
||||||
|
|
@ -9216,6 +9690,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
loupe@3.2.1: {}
|
||||||
|
|
||||||
lower-case@1.1.4: {}
|
lower-case@1.1.4: {}
|
||||||
|
|
||||||
lower-case@2.0.2:
|
lower-case@2.0.2:
|
||||||
|
|
@ -9526,6 +10002,10 @@ snapshots:
|
||||||
|
|
||||||
path-type@4.0.0: {}
|
path-type@4.0.0: {}
|
||||||
|
|
||||||
|
pathe@1.1.2: {}
|
||||||
|
|
||||||
|
pathval@2.0.1: {}
|
||||||
|
|
||||||
pend@1.2.0: {}
|
pend@1.2.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
@ -9849,6 +10329,38 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
rollup@4.62.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.9
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/lzma-linux-x64-gnu': 1.5.1
|
||||||
|
'@rollup/rollup-android-arm-eabi': 4.62.4
|
||||||
|
'@rollup/rollup-android-arm64': 4.62.4
|
||||||
|
'@rollup/rollup-darwin-arm64': 4.62.4
|
||||||
|
'@rollup/rollup-darwin-x64': 4.62.4
|
||||||
|
'@rollup/rollup-freebsd-arm64': 4.62.4
|
||||||
|
'@rollup/rollup-freebsd-x64': 4.62.4
|
||||||
|
'@rollup/rollup-linux-arm-gnueabihf': 4.62.4
|
||||||
|
'@rollup/rollup-linux-arm-musleabihf': 4.62.4
|
||||||
|
'@rollup/rollup-linux-arm64-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-linux-arm64-musl': 4.62.4
|
||||||
|
'@rollup/rollup-linux-loong64-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-linux-loong64-musl': 4.62.4
|
||||||
|
'@rollup/rollup-linux-ppc64-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-linux-ppc64-musl': 4.62.4
|
||||||
|
'@rollup/rollup-linux-riscv64-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-linux-riscv64-musl': 4.62.4
|
||||||
|
'@rollup/rollup-linux-s390x-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-linux-x64-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-linux-x64-musl': 4.62.4
|
||||||
|
'@rollup/rollup-openbsd-x64': 4.62.4
|
||||||
|
'@rollup/rollup-openharmony-arm64': 4.62.4
|
||||||
|
'@rollup/rollup-win32-arm64-msvc': 4.62.4
|
||||||
|
'@rollup/rollup-win32-ia32-msvc': 4.62.4
|
||||||
|
'@rollup/rollup-win32-x64-gnu': 4.62.4
|
||||||
|
'@rollup/rollup-win32-x64-msvc': 4.62.4
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
run-async@2.4.1: {}
|
run-async@2.4.1: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
|
|
@ -10005,6 +10517,8 @@ snapshots:
|
||||||
side-channel-map: 1.0.1
|
side-channel-map: 1.0.1
|
||||||
side-channel-weakmap: 1.0.2
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
signal-exit@3.0.7: {}
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
@ -10065,6 +10579,10 @@ snapshots:
|
||||||
|
|
||||||
split2@4.2.0: {}
|
split2@4.2.0: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
|
std-env@3.10.0: {}
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
@ -10290,8 +10808,18 @@ snapshots:
|
||||||
|
|
||||||
tiny-case@1.0.3: {}
|
tiny-case@1.0.3: {}
|
||||||
|
|
||||||
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@0.3.2: {}
|
||||||
|
|
||||||
tinyexec@1.2.4: {}
|
tinyexec@1.2.4: {}
|
||||||
|
|
||||||
|
tinypool@1.1.1: {}
|
||||||
|
|
||||||
|
tinyrainbow@1.2.0: {}
|
||||||
|
|
||||||
|
tinyspy@3.0.2: {}
|
||||||
|
|
||||||
to-buffer@1.2.2:
|
to-buffer@1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isarray: 2.0.5
|
isarray: 2.0.5
|
||||||
|
|
@ -10450,6 +10978,24 @@ snapshots:
|
||||||
|
|
||||||
validate-npm-package-name@5.0.1: {}
|
validate-npm-package-name@5.0.1: {}
|
||||||
|
|
||||||
|
vite-node@2.1.9(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0):
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.4.3
|
||||||
|
es-module-lexer: 1.7.0
|
||||||
|
pathe: 1.1.2
|
||||||
|
vite: 5.4.21(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
|
||||||
vite-plugin-static-copy@0.17.1(vite@4.5.14(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)):
|
vite-plugin-static-copy@0.17.1(vite@4.5.14(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
|
|
@ -10469,6 +11015,52 @@ snapshots:
|
||||||
sass: 1.100.0
|
sass: 1.100.0
|
||||||
terser: 5.48.0
|
terser: 5.48.0
|
||||||
|
|
||||||
|
vite@5.4.21(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.21.5
|
||||||
|
postcss: 8.5.15
|
||||||
|
rollup: 4.62.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 25.9.2
|
||||||
|
fsevents: 2.3.3
|
||||||
|
sass: 1.100.0
|
||||||
|
terser: 5.48.0
|
||||||
|
|
||||||
|
vitest@2.1.9(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 2.1.9
|
||||||
|
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0))
|
||||||
|
'@vitest/pretty-format': 2.1.9
|
||||||
|
'@vitest/runner': 2.1.9
|
||||||
|
'@vitest/snapshot': 2.1.9
|
||||||
|
'@vitest/spy': 2.1.9
|
||||||
|
'@vitest/utils': 2.1.9
|
||||||
|
chai: 5.3.3
|
||||||
|
debug: 4.4.3
|
||||||
|
expect-type: 1.4.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 1.1.2
|
||||||
|
std-env: 3.10.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 0.3.2
|
||||||
|
tinypool: 1.1.1
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
vite: 5.4.21(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
||||||
|
vite-node: 2.1.9(@types/node@25.9.2)(sass@1.100.0)(terser@5.48.0)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 25.9.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- msw
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
|
||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
|
|
@ -10532,6 +11124,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
wildcard@2.0.1: {}
|
wildcard@2.0.1: {}
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { request } from '@/lib/request'
|
||||||
|
|
||||||
|
export function getOrdersByPhone (phone: string) {
|
||||||
|
return request({
|
||||||
|
url: '/api/h5/order/phone',
|
||||||
|
method: 'GET',
|
||||||
|
params: { phone },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateOrderExtra (data: Record<string, any>) {
|
||||||
|
return request({
|
||||||
|
url: '/api/h5/order',
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { request } from '@/lib/request'
|
||||||
|
import { applyCorpInit } from '@/lib/session'
|
||||||
|
import { buildFileUrl, getFileExtension, pickPresignResult, QINIU_UPLOAD_URL } from '@/lib/qiniu'
|
||||||
|
|
||||||
|
export function getUserConfig (params: Record<string, any> = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/user/config',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initCorp (params: Record<string, any> = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/h5/corp',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
}).then((res) => {
|
||||||
|
applyCorpInit(res)
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
pages: [
|
pages: [
|
||||||
'pages/index/index',
|
'pages/index/index',
|
||||||
'pages/landing/index',
|
'pages/map/index',
|
||||||
'pages/pay/index',
|
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
backgroundTextStyle: 'light',
|
backgroundTextStyle: 'light',
|
||||||
navigationBarBackgroundColor: '#fff',
|
navigationBarBackgroundColor: '#2F4FEE',
|
||||||
navigationBarTitleText: 'WeChat',
|
navigationBarTitleText: '网上快办平台',
|
||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'white'
|
||||||
|
},
|
||||||
|
permission: {
|
||||||
|
'scope.userLocation': {
|
||||||
|
desc: '用于选择门店地图位置'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
requiredPrivateInfos: [
|
||||||
|
'getLocation',
|
||||||
|
'chooseLocation',
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB |
|
|
@ -0,0 +1,47 @@
|
||||||
|
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('获取临时登录态失败')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
export function pickToken (payload: any): string {
|
||||||
|
const first = payload?.data ?? payload
|
||||||
|
const nested = first?.data && typeof first.data === 'object' && ('token' in first.data || !first.token)
|
||||||
|
? first.data
|
||||||
|
: first
|
||||||
|
const token = nested?.token ?? first?.token ?? payload?.token
|
||||||
|
return typeof token === 'string' ? token.trim() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureSessionToken (input: {
|
||||||
|
token?: string
|
||||||
|
fetchUserConfig: () => Promise<unknown>
|
||||||
|
}) {
|
||||||
|
const existing = String(input.token || '').trim()
|
||||||
|
if (existing) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = pickToken(await input.fetchUserConfig())
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('获取临时登录态失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { getUserConfig, initCorp } from '@/api/user'
|
||||||
|
import { ensureSessionToken } from './auth'
|
||||||
|
import { applyLaunchParams, applyServiceConfig, getSession, setSession } from './session'
|
||||||
|
|
||||||
|
export async function bootstrapSession (raw: Record<string, string | undefined> = {}) {
|
||||||
|
applyLaunchParams(raw)
|
||||||
|
try {
|
||||||
|
applyServiceConfig(await getUserConfig())
|
||||||
|
} catch {
|
||||||
|
// 配置拉取失败时仍可继续查询
|
||||||
|
}
|
||||||
|
const token = await ensureSessionToken({
|
||||||
|
token: getSession().token,
|
||||||
|
fetchUserConfig: () => getUserConfig(),
|
||||||
|
})
|
||||||
|
setSession({ token })
|
||||||
|
try {
|
||||||
|
await initCorp()
|
||||||
|
} catch {
|
||||||
|
// 已有临时 token 时,初始化失败不阻断查询
|
||||||
|
}
|
||||||
|
return getSession()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
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 ENTITY_LABELS = {
|
||||||
|
entityName: '门店名称',
|
||||||
|
entityAddress: '门店地址',
|
||||||
|
entityMapLocation: '门店定位',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { hasServiceContact, pickServiceContact } from './contact'
|
||||||
|
|
||||||
|
describe('pickServiceContact', () => {
|
||||||
|
it('reads mobile online phone and weixin from client config', () => {
|
||||||
|
expect(pickServiceContact({
|
||||||
|
'client.service.online.mobile': 'https://chat.example.com',
|
||||||
|
'client.service.phone': '4001166311',
|
||||||
|
'client.service.weixin': 'https://work.weixin.qq.com/kfid/abc',
|
||||||
|
})).toEqual({
|
||||||
|
online: 'https://chat.example.com',
|
||||||
|
phone: '4001166311',
|
||||||
|
weixin: 'https://work.weixin.qq.com/kfid/abc',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to flattened service fields', () => {
|
||||||
|
expect(pickServiceContact({
|
||||||
|
service_phone: '4000000000',
|
||||||
|
service_weixin: 'https://u.wechat.com/xyz',
|
||||||
|
})).toEqual({
|
||||||
|
online: '',
|
||||||
|
phone: '4000000000',
|
||||||
|
weixin: 'https://u.wechat.com/xyz',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hasServiceContact', () => {
|
||||||
|
it('is true when any contact channel exists', () => {
|
||||||
|
expect(hasServiceContact({ 'client.service.phone': '4001166311' })).toBe(true)
|
||||||
|
expect(hasServiceContact({})).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
export type ServiceContact = {
|
||||||
|
online: string
|
||||||
|
phone: string
|
||||||
|
weixin: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function asText (value: unknown) {
|
||||||
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickServiceContact (config?: Record<string, any> | null): ServiceContact {
|
||||||
|
return {
|
||||||
|
online: asText(config?.['client.service.online.mobile'] || config?.service_online),
|
||||||
|
phone: asText(config?.['client.service.phone'] || config?.service_phone),
|
||||||
|
weixin: asText(config?.['client.service.weixin'] || config?.service_weixin),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasServiceContact (config?: Record<string, any> | null) {
|
||||||
|
const contact = pickServiceContact(config)
|
||||||
|
return Boolean(contact.online || contact.phone || contact.weixin)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeServiceConfig (current: Record<string, any> | undefined, payload: any) {
|
||||||
|
const data = payload?.data ?? payload
|
||||||
|
const incoming = data?.config && typeof data.config === 'object' ? data.config : {}
|
||||||
|
const merged = {
|
||||||
|
...(current || {}),
|
||||||
|
...incoming,
|
||||||
|
}
|
||||||
|
if (merged['client.service.phone']) merged.service_phone = merged['client.service.phone']
|
||||||
|
if (merged['client.service.weixin']) merged.service_weixin = merged['client.service.weixin']
|
||||||
|
if (merged['client.service.online.mobile']) merged.service_online = merged['client.service.online.mobile']
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
gcj02ToWgs84,
|
||||||
|
isRegionChangeEnd,
|
||||||
|
isUserRegionChange,
|
||||||
|
parseGeocoderResult,
|
||||||
|
pickMapCenter,
|
||||||
|
} from './geocode'
|
||||||
|
|
||||||
|
describe('pickMapCenter', () => {
|
||||||
|
it('reads nested WeChat regionchange centerLocation', () => {
|
||||||
|
expect(pickMapCenter({
|
||||||
|
detail: {
|
||||||
|
type: 'end',
|
||||||
|
causedBy: 'drag',
|
||||||
|
detail: {
|
||||||
|
centerLocation: { longitude: 121.4737, latitude: 31.2304 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})).toEqual({ lng: 121.4737, lat: 31.2304 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads a flat centerLocation', () => {
|
||||||
|
expect(pickMapCenter({
|
||||||
|
detail: { centerLocation: { longitude: 116.4, latitude: 39.9 } },
|
||||||
|
})).toEqual({ lng: 116.4, lat: 39.9 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('region change helpers', () => {
|
||||||
|
it('detects a finished user drag', () => {
|
||||||
|
const event = { detail: { type: 'end', causedBy: 'drag' } }
|
||||||
|
expect(isRegionChangeEnd(event)).toBe(true)
|
||||||
|
expect(isUserRegionChange(event)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores programmatic updates and begin events', () => {
|
||||||
|
expect(isRegionChangeEnd({ detail: { type: 'begin', causedBy: 'gesture' } })).toBe(false)
|
||||||
|
expect(isUserRegionChange({ detail: { type: 'end', causedBy: 'update' } })).toBe(false)
|
||||||
|
expect(isUserRegionChange({ detail: { type: 'end' } })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parseGeocoderResult', () => {
|
||||||
|
it('prefers formatted address and poi name', () => {
|
||||||
|
expect(parseGeocoderResult({
|
||||||
|
result: {
|
||||||
|
formatted_address: '上海市黄浦区南京东路1号',
|
||||||
|
addressComponent: { poi: '外滩' },
|
||||||
|
},
|
||||||
|
})).toEqual({
|
||||||
|
address: '上海市黄浦区南京东路1号',
|
||||||
|
name: '外滩',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the formatted address when poi is missing', () => {
|
||||||
|
expect(parseGeocoderResult({
|
||||||
|
result: { formatted_address: '北京市东城区' },
|
||||||
|
})).toEqual({
|
||||||
|
address: '北京市东城区',
|
||||||
|
name: '北京市东城区',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('gcj02ToWgs84', () => {
|
||||||
|
it('matches the H5 GCJ-02 to Tianditu conversion for Shanghai', () => {
|
||||||
|
const [lng, lat] = gcj02ToWgs84(121.4737, 31.2304)
|
||||||
|
expect(lng).toBeCloseTo(121.469162, 6)
|
||||||
|
expect(lat).toBeCloseTo(31.232329, 6)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
const GCJ_PI = Math.PI
|
||||||
|
const GCJ_A = 6378245
|
||||||
|
const GCJ_EE = 0.006693421622965943
|
||||||
|
export const TIANDITU_GEOCODER_URL = 'https://api.tianditu.gov.cn/geocoder'
|
||||||
|
|
||||||
|
export type GeoPoint = { lng: number, lat: number }
|
||||||
|
export type GeoPlace = { address: string, name: string }
|
||||||
|
|
||||||
|
function isOutsideChina (lng: number, lat: number) {
|
||||||
|
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformLatitude (lng: number, lat: number) {
|
||||||
|
let result = -100 + 2 * lng + 3 * lat + 0.2 * lat * lat + 0.1 * lng * lat
|
||||||
|
result += 0.2 * Math.sqrt(Math.abs(lng))
|
||||||
|
result += (20 * Math.sin(6 * lng * GCJ_PI) + 20 * Math.sin(2 * lng * GCJ_PI)) * 2 / 3
|
||||||
|
result += (20 * Math.sin(lat * GCJ_PI) + 40 * Math.sin(lat / 3 * GCJ_PI)) * 2 / 3
|
||||||
|
result += (160 * Math.sin(lat / 12 * GCJ_PI) + 320 * Math.sin(lat * GCJ_PI / 30)) * 2 / 3
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformLongitude (lng: number, lat: number) {
|
||||||
|
let result = 300 + lng + 2 * lat + 0.1 * lng * lng + 0.1 * lng * lat
|
||||||
|
result += 0.1 * Math.sqrt(Math.abs(lng))
|
||||||
|
result += (20 * Math.sin(6 * lng * GCJ_PI) + 20 * Math.sin(2 * lng * GCJ_PI)) * 2 / 3
|
||||||
|
result += (20 * Math.sin(lng * GCJ_PI) + 40 * Math.sin(lng / 3 * GCJ_PI)) * 2 / 3
|
||||||
|
result += (150 * Math.sin(lng / 12 * GCJ_PI) + 300 * Math.sin(lng / 30 * GCJ_PI)) * 2 / 3
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGcjOffset (lng: number, lat: number): GeoPoint {
|
||||||
|
const latitudeRadians = lat / 180 * GCJ_PI
|
||||||
|
const magicBase = 1 - GCJ_EE * Math.sin(latitudeRadians) ** 2
|
||||||
|
const magicRoot = Math.sqrt(magicBase)
|
||||||
|
const latitudeOffset =
|
||||||
|
transformLatitude(lng - 105, lat - 35) * 180 /
|
||||||
|
((GCJ_A * (1 - GCJ_EE) / (magicBase * magicRoot)) * GCJ_PI)
|
||||||
|
const longitudeOffset =
|
||||||
|
transformLongitude(lng - 105, lat - 35) * 180 /
|
||||||
|
((GCJ_A / magicRoot * Math.cos(latitudeRadians)) * GCJ_PI)
|
||||||
|
return { lng: longitudeOffset, lat: latitudeOffset }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gcj02ToWgs84 (lng: number, lat: number): [number, number] {
|
||||||
|
if (isOutsideChina(lng, lat)) return [lng, lat]
|
||||||
|
let estimateLng = lng
|
||||||
|
let estimateLat = lat
|
||||||
|
for (let index = 0; index < 3; index += 1) {
|
||||||
|
const offset = getGcjOffset(estimateLng, estimateLat)
|
||||||
|
estimateLng = estimateLng + lng - (estimateLng + offset.lng)
|
||||||
|
estimateLat = estimateLat + lat - (estimateLat + offset.lat)
|
||||||
|
}
|
||||||
|
return [estimateLng, estimateLat]
|
||||||
|
}
|
||||||
|
|
||||||
|
function asRecord (value: unknown): Record<string, any> | null {
|
||||||
|
return value && typeof value === 'object' ? value as Record<string, any> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickMapCenter (event: any): GeoPoint | null {
|
||||||
|
const detail = asRecord(event?.detail) || asRecord(event)
|
||||||
|
const inner = asRecord(detail?.detail) || detail
|
||||||
|
const center = asRecord(inner?.centerLocation) || asRecord(detail?.centerLocation)
|
||||||
|
const lng = Number(center?.longitude)
|
||||||
|
const lat = Number(center?.latitude)
|
||||||
|
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
|
||||||
|
return { lng, lat }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRegionChangeEnd (event: any) {
|
||||||
|
const detail = asRecord(event?.detail) || asRecord(event)
|
||||||
|
const type = detail?.type || asRecord(detail?.detail)?.type || event?.type
|
||||||
|
return type === 'end'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUserRegionChange (event: any) {
|
||||||
|
const detail = asRecord(event?.detail) || asRecord(event)
|
||||||
|
const causedBy = detail?.causedBy || asRecord(detail?.detail)?.causedBy
|
||||||
|
return causedBy !== 'update'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseGeocoderResult (payload: any): GeoPlace {
|
||||||
|
const result = asRecord(payload?.result) || asRecord(payload) || {}
|
||||||
|
const address = typeof result.formatted_address === 'string' ? result.formatted_address : ''
|
||||||
|
const poi = asRecord(result.addressComponent)?.poi
|
||||||
|
const name = typeof poi === 'string' && poi ? poi : address
|
||||||
|
return { address, name }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
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: 'wx6d4f6f29c41aff93',
|
||||||
|
path: 'pages/index/index',
|
||||||
|
scene: '/h/KZ9Q',
|
||||||
|
token: '6224a1fe-d88c-443c-8972-e6a0942d17fd',
|
||||||
|
host: 'nb.batiao8.com',
|
||||||
|
package: '10044',
|
||||||
|
phone: '13800138000',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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('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: '6224a1fe-d88c-443c-8972-e6a0942d17fd',
|
||||||
|
host: 'nb.batiao8.com',
|
||||||
|
packageId: '10044',
|
||||||
|
scene: '/h/KZ9Q',
|
||||||
|
linkId: 'KZ9Q',
|
||||||
|
shareId: '',
|
||||||
|
prevId: '',
|
||||||
|
phone: '13800138000',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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: '13800138000',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
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=wx6d4f6f29c41aff93&path=pages/index/index&query=scene%3D%2Fh%2FKZ9Q%26token%3D6224a1fe-d88c-443c-8972-e6a0942d17fd%26host%3Dnb.batiao8.com%26package%3D10044%26phone%3D13800138000'
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLaunchQuery (raw: Record<string, string | undefined> = {}): LaunchQuery {
|
||||||
|
const scene = safeDecode(asText(raw.scene))
|
||||||
|
return {
|
||||||
|
token: asText(raw.token),
|
||||||
|
host: asText(raw.host),
|
||||||
|
packageId: asText(raw.package),
|
||||||
|
scene,
|
||||||
|
phone: asText(raw.phone),
|
||||||
|
...parseSceneIds(scene),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLaunchQuery (raw: Record<string, string | undefined> = {}): LaunchQuery {
|
||||||
|
const parsed = parseLaunchQuery(raw)
|
||||||
|
const defaults = parseLaunchQuery(parseMiniProgramScheme(DEFAULT_LAUNCH_SCHEME))
|
||||||
|
const scene = parsed.scene || defaults.scene
|
||||||
|
const sceneIds = parsed.scene
|
||||||
|
? { linkId: parsed.linkId, shareId: parsed.shareId, prevId: parsed.prevId }
|
||||||
|
: { linkId: defaults.linkId, shareId: defaults.shareId, prevId: defaults.prevId }
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: parsed.token || defaults.token,
|
||||||
|
host: parsed.host || defaults.host,
|
||||||
|
packageId: parsed.packageId || defaults.packageId,
|
||||||
|
scene,
|
||||||
|
phone: parsed.phone || defaults.phone,
|
||||||
|
...sceneIds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
buildResubmitPayload,
|
||||||
|
filterMapLabelOrders,
|
||||||
|
formatMapCoordinate,
|
||||||
|
getOrderAddressName,
|
||||||
|
getOrderExtraImages,
|
||||||
|
getOrderMapAddress,
|
||||||
|
joinUploadedImageIds,
|
||||||
|
parseCoordinate,
|
||||||
|
} from './order'
|
||||||
|
|
||||||
|
const mapOrder = {
|
||||||
|
order_id: '4',
|
||||||
|
goods_type: 'map_label',
|
||||||
|
entity_name: '老店',
|
||||||
|
entity_phone: '13800138000',
|
||||||
|
entity_extra: { keep: 1 },
|
||||||
|
extra: {
|
||||||
|
entity_address_name: '南京路1号',
|
||||||
|
entity_address: '121.473700,31.230400',
|
||||||
|
entity_storefront_image: '11,12',
|
||||||
|
entity_storefront_image_url: 'https://cdn.example/a.jpg,https://cdn.example/b.jpg',
|
||||||
|
origin_price: '99',
|
||||||
|
goods_param: '{"client.goods.icon":["https://cdn.example/i.png"]}',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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('order address helpers', () => {
|
||||||
|
it('splits named address and coordinate address', () => {
|
||||||
|
expect(getOrderAddressName(mapOrder as any)).toBe('南京路1号')
|
||||||
|
expect(getOrderMapAddress(mapOrder as any)).toBe('121.473700,31.230400')
|
||||||
|
expect(parseCoordinate('121.4737, 31.2304')).toEqual({ lng: 121.4737, lat: 31.2304 })
|
||||||
|
expect(formatMapCoordinate({ lng: 121.4737, lat: 31.2304 })).toBe('121.473700,31.230400')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('order images', () => {
|
||||||
|
it('reads extra image urls and joins uploaded ids', () => {
|
||||||
|
expect(getOrderExtraImages(mapOrder as any, ['entity_storefront_image'])).toEqual([
|
||||||
|
{ id: '11', url: 'https://cdn.example/a.jpg', preview: 'https://cdn.example/a.jpg' },
|
||||||
|
{ id: '12', url: 'https://cdn.example/b.jpg', preview: 'https://cdn.example/b.jpg' },
|
||||||
|
])
|
||||||
|
expect(joinUploadedImageIds([
|
||||||
|
{ id: '11', url: 'https://cdn.example/a.jpg', preview: 'https://cdn.example/a.jpg' },
|
||||||
|
{ id: ' ', url: 'x', preview: 'x' },
|
||||||
|
])).toBe('11')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildResubmitPayload', () => {
|
||||||
|
it('updates extra while dropping upload-only keys', () => {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
storefrontImages: [{ id: '21', url: 'https://cdn.example/c.jpg', preview: 'p' }],
|
||||||
|
licenseImages: [{ id: '31', url: 'https://cdn.example/d.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: '21',
|
||||||
|
entity_business_license_image: '31',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
export type UploadedImage = {
|
||||||
|
id: string
|
||||||
|
url: string
|
||||||
|
preview: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderLike = {
|
||||||
|
order_id?: string
|
||||||
|
goods_type?: string
|
||||||
|
entity_name?: string
|
||||||
|
entity_phone?: string
|
||||||
|
entity_extra?: Record<string, unknown>
|
||||||
|
extra?: Record<string, any>
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResubmitValues = {
|
||||||
|
entity_name?: string
|
||||||
|
entity_address_name?: string
|
||||||
|
entity_address?: string
|
||||||
|
entity_phone?: string
|
||||||
|
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)
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCoordinateAddress (address?: string) {
|
||||||
|
if (!address) return false
|
||||||
|
const parts = address.split(',').map((part) => part.trim())
|
||||||
|
if (parts.length !== 2) return false
|
||||||
|
return parts.every((part) => Number.isFinite(Number(part)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCoordinate (address?: string) {
|
||||||
|
if (!isCoordinateAddress(address)) return null
|
||||||
|
const [lng, lat] = String(address).split(',').map((part) => Number(part.trim()))
|
||||||
|
return { lng, lat }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMapCoordinate (point: { lng: number, lat: number }) {
|
||||||
|
return `${point.lng.toFixed(6)},${point.lat.toFixed(6)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrderAddressName (order: OrderLike) {
|
||||||
|
const extra = order.extra || {}
|
||||||
|
return extra.entity_address_name || (!isCoordinateAddress(extra.entity_address) ? extra.entity_address : '') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrderMapAddress (order: OrderLike) {
|
||||||
|
const extra = order.extra || {}
|
||||||
|
return isCoordinateAddress(extra.entity_address) ? extra.entity_address : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInitialPhone2 (order: OrderLike) {
|
||||||
|
const extra = order.extra || {}
|
||||||
|
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 filterMapLabelOrders<T extends OrderLike> (orders: T[] = []) {
|
||||||
|
return orders
|
||||||
|
.filter((item) => item.goods_type === 'map_label')
|
||||||
|
.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 getOrderExtraImages (order: OrderLike | null | undefined, keys: string[]): UploadedImage[] {
|
||||||
|
const extra = order?.extra || {}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const values = splitValues(extra[key])
|
||||||
|
const explicitIds = splitValues(extra[`${key}_id`])
|
||||||
|
const explicitUrls = splitValues(extra[`${key}_url`])
|
||||||
|
const urls = explicitUrls.length > 0
|
||||||
|
? explicitUrls
|
||||||
|
: values.filter((value) => /^https?:\/\//i.test(value))
|
||||||
|
|
||||||
|
if (urls.length > 0) {
|
||||||
|
return urls.map((url, index) => {
|
||||||
|
const valueId = values[index] && values[index] !== url ? values[index] : ''
|
||||||
|
return {
|
||||||
|
id: valueId || explicitIds[index] || '',
|
||||||
|
url,
|
||||||
|
preview: url,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinUploadedImageIds (images: UploadedImage[]) {
|
||||||
|
return images
|
||||||
|
.map((image) => image.id.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildResubmitPayload (input: {
|
||||||
|
order: OrderLike
|
||||||
|
values: ResubmitValues
|
||||||
|
storefrontImages: UploadedImage[]
|
||||||
|
licenseImages: UploadedImage[]
|
||||||
|
}) {
|
||||||
|
const existingExtra = Object.entries(input.order.extra || {}).reduce<Record<string, unknown>>((result, [key, value]) => {
|
||||||
|
if (!OMIT_EXTRA_KEYS.has(key)) {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(input.order.order_id || ''),
|
||||||
|
corp_id: 0,
|
||||||
|
entity_name: input.values.entity_name || input.order.entity_name || '',
|
||||||
|
entity_phone: input.values.entity_phone || '',
|
||||||
|
entity_extra: input.order.entity_extra || {},
|
||||||
|
extra: {
|
||||||
|
...existingExtra,
|
||||||
|
entity_address_name: input.values.entity_address_name || '',
|
||||||
|
entity_address: input.values.entity_address || '',
|
||||||
|
entity_phone2: input.values.entity_phone2 || '',
|
||||||
|
entity_storefront_image: joinUploadedImageIds(input.storefrontImages),
|
||||||
|
entity_business_license_image: joinUploadedImageIds(input.licenseImages),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { isValidPhone } from './phone'
|
||||||
|
|
||||||
|
describe('isValidPhone', () => {
|
||||||
|
it('accepts mainland mobile numbers', () => {
|
||||||
|
expect(isValidPhone('13800138000')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts landline numbers', () => {
|
||||||
|
expect(isValidPhone('010-12345678')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects empty or invalid values', () => {
|
||||||
|
expect(isValidPhone('')).toBe(false)
|
||||||
|
expect(isValidPhone('12345')).toBe(false)
|
||||||
|
expect(isValidPhone('23800138000')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const PHONE_PATTERN = /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8})$/
|
||||||
|
|
||||||
|
export function isValidPhone (phone: string) {
|
||||||
|
return PHONE_PATTERN.test(String(phone || '').trim())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { decryptResponse } from './crypto'
|
||||||
|
import { resolveApiOrigin, resolveRequestHost } from './origin'
|
||||||
|
import { signRequest } from './sign'
|
||||||
|
import { getSession, getSessionHost, setSession } from './session'
|
||||||
|
|
||||||
|
export const REQUEST_TIMEOUT = 60000
|
||||||
|
export const APP_VERSION = '3.0.0'
|
||||||
|
export const CORP_ID = '10006'
|
||||||
|
|
||||||
|
type RequestOptions = {
|
||||||
|
url: string
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
params?: Record<string, any>
|
||||||
|
data?: any
|
||||||
|
silent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BusinessResponse<T = any> = {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message?: string
|
||||||
|
msg?: string
|
||||||
|
encrypt?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let waitGroup = 0
|
||||||
|
|
||||||
|
function showLoading () {
|
||||||
|
waitGroup += 1
|
||||||
|
if (waitGroup === 1) {
|
||||||
|
Taro.showLoading({ title: '加载中...', mask: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading () {
|
||||||
|
waitGroup = Math.max(0, waitGroup - 1)
|
||||||
|
if (waitGroup === 0) {
|
||||||
|
Taro.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponseMessage (data: any) {
|
||||||
|
const message = data?.message ?? data?.msg ?? '请求失败'
|
||||||
|
return String(message).split(',')[0] || '请求失败'
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResponseBody (data: any) {
|
||||||
|
if (typeof data !== 'string') return data
|
||||||
|
const text = data.trim()
|
||||||
|
if (!text) return data
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toQuery (params: Record<string, any>) {
|
||||||
|
return Object.keys(params)
|
||||||
|
.map((key) => `${key}=${encodeURIComponent(params[key] ?? '')}`)
|
||||||
|
.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeaders () {
|
||||||
|
const session = getSession()
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-device-id': session.deviceId,
|
||||||
|
'x-platform': 'wx-mp',
|
||||||
|
'x-channel': 'wx-mp',
|
||||||
|
'x-version': APP_VERSION,
|
||||||
|
'x-host': resolveRequestHost({
|
||||||
|
host: getSessionHost(),
|
||||||
|
fallbackHost: process.env.TARO_APP_API_HOST,
|
||||||
|
}),
|
||||||
|
'b-corp-id': CORP_ID,
|
||||||
|
}
|
||||||
|
if (session.linkId) headers['b-link-id'] = session.linkId
|
||||||
|
if (session.shareId) headers['b-share-id'] = session.shareId
|
||||||
|
if (session.prevId) headers['b-prev-id'] = session.prevId
|
||||||
|
if (session.token) headers['x-token'] = session.token
|
||||||
|
if (session.packageId) headers['x-package'] = session.packageId
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function request<T = any> (options: RequestOptions): Promise<BusinessResponse<T>> {
|
||||||
|
const method = options.method || 'GET'
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000)
|
||||||
|
const nonce = `${timestamp}-${Math.random().toString(16).slice(2)}`
|
||||||
|
const signed = signRequest({
|
||||||
|
method,
|
||||||
|
params: {
|
||||||
|
...(options.params || {}),
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
data: options.data,
|
||||||
|
})
|
||||||
|
const origin = resolveApiOrigin()
|
||||||
|
const url = `${origin}${options.url}?${toQuery(signed.params)}`
|
||||||
|
|
||||||
|
if (!options.silent) showLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await Taro.request({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data: method === 'GET' || method === 'DELETE' ? undefined : signed.data,
|
||||||
|
header: buildHeaders(),
|
||||||
|
timeout: REQUEST_TIMEOUT,
|
||||||
|
})
|
||||||
|
|
||||||
|
let payload = parseResponseBody(response.data) as BusinessResponse<T>
|
||||||
|
if (payload && typeof payload === 'object' && payload.encrypt) {
|
||||||
|
payload = decryptResponse((payload as any).data)
|
||||||
|
}
|
||||||
|
payload = parseResponseBody(payload)
|
||||||
|
|
||||||
|
if (!payload || payload.code !== 0) {
|
||||||
|
const message = getResponseMessage(payload)
|
||||||
|
if (!options.silent) {
|
||||||
|
Taro.showToast({ title: message, icon: 'none' })
|
||||||
|
}
|
||||||
|
if (payload?.code === 11022 || payload?.code === 1001003 || payload?.code === 1001004) {
|
||||||
|
setSession({ token: '' })
|
||||||
|
}
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message && error.message !== 'request:fail') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
const message = '当前无法连接服务,请稍后再试'
|
||||||
|
if (!options.silent) {
|
||||||
|
Taro.showToast({ title: message, icon: 'none' })
|
||||||
|
}
|
||||||
|
throw new Error(message)
|
||||||
|
} finally {
|
||||||
|
if (!options.silent) hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { gcj02ToWgs84, parseGeocoderResult, TIANDITU_GEOCODER_URL, type GeoPlace } from './geocode'
|
||||||
|
|
||||||
|
export async function reverseGeocode (lng: number, lat: number): Promise<GeoPlace> {
|
||||||
|
const key = process.env.TARO_APP_TIANDITU_KEY
|
||||||
|
if (!key) return { address: '', name: '' }
|
||||||
|
|
||||||
|
const [wgsLng, wgsLat] = gcj02ToWgs84(lng, lat)
|
||||||
|
const response = await Taro.request({
|
||||||
|
url: TIANDITU_GEOCODER_URL,
|
||||||
|
method: 'GET',
|
||||||
|
data: {
|
||||||
|
postStr: JSON.stringify({ lon: wgsLng, lat: wgsLat, ver: 1 }),
|
||||||
|
type: 'geocode',
|
||||||
|
tk: key,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return parseGeocoderResult(response.data)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { mergeServiceConfig } from './contact'
|
||||||
|
import { resolveLaunchQuery, type LaunchQuery } from './launch'
|
||||||
|
import { hostFromOrigin, resolveApiOrigin } from './origin'
|
||||||
|
import { createDeviceId } from './uuid'
|
||||||
|
|
||||||
|
const SESSION_KEY = 'corpMpSession'
|
||||||
|
|
||||||
|
export type SessionState = LaunchQuery & {
|
||||||
|
deviceId: string
|
||||||
|
config: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_SESSION: SessionState = {
|
||||||
|
token: '',
|
||||||
|
host: '',
|
||||||
|
packageId: '',
|
||||||
|
scene: '',
|
||||||
|
linkId: '',
|
||||||
|
shareId: '',
|
||||||
|
prevId: '',
|
||||||
|
phone: '',
|
||||||
|
deviceId: '',
|
||||||
|
config: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStorage (): Partial<SessionState> {
|
||||||
|
try {
|
||||||
|
const value = Taro.getStorageSync(SESSION_KEY)
|
||||||
|
return value && typeof value === 'object' ? value : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStorage (session: SessionState) {
|
||||||
|
try {
|
||||||
|
Taro.setStorageSync(SESSION_KEY, session)
|
||||||
|
} catch {
|
||||||
|
// ignore quota / private mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let memorySession: SessionState | null = null
|
||||||
|
|
||||||
|
export function getSession (): SessionState {
|
||||||
|
if (!memorySession) {
|
||||||
|
const stored = readStorage()
|
||||||
|
memorySession = {
|
||||||
|
...EMPTY_SESSION,
|
||||||
|
...stored,
|
||||||
|
deviceId: stored.deviceId || createDeviceId(),
|
||||||
|
}
|
||||||
|
writeStorage(memorySession)
|
||||||
|
}
|
||||||
|
return memorySession
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSession (patch: Partial<SessionState>) {
|
||||||
|
memorySession = {
|
||||||
|
...getSession(),
|
||||||
|
...patch,
|
||||||
|
}
|
||||||
|
writeStorage(memorySession)
|
||||||
|
return memorySession
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyLaunchParams (raw: Record<string, string | undefined> = {}) {
|
||||||
|
const launch = resolveLaunchQuery(raw)
|
||||||
|
const current = getSession()
|
||||||
|
return setSession({
|
||||||
|
token: launch.token || current.token,
|
||||||
|
host: launch.host || current.host,
|
||||||
|
packageId: launch.packageId || current.packageId,
|
||||||
|
scene: launch.scene || current.scene,
|
||||||
|
linkId: launch.linkId || current.linkId,
|
||||||
|
shareId: launch.shareId || current.shareId,
|
||||||
|
prevId: launch.prevId || current.prevId,
|
||||||
|
phone: launch.phone || current.phone,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSessionHost () {
|
||||||
|
const session = getSession()
|
||||||
|
if (session.host) return session.host
|
||||||
|
return process.env.TARO_APP_API_HOST || hostFromOrigin(resolveApiOrigin())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyServiceConfig (payload: any) {
|
||||||
|
return setSession({
|
||||||
|
config: mergeServiceConfig(getSession().config, payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyCorpInit (payload: any) {
|
||||||
|
const data = payload?.data ?? payload
|
||||||
|
const token = typeof data?.token === 'string' ? data.token.trim() : ''
|
||||||
|
const packageId = data?.package === undefined || data?.package === null ? '' : String(data.package)
|
||||||
|
const patch: Partial<SessionState> = {
|
||||||
|
config: mergeServiceConfig(getSession().config, payload),
|
||||||
|
}
|
||||||
|
if (token) patch.token = token
|
||||||
|
if (packageId) patch.packageId = packageId
|
||||||
|
return setSession(patch)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import MD5 from 'crypto-js/md5'
|
||||||
|
|
||||||
|
export const SIGN_KEY = 'MsvqoWCMxJsozicF5K4EFVSoVf8rEHfn'
|
||||||
|
|
||||||
|
type SignInput = {
|
||||||
|
method?: string
|
||||||
|
params?: Record<string, any>
|
||||||
|
data?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signRequest (input: SignInput) {
|
||||||
|
const method = (input.method || 'GET').toUpperCase()
|
||||||
|
const params = { ...(input.params || {}) }
|
||||||
|
const data = input.data && typeof input.data === 'object' ? input.data : {}
|
||||||
|
const keys = Object.keys(params).sort()
|
||||||
|
const query = keys
|
||||||
|
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||||||
|
.join('&')
|
||||||
|
const secret = MD5(SIGN_KEY).toString()
|
||||||
|
const source = method === 'POST' || method === 'PUT'
|
||||||
|
? `${query}&${JSON.stringify(data)}&${secret}`
|
||||||
|
: `${query}&${secret}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
signature: MD5(source).toString(),
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
export const STATUS_CONFIG: Record<string, { color: string, bg: string, result: string }> = {
|
||||||
|
'4': { color: '#FF8E13', bg: '#FFF2E4', result: '处理中' },
|
||||||
|
'1': { color: '#FF431D', bg: '#fff1e6', result: '待办理' },
|
||||||
|
'3': { color: '#FF431D', bg: '#fff1e6', result: '待办理' },
|
||||||
|
'2': { color: '#37D2AD', bg: '#DFF8F2', result: '已完成' },
|
||||||
|
'5': { color: '#37D2AD', bg: '#DFF8F2', result: '已完成' },
|
||||||
|
'7': { color: '#37D2AD', bg: '#DFF8F2', result: '已完成' },
|
||||||
|
'8': { color: '#37D2AD', bg: '#DFF8F2', result: '已完成' },
|
||||||
|
'6': { color: '#FF8E13', bg: '#FFF2E4', result: '待提交' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PAY_ICON: Record<string, string> = {
|
||||||
|
alipay: 'https://cdn.u8t.cn/frontend-static/corp-h5/static/images/alipay.png',
|
||||||
|
weixin: 'https://cdn.u8t.cn/frontend-static/corp-h5/static/images/weixin.png',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusMeta (status?: string, name?: string) {
|
||||||
|
const config = STATUS_CONFIG[String(status || '')] || { color: '#666666', bg: '#F5F5F5', result: '未知' }
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
result: name || config.result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { Image, Text, View } from '@tarojs/components'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { CORP_ASSET_BASE, STATIC_CDN } from '@/lib/cdn'
|
||||||
|
import { pickServiceContact, type ServiceContact } from '@/lib/contact'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
contact: ServiceContact
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactItem ({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View className='contact-item' onClick={onClick}>
|
||||||
|
<Image className='contact-item__icon' src={icon} />
|
||||||
|
<View className='contact-item__copy'>
|
||||||
|
<Text className='contact-item__title'>{title}</Text>
|
||||||
|
<Text className='contact-item__desc'>{desc}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContactModal ({ open, contact, onCancel }: Props) {
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
const copyLink = async (url: string, title: string) => {
|
||||||
|
await Taro.setClipboardData({ data: url })
|
||||||
|
Taro.showToast({ title: `${title}链接已复制`, icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePhone = () => {
|
||||||
|
if (!contact.phone) return
|
||||||
|
Taro.makePhoneCall({ phoneNumber: contact.phone.replace(/-/g, '') })
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAny = Boolean(contact.online || contact.phone || contact.weixin)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='contact-mask' onClick={onCancel}>
|
||||||
|
<View className='contact-dialog' onClick={(event) => event.stopPropagation()}>
|
||||||
|
<Text className='contact-dialog__title'>联系客服</Text>
|
||||||
|
<Text className='contact-dialog__hours'>工作时间段:法定工作日 9:00-18:00</Text>
|
||||||
|
|
||||||
|
{contact.online ? (
|
||||||
|
<ContactItem
|
||||||
|
icon={`${CORP_ASSET_BASE}/images-pc/icon-contactus-service.png`}
|
||||||
|
title='在线客服'
|
||||||
|
desc='竭诚为您服务~'
|
||||||
|
onClick={() => void copyLink(contact.online, '在线客服')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{contact.weixin ? (
|
||||||
|
<ContactItem
|
||||||
|
icon={`${CORP_ASSET_BASE}/images-pc/icon-service-weixin.png`}
|
||||||
|
title='微信客服'
|
||||||
|
desc='一对一为您服务~'
|
||||||
|
onClick={() => void copyLink(contact.weixin, '微信客服')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{contact.phone ? (
|
||||||
|
<ContactItem
|
||||||
|
icon={`${CORP_ASSET_BASE}/images-pc/icon-contactus-phone.png`}
|
||||||
|
title='电话客服'
|
||||||
|
desc={contact.phone}
|
||||||
|
onClick={handlePhone}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!hasAny ? (
|
||||||
|
<Text className='contact-dialog__empty'>暂无客服信息,请稍后再试</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View className='contact-dialog__close' onClick={onCancel}>
|
||||||
|
<Text>关闭</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContactEntry ({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<View className='contact-entry' onClick={onClick}>
|
||||||
|
<Image className='contact-entry__icon' src={`${STATIC_CDN}/images/icon-contract.png`} />
|
||||||
|
<Text className='contact-entry__text'>联系我们</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '首页'
|
navigationBarTitleText: '网上快办平台',
|
||||||
|
navigationBarBackgroundColor: '#2F4FEE',
|
||||||
|
navigationBarTextStyle: 'white',
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,616 @@
|
||||||
|
.check-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: 48px;
|
||||||
|
background: #2f4fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-hero {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-hero__bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-hero__title {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 72px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
line-height: 96px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 4px 8px #1840ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin: -80px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__header {
|
||||||
|
position: relative;
|
||||||
|
height: 154px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__header-bg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 154px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__header-title {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 50px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 8px 0 #0026b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__shell {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
border-radius: 0 0 28px 28px;
|
||||||
|
background: #cbdeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__body,
|
||||||
|
.check-card__list {
|
||||||
|
padding: 40px 24px 40px;
|
||||||
|
border-radius: 0 0 28px 28px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__note {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #ff2626;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__label-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__required {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #ff2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 88px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__button {
|
||||||
|
margin-top: 32px;
|
||||||
|
height: 88px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0261fc;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__button::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card__empty {
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
padding: 24px 0 32px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card--first {
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__goods {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__goods-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: -8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__goods-name {
|
||||||
|
margin-left: 16px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 102px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 44px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__reject {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff3f3;
|
||||||
|
color: #ff0707;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #222;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__value--id {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__copy {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__pay-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__fee {
|
||||||
|
color: #ff2f2f;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__action {
|
||||||
|
min-width: 160px;
|
||||||
|
height: 64px;
|
||||||
|
margin-left: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #eaf3ff;
|
||||||
|
color: #0261fc;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 64px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-mask,
|
||||||
|
.resubmit-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 32px;
|
||||||
|
left: 32px;
|
||||||
|
padding: 32px 24px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__close {
|
||||||
|
margin-top: 32px;
|
||||||
|
height: 80px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0261fc;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__close::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit-mask {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 84vh;
|
||||||
|
padding: 32px 24px 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 24px 24px 0 0;
|
||||||
|
background: linear-gradient(180deg, #c5eeff 0, #fff 136px) #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__close-text {
|
||||||
|
color: #b3b3b3;
|
||||||
|
font-size: 48px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__reject {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff1f1;
|
||||||
|
color: #ff0707;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__field {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #3d4044;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__label-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__required {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #ff2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__input,
|
||||||
|
.resubmit__map-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 88px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__map-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__map-value {
|
||||||
|
flex: 1;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__map-placeholder {
|
||||||
|
flex: 1;
|
||||||
|
color: #b3b3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__map-arrow {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__tip {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-item,
|
||||||
|
.resubmit__upload-slot {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
margin: 0 16px 16px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-slot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #cfd6de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-plus {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__upload-text {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__error {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #ff2f2f;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__submit {
|
||||||
|
height: 88px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #0261fc;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resubmit__submit::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-entry {
|
||||||
|
position: fixed;
|
||||||
|
top: 24px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 15;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 204px;
|
||||||
|
height: 60px;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-radius: 268px 0 0 268px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-entry__icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-entry__text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 30;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-dialog {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 576px;
|
||||||
|
padding: 32px 28px 28px;
|
||||||
|
border-radius: 32px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-dialog__title {
|
||||||
|
display: block;
|
||||||
|
color: #222;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 48px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-dialog__hours {
|
||||||
|
display: block;
|
||||||
|
margin: 16px 0 28px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-dialog__empty {
|
||||||
|
display: block;
|
||||||
|
padding: 24px 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 26px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 124px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 20px rgba(147, 153, 161, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item__icon {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
margin-right: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item__copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item__title {
|
||||||
|
color: #222;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item__desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-dialog__close {
|
||||||
|
margin-top: 12px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #666;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
@ -1,97 +1,283 @@
|
||||||
import { View, WebView } from '@tarojs/components'
|
import { Button, Image, Input, Text, View } from '@tarojs/components'
|
||||||
import Taro, { useLoad } from '@tarojs/taro'
|
import Taro, { useDidShow, useLoad } from '@tarojs/taro'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { getOrdersByPhone } from '@/api/pay'
|
||||||
|
import { bootstrapSession } from '@/lib/bootstrap'
|
||||||
|
import { BANNER_BG, CARD_HEADER, CORP_ASSET_BASE, ENTITY_LABELS, HOME_ICON_BASE, PAGE_TITLE } from '@/lib/cdn'
|
||||||
|
import { pickServiceContact, type ServiceContact } from '@/lib/contact'
|
||||||
|
import {
|
||||||
|
filterMapLabelOrders,
|
||||||
|
getGoodsIcons,
|
||||||
|
getOrderAddressName,
|
||||||
|
parseCoordinate,
|
||||||
|
type OrderLike,
|
||||||
|
type ResubmitValues,
|
||||||
|
} from '@/lib/order'
|
||||||
|
import { isValidPhone } from '@/lib/phone'
|
||||||
|
import { getStatusMeta, PAY_ICON } from '@/lib/status'
|
||||||
|
import { consumeMapPickerResult, type MapPickerResult } from '@/pages/map/picker'
|
||||||
|
import ContactModal, { ContactEntry } from './contact'
|
||||||
|
import ResubmitDrawer from './resubmit'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
// xunmeng2,devcon
|
|
||||||
|
|
||||||
const LINK_PREFIX = '/p/'
|
const COPY_ICON = `${CORP_ASSET_BASE}/images-mobile/icon-copy.png`
|
||||||
const LANDING_PAGE_URL = '/pages/landing/index'
|
|
||||||
const SCENE_STORAGE_KEY = 'qrcodeScene'
|
|
||||||
|
|
||||||
function isValidLinkCode (code: string) {
|
|
||||||
return Boolean(code) && !/[/?#&=]/.test(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeQrLinkCode (scene?: string) {
|
|
||||||
if (!scene) return ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decodedScene = decodeURIComponent(scene)
|
|
||||||
const linkParam = new URLSearchParams(decodedScene).get('link')
|
|
||||||
const rawCode = (linkParam || decodedScene).trim()
|
|
||||||
const code = rawCode.startsWith(LINK_PREFIX)
|
|
||||||
? rawCode.slice(LINK_PREFIX.length)
|
|
||||||
: rawCode
|
|
||||||
|
|
||||||
return isValidLinkCode(code) ? code : ''
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Invalid qrcode scene.', error)
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWebViewUrl (linkCode: string, mpCode: string, options: Record<string, string>) {
|
|
||||||
const queryParams = new URLSearchParams()
|
|
||||||
|
|
||||||
Object.entries(options).forEach(([key, value]) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
queryParams.set(key, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
queryParams.set('mpCode', mpCode)
|
|
||||||
|
|
||||||
return `https://nb.zuom8.cn${LINK_PREFIX}${encodeURIComponent(linkCode)}?${queryParams.toString()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Index () {
|
export default function Index () {
|
||||||
const [url, setUrl] = useState('')
|
const [ready, setReady] = useState(false)
|
||||||
|
const [inputPhone, setInputPhone] = useState('')
|
||||||
|
const [checking, setChecking] = useState(false)
|
||||||
|
const [searched, setSearched] = useState(false)
|
||||||
|
const [orders, setOrders] = useState<OrderLike[]>([])
|
||||||
|
const [detailOrder, setDetailOrder] = useState<OrderLike | null>(null)
|
||||||
|
const [resubmitOrder, setResubmitOrder] = useState<OrderLike | null>(null)
|
||||||
|
const [mapResult, setMapResult] = useState<MapPickerResult | null>(null)
|
||||||
|
const [showContact, setShowContact] = useState(false)
|
||||||
|
const [contact, setContact] = useState<ServiceContact>({ online: '', phone: '', weixin: '' })
|
||||||
|
|
||||||
useLoad(async (options) => {
|
useLoad(async (options) => {
|
||||||
const currentScene = typeof options?.scene === 'string' ? options.scene.trim() : ''
|
|
||||||
let scene = currentScene
|
|
||||||
|
|
||||||
if (currentScene) {
|
|
||||||
try {
|
try {
|
||||||
Taro.setStorageSync(SCENE_STORAGE_KEY, currentScene)
|
const session = await bootstrapSession(options || {})
|
||||||
|
setInputPhone(session.phone)
|
||||||
|
setContact(pickServiceContact(session.config))
|
||||||
|
setReady(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save qrcode scene.', error)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const storedScene = Taro.getStorageSync(SCENE_STORAGE_KEY)
|
|
||||||
scene = typeof storedScene === 'string' ? storedScene.trim() : ''
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to read stored qrcode scene.', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkCode = normalizeQrLinkCode(scene)
|
|
||||||
|
|
||||||
if (!linkCode) {
|
|
||||||
try {
|
|
||||||
await Taro.redirectTo({ url: LANDING_PAGE_URL })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to open landing page.', error)
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '页面加载失败',
|
title: error instanceof Error ? error.message : '初始化失败',
|
||||||
icon: 'none'
|
icon: 'none',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
const result = consumeMapPickerResult()
|
||||||
|
if (result) {
|
||||||
|
setMapResult(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryOrders = async (phone: string) => {
|
||||||
|
if (!phone) {
|
||||||
|
Taro.showToast({ title: '请输入联系电话', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isValidPhone(phone)) {
|
||||||
|
Taro.showToast({ title: '请填写正确联系电话', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const wxCode = await Taro.login()
|
setChecking(true)
|
||||||
|
try {
|
||||||
|
const res = await getOrdersByPhone(phone)
|
||||||
|
const nextOrders = filterMapLabelOrders(res.data || [])
|
||||||
|
setOrders(nextOrders)
|
||||||
|
setSearched(true)
|
||||||
|
if (!nextOrders.length) {
|
||||||
|
Taro.showToast({ title: '无记录', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Taro.showToast({ title: '当前无法查询,请稍后再试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setUrl(buildWebViewUrl(linkCode, wxCode.code, options))
|
const handleChooseMap = (draft: ResubmitValues) => {
|
||||||
|
const point = parseCoordinate(draft.entity_address)
|
||||||
|
const query = [
|
||||||
|
point ? `lng=${point.lng}` : '',
|
||||||
|
point ? `lat=${point.lat}` : '',
|
||||||
|
draft.entity_address_name ? `name=${encodeURIComponent(draft.entity_address_name)}` : '',
|
||||||
|
].filter(Boolean).join('&')
|
||||||
|
|
||||||
|
Taro.navigateTo({
|
||||||
|
url: query ? `/pages/map/index?${query}` : '/pages/map/index',
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='index'>
|
<View className='check-page'>
|
||||||
{
|
<View className='check-hero'>
|
||||||
url && <WebView src={url}></WebView>
|
<Image className='check-hero__bg' src={BANNER_BG} mode='aspectFill' />
|
||||||
}
|
<Text className='check-hero__title'>{PAGE_TITLE}</Text>
|
||||||
|
</View>
|
||||||
|
<ContactEntry onClick={() => setShowContact(true)} />
|
||||||
|
|
||||||
|
<View className='check-card'>
|
||||||
|
<View className='check-card__header'>
|
||||||
|
<Image className='check-card__header-bg' src={CARD_HEADER} mode='scaleToFill' />
|
||||||
|
<Text className='check-card__header-title'>进度查询</Text>
|
||||||
|
</View>
|
||||||
|
<View className='check-card__shell'>
|
||||||
|
<View className='check-card__body'>
|
||||||
|
<Text className='check-card__note'>注:无法查询订单请联系客服</Text>
|
||||||
|
<View className='check-card__label'>
|
||||||
|
<Image className='check-card__label-icon' src={`${HOME_ICON_BASE}/icon_form_phone@2x.png`} />
|
||||||
|
<Text>联系电话</Text>
|
||||||
|
<Text className='check-card__required'>*</Text>
|
||||||
|
</View>
|
||||||
|
<Input
|
||||||
|
className='check-card__input'
|
||||||
|
type='number'
|
||||||
|
maxlength={12}
|
||||||
|
placeholder='请输入联系电话'
|
||||||
|
value={inputPhone}
|
||||||
|
onInput={(event) => setInputPhone(event.detail.value.replace(/\D/g, ''))}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className='check-card__button'
|
||||||
|
loading={checking}
|
||||||
|
disabled={!ready || checking}
|
||||||
|
onClick={() => void queryOrders(inputPhone)}
|
||||||
|
>
|
||||||
|
查看订单进度
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{searched ? (
|
||||||
|
<View className='check-card'>
|
||||||
|
<View className='check-card__header'>
|
||||||
|
<Image className='check-card__header-bg' src={CARD_HEADER} mode='scaleToFill' />
|
||||||
|
<Text className='check-card__header-title'>进度查询</Text>
|
||||||
|
</View>
|
||||||
|
<View className='check-card__shell'>
|
||||||
|
<View className='check-card__list'>
|
||||||
|
{orders.length ? orders.map((order, index) => (
|
||||||
|
<OrderCard
|
||||||
|
key={order.order_id || order.out_trade_no || index}
|
||||||
|
order={order}
|
||||||
|
first={index === 0}
|
||||||
|
onCopy={() => {
|
||||||
|
if (!order.out_trade_no) return
|
||||||
|
Taro.setClipboardData({ data: String(order.out_trade_no) })
|
||||||
|
}}
|
||||||
|
onViewDetail={() => setDetailOrder(order)}
|
||||||
|
onResubmit={() => {
|
||||||
|
setMapResult(null)
|
||||||
|
setResubmitOrder(order)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)) : (
|
||||||
|
<View className='check-card__empty'>
|
||||||
|
<Text>暂无记录</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{detailOrder ? (
|
||||||
|
<View className='detail-mask' onClick={() => setDetailOrder(null)}>
|
||||||
|
<View className='detail-dialog' onClick={(event) => event.stopPropagation()}>
|
||||||
|
<Text className='detail-dialog__title'>订单详情</Text>
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityName}:`} value={detailOrder.entity_name || '-'} />
|
||||||
|
<InfoLine label='联系电话:' value={detailOrder.entity_phone || '-'} />
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityAddress}:`} value={getOrderAddressName(detailOrder) || '-'} />
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityMapLocation}:`} value={detailOrder.extra?.entity_address || '-'} />
|
||||||
|
<InfoLine label='办理说明:' value='各大平台门店管理审核时间一般为1-7个工作日,审核通过后可在对应地图平台查询。' />
|
||||||
|
<Button className='detail-dialog__close' onClick={() => setDetailOrder(null)}>关闭</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<ContactModal
|
||||||
|
open={showContact}
|
||||||
|
contact={contact}
|
||||||
|
onCancel={() => setShowContact(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResubmitDrawer
|
||||||
|
open={Boolean(resubmitOrder)}
|
||||||
|
order={resubmitOrder}
|
||||||
|
mapResult={mapResult}
|
||||||
|
onCancel={() => {
|
||||||
|
setResubmitOrder(null)
|
||||||
|
setMapResult(null)
|
||||||
|
}}
|
||||||
|
onChooseMap={handleChooseMap}
|
||||||
|
onSuccess={() => {
|
||||||
|
setResubmitOrder(null)
|
||||||
|
setMapResult(null)
|
||||||
|
void queryOrders(inputPhone)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OrderCard ({
|
||||||
|
order,
|
||||||
|
first,
|
||||||
|
onCopy,
|
||||||
|
onViewDetail,
|
||||||
|
onResubmit,
|
||||||
|
}: {
|
||||||
|
order: OrderLike
|
||||||
|
first: boolean
|
||||||
|
onCopy: () => void
|
||||||
|
onViewDetail: () => void
|
||||||
|
onResubmit: () => void
|
||||||
|
}) {
|
||||||
|
const status = getStatusMeta(order.process_status, order.process_status_name)
|
||||||
|
const goodsIcons = getGoodsIcons(order)
|
||||||
|
const showDetail = order.process_status === '2'
|
||||||
|
const payIcon = PAY_ICON[order.pay_type]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={`order-card ${first ? 'order-card--first' : ''}`}>
|
||||||
|
<View className='order-card__head'>
|
||||||
|
<View className='order-card__goods'>
|
||||||
|
{goodsIcons.map((icon: string, index: number) => (
|
||||||
|
<Image className='order-card__goods-icon' key={`${icon}-${index}`} src={icon} />
|
||||||
|
))}
|
||||||
|
<Text className='order-card__goods-name'>{order.goods_name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='order-card__status' style={{ color: status.color, background: status.bg }}>
|
||||||
|
<Text>{status.result}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{order.reject_reason ? (
|
||||||
|
<View className='order-card__reject'>驳回理由:{order.reject_reason}</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityName}:`} value={order.entity_name || '-'} />
|
||||||
|
<InfoLine label='联系电话:' value={order.entity_phone || '-'} />
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityAddress}:`} value={getOrderAddressName(order) || '-'} />
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityMapLocation}:`} value={order.extra?.entity_address || '-'} />
|
||||||
|
<InfoLine label='订单时间:' value={order.create_time || order.pay_time || '-'} />
|
||||||
|
|
||||||
|
<View className='order-card__row'>
|
||||||
|
<Text className='order-card__label'>订单ID:</Text>
|
||||||
|
<Text className='order-card__value order-card__value--id'>{order.out_trade_no || '-'}</Text>
|
||||||
|
{order.out_trade_no ? (
|
||||||
|
<Image className='order-card__copy' src={COPY_ICON} onClick={onCopy} />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='order-card__row'>
|
||||||
|
<Text className='order-card__label'>支付方式:</Text>
|
||||||
|
{payIcon ? <Image className='order-card__pay-icon' src={payIcon} /> : null}
|
||||||
|
<Text className='order-card__fee'>¥{order.total_fee || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='order-card__actions'>
|
||||||
|
{showDetail ? (
|
||||||
|
<View className='order-card__action' onClick={onViewDetail}>查看详情</View>
|
||||||
|
) : null}
|
||||||
|
<View className='order-card__action' onClick={onResubmit}>补充资料</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoLine ({ label, value }: { label: string, value: string }) {
|
||||||
|
return (
|
||||||
|
<View className='order-card__row'>
|
||||||
|
<Text className='order-card__label'>{label}</Text>
|
||||||
|
<Text className='order-card__value'>{value}</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,331 @@
|
||||||
|
import { Button, Image, Input, Text, View } from '@tarojs/components'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { uploadImage } from '@/api/user'
|
||||||
|
import { updateOrderExtra } from '@/api/pay'
|
||||||
|
import { ENTITY_LABELS, HOME_ICON_BASE, MAP_ASSET_BASE } from '@/lib/cdn'
|
||||||
|
import { isValidPhone } from '@/lib/phone'
|
||||||
|
import {
|
||||||
|
buildResubmitPayload,
|
||||||
|
formatMapCoordinate,
|
||||||
|
getInitialPhone2,
|
||||||
|
getOrderAddressName,
|
||||||
|
getOrderExtraImages,
|
||||||
|
getOrderMapAddress,
|
||||||
|
parseCoordinate,
|
||||||
|
type OrderLike,
|
||||||
|
type ResubmitValues,
|
||||||
|
type UploadedImage,
|
||||||
|
} from '@/lib/order'
|
||||||
|
import type { MapPickerResult } from '@/pages/map/picker'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
order: OrderLike | null
|
||||||
|
mapResult?: MapPickerResult | null
|
||||||
|
onCancel: () => void
|
||||||
|
onChooseMap: (draft: ResubmitValues) => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldLabel ({ icon, text, required }: { icon: string, text: string, required?: boolean }) {
|
||||||
|
return (
|
||||||
|
<View className='resubmit__label'>
|
||||||
|
<Image className='resubmit__label-icon' src={icon} />
|
||||||
|
<Text>{text}</Text>
|
||||||
|
{required ? <Text className='resubmit__required'>*</Text> : null}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadField ({
|
||||||
|
label,
|
||||||
|
values,
|
||||||
|
multiple = false,
|
||||||
|
uploading,
|
||||||
|
progress,
|
||||||
|
onChange,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
values: UploadedImage[]
|
||||||
|
multiple?: boolean
|
||||||
|
uploading: boolean
|
||||||
|
progress: number
|
||||||
|
onChange: (filePaths: string[]) => void
|
||||||
|
onRemove: (index: number) => void
|
||||||
|
}) {
|
||||||
|
const showUpload = multiple || values.length === 0
|
||||||
|
|
||||||
|
const handleChoose = async () => {
|
||||||
|
const res = await Taro.chooseImage({
|
||||||
|
count: multiple ? 6 : 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
})
|
||||||
|
if (res.tempFilePaths?.length) {
|
||||||
|
onChange(res.tempFilePaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='resubmit__upload-list'>
|
||||||
|
{values.map((value, index) => (
|
||||||
|
<View className='resubmit__upload-item' key={`${value.id}-${index}`}>
|
||||||
|
<Image className='resubmit__upload-image' src={value.preview || value.url} mode='aspectFill' />
|
||||||
|
<View className='resubmit__upload-remove' onClick={() => onRemove(index)}>
|
||||||
|
<Text>×</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{showUpload ? (
|
||||||
|
<View className='resubmit__upload-slot' onClick={() => void handleChoose()}>
|
||||||
|
<Image className='resubmit__upload-plus' src={`${MAP_ASSET_BASE}/pay-success-upload.png`} />
|
||||||
|
<Text className='resubmit__upload-text'>
|
||||||
|
{uploading ? `上传中 ${Math.round(progress)}%` : (multiple && values.length > 0 ? '继续上传' : `请上传${label}`)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResubmitDrawer ({
|
||||||
|
open,
|
||||||
|
order,
|
||||||
|
mapResult,
|
||||||
|
onCancel,
|
||||||
|
onChooseMap,
|
||||||
|
onSuccess,
|
||||||
|
}: Props) {
|
||||||
|
const [values, setValues] = useState<ResubmitValues>({})
|
||||||
|
const [storefrontImages, setStorefrontImages] = useState<UploadedImage[]>([])
|
||||||
|
const [licenseImages, setLicenseImages] = useState<UploadedImage[]>([])
|
||||||
|
const [uploadingField, setUploadingField] = useState<'storefront' | 'license' | null>(null)
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0)
|
||||||
|
const [formError, setFormError] = useState('')
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!order || !open) return
|
||||||
|
|
||||||
|
const mapAddress = mapResult ? formatMapCoordinate(mapResult) : ''
|
||||||
|
const mapAddressName = mapResult?.address || mapResult?.name || ''
|
||||||
|
setValues({
|
||||||
|
entity_name: values.entity_name || order.entity_name || mapResult?.name || '',
|
||||||
|
entity_address_name: mapResult ? mapAddressName || getOrderAddressName(order) : values.entity_address_name || getOrderAddressName(order),
|
||||||
|
entity_address: mapResult ? mapAddress : values.entity_address || getOrderMapAddress(order),
|
||||||
|
entity_phone: values.entity_phone || order.entity_phone || '',
|
||||||
|
entity_phone2: values.entity_phone2 || getInitialPhone2(order),
|
||||||
|
})
|
||||||
|
if (storefrontImages.length === 0) {
|
||||||
|
setStorefrontImages(getOrderExtraImages(order, ['entity_storefront_image', 'storefront_image']))
|
||||||
|
}
|
||||||
|
if (licenseImages.length === 0) {
|
||||||
|
setLicenseImages(getOrderExtraImages(order, ['entity_business_license_image', 'business_license_image']).slice(0, 1))
|
||||||
|
}
|
||||||
|
setFormError('')
|
||||||
|
// Only seed from the current order / returned map point.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open, order, mapResult])
|
||||||
|
|
||||||
|
const updateField = <K extends keyof ResubmitValues>(field: K, value: ResubmitValues[K]) => {
|
||||||
|
setValues((current) => ({ ...current, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpload = async (field: 'storefront' | 'license', filePaths: string[]) => {
|
||||||
|
setUploadingField(field)
|
||||||
|
setUploadProgress(0)
|
||||||
|
setFormError('')
|
||||||
|
try {
|
||||||
|
const images: UploadedImage[] = []
|
||||||
|
for (const [index, filePath] of filePaths.entries()) {
|
||||||
|
const uploaded = await uploadImage(filePath, (percent) => {
|
||||||
|
setUploadProgress(((index + percent / 100) / filePaths.length) * 100)
|
||||||
|
})
|
||||||
|
images.push({
|
||||||
|
id: uploaded.id,
|
||||||
|
url: uploaded.url,
|
||||||
|
preview: filePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (field === 'storefront') {
|
||||||
|
setStorefrontImages((current) => [...current, ...images])
|
||||||
|
} else {
|
||||||
|
setLicenseImages(images.slice(0, 1))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setFormError(error instanceof Error ? error.message : '图片上传失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
setUploadingField(null)
|
||||||
|
setUploadProgress(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!order || submitting) 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: '请填写正确的联系电话1', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isValidPhone(values.entity_phone2 || '')) {
|
||||||
|
Taro.showToast({ title: '请填写正确的联系电话2', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (storefrontImages.length === 0) {
|
||||||
|
Taro.showToast({ title: '请上传门头照片', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true)
|
||||||
|
setFormError('')
|
||||||
|
try {
|
||||||
|
await updateOrderExtra(buildResubmitPayload({
|
||||||
|
order,
|
||||||
|
values,
|
||||||
|
storefrontImages,
|
||||||
|
licenseImages,
|
||||||
|
}))
|
||||||
|
Taro.showToast({ title: '重新提交成功', icon: 'success' })
|
||||||
|
onSuccess()
|
||||||
|
} catch {
|
||||||
|
Taro.showToast({ title: '重新提交失败,请稍后再试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open || !order) return null
|
||||||
|
|
||||||
|
const mapPoint = parseCoordinate(values.entity_address)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='resubmit-mask'>
|
||||||
|
<View className='resubmit'>
|
||||||
|
<View className='resubmit__close' onClick={onCancel}>
|
||||||
|
<Text className='resubmit__close-text'>×</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='resubmit__title'>重新提交</Text>
|
||||||
|
{order.reject_reason ? (
|
||||||
|
<View className='resubmit__reject'>驳回理由:{order.reject_reason}</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View className='resubmit__form'>
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${HOME_ICON_BASE}/icon_form_marker@2x.png`} text={ENTITY_LABELS.entityName} required />
|
||||||
|
<Input
|
||||||
|
className='resubmit__input'
|
||||||
|
value={values.entity_name}
|
||||||
|
placeholder='请输入店铺/公司名称'
|
||||||
|
onInput={(event) => updateField('entity_name', event.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${HOME_ICON_BASE}/icon_form_location@2x.png`} text={ENTITY_LABELS.entityAddress} required />
|
||||||
|
<Input
|
||||||
|
className='resubmit__input'
|
||||||
|
value={values.entity_address_name}
|
||||||
|
placeholder='请输入实际经营地址'
|
||||||
|
onInput={(event) => updateField('entity_address_name', event.detail.value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${HOME_ICON_BASE}/icon_form_location_lnglat@2x.png`} text={ENTITY_LABELS.entityMapLocation} required />
|
||||||
|
<View
|
||||||
|
className='resubmit__map-input'
|
||||||
|
onClick={() => onChooseMap({
|
||||||
|
...values,
|
||||||
|
entity_address: mapPoint ? formatMapCoordinate(mapPoint) : values.entity_address,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text className={values.entity_address ? 'resubmit__map-value' : 'resubmit__map-placeholder'}>
|
||||||
|
{values.entity_address || '请选择地图位置'}
|
||||||
|
</Text>
|
||||||
|
<Image className='resubmit__map-arrow' src={`${MAP_ASSET_BASE}/icon_input_arrow@2x.png`} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${HOME_ICON_BASE}/icon_form_phone@2x.png`} text='联系电话1' required />
|
||||||
|
<Input
|
||||||
|
className='resubmit__input'
|
||||||
|
type='number'
|
||||||
|
maxlength={12}
|
||||||
|
value={values.entity_phone}
|
||||||
|
placeholder='请输入'
|
||||||
|
onInput={(event) => updateField('entity_phone', event.detail.value.replace(/\D/g, ''))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${HOME_ICON_BASE}/icon_form_phone@2x.png`} text='联系电话2' required />
|
||||||
|
<Input
|
||||||
|
className='resubmit__input'
|
||||||
|
type='number'
|
||||||
|
maxlength={12}
|
||||||
|
value={values.entity_phone2}
|
||||||
|
placeholder='请输入'
|
||||||
|
onInput={(event) => updateField('entity_phone2', event.detail.value.replace(/\D/g, ''))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text className='resubmit__tip'>温馨提示:请确保您的电话畅通,工作人员会在1-7个工作日联系您</Text>
|
||||||
|
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${MAP_ASSET_BASE}/pay-success-storefront.png`} text='门头照片' required />
|
||||||
|
<UploadField
|
||||||
|
label='门头照片'
|
||||||
|
values={storefrontImages}
|
||||||
|
multiple
|
||||||
|
uploading={uploadingField === 'storefront'}
|
||||||
|
progress={uploadProgress}
|
||||||
|
onChange={(paths) => void handleUpload('storefront', paths)}
|
||||||
|
onRemove={(index) => setStorefrontImages((current) => current.filter((_, itemIndex) => itemIndex !== index))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='resubmit__field'>
|
||||||
|
<FieldLabel icon={`${MAP_ASSET_BASE}/pay-success-license.png`} text='营业执照' />
|
||||||
|
<UploadField
|
||||||
|
label='营业执照'
|
||||||
|
values={licenseImages}
|
||||||
|
uploading={uploadingField === 'license'}
|
||||||
|
progress={uploadProgress}
|
||||||
|
onChange={(paths) => void handleUpload('license', paths)}
|
||||||
|
onRemove={() => setLicenseImages([])}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{formError ? <Text className='resubmit__error'>{formError}</Text> : null}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className='resubmit__submit'
|
||||||
|
loading={submitting}
|
||||||
|
disabled={Boolean(uploadingField)}
|
||||||
|
onClick={() => void handleSubmit()}
|
||||||
|
>
|
||||||
|
立即提交
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
/* stylelint-disable selector-class-pattern -- The project requires BEM class names. */
|
|
||||||
|
|
||||||
.landing-page {
|
|
||||||
--landing-green: #07c160;
|
|
||||||
--landing-green-dark: #06ae56;
|
|
||||||
--landing-canvas: #f2f7fb;
|
|
||||||
--landing-surface: #fff;
|
|
||||||
--landing-ink: #17181a;
|
|
||||||
--landing-muted: #606772;
|
|
||||||
--landing-placeholder: #959ba3;
|
|
||||||
--landing-border: #c7ccd2;
|
|
||||||
--landing-error: #d94242;
|
|
||||||
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 54px 30px calc(38px + env(safe-area-inset-bottom));
|
|
||||||
overflow-x: hidden;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--landing-ink);
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
background: var(--landing-canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__intro {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__title {
|
|
||||||
display: block;
|
|
||||||
font-size: 48px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.35;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__description {
|
|
||||||
display: block;
|
|
||||||
margin-top: 18px;
|
|
||||||
color: var(--landing-muted);
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__notice {
|
|
||||||
display: block;
|
|
||||||
margin-top: 14px;
|
|
||||||
color: var(--landing-muted);
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__form {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding: 28px;
|
|
||||||
border: 1px solid #d9dde1;
|
|
||||||
border-radius: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: var(--landing-surface);
|
|
||||||
box-shadow: 0 8px 28px rgb(40 65 86 / 5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__field + .landing-page__field {
|
|
||||||
margin-top: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__service-types {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__service-type {
|
|
||||||
display: flex;
|
|
||||||
height: 72px;
|
|
||||||
flex: 1;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--landing-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 28px;
|
|
||||||
background: var(--landing-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__service-type--selected {
|
|
||||||
border-color: rgb(7 193 96 / 45%);
|
|
||||||
background: rgb(7 193 96 / 3%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__radio {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__input,
|
|
||||||
.landing-page__textarea {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid var(--landing-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--landing-ink);
|
|
||||||
font-size: 28px;
|
|
||||||
background: var(--landing-surface);
|
|
||||||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__input {
|
|
||||||
height: 72px;
|
|
||||||
padding: 0 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__textarea {
|
|
||||||
height: 116px;
|
|
||||||
padding: 18px 22px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__input:focus,
|
|
||||||
.landing-page__textarea:focus {
|
|
||||||
border-color: var(--landing-green);
|
|
||||||
box-shadow: 0 0 0 3px rgb(7 193 96 / 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__input--error {
|
|
||||||
border-color: var(--landing-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__placeholder {
|
|
||||||
color: var(--landing-placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__error,
|
|
||||||
.landing-page__agreement-error {
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
color: var(--landing-error);
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__agreement {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 26px 18px 0;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__checkbox {
|
|
||||||
margin-right: 10px;
|
|
||||||
transform: scale(0.88);
|
|
||||||
transform-origin: left center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__agreement-text {
|
|
||||||
color: var(--landing-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__link {
|
|
||||||
color: var(--landing-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__agreement-error {
|
|
||||||
margin-right: 18px;
|
|
||||||
margin-left: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__submit {
|
|
||||||
height: 84px;
|
|
||||||
margin: 24px 10px 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 84px;
|
|
||||||
background: var(--landing-green);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__submit::after {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__submit--pressed {
|
|
||||||
background: var(--landing-green-dark);
|
|
||||||
transform: scale(0.99);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 28px;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--landing-muted);
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-mask {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 40;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
padding-top: 80px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: rgb(0 0 0 / 48%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 88vh;
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 28px 28px 0 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--landing-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 28px 30px 22px;
|
|
||||||
border-bottom: 1px solid #e5e7e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-title,
|
|
||||||
.landing-page__document-date,
|
|
||||||
.landing-page__document-heading,
|
|
||||||
.landing-page__document-copy {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-title {
|
|
||||||
font-size: 34px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-date {
|
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--landing-muted);
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-close {
|
|
||||||
height: 60px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 24px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: var(--landing-green-dark);
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 60px;
|
|
||||||
background: rgb(7 193 96 / 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-close::after {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-body {
|
|
||||||
height: 860px;
|
|
||||||
padding: 4px 30px 36px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-section {
|
|
||||||
padding-top: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-heading {
|
|
||||||
font-size: 27px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-page__document-copy {
|
|
||||||
margin-top: 10px;
|
|
||||||
color: var(--landing-muted);
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.75;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.landing-page__input,
|
|
||||||
.landing-page__textarea,
|
|
||||||
.landing-page__submit {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
import { Button, Checkbox, CheckboxGroup, Input, Radio, RadioGroup, ScrollView, Text, Textarea, View } from '@tarojs/components'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
type ConsultationType = 'operation' | 'brand'
|
|
||||||
type DocumentType = 'privacy' | 'service'
|
|
||||||
|
|
||||||
type ConsultationForm = {
|
|
||||||
consultationType: ConsultationType
|
|
||||||
businessName: string
|
|
||||||
businessCategory: string
|
|
||||||
contactName: string
|
|
||||||
contactPhone: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormErrors = Partial<Record<keyof ConsultationForm | 'agreement', string>>
|
|
||||||
|
|
||||||
type DocumentSection = {
|
|
||||||
heading: string
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONTACT_PHONE = '4001166311'
|
|
||||||
|
|
||||||
const INITIAL_FORM: ConsultationForm = {
|
|
||||||
consultationType: 'operation',
|
|
||||||
businessName: '',
|
|
||||||
businessCategory: '',
|
|
||||||
contactName: '',
|
|
||||||
contactPhone: '',
|
|
||||||
description: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const DOCUMENTS: Record<DocumentType, { title: string, updatedAt: string, sections: DocumentSection[] }> = {
|
|
||||||
privacy: {
|
|
||||||
title: '隐私政策',
|
|
||||||
updatedAt: '更新日期:2026年7月20日',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、我们处理的信息',
|
|
||||||
content: '当您使用咨询信息填写功能时,页面会处理您主动填写的企业或门店名称、经营类别、联系人姓名、联系电话和咨询内容。我们不会要求您提供身份证件、精确位置、通讯录、相册或其他与咨询无关的信息。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '二、处理目的和方式',
|
|
||||||
content: '上述信息仅用于在当前页面核对咨询资料是否填写完整。当前版本没有服务器提交接口,不会将表单内容上传、保存或提供给第三方。关闭或刷新页面后,已填写内容不会由本小程序留存。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '三、权限与第三方共享',
|
|
||||||
content: '本页面不会申请位置、相机、相册、麦克风或通讯录权限,也不会通过本页面向任何第三方共享、转让或公开披露您填写的信息。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '四、您的权利',
|
|
||||||
content: '您可以随时修改或清空尚未提交的内容,也可以不填写表单并直接退出页面。若您对个人信息保护有疑问,可在工作时间拨打页面底部客服电话联系我们。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '五、政策更新',
|
|
||||||
content: '如后续开通在线提交或存储功能,我们会在收集信息前更新本政策,明确告知收集范围、使用目的、保存期限和保护措施,并重新取得您的同意。',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
service: {
|
|
||||||
title: '用户服务协议',
|
|
||||||
updatedAt: '生效日期:2026年7月20日',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、服务内容',
|
|
||||||
content: '本页面提供门店经营信息梳理、品牌展示建议和宣传资料制作等企业咨询信息填写服务。本服务不提供任何平台的官方认证、官方代办或结果承诺。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '二、使用方式',
|
|
||||||
content: '您应根据实际需求填写企业或门店信息。页面校验完成后,您可自行选择是否拨打客服电话进行正式咨询。当前版本不会在线提交或保存表单内容。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '三、用户责任',
|
|
||||||
content: '您应保证提供的信息真实、合法且不侵犯他人权益,不得利用本服务提交违法、虚假、侵权或与企业咨询无关的内容。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '四、服务费用',
|
|
||||||
content: '信息填写和电话咨询不收取费用。如后续服务产生费用,工作人员会在服务开始前说明具体项目、价格和退款规则,由您自主决定是否购买。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '五、免责声明',
|
|
||||||
content: '咨询建议仅供经营决策参考,实际执行效果会受到经营环境、市场变化和用户自身情况影响。我们不会承诺特定经营结果。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '六、联系我们',
|
|
||||||
content: '如您对服务内容或本协议有疑问,可在工作日9:00-18:00拨打客服电话400-116-6311。',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const PHONE_PATTERN = /^1[3-9]\d{9}$/
|
|
||||||
|
|
||||||
const validateForm = (form: ConsultationForm, agreed: boolean) => {
|
|
||||||
const errors: FormErrors = {}
|
|
||||||
|
|
||||||
if (!form.businessName.trim()) errors.businessName = '请输入企业或门店名称'
|
|
||||||
if (!form.businessCategory.trim()) errors.businessCategory = '请输入经营类别'
|
|
||||||
if (!form.contactName.trim()) errors.contactName = '请输入联系人姓名'
|
|
||||||
|
|
||||||
if (!form.contactPhone.trim()) {
|
|
||||||
errors.contactPhone = '请输入联系电话'
|
|
||||||
} else if (!PHONE_PATTERN.test(form.contactPhone.trim())) {
|
|
||||||
errors.contactPhone = '请输入正确的手机号码'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!form.description.trim()) errors.description = '请描述您的咨询需求'
|
|
||||||
if (!agreed) errors.agreement = '请阅读并同意隐私政策和用户服务协议'
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Landing () {
|
|
||||||
const [form, setForm] = useState<ConsultationForm>(INITIAL_FORM)
|
|
||||||
const [agreed, setAgreed] = useState(false)
|
|
||||||
const [errors, setErrors] = useState<FormErrors>({})
|
|
||||||
const [activeDocument, setActiveDocument] = useState<DocumentType | null>(null)
|
|
||||||
|
|
||||||
const updateField = <K extends keyof ConsultationForm>(field: K, value: ConsultationForm[K]) => {
|
|
||||||
setForm((current) => ({ ...current, [field]: value }))
|
|
||||||
setErrors((current) => ({ ...current, [field]: undefined }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const validationErrors = validateForm(form, agreed)
|
|
||||||
const firstError = Object.values(validationErrors)[0]
|
|
||||||
|
|
||||||
setErrors(validationErrors)
|
|
||||||
|
|
||||||
if (firstError) {
|
|
||||||
Taro.showToast({ title: firstError, icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await Taro.showModal({
|
|
||||||
title: '信息填写完成',
|
|
||||||
content: '当前版本不会上传或保存表单信息。如需正式咨询,请拨打客服电话400-116-6311。',
|
|
||||||
cancelText: '稍后联系',
|
|
||||||
confirmText: '拨打客服',
|
|
||||||
confirmColor: '#07c160',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.confirm) {
|
|
||||||
await Taro.makePhoneCall({ phoneNumber: CONTACT_PHONE })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const document = activeDocument ? DOCUMENTS[activeDocument] : null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='landing-page'>
|
|
||||||
<View className='landing-page__intro'>
|
|
||||||
<Text className='landing-page__title'>门店经营咨询,在线填写</Text>
|
|
||||||
<Text className='landing-page__description'>
|
|
||||||
提供经营信息梳理、品牌展示建议和宣传资料制作咨询,帮助线下门店明确实际需求。
|
|
||||||
</Text>
|
|
||||||
<Text className='landing-page__notice'>企业咨询服务,不提供任何平台官方代办服务</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='landing-page__form'>
|
|
||||||
<View className='landing-page__field'>
|
|
||||||
<Text className='landing-page__label'>咨询类型</Text>
|
|
||||||
<RadioGroup
|
|
||||||
className='landing-page__service-types'
|
|
||||||
onChange={(event) => updateField('consultationType', event.detail.value as ConsultationType)}
|
|
||||||
>
|
|
||||||
<View className={`landing-page__service-type ${form.consultationType === 'operation' ? 'landing-page__service-type--selected' : ''}`}>
|
|
||||||
<Radio className='landing-page__radio' value='operation' checked={form.consultationType === 'operation'} color='#07c160'>经营信息咨询</Radio>
|
|
||||||
</View>
|
|
||||||
<View className={`landing-page__service-type ${form.consultationType === 'brand' ? 'landing-page__service-type--selected' : ''}`}>
|
|
||||||
<Radio className='landing-page__radio' value='brand' checked={form.consultationType === 'brand'} color='#07c160'>品牌展示咨询</Radio>
|
|
||||||
</View>
|
|
||||||
</RadioGroup>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='landing-page__field'>
|
|
||||||
<Text className='landing-page__label'>企业或门店名称</Text>
|
|
||||||
<Input
|
|
||||||
className={`landing-page__input ${errors.businessName ? 'landing-page__input--error' : ''}`}
|
|
||||||
value={form.businessName}
|
|
||||||
maxlength={50}
|
|
||||||
placeholder='请输入企业或门店名称'
|
|
||||||
placeholderClass='landing-page__placeholder'
|
|
||||||
onInput={(event) => updateField('businessName', event.detail.value)}
|
|
||||||
/>
|
|
||||||
{errors.businessName && <Text className='landing-page__error'>{errors.businessName}</Text>}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='landing-page__field'>
|
|
||||||
<Text className='landing-page__label'>经营类别</Text>
|
|
||||||
<Input
|
|
||||||
className={`landing-page__input ${errors.businessCategory ? 'landing-page__input--error' : ''}`}
|
|
||||||
value={form.businessCategory}
|
|
||||||
maxlength={40}
|
|
||||||
placeholder='例如:餐饮、零售、生活服务'
|
|
||||||
placeholderClass='landing-page__placeholder'
|
|
||||||
onInput={(event) => updateField('businessCategory', event.detail.value)}
|
|
||||||
/>
|
|
||||||
{errors.businessCategory && <Text className='landing-page__error'>{errors.businessCategory}</Text>}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='landing-page__field'>
|
|
||||||
<Text className='landing-page__label'>联系人</Text>
|
|
||||||
<Input
|
|
||||||
className={`landing-page__input ${errors.contactName ? 'landing-page__input--error' : ''}`}
|
|
||||||
value={form.contactName}
|
|
||||||
maxlength={30}
|
|
||||||
placeholder='请输入联系人姓名'
|
|
||||||
placeholderClass='landing-page__placeholder'
|
|
||||||
onInput={(event) => updateField('contactName', event.detail.value)}
|
|
||||||
/>
|
|
||||||
{errors.contactName && <Text className='landing-page__error'>{errors.contactName}</Text>}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='landing-page__field'>
|
|
||||||
<Text className='landing-page__label'>联系电话</Text>
|
|
||||||
<Input
|
|
||||||
className={`landing-page__input ${errors.contactPhone ? 'landing-page__input--error' : ''}`}
|
|
||||||
value={form.contactPhone}
|
|
||||||
type='number'
|
|
||||||
maxlength={11}
|
|
||||||
placeholder='请输入手机号码'
|
|
||||||
placeholderClass='landing-page__placeholder'
|
|
||||||
onInput={(event) => updateField('contactPhone', event.detail.value)}
|
|
||||||
/>
|
|
||||||
{errors.contactPhone && <Text className='landing-page__error'>{errors.contactPhone}</Text>}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='landing-page__field'>
|
|
||||||
<Text className='landing-page__label'>咨询需求</Text>
|
|
||||||
<Textarea
|
|
||||||
className={`landing-page__textarea ${errors.description ? 'landing-page__input--error' : ''}`}
|
|
||||||
value={form.description}
|
|
||||||
maxlength={300}
|
|
||||||
placeholder='请描述希望咨询的经营问题或资料制作需求'
|
|
||||||
placeholderClass='landing-page__placeholder'
|
|
||||||
onInput={(event) => updateField('description', event.detail.value)}
|
|
||||||
/>
|
|
||||||
{errors.description && <Text className='landing-page__error'>{errors.description}</Text>}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<CheckboxGroup
|
|
||||||
className='landing-page__agreement'
|
|
||||||
onChange={(event) => {
|
|
||||||
setAgreed(event.detail.value.includes('agreed'))
|
|
||||||
setErrors((current) => ({ ...current, agreement: undefined }))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox className='landing-page__checkbox' value='agreed' checked={agreed} color='#07c160' />
|
|
||||||
<Text className='landing-page__agreement-text'>我已阅读并同意</Text>
|
|
||||||
<Text className='landing-page__link' onClick={() => setActiveDocument('privacy')}>《隐私政策》</Text>
|
|
||||||
<Text className='landing-page__agreement-text'>和</Text>
|
|
||||||
<Text className='landing-page__link' onClick={() => setActiveDocument('service')}>《用户服务协议》</Text>
|
|
||||||
</CheckboxGroup>
|
|
||||||
{errors.agreement && <Text className='landing-page__agreement-error'>{errors.agreement}</Text>}
|
|
||||||
|
|
||||||
<Button className='landing-page__submit' hoverClass='landing-page__submit--pressed' onClick={handleSubmit}>
|
|
||||||
联系咨询
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<View className='landing-page__footer'>
|
|
||||||
<Text>服务时间:工作日 9:00-18:00</Text>
|
|
||||||
<Text>客服电话:400-116-6311</Text>
|
|
||||||
<Text>企业咨询服务,请根据实际需求自愿填写</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{document && (
|
|
||||||
<View className='landing-page__document-mask'>
|
|
||||||
<View className='landing-page__document'>
|
|
||||||
<View className='landing-page__document-header'>
|
|
||||||
<View>
|
|
||||||
<Text className='landing-page__document-title'>{document.title}</Text>
|
|
||||||
<Text className='landing-page__document-date'>{document.updatedAt}</Text>
|
|
||||||
</View>
|
|
||||||
<Button className='landing-page__document-close' onClick={() => setActiveDocument(null)}>关闭</Button>
|
|
||||||
</View>
|
|
||||||
<ScrollView className='landing-page__document-body' scrollY>
|
|
||||||
{document.sections.map((section) => (
|
|
||||||
<View className='landing-page__document-section' key={section.heading}>
|
|
||||||
<Text className='landing-page__document-heading'>{section.heading}</Text>
|
|
||||||
<Text className='landing-page__document-copy'>{section.content}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '门店经营咨询',
|
navigationBarTitleText: '选择位置',
|
||||||
navigationBarBackgroundColor: '#ffffff',
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
navigationBarTextStyle: 'black',
|
navigationBarTextStyle: 'black',
|
||||||
backgroundColor: '#f2f7fb',
|
disableScroll: true,
|
||||||
backgroundTextStyle: 'dark'
|
|
||||||
})
|
})
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
.map-page {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__pin {
|
||||||
|
position: absolute;
|
||||||
|
top: 46%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-left: -14px;
|
||||||
|
margin-top: -28px;
|
||||||
|
border: 6px solid #0261fc;
|
||||||
|
border-radius: 50% 50% 50% 0;
|
||||||
|
background: #fff;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__search {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
left: 24px;
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 80px;
|
||||||
|
padding: 0 28px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__search-text {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #222;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 40px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__locate {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 360px;
|
||||||
|
z-index: 2;
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 48px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #0261fc;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 28px 24px 48px;
|
||||||
|
border-radius: 24px 24px 0 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__coord {
|
||||||
|
display: block;
|
||||||
|
color: #0261fc;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__address {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__confirm {
|
||||||
|
margin-top: 24px;
|
||||||
|
height: 88px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #0261fc;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-page__confirm::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { Button, Map, Text, View } from '@tarojs/components'
|
||||||
|
import Taro, { useLoad } from '@tarojs/taro'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
isRegionChangeEnd,
|
||||||
|
isUserRegionChange,
|
||||||
|
pickMapCenter,
|
||||||
|
} from '@/lib/geocode'
|
||||||
|
import { reverseGeocode } from '@/lib/reverseGeocode'
|
||||||
|
import { formatMapCoordinate } from '@/lib/order'
|
||||||
|
import { saveMapPickerResult, type MapPickerResult } from './picker'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
const DEFAULT_CENTER = { lng: 116.397428, lat: 39.90923 }
|
||||||
|
const MAP_ID = 'picker-map'
|
||||||
|
|
||||||
|
function asNumber (value?: string) {
|
||||||
|
const next = Number(value)
|
||||||
|
return Number.isFinite(next) ? next : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeText (value?: string) {
|
||||||
|
if (!value) return ''
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value)
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function samePoint (left: { lng: number, lat: number }, lng: number, lat: number) {
|
||||||
|
return Math.abs(left.lng - lng) < 0.000001 && Math.abs(left.lat - lat) < 0.000001
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MapPage () {
|
||||||
|
const [center, setCenter] = useState(DEFAULT_CENTER)
|
||||||
|
const [selected, setSelected] = useState<MapPickerResult>(DEFAULT_CENTER)
|
||||||
|
const [locating, setLocating] = useState(false)
|
||||||
|
const [resolving, setResolving] = useState(false)
|
||||||
|
const geocodeSeq = useRef(0)
|
||||||
|
|
||||||
|
const applyPoint = (point: MapPickerResult, moveMap = false) => {
|
||||||
|
const next = {
|
||||||
|
lng: point.lng,
|
||||||
|
lat: point.lat,
|
||||||
|
name: point.name || '',
|
||||||
|
address: point.address || '',
|
||||||
|
}
|
||||||
|
setSelected(next)
|
||||||
|
if (moveMap) setCenter({ lng: next.lng, lat: next.lat })
|
||||||
|
if (!next.address && !next.name) {
|
||||||
|
void fillPlaceName(next.lng, next.lat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillPlaceName = async (lng: number, lat: number) => {
|
||||||
|
const seq = geocodeSeq.current + 1
|
||||||
|
geocodeSeq.current = seq
|
||||||
|
setResolving(true)
|
||||||
|
try {
|
||||||
|
const place = await reverseGeocode(lng, lat)
|
||||||
|
if (seq !== geocodeSeq.current) return
|
||||||
|
if (!place.address && !place.name) return
|
||||||
|
setSelected((current) => {
|
||||||
|
if (!samePoint(current, lng, lat)) return current
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
address: place.address || current.address,
|
||||||
|
name: place.name || current.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// keep coordinates when geocode fails
|
||||||
|
} finally {
|
||||||
|
if (seq === geocodeSeq.current) setResolving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyCenter = (lng: number, lat: number, moveMap = false) => {
|
||||||
|
applyPoint({ lng, lat }, moveMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
const locateCurrent = async (silent = false) => {
|
||||||
|
if (!silent) setLocating(true)
|
||||||
|
try {
|
||||||
|
const location = await Taro.getLocation({ type: 'gcj02' })
|
||||||
|
applyCenter(location.longitude, location.latitude, true)
|
||||||
|
} catch {
|
||||||
|
if (!silent) {
|
||||||
|
Taro.showToast({ title: '定位失败,请检查定位权限', icon: 'none' })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!silent) setLocating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useLoad((options) => {
|
||||||
|
const lng = asNumber(options?.lng)
|
||||||
|
const lat = asNumber(options?.lat)
|
||||||
|
if (lng !== null && lat !== null) {
|
||||||
|
applyPoint({
|
||||||
|
lng,
|
||||||
|
lat,
|
||||||
|
name: decodeText(options?.name),
|
||||||
|
address: decodeText(options?.address),
|
||||||
|
}, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void locateCurrent(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
const result = await Taro.chooseLocation({})
|
||||||
|
applyPoint({
|
||||||
|
lng: result.longitude,
|
||||||
|
lat: result.latitude,
|
||||||
|
name: result.name,
|
||||||
|
address: result.address,
|
||||||
|
}, true)
|
||||||
|
} catch {
|
||||||
|
// user cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegionChange = (event: any) => {
|
||||||
|
if (!isRegionChangeEnd(event) || !isUserRegionChange(event)) return
|
||||||
|
|
||||||
|
const fromEvent = pickMapCenter(event)
|
||||||
|
if (fromEvent) {
|
||||||
|
applyCenter(fromEvent.lng, fromEvent.lat)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.createMapContext(MAP_ID).getCenterLocation({
|
||||||
|
success (res) {
|
||||||
|
if (typeof res.longitude === 'number' && typeof res.latitude === 'number') {
|
||||||
|
applyCenter(res.longitude, res.latitude)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
saveMapPickerResult(selected)
|
||||||
|
Taro.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeText = selected.name || selected.address || (resolving ? '正在识别位置…' : '滑动地图选择位置')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='map-page'>
|
||||||
|
<Map
|
||||||
|
id={MAP_ID}
|
||||||
|
className='map-page__map'
|
||||||
|
longitude={center.lng}
|
||||||
|
latitude={center.lat}
|
||||||
|
scale={16}
|
||||||
|
showLocation
|
||||||
|
enablePoi
|
||||||
|
onError={() => undefined}
|
||||||
|
onRegionChange={handleRegionChange}
|
||||||
|
onPoiTap={(event) => {
|
||||||
|
const { longitude, latitude, name } = event.detail || {}
|
||||||
|
if (typeof longitude === 'number' && typeof latitude === 'number') {
|
||||||
|
applyPoint({ lng: longitude, lat: latitude, name, address: name }, true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onTap={(event) => {
|
||||||
|
const { longitude, latitude } = event.detail || {}
|
||||||
|
if (typeof longitude === 'number' && typeof latitude === 'number') {
|
||||||
|
applyCenter(longitude, latitude, true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View className='map-page__pin' />
|
||||||
|
|
||||||
|
<View className='map-page__search' onClick={handleSearch}>
|
||||||
|
<Text className='map-page__search-text'>{placeText === '滑动地图选择位置' ? '搜索地点' : placeText}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='map-page__locate' onClick={() => void locateCurrent()}>
|
||||||
|
<Text>{locating ? '定位中' : '定位'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='map-page__panel'>
|
||||||
|
<Text className='map-page__coord'>{formatMapCoordinate(selected)}</Text>
|
||||||
|
<Text className='map-page__address'>{placeText}</Text>
|
||||||
|
<Button className='map-page__confirm' onClick={handleConfirm}>确认位置</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
|
export type MapPickerResult = {
|
||||||
|
lng: number
|
||||||
|
lat: number
|
||||||
|
address?: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESULT_KEY = 'mapPickerResult'
|
||||||
|
|
||||||
|
export function saveMapPickerResult (result: MapPickerResult) {
|
||||||
|
try {
|
||||||
|
Taro.setStorageSync(RESULT_KEY, result)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeMapPickerResult (): MapPickerResult | null {
|
||||||
|
try {
|
||||||
|
const result = Taro.getStorageSync(RESULT_KEY)
|
||||||
|
Taro.removeStorageSync(RESULT_KEY)
|
||||||
|
if (!result || typeof result.lng !== 'number' || typeof result.lat !== 'number') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '支付'
|
|
||||||
})
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
.pay-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 48px 32px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #f6f7fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 300px;
|
|
||||||
padding: 40px 28px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e8ebf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-card--success {
|
|
||||||
border-color: #17a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-card--failed {
|
|
||||||
border-color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-card--canceled {
|
|
||||||
border-color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-icon {
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-label {
|
|
||||||
color: #667085;
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-amount {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: #ff2626;
|
|
||||||
font-size: 72px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-status {
|
|
||||||
margin-top: 20px;
|
|
||||||
color: #344054;
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-info {
|
|
||||||
margin-top: 24px;
|
|
||||||
padding: 8px 28px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e8ebf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 24px 0;
|
|
||||||
border-bottom: 1px solid #edf0f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-row:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-row__label {
|
|
||||||
flex: 0 0 144px;
|
|
||||||
color: #667085;
|
|
||||||
font-size: 26px;
|
|
||||||
line-height: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-row__value {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
color: #111827;
|
|
||||||
font-size: 26px;
|
|
||||||
line-height: 38px;
|
|
||||||
text-align: right;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-button {
|
|
||||||
margin-top: 32px;
|
|
||||||
height: 88px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 88px;
|
|
||||||
background: #07c160;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-button[disabled] {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
background: #84d8a9;
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
import { Button, Image, Text, View } from '@tarojs/components'
|
|
||||||
import Taro, { useLoad } from '@tarojs/taro'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { decodeOrderParam } from '@/utils/base64'
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
type PayStatus = 'loading' | 'paying' | 'success' | 'failed' | 'canceled'
|
|
||||||
|
|
||||||
type OrderInfo = {
|
|
||||||
qrcode?: string | null
|
|
||||||
orderId: string
|
|
||||||
jumpUrl?: string | null
|
|
||||||
qrcodeUrl?: string | null
|
|
||||||
payFee?: number
|
|
||||||
appId?: string
|
|
||||||
timeStamp?: string
|
|
||||||
nonceStr?: string
|
|
||||||
package?: string
|
|
||||||
signType?: string
|
|
||||||
paySign?: string
|
|
||||||
icon?: string
|
|
||||||
outTradeNo?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormInfo = {
|
|
||||||
goods_type: string
|
|
||||||
entity_phone
|
|
||||||
extra: {
|
|
||||||
entity_address: string
|
|
||||||
entity_address_name: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectedPlan = {
|
|
||||||
name: string
|
|
||||||
icon: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown) => {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === 'object' && error && 'errMsg' in error) {
|
|
||||||
return String((error as { errMsg?: string }).errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return '支付失败'
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasPayParams = (info: OrderInfo) =>
|
|
||||||
Boolean(info.timeStamp && info.nonceStr && info.package && info.paySign)
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const [orderInfo, setOrderInfo] = useState<OrderInfo | null>(null)
|
|
||||||
const [formInfo, setFormInfo] = useState<FormInfo | null>(null)
|
|
||||||
const [selectedPlan, setSelectedPlan] = useState<SelectedPlan | null>(null)
|
|
||||||
const [status, setStatus] = useState<PayStatus>('loading')
|
|
||||||
const [message, setMessage] = useState('正在准备订单')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const requestPay = async (info: OrderInfo) => {
|
|
||||||
if (loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
setStatus('paying')
|
|
||||||
setMessage('正在拉起微信支付')
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) {
|
|
||||||
const res = await new Promise((resolve, reject) => {
|
|
||||||
Taro.requestPayment({
|
|
||||||
timeStamp: info.timeStamp as string,
|
|
||||||
nonceStr: info.nonceStr as string,
|
|
||||||
package: info.package as string,
|
|
||||||
signType: (info.signType as 'RSA') || 'RSA',
|
|
||||||
paySign: info.paySign as string,
|
|
||||||
success(r) {
|
|
||||||
console.log('success', r)
|
|
||||||
resolve(r)
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
console.log('fail', err)
|
|
||||||
reject(err)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.log("res", res)
|
|
||||||
} else {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus('success')
|
|
||||||
setMessage('支付成功')
|
|
||||||
Taro.showToast({
|
|
||||||
title: '支付成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
// 返回 WebView(H5),让公众号页面刷新订单状态
|
|
||||||
setTimeout(() => {
|
|
||||||
Taro.navigateBack()
|
|
||||||
}, 800)
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getErrorMessage(error)
|
|
||||||
const isCancel = errorMessage.includes('cancel')
|
|
||||||
|
|
||||||
setStatus(isCancel ? 'canceled' : 'failed')
|
|
||||||
setMessage(isCancel ? '已取消支付,可重新发起' : errorMessage)
|
|
||||||
Taro.showToast({
|
|
||||||
title: isCancel ? '已取消支付' : '支付失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useLoad(() => {
|
|
||||||
const { order, formInfo, selectedPlanInfo } = Taro.getCurrentInstance().router?.params ?? {}
|
|
||||||
const info = decodeOrderParam<OrderInfo>(order)
|
|
||||||
const form = decodeOrderParam<FormInfo>(formInfo)
|
|
||||||
const selectedPlan = decodeOrderParam<SelectedPlan>(selectedPlanInfo)
|
|
||||||
console.log("info", info)
|
|
||||||
console.log("form", form)
|
|
||||||
console.log("selectedPlan", selectedPlan)
|
|
||||||
if (!info || !hasPayParams(info)) {
|
|
||||||
setStatus('failed')
|
|
||||||
setMessage('订单信息异常,无法发起支付')
|
|
||||||
Taro.showToast({
|
|
||||||
title: '订单信息异常',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setOrderInfo(info)
|
|
||||||
setFormInfo(form)
|
|
||||||
setSelectedPlan(selectedPlan)
|
|
||||||
void requestPay(info)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='pay-page'>
|
|
||||||
<View className={`pay-card pay-card--${status}`}>
|
|
||||||
<Text className='pay-label'>支付金额</Text>
|
|
||||||
<Text className='pay-amount'>
|
|
||||||
<Text style="font-size: 40rpx">¥</Text>{orderInfo?.payFee}
|
|
||||||
</Text>
|
|
||||||
<Text className='pay-status'>{message}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{orderInfo ? (
|
|
||||||
<View className='pay-info'>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label' style="color:#000;font-weight:bold;">订单信息</Text>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label'>订单号</Text>
|
|
||||||
<Text className='pay-row__value'>{orderInfo.outTradeNo}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label'>创建时间</Text>
|
|
||||||
<Text className='pay-row__value'>{orderInfo.timeStamp}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label' style="color:#000;font-weight:bold;">商品信息</Text>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label'>商品类型</Text>
|
|
||||||
<View className='pay-row__value' style="display:flex;justify-content:flex-end;flex-flow:wrap;">
|
|
||||||
<View style="display:flex;flex-direction:row;align-items:center;">
|
|
||||||
{
|
|
||||||
selectedPlan?.icon.map((icon, index) => <Image style="width:40rpx;height:40rpx" key={index} src={icon} />)
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
<Text>{selectedPlan?.name}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label'>标注地址</Text>
|
|
||||||
<Text className='pay-row__value'>{formInfo?.extra?.entity_address_name}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label'>地图位置</Text>
|
|
||||||
<Text className='pay-row__value'>{formInfo?.extra?.entity_address}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='pay-row'>
|
|
||||||
<Text className='pay-row__label'>联系电话</Text>
|
|
||||||
<Text className='pay-row__value'>{formInfo?.entity_phone}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{orderInfo ? (
|
|
||||||
<Button
|
|
||||||
className='pay-button'
|
|
||||||
loading={loading}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() => requestPay(orderInfo)}
|
|
||||||
>
|
|
||||||
{loading ? '支付中' : status === 'success' ? '支付完成' : '立即支付'}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
const BASE64_CHARS =
|
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手写 atob:weapp 环境没有全局 atob。
|
|
||||||
* 输入标准 base64 字符串,输出二进制字符串(每个字符的 charCode 为一个字节)。
|
|
||||||
*/
|
|
||||||
function base64Decode(input: string): string {
|
|
||||||
// 去掉所有非 base64 字符(换行、空白等),并去掉 padding 用于循环
|
|
||||||
const cleaned = input.replace(/[^A-Za-z0-9+/]/g, '')
|
|
||||||
let output = ''
|
|
||||||
|
|
||||||
for (let i = 0; i < cleaned.length; i += 4) {
|
|
||||||
const c1 = BASE64_CHARS.indexOf(cleaned[i])
|
|
||||||
const c2 = BASE64_CHARS.indexOf(cleaned[i + 1])
|
|
||||||
const c3 = BASE64_CHARS.indexOf(cleaned[i + 2])
|
|
||||||
const c4 = BASE64_CHARS.indexOf(cleaned[i + 3])
|
|
||||||
|
|
||||||
const byte1 = (c1 << 2) | (c2 >> 4)
|
|
||||||
output += String.fromCharCode(byte1)
|
|
||||||
|
|
||||||
if (c3 !== -1) {
|
|
||||||
const byte2 = ((c2 & 15) << 4) | (c3 >> 2)
|
|
||||||
output += String.fromCharCode(byte2)
|
|
||||||
}
|
|
||||||
if (c4 !== -1) {
|
|
||||||
const byte3 = ((c3 & 3) << 6) | c4
|
|
||||||
output += String.fromCharCode(byte3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 严格逆 H5 端的 utf8ToBase64:
|
|
||||||
* utf8ToBase64(v) = btoa(unescape(encodeURIComponent(JSON.stringify(v))))
|
|
||||||
* 这里做反向:base64 → 二进制串 → escape → decodeURIComponent 还原 UTF-8 文本。
|
|
||||||
*
|
|
||||||
* URL 兼容:H5 未对 base64 串做 encodeURIComponent 直接拼进 query,
|
|
||||||
* 路由解析可能把 base64 里的 '+' 还原成空格,这里先补回来。
|
|
||||||
*/
|
|
||||||
export function base64ToUtf8(raw: string): string {
|
|
||||||
const normalized = raw.replace(/ /g, '+')
|
|
||||||
return decodeURIComponent(escape(base64Decode(normalized)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解码路由传来的 base64 JSON 参数,失败返回 null。
|
|
||||||
*/
|
|
||||||
export function decodeOrderParam<T>(raw?: string): T | null {
|
|
||||||
if (!raw) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(base64ToUtf8(raw)) as T
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,6 +23,12 @@ declare namespace NodeJS {
|
||||||
* @see https://taro-docs.jd.com/docs/next/env-mode-config#特殊环境变量-taro_app_id
|
* @see https://taro-docs.jd.com/docs/next/env-mode-config#特殊环境变量-taro_app_id
|
||||||
*/
|
*/
|
||||||
TARO_APP_ID: string
|
TARO_APP_ID: string
|
||||||
|
/** 接口域名,开发/生产分别来自 .env.development / .env.production */
|
||||||
|
TARO_APP_API_ORIGIN: string
|
||||||
|
/** x-host 回退值 */
|
||||||
|
TARO_APP_API_HOST: string
|
||||||
|
/** 天地图逆地理编码 key,用于地图选点回填地址名称 */
|
||||||
|
TARO_APP_TIANDITU_KEY: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import path from 'node:path'
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue