first commit
This commit is contained in:
commit
d7a4223640
|
|
@ -0,0 +1,12 @@
|
|||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config
|
||||
TARO_APP_ID="wx6d4f6f29c41aff93"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# TARO_APP_ID="生产环境下的小程序 AppID"
|
||||
TARO_APP_ID="wx6d4f6f29c41aff93"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": ["taro/react"],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
dist/
|
||||
deploy_versions/
|
||||
.temp/
|
||||
.rn_temp/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.swc
|
||||
*.local
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# 运行 commitlint 检查 commit message
|
||||
npx --no -- commitlint --edit ${1}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
`corp-mp` is a **Taro 4.2** cross-platform app (React 18 + TypeScript + SCSS, compiled with **Vite**). The primary target is the WeChat mini-program (`weapp`); Taro also compiles it to H5, Alipay, Swan, TT, QQ, JD, RN, and Harmony.
|
||||
|
||||
## Commands
|
||||
|
||||
Package manager is **pnpm** (a `pnpm-lock.yaml` is committed).
|
||||
|
||||
```bash
|
||||
pnpm install # install deps
|
||||
pnpm dev:weapp # WeChat mini-program dev build with --watch → ./dist
|
||||
pnpm build:weapp # production mini-program build → ./dist
|
||||
pnpm dev:h5 / build:h5 # H5 target
|
||||
```
|
||||
|
||||
Other targets follow `pnpm dev:<platform>` / `pnpm build:<platform>` (`alipay`, `swan`, `tt`, `qq`, `jd`, `rn`, `harmony-hybrid`).
|
||||
|
||||
**Running the mini-program:** builds output to `./dist`. Open `./dist` in WeChat DevTools (`project.config.json` already points `miniprogramRoot` there; appId `wx6d4f6f29c41aff93`).
|
||||
|
||||
**There is no test runner and no lint script in `package.json`.** ESLint (`taro/react`) and Stylelint are configured but run manually. The only enforced gate is a Husky `commit-msg` hook running **commitlint** (Conventional Commits) — non-conforming commit messages are rejected.
|
||||
|
||||
## 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`:
|
||||
|
||||
**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.
|
||||
|
||||
**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:
|
||||
- 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.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Page = folder** under `src/pages/<name>/` with `index.tsx` + `index.scss` + `index.config.ts`. New pages **must** be added to the `pages` array in `src/app.config.ts` or they won't route.
|
||||
- **Lifecycle:** use Taro hooks — `useLoad` for page init (reads route params via `Taro.getCurrentInstance().router?.params`), `useLaunch` for app init — not bare `useEffect`.
|
||||
- **Native API calls must be env-guarded:** wrap `weapp`-only APIs (e.g. `Taro.requestPayment`) in `if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP)` with a fallback branch for other targets.
|
||||
- **Styling:** SCSS with BEM (`block__element--modifier`); CSS Modules are disabled. Design width is `750`, so author dimensions in `px` — Taro converts to `rpx`/`rem` at compile time.
|
||||
- **Imports:** `@/*` is aliased to `src/*` (see `tsconfig.json`).
|
||||
- `strictNullChecks`, `noUnusedLocals`, and `noUnusedParameters` are on; `noImplicitAny` is off.
|
||||
|
||||
A more verbose Chinese-language version of these notes lives in `GEMINI.md`.
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# GEMINI.md - 项目开发规范与上下文指南
|
||||
|
||||
本文件是 `corp-mp` 项目的基础开发规范与指令上下文,用于指导未来 AI 助手及团队成员的高效协作。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
`corp-mp` 是一个基于 **Taro 4.2.0** 跨端开发框架构建的移动端/小程序项目,核心技术栈为 **React 18 + TypeScript + SCSS + Vite**。
|
||||
|
||||
### 核心功能模块
|
||||
1. **微信快捷授权与 WebView 承载 (`pages/index/index`)**:
|
||||
- 页面加载时自动调用 `Taro.login()` 获取微信授权 `code`(即 `wxCode`)。
|
||||
- 将 `code` 作为 Query 参数拼接至特定业务 H5 URL(例如 `mpCode=xxx`),使用 `<WebView>` 组件进行全屏承载。
|
||||
2. **模拟微信原生支付 (`pages/pay/index`)**:
|
||||
- 接收后端支付预订单数据。
|
||||
- 在微信小程序环境下安全调用 `Taro.requestPayment` 发起微信官方原生支付。
|
||||
- 提供优雅的支付状态管理(准备、支付中、支付成功、取消、失败等)和完备的异常捕获与友好提示。
|
||||
|
||||
---
|
||||
|
||||
## 2. 环境依赖与包管理
|
||||
|
||||
- **包管理器**:推荐且必须使用 **`pnpm`**(根目录下存在 `pnpm-lock.yaml`)。
|
||||
- **Node.js 版本**:建议使用 Node.js v18 或更高版本。
|
||||
- **运行框架**:Taro 4.2.0。
|
||||
- **构建编译工具**:Vite 4.x (`@tarojs/vite-runner`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心命令指南
|
||||
|
||||
在执行命令前,请确保已全局安装了 `pnpm`。
|
||||
|
||||
### 3.1 依赖安装
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3.2 微信小程序开发与编译
|
||||
```bash
|
||||
# 本地开发(带热更新监听)
|
||||
pnpm dev:weapp
|
||||
|
||||
# 生产环境打包构建
|
||||
pnpm build:weapp
|
||||
```
|
||||
> **提示**:编译产物将默认输出至根目录下的 `/dist` 目录。在微信开发者工具中,应将项目根目录或指定小程序根目录指向该 `/dist`。
|
||||
|
||||
### 3.3 H5/Web 端开发与编译
|
||||
```bash
|
||||
# 本地 H5 开发
|
||||
pnpm dev:h5
|
||||
|
||||
# 生产 H5 打包
|
||||
pnpm build:h5
|
||||
```
|
||||
|
||||
### 3.4 其它跨端支持
|
||||
项目同样提供了对百度小程序 (`swan`)、支付宝小程序 (`alipay`)、字节小程序 (`tt`)、QQ 小程序 (`qq`)、京东小程序 (`jd`)、React Native (`rn`)、鸿蒙 Hybrid (`harmony-hybrid`) 等多端编译指令,其格式均为 `pnpm dev:<platform>` 或 `pnpm build:<platform>`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 目录结构解析
|
||||
|
||||
```text
|
||||
D:\frontendProject\corp-mp\
|
||||
├───config\ # Taro 编译配置文件目录
|
||||
│ ├───dev.ts # 开发环境特定配置
|
||||
│ ├───index.ts # 公共基础配置(Vite 编译器、设计稿尺寸等)
|
||||
│ └───prod.ts # 生产环境特定配置
|
||||
├───dist\ # 编译后的输出目录(不可提交至 Git)
|
||||
├───src\ # 源代码主体目录
|
||||
│ ├───app.config.ts # 小程序全局应用配置(包括页面路由 pages、window 样式等)
|
||||
│ ├───app.scss # 全局公共样式文件
|
||||
│ ├───app.ts # 小程序生命周期与应用入口
|
||||
│ ├───index.html # H5 编译使用的 HTML 模版
|
||||
│ └───pages\ # 页面模块目录
|
||||
│ ├───index\ # 授权 WebView 页面
|
||||
│ │ ├───index.config.ts # 页面特定配置
|
||||
│ │ ├───index.scss # 页面样式(BEM 命名)
|
||||
│ │ └───index.tsx # 页面逻辑代码
|
||||
│ └───pay\ # 微信支付页面
|
||||
│ ├───index.config.ts
|
||||
│ ├───index.scss
|
||||
│ └───index.tsx
|
||||
├───types\ # TypeScript 全局声明目录
|
||||
│ └───global.d.ts # Taro 相关的全局补丁与自定义声明
|
||||
├───.env.development # 开发环境变量(如 TARO_APP_ID、BASE_URL)
|
||||
├───.env.production # 生产环境变量
|
||||
├───.env.test # 测试环境变量
|
||||
├───project.config.json # 微信开发者工具项目配置文件
|
||||
├───tsconfig.json # TypeScript 配置
|
||||
├───.eslintrc # ESLint 配置(继承 taro/react)
|
||||
├───stylelint.config.mjs # Stylelint CSS/SCSS 样式规范配置
|
||||
└───commitlint.config.mjs # Git Commit 规范约束配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 开发与编码守则
|
||||
|
||||
### 5.1 React 18 编写规范
|
||||
1. **函数式组件优先**:所有页面和组件均采用 Function Component 与 React Hooks 风格。
|
||||
2. **Taro 专属生命周期 Hooks**:
|
||||
- 页面加载逻辑使用 `useLoad`(而非普通的 `useEffect`),用于准确捕获页面入参和执行初始化。
|
||||
- 应用启动钩子使用 `useLaunch`。
|
||||
3. **状态声明与操作**:
|
||||
- 保持 state 尽量扁平,避免无谓的深层嵌套。
|
||||
- 在进行非安全型异步操作(如接口请求、支付拉起)时,务必伴随 `loading` 状态的管理,防止用户重复触发。
|
||||
|
||||
### 5.2 样式规范
|
||||
1. **SCSS 与 BEM 命名法**:
|
||||
- 为避免多端渲染时的类名冲突,样式推荐采用传统的 **BEM (Block-Element-Modifier)** 规范进行书写。
|
||||
- 示例:
|
||||
```scss
|
||||
.pay-page {
|
||||
.pay-card {
|
||||
&--success { color: green; }
|
||||
&--failed { color: red; }
|
||||
}
|
||||
.pay-row {
|
||||
&__label { font-weight: bold; }
|
||||
&__value { color: #333; }
|
||||
}
|
||||
}
|
||||
```
|
||||
2. **移动端自适应**:
|
||||
- 默认设计稿尺寸为 `750px`(在 `config/index.ts` 中设定了 `designWidth: 750`)。
|
||||
- 在编写 CSS 时,所有尺寸可以直接编写 `px`,Taro 在编译时会根据配置自动将其转换为 `rpx` (小程序端) 或 `rem` (H5 端)。
|
||||
|
||||
### 5.3 多端兼容安全调用
|
||||
在调用一些微信/小程序特有的原生 API(如 `Taro.requestPayment`)时,务必先通过环境判断确保执行安全:
|
||||
```typescript
|
||||
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) {
|
||||
// 仅在微信小程序环境下执行
|
||||
await Taro.requestPayment(...)
|
||||
} else {
|
||||
// 其它环境降级或模拟逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 代码校验与规范化
|
||||
项目配置了严苛的 Lint 和 Git 提交约束,提交前请确保代码不报错:
|
||||
- **ESLint 校验**:继承自 `taro/react`,并在其中免除了 `react-in-jsx-scope` 限制(允许不显式 `import React`)。
|
||||
- **Stylelint 校验**:保持 SCSS 代码的整洁性。
|
||||
- **Commit 规范**:使用 Husky + Commitlint。在进行 Git 提交时,Commit 消息必须严格遵守 Conventional Commits 规范(如 `feat: 新增支付渠道`, `fix: 修复支付状态未更新` 等),否则会导致提交失败。
|
||||
|
||||
---
|
||||
|
||||
## 6. 新增页面/组件工作流
|
||||
|
||||
1. **新建页面目录**:在 `src/pages/` 下新建页面文件夹(如 `src/pages/member/`),并准备 `index.tsx`、`index.scss`、`index.config.ts`。
|
||||
2. **注册路由**:在 `src/app.config.ts` 的 `pages` 数组中添加页面路径。
|
||||
```typescript
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/pay/index',
|
||||
'pages/member/index', // 新增
|
||||
],
|
||||
// ...
|
||||
})
|
||||
```
|
||||
3. **保持状态与路由极简**:对于非必要的全局状态,尽量维持页面局部 State,避免全局状态污染。
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// babel-preset-taro 更多选项和默认值:
|
||||
// https://docs.taro.zone/docs/next/babel-config
|
||||
module.exports = {
|
||||
presets: [
|
||||
['taro', {
|
||||
framework: 'react',
|
||||
ts: true,
|
||||
compiler: 'vite',
|
||||
useBuiltIns: process.env.TARO_ENV === 'h5' ? 'usage' : false
|
||||
}]
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export default { extends: ["@commitlint/config-conventional"] };
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { UserConfigExport } from "@tarojs/cli"
|
||||
|
||||
export default {
|
||||
|
||||
mini: {},
|
||||
h5: {}
|
||||
} satisfies UserConfigExport<'vite'>
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import path from 'node:path'
|
||||
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
|
||||
import devConfig from './dev'
|
||||
import prodConfig from './prod'
|
||||
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
export default defineConfig<'vite'>(async (merge, { command, mode }) => {
|
||||
const baseConfig: UserConfigExport<'vite'> = {
|
||||
projectName: 'corp-mp',
|
||||
date: '2026-6-8',
|
||||
designWidth: 750,
|
||||
deviceRatio: {
|
||||
640: 2.34 / 2,
|
||||
750: 1,
|
||||
375: 2,
|
||||
828: 1.81 / 2
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '..', 'src')
|
||||
},
|
||||
plugins: [
|
||||
"@tarojs/plugin-generator"
|
||||
],
|
||||
defineConstants: {
|
||||
},
|
||||
copy: {
|
||||
patterns: [
|
||||
],
|
||||
options: {
|
||||
}
|
||||
},
|
||||
framework: 'react',
|
||||
compiler: 'vite',
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: {
|
||||
enable: true,
|
||||
config: {
|
||||
|
||||
}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true,
|
||||
filename: 'css/[name].[hash].css',
|
||||
chunkFilename: 'css/[name].[chunkhash].css'
|
||||
},
|
||||
postcss: {
|
||||
autoprefixer: {
|
||||
enable: true,
|
||||
config: {}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
rn: {
|
||||
appName: 'taroDemo',
|
||||
postcss: {
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.env.BROWSERSLIST_ENV = process.env.NODE_ENV
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 本地开发构建配置(不混淆压缩)
|
||||
return merge({}, baseConfig, devConfig)
|
||||
}
|
||||
// 生产构建配置(默认开启压缩混淆等)
|
||||
return merge({}, baseConfig, prodConfig)
|
||||
})
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { UserConfigExport } from "@tarojs/cli"
|
||||
|
||||
export default {
|
||||
mini: {},
|
||||
h5: {
|
||||
// 确保产物为 es5
|
||||
legacy: true,
|
||||
/**
|
||||
* WebpackChain 插件配置
|
||||
* @docs https://github.com/neutrinojs/webpack-chain
|
||||
*/
|
||||
// webpackChain (chain) {
|
||||
// /**
|
||||
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
|
||||
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
|
||||
// */
|
||||
// chain.plugin('analyzer')
|
||||
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
|
||||
// /**
|
||||
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
|
||||
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
|
||||
// */
|
||||
// const path = require('path')
|
||||
// const Prerender = require('prerender-spa-plugin')
|
||||
// const staticDir = path.join(__dirname, '..', 'dist')
|
||||
// chain
|
||||
// .plugin('prerender')
|
||||
// .use(new Prerender({
|
||||
// staticDir,
|
||||
// routes: [ '/pages/index/index' ],
|
||||
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
} satisfies UserConfigExport<'vite'>
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"name": "corp-mp",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"templateInfo": {
|
||||
"name": "default",
|
||||
"typescript": true,
|
||||
"css": "Sass",
|
||||
"framework": "React"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"new": "taro new",
|
||||
"build:weapp": "taro build --type weapp",
|
||||
"build:swan": "taro build --type swan",
|
||||
"build:alipay": "taro build --type alipay",
|
||||
"build:tt": "taro build --type tt",
|
||||
"build:h5": "taro build --type h5",
|
||||
"build:rn": "taro build --type rn",
|
||||
"build:qq": "taro build --type qq",
|
||||
"build:jd": "taro build --type jd",
|
||||
"build:harmony-hybrid": "taro build --type harmony-hybrid",
|
||||
"dev:weapp": "npm run build:weapp -- --watch",
|
||||
"dev:swan": "npm run build:swan -- --watch",
|
||||
"dev:alipay": "npm run build:alipay -- --watch",
|
||||
"dev:tt": "npm run build:tt -- --watch",
|
||||
"dev:h5": "npm run build:h5 -- --watch",
|
||||
"dev:rn": "npm run build:rn -- --watch",
|
||||
"dev:qq": "npm run build:qq -- --watch",
|
||||
"dev:jd": "npm run build:jd -- --watch",
|
||||
"dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch"
|
||||
},
|
||||
"browserslist": {
|
||||
"development": [
|
||||
"defaults and fully supports es6-module",
|
||||
"maintained node versions"
|
||||
],
|
||||
"production": [
|
||||
"last 3 versions",
|
||||
"Android >= 4.1",
|
||||
"ios >= 8"
|
||||
]
|
||||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.4",
|
||||
"@tarojs/components": "4.2.0",
|
||||
"@tarojs/helper": "4.2.0",
|
||||
"@tarojs/plugin-platform-weapp": "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-harmony-hybrid": "4.2.0",
|
||||
"@tarojs/runtime": "4.2.0",
|
||||
"@tarojs/shared": "4.2.0",
|
||||
"@tarojs/taro": "4.2.0",
|
||||
"@tarojs/plugin-framework-react": "4.2.0",
|
||||
"@tarojs/react": "4.2.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"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/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",
|
||||
"@types/react": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
"sass": "^1.75.0",
|
||||
"typescript": "^5.4.5",
|
||||
"postcss": "^8.5.6",
|
||||
"@types/minimatch": "^5"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"miniprogramRoot": "./dist",
|
||||
"projectname": "corp-mp",
|
||||
"description": "",
|
||||
"appid": "wx6d4f6f29c41aff93",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
"enhance": false,
|
||||
"compileHotReLoad": false,
|
||||
"postcss": false,
|
||||
"minified": false
|
||||
},
|
||||
"compileType": "miniprogram"
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"caveman": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/caveman/SKILL.md",
|
||||
"computedHash": "536908fcfcb232600a5875aa85f1fd50fd13305e9d67379bcd95f07c8c916f3f"
|
||||
},
|
||||
"design-an-interface": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/deprecated/design-an-interface/SKILL.md",
|
||||
"computedHash": "f3f225914515407b8224e8ec6187db337c5e65bad0703936a0acb3f802947a7d"
|
||||
},
|
||||
"diagnose": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/diagnose/SKILL.md",
|
||||
"computedHash": "1c3c85517ac42116fe5f2bfb5150f7b3e38ad23808e40b33fbb01f1afb611983"
|
||||
},
|
||||
"edit-article": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/personal/edit-article/SKILL.md",
|
||||
"computedHash": "e0c50de6d64f528cb4ea32b142e2ec499e291efbc6996369ac4a1eea76d4870c"
|
||||
},
|
||||
"git-guardrails-claude-code": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/misc/git-guardrails-claude-code/SKILL.md",
|
||||
"computedHash": "a04bf71be9abc44ca3b00e2f1740b37d2e471abadd20d43a31bccff7135baedf"
|
||||
},
|
||||
"grill-me": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/grill-me/SKILL.md",
|
||||
"computedHash": "daf64ca15f4fa081a6747766db538e2dbd1131725ed4fcdd3d538dc62c7035ba"
|
||||
},
|
||||
"grill-with-docs": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/grill-with-docs/SKILL.md",
|
||||
"computedHash": "7c212cee6a823b956aaec7dff2a846814fe9986caa2e62313734010c7bd3db70"
|
||||
},
|
||||
"handoff": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/handoff/SKILL.md",
|
||||
"computedHash": "3fdffd9dce6fc3e9f9f756ba79d694c5f162e11c7e7fbc72a0958f204312e0b1"
|
||||
},
|
||||
"improve-codebase-architecture": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/improve-codebase-architecture/SKILL.md",
|
||||
"computedHash": "5d9444ffe5c240f2d20081c42c547112e1b8c372375a14e4c98df4d94bde11e1"
|
||||
},
|
||||
"migrate-to-shoehorn": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/misc/migrate-to-shoehorn/SKILL.md",
|
||||
"computedHash": "67fdd18f8f4f7c89b3003eb944220679272738c1deb680719fd2e282495d87f4"
|
||||
},
|
||||
"obsidian-vault": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/personal/obsidian-vault/SKILL.md",
|
||||
"computedHash": "a5dd44e7af2bfeb2cb2e3f421fdccef62dd334038ad06e18767b0ddcf0e75c13"
|
||||
},
|
||||
"prototype": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/prototype/SKILL.md",
|
||||
"computedHash": "f52c019c3e0af2ad1807332d553b9ee7282a28c162d78cdeddd89dc8456b73ff"
|
||||
},
|
||||
"qa": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/deprecated/qa/SKILL.md",
|
||||
"computedHash": "a608d889794ef7f21a5dd31d96ca502b5e6f27b466ada759a3bf9846a2e6b236"
|
||||
},
|
||||
"request-refactor-plan": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/deprecated/request-refactor-plan/SKILL.md",
|
||||
"computedHash": "6811250488d1dafcb111e7ef0dc1124211369c5f6f47ce820b7527c255489178"
|
||||
},
|
||||
"review": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/in-progress/review/SKILL.md",
|
||||
"computedHash": "fc4ca8a9495e93692282bebeb3bc99a6d46e27e2fccf4730001df98451b72f80"
|
||||
},
|
||||
"scaffold-exercises": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/misc/scaffold-exercises/SKILL.md",
|
||||
"computedHash": "1127f69a77b48cb855067db4c9c42e7638e6672f1cb3fafb0ffd61a1ecdfae70"
|
||||
},
|
||||
"setup-matt-pocock-skills": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/setup-matt-pocock-skills/SKILL.md",
|
||||
"computedHash": "f686b246e95afeaeeb3ff21b0f9f2c1ab29b02958468170cc9802fb477508488"
|
||||
},
|
||||
"setup-pre-commit": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/misc/setup-pre-commit/SKILL.md",
|
||||
"computedHash": "f2ce8663857d919cf7e4146cd716b0aee1757b85123b758e6e7d98fcbaf14054"
|
||||
},
|
||||
"tdd": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/tdd/SKILL.md",
|
||||
"computedHash": "78b31b2120c5fe7aced1cebfd4c7c94acb0037fd4f89c83c67584414aa4173bd"
|
||||
},
|
||||
"teach": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/teach/SKILL.md",
|
||||
"computedHash": "0743fa1210ec3591bcbee532484169ee10923e1b13537be24431fbaf38206eb2"
|
||||
},
|
||||
"to-issues": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/to-issues/SKILL.md",
|
||||
"computedHash": "41915d1686991b0d3796eb0b52f3aa8e9021abc5f268d14e6dc62e95dbf8e044"
|
||||
},
|
||||
"to-prd": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
||||
"computedHash": "1f5f7a475757eb4030155130f4fd7f161a20e5731c4b39edd4abe2df9cc72901"
|
||||
},
|
||||
"triage": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/triage/SKILL.md",
|
||||
"computedHash": "56ff15b41bbebfa4cb329d96150d9b297c1d919ce30784d883b8755b4bfd8e7e"
|
||||
},
|
||||
"ubiquitous-language": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/deprecated/ubiquitous-language/SKILL.md",
|
||||
"computedHash": "4828c4957014e7d9c1fd62d65e92264d6d46808089953e14d1bbba5a92dfff4d"
|
||||
},
|
||||
"write-a-skill": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/write-a-skill/SKILL.md",
|
||||
"computedHash": "3b58a16bde08f84ed490cd449ecdc40289216d660e070c485f53bc2d1ed2b843"
|
||||
},
|
||||
"writing-beats": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/in-progress/writing-beats/SKILL.md",
|
||||
"computedHash": "b416f5b8d349d40c604ca2fd1d1664e8672c236e188fa524ae4e8db5a8174580"
|
||||
},
|
||||
"writing-fragments": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/in-progress/writing-fragments/SKILL.md",
|
||||
"computedHash": "248bdd9334033ab044dfe29458866f9b2353a8006416e00d1058a3914981ae6e"
|
||||
},
|
||||
"writing-shape": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/in-progress/writing-shape/SKILL.md",
|
||||
"computedHash": "db0659096afcae5bfe7d913e46c1dc1f49357d7b362791613fe5f62fcc00daf7"
|
||||
},
|
||||
"zoom-out": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/zoom-out/SKILL.md",
|
||||
"computedHash": "a8b8ed45609fdfa9f184d0c9f69326e43822a42eebea14db2792d777373de562"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/pay/index',
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { PropsWithChildren } from 'react'
|
||||
import { useLaunch } from '@tarojs/taro'
|
||||
|
||||
import './app.scss'
|
||||
|
||||
function App({ children }: PropsWithChildren<any>) {
|
||||
useLaunch(() => {
|
||||
console.log('App launched.')
|
||||
})
|
||||
|
||||
// children 是将要会渲染的页面
|
||||
return children
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-touch-fullscreen" content="yes">
|
||||
<meta name="format-detection" content="telephone=no,address=no">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
|
||||
<title>corp-mp</title>
|
||||
<script><%= htmlWebpackPlugin.options.script %></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页'
|
||||
})
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { View, WebView } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
|
||||
const LINK_PREFIX = '/p/'
|
||||
const DEFAULT_LINK_CODE = '57yE' //'Jl2G'
|
||||
|
||||
function isValidLinkCode (code: string) {
|
||||
return Boolean(code) && !/[/?#&=]/.test(code)
|
||||
}
|
||||
|
||||
function normalizeQrLinkCode (scene?: string) {
|
||||
if (!scene) return DEFAULT_LINK_CODE
|
||||
|
||||
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 : DEFAULT_LINK_CODE
|
||||
} catch (error) {
|
||||
console.warn('Invalid qrcode scene.', error)
|
||||
return DEFAULT_LINK_CODE
|
||||
}
|
||||
}
|
||||
|
||||
export default function Index () {
|
||||
const [url, setUrl] = useState('')
|
||||
useLoad(async (options) => {
|
||||
const wxCode = await Taro.login()
|
||||
const linkCode = normalizeQrLinkCode(options?.scene as string | undefined)
|
||||
|
||||
console.log('Page loaded.', wxCode)
|
||||
// setUrl(`http://niubsw.com${LINK_PREFIX}${encodeURIComponent(linkCode)}?mpCode=${encodeURIComponent(wxCode.code)}`)
|
||||
setUrl(`http://nb.batiao8.com${LINK_PREFIX}${encodeURIComponent(linkCode)}?mpCode=${encodeURIComponent(wxCode.code)}`)
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='index'>
|
||||
{
|
||||
url && <WebView src={url}></WebView>
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default definePageConfig({
|
||||
navigationBarTitleText: '支付'
|
||||
})
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
.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;
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('stylelint').Config} */
|
||||
export default {
|
||||
extends: "stylelint-config-standard",
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "lib",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"paths": {
|
||||
// TS5090 leading './'
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src", "./types", "./config"],
|
||||
"compileOnSave": false
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/// <reference types="@tarojs/taro" />
|
||||
|
||||
declare module '*.png';
|
||||
declare module '*.gif';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.svg';
|
||||
declare module '*.css';
|
||||
declare module '*.less';
|
||||
declare module '*.scss';
|
||||
declare module '*.sass';
|
||||
declare module '*.styl';
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/** NODE 内置环境变量, 会影响到最终构建生成产物 */
|
||||
NODE_ENV: 'development' | 'production',
|
||||
/** 当前构建的平台 */
|
||||
TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'qq' | 'jd' | 'harmony' | 'jdrn'
|
||||
/**
|
||||
* 当前构建的小程序 appid
|
||||
* @description 若不同环境有不同的小程序,可通过在 env 文件中配置环境变量`TARO_APP_ID`来方便快速切换 appid, 而不必手动去修改 dist/project.config.json 文件
|
||||
* @see https://taro-docs.jd.com/docs/next/env-mode-config#特殊环境变量-taro_app_id
|
||||
*/
|
||||
TARO_APP_ID: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue