feat(admin): 添加管理后台logo文件- 新增128x128尺寸的SVG格式logo文件

- 使用Method Draw工具创建矢量图形
- 包含背景层和图层1的基础结构
- 支持透明背景显示
- 为管理后台界面提供品牌标识- 便于后续UI组件中引用和展示
This commit is contained in:
2025-10-18 09:16:51 +08:00
parent 4c7a7e2452
commit ba6896855a
1195 changed files with 225615 additions and 9 deletions

24
dict/taro/babel.config.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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
View File

@@ -0,0 +1,13 @@
{
"miniprogramRoot": "./",
"projectname": "bszx-react",
"description": "百色中学小程序",
"appid": "touristappid",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram"
}

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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

View File

@@ -0,0 +1,6 @@
function MyGap({height}){
return (
<div style={{height}} className={'bg-gray-100'}></div>
)
}
export default MyGap;

View 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

View 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;

View 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

View 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

View 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

View 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;

View 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;

View 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
};
}

View 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
};
};

View 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
}
}

View 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
}
}

View 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;

View 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;

View 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
};
}

View 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]);
// 获取深色模式LogoAppInfo中无此字段使用普通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
};
};

View 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
}
}

View 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
};
}

View 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
};
};

View 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
View 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>

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '学习'
})

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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>
)
}

View File

@@ -0,0 +1,7 @@
.index {
width: 100vw;
height: 100vh;
background-color: #F3F3F3;
background-repeat: no-repeat;
background-size: 100%;
}

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: 'shopLnk.cn - 数灵云店',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
})

View File

@@ -0,0 +1,4 @@
page {
background: url("https://oss.wsdns.cn/20250414/5bed65bff2f8434995e6c22d67271c77.png");
background-size: cover;
}

View 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

View 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;
}

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '商品列表',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
})

View 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;

View File

@@ -0,0 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '订单列表',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
})

View 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;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '学习'
})

View 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

View 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

View 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;

View 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

View 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

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '我的'
})

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '服务协议与隐私政策',
navigationBarTextStyle: 'black'
})

View File

@@ -0,0 +1,4 @@
.content{
padding: 32px;
line-height: 2.4rem;
}

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '忘记密码',
navigationBarTextStyle: 'black'
})

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '管理员登录',
navigationBarTextStyle: 'black'
})

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '注册账号',
navigationBarTextStyle: 'black'
})

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '服务配置',
navigationBarTextStyle: 'black'
})

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '验证码登录',
navigationBarTextStyle: 'black'
})

View 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

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '快捷登录',
navigationBarTextStyle: 'black'
})

View 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

View 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
});
}
});
}

View 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.';

View 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()}`;
}

View 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
}
}

View 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;
}
}

View 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
}

View 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)
}

Some files were not shown because too many files have changed in this diff Show More