refactor: migrate mini program to Taro React
This commit is contained in:
parent
c484e6997a
commit
1a31c85e27
|
|
@ -0,0 +1,26 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'react-hooks'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist/', 'node_modules/'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
}
|
||||
|
|
@ -1 +1,7 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.temp/
|
||||
.cache/
|
||||
.swc/
|
||||
unpackage/
|
||||
*.log
|
||||
|
|
|
|||
20
App.vue
20
App.vue
|
|
@ -1,20 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
globalData: {
|
||||
token: '',
|
||||
},
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# batiao-pay
|
||||
|
||||
基于 Taro 4、React 18 和 TypeScript 的微信/支付宝支付小程序。
|
||||
|
||||
## 开发与构建
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev:weapp
|
||||
pnpm dev:alipay
|
||||
pnpm build:weapp
|
||||
pnpm build:alipay
|
||||
```
|
||||
|
||||
微信构建产物位于 `dist/weapp`,支付宝构建产物位于 `dist/alipay`。
|
||||
|
||||
## 质量检查
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
pnpm test
|
||||
```
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'taro',
|
||||
{
|
||||
framework: 'react',
|
||||
ts: true,
|
||||
compiler: 'webpack5',
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
import path from 'node:path'
|
||||
|
||||
const outputRoot = `dist/${process.env.TARO_ENV ?? 'weapp'}`
|
||||
|
||||
const config: UserConfigExport<'webpack5'> = {
|
||||
projectName: 'batiao-pay',
|
||||
date: '2026-07-14',
|
||||
designWidth: 750,
|
||||
deviceRatio: {
|
||||
640: 2.34 / 2,
|
||||
750: 1,
|
||||
828: 1.81 / 2,
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot,
|
||||
framework: 'react',
|
||||
compiler: 'webpack5',
|
||||
plugins: ['@tarojs/plugin-framework-react'],
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '..', 'src'),
|
||||
},
|
||||
defineConstants: {},
|
||||
copy: {
|
||||
patterns: [
|
||||
{
|
||||
from: 'static/image',
|
||||
to: `${outputRoot}/static/image`,
|
||||
},
|
||||
],
|
||||
options: {},
|
||||
},
|
||||
cache: {
|
||||
enable: false,
|
||||
},
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: {
|
||||
enable: true,
|
||||
config: {},
|
||||
},
|
||||
url: {
|
||||
enable: true,
|
||||
config: {
|
||||
limit: 1024,
|
||||
},
|
||||
},
|
||||
cssModules: {
|
||||
enable: false,
|
||||
config: {
|
||||
namingPattern: 'module',
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default defineConfig<'webpack5'>(() => config)
|
||||
20
index.html
20
index.html
|
|
@ -1,20 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// const BASE_URL = 'https://u.batiao8.com' //测试
|
||||
const BASE_URL = 'https://pay.batiao8.com' //正式
|
||||
export default BASE_URL
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import BASE_URL from './baseUrl.js'
|
||||
|
||||
const request = async ({
|
||||
url,
|
||||
method,
|
||||
data
|
||||
}) => {
|
||||
try {
|
||||
const app = getApp();
|
||||
const globalData = app.globalData || {}
|
||||
let header = {};
|
||||
if (globalData.token) {
|
||||
header['x-token'] = globalData.token;
|
||||
}
|
||||
let res = await uni.request({
|
||||
url: BASE_URL + url,
|
||||
method: method,
|
||||
data: data,
|
||||
// header: header
|
||||
})
|
||||
console.log('--res', res)
|
||||
if (res.data) {
|
||||
if(res.data.code && res.data.code !== 200) {
|
||||
uni.showToast({
|
||||
title: res.data.message.split(',')[0],
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
return res.data
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message,
|
||||
icon: 'none',
|
||||
})
|
||||
return {}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('--err', err)
|
||||
uni.showToast({
|
||||
title: err.errMsg,
|
||||
icon: 'none',
|
||||
})
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default request
|
||||
22
main.js
22
main.js
|
|
@ -1,22 +0,0 @@
|
|||
import App from './App'
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
{
|
||||
"name" : "batiao-pay",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wxb0e5bc0e275ba55c",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true,
|
||||
"appid" : "2021005194639068"
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "batiao-pay",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Batiao payment mini program",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@swc/core",
|
||||
"@tarojs/binding",
|
||||
"@tarojs/cli",
|
||||
"core-js",
|
||||
"core-js-pure",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"dev:weapp": "taro build --type weapp --watch",
|
||||
"dev:alipay": "taro build --type alipay --watch",
|
||||
"build:weapp": "taro build --type weapp",
|
||||
"build:alipay": "taro build --type alipay",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src config vitest.config.ts --ext .ts,.tsx",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@tarojs/components": "4.2.0",
|
||||
"@tarojs/helper": "4.2.0",
|
||||
"@tarojs/plugin-framework-react": "4.2.0",
|
||||
"@tarojs/plugin-platform-alipay": "4.2.0",
|
||||
"@tarojs/plugin-platform-weapp": "4.2.0",
|
||||
"@tarojs/react": "4.2.0",
|
||||
"@tarojs/runtime": "4.2.0",
|
||||
"@tarojs/shared": "4.2.0",
|
||||
"@tarojs/taro": "4.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@tarojs/cli": "4.2.0",
|
||||
"@tarojs/taro-loader": "4.2.0",
|
||||
"@tarojs/webpack5-runner": "4.2.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"babel-preset-taro": "4.2.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"less": "4.6.4",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "4.5.14",
|
||||
"vitest": "1.6.1"
|
||||
}
|
||||
}
|
||||
17
pages.json
17
pages.json
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
<template>
|
||||
<view class="wrapper" style="height: 100vh">
|
||||
<!-- 浏览器不支持提示 -->
|
||||
<view class="wrapper" id="pay-result" style="background: #fff" v-if="envErrType">
|
||||
<view class="pay-result-container">
|
||||
<image src="/static/image/payerr.png" style="width: 96px; height: 96px"></image>
|
||||
<view class="env-err-text">{{ envErrText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="wrapper" id="pay-result" style="background: #fff" v-else-if="orderInfo.status === 2">
|
||||
<view class="pay-result-container">
|
||||
<image v-if="browerEnv === 'alipay'" src="/static/image/ali-payok.png" style="width: 96px; height: 96px"></image>
|
||||
<image v-if="browerEnv === 'weixin'" src="/static/image/wx-payok.png" style="width: 96px; height: 96px"></image>
|
||||
<view class="pay-result-text">支付已完成</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付页面 -->
|
||||
<view class="wrapper" id="pay-page" v-else-if="currentPage === 'pay-page'">
|
||||
<view class="item-name">{{ orderInfo.goodsName }}</view>
|
||||
<view class="item-count">
|
||||
<text style="font-size: 26px">¥ </text>{{ numberFormat(orderInfo.totalFee) }}
|
||||
</view>
|
||||
<!-- <view class="company">重庆八条科技有限公司</view> -->
|
||||
|
||||
<!-- 支付按钮 -->
|
||||
<view class="pay-methods">
|
||||
<view v-for="item in payMethodList" :key="item.id" class="pay-method-item">
|
||||
<view class="pay-method-item-left">
|
||||
<image :src="item.logo" style="width: 30px; height: 30px"></image>
|
||||
<text class="pay-method-name">{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="pay-method-item-right">
|
||||
<image :src="item.checkIcon" style="width: 18px; height: 18px"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button id="pay-btn" :class="[`${browerEnv}-btn`]" @click="goPay">
|
||||
立即支付
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 支付结果页面 -->
|
||||
<view class="wrapper" id="pay-result" style="background: #fff" v-else-if="currentPage === 'pay-result'">
|
||||
<view class="pay-result-container">
|
||||
<image :src="payResultItem.icon" style="width: 96px; height: 96px"></image>
|
||||
<view class="pay-result-text">{{ payResultItem.resultText }}</view>
|
||||
<button v-if="fromApp" class="back-btn" open-type="launchApp" app-parameter="wechat" :binderror="launchAppError">返回APP</button>
|
||||
<button v-else class="back-btn" :style="payResultItem.style" @click="goBackPay">
|
||||
{{ payResultItem.backText }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/lib/request.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
authCode: '',
|
||||
encOrderId: "",
|
||||
outTradeNo: "",
|
||||
browerEnv: "",
|
||||
orderInfo: {},
|
||||
currentPage: "pay-page",
|
||||
paySuccess: true,
|
||||
payMethodList: [{
|
||||
name: "支付宝支付",
|
||||
id: "alipay",
|
||||
logo: "/static/image/ali-logo.png",
|
||||
checkIcon: "/static/image/ali-check.png",
|
||||
},
|
||||
{
|
||||
name: "微信支付",
|
||||
id: "weixin",
|
||||
logo: "/static/image/wx-logo.png",
|
||||
checkIcon: "/static/image/wx-check.png",
|
||||
},
|
||||
],
|
||||
timer: null,
|
||||
fromApp: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
payResultList() {
|
||||
return [{
|
||||
icon: "/static/image/payerr.png",
|
||||
resultText: "抱歉,订单支付失败",
|
||||
backText: "重新支付",
|
||||
style: {background: this.browerEnv === "weixin" ? '#05bf5e' : '#2c78fe', color: '#fff'},
|
||||
},
|
||||
{
|
||||
icon: this.browerEnv === "weixin" ?
|
||||
"/static/image/wx-payok.png" : "/static/image/ali-payok.png",
|
||||
resultText: "支付成功",
|
||||
backText: "返回",
|
||||
},
|
||||
];
|
||||
},
|
||||
payResultItem() {
|
||||
return this.paySuccess ? this.payResultList[1] : this.payResultList[0];
|
||||
},
|
||||
envErrType() {
|
||||
if(!this.browerEnv) return '';
|
||||
if (this.browerEnv === "browser") {
|
||||
return "envErr:browser";
|
||||
} else if (!this.encOrderId && !this.outTradeNo) {
|
||||
return "envErr:noParams";
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
envErrText() {
|
||||
const group = {
|
||||
"envErr:browser": "不支持浏览器,请使用支付宝或微信重新扫码",
|
||||
"envErr:noParams": "请通过扫描二维码使用小程序",
|
||||
"envErr:outTradeNo": "获取订单信息失败",
|
||||
};
|
||||
return group[this.envErrType];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
launchAppError(e) {
|
||||
console.log(e.detail.errMsg)
|
||||
},
|
||||
numberFormat(value) {
|
||||
if (value) return value.toFixed(2);
|
||||
return "0.00";
|
||||
},
|
||||
// 获取支付宝用户信息
|
||||
getAliUserInfo() {
|
||||
// 检查是否已授权
|
||||
// #ifdef MP-ALIPAY
|
||||
my.getAuthCode({
|
||||
scopes: ["auth_base"],
|
||||
success: (res) => {
|
||||
console.log("获取授权码成功", res);
|
||||
this.authCode = res.authCode;
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error("获取授权码失败", error);
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "获取授权码失败",
|
||||
});
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
// 获取微信用户信息
|
||||
getWxUserInfo() {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.getProvider({
|
||||
service: 'oauth',
|
||||
success: (res) => {
|
||||
console.log('getProvider', res.provider)
|
||||
uni.login({
|
||||
provider: res.provider,
|
||||
success: (loginRes) => {
|
||||
this.authCode = loginRes.code
|
||||
console.log('loginRes', this.authCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 支付按钮点击事件
|
||||
async goPay() {
|
||||
uni.showLoading()
|
||||
try {
|
||||
// 应用下单
|
||||
const {
|
||||
outTradeNo,
|
||||
paySource = "jsapi"
|
||||
} = this.orderInfo;
|
||||
let extra = {};
|
||||
// #ifdef MP-ALIPAY
|
||||
extra.alipayAuthCode = this.authCode;
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
extra.weixinAuthCode = this.authCode;
|
||||
// #endif
|
||||
console.log('authCode',this.authCode)
|
||||
if (!this.authCode) {
|
||||
console.error("没有授权码");
|
||||
return;
|
||||
}
|
||||
let orderData = await this.handleOrder({
|
||||
outTradeNo,
|
||||
payType: this.browerEnv,
|
||||
paySource: paySource,
|
||||
extra,
|
||||
});
|
||||
let tradeNo = orderData.tradeNo;
|
||||
console.log("tradeNo", tradeNo);
|
||||
if(this.browerEnv === 'alipay') {
|
||||
// 调起支付宝支付
|
||||
let res = await this.handleAlipay({
|
||||
tradeNO: tradeNo,
|
||||
});
|
||||
|
||||
console.log('支付宝支付成功', res)
|
||||
} else if(this.browerEnv === 'weixin') {
|
||||
let res = await this.handleWechatPay(orderData);
|
||||
|
||||
console.log('微信支付成功', res)
|
||||
}
|
||||
|
||||
|
||||
// 支付成功
|
||||
let orderRes = await this.getOrderInfo()
|
||||
this.currentPage = "pay-result";
|
||||
if (orderRes.status === 2) {
|
||||
this.paySuccess = true;
|
||||
// clearInterval(this.timer)
|
||||
// this.timer = null
|
||||
} else {
|
||||
this.paySuccess = false;
|
||||
}
|
||||
uni.hideLoading()
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
this.currentPage = "pay-result";
|
||||
this.paySuccess = false; // 失败
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
// 查询订单
|
||||
async getOrderInfo() {
|
||||
let res = await request({
|
||||
url: "/api/pay/order",
|
||||
method: "GET",
|
||||
data: {
|
||||
encOrderId: this.encOrderId,
|
||||
outTradeNo: this.outTradeNo
|
||||
},
|
||||
});
|
||||
const data = {
|
||||
...res.data,
|
||||
totalFee: res.data.totalFee / 100,
|
||||
};
|
||||
return data;
|
||||
},
|
||||
// 应用下单
|
||||
async handleOrder(data) {
|
||||
let res = await request({
|
||||
url: "/api/pay/order",
|
||||
method: "POST",
|
||||
data: data,
|
||||
});
|
||||
console.log("handleOrder", res.data);
|
||||
return res.data || {};
|
||||
},
|
||||
|
||||
// 微信支付处理
|
||||
handleWechatPay(payParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 在Uniapp中,可以使用uni.requestPayment API
|
||||
uni.requestPayment({
|
||||
provider: "wxpay",
|
||||
timeStamp: payParams.timeStamp,
|
||||
nonceStr: payParams.nonceStr,
|
||||
package: payParams.package,
|
||||
signType: payParams.signType,
|
||||
paySign: payParams.paySign,
|
||||
success: (res) => {
|
||||
console.log("支付成功:", res);
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("支付失败:", err);
|
||||
reject(new Error("支付失败"));
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 支付宝支付处理
|
||||
handleAlipay(payParams) {
|
||||
console.log("支付宝支付处理");
|
||||
return new Promise((resolve, reject) => {
|
||||
// 在Uniapp中,可以使用uni.requestPayment API
|
||||
uni.requestPayment({
|
||||
provider: "alipay",
|
||||
orderInfo: payParams.tradeNO, // 支付宝交易号
|
||||
success: (res) => {
|
||||
if (res.resultCode === "9000") {
|
||||
console.log("支付成功", res);
|
||||
// 建议以后端异步通知或主动查询订单状态为准
|
||||
resolve();
|
||||
} else {
|
||||
console.log("支付处理中或失败", res);
|
||||
// 处理其他结果码
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("支付失败:", err);
|
||||
reject(new Error("支付失败"));
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async goBackPay() {
|
||||
this.orderInfo = await this.getOrderInfo();
|
||||
this.currentPage = "pay-page";
|
||||
},
|
||||
// 获取URL参数
|
||||
getQuery() {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options || {};
|
||||
return options;
|
||||
},
|
||||
// 判断浏览器环境是微信/支付宝/普通浏览器
|
||||
getBrowserEnv() {
|
||||
// 非H5环境,根据平台判断
|
||||
// #ifdef MP-WEIXIN
|
||||
return "weixin";
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
return "alipay";
|
||||
// #endif
|
||||
},
|
||||
extractUrlParams(url) {
|
||||
if (!url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 使用正则表达式解析URL参数,兼容支付宝环境
|
||||
const params = {};
|
||||
const queryString = url.split("?")[1];
|
||||
|
||||
if (queryString) {
|
||||
const pairs = queryString.split("&");
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const pair = pairs[i].split("=");
|
||||
const key = decodeURIComponent(pair[0]);
|
||||
const value = decodeURIComponent(pair[1] || "");
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
},
|
||||
async onShow() {
|
||||
console.log("on Show");
|
||||
// 支付宝
|
||||
// #ifdef MP-ALIPAY
|
||||
if (my) {
|
||||
let res = my.getLaunchOptionsSync();
|
||||
console.log("res", res);
|
||||
const qrCode = res.query ? res.query.qrCode : '';
|
||||
let {
|
||||
encOrderId
|
||||
} = this.extractUrlParams(qrCode);
|
||||
if (encOrderId) {
|
||||
this.encOrderId = encOrderId;
|
||||
this.orderInfo = await this.getOrderInfo();
|
||||
}
|
||||
this.getAliUserInfo()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
async onLoad(options) {
|
||||
console.log("options", options);
|
||||
|
||||
const env = this.getBrowserEnv();
|
||||
console.log('env', env)
|
||||
this.browerEnv = env;
|
||||
if (env === "browser") return;
|
||||
|
||||
// 过滤支付方式,只显示当前环境支持的支付方式
|
||||
this.payMethodList = this.payMethodList.filter((item) => item.id === env);
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if(options.q) { // q: 二维码地址
|
||||
let {
|
||||
encOrderId
|
||||
} = this.extractUrlParams(decodeURIComponent(options.q));
|
||||
console.log('encOrderId', encOrderId)
|
||||
if (encOrderId) {
|
||||
this.encOrderId = encOrderId;
|
||||
this.orderInfo = await this.getOrderInfo();
|
||||
}
|
||||
}
|
||||
// options.outTradeNo = "10050_20260608161529904_10018"
|
||||
options.outTradeNo = "10044_20260608164234963_1884228"
|
||||
if(options.outTradeNo) {
|
||||
// app进入的
|
||||
this.fromApp = true;
|
||||
this.outTradeNo = options.outTradeNo;
|
||||
this.orderInfo = await this.getOrderInfo();
|
||||
}
|
||||
console.log('encOrderId', this.encOrderId)
|
||||
console.log('outTradeNo', this.outTradeNo)
|
||||
this.getWxUserInfo()
|
||||
// #endif
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import url("/static/app.css");
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"appid": "2021005194639068",
|
||||
"projectname": "batiao-pay",
|
||||
"miniprogramRoot": "dist/alipay/",
|
||||
"component2": true,
|
||||
"enableAppxNg": true,
|
||||
"axmlStrictCheck": false
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"description": "Batiao payment mini program",
|
||||
"miniprogramRoot": "dist/weapp/",
|
||||
"projectname": "batiao-pay",
|
||||
"appid": "wxb0e5bc0e275ba55c",
|
||||
"compileType": "miniprogram",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export default defineAppConfig({
|
||||
pages: ['pages/index/index'],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#F8F8F8',
|
||||
navigationBarTitleText: '',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#F8F8F8',
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
page {
|
||||
min-height: 100%;
|
||||
background-color: #f2f2f2;
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import type { PropsWithChildren } from 'react'
|
||||
|
||||
import './app.css'
|
||||
|
||||
function App({ children }: PropsWithChildren) {
|
||||
return children
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
declare const my: {
|
||||
getAuthCode(options: {
|
||||
scopes: string[]
|
||||
success: (result: { authCode?: string }) => void
|
||||
fail: (error: { errorMessage?: string; errMsg?: string }) => void
|
||||
}): void
|
||||
getLaunchOptionsSync(): {
|
||||
query?: {
|
||||
qrCode?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
TARO_ENV?: 'weapp' | 'alipay'
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default definePageConfig({
|
||||
navigationBarTitleText: '',
|
||||
})
|
||||
|
|
@ -1,61 +1,46 @@
|
|||
body, page {
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
text-align: center;
|
||||
background-color: #f2f2f2;
|
||||
page {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #f2f2f2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-placeholder,
|
||||
.pay-result {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
/* width: 160rpx; */
|
||||
height: 56rpx;
|
||||
margin-top: 140rpx;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-family: PingFang SC, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 40rpx;
|
||||
color: #1A1A1A;
|
||||
color: #1a1a1a;
|
||||
line-height: 56rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
/* width: 250rpx; */
|
||||
height: 92rpx;
|
||||
font-family: D-DIN-PRO, D-DIN-PRO;
|
||||
margin: 12rpx 0 16rpx;
|
||||
font-family: D-DIN-PRO, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 80rpx;
|
||||
color: #FF2C2C;
|
||||
color: #ff2c2c;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
margin: 12rpx 0 16rpx 0;
|
||||
}
|
||||
|
||||
.company {
|
||||
/* width: 280rpx; */
|
||||
height: 40rpx;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #53896D;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.pay-methods {
|
||||
|
|
@ -66,21 +51,17 @@ body, page {
|
|||
.pay-method-item {
|
||||
width: 640rpx;
|
||||
height: 120rpx;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0rpx 0rpx 20rpx 0rpx rgba(36, 36, 36, 0.05);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 28rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 20rpx rgba(36, 36, 36, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.pay-method-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pay-method-item-left {
|
||||
.pay-method-item-left,
|
||||
.pay-method-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -89,31 +70,43 @@ body, page {
|
|||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
#pay-btn {
|
||||
width: 686rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 1842rpx;
|
||||
.pay-btn,
|
||||
.back-btn {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-family: PingFang SC, sans-serif;
|
||||
font-size: 32rpx;
|
||||
color: #FFFFFF;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.pay-btn::after,
|
||||
.back-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.pay-btn {
|
||||
width: 686rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 1842rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.weixin-btn {
|
||||
background: #06BF5E;
|
||||
background: #06bf5e;
|
||||
}
|
||||
|
||||
.alipay-btn {
|
||||
background: #2c78fe;
|
||||
}
|
||||
|
||||
.pay-btn-disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.pay-result-container {
|
||||
margin-top: 140rpx;
|
||||
display: flex;
|
||||
|
|
@ -123,45 +116,32 @@ body, page {
|
|||
}
|
||||
|
||||
.pay-result-text {
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
margin: 52rpx 0 140rpx;
|
||||
font-family: PingFang SC, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 36rpx;
|
||||
color: #1A1A1A;
|
||||
color: #1a1a1a;
|
||||
line-height: 52rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
margin: 52rpx 0 140rpx 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 312rpx;
|
||||
height: 104rpx;
|
||||
background: #E7E7E7;
|
||||
background: #e7e7e7;
|
||||
border-radius: 1842rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #666666;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.env-err-text {
|
||||
margin-top: 82rpx;
|
||||
width: 504rpx;
|
||||
height: 112rpx;
|
||||
font-family: AlibabaPuHuiTi, AlibabaPuHuiTi;
|
||||
min-height: 112rpx;
|
||||
margin-top: 82rpx;
|
||||
font-family: AlibabaPuHuiTi, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #F64F50;
|
||||
color: #f64f50;
|
||||
line-height: 56rpx;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
import Taro, { useDidShow, useLoad } from '@tarojs/taro'
|
||||
import { Button, Image, Text, View } from '@tarojs/components'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import {
|
||||
getAlipayLaunchQrCode,
|
||||
getPlatformAuthCode,
|
||||
PAY_ENVIRONMENT,
|
||||
requestPlatformPayment,
|
||||
} from '@/platform/payment'
|
||||
import { createPaymentOrder, getOrderInfo } from '@/services/payment'
|
||||
import type { OrderInfo, OrderLookup } from '@/types/payment'
|
||||
import {
|
||||
createSubmissionGuard,
|
||||
formatAmount,
|
||||
isPaid,
|
||||
resolveAlipayLaunchParams,
|
||||
resolveWeappLaunchParams,
|
||||
type LaunchResolution,
|
||||
} from '@/utils/payment'
|
||||
|
||||
import './index.css'
|
||||
|
||||
type PageView = 'loading' | 'pay' | 'result' | 'completed' | 'error'
|
||||
|
||||
const IMAGE_PATHS = {
|
||||
alipayCheck: '/static/image/ali-check.png',
|
||||
alipayLogo: '/static/image/ali-logo.png',
|
||||
alipaySuccess: '/static/image/ali-payok.png',
|
||||
error: '/static/image/payerr.png',
|
||||
weixinCheck: '/static/image/wx-check.png',
|
||||
weixinLogo: '/static/image/wx-logo.png',
|
||||
weixinSuccess: '/static/image/wx-payok.png',
|
||||
} as const
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function IndexPage() {
|
||||
const [authCode, setAuthCode] = useState('')
|
||||
const [errorText, setErrorText] = useState('')
|
||||
const [fromApp, setFromApp] = useState(false)
|
||||
const [lookup, setLookup] = useState<OrderLookup>({})
|
||||
const [orderInfo, setOrderInfo] = useState<OrderInfo | null>(null)
|
||||
const [paySuccess, setPaySuccess] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [view, setView] = useState<PageView>('loading')
|
||||
const initializingRef = useRef(false)
|
||||
const submissionGuardRef = useRef(createSubmissionGuard())
|
||||
|
||||
const payMethod =
|
||||
PAY_ENVIRONMENT === 'weixin'
|
||||
? {
|
||||
name: '微信支付',
|
||||
logo: IMAGE_PATHS.weixinLogo,
|
||||
checkIcon: IMAGE_PATHS.weixinCheck,
|
||||
}
|
||||
: {
|
||||
name: '支付宝支付',
|
||||
logo: IMAGE_PATHS.alipayLogo,
|
||||
checkIcon: IMAGE_PATHS.alipayCheck,
|
||||
}
|
||||
|
||||
const successIcon =
|
||||
PAY_ENVIRONMENT === 'weixin' ? IMAGE_PATHS.weixinSuccess : IMAGE_PATHS.alipaySuccess
|
||||
|
||||
async function initializePage(resolution: LaunchResolution | null): Promise<void> {
|
||||
if (initializingRef.current || submissionGuardRef.current.isLocked()) {
|
||||
return
|
||||
}
|
||||
if (!resolution) {
|
||||
setErrorText('请通过扫描二维码使用小程序')
|
||||
setView('error')
|
||||
return
|
||||
}
|
||||
|
||||
initializingRef.current = true
|
||||
setLookup(resolution.lookup)
|
||||
setFromApp(resolution.fromApp)
|
||||
setView('loading')
|
||||
Taro.showLoading({ title: '加载中', mask: true })
|
||||
try {
|
||||
const [nextOrderInfo, nextAuthCode] = await Promise.all([
|
||||
getOrderInfo(resolution.lookup),
|
||||
getPlatformAuthCode(),
|
||||
])
|
||||
setOrderInfo(nextOrderInfo)
|
||||
setAuthCode(nextAuthCode)
|
||||
setView(isPaid(nextOrderInfo.status) ? 'completed' : 'pay')
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error, '获取订单信息失败')
|
||||
setErrorText(message)
|
||||
setView('error')
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
} finally {
|
||||
initializingRef.current = false
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
useLoad((options) => {
|
||||
if (process.env.TARO_ENV !== 'weapp') {
|
||||
return
|
||||
}
|
||||
void initializePage(resolveWeappLaunchParams(options))
|
||||
})
|
||||
|
||||
useDidShow(() => {
|
||||
if (process.env.TARO_ENV !== 'alipay') {
|
||||
return
|
||||
}
|
||||
const resolution = resolveAlipayLaunchParams(getAlipayLaunchQrCode())
|
||||
void initializePage(resolution)
|
||||
})
|
||||
|
||||
async function handlePay(): Promise<void> {
|
||||
if (!submissionGuardRef.current.tryLock()) {
|
||||
return
|
||||
}
|
||||
if (!orderInfo || !authCode) {
|
||||
submissionGuardRef.current.unlock()
|
||||
Taro.showToast({ title: '支付信息尚未准备完成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
Taro.showLoading({ title: '支付中', mask: true })
|
||||
try {
|
||||
const paymentOrder = await createPaymentOrder({
|
||||
outTradeNo: orderInfo.outTradeNo,
|
||||
payType: PAY_ENVIRONMENT,
|
||||
paySource: orderInfo.paySource || 'jsapi',
|
||||
extra:
|
||||
PAY_ENVIRONMENT === 'weixin'
|
||||
? { weixinAuthCode: authCode }
|
||||
: { alipayAuthCode: authCode },
|
||||
})
|
||||
await requestPlatformPayment(paymentOrder)
|
||||
|
||||
const latestOrderInfo = await getOrderInfo(lookup)
|
||||
const paid = isPaid(latestOrderInfo.status)
|
||||
setPaySuccess(paid)
|
||||
setView('result')
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error, '支付失败')
|
||||
setPaySuccess(false)
|
||||
setView('result')
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
submissionGuardRef.current.unlock()
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBackToPay(): Promise<void> {
|
||||
Taro.showLoading({ title: '加载中', mask: true })
|
||||
try {
|
||||
const latestOrderInfo = await getOrderInfo(lookup)
|
||||
setOrderInfo(latestOrderInfo)
|
||||
setView(isPaid(latestOrderInfo.status) ? 'completed' : 'pay')
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error, '获取订单信息失败')
|
||||
setErrorText(message)
|
||||
setView('error')
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function handleLaunchAppError(event: { detail?: { errMsg?: string } }): void {
|
||||
const message = event.detail?.errMsg || '返回 APP 失败'
|
||||
Taro.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
|
||||
if (view === 'loading') {
|
||||
return <View className='wrapper loading-placeholder' />
|
||||
}
|
||||
|
||||
if (view === 'error') {
|
||||
return (
|
||||
<View className='wrapper pay-result'>
|
||||
<View className='pay-result-container'>
|
||||
<Image src={IMAGE_PATHS.error} style={{ width: '96px', height: '96px' }} />
|
||||
<View className='env-err-text'>{errorText}</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (view === 'completed') {
|
||||
return (
|
||||
<View className='wrapper pay-result'>
|
||||
<View className='pay-result-container'>
|
||||
<Image src={successIcon} style={{ width: '96px', height: '96px' }} />
|
||||
<View className='pay-result-text'>支付已完成</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (view === 'result') {
|
||||
const resultIcon = paySuccess ? successIcon : IMAGE_PATHS.error
|
||||
const buttonStyle = paySuccess
|
||||
? undefined
|
||||
: {
|
||||
background: PAY_ENVIRONMENT === 'weixin' ? '#05bf5e' : '#2c78fe',
|
||||
color: '#fff',
|
||||
}
|
||||
return (
|
||||
<View className='wrapper pay-result'>
|
||||
<View className='pay-result-container'>
|
||||
<Image src={resultIcon} style={{ width: '96px', height: '96px' }} />
|
||||
<View className='pay-result-text'>{paySuccess ? '支付成功' : '抱歉,订单支付失败'}</View>
|
||||
{fromApp && process.env.TARO_ENV === 'weapp' ? (
|
||||
<Button
|
||||
className='back-btn'
|
||||
openType='launchApp'
|
||||
appParameter='wechat'
|
||||
onError={handleLaunchAppError}
|
||||
>
|
||||
返回APP
|
||||
</Button>
|
||||
) : (
|
||||
<Button className='back-btn' style={buttonStyle} onClick={handleBackToPay}>
|
||||
{paySuccess ? '返回' : '重新支付'}
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const paymentDisabled = submitting || !orderInfo || !authCode
|
||||
return (
|
||||
<View className='wrapper pay-page'>
|
||||
<View className='item-name'>{orderInfo?.goodsName || ''}</View>
|
||||
<View className='item-count'>
|
||||
<Text style={{ fontSize: '26px' }}>¥ </Text>
|
||||
{formatAmount(orderInfo?.totalFee)}
|
||||
</View>
|
||||
|
||||
<View className='pay-methods'>
|
||||
<View className='pay-method-item'>
|
||||
<View className='pay-method-item-left'>
|
||||
<Image src={payMethod.logo} style={{ width: '30px', height: '30px' }} />
|
||||
<Text className='pay-method-name'>{payMethod.name}</Text>
|
||||
</View>
|
||||
<View className='pay-method-item-right'>
|
||||
<Image src={payMethod.checkIcon} style={{ width: '18px', height: '18px' }} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
className={`pay-btn ${PAY_ENVIRONMENT}-btn${paymentDisabled ? ' pay-btn-disabled' : ''}`}
|
||||
disabled={paymentDisabled}
|
||||
onClick={handlePay}
|
||||
>
|
||||
立即支付
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexPage
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
|
||||
import type { PaymentOrderResponse } from '@/types/payment'
|
||||
|
||||
import { PlatformPaymentError } from './payment.types'
|
||||
|
||||
export const PAY_ENVIRONMENT = 'alipay' as const
|
||||
|
||||
export function getPlatformAuthCode(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
my.getAuthCode({
|
||||
scopes: ['auth_base'],
|
||||
success: (result) => {
|
||||
if (result.authCode) {
|
||||
resolve(result.authCode)
|
||||
return
|
||||
}
|
||||
reject(new PlatformPaymentError('获取支付宝授权码失败'))
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(
|
||||
new PlatformPaymentError(error.errorMessage || error.errMsg || '获取支付宝授权码失败'),
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function getAlipayLaunchQrCode(): string {
|
||||
return my.getLaunchOptionsSync().query?.qrCode || ''
|
||||
}
|
||||
|
||||
export async function requestPlatformPayment(params: PaymentOrderResponse): Promise<void> {
|
||||
if (!params.tradeNo) {
|
||||
throw new PlatformPaymentError('支付宝支付参数不完整')
|
||||
}
|
||||
|
||||
const result = await Taro.tradePay({ tradeNO: params.tradeNo })
|
||||
const resultCode = String(result.response?.resultCode || '')
|
||||
if (resultCode !== '9000') {
|
||||
const message = resultCode === '6001' ? '支付已取消' : '支付宝支付未完成'
|
||||
throw new PlatformPaymentError(message, resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
export { PlatformPaymentError } from './payment.types'
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import type { PaymentOrderResponse } from '@/types/payment'
|
||||
|
||||
import type { PaymentAdapter } from './payment.types'
|
||||
|
||||
const adapterModule =
|
||||
process.env.TARO_ENV === 'alipay'
|
||||
// Taro removes the non-target require branch at compile time.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
? (require('./payment.alipay') as { default: PaymentAdapter })
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
: (require('./payment.weapp') as { default: PaymentAdapter })
|
||||
|
||||
const adapter = adapterModule.default
|
||||
|
||||
export const PAY_ENVIRONMENT = adapter.environment
|
||||
|
||||
export function getPlatformAuthCode(): Promise<string> {
|
||||
return adapter.getAuthCode()
|
||||
}
|
||||
|
||||
export function getAlipayLaunchQrCode(): string {
|
||||
return adapter.getLaunchQrCode()
|
||||
}
|
||||
|
||||
export function requestPlatformPayment(params: PaymentOrderResponse): Promise<void> {
|
||||
return adapter.requestPayment(params)
|
||||
}
|
||||
|
||||
export { PlatformPaymentError } from './payment.types'
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import type { PayEnvironment, PaymentOrderResponse } from '@/types/payment'
|
||||
|
||||
export interface PaymentAdapter {
|
||||
environment: PayEnvironment
|
||||
getAuthCode(): Promise<string>
|
||||
getLaunchQrCode(): string
|
||||
requestPayment(params: PaymentOrderResponse): Promise<void>
|
||||
}
|
||||
|
||||
export class PlatformPaymentError extends Error {
|
||||
readonly resultCode?: string
|
||||
|
||||
constructor(message: string, resultCode?: string) {
|
||||
super(message)
|
||||
this.name = 'PlatformPaymentError'
|
||||
this.resultCode = resultCode
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
|
||||
import type { PaymentOrderResponse } from '@/types/payment'
|
||||
|
||||
import { PlatformPaymentError } from './payment.types'
|
||||
|
||||
export const PAY_ENVIRONMENT = 'weixin' as const
|
||||
|
||||
function validatePaymentParameters(params: PaymentOrderResponse): asserts params is Required<
|
||||
Pick<PaymentOrderResponse, 'timeStamp' | 'nonceStr' | 'package' | 'paySign'>
|
||||
> &
|
||||
PaymentOrderResponse {
|
||||
if (!params.timeStamp || !params.nonceStr || !params.package || !params.paySign) {
|
||||
throw new PlatformPaymentError('微信支付参数不完整')
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPlatformAuthCode(): Promise<string> {
|
||||
const result = await Taro.login()
|
||||
if (!result.code) {
|
||||
throw new PlatformPaymentError('获取微信登录凭证失败')
|
||||
}
|
||||
return result.code
|
||||
}
|
||||
|
||||
export function getAlipayLaunchQrCode(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
export async function requestPlatformPayment(params: PaymentOrderResponse): Promise<void> {
|
||||
validatePaymentParameters(params)
|
||||
await Taro.requestPayment({
|
||||
timeStamp: params.timeStamp,
|
||||
nonceStr: params.nonceStr,
|
||||
package: params.package,
|
||||
signType: params.signType || 'MD5',
|
||||
paySign: params.paySign,
|
||||
})
|
||||
}
|
||||
|
||||
export { PlatformPaymentError } from './payment.types'
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { ApiError, unwrapApiEnvelope } from './api-envelope'
|
||||
|
||||
describe('unwrapApiEnvelope', () => {
|
||||
it('returns data for a successful response', () => {
|
||||
expect(unwrapApiEnvelope({ code: 200, data: { id: 1 } })).toEqual({ id: 1 })
|
||||
})
|
||||
|
||||
it('supports a successful response without a code for backwards compatibility', () => {
|
||||
expect(unwrapApiEnvelope({ data: 'ok' })).toBe('ok')
|
||||
})
|
||||
|
||||
it('throws the first backend message for a business error', () => {
|
||||
expect(() => unwrapApiEnvelope({ code: 500, message: '订单失效,请重试', data: {} })).toThrow(
|
||||
'订单失效',
|
||||
)
|
||||
})
|
||||
|
||||
it('throws a typed error for an empty or malformed response', () => {
|
||||
expect(() => unwrapApiEnvelope(null)).toThrow(ApiError)
|
||||
expect(() => unwrapApiEnvelope({ code: 200 })).toThrow('服务响应缺少数据')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
export interface ApiEnvelope<T> {
|
||||
code?: number
|
||||
data?: T
|
||||
message?: string
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly code?: number
|
||||
|
||||
constructor(message: string, code?: number) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
export function unwrapApiEnvelope<T>(value: unknown): T {
|
||||
if (!value || typeof value !== 'object') {
|
||||
throw new ApiError('服务响应异常')
|
||||
}
|
||||
|
||||
const envelope = value as ApiEnvelope<T>
|
||||
if (envelope.code !== undefined && envelope.code !== 200) {
|
||||
const message = envelope.message?.split(',')[0] || '请求失败'
|
||||
throw new ApiError(message, envelope.code)
|
||||
}
|
||||
|
||||
if (envelope.data === undefined || envelope.data === null) {
|
||||
throw new ApiError(envelope.message || '服务响应缺少数据')
|
||||
}
|
||||
|
||||
return envelope.data
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import type {
|
||||
CreatePaymentRequest,
|
||||
OrderInfo,
|
||||
OrderInfoResponse,
|
||||
OrderLookup,
|
||||
PaymentOrderResponse,
|
||||
} from '@/types/payment'
|
||||
|
||||
import { ApiError } from './api-envelope'
|
||||
import { request } from './request'
|
||||
|
||||
function normalizeOrderInfo(data: OrderInfoResponse): OrderInfo {
|
||||
if (!Number.isFinite(data.totalFee) || !Number.isFinite(data.status) || !data.outTradeNo) {
|
||||
throw new ApiError('订单数据异常')
|
||||
}
|
||||
|
||||
return {
|
||||
goodsName: data.goodsName || '',
|
||||
totalFee: Number(data.totalFee) / 100,
|
||||
status: Number(data.status),
|
||||
outTradeNo: data.outTradeNo,
|
||||
paySource: data.paySource,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrderInfo(lookup: OrderLookup): Promise<OrderInfo> {
|
||||
const data = await request<OrderInfoResponse, OrderLookup>({
|
||||
url: '/api/pay/order',
|
||||
method: 'GET',
|
||||
data: lookup,
|
||||
})
|
||||
return normalizeOrderInfo(data)
|
||||
}
|
||||
|
||||
export function createPaymentOrder(data: CreatePaymentRequest): Promise<PaymentOrderResponse> {
|
||||
return request<PaymentOrderResponse, CreatePaymentRequest>({
|
||||
url: '/api/pay/order',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import Taro from '@tarojs/taro'
|
||||
|
||||
import { ApiError, type ApiEnvelope, unwrapApiEnvelope } from './api-envelope'
|
||||
|
||||
const API_BASE_URL = 'https://pay.batiao8.com'
|
||||
|
||||
interface RequestOptions<TData> {
|
||||
url: string
|
||||
method: 'GET' | 'POST'
|
||||
data: TData
|
||||
}
|
||||
|
||||
function getNetworkErrorMessage(error: unknown): string {
|
||||
if (error && typeof error === 'object') {
|
||||
const candidate = error as { errMsg?: string; errorMessage?: string; message?: string }
|
||||
return candidate.errorMessage || candidate.errMsg || candidate.message || '网络请求失败'
|
||||
}
|
||||
return '网络请求失败'
|
||||
}
|
||||
|
||||
export async function request<TResponse, TData extends object>(
|
||||
options: RequestOptions<TData>,
|
||||
): Promise<TResponse> {
|
||||
try {
|
||||
const response = await Taro.request<ApiEnvelope<TResponse>, TData>({
|
||||
url: `${API_BASE_URL}${options.url}`,
|
||||
method: options.method,
|
||||
data: options.data,
|
||||
})
|
||||
return unwrapApiEnvelope<TResponse>(response.data)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error
|
||||
}
|
||||
throw new ApiError(getNetworkErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
export type PayEnvironment = 'weixin' | 'alipay'
|
||||
|
||||
export interface OrderLookup {
|
||||
encOrderId?: string
|
||||
outTradeNo?: string
|
||||
}
|
||||
|
||||
export interface OrderInfoResponse {
|
||||
goodsName?: string
|
||||
totalFee?: number
|
||||
status?: number
|
||||
outTradeNo?: string
|
||||
paySource?: string
|
||||
}
|
||||
|
||||
export interface OrderInfo {
|
||||
goodsName: string
|
||||
totalFee: number
|
||||
status: number
|
||||
outTradeNo: string
|
||||
paySource?: string
|
||||
}
|
||||
|
||||
export interface CreatePaymentRequest {
|
||||
outTradeNo: string
|
||||
payType: PayEnvironment
|
||||
paySource: string
|
||||
extra: {
|
||||
alipayAuthCode?: string
|
||||
weixinAuthCode?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface PaymentOrderResponse {
|
||||
tradeNo?: string
|
||||
timeStamp?: string
|
||||
nonceStr?: string
|
||||
package?: string
|
||||
signType?: 'MD5' | 'HMAC-SHA256' | 'RSA'
|
||||
paySign?: string
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
createSubmissionGuard,
|
||||
extractUrlParams,
|
||||
formatAmount,
|
||||
isPaid,
|
||||
resolveAlipayLaunchParams,
|
||||
resolveWeappLaunchParams,
|
||||
} from './payment'
|
||||
|
||||
describe('payment launch utilities', () => {
|
||||
it('extracts decoded URL parameters', () => {
|
||||
expect(extractUrlParams('https://pay.test/path?encOrderId=abc%20123&empty=')).toEqual({
|
||||
encOrderId: 'abc 123',
|
||||
empty: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps malformed encoded values instead of throwing', () => {
|
||||
expect(extractUrlParams('https://pay.test/path?encOrderId=%E0%A4%A')).toEqual({
|
||||
encOrderId: '%E0%A4%A',
|
||||
})
|
||||
})
|
||||
|
||||
it('gives direct app outTradeNo precedence over QR parameters', () => {
|
||||
const resolution = resolveWeappLaunchParams({
|
||||
outTradeNo: 'order-from-app',
|
||||
q: encodeURIComponent('https://pay.test/path?encOrderId=order-from-qr'),
|
||||
})
|
||||
expect(resolution).toEqual({
|
||||
lookup: { outTradeNo: 'order-from-app' },
|
||||
fromApp: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves WeChat and Alipay QR launch parameters', () => {
|
||||
expect(
|
||||
resolveWeappLaunchParams({
|
||||
q: encodeURIComponent('https://pay.test/path?encOrderId=wechat-order'),
|
||||
}),
|
||||
).toEqual({ lookup: { encOrderId: 'wechat-order' }, fromApp: false })
|
||||
expect(resolveAlipayLaunchParams('https://pay.test/path?encOrderId=alipay-order')).toEqual({
|
||||
lookup: { encOrderId: 'alipay-order' },
|
||||
fromApp: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when launch parameters are missing', () => {
|
||||
expect(resolveWeappLaunchParams({})).toBeNull()
|
||||
expect(resolveAlipayLaunchParams('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('payment state helpers', () => {
|
||||
it('formats zero and invalid monetary values safely', () => {
|
||||
expect(formatAmount(0)).toBe('0.00')
|
||||
expect(formatAmount(12.5)).toBe('12.50')
|
||||
expect(formatAmount(Number.NaN)).toBe('0.00')
|
||||
})
|
||||
|
||||
it('only treats status 2 as paid', () => {
|
||||
expect(isPaid(2)).toBe(true)
|
||||
expect(isPaid(1)).toBe(false)
|
||||
expect(isPaid(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('prevents duplicate submission until unlocked', () => {
|
||||
const guard = createSubmissionGuard()
|
||||
expect(guard.tryLock()).toBe(true)
|
||||
expect(guard.tryLock()).toBe(false)
|
||||
expect(guard.isLocked()).toBe(true)
|
||||
guard.unlock()
|
||||
expect(guard.tryLock()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import type { OrderLookup } from '@/types/payment'
|
||||
|
||||
export interface LaunchResolution {
|
||||
lookup: OrderLookup
|
||||
fromApp: boolean
|
||||
}
|
||||
|
||||
function safeDecode(value: string): string {
|
||||
try {
|
||||
return decodeURIComponent(value.replace(/\+/g, ' '))
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function extractUrlParams(url: string): Record<string, string> {
|
||||
if (!url) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const questionMark = url.indexOf('?')
|
||||
if (questionMark < 0) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const hashMark = url.indexOf('#', questionMark)
|
||||
const queryString = url.slice(questionMark + 1, hashMark < 0 ? undefined : hashMark)
|
||||
return queryString.split('&').reduce<Record<string, string>>((result, entry) => {
|
||||
if (!entry) {
|
||||
return result
|
||||
}
|
||||
const separator = entry.indexOf('=')
|
||||
const rawKey = separator < 0 ? entry : entry.slice(0, separator)
|
||||
const rawValue = separator < 0 ? '' : entry.slice(separator + 1)
|
||||
const key = safeDecode(rawKey)
|
||||
if (key) {
|
||||
result[key] = safeDecode(rawValue)
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function resolveWeappLaunchParams(
|
||||
options: Record<string, string | undefined>,
|
||||
): LaunchResolution | null {
|
||||
const outTradeNo = options.outTradeNo?.trim()
|
||||
if (outTradeNo) {
|
||||
return {
|
||||
lookup: { outTradeNo },
|
||||
fromApp: true,
|
||||
}
|
||||
}
|
||||
|
||||
const encodedQrCode = options.q?.trim()
|
||||
if (!encodedQrCode) {
|
||||
return null
|
||||
}
|
||||
const encOrderId = extractUrlParams(safeDecode(encodedQrCode)).encOrderId?.trim()
|
||||
return encOrderId ? { lookup: { encOrderId }, fromApp: false } : null
|
||||
}
|
||||
|
||||
export function resolveAlipayLaunchParams(qrCode: string): LaunchResolution | null {
|
||||
const encOrderId = extractUrlParams(qrCode).encOrderId?.trim()
|
||||
return encOrderId ? { lookup: { encOrderId }, fromApp: false } : null
|
||||
}
|
||||
|
||||
export function formatAmount(value: number | undefined): string {
|
||||
return Number.isFinite(value) ? Number(value).toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
export function isPaid(status: number | undefined): boolean {
|
||||
return status === 2
|
||||
}
|
||||
|
||||
export interface SubmissionGuard {
|
||||
isLocked(): boolean
|
||||
tryLock(): boolean
|
||||
unlock(): void
|
||||
}
|
||||
|
||||
export function createSubmissionGuard(): SubmissionGuard {
|
||||
let locked = false
|
||||
return {
|
||||
isLocked: () => locked,
|
||||
tryLock: () => {
|
||||
if (locked) {
|
||||
return false
|
||||
}
|
||||
locked = true
|
||||
return true
|
||||
},
|
||||
unlock: () => {
|
||||
locked = false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowJs": false,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["@tarojs/taro", "node"]
|
||||
},
|
||||
"include": ["src", "config", "vitest.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
76
uni.scss
76
uni.scss
|
|
@ -1,76 +0,0 @@
|
|||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
clearMocks: true,
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue