feat(admin): 添加管理后台logo文件- 新增128x128尺寸的SVG格式logo文件
- 使用Method Draw工具创建矢量图形 - 包含背景层和图层1的基础结构 - 支持透明背景显示 - 为管理后台界面提供品牌标识- 便于后续UI组件中引用和展示
24
dict/taro/babel.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// babel-preset-taro 更多选项和默认值:
|
||||
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
|
||||
module.exports = {
|
||||
presets: [
|
||||
['taro',
|
||||
{
|
||||
framework: 'react',
|
||||
ts: 'true',
|
||||
compiler: 'webpack5',
|
||||
}]
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
"import",
|
||||
{
|
||||
"libraryName": "@nutui/nutui-react-taro",
|
||||
"libraryDirectory": "dist/esm",
|
||||
"style": 'css',
|
||||
"camel2DashComponentName": false
|
||||
},
|
||||
'nutui-react-taro'
|
||||
]
|
||||
]
|
||||
}
|
||||
10
dict/taro/config/app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { API_BASE_URL } from './env'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '10519';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
export const Version = 'v3.0.8';
|
||||
// 版权信息
|
||||
export const Copyright = 'WebSoft Inc.';
|
||||
13
dict/taro/config/dev.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli";
|
||||
export default {
|
||||
logger: {
|
||||
quiet: false,
|
||||
stats: true
|
||||
},
|
||||
mini: {
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true
|
||||
}
|
||||
},
|
||||
h5: {}
|
||||
} satisfies UserConfigExport<'webpack5'>
|
||||
42
dict/taro/config/env.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// 环境变量配置
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '邕递+',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export function getEnvConfig() {
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
if (env === 'production') {
|
||||
return ENV_CONFIG.production
|
||||
} else { // @ts-ignore
|
||||
if (env === 'test') {
|
||||
return ENV_CONFIG.test
|
||||
} else {
|
||||
return ENV_CONFIG.development
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出环境变量
|
||||
export const {
|
||||
API_BASE_URL,
|
||||
APP_NAME,
|
||||
DEBUG
|
||||
} = getEnvConfig()
|
||||
113
dict/taro/config/index.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
|
||||
import devConfig from './dev'
|
||||
import prodConfig from './prod'
|
||||
|
||||
// import vitePluginImp from 'vite-plugin-imp'
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
export default defineConfig<'webpack5'>(async (merge, {}) => {
|
||||
const baseConfig: UserConfigExport<'webpack5'> = {
|
||||
projectName: 'websoft-react',
|
||||
date: '2024-12-30',
|
||||
plugins: ['@tarojs/plugin-html'],
|
||||
designWidth (input:any) {
|
||||
// 配置 NutUI 375 尺寸
|
||||
if (input?.file?.replace(/\\+/g, '/').indexOf('@nutui') > -1) {
|
||||
return 375
|
||||
}
|
||||
// 全局使用 Taro 默认的 750 尺寸
|
||||
return 750
|
||||
},
|
||||
deviceRatio: {
|
||||
640: 2.34 / 2,
|
||||
750: 1,
|
||||
828: 1.81 / 2,
|
||||
375: 2 / 1
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
defineConstants: {
|
||||
},
|
||||
copy: {
|
||||
patterns: [
|
||||
],
|
||||
options: {
|
||||
}
|
||||
},
|
||||
framework: 'react',
|
||||
compiler: {
|
||||
|
||||
type: 'webpack5',
|
||||
prebundle: {
|
||||
exclude: ['@nutui/nutui-react-taro', '@nutui/icons-react-taro'],
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
enable: false // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
|
||||
},
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: {
|
||||
enable: true,
|
||||
config: {
|
||||
selectorBlackList: ['nut-']
|
||||
}
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
||||
}
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
output: {
|
||||
filename: 'js/[name].[hash:8].js',
|
||||
chunkFilename: 'js/[name].[chunkhash:8].js'
|
||||
},
|
||||
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]'
|
||||
}
|
||||
}
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
|
||||
}
|
||||
},
|
||||
rn: {
|
||||
appName: 'taroDemo',
|
||||
postcss: {
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 本地开发构建配置(不混淆压缩)
|
||||
return merge({}, baseConfig, devConfig)
|
||||
}
|
||||
// 生产构建配置(默认开启压缩混淆等)
|
||||
return merge({}, baseConfig, prodConfig)
|
||||
})
|
||||
36
dict/taro/config/prod.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli";
|
||||
export default {
|
||||
mini: {
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true
|
||||
}
|
||||
},
|
||||
h5: {
|
||||
/**
|
||||
* 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<'webpack5'>
|
||||
97
dict/taro/package.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "template-10519",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "WebSoft Inc.",
|
||||
"templateInfo": {
|
||||
"name": "react-NutUI",
|
||||
"typescript": true,
|
||||
"css": "Sass",
|
||||
"framework": "React"
|
||||
},
|
||||
"scripts": {
|
||||
"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:quickapp": "taro build --type quickapp",
|
||||
"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:quickapp": "npm run build:quickapp -- --watch",
|
||||
"build:tailwind": "postcss --config tailwind.config.js -o ./dist/index.css ./src/app.css"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 versions",
|
||||
"Android >= 4.1",
|
||||
"ios >= 8"
|
||||
],
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@nutui/icons-react-taro": "^2.0.1",
|
||||
"@nutui/nutui-react-taro": "^2.7.4",
|
||||
"@tarojs/components": "4.0.8",
|
||||
"@tarojs/helper": "4.0.8",
|
||||
"@tarojs/plugin-framework-react": "4.0.8",
|
||||
"@tarojs/plugin-html": "4.0.8",
|
||||
"@tarojs/plugin-platform-alipay": "4.0.8",
|
||||
"@tarojs/plugin-platform-h5": "4.0.8",
|
||||
"@tarojs/plugin-platform-jd": "4.0.8",
|
||||
"@tarojs/plugin-platform-qq": "4.0.8",
|
||||
"@tarojs/plugin-platform-swan": "4.0.8",
|
||||
"@tarojs/plugin-platform-tt": "4.0.8",
|
||||
"@tarojs/plugin-platform-weapp": "4.0.8",
|
||||
"@tarojs/react": "4.0.8",
|
||||
"@tarojs/runtime": "4.0.8",
|
||||
"@tarojs/shared": "4.0.8",
|
||||
"@tarojs/taro": "4.0.8",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts-taro3-react": "^1.0.13",
|
||||
"js-base64": "^3.7.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.14.5",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@tarojs/cli": "4.0.8",
|
||||
"@tarojs/taro-loader": "4.0.8",
|
||||
"@tarojs/webpack5-runner": "4.0.8",
|
||||
"@types/node": "^18.19.68",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-import": "^1.13.8",
|
||||
"babel-preset-taro": "4.0.8",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-taro": "4.0.8",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"postcss": "^8.4.49",
|
||||
"react-refresh": "^0.11.0",
|
||||
"stylelint": "^14.16.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.2.0",
|
||||
"typescript": "^5.7.2",
|
||||
"webpack": "5.78.0"
|
||||
}
|
||||
}
|
||||
14161
dict/taro/pnpm-lock.yaml
generated
Normal file
6
dict/taro/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
25
dict/taro/project.config.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "websoft",
|
||||
"description": "网宿软件",
|
||||
"appid": "wxd2723d1afd9c4553",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
"enhance": false,
|
||||
"compileHotReLoad": false,
|
||||
"postcss": false,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": false,
|
||||
"newFeature": true,
|
||||
"autoAudits": false,
|
||||
"coverView": true,
|
||||
"showShadowRootInWxmlPanel": false,
|
||||
"scopeDataCheck": false,
|
||||
"useCompilerModule": false
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {}
|
||||
}
|
||||
13
dict/taro/project.tt.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "bszx-react",
|
||||
"description": "百色中学小程序",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
"postcss": false,
|
||||
"minified": false
|
||||
},
|
||||
"compileType": "miniprogram"
|
||||
}
|
||||
62
dict/taro/src/api/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 接口统一返回结果
|
||||
*/
|
||||
export interface ApiResult<T> {
|
||||
// 状态码
|
||||
code: number;
|
||||
// 状态信息
|
||||
message?: string;
|
||||
// 返回数据
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询统一结果
|
||||
*/
|
||||
export interface PageResult<T> {
|
||||
// 返回数据
|
||||
list: T[];
|
||||
// 总数量
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询基本参数
|
||||
*/
|
||||
export interface PageParam {
|
||||
// 第几页
|
||||
page?: number;
|
||||
// 每页多少条
|
||||
limit?: number;
|
||||
// 排序字段
|
||||
sort?: string;
|
||||
sortNum?: string;
|
||||
// 排序方式, asc升序, desc降序
|
||||
order?: string;
|
||||
// 租户ID
|
||||
tenantId?: number;
|
||||
// 企业ID
|
||||
companyId?: number;
|
||||
// 商户ID
|
||||
merchantId?: number;
|
||||
merchantName?: string;
|
||||
categoryIds?: any;
|
||||
// 商品分类
|
||||
categoryId?: number;
|
||||
categoryName?: string;
|
||||
// 搜素关键词
|
||||
keywords?: string;
|
||||
// 起始时间
|
||||
createTimeStart?: string;
|
||||
// 结束时间
|
||||
createTimeEnd?: string;
|
||||
timeStart?: number;
|
||||
timeEnd?: number;
|
||||
isExpireTime?: number;
|
||||
showSoldStatus?: boolean;
|
||||
dateTime?: string;
|
||||
lang?: string;
|
||||
model?: string;
|
||||
type?: string;
|
||||
BaseUrl?: string;
|
||||
}
|
||||
129
dict/taro/src/app.config.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/order/order',
|
||||
'pages/kefu/kefu',
|
||||
'pages/user/user',
|
||||
'pages/article/article',
|
||||
'pages/study/study'
|
||||
],
|
||||
"subpackages": [
|
||||
{
|
||||
"root": "passport",
|
||||
"pages": [
|
||||
"wxLogin",
|
||||
"login",
|
||||
"register",
|
||||
"forget",
|
||||
"setting",
|
||||
"agreement",
|
||||
"sms-login"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "cms",
|
||||
"pages": [
|
||||
"about",
|
||||
"article",
|
||||
"detail",
|
||||
"help"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "user",
|
||||
"pages": [
|
||||
"car/index",
|
||||
"company/company",
|
||||
"profile/profile",
|
||||
"setting/setting",
|
||||
"userVerify/index",
|
||||
"userVerify/admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "hjm",
|
||||
"pages": [
|
||||
"list",
|
||||
"location",
|
||||
"query",
|
||||
"fence",
|
||||
"practice/practice",
|
||||
"exam/exam",
|
||||
"bx/bx",
|
||||
"bx/bx-add",
|
||||
"violation/add",
|
||||
"violation/list",
|
||||
"violation/detail",
|
||||
"trajectory/trajectory",
|
||||
"gps-log/gps-log"
|
||||
// "bx/bx-list",
|
||||
// "question/detail"
|
||||
]
|
||||
}
|
||||
// {
|
||||
// "root": "shop",
|
||||
// "pages": [
|
||||
// "bm",
|
||||
// "bm/detail",
|
||||
// "item",
|
||||
// "pdf",
|
||||
// "flash",
|
||||
// "bm-log/bm-log",
|
||||
// 'bm-cert/bm-cert',
|
||||
// "pay/pay",
|
||||
// "pay/detail",
|
||||
// 'pay-log/pay-log',
|
||||
// 'pay-record/pay-record',
|
||||
// 'pay-cert/pay-cert',
|
||||
// 'cert-query/cert-query'
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'dark',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTextStyle: 'black'
|
||||
},
|
||||
tabBar: {
|
||||
custom: false,
|
||||
color: "#8a8a8a",
|
||||
selectedColor: "#9a23d4",
|
||||
backgroundColor: "#ffffff",
|
||||
list: [
|
||||
{
|
||||
pagePath: "pages/index/index",
|
||||
iconPath: "assets/tabbar/home.png",
|
||||
selectedIconPath: "assets/tabbar/home-active.png",
|
||||
text: "首页",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/study/study",
|
||||
iconPath: "assets/tabbar/order.png",
|
||||
selectedIconPath: "assets/tabbar/order-active.png",
|
||||
text: "学习",
|
||||
},
|
||||
// {
|
||||
// pagePath: "pages/kefu/kefu",
|
||||
// iconPath: "assets/tabbar/kefu.png",
|
||||
// selectedIconPath: "assets/tabbar/kefu-active.png",
|
||||
// text: "客服",
|
||||
// },
|
||||
{
|
||||
pagePath: "pages/user/user",
|
||||
iconPath: "assets/tabbar/user.png",
|
||||
selectedIconPath: "assets/tabbar/user-active.png",
|
||||
text: "我的",
|
||||
},
|
||||
],
|
||||
},
|
||||
requiredPrivateInfos: [
|
||||
"getLocation",
|
||||
"chooseLocation"
|
||||
],
|
||||
permission: {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
}
|
||||
}
|
||||
})
|
||||
36
dict/taro/src/app.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
/* ./src/index.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
page{
|
||||
background-color: #f5f5f5;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
background-position: bottom;
|
||||
}
|
||||
|
||||
// 在全局样式文件中添加
|
||||
button {
|
||||
&::after {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 微信授权按钮的特殊样式
|
||||
button[open-type="getPhoneNumber"] {
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
line-height: inherit !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
button[open-type="chooseAvatar"] {
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
line-height: inherit !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
49
dict/taro/src/app.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {useEffect} from 'react'
|
||||
import Taro, {useDidShow, useDidHide} from '@tarojs/taro'
|
||||
|
||||
// 全局样式
|
||||
import './app.scss'
|
||||
import {loginByOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/utils/config";
|
||||
// import {saveStorageByLoginUser} from "@/utils/server";
|
||||
// import {mqttStart} from "@/api/hjm/hjmCar";
|
||||
|
||||
function App(props) {
|
||||
const reload = () => {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
loginByOpenId({
|
||||
code: res.code,
|
||||
tenantId: TenantId
|
||||
}).then(data => {
|
||||
if (data) {
|
||||
// saveStorageByLoginUser(data.access_token, data.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
// 可以使用所有的 React Hooks
|
||||
useEffect(() => {
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 对应 onShow
|
||||
useDidShow(() => {
|
||||
})
|
||||
|
||||
// 对应 onHide
|
||||
useDidHide(() => {
|
||||
})
|
||||
|
||||
return props.children
|
||||
}
|
||||
|
||||
export default App
|
||||
BIN
dict/taro/src/assets/tabbar/home-active.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
dict/taro/src/assets/tabbar/home.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
dict/taro/src/assets/tabbar/kefu-active.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
dict/taro/src/assets/tabbar/kefu.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
dict/taro/src/assets/tabbar/order-active.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
dict/taro/src/assets/tabbar/order.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
dict/taro/src/assets/tabbar/shop-active.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
dict/taro/src/assets/tabbar/shop.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
dict/taro/src/assets/tabbar/store-active.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
dict/taro/src/assets/tabbar/store.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
dict/taro/src/assets/tabbar/user-active.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
dict/taro/src/assets/tabbar/user.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
70
dict/taro/src/components/AddCartBar.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {Headphones, Share} from '@nutui/icons-react-taro'
|
||||
import navTo from "@/utils/common";
|
||||
import Taro, { getCurrentInstance } from '@tarojs/taro';
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import {useEffect, useState} from "react";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
|
||||
function AddCartBar() {
|
||||
const { router } = getCurrentInstance();
|
||||
const [id, setId] = useState<number>()
|
||||
const [article, setArticle] = useState<CmsArticle>()
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const onPay = () => {
|
||||
if (!IsLogin) {
|
||||
Taro.showToast({title: `请先登录`, icon: 'error'})
|
||||
setTimeout(() => {
|
||||
Taro.switchTab(
|
||||
{
|
||||
url: '/pages/user/user',
|
||||
},
|
||||
)
|
||||
}, 1000)
|
||||
return false;
|
||||
}
|
||||
if (article?.model == 'bm') {
|
||||
navTo('/bszx/bm/bm?id=' + id)
|
||||
}
|
||||
if (article?.model == 'pay') {
|
||||
navTo('/bszx/pay/pay?id=' + id)
|
||||
}
|
||||
}
|
||||
const reload = (id) => {
|
||||
getCmsArticle(id).then(data => {
|
||||
setArticle(data)
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const id = router?.params.id as number | undefined;
|
||||
setId(id)
|
||||
reload(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'flex justify-between items-center w-full fixed bottom-0 bg-gray-100 pb-5'}>
|
||||
<div className={'btn flex px-5 items-center gap-4'}>
|
||||
<button className={'item px-4 py-1 bg-white flex items-center gap-2 text-nowrap whitespace-nowrap'} open-type="contact">
|
||||
<Headphones size={16}/>咨询
|
||||
</button>
|
||||
<button className={'item px-4 py-1 bg-white flex items-center gap-2 text-nowrap whitespace-nowrap'} open-type="share"><Share
|
||||
size={16}/>分享
|
||||
</button>
|
||||
</div>
|
||||
<div className={'bg-red-500 py-3 px-10 text-white'} style={{ whiteSpace: 'nowrap'}}
|
||||
onClick={onPay}>{article?.model == 'pay' ? '我要捐款' : '我要报名'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 监听页面分享事件
|
||||
export default AddCartBar
|
||||
6
dict/taro/src/components/Gap.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
function MyGap({height}){
|
||||
return (
|
||||
<div style={{height}} className={'bg-gray-100'}></div>
|
||||
)
|
||||
}
|
||||
export default MyGap;
|
||||
162
dict/taro/src/components/GoodsList.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import {Avatar, Cell, Space, Tabs, Button, TabPane, Swiper} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState, CSSProperties, useRef} from "react";
|
||||
import {BszxPay} from "@/api/bszx/bszxPay/model";
|
||||
import {InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import dayjs from "dayjs";
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
import {copyText} from "@/utils/common";
|
||||
|
||||
const InfiniteUlStyle: CSSProperties = {
|
||||
marginTop: '84px',
|
||||
height: '82vh',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}
|
||||
const tabs = [
|
||||
{
|
||||
index: 0,
|
||||
key: '全部',
|
||||
title: '全部'
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
key: '已上架',
|
||||
title: '已上架'
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
key: '已下架',
|
||||
title: '已下架'
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
key: '已售罄',
|
||||
title: '已售罄'
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
key: '警戒库存',
|
||||
title: '警戒库存'
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
key: '回收站',
|
||||
title: '回收站'
|
||||
},
|
||||
]
|
||||
|
||||
function GoodsList(props: any) {
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const swiperRef = useRef<React.ElementRef<typeof Swiper> | null>(null)
|
||||
const [tabIndex, setTabIndex] = useState<string | number>(0)
|
||||
|
||||
console.log(props.statusBarHeight, 'ppp')
|
||||
const reload = async () => {
|
||||
pageShopOrder({page}).then(res => {
|
||||
let newList: BszxPay[] | undefined = []
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
newList = list?.concat(res.list)
|
||||
setHasMore(true)
|
||||
} else {
|
||||
newList = res?.list
|
||||
setHasMore(false)
|
||||
}
|
||||
setList(newList || []);
|
||||
})
|
||||
}
|
||||
|
||||
const reloadMore = async () => {
|
||||
setPage(page + 1)
|
||||
reload();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setPage(2)
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
align={'left'}
|
||||
className={'fixed left-0'}
|
||||
style={{ top: '84px'}}
|
||||
value={tabIndex}
|
||||
onChange={(page) => {
|
||||
swiperRef.current?.to(page)
|
||||
setTabIndex(page)
|
||||
}}
|
||||
>
|
||||
{
|
||||
tabs?.map((item, index) => {
|
||||
return <TabPane key={index} title={item.title}></TabPane>
|
||||
})
|
||||
}
|
||||
</Tabs>
|
||||
<div style={InfiniteUlStyle} id="scroll">
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
<>
|
||||
没有更多了
|
||||
</>
|
||||
}
|
||||
>
|
||||
{list?.map(item => {
|
||||
return (
|
||||
<Cell style={{padding: '16px'}}>
|
||||
<Space direction={'vertical'} className={'w-full flex flex-col'}>
|
||||
<div className={'order-no flex justify-between'}>
|
||||
<span className={'text-gray-700 font-bold text-sm'}
|
||||
onClick={() => copyText(`${item.orderNo}`)}>{item.orderNo}</span>
|
||||
<span className={'text-orange-500'}>待付款</span>
|
||||
</div>
|
||||
<div
|
||||
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
|
||||
<div className={'goods-info'}>
|
||||
<div className={'flex items-center'}>
|
||||
<div className={'flex items-center'}>
|
||||
<Avatar
|
||||
src='34'
|
||||
size={'45'}
|
||||
shape={'square'}
|
||||
/>
|
||||
<div className={'ml-2'}>{item.realName}</div>
|
||||
</div>
|
||||
<div className={'text-gray-400 text-xs'}>{item.totalNum}件商品</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={' w-full text-right'}>实付金额:¥{item.payPrice}</div>
|
||||
<Space className={'btn flex justify-end'}>
|
||||
<Button size={'small'}>取消订单</Button>
|
||||
<Button size={'small'}>发货</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GoodsList
|
||||
31
dict/taro/src/components/Header.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import {NavBar} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
function Header(props) {
|
||||
return (
|
||||
<>
|
||||
<NavBar
|
||||
style={{
|
||||
background: 'url(https://oss.wsdns.cn/20250413/defb52abb1414429930ae2727d2b8ff6.png)',
|
||||
backgroundSize: 'cover',
|
||||
color: '#fff',
|
||||
}}
|
||||
onBackClick={() => {
|
||||
}}
|
||||
back={
|
||||
<>
|
||||
<div className={'flex items-center'} onClick={() => Taro.navigateBack()}>
|
||||
<ArrowLeft size={14}/>
|
||||
返回
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span className={'text-white'}>{props?.title || '标题'}</span>
|
||||
</NavBar>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header;
|
||||
170
dict/taro/src/components/OrderList.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import {Avatar, Cell, Space, Tabs, Button, TabPane} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState, CSSProperties} from "react";
|
||||
import {BszxPay} from "@/api/bszx/bszxPay/model";
|
||||
import {InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import dayjs from "dayjs";
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||
import {copyText} from "@/utils/common";
|
||||
|
||||
const InfiniteUlStyle: CSSProperties = {
|
||||
marginTop: '84px',
|
||||
height: '82vh',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}
|
||||
const tabs = [
|
||||
{
|
||||
index: 0,
|
||||
key: '全部',
|
||||
title: '全部'
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
key: '待发货',
|
||||
title: '待发货'
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
key: '待发货',
|
||||
title: '待发货'
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
key: '待核销',
|
||||
title: '待核销'
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
key: '已收货',
|
||||
title: '已收货'
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
key: '已完成',
|
||||
title: '已完成'
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
key: '已退款',
|
||||
title: '已退款'
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
key: '已删除',
|
||||
title: '已删除'
|
||||
}
|
||||
]
|
||||
|
||||
function OrderList(props: any) {
|
||||
const [list, setList] = useState<ShopOrder[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [tapIndex, setTapIndex] = useState<string | number>('0')
|
||||
|
||||
console.log(props.statusBarHeight, 'ppp')
|
||||
const reload = async () => {
|
||||
pageShopOrder({page}).then(res => {
|
||||
let newList: BszxPay[] | undefined = []
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
newList = list?.concat(res.list)
|
||||
setHasMore(true)
|
||||
} else {
|
||||
newList = res?.list
|
||||
setHasMore(false)
|
||||
}
|
||||
setList(newList || []);
|
||||
})
|
||||
}
|
||||
|
||||
const reloadMore = async () => {
|
||||
setPage(page + 1)
|
||||
reload();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setPage(2)
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
align={'left'}
|
||||
className={'fixed left-0'}
|
||||
style={{ top: '84px'}}
|
||||
value={tapIndex}
|
||||
onChange={(paneKey) => {
|
||||
setTapIndex(paneKey)
|
||||
}}
|
||||
>
|
||||
{
|
||||
tabs?.map((item, index) => {
|
||||
return <TabPane key={index} title={item.title}></TabPane>
|
||||
})
|
||||
}
|
||||
</Tabs>
|
||||
<div style={InfiniteUlStyle} id="scroll">
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
<>
|
||||
没有更多了
|
||||
</>
|
||||
}
|
||||
>
|
||||
{list?.map(item => {
|
||||
return (
|
||||
<Cell style={{padding: '16px'}}>
|
||||
<Space direction={'vertical'} className={'w-full flex flex-col'}>
|
||||
<div className={'order-no flex justify-between'}>
|
||||
<span className={'text-gray-700 font-bold text-sm'}
|
||||
onClick={() => copyText(`${item.orderNo}`)}>{item.orderNo}</span>
|
||||
<span className={'text-orange-500'}>待付款</span>
|
||||
</div>
|
||||
<div
|
||||
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
|
||||
<div className={'goods-info'}>
|
||||
<div className={'flex items-center'}>
|
||||
<div className={'flex items-center'}>
|
||||
<Avatar
|
||||
src='34'
|
||||
size={'45'}
|
||||
shape={'square'}
|
||||
/>
|
||||
<div className={'ml-2'}>{item.realName}</div>
|
||||
</div>
|
||||
<div className={'text-gray-400 text-xs'}>{item.totalNum}件商品</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={' w-full text-right'}>实付金额:¥{item.payPrice}</div>
|
||||
<Space className={'btn flex justify-end'}>
|
||||
<Button size={'small'}>取消订单</Button>
|
||||
<Button size={'small'}>发货</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderList
|
||||
118
dict/taro/src/components/PayRecord.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import {Avatar, Cell, Space} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState, CSSProperties} from "react";
|
||||
import {BszxPay} from "@/api/bszx/bszxPay/model";
|
||||
import {getCount, pageBszxPay} from "@/api/bszx/bszxPay";
|
||||
import {InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const InfiniteUlStyle: CSSProperties = {
|
||||
height: '70vh',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}
|
||||
function PayRecord() {
|
||||
const [list, setList] = useState<BszxPay[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [totalMoney, setTotalMoney] = useState()
|
||||
const [numbers, setNumbers] = useState()
|
||||
const reload = async () => {
|
||||
pageBszxPay({page}).then(res => {
|
||||
let newList: BszxPay[] | undefined = []
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
newList = list?.concat(res.list)
|
||||
setHasMore(true)
|
||||
} else {
|
||||
newList = res?.list
|
||||
setHasMore(false)
|
||||
}
|
||||
setList(newList || []);
|
||||
})
|
||||
getCount().then(res => {
|
||||
setNumbers(res.numbers);
|
||||
setTotalMoney(res.totalMoney);
|
||||
})
|
||||
}
|
||||
|
||||
const reloadMore = async () => {
|
||||
setPage(page + 1)
|
||||
reload();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setPage(2)
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'px-2'}>
|
||||
<Cell>
|
||||
<div className={'flex w-full text-center justify-around'}>
|
||||
<div className={'item py-1'}>
|
||||
<span className={'text-gray-400'}>已筹资金(元)</span>
|
||||
<span className={'text-xl py-1 font-bold'}>¥{totalMoney}元</span>
|
||||
</div>
|
||||
<div className={'item py-1'}>
|
||||
<span className={'text-gray-400'}>爱心人次</span>
|
||||
<span className={'text-xl py-1 font-bold'}>{numbers}次</span>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<ul style={InfiniteUlStyle} id="scroll">
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
console.log('onScroll')
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
console.log('onScrollToUpper')
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
<>
|
||||
没有更多了
|
||||
</>
|
||||
}
|
||||
>
|
||||
{list?.map(item => {
|
||||
return (
|
||||
<Cell style={{padding: '0'}}>
|
||||
<div className={'flex w-full justify-between items-center'}>
|
||||
<div className={'flex'}>
|
||||
<Space>
|
||||
<Avatar
|
||||
src={item.avatar}
|
||||
/>
|
||||
<div className={'flex flex-col'}>
|
||||
<div className={'real-name text-lg'}>
|
||||
{item.name || '匿名'}
|
||||
</div>
|
||||
<div style={{maxWidth: '240px'}} className={'text-gray-400'}>{item.formName},{dayjs(item.createTime).format('YYYY-MM-DD HH:mm')}</div>
|
||||
<div className={'text-green-600 my-1'}>心愿:{item.comments}</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
<div className={'price text-red-500 text-xl font-bold'}>
|
||||
¥{item.price}
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
</ul>
|
||||
</Cell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PayRecord
|
||||
42
dict/taro/src/components/Questions.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {pageHjmQuestions} from "@/api/hjm/hjmQuestions";
|
||||
import {HjmQuestions} from "@/api/hjm/hjmQuestions/model";
|
||||
|
||||
/**
|
||||
* 文章终极列表
|
||||
* @constructor
|
||||
*/
|
||||
const Questions = () => {
|
||||
const [list, setList] = useState<HjmQuestions[]>([])
|
||||
|
||||
const reload = () => {
|
||||
pageHjmQuestions({}).then(res => {
|
||||
if (res?.list) {
|
||||
setList(res?.list)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'px-3 mb-10'}>
|
||||
<div className={'flex flex-col justify-between items-center bg-white rounded-lg p-4'}>
|
||||
<div className={'bg-white w-full'}>
|
||||
{
|
||||
list.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={'flex justify-between items-center py-2'}>
|
||||
<div className={'text-sm'}>{item.question}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Questions
|
||||
28
dict/taro/src/components/TabBar.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Tabbar } from '@nutui/nutui-react-taro'
|
||||
import { Home, User } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
function TabBar(){
|
||||
return (
|
||||
<Tabbar
|
||||
fixed
|
||||
onSwitch={(index) => {
|
||||
console.log(index)
|
||||
if(index == 0){
|
||||
Taro.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
// if(index == 1){
|
||||
// Taro.navigateTo({ url: '/pages/detail/detail' })
|
||||
// }
|
||||
if(index == 1){
|
||||
Taro.switchTab({ url: '/pages/user/user' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tabbar.Item title="首页" icon={<Home size={18} />} />
|
||||
{/*<Tabbar.Item title="分类" icon={<Date size={18} />} />*/}
|
||||
<Tabbar.Item title="我的" icon={<User size={18} />} />
|
||||
</Tabbar>
|
||||
)
|
||||
}
|
||||
export default TabBar;
|
||||
126
dict/taro/src/components/UnifiedQRButton.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@nutui/nutui-react-taro';
|
||||
import { View } from '@tarojs/components';
|
||||
import { Scan } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
|
||||
export interface UnifiedQRButtonProps {
|
||||
/** 按钮类型 */
|
||||
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
|
||||
/** 按钮大小 */
|
||||
size?: 'large' | 'normal' | 'small';
|
||||
/** 按钮文本 */
|
||||
text?: string;
|
||||
/** 是否显示图标 */
|
||||
showIcon?: boolean;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 扫码成功回调 */
|
||||
onSuccess?: (result: UnifiedScanResult) => void;
|
||||
/** 扫码失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 是否使用页面模式(跳转到专门页面) */
|
||||
usePageMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码按钮组件
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
*/
|
||||
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
type = 'default',
|
||||
size = 'small',
|
||||
text = '扫码',
|
||||
showIcon = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
usePageMode = false
|
||||
}) => {
|
||||
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
|
||||
console.log(result,'useUnifiedQRScan>>result')
|
||||
// 处理点击事件
|
||||
const handleClick = async () => {
|
||||
if (usePageMode) {
|
||||
// 跳转到专门的统一扫码页面
|
||||
if (canScan()) {
|
||||
Taro.navigateTo({
|
||||
url: '/passport/unified-qr/index'
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接执行扫码
|
||||
try {
|
||||
const scanResult = await startScan();
|
||||
if (scanResult) {
|
||||
onSuccess?.(scanResult);
|
||||
|
||||
// 根据扫码类型给出不同的后续提示
|
||||
if (scanResult.type === ScanType.VERIFICATION) {
|
||||
// 核销成功后可以继续扫码
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || '扫码失败');
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = !canScan() || isLoading;
|
||||
|
||||
// 根据当前状态动态显示文本
|
||||
const getButtonText = () => {
|
||||
if (isLoading) {
|
||||
switch (state) {
|
||||
case 'scanning':
|
||||
return '扫码中...';
|
||||
case 'processing':
|
||||
return '处理中...';
|
||||
default:
|
||||
return '扫码中...';
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled && !canScan()) {
|
||||
return '请先登录';
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
size={size}
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<View className="flex items-center justify-center">
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{getButtonText()}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedQRButton;
|
||||
66
dict/taro/src/hooks/useAdminMode.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
/**
|
||||
* 管理员模式Hook
|
||||
* 用于管理管理员用户的模式切换(普通用户模式 vs 管理员模式)
|
||||
*/
|
||||
export function useAdminMode() {
|
||||
const [isAdminMode, setIsAdminMode] = useState<boolean>(false);
|
||||
|
||||
// 从本地存储加载管理员模式状态
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedMode = Taro.getStorageSync('admin_mode');
|
||||
if (savedMode !== undefined) {
|
||||
setIsAdminMode(savedMode === 'true' || savedMode === true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load admin mode from storage:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 切换管理员模式
|
||||
const toggleAdminMode = useCallback(() => {
|
||||
const newMode = !isAdminMode;
|
||||
setIsAdminMode(newMode);
|
||||
|
||||
try {
|
||||
// 保存到本地存储
|
||||
Taro.setStorageSync('admin_mode', newMode);
|
||||
|
||||
// 显示切换提示
|
||||
Taro.showToast({
|
||||
title: newMode ? '已切换到管理员模式' : '已切换到普通用户模式',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save admin mode to storage:', error);
|
||||
}
|
||||
}, [isAdminMode]);
|
||||
|
||||
// 设置管理员模式
|
||||
const setAdminMode = useCallback((mode: boolean) => {
|
||||
if (mode !== isAdminMode) {
|
||||
setIsAdminMode(mode);
|
||||
try {
|
||||
Taro.setStorageSync('admin_mode', mode);
|
||||
} catch (error) {
|
||||
console.error('Failed to save admin mode to storage:', error);
|
||||
}
|
||||
}
|
||||
}, [isAdminMode]);
|
||||
|
||||
// 重置为普通用户模式
|
||||
const resetToUserMode = useCallback(() => {
|
||||
setAdminMode(false);
|
||||
}, [setAdminMode]);
|
||||
|
||||
return {
|
||||
isAdminMode,
|
||||
toggleAdminMode,
|
||||
setAdminMode,
|
||||
resetToUserMode
|
||||
};
|
||||
}
|
||||
161
dict/taro/src/hooks/useCart.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
// 购物车商品接口
|
||||
export interface CartItem {
|
||||
goodsId: number;
|
||||
name: string;
|
||||
price: string;
|
||||
image: string;
|
||||
quantity: number;
|
||||
addTime: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}
|
||||
|
||||
// 购物车Hook
|
||||
export const useCart = () => {
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||
const [cartCount, setCartCount] = useState(0);
|
||||
|
||||
// 从本地存储加载购物车数据
|
||||
const loadCartFromStorage = () => {
|
||||
try {
|
||||
const cartData = Taro.getStorageSync('cart_items');
|
||||
if (cartData) {
|
||||
const items = JSON.parse(cartData) as CartItem[];
|
||||
setCartItems(items);
|
||||
updateCartCount(items);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载购物车数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存购物车数据到本地存储
|
||||
const saveCartToStorage = (items: CartItem[]) => {
|
||||
try {
|
||||
Taro.setStorageSync('cart_items', JSON.stringify(items));
|
||||
} catch (error) {
|
||||
console.error('保存购物车数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新购物车数量
|
||||
const updateCartCount = (items: CartItem[]) => {
|
||||
const count = items.reduce((total, item) => total + item.quantity, 0);
|
||||
setCartCount(count);
|
||||
};
|
||||
|
||||
// 添加商品到购物车
|
||||
const addToCart = (goods: {
|
||||
goodsId: number;
|
||||
name: string;
|
||||
price: string;
|
||||
image: string;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}, quantity: number = 1) => {
|
||||
const newItems = [...cartItems];
|
||||
// 如果有SKU,需要根据goodsId和skuId来判断是否为同一商品
|
||||
const existingItemIndex = newItems.findIndex(item =>
|
||||
item.goodsId === goods.goodsId &&
|
||||
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
|
||||
);
|
||||
|
||||
if (existingItemIndex >= 0) {
|
||||
// 如果商品已存在,增加数量
|
||||
newItems[existingItemIndex].quantity += quantity;
|
||||
} else {
|
||||
// 如果商品不存在,添加新商品
|
||||
const newItem: CartItem = {
|
||||
goodsId: goods.goodsId,
|
||||
name: goods.name,
|
||||
price: goods.price,
|
||||
image: goods.image,
|
||||
quantity,
|
||||
addTime: Date.now(),
|
||||
skuId: goods.skuId,
|
||||
specInfo: goods.specInfo
|
||||
};
|
||||
newItems.push(newItem);
|
||||
}
|
||||
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '加入购物车成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
};
|
||||
|
||||
// 从购物车移除商品
|
||||
const removeFromCart = (goodsId: number) => {
|
||||
const newItems = cartItems.filter(item => item.goodsId !== goodsId);
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
};
|
||||
|
||||
// 更新商品数量
|
||||
const updateQuantity = (goodsId: number, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(goodsId);
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = cartItems.map(item =>
|
||||
item.goodsId === goodsId ? { ...item, quantity } : item
|
||||
);
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
};
|
||||
|
||||
// 清空购物车
|
||||
const clearCart = () => {
|
||||
setCartItems([]);
|
||||
setCartCount(0);
|
||||
Taro.removeStorageSync('cart_items');
|
||||
};
|
||||
|
||||
// 获取购物车总价
|
||||
const getTotalPrice = () => {
|
||||
return cartItems.reduce((total, item) => {
|
||||
return total + (parseFloat(item.price) * item.quantity);
|
||||
}, 0).toFixed(2);
|
||||
};
|
||||
|
||||
// 检查商品是否在购物车中
|
||||
const isInCart = (goodsId: number) => {
|
||||
return cartItems.some(item => item.goodsId === goodsId);
|
||||
};
|
||||
|
||||
// 获取商品在购物车中的数量
|
||||
const getItemQuantity = (goodsId: number) => {
|
||||
const item = cartItems.find(item => item.goodsId === goodsId);
|
||||
return item ? item.quantity : 0;
|
||||
};
|
||||
|
||||
// 初始化时加载购物车数据
|
||||
useEffect(() => {
|
||||
loadCartFromStorage();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
cartItems,
|
||||
cartCount,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart,
|
||||
getTotalPrice,
|
||||
isInCart,
|
||||
getItemQuantity,
|
||||
loadCartFromStorage
|
||||
};
|
||||
};
|
||||
81
dict/taro/src/hooks/useDealerApply.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getShopDealerApply} from '@/api/shop/shopDealerApply'
|
||||
import type {ShopDealerApply} from '@/api/shop/shopDealerApply/model'
|
||||
|
||||
// Hook 返回值接口
|
||||
export interface UseDealerApplyReturn {
|
||||
// 经销商用户信息
|
||||
dealerApply: ShopDealerApply | null
|
||||
// 加载状态
|
||||
loading: boolean
|
||||
// 错误信息
|
||||
error: string | null
|
||||
// 刷新数据
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 经销商用户 Hook - 简化版本
|
||||
* 只查询经销商用户信息和判断是否存在
|
||||
*/
|
||||
export const useDealerApply = (): UseDealerApplyReturn => {
|
||||
const [dealerApply, setDealerApply] = useState<ShopDealerApply | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
setDealerApply(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerApply(userId)
|
||||
|
||||
if (dealer) {
|
||||
setDealerApply(dealer)
|
||||
} else {
|
||||
setDealerApply(null)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
setError(errorMessage)
|
||||
setDealerApply(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchDealerData()
|
||||
}, [fetchDealerData])
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
|
||||
return {
|
||||
dealerApply,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
81
dict/taro/src/hooks/useDealerUser.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
|
||||
|
||||
// Hook 返回值接口
|
||||
export interface UseDealerUserReturn {
|
||||
// 经销商用户信息
|
||||
dealerUser: ShopDealerUser | null
|
||||
// 加载状态
|
||||
loading: boolean
|
||||
// 错误信息
|
||||
error: string | null
|
||||
// 刷新数据
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 经销商用户 Hook - 简化版本
|
||||
* 只查询经销商用户信息和判断是否存在
|
||||
*/
|
||||
export const useDealerUser = (): UseDealerUserReturn => {
|
||||
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
setDealerUser(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerUser(userId)
|
||||
|
||||
if (dealer) {
|
||||
setDealerUser(dealer)
|
||||
} else {
|
||||
setDealerUser(null)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
setError(errorMessage)
|
||||
setDealerUser(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchDealerData()
|
||||
}, [fetchDealerData])
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
|
||||
return {
|
||||
dealerUser,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
120
dict/taro/src/hooks/useOrderStats.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { UserOrderStats } from '@/api/user';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
|
||||
/**
|
||||
* 订单统计Hook
|
||||
* 用于管理用户订单各状态的数量统计
|
||||
*/
|
||||
export const useOrderStats = () => {
|
||||
const [orderStats, setOrderStats] = useState<UserOrderStats>({
|
||||
pending: 0, // 待付款
|
||||
paid: 0, // 待发货
|
||||
shipped: 0, // 待收货
|
||||
completed: 0, // 已完成
|
||||
refund: 0, // 退货/售后
|
||||
total: 0 // 总订单数
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* 获取订单统计数据
|
||||
*/
|
||||
const fetchOrderStats = useCallback(async (showToast = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return false;
|
||||
}
|
||||
// TODO 读取订单数量
|
||||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
|
||||
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
|
||||
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
|
||||
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
|
||||
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
|
||||
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
|
||||
setOrderStats({
|
||||
pending: pending?.count || 0,
|
||||
paid: paid?.count || 0,
|
||||
shipped: shipped?.count || 0,
|
||||
completed: completed?.count || 0,
|
||||
refund: refund?.count || 0,
|
||||
total: total?.count || 0
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: '数据已更新',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.message || '获取订单统计失败';
|
||||
setError(errorMessage);
|
||||
|
||||
console.error('获取订单统计失败:', err);
|
||||
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 刷新订单统计数据
|
||||
*/
|
||||
const refreshOrderStats = useCallback(() => {
|
||||
return fetchOrderStats(true);
|
||||
}, [fetchOrderStats]);
|
||||
|
||||
/**
|
||||
* 获取指定状态的订单数量
|
||||
*/
|
||||
const getOrderCount = useCallback((status: keyof UserOrderStats) => {
|
||||
return orderStats[status] || 0;
|
||||
}, [orderStats]);
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的订单
|
||||
*/
|
||||
const hasPendingOrders = useCallback(() => {
|
||||
return orderStats.pending > 0 || orderStats.paid > 0 || orderStats.shipped > 0;
|
||||
}, [orderStats]);
|
||||
|
||||
/**
|
||||
* 获取总的待处理订单数量
|
||||
*/
|
||||
const getTotalPendingCount = useCallback(() => {
|
||||
return orderStats.pending + orderStats.paid + orderStats.shipped;
|
||||
}, [orderStats]);
|
||||
|
||||
// 组件挂载时自动获取数据
|
||||
useEffect(() => {
|
||||
fetchOrderStats();
|
||||
}, [fetchOrderStats]);
|
||||
|
||||
return {
|
||||
orderStats,
|
||||
loading,
|
||||
error,
|
||||
fetchOrderStats,
|
||||
refreshOrderStats,
|
||||
getOrderCount,
|
||||
hasPendingOrders,
|
||||
getTotalPendingCount
|
||||
};
|
||||
};
|
||||
|
||||
export default useOrderStats;
|
||||
163
dict/taro/src/hooks/usePaymentCountdown.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
|
||||
// 扩展dayjs支持duration
|
||||
dayjs.extend(duration);
|
||||
|
||||
export interface CountdownTime {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
isExpired: boolean;
|
||||
totalMinutes: number; // 总剩余分钟数
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付倒计时Hook
|
||||
* @param createTime 订单创建时间
|
||||
* @param payStatus 支付状态
|
||||
* @param realTime 是否实时更新(详情页用true,列表页用false)
|
||||
* @param timeoutHours 超时小时数,默认24小时
|
||||
*/
|
||||
export const usePaymentCountdown = (
|
||||
createTime?: string,
|
||||
payStatus?: boolean,
|
||||
realTime: boolean = false,
|
||||
timeoutHours: number = 24
|
||||
): CountdownTime => {
|
||||
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
});
|
||||
|
||||
// 计算剩余时间的函数
|
||||
const calculateTimeLeft = useMemo(() => {
|
||||
return (): CountdownTime => {
|
||||
if (!createTime || payStatus) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const createTimeObj = dayjs(createTime);
|
||||
const expireTime = createTimeObj.add(timeoutHours, 'hour');
|
||||
const now = dayjs();
|
||||
const diff = expireTime.diff(now);
|
||||
|
||||
if (diff <= 0) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: true,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const durationObj = dayjs.duration(diff);
|
||||
const hours = Math.floor(durationObj.asHours());
|
||||
const minutes = durationObj.minutes();
|
||||
const seconds = durationObj.seconds();
|
||||
const totalMinutes = Math.floor(durationObj.asMinutes());
|
||||
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
isExpired: false,
|
||||
totalMinutes
|
||||
};
|
||||
};
|
||||
}, [createTime, payStatus, timeoutHours]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!createTime || payStatus) {
|
||||
setTimeLeft({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即计算一次
|
||||
const initialTime = calculateTimeLeft();
|
||||
setTimeLeft(initialTime);
|
||||
|
||||
// 如果不需要实时更新,直接返回
|
||||
if (!realTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果需要实时更新,设置定时器
|
||||
const timer = setInterval(() => {
|
||||
const newTimeLeft = calculateTimeLeft();
|
||||
setTimeLeft(newTimeLeft);
|
||||
|
||||
// 如果已过期,清除定时器
|
||||
if (newTimeLeft.isExpired) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [createTime, payStatus, realTime, calculateTimeLeft]);
|
||||
|
||||
return timeLeft;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化倒计时文本
|
||||
* @param timeLeft 倒计时时间对象
|
||||
* @param showSeconds 是否显示秒数
|
||||
*/
|
||||
export const formatCountdownText = (
|
||||
timeLeft: CountdownTime,
|
||||
showSeconds: boolean = false
|
||||
): string => {
|
||||
if (timeLeft.isExpired) {
|
||||
return '已过期';
|
||||
}
|
||||
|
||||
if (timeLeft.hours > 0) {
|
||||
if (showSeconds) {
|
||||
return `${timeLeft.hours}小时${timeLeft.minutes}分${timeLeft.seconds}秒`;
|
||||
} else {
|
||||
return `${timeLeft.hours}小时${timeLeft.minutes}分钟`;
|
||||
}
|
||||
} else if (timeLeft.minutes > 0) {
|
||||
if (showSeconds) {
|
||||
return `${timeLeft.minutes}分${timeLeft.seconds}秒`;
|
||||
} else {
|
||||
return `${timeLeft.minutes}分钟`;
|
||||
}
|
||||
} else {
|
||||
return `${timeLeft.seconds}秒`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为紧急状态(剩余时间少于1小时)
|
||||
*/
|
||||
export const isUrgentCountdown = (timeLeft: CountdownTime): boolean => {
|
||||
return !timeLeft.isExpired && timeLeft.totalMinutes < 60;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为非常紧急状态(剩余时间少于10分钟)
|
||||
*/
|
||||
export const isCriticalCountdown = (timeLeft: CountdownTime): boolean => {
|
||||
return !timeLeft.isExpired && timeLeft.totalMinutes < 10;
|
||||
};
|
||||
|
||||
export default usePaymentCountdown;
|
||||
228
dict/taro/src/hooks/useQRLogin.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent,
|
||||
type ConfirmLoginResult
|
||||
} from '@/api/passport/qr-login';
|
||||
|
||||
/**
|
||||
* 扫码登录状态
|
||||
*/
|
||||
export enum ScanLoginState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
CONFIRMING = 'confirming', // 正在确认登录
|
||||
SUCCESS = 'success', // 登录成功
|
||||
ERROR = 'error' // 登录失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录Hook
|
||||
*/
|
||||
export function useQRLogin() {
|
||||
const [state, setState] = useState<ScanLoginState>(ScanLoginState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<ConfirmLoginResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(ScanLoginState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 开始扫码登录
|
||||
*/
|
||||
const startScan = useCallback(async () => {
|
||||
try {
|
||||
reset();
|
||||
setState(ScanLoginState.SCANNING);
|
||||
|
||||
// 检查用户是否已登录
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
// 调用扫码API
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(scanResult);
|
||||
console.log('解析二维码内容2:',token)
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
// 确认登录
|
||||
setState(ScanLoginState.CONFIRMING);
|
||||
setIsLoading(true);
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
console.log(confirmResult,'confirmResult>>>>')
|
||||
if (cancelRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmResult.success) {
|
||||
setState(ScanLoginState.SUCCESS);
|
||||
setResult(confirmResult);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(ScanLoginState.ERROR);
|
||||
const errorMessage = err.message || '扫码登录失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 取消扫码登录
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 处理扫码结果(用于已有扫码结果的情况)
|
||||
*/
|
||||
const handleScanResult = useCallback(async (qrContent: string) => {
|
||||
try {
|
||||
reset();
|
||||
setState(ScanLoginState.CONFIRMING);
|
||||
setIsLoading(true);
|
||||
|
||||
// 检查用户是否已登录
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(qrContent);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
// 确认登录
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
|
||||
if (confirmResult.success) {
|
||||
setState(ScanLoginState.SUCCESS);
|
||||
setResult(confirmResult);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
setState(ScanLoginState.ERROR);
|
||||
const errorMessage = err.message || '登录确认失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码登录
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
handleScanResult,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === ScanLoginState.IDLE,
|
||||
isScanning: state === ScanLoginState.SCANNING,
|
||||
isConfirming: state === ScanLoginState.CONFIRMING,
|
||||
isSuccess: state === ScanLoginState.SUCCESS,
|
||||
isError: state === ScanLoginState.ERROR
|
||||
};
|
||||
}
|
||||
323
dict/taro/src/hooks/useShopInfo.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useState, useEffect, useCallback} from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {AppInfo} from '@/api/cms/cmsWebsite/model';
|
||||
import {getShopInfo} from '@/api/layout';
|
||||
|
||||
// 本地存储键名
|
||||
const SHOP_INFO_STORAGE_KEY = 'shop_info';
|
||||
const SHOP_INFO_CACHE_TIME_KEY = 'shop_info_cache_time';
|
||||
|
||||
// 缓存有效期(毫秒)- 默认30分钟
|
||||
const CACHE_DURATION = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 商店信息Hook
|
||||
* 提供商店信息的获取、缓存和管理功能
|
||||
*/
|
||||
export const useShopInfo = () => {
|
||||
const [shopInfo, setShopInfo] = useState<AppInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 从本地存储加载商店信息
|
||||
const loadShopInfoFromStorage = useCallback(() => {
|
||||
try {
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
const cacheTime = Taro.getStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
|
||||
if (cachedData && cacheTime) {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - cacheTime;
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (timeDiff < CACHE_DURATION) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
setLoading(false);
|
||||
return true; // 返回true表示使用了缓存
|
||||
} else {
|
||||
// 缓存过期,清除旧数据
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商店信息缓存失败:', error);
|
||||
}
|
||||
return false; // 返回false表示没有使用缓存
|
||||
}, []);
|
||||
|
||||
// 保存商店信息到本地存储
|
||||
const saveShopInfoToStorage = useCallback((data: AppInfo) => {
|
||||
try {
|
||||
Taro.setStorageSync(SHOP_INFO_STORAGE_KEY, data);
|
||||
Taro.setStorageSync(SHOP_INFO_CACHE_TIME_KEY, Date.now());
|
||||
} catch (error) {
|
||||
console.error('保存商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从服务器获取商店信息
|
||||
const fetchShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('获取商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
|
||||
// 如果网络请求失败,尝试使用缓存数据(即使过期)
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
if (cachedData) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
console.warn('网络请求失败,使用缓存数据');
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 刷新商店信息
|
||||
const refreshShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('刷新商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 清除缓存
|
||||
const clearCache = useCallback(() => {
|
||||
try {
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
setShopInfo(null);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error('清除商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取应用名称
|
||||
const getAppName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站名称(兼容旧方法名)
|
||||
const getWebsiteName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用Logo
|
||||
const getAppLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站Logo(兼容旧方法名)
|
||||
const getWebsiteLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用图标
|
||||
const getAppIcon = useCallback(() => {
|
||||
return shopInfo?.icon || shopInfo?.logo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取深色模式Logo(AppInfo中无此字段,使用普通Logo)
|
||||
const getDarkLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用域名
|
||||
const getDomain = useCallback(() => {
|
||||
return shopInfo?.domain || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用描述
|
||||
const getDescription = useCallback(() => {
|
||||
return shopInfo?.description || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用关键词
|
||||
const getKeywords = useCallback(() => {
|
||||
return shopInfo?.keywords || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用标题
|
||||
const getTitle = useCallback(() => {
|
||||
return shopInfo?.title || shopInfo?.appName || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取小程序二维码
|
||||
const getMpQrCode = useCallback(() => {
|
||||
return shopInfo?.mpQrCode || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取联系电话(AppInfo中无此字段,从config中获取)
|
||||
const getPhone = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.phone || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取邮箱(AppInfo中无此字段,从config中获取)
|
||||
const getEmail = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.email || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取地址(AppInfo中无此字段,从config中获取)
|
||||
const getAddress = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.address || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取ICP备案号(AppInfo中无此字段,从config中获取)
|
||||
const getIcpNo = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.icpNo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用状态
|
||||
const getStatus = useCallback(() => {
|
||||
return {
|
||||
running: shopInfo?.running || 0,
|
||||
statusText: shopInfo?.statusText || '',
|
||||
statusIcon: shopInfo?.statusIcon || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用配置
|
||||
const getConfig = useCallback(() => {
|
||||
return shopInfo?.config || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用设置
|
||||
const getSetting = useCallback(() => {
|
||||
return shopInfo?.setting || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取服务器时间
|
||||
const getServerTime = useCallback(() => {
|
||||
return shopInfo?.serverTime || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取导航菜单
|
||||
const getNavigation = useCallback(() => {
|
||||
return {
|
||||
topNavs: shopInfo?.topNavs || [],
|
||||
bottomNavs: shopInfo?.bottomNavs || []
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否支持搜索(从config中获取)
|
||||
const isSearchEnabled = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.search === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用版本信息
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return {
|
||||
version: shopInfo?.version || 10,
|
||||
expirationTime: shopInfo?.expirationTime || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查应用是否过期
|
||||
const isExpired = useCallback(() => {
|
||||
return shopInfo?.expired === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取过期天数
|
||||
const getExpiredDays = useCallback(() => {
|
||||
return shopInfo?.expiredDays || 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否即将过期
|
||||
const isSoonExpired = useCallback(() => {
|
||||
return (shopInfo?.soon || 0) > 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 初始化时加载商店信息
|
||||
useEffect(() => {
|
||||
const initShopInfo = async () => {
|
||||
// 先尝试从缓存加载
|
||||
const hasCache = loadShopInfoFromStorage();
|
||||
|
||||
// 如果没有缓存或需要刷新,则从服务器获取
|
||||
if (!hasCache) {
|
||||
await fetchShopInfo();
|
||||
}
|
||||
};
|
||||
|
||||
initShopInfo();
|
||||
}, []); // 空依赖数组,只在组件挂载时执行一次
|
||||
|
||||
return {
|
||||
// 状态
|
||||
shopInfo,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 方法
|
||||
fetchShopInfo,
|
||||
refreshShopInfo,
|
||||
clearCache,
|
||||
|
||||
// 新的工具方法(基于AppInfo字段)
|
||||
getAppName,
|
||||
getAppLogo,
|
||||
getAppIcon,
|
||||
getDescription,
|
||||
getKeywords,
|
||||
getTitle,
|
||||
getMpQrCode,
|
||||
getDomain,
|
||||
getConfig,
|
||||
getSetting,
|
||||
getServerTime,
|
||||
getNavigation,
|
||||
getStatus,
|
||||
getVersionInfo,
|
||||
isExpired,
|
||||
getExpiredDays,
|
||||
isSoonExpired,
|
||||
|
||||
// 兼容旧方法名
|
||||
getWebsiteName,
|
||||
getWebsiteLogo,
|
||||
getDarkLogo,
|
||||
getPhone,
|
||||
getEmail,
|
||||
getAddress,
|
||||
getIcpNo,
|
||||
isSearchEnabled
|
||||
};
|
||||
};
|
||||
95
dict/taro/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
currentTheme: GradientTheme
|
||||
setTheme: (themeName: string) => void
|
||||
isAutoTheme: boolean
|
||||
refreshTheme: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题管理Hook
|
||||
* 提供主题切换和状态管理功能
|
||||
*/
|
||||
export const useTheme = (): UseThemeReturn => {
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
|
||||
|
||||
// 获取当前主题
|
||||
const getCurrentTheme = (): GradientTheme => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
|
||||
if (savedTheme === 'auto') {
|
||||
// 自动主题:根据用户ID生成
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
return gradientUtils.getThemeByUserId(userId)
|
||||
} else {
|
||||
// 手动选择的主题
|
||||
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
setIsAutoTheme(savedTheme === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
}, [])
|
||||
|
||||
// 设置主题
|
||||
const setTheme = (themeName: string) => {
|
||||
try {
|
||||
Taro.setStorageSync('user_theme', themeName)
|
||||
setIsAutoTheme(themeName === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
} catch (error) {
|
||||
console.error('保存主题失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||
const refreshTheme = () => {
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
}
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
setTheme,
|
||||
isAutoTheme,
|
||||
refreshTheme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题的样式对象
|
||||
* 用于直接应用到组件样式中
|
||||
*/
|
||||
export const useThemeStyles = () => {
|
||||
const { currentTheme } = useTheme()
|
||||
|
||||
return {
|
||||
// 主要背景样式
|
||||
primaryBackground: {
|
||||
background: currentTheme.background,
|
||||
color: currentTheme.textColor
|
||||
},
|
||||
|
||||
// 按钮样式
|
||||
primaryButton: {
|
||||
background: currentTheme.background,
|
||||
border: 'none',
|
||||
color: currentTheme.textColor
|
||||
},
|
||||
|
||||
// 强调色
|
||||
accentColor: currentTheme.primary,
|
||||
|
||||
// 文字颜色
|
||||
textColor: currentTheme.textColor,
|
||||
|
||||
// 完整主题对象
|
||||
theme: currentTheme
|
||||
}
|
||||
}
|
||||
331
dict/taro/src/hooks/useUnifiedQRScan.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 统一扫码状态
|
||||
*/
|
||||
export enum UnifiedScanState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
PROCESSING = 'processing', // 正在处理
|
||||
SUCCESS = 'success', // 处理成功
|
||||
ERROR = 'error' // 处理失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码类型
|
||||
*/
|
||||
export enum ScanType {
|
||||
LOGIN = 'login', // 登录二维码
|
||||
VERIFICATION = 'verification', // 核销二维码
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
export interface UnifiedScanResult {
|
||||
type: ScanType;
|
||||
data: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码Hook
|
||||
* 可以处理登录和核销两种类型的二维码
|
||||
*/
|
||||
export function useUnifiedQRScan() {
|
||||
const { isAdmin } = useUser();
|
||||
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<UnifiedScanResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scanType, setScanType] = useState<ScanType>(ScanType.UNKNOWN);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(UnifiedScanState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
setScanType(ScanType.UNKNOWN);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 检测二维码类型
|
||||
*/
|
||||
const detectScanType = useCallback((scanResult: string): ScanType => {
|
||||
try {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否为登录二维码
|
||||
const loginToken = parseQRContent(scanResult);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (error) {
|
||||
console.error('检测二维码类型失败:', error);
|
||||
return ScanType.UNKNOWN;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理登录二维码
|
||||
*/
|
||||
const handleLoginQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
if (confirmResult.status === 'confirmed') {
|
||||
return {
|
||||
type: ScanType.LOGIN,
|
||||
data: confirmResult,
|
||||
message: '登录成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理核销二维码
|
||||
*/
|
||||
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
if (!isAdmin()) {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
}
|
||||
|
||||
if (gift.status === 1) {
|
||||
throw new Error('此礼品码已使用');
|
||||
}
|
||||
|
||||
if (gift.status === 2) {
|
||||
throw new Error('此礼品码已失效');
|
||||
}
|
||||
|
||||
if (gift.userId === 0) {
|
||||
throw new Error('此礼品码未认领');
|
||||
}
|
||||
|
||||
// 执行核销
|
||||
await updateShopGift({
|
||||
...gift,
|
||||
status: 1,
|
||||
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
|
||||
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
/**
|
||||
* 开始扫码
|
||||
*/
|
||||
const startScan = useCallback(async (): Promise<UnifiedScanResult | null> => {
|
||||
try {
|
||||
reset();
|
||||
setState(UnifiedScanState.SCANNING);
|
||||
|
||||
// 调用扫码API
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检测二维码类型
|
||||
const type = detectScanType(scanResult);
|
||||
setScanType(type);
|
||||
|
||||
if (type === ScanType.UNKNOWN) {
|
||||
throw new Error('不支持的二维码类型');
|
||||
}
|
||||
|
||||
// 开始处理
|
||||
setState(UnifiedScanState.PROCESSING);
|
||||
setIsLoading(true);
|
||||
|
||||
let result: UnifiedScanResult;
|
||||
|
||||
switch (type) {
|
||||
case ScanType.LOGIN:
|
||||
result = await handleLoginQR(scanResult);
|
||||
break;
|
||||
case ScanType.VERIFICATION:
|
||||
result = await handleVerificationQR(scanResult);
|
||||
break;
|
||||
default:
|
||||
throw new Error('未知的扫码类型');
|
||||
}
|
||||
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(UnifiedScanState.SUCCESS);
|
||||
setResult(result);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: result.message,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(UnifiedScanState.ERROR);
|
||||
const errorMessage = err.message || '处理失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
|
||||
|
||||
/**
|
||||
* 取消扫码
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
scanType,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === UnifiedScanState.IDLE,
|
||||
isScanning: state === UnifiedScanState.SCANNING,
|
||||
isProcessing: state === UnifiedScanState.PROCESSING,
|
||||
isSuccess: state === UnifiedScanState.SUCCESS,
|
||||
isError: state === UnifiedScanState.ERROR
|
||||
};
|
||||
}
|
||||
334
dict/taro/src/hooks/useUser.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { User } from '@/api/system/user/model';
|
||||
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
||||
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
|
||||
|
||||
// 用户Hook
|
||||
export const useUser = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 自动登录(通过OpenID)
|
||||
const autoLoginByOpenId = async () => {
|
||||
try {
|
||||
const res = await new Promise<any>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (loginRes) => {
|
||||
loginByOpenId({
|
||||
code: loginRes.code,
|
||||
tenantId: 10519
|
||||
}).then(async (data) => {
|
||||
if (data) {
|
||||
// 保存登录信息
|
||||
saveUserToStorage(data.access_token, data.user);
|
||||
setUser(data.user);
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// 处理邀请关系
|
||||
if (data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(data.user.userId);
|
||||
if (inviteSuccess) {
|
||||
console.log('自动登录时邀请关系建立成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动登录时处理邀请关系失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(data.user);
|
||||
} else {
|
||||
reject(new Error('自动登录失败'));
|
||||
}
|
||||
}).catch(_ => {
|
||||
// 首次注册,跳转到邀请注册页面
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
|
||||
return Taro.navigateTo({
|
||||
url: '/dealer/apply/add'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('自动登录失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储加载用户数据
|
||||
const loadUserFromStorage = async () => {
|
||||
try {
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
const userData = Taro.getStorageSync('User');
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const tenantId = Taro.getStorageSync('TenantId');
|
||||
|
||||
if (token && userData) {
|
||||
const userInfo = typeof userData === 'string' ? JSON.parse(userData) : userData;
|
||||
setUser(userInfo);
|
||||
setIsLoggedIn(true);
|
||||
} else if (token && userId) {
|
||||
// 如果有token和userId但没有完整用户信息,标记为已登录但需要获取用户信息
|
||||
setIsLoggedIn(true);
|
||||
setUser({ userId, tenantId } as User);
|
||||
} else {
|
||||
// 没有本地登录信息,尝试自动登录
|
||||
console.log('没有本地登录信息,尝试自动登录...');
|
||||
const autoLoginResult = await autoLoginByOpenId();
|
||||
if (!autoLoginResult) {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户数据失败:', error);
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存用户数据到本地存储
|
||||
const saveUserToStorage = (token: string, userInfo: User) => {
|
||||
try {
|
||||
Taro.setStorageSync('access_token', token);
|
||||
Taro.setStorageSync('User', userInfo);
|
||||
|
||||
// 确保关键字段不为空时才保存,避免覆盖现有数据
|
||||
if (userInfo.userId) {
|
||||
Taro.setStorageSync('UserId', userInfo.userId);
|
||||
}
|
||||
if (userInfo.tenantId) {
|
||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
||||
}
|
||||
if (userInfo.phone) {
|
||||
Taro.setStorageSync('Phone', userInfo.phone);
|
||||
}
|
||||
// 保存头像和昵称信息
|
||||
if (userInfo.avatar) {
|
||||
Taro.setStorageSync('Avatar', userInfo.avatar);
|
||||
}
|
||||
if (userInfo.nickname) {
|
||||
Taro.setStorageSync('Nickname', userInfo.nickname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 登录用户
|
||||
const loginUser = (token: string, userInfo: User) => {
|
||||
setUser(userInfo);
|
||||
setIsLoggedIn(true);
|
||||
saveUserToStorage(token, userInfo);
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const logoutUser = () => {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
|
||||
// 清除本地存储
|
||||
try {
|
||||
Taro.removeStorageSync('access_token');
|
||||
Taro.removeStorageSync('User');
|
||||
Taro.removeStorageSync('UserId');
|
||||
Taro.removeStorageSync('TenantId');
|
||||
Taro.removeStorageSync('Phone');
|
||||
Taro.removeStorageSync('userInfo');
|
||||
} catch (error) {
|
||||
console.error('清除用户数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从服务器获取最新用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
if (!isLoggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
setUser(userInfo);
|
||||
|
||||
// 更新本地存储
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
if (token) {
|
||||
saveUserToStorage(token, userInfo);
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
// 如果获取失败,可能是token过期,清除登录状态
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage?.includes('401') || errorMessage?.includes('未授权')) {
|
||||
logoutUser();
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
const updateUser = async (userData: Partial<User>) => {
|
||||
if (!user) {
|
||||
throw new Error('用户未登录');
|
||||
}
|
||||
|
||||
try {
|
||||
// 先获取最新的用户信息,确保我们有完整的数据
|
||||
const latestUserInfo = await getUserInfo();
|
||||
|
||||
// 合并最新的用户信息和要更新的数据
|
||||
const updatedUser = { ...latestUserInfo, ...userData };
|
||||
|
||||
// 调用API更新用户信息
|
||||
await updateUserInfo(updatedUser);
|
||||
|
||||
// 更新本地状态
|
||||
setUser(updatedUser);
|
||||
|
||||
// 更新本地存储
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
if (token) {
|
||||
saveUserToStorage(token, updatedUser);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error);
|
||||
Taro.showToast({
|
||||
title: '更新失败',
|
||||
icon: 'error',
|
||||
duration: 1500
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否有特定权限
|
||||
const hasPermission = (permission: string) => {
|
||||
if (!user || !user.authorities) {
|
||||
return false;
|
||||
}
|
||||
return user.authorities.some(auth => auth.authority === permission);
|
||||
};
|
||||
|
||||
// 检查是否有特定角色
|
||||
const hasRole = (roleCode: string) => {
|
||||
if (!user || !user.roles) {
|
||||
return false;
|
||||
}
|
||||
return user.roles.some(role => role.roleCode === roleCode);
|
||||
};
|
||||
|
||||
// 获取用户头像URL
|
||||
const getAvatarUrl = () => {
|
||||
return user?.avatar || user?.avatarUrl || '';
|
||||
};
|
||||
|
||||
const getUserId = () => {
|
||||
return user?.userId;
|
||||
};
|
||||
|
||||
// 获取用户显示名称
|
||||
const getDisplayName = () => {
|
||||
return user?.nickname || user?.realName || user?.username || '未登录';
|
||||
};
|
||||
|
||||
// 获取用户显示的角色(同步版本)
|
||||
const getRoleName = () => {
|
||||
if(hasRole('superAdmin')){
|
||||
return '超级管理员';
|
||||
}
|
||||
if(hasRole('admin')){
|
||||
return '管理员';
|
||||
}
|
||||
if(hasRole('staff')){
|
||||
return '员工';
|
||||
}
|
||||
if(hasRole('vip')){
|
||||
return 'VIP会员';
|
||||
}
|
||||
return '注册用户';
|
||||
}
|
||||
|
||||
// 检查用户是否已实名认证
|
||||
const isCertified = () => {
|
||||
return user?.certification === true;
|
||||
};
|
||||
|
||||
// 检查用户是否是管理员
|
||||
const isAdmin = () => {
|
||||
return user?.isAdmin === true;
|
||||
};
|
||||
|
||||
const isSuperAdmin = () => {
|
||||
return user?.isSuperAdmin === true;
|
||||
};
|
||||
|
||||
// 获取用户余额
|
||||
const getBalance = () => {
|
||||
return user?.balance || 0;
|
||||
};
|
||||
|
||||
// 获取用户积分
|
||||
const getPoints = () => {
|
||||
return user?.points || 0;
|
||||
};
|
||||
|
||||
// 初始化时加载用户数据
|
||||
useEffect(() => {
|
||||
loadUserFromStorage().catch(error => {
|
||||
console.error('初始化用户数据失败:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
isLoggedIn,
|
||||
loading,
|
||||
|
||||
// 方法
|
||||
loginUser,
|
||||
logoutUser,
|
||||
fetchUserInfo,
|
||||
updateUser,
|
||||
loadUserFromStorage,
|
||||
autoLoginByOpenId,
|
||||
|
||||
// 工具方法
|
||||
hasPermission,
|
||||
hasRole,
|
||||
getAvatarUrl,
|
||||
getDisplayName,
|
||||
getRoleName,
|
||||
isCertified,
|
||||
isAdmin,
|
||||
getBalance,
|
||||
getPoints,
|
||||
getUserId,
|
||||
isSuperAdmin
|
||||
};
|
||||
};
|
||||
136
dict/taro/src/hooks/useUserData.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
|
||||
import {pageShopGift} from "@/api/shop/shopGift";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
|
||||
interface UserData {
|
||||
balance: number
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
orders: {
|
||||
pending: number
|
||||
paid: number
|
||||
shipped: number
|
||||
completed: number
|
||||
refund: number
|
||||
}
|
||||
}
|
||||
|
||||
interface UseUserDataReturn {
|
||||
data: UserData | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refresh: () => Promise<void>
|
||||
updateBalance: (newBalance: number) => void
|
||||
updatePoints: (newPoints: number) => void
|
||||
}
|
||||
|
||||
export const useUserData = (): UseUserDataReturn => {
|
||||
const [data, setData] = useState<UserData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 获取用户数据
|
||||
const fetchUserData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发请求所有数据
|
||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||
getUserInfo(),
|
||||
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
|
||||
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
|
||||
])
|
||||
|
||||
const newData: UserData = {
|
||||
balance: userDataRes?.balance || 0.00,
|
||||
points: userDataRes?.points || 0,
|
||||
coupons: couponsRes?.count || 0,
|
||||
giftCards: giftCardsRes?.count || 0,
|
||||
orders: {
|
||||
pending: 0,
|
||||
paid: 0,
|
||||
shipped: 0,
|
||||
completed: 0,
|
||||
refund: 0
|
||||
}
|
||||
}
|
||||
|
||||
setData(newData)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取用户数据失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchUserData()
|
||||
}, [fetchUserData])
|
||||
|
||||
// 更新余额(本地更新,避免频繁请求)
|
||||
const updateBalance = useCallback((newBalance: number) => {
|
||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||
}, [])
|
||||
|
||||
// 更新积分
|
||||
const updatePoints = useCallback((newPoints: number) => {
|
||||
setData(prev => prev ? { ...prev, points: newPoints } : null)
|
||||
}, [])
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
fetchUserData().then()
|
||||
}, [fetchUserData])
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
updateBalance,
|
||||
updatePoints
|
||||
}
|
||||
}
|
||||
|
||||
// 轻量级版本 - 只获取基础数据
|
||||
export const useUserBasicData = () => {
|
||||
const {user} = useUser()
|
||||
const [balance, setBalance] = useState<number>(0)
|
||||
const [points, setPoints] = useState<number>(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchBasicData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
setBalance(user?.balance || 0)
|
||||
setPoints(user?.points || 0)
|
||||
} catch (error) {
|
||||
console.error('获取基础数据失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchBasicData().then()
|
||||
}, [fetchBasicData])
|
||||
|
||||
return {
|
||||
balance,
|
||||
points,
|
||||
loading,
|
||||
refresh: fetchBasicData,
|
||||
updateBalance: setBalance,
|
||||
updatePoints: setPoints
|
||||
}
|
||||
}
|
||||
17
dict/taro/src/index.html
Normal file
@@ -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>bszx-react</title>
|
||||
<script><%= htmlWebpackPlugin.options.script %></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
3
dict/taro/src/pages/article/article.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '学习'
|
||||
})
|
||||
50
dict/taro/src/pages/article/article.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {ArrowRight} from '@nutui/icons-react-taro'
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
/**
|
||||
* 文章终极列表
|
||||
* @constructor
|
||||
*/
|
||||
const Article = () => {
|
||||
// const {params} = useRouter();
|
||||
// const [categoryId, setCategoryId] = useState<number>(3494)
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
|
||||
const reload = () => {
|
||||
// if (params.id) {
|
||||
// setCategoryId(Number(params.id))
|
||||
// }
|
||||
pageCmsArticle({}).then(res => {
|
||||
if (res?.list) {
|
||||
setList(res?.list)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'px-3 mt-4 mb-10'}>
|
||||
<div className={'flex flex-col justify-between items-center bg-white rounded-lg p-4'}>
|
||||
<div className={'bg-white w-full'}>
|
||||
{
|
||||
list.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={'flex justify-between items-center py-2'} onClick={() => Taro.navigateTo({url: `/cms/help?id=${item.articleId}`}) }>
|
||||
<div className={'text-sm'}>{item.title}</div>
|
||||
<ArrowRight color={'#cccccc'} size={18} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Article
|
||||
30
dict/taro/src/pages/index/Banner.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Swiper } from '@nutui/nutui-react-taro'
|
||||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||
import {getCmsAd} from "@/api/cms/cmsAd";
|
||||
|
||||
const MyPage = () => {
|
||||
const [item, setItem] = useState<CmsAd>()
|
||||
const reload = () => {
|
||||
getCmsAd(366).then(data => {
|
||||
setItem(data)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swiper defaultValue={0} height={279} indicator style={{ height: '280px' }}>
|
||||
{item?.imageList?.map((item) => (
|
||||
<Swiper.Item key={item}>
|
||||
<img width="100%" height="100%" src={item.url} alt="" style={{ height: '280px' }} />
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MyPage
|
||||
44
dict/taro/src/pages/index/BestSellers.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import {useEffect} from "react";
|
||||
import {Image, Space} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const BestSellers = (props: any) => {
|
||||
const reload = () => {
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'px-2 mb-4'}>
|
||||
<div className={'flex flex-col justify-between items-center rounded-lg p-3'}>
|
||||
{props.data?.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={'flex bg-white rounded-lg w-full p-3 mb-2'}
|
||||
onClick={() => Taro.navigateTo({url: '/hjm/location?id=' + item.id})}>
|
||||
<Image src={item.image} mode={'scaleToFill'}
|
||||
radius="10%" width="80" height="80"/>
|
||||
<div className={'mx-3 flex flex-col'}>
|
||||
<Space direction={'vertical'}>
|
||||
<div className={'car-no text-lg font-bold'}>{item.code}</div>
|
||||
<div className={'flex text-xs text-gray-500'}>快递公司:<span
|
||||
className={'text-gray-700'}>{item.parentOrganization}</span></div>
|
||||
<div className={'flex text-xs text-gray-500'}>保险状态:<span className={'text-green-600'}>{item.insuranceStatus}</span>
|
||||
</div>
|
||||
<div className={'flex text-xs text-gray-500'}>车架号:<span
|
||||
className={'text-gray-700'}>{item.vinCode}</span></div>
|
||||
<div className={'flex text-xs text-gray-500'}>绑定操作员:<span
|
||||
className={'text-gray-700'}>{item.driver}</span></div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{height: '170px'}}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default BestSellers
|
||||
69
dict/taro/src/pages/index/Chart.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Tabs, TabPane} from '@nutui/nutui-react-taro'
|
||||
|
||||
const list = [
|
||||
{
|
||||
title: '今天',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
title: '昨天',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
title: '过去7天',
|
||||
id: 3
|
||||
},
|
||||
{
|
||||
title: '过去30天',
|
||||
id: 4
|
||||
}
|
||||
]
|
||||
const Chart = () => {
|
||||
const [tapIndex, setTapIndex] = useState<string | number>('0')
|
||||
const reload = () => {
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
align={'left'}
|
||||
tabStyle={{position: 'sticky', top: '0px'}}
|
||||
value={tapIndex}
|
||||
onChange={(paneKey) => {
|
||||
setTapIndex(paneKey)
|
||||
}}
|
||||
>
|
||||
{
|
||||
list?.map((item, index) => {
|
||||
return (
|
||||
<TabPane key={index} title={item.title}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Tabs>
|
||||
{
|
||||
list?.map((item, index) => {
|
||||
console.log(item.title)
|
||||
return (
|
||||
<div key={index} className={'px-3'}>
|
||||
{
|
||||
tapIndex != index ? null :
|
||||
<div className={'bg-white rounded-lg p-4 flex justify-center items-center text-center text-gray-300'} style={{height: '200px'}}>
|
||||
线状图
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default Chart
|
||||
259
dict/taro/src/pages/index/ExpirationTime.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Target, Scan, Truck} from '@nutui/icons-react-taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageHjmCar} from "@/api/hjm/hjmCar";
|
||||
import { ScanType } from '@/hooks/useUnifiedQRScan';
|
||||
import { isValidJSON } from '@/utils/jsonUtils';
|
||||
import { parseQRContent, confirmWechatQRLogin } from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from '@/hooks/useUser';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const ExpirationTime = () => {
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false)
|
||||
const [roleName, setRoleName] = useState<string>()
|
||||
const { isAdmin: isUserAdmin } = useUser();
|
||||
|
||||
// 检测二维码类型
|
||||
const detectScanType = (scanResult: string): ScanType => {
|
||||
try {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否为登录二维码
|
||||
const loginToken = parseQRContent(scanResult);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (error) {
|
||||
console.error('检测二维码类型失败:', error);
|
||||
return ScanType.UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理登录二维码
|
||||
const handleLoginQR = async (scanResult: string) => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
if (confirmResult.success || confirmResult.status === 'confirmed') {
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理核销二维码
|
||||
const handleVerificationQR = async (scanResult: string) => {
|
||||
if (!isUserAdmin()) {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
}
|
||||
|
||||
if (gift.status === 1) {
|
||||
throw new Error('此礼品码已使用');
|
||||
}
|
||||
|
||||
if (gift.status === 2) {
|
||||
throw new Error('此礼品码已失效');
|
||||
}
|
||||
|
||||
if (gift.userId === 0) {
|
||||
throw new Error('此礼品码未认领');
|
||||
}
|
||||
|
||||
// 执行核销
|
||||
await updateShopGift({
|
||||
...gift,
|
||||
status: 1,
|
||||
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
|
||||
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '核销成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const onScanCode = () => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: async (res) => {
|
||||
console.log(res, 'qrcode...')
|
||||
const scanContent = res.result;
|
||||
|
||||
// 检测二维码类型
|
||||
const scanType = detectScanType(scanContent);
|
||||
|
||||
try {
|
||||
if (scanType === ScanType.LOGIN) {
|
||||
// 处理登录二维码
|
||||
await handleLoginQR(scanContent);
|
||||
console.log('登录二维码处理成功');
|
||||
return;
|
||||
} else if (scanType === ScanType.VERIFICATION) {
|
||||
// 处理核销二维码
|
||||
await handleVerificationQR(scanContent);
|
||||
console.log('核销二维码处理成功');
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('特殊二维码处理失败:', error.message);
|
||||
Taro.showToast({
|
||||
title: error.message,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果不是特殊二维码,作为车辆查询处理
|
||||
console.log('作为车辆查询二维码处理:', scanContent);
|
||||
Taro.navigateTo({url: '/hjm/query?id=' + scanContent});
|
||||
},
|
||||
fail: (res) => {
|
||||
console.log(res, '扫码失败')
|
||||
Taro.showToast({
|
||||
title: '扫码失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const navToCarList = () => {
|
||||
if (isAdmin) {
|
||||
navTo('/hjm/list', true)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
if(data.certification){
|
||||
setIsAdmin( true)
|
||||
}
|
||||
if(Taro.getStorageSync('Certification') == 'jj'){
|
||||
setIsAdmin(true)
|
||||
}
|
||||
if(Taro.getStorageSync('Certification') == 'yz'){
|
||||
setIsAdmin(true)
|
||||
}
|
||||
if(Taro.getStorageSync('RoleCode') == 'Installer'){
|
||||
setIsAdmin(true)
|
||||
}
|
||||
data.roles?.map((item, index) => {
|
||||
if (index == 0) {
|
||||
setRoleName(item.roleCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
pageHjmCar({driverId: Taro.getStorageSync('UserId')}).then(res => {
|
||||
if(res?.list && res.list.length > 0){
|
||||
setIsAdmin(true)
|
||||
}
|
||||
})
|
||||
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'mb-3 fixed top-36 z-20'} style={{width: '96%', marginLeft: '3%'}}>
|
||||
<div className={'w-full flex justify-around items-center py-3 rounded-lg'}>
|
||||
<>
|
||||
<Button size={'large'}
|
||||
style={{background: 'linear-gradient(to right, #f3f2f7, #805de1)', borderColor: '#f3f2f7'}}
|
||||
icon={<Truck/>} onClick={navToCarList}>车辆列表</Button>
|
||||
<Button size={'large'}
|
||||
style={{background: 'linear-gradient(to right, #fffbe6, #ffc53d)', borderColor: '#f3f2f7'}}
|
||||
icon={<Scan/>}
|
||||
onClick={onScanCode}>扫一扫
|
||||
</Button>
|
||||
</>
|
||||
|
||||
{
|
||||
roleName == 'youzheng' && <Button size={'large'} style={{
|
||||
background: 'linear-gradient(to right, #eaff8f, #7cb305)',
|
||||
borderColor: '#f3f2f7'
|
||||
}} icon={<Target/>} onClick={() => Taro.navigateTo({url: '/hjm/fence'})}>电子围栏</Button>
|
||||
}
|
||||
|
||||
{
|
||||
roleName == 'kuaidiyuan' && <Button size={'large'} style={{
|
||||
background: 'linear-gradient(to right, #ffa39e, #ff4d4f)',
|
||||
borderColor: '#f3f2f7'
|
||||
}} icon={<Target/>} onClick={() => Taro.navigateTo({url: '/hjm/bx/bx-add'})}>一键报险</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ExpirationTime
|
||||
214
dict/taro/src/pages/index/Header.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import {Button, Space} from '@nutui/nutui-react-taro'
|
||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
||||
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import {TenantId} from "@/utils/config";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
|
||||
const Header = (props: any) => {
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
||||
const [showBasic, setShowBasic] = useState(false)
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
const [roleName, setRoleName] = useState<string>()
|
||||
|
||||
const onNav = () => {
|
||||
if (!IsLogin) {
|
||||
|
||||
return false;
|
||||
}
|
||||
Taro.switchTab({
|
||||
url: '/pages/user/user',
|
||||
})
|
||||
}
|
||||
const reload = () => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(res.statusBarHeight)
|
||||
},
|
||||
})
|
||||
getUserInfo().then( async (data) => {
|
||||
if (data) {
|
||||
console.log(data.organizationName,'0000')
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
Taro.setStorageSync('Phone',data.phone)
|
||||
// 机构ID
|
||||
Taro.setStorageSync('OrganizationId',data.organizationId)
|
||||
// 父级机构ID
|
||||
await getOrganization(Number(data.organizationId)).then(res => {
|
||||
Taro.setStorageSync('OrganizationParentId',res.parentId)
|
||||
})
|
||||
// 所属站点名称
|
||||
if(data.organizationName){
|
||||
Taro.setStorageSync('OrganizationName',data.organizationName)
|
||||
}
|
||||
// 是否已认证
|
||||
if(data.certification){
|
||||
Taro.setStorageSync('Certification','1')
|
||||
}
|
||||
// 安装人员
|
||||
const isInstaller = data.roles?.findIndex(item => item.roleCode == 'Installer')
|
||||
if(isInstaller != -1){
|
||||
setRoleName('安装人员')
|
||||
Taro.setStorageSync('RoleName', '安装人员')
|
||||
Taro.setStorageSync('RoleCode', 'Installer')
|
||||
return false;
|
||||
}
|
||||
// 管理员
|
||||
const isKdy = data.roles?.findIndex(item => item.roleCode == 'admin')
|
||||
if(isKdy != -1){
|
||||
setRoleName('管理员')
|
||||
Taro.setStorageSync('RoleName', '管理')
|
||||
Taro.setStorageSync('RoleCode', 'admin')
|
||||
return false;
|
||||
}
|
||||
// 交警
|
||||
const isJj = data.roles?.findIndex(item => item.roleCode == 'jiaojing')
|
||||
if(isJj != -1){
|
||||
setRoleName('交警')
|
||||
Taro.setStorageSync('RoleName', '交警')
|
||||
Taro.setStorageSync('RoleCode', 'jiaojing')
|
||||
Taro.setStorageSync('Certification', 'jj')
|
||||
return false;
|
||||
}
|
||||
// 邮政协会/管局
|
||||
const isYz = data.roles?.findIndex(item => item.roleCode == 'youzheng')
|
||||
if(isYz != -1){
|
||||
setRoleName('邮政协会/管局')
|
||||
Taro.setStorageSync('RoleName', '邮政协会/管局')
|
||||
Taro.setStorageSync('RoleCode', 'youzheng')
|
||||
Taro.setStorageSync('Certification', 'yz')
|
||||
return false;
|
||||
}
|
||||
// 快递公司
|
||||
const isKd = data.roles?.findIndex(item => item.roleCode == 'kuaidi')
|
||||
if(isKd != -1){
|
||||
setRoleName('快递公司')
|
||||
Taro.setStorageSync('RoleName', '快递公司')
|
||||
Taro.setStorageSync('RoleCode', 'kuaidi')
|
||||
return false;
|
||||
}
|
||||
const isZD = data.roles?.findIndex(item => item.roleCode == 'zhandian')
|
||||
if(isZD != -1){
|
||||
setRoleName('快递站点')
|
||||
Taro.setStorageSync('RoleName', '快递站点')
|
||||
Taro.setStorageSync('RoleCode', 'zhandian')
|
||||
}
|
||||
// 快递员
|
||||
const isKdyy = data.roles?.findIndex(item => item.roleCode == 'kuaidiyuan')
|
||||
if(isKdyy != -1){
|
||||
setRoleName('快递员')
|
||||
Taro.setStorageSync('RoleName', '快递员')
|
||||
Taro.setStorageSync('RoleCode', 'kuaidiyuan')
|
||||
return false;
|
||||
}
|
||||
// 注册用户
|
||||
const isUser = data.roles?.findIndex(item => item.roleCode == 'user')
|
||||
if(isUser != -1){
|
||||
setRoleName('注册用户')
|
||||
Taro.setStorageSync('RoleName', '注册用户')
|
||||
Taro.setStorageSync('RoleCode', 'user')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
setIsLogin(false);
|
||||
console.log('未登录')
|
||||
});
|
||||
myUserVerify({status: 1}).then(data => {
|
||||
if(data?.realName){
|
||||
Taro.setStorageSync('RealName',data.realName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId
|
||||
},
|
||||
success: function (res) {
|
||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||||
setIsLogin(true)
|
||||
// 重新加载小程序
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('登录失败!')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar
|
||||
fixed={true}
|
||||
style={{marginTop: `${statusBarHeight}px`, backgroundColor: 'transparent'}}
|
||||
onBackClick={() => {
|
||||
}}
|
||||
left={
|
||||
!IsLogin ? (
|
||||
<div style={{display: 'flex', alignItems: 'center'}} onClick={() => Taro.navigateTo({url: '/passport/wxLogin'})}>
|
||||
<Space>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={props.user?.avatar}
|
||||
/>
|
||||
<span style={{color: '#000'}}>{props.user?.nickname}</span>
|
||||
</Space>
|
||||
<TriangleDown size={9}/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}} onClick={onNav}>
|
||||
<Avatar
|
||||
size="22"
|
||||
src={props.user?.avatar}
|
||||
/>
|
||||
{props.user?.nickname}{roleName && <span>({roleName})</span>}
|
||||
<TriangleDown size={9}/>
|
||||
</div>
|
||||
)}>
|
||||
</NavBar>
|
||||
<Popup
|
||||
visible={showBasic}
|
||||
position="bottom"
|
||||
style={{width: '100%', height: '100%'}}
|
||||
onClose={() => {
|
||||
setShowBasic(false)
|
||||
}}
|
||||
>
|
||||
<div style={{padding: '12px 0', fontWeight: 'bold', textAlign: 'center'}}>车辆信息</div>
|
||||
</Popup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Header
|
||||
68
dict/taro/src/pages/index/Help.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {ArrowRight} from '@nutui/icons-react-taro'
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {BaseUrl} from "@/utils/config";
|
||||
import {TEMPLATE_ID} from "@/utils/server";
|
||||
|
||||
/**
|
||||
* 帮助中心
|
||||
* @constructor
|
||||
*/
|
||||
const Help = () => {
|
||||
const {params} = useRouter();
|
||||
const [categoryId, setCategoryId] = useState<number>(3494)
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
|
||||
const reload = () => {
|
||||
if (params.id) {
|
||||
setCategoryId(Number(params.id))
|
||||
}
|
||||
Taro.request({
|
||||
url: BaseUrl + '/cms/cms-article/page',
|
||||
method: 'GET',
|
||||
data: {
|
||||
categoryId
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId: TEMPLATE_ID
|
||||
},
|
||||
success: function (res) {
|
||||
const data = res.data.data;
|
||||
if (data?.list) {
|
||||
setList(data?.list)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'px-3 mb-10'}>
|
||||
<div className={'flex flex-col justify-between items-center bg-white rounded-lg p-4'}>
|
||||
<div className={'title-bar flex justify-between items-center w-full mb-2'}>
|
||||
<div className={'font-bold text-lg flex text-gray-800 justify-center items-center'}>帮助中心</div>
|
||||
<a className={'text-gray-400 text-sm'} onClick={() => Taro.navigateTo({url: `/cms/article?id=${categoryId}`})}>查看全部</a>
|
||||
</div>
|
||||
<div className={'bg-white min-h-36 w-full'}>
|
||||
{
|
||||
list.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={'flex justify-between items-center py-2'} onClick={() => Taro.navigateTo({url: `/cms/help?id=${item.articleId}`}) }>
|
||||
<div className={'text-sm'}>{item.title}</div>
|
||||
<ArrowRight color={'#cccccc'} size={18} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Help
|
||||
75
dict/taro/src/pages/index/Login.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import './login.scss';
|
||||
import {User} from "@/api/system/user/model";
|
||||
|
||||
interface LoginProps {
|
||||
done?: (data: User) => void;
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({ done }) => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const [env, setEnv] = useState<string>()
|
||||
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
setEnv(Taro.getEnv())
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{height: '80vh'}} className={'flex flex-col justify-center px-5'}>
|
||||
<div className={'text-3xl text-center py-5 font-normal mb-10 '}>登录</div>
|
||||
{
|
||||
env === 'WEAPP' && (
|
||||
<>
|
||||
<div className={'flex flex-col w-full text-white rounded-full justify-between items-center my-2'} style={{ background: 'linear-gradient(to right, #7e22ce, #9333ea)'}}>
|
||||
<Button onClick={() => Taro.navigateTo({url: '/passport/wxLogin'})}>
|
||||
授权手机号登录
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
env === 'WEB' && (
|
||||
<>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="text" placeholder="手机号" maxLength={11}
|
||||
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex justify-between my-2 text-left px-1'}>
|
||||
<a href={'#'} className={'text-blue-600 text-sm'}
|
||||
onClick={() => Taro.navigateTo({url: '/passport/forget'})}>忘记密码</a>
|
||||
<a href={'#'} className={'text-blue-600 text-sm'}
|
||||
onClick={() => Taro.navigateTo({url: '/passport/setting'})}>服务配置</a>
|
||||
</div>
|
||||
<div className={'flex justify-center my-5'}>
|
||||
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}>登录</Button>
|
||||
</div>
|
||||
<div className={'w-full bottom-20 my-2 flex justify-center text-sm items-center text-center'}>
|
||||
没有账号?<a href={''} onClick={() => Taro.navigateTo({url: '/passport/register'})}
|
||||
className={'text-blue-600'}>立即注册</a>
|
||||
</div>
|
||||
<div className={'my-2 flex fixed bottom-20 text-sm items-center px-1'}>
|
||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>登录表示您已阅读并同意</span><a
|
||||
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
|
||||
className={'text-blue-600'}>《服务协议及隐私政策》</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Login
|
||||
163
dict/taro/src/pages/index/Menu.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {navigateTo} from '@tarojs/taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {myPageBszxBm} from "@/api/bszx/bszxBm";
|
||||
import {listCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||
|
||||
const Page = () => {
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [bmLogs, setBmLogs] = useState<any>()
|
||||
const [navItems, setNavItems] = useState<any>([])
|
||||
|
||||
const onLogin = (item: any, index: number) => {
|
||||
if(!isLogin){
|
||||
return navigateTo({url: `/pages/category/category?id=${item.navigationId}`})
|
||||
}else {
|
||||
// 报名链接
|
||||
if(index == 0){
|
||||
console.log(bmLogs,'bmLogs')
|
||||
if(bmLogs && bmLogs.length > 0){
|
||||
return navigateTo({url: `/bszx/bm-cert/bm-cert?id=${bmLogs[0].id}`})
|
||||
}else {
|
||||
navigateTo({url: `/user/profile/profile`})
|
||||
}
|
||||
}
|
||||
// 善款明细
|
||||
if(item.navigationId == 4119){
|
||||
return navigateTo({url: `/bszx/pay-record/pay-record`})
|
||||
}
|
||||
return navigateTo({url: `/pages/category/category?id=${item.navigationId}`})
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
// 读取栏目
|
||||
listCmsNavigation({parentId: 2828,hide: 0}).then(res => {
|
||||
console.log(res,'9999')
|
||||
setNavItems(res);
|
||||
})
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
setUserInfo({
|
||||
avatar,
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
console.log(userInfo, 'userInfo...')
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
});
|
||||
// 报名日志
|
||||
myPageBszxBm({limit: 1}).then(res => {
|
||||
if (res.list) {
|
||||
setBmLogs(res.list);
|
||||
}
|
||||
})
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
reload();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'my-3'}>
|
||||
<div className={'pt-4 bg-yellow-50 rounded-2xl'}
|
||||
style={{background: 'linear-gradient(to bottom, #ffffff, #ffffcc)'}}>
|
||||
<div className={'flex justify-between pb-2 px-1'}>
|
||||
{
|
||||
navItems.map((item, index) => (
|
||||
<div key={index} className={'text-center'}>
|
||||
{
|
||||
isLogin && !loading ?
|
||||
<div className={'flex flex-col justify-center items-center'} onClick={() => {
|
||||
onLogin(item, index)
|
||||
}}>
|
||||
<Image src={item.icon} height={28} width={28}/>
|
||||
<div className={'mt-2'} style={{fontSize: '15px'}}>{item?.title}</div>
|
||||
</div>
|
||||
:
|
||||
<div className={'flex flex-col justify-center items-center'} onClick={() => Taro.navigateTo({url: '/passport/wxLogin'})}>
|
||||
<Image src={item.icon} height={28} width={28}/>
|
||||
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>{item?.title}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Page
|
||||
29
dict/taro/src/pages/index/SiteUrl.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {copyText} from "@/utils/common";
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const SiteUrl = (props: any) => {
|
||||
const [siteUrl, setSiteUrl] = useState<string>('')
|
||||
const reload = () => {
|
||||
if(props.tenantId){
|
||||
setSiteUrl(`https://${props.tenantId}.shoplnk.cn`)
|
||||
}else {
|
||||
setSiteUrl(`https://${Taro.getStorageSync('TenantId')}.shoplnk.cn`)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [props])
|
||||
|
||||
return (
|
||||
<div className={'px-3 mt-1 mb-4'}>
|
||||
<div className={'flex justify-between items-center bg-gray-300 rounded-lg pr-2'}>
|
||||
<Input type="text" value={siteUrl} disabled style={{backgroundColor: '#d1d5db', borderRadius: '8px'}}/>
|
||||
<Button type={'info'} onClick={() => copyText(siteUrl)}>复制</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SiteUrl
|
||||
38
dict/taro/src/pages/index/chart/DemoLine.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { View } from '@tarojs/components'
|
||||
import { EChart } from "echarts-taro3-react";
|
||||
import './index.scss'
|
||||
|
||||
export default function Index() {
|
||||
const refBarChart = useRef<any>()
|
||||
const defautOption = {
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [120, 200, 150, 80, 70, 110, 130],
|
||||
type: "line",
|
||||
showBackground: true,
|
||||
backgroundStyle: {
|
||||
color: "rgba(220, 220, 220, 0.8)",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
useEffect(() => {
|
||||
if(refBarChart.current) {
|
||||
refBarChart.current?.refresh(defautOption);
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='index'>
|
||||
<EChart ref={refBarChart} canvasId='line-canvas' />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
7
dict/taro/src/pages/index/chart/index.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.index {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #F3F3F3;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
5
dict/taro/src/pages/index/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: 'shopLnk.cn - 数灵云店',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
4
dict/taro/src/pages/index/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
page {
|
||||
background: url("https://oss.wsdns.cn/20250414/5bed65bff2f8434995e6c22d67271c77.png");
|
||||
background-size: cover;
|
||||
}
|
||||
387
dict/taro/src/pages/index/index.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import Header from './Header'
|
||||
import './index.scss'
|
||||
import Taro from '@tarojs/taro';
|
||||
import {Map} from '@tarojs/components'
|
||||
import {Search} from '@nutui/icons-react-taro'
|
||||
import {Button, Input} from '@nutui/nutui-react-taro'
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import ExpirationTime from "./ExpirationTime";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {getSiteInfo, getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import Login from "./Login";
|
||||
import {pageByQQMap, pageHjmCar} from "@/api/hjm/hjmCar";
|
||||
import {HjmCar} from "@/api/hjm/hjmCar/model";
|
||||
|
||||
export interface Market {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
name?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(true)
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false)
|
||||
const [search, setSearch] = useState(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [longitude, setLongitude] = useState<any>()
|
||||
const [latitude, setLatitude] = useState<any>()
|
||||
const [markers, setMarkers] = useState<Market[]>([])
|
||||
const [scale, setScale] = useState<any>(12)
|
||||
const [keywords, setKeywords] = useState<string>('')
|
||||
const [list, setList] = useState<HjmCar[]>([])
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '注册即可开通 - webSoft云应用',
|
||||
path: `/pages/index/index`
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '注册即可开通 - webSoft云应用',
|
||||
path: `/pages/index/index`,
|
||||
success: function (res) {
|
||||
console.log('分享成功', res);
|
||||
},
|
||||
fail: function (res) {
|
||||
console.log('分享失败', res);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// const reloadMore = async () => {
|
||||
// setPage(page + 1)
|
||||
// }
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onKeywords = (keywords: string) => {
|
||||
setKeywords(keywords)
|
||||
}
|
||||
|
||||
// 登录成功后回调
|
||||
const handleLogin = (data: User) => {
|
||||
setIsLogin(true)
|
||||
setUserInfo(data)
|
||||
Taro.showTabBar()
|
||||
reload();
|
||||
}
|
||||
|
||||
// 获取当前位置
|
||||
const getLocation = async () => {
|
||||
try {
|
||||
const res = await Taro.getLocation({
|
||||
type: 'gcj02' //返回可以用于wx.openLocation的经纬度
|
||||
})
|
||||
|
||||
if (res.latitude) {
|
||||
setLatitude(res.latitude)
|
||||
}
|
||||
if (res.longitude) {
|
||||
setLongitude(res.longitude)
|
||||
}
|
||||
|
||||
// 已认证用户
|
||||
if (Taro.getStorageSync('Certification')) {
|
||||
setIsAdmin(true)
|
||||
setScale(11)
|
||||
// pageHjmCarByMap(res.latitude, res.longitude)
|
||||
}
|
||||
|
||||
// 游客
|
||||
if (!Taro.getStorageSync('access_token') || Taro.getStorageSync('RoleName') == '注册用户') {
|
||||
setScale(15)
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error('获取位置失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const onQuery = () => {
|
||||
if (!keywords) {
|
||||
Taro.showToast({
|
||||
title: '请输入关键字',
|
||||
icon: 'none'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
reload();
|
||||
}
|
||||
|
||||
const pageHjmCarByMap = async (latitude?: any, longitude?: any) => {
|
||||
// 搜索条件
|
||||
const where = {}
|
||||
|
||||
const user = await getUserInfo();
|
||||
|
||||
// 交警和邮管不能在小程序主界面地图上看到任何一个车辆的定位
|
||||
if(Taro.getStorageSync('RoleCode') == 'jiaojing' || Taro.getStorageSync('RoleCode') == 'youzheng'){
|
||||
return false
|
||||
}
|
||||
if (latitude) {
|
||||
// @ts-ignore
|
||||
where.latitude = latitude
|
||||
}
|
||||
if (longitude) {
|
||||
// @ts-ignore
|
||||
where.longitude = longitude
|
||||
}
|
||||
// 判断身份
|
||||
const roleCode = Taro.getStorageSync('RoleCode');
|
||||
if (roleCode == 'kuaidiyuan') {
|
||||
// @ts-ignore
|
||||
where.driverId = user.userId;
|
||||
}
|
||||
if (roleCode == 'zhandian') {
|
||||
// @ts-ignore
|
||||
where.organizationId = user.organizationId;
|
||||
}
|
||||
if (roleCode == 'kuaidi') {
|
||||
// @ts-ignore
|
||||
where.organizationParentId = user.organizationId;
|
||||
}
|
||||
|
||||
pageByQQMap(where).then(res => {
|
||||
console.log(res?.count, 'pageByQQMap')
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
const data = res?.list;
|
||||
const arr = []
|
||||
data?.map((item: HjmCar) => {
|
||||
// @ts-ignore
|
||||
arr.push({
|
||||
id: item.id,
|
||||
latitude: item.latitude,
|
||||
longitude: item.longitude,
|
||||
label: {
|
||||
content: `${item?.code}`,
|
||||
color: '#000000',
|
||||
fontSize: 12,
|
||||
borderRadius: 5,
|
||||
bgColor: '#FFFFFF',
|
||||
// @ts-ignore
|
||||
padding: '5px 5px',
|
||||
borderWidth: 1
|
||||
},
|
||||
title: `${item.organization}`,
|
||||
name: item.organization
|
||||
})
|
||||
})
|
||||
setMarkers(arr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
setLoading(true)
|
||||
if (!Taro.getStorageSync('access_token')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pageHjmCarByMap(latitude, longitude)
|
||||
if (!isAdmin) {
|
||||
return false;
|
||||
}
|
||||
setMarkers([])
|
||||
setScale(12)
|
||||
pageHjmCar({keywords, deleted: 0}).then(res => {
|
||||
if (res?.count == 0) {
|
||||
Taro.showToast({
|
||||
title: '没有搜索结果',
|
||||
icon: 'none'
|
||||
});
|
||||
return false
|
||||
}
|
||||
setList(res?.list || [])
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
const data = res?.list[0];
|
||||
setLongitude(data?.longitude)
|
||||
setLatitude(data?.latitude)
|
||||
if (isAdmin) {
|
||||
setMarkers([{
|
||||
id: data.id,
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
// @ts-ignore
|
||||
label: {
|
||||
content: `${data?.code}`,
|
||||
color: '#000000',
|
||||
fontSize: 12,
|
||||
borderRadius: 5,
|
||||
bgColor: '#FFFFFF',
|
||||
// @ts-ignore
|
||||
padding: '5px 5px',
|
||||
borderWidth: 1
|
||||
},
|
||||
title: `${data.organization}`,
|
||||
name: `${data.organization}`
|
||||
}])
|
||||
}
|
||||
}
|
||||
console.log(list.length, 'carList.length')
|
||||
})
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Taro.hideTabBar()
|
||||
getLocation().then()
|
||||
// 获取站点信息
|
||||
getSiteInfo().then((data) => {
|
||||
console.log(data, 'siteInfo')
|
||||
if (data.search) {
|
||||
setSearch(false);
|
||||
}
|
||||
})
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 获取用户信息
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
setUserInfo({
|
||||
avatar,
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
// 是否管理员
|
||||
if (data.certification) {
|
||||
setIsAdmin(true)
|
||||
}
|
||||
// 是否交警
|
||||
if (Taro.getStorageSync('Certification') == 'jj') {
|
||||
console.log('交警', '12312')
|
||||
setIsAdmin(true)
|
||||
}
|
||||
if (Taro.getStorageSync('RoleCode') == 'Installer') {
|
||||
setIsAdmin(true)
|
||||
}
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
// 排查交警和邮政角色不保存openid
|
||||
if (Taro.getStorageSync('RoleCode') !== 'jiaojing' || Taro.getStorageSync('RoleCode') !== 'youzheng' || Taro.getStorageSync('RoleCode') !== 'Installer') {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
setIsLogin(false);
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!IsLogin && search ? (<Login done={handleLogin}/>) : (<>
|
||||
<Header user={userInfo}/>
|
||||
<ExpirationTime/>
|
||||
<div className={'fixed z-20 top-24 left-0 w-full'}>
|
||||
<div className={'px-2'}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: '#fff',
|
||||
padding: '0 7px',
|
||||
borderRadius: '20px'
|
||||
}}
|
||||
>
|
||||
<Search className={'mx-2'}/>
|
||||
<Input
|
||||
placeholder="车辆编号"
|
||||
value={keywords}
|
||||
onChange={onKeywords}
|
||||
onConfirm={onQuery}
|
||||
/>
|
||||
<div
|
||||
className={'flex items-center'}
|
||||
>
|
||||
<Button type="warning" onClick={onQuery}>
|
||||
查询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!loading && (
|
||||
<Map
|
||||
id="map"
|
||||
longitude={longitude}
|
||||
latitude={latitude}
|
||||
scale={scale}
|
||||
// @ts-ignore
|
||||
markers={markers}
|
||||
onTap={(map) => {
|
||||
console.log('map tap', map)
|
||||
}}
|
||||
style={{width: '100%', height: '100vh'}}
|
||||
/>
|
||||
)}
|
||||
</>)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
10
dict/taro/src/pages/index/login.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
// 微信授权按钮的特殊样式
|
||||
button[open-type="getPhoneNumber"] {
|
||||
width: 100%;
|
||||
padding: 8px 0 !important;
|
||||
height: 80px;
|
||||
color: #ffffff !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 50px !important;
|
||||
}
|
||||
5
dict/taro/src/pages/kefu/kefu.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品列表',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
67
dict/taro/src/pages/kefu/kefu.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import {useEffect, useState} from "react"; // 添加 useCallback 引入
|
||||
import Taro, {useShareAppMessage, useShareTimeline} from '@tarojs/taro';
|
||||
import {Space, NavBar} from '@nutui/nutui-react-taro'
|
||||
import {Search, Received, Scan} from '@nutui/icons-react-taro'
|
||||
import GoodsList from "@/components/GoodsList";
|
||||
|
||||
function Kefu() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '注册即可开通 - webSoft云应用'
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '注册即可开通 - webSoft云应用',
|
||||
success: function (res) {
|
||||
console.log('分享成功', res);
|
||||
},
|
||||
fail: function (res) {
|
||||
console.log('分享失败', res);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(res.statusBarHeight)
|
||||
},
|
||||
})
|
||||
// 设置导航栏背景色(含状态栏)
|
||||
Taro.setNavigationBarColor({
|
||||
backgroundColor: '#ffffff', // 状态栏+导航栏背景色
|
||||
frontColor: 'black', // 状态栏文字颜色(仅支持 black/white)
|
||||
});
|
||||
}, []); // 新增: 添加滚动事件监听
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar
|
||||
fixed={true}
|
||||
style={{marginTop: `${statusBarHeight}px`}}
|
||||
onBackClick={() => {
|
||||
}}
|
||||
left={
|
||||
<>
|
||||
<div className={'flex justify-between items-center w-full'}>
|
||||
<Space>
|
||||
<Search size={18} className={'mx-1'}/>
|
||||
<Received size={18} className={'mx-1'}/>
|
||||
<Scan size={18} className={'mx-1'}/>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>商品</span>
|
||||
</NavBar>
|
||||
<GoodsList/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Kefu;
|
||||
5
dict/taro/src/pages/order/order.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单列表',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
53
dict/taro/src/pages/order/order.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {useEffect, useState} from "react"; // 添加 useCallback 引入
|
||||
import Taro from '@tarojs/taro';
|
||||
import {NavBar, Space} from '@nutui/nutui-react-taro'
|
||||
import {Filter,Search} from '@nutui/icons-react-taro'
|
||||
import OrderList from "@/components/OrderList";
|
||||
|
||||
function Order() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(res.statusBarHeight)
|
||||
},
|
||||
})
|
||||
// 设置导航栏背景色(含状态栏)
|
||||
Taro.setNavigationBarColor({
|
||||
backgroundColor: '#ffffff', // 状态栏+导航栏背景色
|
||||
frontColor: 'black', // 状态栏文字颜色(仅支持 black/white)
|
||||
});
|
||||
}, []); // 新增: 添加滚动事件监听
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavBar
|
||||
fixed={true}
|
||||
style={{marginTop: `${statusBarHeight}px`}}
|
||||
onBackClick={() => {
|
||||
}}
|
||||
left={
|
||||
<>
|
||||
<div className={'flex justify-between items-center w-full'}>
|
||||
<Space>
|
||||
<Search size={18} className={'mx-1'}/>
|
||||
<Filter size={18} className={'mx-1'}/>
|
||||
</Space>
|
||||
</div>
|
||||
{/*<SearchBar shape="round" maxLength={5} style={{paddingLeft: '1px'}}/>*/}
|
||||
{/*<div className={'flex flex-col text-center justify-center items-center'}>*/}
|
||||
{/* <Filter size={14}/>*/}
|
||||
{/* <div className={'text-xs text-gray-600 whitespace-nowrap'}>筛选</div>*/}
|
||||
{/*</div>*/}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>订单</span>
|
||||
</NavBar>
|
||||
<OrderList/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Order;
|
||||
3
dict/taro/src/pages/study/study.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '学习'
|
||||
})
|
||||
74
dict/taro/src/pages/study/study.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Image} from '@nutui/nutui-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import {checkMonthTaskCompleted} from "@/api/hjm/hjmExamLog";
|
||||
import Questions from '@/components/Questions';
|
||||
import {getWebsiteField} from "@/api/system/website/field";
|
||||
|
||||
/**
|
||||
* 文章终极列表
|
||||
* @constructor
|
||||
*/
|
||||
const Study = () => {
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [list, setList] = useState<CmsArticle[]>()
|
||||
const [monthTaskCompleted, setMonthTaskCompleted] = useState<boolean>(false)
|
||||
|
||||
const reload = async () => {
|
||||
setLoading(true)
|
||||
const field = await getWebsiteField(15524);
|
||||
if (field.value == '0') {
|
||||
setIsAdmin(true)
|
||||
}else {
|
||||
setIsAdmin(false)
|
||||
}
|
||||
const article = await pageCmsArticle({categoryId: 4289, status: 0})
|
||||
if(article){
|
||||
setList(article?.list)
|
||||
}
|
||||
const promise = await checkMonthTaskCompleted();
|
||||
if(promise){
|
||||
setMonthTaskCompleted(true)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
console.log('初始化完成')
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAdmin && (
|
||||
<div className={'px-3 mt-4 mb-10'}>
|
||||
{/* 已完成任务 */}
|
||||
{monthTaskCompleted && !loading && (
|
||||
<div style={{backgroundColor: 'white', borderRadius: '8px', padding: '24px', textAlign: 'center'}}>
|
||||
<h1 style={{fontSize: '20px', fontWeight: 'bold', marginBottom: '16px', color: '#52c41a'}}>🎉
|
||||
本月学习任务已完成!</h1>
|
||||
<div style={{marginBottom: '16px', color: '#666'}}>
|
||||
您已经完成了本月的学习任务,无需再进行考试。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!monthTaskCompleted && list?.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={'flex flex-col justify-between items-center bg-white rounded-lg p-2'}
|
||||
onClick={() => Taro.navigateTo({url: `/hjm/practice/practice?id=${item.articleId}`})}>
|
||||
<Image src={item.image} height={200}/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{!isAdmin && <Questions/>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Study
|
||||
163
dict/taro/src/pages/user/components/OrderIcon.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {navigateTo} from '@tarojs/taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import {myPageBszxBm} from "@/api/bszx/bszxBm";
|
||||
import {listCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||
|
||||
const OrderIcon = () => {
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [bmLogs, setBmLogs] = useState<any>()
|
||||
const [navItems, setNavItems] = useState<any>([])
|
||||
|
||||
const onLogin = (item: any, index: number) => {
|
||||
if(!isLogin){
|
||||
return navigateTo({url: `/pages/category/category?id=${item.navigationId}`})
|
||||
}else {
|
||||
// 报名链接
|
||||
if(index == 0){
|
||||
console.log(bmLogs,'bmLogs')
|
||||
if(bmLogs && bmLogs.length > 0){
|
||||
return navigateTo({url: `/bszx/bm-cert/bm-cert?id=${bmLogs[0].id}`})
|
||||
}else {
|
||||
navigateTo({url: `/user/profile/profile`})
|
||||
}
|
||||
}
|
||||
// 善款明细
|
||||
if(item.navigationId == 4119){
|
||||
return navigateTo({url: `/bszx/pay-record/pay-record`})
|
||||
}
|
||||
return navigateTo({url: `/pages/category/category?id=${item.navigationId}`})
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
// 读取栏目
|
||||
listCmsNavigation({parentId: 2828,hide: 0}).then(res => {
|
||||
console.log(res,'9999')
|
||||
setNavItems(res);
|
||||
})
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
setUserInfo({
|
||||
avatar,
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
console.log(userInfo, 'userInfo...')
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
});
|
||||
// 报名日志
|
||||
myPageBszxBm({limit: 1}).then(res => {
|
||||
if (res.list) {
|
||||
setBmLogs(res.list);
|
||||
}
|
||||
})
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
reload();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'my-3'}>
|
||||
<div className={'pt-4 bg-yellow-50 rounded-2xl'}
|
||||
style={{background: 'linear-gradient(to bottom, #ffffff, #ffffcc)'}}>
|
||||
<div className={'flex justify-between pb-2 px-1'}>
|
||||
{
|
||||
navItems.map((item, index) => (
|
||||
<div key={index} className={'text-center'}>
|
||||
{
|
||||
isLogin && !loading ?
|
||||
<div className={'flex flex-col justify-center items-center'} onClick={() => {
|
||||
onLogin(item, index)
|
||||
}}>
|
||||
<Image src={item.icon} height={28} width={28}/>
|
||||
<div className={'mt-2'} style={{fontSize: '15px'}}>{item?.title}</div>
|
||||
</div>
|
||||
:
|
||||
<div className={'flex flex-col justify-center items-center'} onClick={() => Taro.navigateTo({url: '/passport/wxLogin'})}>
|
||||
<Image src={item.icon} height={28} width={28}/>
|
||||
<div className={'mt-2 text-gray-700'} style={{fontSize: '15px'}}>{item?.title}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default OrderIcon
|
||||
155
dict/taro/src/pages/user/components/UserCard.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import {Avatar, Tag, Space} from '@nutui/nutui-react-taro'
|
||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useEffect, useState} from "react";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import navTo from "@/utils/common";
|
||||
|
||||
function UserCard() {
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [roleName, setRoleName] = useState<string>('注册用户')
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const reload = () => {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
setUserInfo({
|
||||
avatar,
|
||||
nickname: res.userInfo.nickName,
|
||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||
})
|
||||
getUserInfo().then((data) => {
|
||||
if (data) {
|
||||
setUserInfo(data)
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
success: (res) => {
|
||||
getWxOpenId({code: res.code}).then(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// 判断身份
|
||||
const roleName = Taro.getStorageSync('RoleName');
|
||||
if(roleName){
|
||||
setRoleName(roleName)
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log('未登录')
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'p-4'}>
|
||||
<div
|
||||
className={'user-card w-full bg-blue-900 flex flex-col justify-center rounded-xl'}
|
||||
style={{
|
||||
background: 'linear-gradient(to bottom, #e1dfff, #f3f2f7)', // 这种情况建议使用类名来控制样式(引入外联样式)
|
||||
// width: '720rpx',
|
||||
// margin: '10px auto 0px auto',
|
||||
height: '144px',
|
||||
// borderRadius: '22px 22px 0 0',
|
||||
}}
|
||||
>
|
||||
<div className={'user-card-header flex w-full justify-between items-center'}>
|
||||
<div className={'flex items-center mx-4'}>
|
||||
{
|
||||
IsLogin ? (
|
||||
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
||||
) : (
|
||||
<div onClick={() => Taro.navigateTo({url: '/passport/wxLogin'})}>
|
||||
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={'user-info flex flex-col px-2'}>
|
||||
<div className={'py-1 text-black font-bold'}>{IsLogin ? userInfo?.mobile : '请点击头像登录'}</div>
|
||||
{IsLogin ? (
|
||||
<Space className={'grade text-xs py-1'}>
|
||||
<Tag type="success" round>
|
||||
<div className={'p-1'}>{roleName || '注册用户'}</div>
|
||||
</Tag>
|
||||
{/*{*/}
|
||||
{/* userInfo?.organizationName && (*/}
|
||||
{/* <Tag type="warning" round>*/}
|
||||
{/* <div className={'p-1'}>{userInfo?.organizationName}</div>*/}
|
||||
{/* </Tag>*/}
|
||||
{/* )*/}
|
||||
{/*}*/}
|
||||
</Space>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mx-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
|
||||
onClick={() => navTo('/user/profile/profile', true)}>
|
||||
{'个人资料'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default UserCard;
|
||||
272
dict/taro/src/pages/user/components/UserCell.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import {Cell, InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
import navTo from "@/utils/common";
|
||||
import UserFooter from "./UserFooter";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {ArrowRight, ShieldCheck, Truck, LogisticsError} from '@nutui/icons-react-taro'
|
||||
import {CSSProperties, useEffect, useState} from "react";
|
||||
|
||||
const UserCell = () => {
|
||||
const [roleName, setRoleName] = useState<string>('')
|
||||
const InfiniteUlStyle: CSSProperties = {
|
||||
height: '88vh',
|
||||
padding: '16px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
Taro.clearStorageSync()
|
||||
Taro.removeStorageSync('access_token')
|
||||
Taro.removeStorageSync('TenantId')
|
||||
Taro.removeStorageSync('UserId')
|
||||
Taro.removeStorageSync('userInfo')
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setRoleName(Taro.getStorageSync('RoleCode'))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={InfiniteUlStyle} id="scroll">
|
||||
<InfiniteLoading>
|
||||
<Cell.Group divider={true}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<ShieldCheck size={16}/>
|
||||
<span className={'pl-3 text-sm'}>实名认证</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/userVerify/index', true)
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
{
|
||||
(roleName === 'kuaidi' || roleName == 'zhandian' || roleName == 'youzheng') && (
|
||||
<>
|
||||
{
|
||||
roleName != 'youzheng' && (
|
||||
<Cell.Group divider={true}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<ShieldCheck size={16}/>
|
||||
<span className={'pl-3 text-sm'}>实名认证审核</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/userVerify/admin', true)
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
)
|
||||
}
|
||||
<Cell.Group divider={true}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Truck size={16}/>
|
||||
<span className={'pl-3 text-sm'}>违章记录</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/hjm/violation/list', true)
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
roleName === 'kuaidiyuan' && (
|
||||
<>
|
||||
<Cell.Group divider={true}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Truck size={16}/>
|
||||
<span className={'pl-3 text-sm'}>车辆信息</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/user/car/index', true)
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
<Cell.Group divider={true}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<LogisticsError size={16}/>
|
||||
<span className={'pl-3 text-sm'}>报险记录</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/hjm/bx/bx', true)
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
roleName === 'jiaojing' && (
|
||||
<Cell.Group divider={true}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Truck size={16}/>
|
||||
<span className={'pl-3 text-sm'}>违章记录</span>
|
||||
</div>
|
||||
}
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/hjm/violation/list', true)
|
||||
}}
|
||||
/>
|
||||
</Cell.Group>
|
||||
)
|
||||
}
|
||||
|
||||
{/*<Cell.Group divider={true} description={*/}
|
||||
{/* <div style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <span style={{marginTop: '12px'}}>管理</span>*/}
|
||||
{/* </div>*/}
|
||||
{/*}>*/}
|
||||
{/* <Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title={*/}
|
||||
{/* <div style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <Presentation size={18}/>*/}
|
||||
{/* <span style={{marginLeft: '5px'}}>分析</span>*/}
|
||||
{/* </div>*/}
|
||||
{/* }*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/bszx/bm-cert/bm-cert', true)*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* <Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title={*/}
|
||||
{/* <div style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <PickedUp size={18}/>*/}
|
||||
{/* <span style={{marginLeft: '5px'}}>客户</span>*/}
|
||||
{/* </div>*/}
|
||||
{/* }*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/bszx/pay-log/pay-log', true)*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* <Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title={*/}
|
||||
{/* <div style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <Coupon size={18}/>*/}
|
||||
{/* <span style={{marginLeft: '5px'}}>折扣</span>*/}
|
||||
{/* </div>*/}
|
||||
{/* }*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/user/profile/profile', true)*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/*</Cell.Group>*/}
|
||||
{/*<Cell.Group divider={true} description={*/}
|
||||
{/* <div style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||
{/* <span style={{marginTop: '12px'}}>设置与帮助</span>*/}
|
||||
{/* </div>*/}
|
||||
{/*}>*/}
|
||||
{/* <Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title="店铺设置"*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => Taro.navigateTo({url: '/website/modify'})}*/}
|
||||
{/* />*/}
|
||||
{/* <Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title="帮助中心"*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/user/profile/profile', true)*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* <Cell*/}
|
||||
{/* className="nutui-cell-clickable"*/}
|
||||
{/* title="问题反馈"*/}
|
||||
{/* align="center"*/}
|
||||
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* navTo('/user/profile/profile', true)*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/*</Cell.Group>*/}
|
||||
<Cell.Group divider={true} description={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<span style={{marginTop: '12px'}}>账号管理</span>
|
||||
</div>
|
||||
}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title="账号安全"
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/user/profile/profile', true)}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title="管理员登录"
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/passport/login', true)}
|
||||
/>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
title="退出登录"
|
||||
align="center"
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={onLogout}
|
||||
/>
|
||||
</Cell.Group>
|
||||
<UserFooter/>
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
102
dict/taro/src/pages/user/components/UserFooter.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import {loginBySms} from "@/api/passport/login";
|
||||
import {useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Popup} from '@nutui/nutui-react-taro'
|
||||
import {UserParam} from "@/api/system/user/model";
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Form, Input} from '@nutui/nutui-react-taro'
|
||||
import {Copyright, Version} from "@/utils/config";
|
||||
const UserFooter = () => {
|
||||
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
|
||||
const [clickNum, setClickNum] = useState<number>(0)
|
||||
const [FormData, setFormData] = useState<UserParam>(
|
||||
{
|
||||
phone: undefined,
|
||||
password: undefined
|
||||
}
|
||||
)
|
||||
|
||||
const onLoginByPhone = () => {
|
||||
setFormData({})
|
||||
setClickNum(clickNum + 1);
|
||||
if (clickNum > 10) {
|
||||
setOpenLoginByPhone(true);
|
||||
}
|
||||
}
|
||||
|
||||
const closeLoginByPhone = () => {
|
||||
setClickNum(0)
|
||||
setOpenLoginByPhone(false)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitByPhone = (values: any) => {
|
||||
loginBySms({
|
||||
phone: values.phone,
|
||||
code: values.code
|
||||
}).then(() => {
|
||||
setOpenLoginByPhone(false);
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
},1000)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
||||
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
|
||||
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
||||
</div>
|
||||
|
||||
<Popup
|
||||
style={{width: '350px', padding: '10px'}}
|
||||
visible={openLoginByPhone}
|
||||
closeOnOverlayClick={false}
|
||||
closeable={true}
|
||||
onClose={closeLoginByPhone}
|
||||
>
|
||||
<Form
|
||||
style={{width: '350px',padding: '10px'}}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitByPhone(values)}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
label={'手机号码'}
|
||||
name="phone"
|
||||
required
|
||||
rules={[{message: '手机号码'}]}
|
||||
>
|
||||
<Input placeholder="请输入手机号码" maxLength={11} type="text"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'短信验证码'}
|
||||
name="code"
|
||||
required
|
||||
rules={[{message: '短信验证码'}]}
|
||||
>
|
||||
<Input placeholder="请输入短信验证码" maxLength={6} type="text"/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Popup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserFooter
|
||||
3
dict/taro/src/pages/user/user.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的'
|
||||
})
|
||||
19
dict/taro/src/pages/user/user.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import {useEffect} from 'react'
|
||||
import UserCard from "./components/UserCard";
|
||||
import UserCell from "./components/UserCell";
|
||||
|
||||
function User() {
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div className={'fixed w-full'}>
|
||||
<UserCard />
|
||||
<UserCell />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default User
|
||||
4
dict/taro/src/passport/agreement.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '服务协议与隐私政策',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
4
dict/taro/src/passport/agreement.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.content{
|
||||
padding: 32px;
|
||||
line-height: 2.4rem;
|
||||
}
|
||||
43
dict/taro/src/passport/agreement.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model"
|
||||
// import ReactMarkdown from 'react-markdown';
|
||||
// 显示html富文本
|
||||
import {View, RichText} from '@tarojs/components'
|
||||
import Line from "@/components/Gap";
|
||||
import {wxParse} from "@/utils/common";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import './agreement.scss'
|
||||
|
||||
function Detail() {
|
||||
// 文章详情
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const reload = () => {
|
||||
getCmsArticle(10112).then(data => {
|
||||
if(data){
|
||||
data.content = wxParse(data.content)
|
||||
setItem(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'bg-white'}>
|
||||
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
|
||||
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
|
||||
<View className={'content text-gray-700 text-sm'}>
|
||||
{
|
||||
item?.editor === 1 ?
|
||||
<RichText nodes={item?.content} /> :
|
||||
null
|
||||
}
|
||||
</View>
|
||||
<Line height={44}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
4
dict/taro/src/passport/forget.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '忘记密码',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
36
dict/taro/src/passport/forget.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import {useEffect} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {copyText} from "@/utils/common";
|
||||
|
||||
const Register = () => {
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-3'}>
|
||||
<div className={'text-sm py-2'}>请验证您的登录账号,以进行重设密码</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="text" placeholder="手机号" maxLength={11} style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="password" placeholder="新的密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex justify-between items-center bg-white rounded-lg my-2 pr-2'}>
|
||||
<Input type="text" placeholder="短信验证码" style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
<Button onClick={() => copyText('https://site-10398.shoplnk.cn?v=1.33')}>发送</Button>
|
||||
</div>
|
||||
<div className={'flex justify-center my-5'}>
|
||||
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'}>确认修改</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Register
|
||||
4
dict/taro/src/passport/login.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '管理员登录',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
411
dict/taro/src/passport/login.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import {loginBySms, getCaptcha, sendSmsCaptcha} from '@/api/passport/login'
|
||||
|
||||
const Login = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
// 只保留短信登录方式
|
||||
const [loginType, setLoginType] = useState('sms')
|
||||
// const [username, setUsername] = useState('')
|
||||
// const [password, setPassword] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
const [smsCode, setSmsCode] = useState('')
|
||||
const [captchaImg, setCaptchaImg] = useState('')
|
||||
const [captchaCode, setCaptchaCode] = useState('')
|
||||
const [showCaptchaModal, setShowCaptchaModal] = useState(false)
|
||||
const [countdown, setCountdown] = useState(0) // 短信验证码倒计时
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
|
||||
// 获取图形验证码
|
||||
const fetchCaptcha = async () => {
|
||||
try {
|
||||
const res = await getCaptcha()
|
||||
setCaptchaImg(res.base64)
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '获取验证码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 发送短信验证码
|
||||
const handleSendSmsCode = async () => {
|
||||
if (!phone) {
|
||||
Taro.showToast({
|
||||
title: '请输入手机号',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
if (!phoneReg.test(phone)) {
|
||||
Taro.showToast({
|
||||
title: '手机号格式不正确',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 显示图形验证码弹窗
|
||||
fetchCaptcha()
|
||||
setShowCaptchaModal(true)
|
||||
}
|
||||
|
||||
// 确认发送短信验证码
|
||||
const confirmSendSmsCode = async () => {
|
||||
if (!captchaCode) {
|
||||
Taro.showToast({
|
||||
title: '请输入图形验证码',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
// 发送短信验证码时需要传入手机号和图形验证码
|
||||
await sendSmsCaptcha({ phone, code: captchaCode })
|
||||
Taro.showToast({
|
||||
title: '短信验证码已发送',
|
||||
icon: 'success'
|
||||
})
|
||||
setShowCaptchaModal(false)
|
||||
setCaptchaCode('')
|
||||
|
||||
// 开始倒计时
|
||||
setCountdown(60)
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: error.message || '发送失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 短信验证码登录
|
||||
const handleSmsLogin = async () => {
|
||||
if (!phone) {
|
||||
Taro.showToast({
|
||||
title: '请输入手机号',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
if (!phoneReg.test(phone)) {
|
||||
Taro.showToast({
|
||||
title: '手机号格式不正确',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!smsCode) {
|
||||
Taro.showToast({
|
||||
title: '请输入短信验证码',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
// 短信登录时传入手机号和短信验证码
|
||||
const res = await loginBySms({ phone, code: smsCode })
|
||||
|
||||
console.log(res,'.......')
|
||||
Taro.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/index/index' })
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: error.message || '登录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 登录处理
|
||||
const onLogin = async () => {
|
||||
if (!isAgree) {
|
||||
Taro.showToast({
|
||||
title: '请先同意服务协议',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleSmsLogin()
|
||||
}
|
||||
|
||||
// 倒计时处理
|
||||
useEffect(() => {
|
||||
let timer: any
|
||||
if (countdown > 0) {
|
||||
timer = setTimeout(() => {
|
||||
setCountdown(countdown - 1)
|
||||
}, 1000)
|
||||
}
|
||||
return () => clearTimeout(timer)
|
||||
}, [countdown])
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: '0 20px',
|
||||
minHeight: '70vh',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
padding: '20px 0',
|
||||
fontWeight: 'normal',
|
||||
margin: '20px 0 20px 0'
|
||||
}}>管理员登录</div>
|
||||
|
||||
{/* 登录方式切换 - 隐藏账号登录 */}
|
||||
<div style={{
|
||||
display: 'none', // 隐藏登录方式切换
|
||||
justifyContent: 'center',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
borderBottom: loginType === 'account' ? '2px solid #1890ff' : 'none',
|
||||
color: loginType === 'account' ? '#1890ff' : '#999',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => setLoginType('account')}
|
||||
>
|
||||
账号登录
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
borderBottom: loginType === 'sms' ? '2px solid #1890ff' : 'none',
|
||||
color: loginType === 'sms' ? '#1890ff' : '#999',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => setLoginType('sms')}
|
||||
>
|
||||
短信登录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 短信验证码登录 - 始终显示 */}
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
margin: '10px 0'
|
||||
}}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="手机号"
|
||||
maxLength={11}
|
||||
value={phone}
|
||||
onChange={(val) => setPhone(val)}
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
width: '100%',
|
||||
padding: '10px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
margin: '10px 0'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="短信验证码"
|
||||
maxLength={6}
|
||||
value={smsCode}
|
||||
onChange={(val) => setSmsCode(val)}
|
||||
style={{
|
||||
flex: 1,
|
||||
border: 'none',
|
||||
padding: '10px'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="info"
|
||||
size="small"
|
||||
disabled={countdown > 0}
|
||||
onClick={handleSendSmsCode}
|
||||
style={{
|
||||
borderRadius: '0 8px 8px 0',
|
||||
height: '40px'
|
||||
}}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: '20px 0'
|
||||
}}>
|
||||
<Button
|
||||
type="info"
|
||||
size={'large'}
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
padding: '10px'
|
||||
}}
|
||||
disabled={!isAgree}
|
||||
loading={loading}
|
||||
onClick={onLogin}
|
||||
>
|
||||
立即登录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 5px',
|
||||
margin: '10px 0'
|
||||
}}>
|
||||
<Radio
|
||||
style={{color: '#333333'}}
|
||||
checked={isAgree}
|
||||
onClick={() => setIsAgree(!isAgree)}
|
||||
/>
|
||||
<span style={{color: '#999', marginLeft: '5px'}} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span>
|
||||
<a
|
||||
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
|
||||
style={{color: '#1890ff'}}
|
||||
>
|
||||
《服务协议及隐私政策》
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图形验证码弹窗 */}
|
||||
{showCaptchaModal && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
width: '80%'
|
||||
}}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '15px',
|
||||
fontSize: '16px'
|
||||
}}>请输入图形验证码</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '15px'
|
||||
}}>
|
||||
{captchaImg && (
|
||||
<img
|
||||
src={`data:image/png;base64,${captchaImg}`}
|
||||
alt="验证码"
|
||||
style={{
|
||||
width: '128px',
|
||||
height: '48px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={fetchCaptcha}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入验证码"
|
||||
value={captchaCode}
|
||||
onChange={(val) => setCaptchaCode(val)}
|
||||
style={{
|
||||
marginBottom: '15px'
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
setShowCaptchaModal(false)
|
||||
setCaptchaCode('')
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="info"
|
||||
loading={loading}
|
||||
onClick={confirmSendSmsCode}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
4
dict/taro/src/passport/register.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '注册账号',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
47
dict/taro/src/passport/register.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
|
||||
const Register = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-3'}>
|
||||
<div className={'text-xl font-bold py-2'}>免费试用14天,快速上手独立站</div>
|
||||
<div className={'text-sm py-1 font-normal text-gray-500'}>建站、选品、营销、支付、物流,全部搞定</div>
|
||||
<div className={'text-sm pb-4 font-normal text-gray-500'}>
|
||||
WebSoft为您提供独立站的解决方案,提供专业、高效、安全的运营服务。
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="text" placeholder="手机号" maxLength={11} style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="password" placeholder="再次输入密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex justify-center my-5'}>
|
||||
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}>免费试用</Button>
|
||||
</div>
|
||||
<div className={'my-2 flex text-sm items-center px-1'}>
|
||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span>
|
||||
<a onClick={() => Taro.navigateTo({url: '/passport/agreement'})} className={'text-blue-600'}>《服务协议及隐私政策》</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'w-full fixed bottom-20 my-2 flex justify-center text-sm items-center text-center'}>
|
||||
已有账号?<a className={'text-blue-600'} onClick={() => Taro.navigateBack()}>返回登录</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Register
|
||||
4
dict/taro/src/passport/setting.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '服务配置',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
82
dict/taro/src/passport/setting.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Button,Form} from '@nutui/nutui-react-taro'
|
||||
|
||||
const Setting = () => {
|
||||
const [FormData, setFormData] = useState<any>(
|
||||
{
|
||||
domain: undefined
|
||||
}
|
||||
)
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = (values: any) => {
|
||||
if(values.domain){
|
||||
Taro.setStorageSync('ServerUrl',values.domain)
|
||||
setFormData({
|
||||
domain: values.domain
|
||||
})
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
});
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
},500)
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
// Taro.showToast({ title: error[0].message, icon: 'error' })
|
||||
}
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
if (Taro.getStorageSync('ServerUrl')) {
|
||||
setFormData({
|
||||
domain: Taro.getStorageSync('ServerUrl')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Button nativeType="submit" block type="info" size={'large'}>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-col justify-center pt-3'}>
|
||||
<div className={'text-sm py-1 px-4'}>服务域名</div>
|
||||
<Form.Item
|
||||
name="domain"
|
||||
initialValue={FormData.domain}
|
||||
rules={[{message: '请输入服务域名'}]}
|
||||
>
|
||||
<Input placeholder="https://domain.com/api" type="text" style={{backgroundColor: '#f5f5f5', borderRadius: '8px', padding: '5px 10px'}}/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Setting
|
||||
4
dict/taro/src/passport/sms-login.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '验证码登录',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
204
dict/taro/src/passport/sms-login.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
|
||||
import {LoginParam} from "@/api/passport/login/model";
|
||||
|
||||
const SmsLogin = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [sendingCode, setSendingCode] = useState<boolean>(false)
|
||||
const [countdown, setCountdown] = useState<number>(0)
|
||||
const [formData, setFormData] = useState<LoginParam>({
|
||||
phone: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout
|
||||
if (countdown > 0) {
|
||||
timer = setTimeout(() => {
|
||||
setCountdown(countdown - 1)
|
||||
}, 1000)
|
||||
}
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}, [countdown])
|
||||
|
||||
// 验证手机号格式
|
||||
const validatePhone = (phone: string): boolean => {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
||||
// 发送短信验证码
|
||||
const handleSendCode = async () => {
|
||||
if (!formData.phone) {
|
||||
Taro.showToast({
|
||||
title: '请输入手机号码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!validatePhone(formData.phone)) {
|
||||
Taro.showToast({
|
||||
title: '请输入正确的手机号码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (sendingCode || countdown > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSendingCode(true)
|
||||
await sendSmsCaptcha({ phone: formData.phone })
|
||||
|
||||
Taro.showToast({
|
||||
title: '验证码已发送',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 开始60秒倒计时
|
||||
setCountdown(60)
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error.message || '发送失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSendingCode(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
// 防止重复提交
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
if (!formData.phone) {
|
||||
Taro.showToast({
|
||||
title: '请输入手机号码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!validatePhone(formData.phone)) {
|
||||
Taro.showToast({
|
||||
title: '请输入正确的手机号码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.code) {
|
||||
Taro.showToast({
|
||||
title: '请输入验证码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.code.length !== 6) {
|
||||
Taro.showToast({
|
||||
title: '请输入6位验证码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
await loginBySms({
|
||||
phone: formData.phone,
|
||||
code: formData.code
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟跳转到首页
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error.message || '登录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-3'}>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入手机号码"
|
||||
maxLength={11}
|
||||
value={formData.phone}
|
||||
onChange={(value) => setFormData({...formData, phone: value})}
|
||||
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex justify-between items-center bg-white rounded-lg my-2 pr-2'}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入6位验证码"
|
||||
maxLength={6}
|
||||
value={formData.code}
|
||||
onChange={(value) => setFormData({...formData, code: value})}
|
||||
style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
type={countdown > 0 ? "default" : "primary"}
|
||||
loading={sendingCode}
|
||||
disabled={sendingCode || countdown > 0}
|
||||
onClick={handleSendCode}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : sendingCode ? '发送中...' : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={'flex justify-center my-5'}>
|
||||
<Button
|
||||
type="info"
|
||||
size={'large'}
|
||||
className={'w-full rounded-lg p-2'}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default SmsLogin
|
||||
4
dict/taro/src/passport/wxLogin.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '快捷登录',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
108
dict/taro/src/passport/wxLogin.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import {createWxLoginHandler} from '@/utils/wxLogin'
|
||||
import {TenantId} from "@/utils/config";
|
||||
|
||||
const Login = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openSetting = () => {
|
||||
// Taro.openSetting:调起客户端小程序设置界面,返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
|
||||
Taro.openSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户授权成功,可以获取用户信息
|
||||
reload();
|
||||
} else {
|
||||
// 用户拒绝授权,提示授权失败
|
||||
Taro.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 创建微信登录处理函数
|
||||
const handleGetPhoneNumber = createWxLoginHandler({
|
||||
onSuccess: (user) => {
|
||||
console.log('登录成功:', user);
|
||||
// 可以在这里添加额外的成功处理逻辑
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('登录失败:', error);
|
||||
},
|
||||
showToast: true,
|
||||
navigateBack: true,
|
||||
tenantId: TenantId
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-5'}>
|
||||
<div className={'text-3xl text-center py-5 font-normal my-10'}>快捷登录</div>
|
||||
|
||||
<>
|
||||
<div className={'flex justify-center my-5 rounded-lg'} style={{
|
||||
backgroundColor: isAgree ? '#9a23d4' : '#c0c4cc',
|
||||
}}>
|
||||
<Button
|
||||
type="info" size={'large'}
|
||||
className={'w-full rounded-lg p-2'}
|
||||
disabled={!isAgree}
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}>手机号一键登录</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div className={'my-2 flex text-sm items-center px-1'}>
|
||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span><a
|
||||
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
|
||||
className={'text-purple-700'}>《服务协议及隐私政策》</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Login
|
||||
62
dict/taro/src/utils/common.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export default function navTo(url: string, isLogin = false) {
|
||||
if (isLogin) {
|
||||
if (!Taro.getStorageSync('access_token')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Taro.navigateTo({
|
||||
url: url
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 转base64
|
||||
export function fileToBase64(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
let fileManager = Taro.getFileSystemManager();
|
||||
fileManager.readFile({
|
||||
filePath,
|
||||
encoding: 'base64',
|
||||
success: (e: any) => {
|
||||
resolve(`data:image/jpg;base64,${e.data}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 转义微信富文本图片样式
|
||||
* @param htmlText
|
||||
*/
|
||||
export function wxParse(htmlText) {
|
||||
// Replace <img> tags with max-width and height styles
|
||||
htmlText = htmlText.replace(/\<img/gi, '<img style="max-width:100%;height:auto;"');
|
||||
|
||||
// Replace style attributes that do not contain text-align
|
||||
htmlText = htmlText.replace(/style\s*?=\s*?(['"])(?!.*?text-align)[\s\S]*?\1/ig, 'style="max-width:100%;height:auto;"');
|
||||
|
||||
return htmlText;
|
||||
}
|
||||
|
||||
|
||||
export function copyText(text: string) {
|
||||
Taro.setClipboardData({
|
||||
data: text,
|
||||
success: function () {
|
||||
Taro.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
},
|
||||
fail: function () {
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
8
dict/taro/src/utils/config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// 租户ID
|
||||
export const TenantId = 10519;
|
||||
// 接口地址
|
||||
export const BaseUrl = 'https://cms-api.websoft.top/api';
|
||||
// 当前版本
|
||||
export const Version = 'v3.0.8';
|
||||
// 版权信息
|
||||
export const Copyright = 'WebSoft Inc.';
|
||||
110
dict/taro/src/utils/domain.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// 解析域名结构
|
||||
export function getHost(): any {
|
||||
const host = window.location.host;
|
||||
return host.split('.');
|
||||
}
|
||||
|
||||
// 是否https
|
||||
export function isHttps() {
|
||||
const protocol = window.location.protocol;
|
||||
if (protocol == 'https:') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始域名
|
||||
* @return http://www.domain.com
|
||||
*/
|
||||
export function getOriginDomain(): string {
|
||||
return window.origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名的第一部分
|
||||
* 获取tenantId
|
||||
* @return 10140
|
||||
*/
|
||||
export function getDomainPart1(): any {
|
||||
const split = getHost();
|
||||
if (split[0] == '127') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof (split[0])) {
|
||||
return split[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过解析泛域名获取租户ID
|
||||
* https://10140.wsdns.cn
|
||||
* @return 10140
|
||||
*/
|
||||
export function getTenantId() {
|
||||
let tenantId = localStorage.getItem('TenantId');
|
||||
if(getDomainPart1()){
|
||||
tenantId = getDomainPart1();
|
||||
return tenantId;
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根域名
|
||||
* hostname
|
||||
*/
|
||||
export function getHostname(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名
|
||||
* @return https://www.domain.com
|
||||
*/
|
||||
export function getDomain(): string {
|
||||
return window.location.protocol + '//www.' + getRootDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根域名
|
||||
* abc.com
|
||||
*/
|
||||
export function getRootDomain(): string {
|
||||
const split = getHost();
|
||||
return split[split.length - 2] + '.' + split[split.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取二级域名
|
||||
* @return abc.com
|
||||
*/
|
||||
export function getSubDomainPath(): string {
|
||||
const split = getHost();
|
||||
if (split.length == 2) {
|
||||
return '';
|
||||
}
|
||||
return split[split.length - 3];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品标识
|
||||
* @return 10048
|
||||
*/
|
||||
export function getProductCode(): string | null {
|
||||
const subDomain = getSubDomainPath();
|
||||
if (subDomain == undefined) {
|
||||
return null;
|
||||
}
|
||||
const split = subDomain.split('-');
|
||||
return split[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台域名
|
||||
*/
|
||||
export function navSubDomain(path: string): string {
|
||||
return `${window.location.protocol}//${path}.${getRootDomain()}`;
|
||||
}
|
||||
|
||||
484
dict/taro/src/utils/invite.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { bindRefereeRelation } from '@/api/invite'
|
||||
|
||||
/**
|
||||
* 邀请参数接口
|
||||
*/
|
||||
export interface InviteParams {
|
||||
inviter?: string;
|
||||
source?: string;
|
||||
t?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析小程序启动参数中的邀请信息
|
||||
*/
|
||||
export function parseInviteParams(options: any): InviteParams | null {
|
||||
try {
|
||||
// 优先从 query.scene 参数中解析邀请信息
|
||||
let sceneStr: string | null = null
|
||||
if (options.query && options.query.scene) {
|
||||
sceneStr = typeof options.query.scene === 'string' ? options.query.scene : String(options.query.scene)
|
||||
} else if (options.scene) {
|
||||
// 兼容直接从 scene 参数解析
|
||||
sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene)
|
||||
}
|
||||
|
||||
// 从 scene 参数中解析邀请信息
|
||||
if (sceneStr) {
|
||||
// 处理 uid_xxx 格式的邀请码
|
||||
if (sceneStr.startsWith('uid_')) {
|
||||
const inviterId = sceneStr.replace('uid_', '')
|
||||
|
||||
if (inviterId && !isNaN(parseInt(inviterId))) {
|
||||
return {
|
||||
inviter: inviterId,
|
||||
source: 'qrcode',
|
||||
t: Date.now().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理传统的 key=value&key=value 格式
|
||||
const params: InviteParams = {}
|
||||
const pairs = sceneStr.split('&')
|
||||
|
||||
pairs.forEach((pair: string) => {
|
||||
const [key, value] = pair.split('=')
|
||||
if (key && value) {
|
||||
switch (key) {
|
||||
case 'inviter':
|
||||
params.inviter = decodeURIComponent(value)
|
||||
break
|
||||
case 'source':
|
||||
params.source = decodeURIComponent(value)
|
||||
break
|
||||
case 't':
|
||||
params.t = decodeURIComponent(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (params.inviter) {
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
// 从 query 参数中解析邀请信息(处理首页分享链接)
|
||||
if (options.query) {
|
||||
const query = options.query
|
||||
if (query.inviter) {
|
||||
return {
|
||||
inviter: query.inviter,
|
||||
source: query.source || 'share',
|
||||
t: query.t
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧版本
|
||||
if (query.referrer) {
|
||||
return {
|
||||
inviter: query.referrer,
|
||||
source: 'link'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('解析邀请参数失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存邀请信息到本地存储
|
||||
*/
|
||||
export function saveInviteParams(params: InviteParams) {
|
||||
try {
|
||||
const saveData = {
|
||||
...params,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
Taro.setStorageSync('invite_params', saveData)
|
||||
} catch (error) {
|
||||
console.error('保存邀请参数失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地存储的邀请信息
|
||||
*/
|
||||
export function getStoredInviteParams(): InviteParams | null {
|
||||
try {
|
||||
const stored = Taro.getStorageSync('invite_params')
|
||||
|
||||
if (stored && stored.inviter) {
|
||||
// 检查是否过期(24小时)
|
||||
const now = Date.now()
|
||||
const expireTime = 24 * 60 * 60 * 1000 // 24小时
|
||||
|
||||
if (now - stored.timestamp < expireTime) {
|
||||
return {
|
||||
inviter: stored.inviter,
|
||||
source: stored.source || 'unknown',
|
||||
t: stored.t
|
||||
}
|
||||
} else {
|
||||
// 过期则清除
|
||||
clearInviteParams()
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取邀请参数失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除本地存储的邀请信息
|
||||
*/
|
||||
export function clearInviteParams() {
|
||||
try {
|
||||
Taro.removeStorageSync('invite_params')
|
||||
} catch (error) {
|
||||
console.error('清除邀请参数失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邀请关系建立
|
||||
*/
|
||||
export async function handleInviteRelation(userId: number): Promise<boolean> {
|
||||
try {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (!inviteParams || !inviteParams.inviter) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inviterId = parseInt(inviteParams.inviter)
|
||||
if (isNaN(inviterId) || inviterId === userId) {
|
||||
// 邀请人ID无效或自己邀请自己
|
||||
clearInviteParams()
|
||||
return false
|
||||
}
|
||||
|
||||
// 防重复检查:检查是否已经处理过这个邀请关系
|
||||
const relationKey = `invite_relation_${inviterId}_${userId}`
|
||||
const existingRelation = Taro.getStorageSync(relationKey)
|
||||
|
||||
if (existingRelation) {
|
||||
clearInviteParams() // 清除邀请参数
|
||||
return true // 返回true表示关系已存在
|
||||
}
|
||||
|
||||
// 设置API调用超时
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('API调用超时')), 5000)
|
||||
);
|
||||
|
||||
// 使用新的绑定推荐关系接口
|
||||
const apiPromise = bindRefereeRelation({
|
||||
dealerId: inviterId,
|
||||
userId: userId,
|
||||
source: inviteParams.source || 'qrcode',
|
||||
scene: inviteParams.source === 'qrcode' ? `uid_${inviterId}` : `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}`
|
||||
});
|
||||
|
||||
// 等待API调用完成或超时
|
||||
await Promise.race([apiPromise, timeoutPromise]);
|
||||
|
||||
// 标记邀请关系已处理(设置过期时间为7天)
|
||||
Taro.setStorageSync(relationKey, {
|
||||
inviterId,
|
||||
userId,
|
||||
timestamp: Date.now(),
|
||||
source: inviteParams.source || 'qrcode'
|
||||
})
|
||||
|
||||
// 清除本地存储的邀请参数
|
||||
clearInviteParams()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('建立邀请关系失败:', error)
|
||||
|
||||
// 如果是网络错误或超时,不清除邀请参数,允许稍后重试
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (errorMessage.includes('超时') || errorMessage.includes('网络')) {
|
||||
console.log('网络问题,保留邀请参数供稍后重试')
|
||||
return false
|
||||
}
|
||||
|
||||
// 其他错误(如业务逻辑错误),清除邀请参数
|
||||
clearInviteParams()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的邀请
|
||||
*/
|
||||
export function hasPendingInvite(): boolean {
|
||||
const params = getStoredInviteParams()
|
||||
return !!(params && params.inviter)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请来源的显示名称
|
||||
*/
|
||||
export function getSourceDisplayName(source: string): string {
|
||||
const sourceMap: Record<string, string> = {
|
||||
'qrcode': '小程序码',
|
||||
'link': '分享链接',
|
||||
'share': '好友分享',
|
||||
'poster': '海报分享',
|
||||
'unknown': '未知来源'
|
||||
}
|
||||
|
||||
return sourceMap[source] || source
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请码格式
|
||||
*/
|
||||
export function validateInviteCode(scene: string): boolean {
|
||||
try {
|
||||
if (!scene) return false
|
||||
|
||||
// 检查是否包含必要的参数
|
||||
const hasInviter = scene.includes('inviter=')
|
||||
const hasSource = scene.includes('source=')
|
||||
|
||||
return hasInviter && hasSource
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成邀请场景值
|
||||
*/
|
||||
export function generateInviteScene(inviterId: number, source: string): string {
|
||||
const timestamp = Date.now()
|
||||
return `inviter=${inviterId}&source=${source}&t=${timestamp}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计邀请来源
|
||||
*/
|
||||
export function trackInviteSource(source: string, inviterId?: number) {
|
||||
try {
|
||||
// 记录邀请来源统计
|
||||
const trackData = {
|
||||
source,
|
||||
inviterId,
|
||||
timestamp: Date.now(),
|
||||
userAgent: Taro.getSystemInfoSync()
|
||||
}
|
||||
|
||||
// 可以发送到统计服务
|
||||
console.log('邀请来源统计:', trackData)
|
||||
|
||||
// 暂存到本地,后续可批量上报
|
||||
const existingTracks = Taro.getStorageSync('invite_tracks') || []
|
||||
existingTracks.push(trackData)
|
||||
|
||||
// 只保留最近100条记录
|
||||
if (existingTracks.length > 100) {
|
||||
existingTracks.splice(0, existingTracks.length - 100)
|
||||
}
|
||||
|
||||
Taro.setStorageSync('invite_tracks', existingTracks)
|
||||
} catch (error) {
|
||||
console.error('统计邀请来源失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试工具:打印所有邀请相关的存储信息
|
||||
*/
|
||||
export function debugInviteInfo() {
|
||||
try {
|
||||
console.log('=== 邀请参数调试信息 ===')
|
||||
|
||||
// 获取启动参数
|
||||
const launchOptions = Taro.getLaunchOptionsSync()
|
||||
console.log('启动参数:', JSON.stringify(launchOptions, null, 2))
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const storedParams = Taro.getStorageSync('invite_params')
|
||||
console.log('存储的邀请参数:', JSON.stringify(storedParams, null, 2))
|
||||
|
||||
// 获取用户信息
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
const userInfo = Taro.getStorageSync('userInfo')
|
||||
console.log('用户ID:', userId)
|
||||
console.log('用户信息:', JSON.stringify(userInfo, null, 2))
|
||||
|
||||
// 获取邀请统计
|
||||
const inviteTracks = Taro.getStorageSync('invite_tracks')
|
||||
console.log('邀请统计:', JSON.stringify(inviteTracks, null, 2))
|
||||
|
||||
console.log('=== 调试信息结束 ===')
|
||||
|
||||
return {
|
||||
launchOptions,
|
||||
storedParams,
|
||||
userId,
|
||||
userInfo,
|
||||
inviteTracks
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取调试信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并处理当前用户的邀请关系
|
||||
* 用于在用户登录后立即检查是否需要建立邀请关系
|
||||
*/
|
||||
export async function checkAndHandleInviteRelation(): Promise<boolean> {
|
||||
try {
|
||||
// 清理过期的防重记录
|
||||
cleanExpiredInviteRelations()
|
||||
|
||||
// 获取当前用户信息
|
||||
const userInfo = Taro.getStorageSync('userInfo')
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
|
||||
const finalUserId = userId || userInfo?.userId
|
||||
|
||||
if (!finalUserId) {
|
||||
console.log('用户未登录,无法处理邀请关系')
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('使用用户ID处理邀请关系:', finalUserId)
|
||||
|
||||
// 设置整体超时保护
|
||||
const timeoutPromise = new Promise<boolean>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('邀请关系处理整体超时')), 6000)
|
||||
);
|
||||
|
||||
const handlePromise = handleInviteRelation(parseInt(finalUserId));
|
||||
|
||||
return await Promise.race([handlePromise, timeoutPromise]);
|
||||
} catch (error) {
|
||||
console.error('检查邀请关系失败:', error)
|
||||
|
||||
// 记录失败次数,避免无限重试
|
||||
const failKey = 'invite_handle_fail_count'
|
||||
const failCount = Taro.getStorageSync(failKey) || 0
|
||||
|
||||
if (failCount >= 3) {
|
||||
console.log('邀请关系处理失败次数过多,清除邀请参数')
|
||||
clearInviteParams()
|
||||
Taro.removeStorageSync(failKey)
|
||||
} else {
|
||||
Taro.setStorageSync(failKey, failCount + 1)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发邀请关系建立
|
||||
* 用于在特定页面或时机手动建立邀请关系
|
||||
*/
|
||||
export async function manualHandleInviteRelation(userId: number): Promise<boolean> {
|
||||
try {
|
||||
console.log('手动触发邀请关系建立,用户ID:', userId)
|
||||
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (!inviteParams || !inviteParams.inviter) {
|
||||
console.log('没有待处理的邀请参数')
|
||||
return false
|
||||
}
|
||||
|
||||
const result = await handleInviteRelation(userId)
|
||||
|
||||
if (result) {
|
||||
// 显示成功提示
|
||||
Taro.showModal({
|
||||
title: '邀请成功',
|
||||
content: '您已成功加入邀请人的团队!',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('手动处理邀请关系失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的邀请关系防重记录
|
||||
*/
|
||||
export function cleanExpiredInviteRelations() {
|
||||
try {
|
||||
const keys = Taro.getStorageInfoSync().keys
|
||||
const expireTime = 7 * 24 * 60 * 60 * 1000 // 7天
|
||||
const now = Date.now()
|
||||
|
||||
keys.forEach(key => {
|
||||
if (key.startsWith('invite_relation_')) {
|
||||
try {
|
||||
const data = Taro.getStorageSync(key)
|
||||
if (data && data.timestamp && (now - data.timestamp > expireTime)) {
|
||||
Taro.removeStorageSync(key)
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果读取失败,直接删除
|
||||
Taro.removeStorageSync(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('清理过期邀请关系记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接绑定推荐关系
|
||||
* 用于直接调用绑定推荐关系接口
|
||||
*/
|
||||
export async function bindReferee(refereeId: number, userId?: number, source: string = 'qrcode'): Promise<boolean> {
|
||||
try {
|
||||
// 如果没有传入userId,尝试从本地存储获取
|
||||
let targetUserId = userId
|
||||
if (!targetUserId) {
|
||||
const userInfo = Taro.getStorageSync('userInfo')
|
||||
if (userInfo && userInfo.userId) {
|
||||
targetUserId = userInfo.userId
|
||||
} else {
|
||||
throw new Error('无法获取用户ID')
|
||||
}
|
||||
}
|
||||
|
||||
// 防止自己推荐自己
|
||||
if (refereeId === targetUserId) {
|
||||
throw new Error('不能推荐自己')
|
||||
}
|
||||
|
||||
await bindRefereeRelation({
|
||||
dealerId: refereeId,
|
||||
userId: targetUserId,
|
||||
source: source,
|
||||
scene: source === 'qrcode' ? `uid_${refereeId}` : undefined
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('绑定推荐关系失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
31
dict/taro/src/utils/jsonUtils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 判断字符串是否为有效的JSON格式
|
||||
* @param str 要检测的字符串
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isValidJSON(str: string): boolean {
|
||||
if (typeof str !== 'string' || str.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析JSON,失败时返回默认值
|
||||
* @param str JSON字符串
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析结果或默认值
|
||||
*/
|
||||
export function safeJSONParse<T>(str: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
89
dict/taro/src/utils/request.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {BaseUrl, TenantId} from "@/utils/config";
|
||||
|
||||
let baseUrl = BaseUrl
|
||||
|
||||
if(process.env.NODE_ENV === 'development'){
|
||||
baseUrl = 'http://localhost:9200/api'
|
||||
}
|
||||
export function request<T>(options:any) {
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
'TenantId': Taro.getStorageSync('TenantId') || TenantId
|
||||
}
|
||||
if(token){
|
||||
header['Authorization'] = token;
|
||||
}
|
||||
// 发起网络请求
|
||||
return <T>new Promise((resolve, reject) => {
|
||||
Taro.request({
|
||||
url: options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: options.header || header,
|
||||
success: (res) => {
|
||||
resolve(res.data)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
// 可以添加其他Taro.request支持的参数
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function get<T>(url: string,data?: any) {
|
||||
if(url.indexOf('http') === -1){
|
||||
url = baseUrl + url
|
||||
}
|
||||
if(data){
|
||||
url = url + '?' + Object.keys(data).map(key => {
|
||||
return key + '=' + data[key]
|
||||
}).join('&')
|
||||
}
|
||||
return <T>request({
|
||||
url,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
export function post<T>(url:string, data?:any) {
|
||||
if(url.indexOf('http') === -1){
|
||||
url = baseUrl + url
|
||||
}
|
||||
return <T>request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function put<T>(url:string, data?:any) {
|
||||
if(url.indexOf('http') === -1){
|
||||
url = baseUrl + url
|
||||
}
|
||||
return <T>request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function del<T>(url:string,data?: any) {
|
||||
if(url.indexOf('http') === -1){
|
||||
url = baseUrl + url
|
||||
}
|
||||
return <T>request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
request,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del
|
||||
}
|
||||
20
dict/taro/src/utils/server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import {User} from "@/api/system/user/model";
|
||||
|
||||
// 模版套餐ID
|
||||
export const TEMPLATE_ID = 10398;
|
||||
// 服务接口
|
||||
export const SERVER_API_URL = 'https://server.websoft.top/api';
|
||||
// export const SERVER_API_URL = 'http://127.0.0.1:8000/api';
|
||||
/**
|
||||
* 保存用户信息到本地存储
|
||||
* @param token
|
||||
* @param user
|
||||
*/
|
||||
export function saveStorageByLoginUser(token: string, user: User) {
|
||||
Taro.setStorageSync('TenantId',user.tenantId)
|
||||
Taro.setStorageSync('access_token', token)
|
||||
Taro.setStorageSync('UserId', user.userId)
|
||||
Taro.setStorageSync('Phone', user.phone)
|
||||
Taro.setStorageSync('User', user)
|
||||
}
|
||||