init
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
6
.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// ESLint 检查 .vue 文件需要单独配置编辑器:
|
||||||
|
// https://eslint.vuejs.org/user-guide/#editor-integrations
|
||||||
|
{
|
||||||
|
"extends": ["taro/vue3"]
|
||||||
|
}
|
||||||
|
|
||||||
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
dist/
|
||||||
|
deploy_versions/
|
||||||
|
.temp/
|
||||||
|
.rn_temp/
|
||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
.swc
|
||||||
|
.history/
|
||||||
11
babel.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// babel-preset-taro 更多选项和默认值:
|
||||||
|
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
['taro', {
|
||||||
|
framework: 'vue3',
|
||||||
|
ts: false,
|
||||||
|
compiler: 'vite',
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}
|
||||||
18
components.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
CarForm: typeof import('./src/components/CarForm.vue')['default']
|
||||||
|
NutButton: typeof import('@nutui/nutui-taro')['Button']
|
||||||
|
NutForm: typeof import('@nutui/nutui-taro')['Form']
|
||||||
|
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
|
||||||
|
NutInput: typeof import('@nutui/nutui-taro')['Input']
|
||||||
|
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
|
||||||
|
NutToast: typeof import('@nutui/nutui-taro')['Toast']
|
||||||
|
}
|
||||||
|
}
|
||||||
5
config/dev.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
|
||||||
|
mini: {},
|
||||||
|
h5: {}
|
||||||
|
}
|
||||||
107
config/index.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { defineConfig } from '@tarojs/cli'
|
||||||
|
|
||||||
|
import devConfig from './dev'
|
||||||
|
import prodConfig from './prod'
|
||||||
|
import NutUIResolver from '@nutui/auto-import-resolver'
|
||||||
|
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
|
||||||
|
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||||
|
export default defineConfig(async (merge, { command, mode }) => {
|
||||||
|
const baseConfig = {
|
||||||
|
projectName: 'my-mini-app',
|
||||||
|
date: '2025-9-15',
|
||||||
|
designWidth(input) {
|
||||||
|
// 配置 NutUI 375 尺寸
|
||||||
|
if (input?.file?.replace(/\\+/g, '/').indexOf('@nutui') > -1) {
|
||||||
|
return 375
|
||||||
|
}
|
||||||
|
// 全局使用 Taro 默认的 750 尺寸
|
||||||
|
return 750
|
||||||
|
},
|
||||||
|
deviceRatio: {
|
||||||
|
640: 2.34 / 2,
|
||||||
|
750: 1,
|
||||||
|
375: 2,
|
||||||
|
828: 1.81 / 2
|
||||||
|
},
|
||||||
|
sourceRoot: 'src',
|
||||||
|
outputRoot: 'dist',
|
||||||
|
plugins: ['@tarojs/plugin-html'],
|
||||||
|
defineConstants: {
|
||||||
|
},
|
||||||
|
copy: {
|
||||||
|
patterns: [
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
framework: 'vue3',
|
||||||
|
compiler: {
|
||||||
|
type: 'vite',
|
||||||
|
vitePlugins: [
|
||||||
|
Components({
|
||||||
|
resolvers: [NutUIResolver({ taro: true })]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
mini: {
|
||||||
|
postcss: {
|
||||||
|
pxtransform: {
|
||||||
|
enable: true,
|
||||||
|
config: {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cssModules: {
|
||||||
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
|
config: {
|
||||||
|
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||||
|
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimizeMainPackage: {
|
||||||
|
enable: false
|
||||||
|
},
|
||||||
|
commonChunks: ['runtime', 'vendors', 'taro', 'common']
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
publicPath: '/',
|
||||||
|
staticDirectory: 'static',
|
||||||
|
|
||||||
|
miniCssExtractPluginOption: {
|
||||||
|
ignoreOrder: true,
|
||||||
|
filename: 'css/[name].[hash].css',
|
||||||
|
chunkFilename: 'css/[name].[chunkhash].css'
|
||||||
|
},
|
||||||
|
postcss: {
|
||||||
|
autoprefixer: {
|
||||||
|
enable: true,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
cssModules: {
|
||||||
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
|
config: {
|
||||||
|
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||||
|
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rn: {
|
||||||
|
appName: 'taroDemo',
|
||||||
|
postcss: {
|
||||||
|
cssModules: {
|
||||||
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// 本地开发构建配置(不混淆压缩)
|
||||||
|
return merge({}, baseConfig, devConfig)
|
||||||
|
}
|
||||||
|
// 生产构建配置(默认开启压缩混淆等)
|
||||||
|
return merge({}, baseConfig, prodConfig)
|
||||||
|
})
|
||||||
31
config/prod.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export default {
|
||||||
|
mini: {},
|
||||||
|
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') })
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
748
docs/核心组件文档.md
Normal file
@@ -0,0 +1,748 @@
|
|||||||
|
# 核心组件文档
|
||||||
|
|
||||||
|
## 1. 认证组件 (Auth)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
用户认证和权限管理组件,负责登录状态检查、用户信息存储和token管理。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
// 无直接入参,通过方法调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 用户信息对象
|
||||||
|
{
|
||||||
|
isLogin: boolean, // 是否已登录
|
||||||
|
token: string, // 访问令牌
|
||||||
|
refreshToken: string, // 刷新令牌
|
||||||
|
expiresIn: number, // 过期时间
|
||||||
|
tokenType: string, // 令牌类型
|
||||||
|
userId: string, // 用户ID
|
||||||
|
username: string, // 用户名
|
||||||
|
avatar: string, // 头像
|
||||||
|
loginTime: number, // 登录时间
|
||||||
|
isDriver: boolean, // 是否为司机
|
||||||
|
driverInfo: object // 司机信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
saveUserInfo,
|
||||||
|
getUserInfo,
|
||||||
|
getToken,
|
||||||
|
isLoggedIn,
|
||||||
|
isDriver,
|
||||||
|
getDriverInfo,
|
||||||
|
clearUserInfo,
|
||||||
|
logout,
|
||||||
|
checkLoginAndRedirect
|
||||||
|
} from '../utils/auth.js'
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
saveUserInfo(userInfo)
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
// 已登录逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为司机
|
||||||
|
if (isDriver()) {
|
||||||
|
const driverInfo = getDriverInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
logout()
|
||||||
|
|
||||||
|
// 检查登录并重定向
|
||||||
|
checkLoginAndRedirect()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 用户信息模型
|
||||||
|
interface UserInfo {
|
||||||
|
isLogin: boolean
|
||||||
|
token: string
|
||||||
|
refreshToken: string
|
||||||
|
expiresIn: number
|
||||||
|
tokenType: string
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
avatar: string
|
||||||
|
loginTime: number
|
||||||
|
isDriver?: boolean
|
||||||
|
driverInfo?: DriverInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 司机信息模型
|
||||||
|
interface DriverInfo {
|
||||||
|
id: string
|
||||||
|
driverId: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
status: string
|
||||||
|
data: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 请求组件 (Request)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
统一的HTTP请求组件,处理API调用、token注入、错误处理和用户凭证过期检测。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
// GET请求
|
||||||
|
get(url: string, params?: object, headers?: object)
|
||||||
|
|
||||||
|
// POST请求
|
||||||
|
post(url: string, data?: object, headers?: object)
|
||||||
|
|
||||||
|
// PUT请求
|
||||||
|
put(url: string, data?: object, headers?: object)
|
||||||
|
|
||||||
|
// DELETE请求
|
||||||
|
del(url: string, data?: object, headers?: object)
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
uploadFile(url: string, filePath: string, name?: string, formData?: object, headers?: object)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 标准响应格式
|
||||||
|
{
|
||||||
|
statusCode: number, // HTTP状态码
|
||||||
|
data: object, // 响应数据
|
||||||
|
header: object, // 响应头
|
||||||
|
errMsg: string // 错误信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```javascript
|
||||||
|
import { get, post, put, del, uploadFile } from '../utils/request.js'
|
||||||
|
|
||||||
|
// GET请求
|
||||||
|
const response = await get('/api/users', { page: 1, size: 10 })
|
||||||
|
|
||||||
|
// POST请求
|
||||||
|
const result = await post('/api/login', { username: 'user', password: 'pass' })
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
const uploadResult = await uploadFile('/api/upload', filePath, 'file', { type: 'image' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 请求配置模型
|
||||||
|
interface RequestOptions {
|
||||||
|
url: string
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
data?: object
|
||||||
|
headers?: object
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应模型
|
||||||
|
interface ApiResponse {
|
||||||
|
statusCode: number
|
||||||
|
data: {
|
||||||
|
code?: number
|
||||||
|
msg?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
header: object
|
||||||
|
errMsg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误模型
|
||||||
|
interface ApiError {
|
||||||
|
errMsg: string
|
||||||
|
statusCode?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 位置组件 (Location)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
地理位置获取和权限管理组件,提供位置获取、权限检查和用户引导功能。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
// 无直接入参,通过方法调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 位置信息对象
|
||||||
|
{
|
||||||
|
latitude: number, // 纬度
|
||||||
|
longitude: number, // 经度
|
||||||
|
accuracy: number, // 精度
|
||||||
|
altitude: number, // 海拔
|
||||||
|
speed: number, // 速度
|
||||||
|
timestamp: number // 时间戳
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
getCurrentLocation,
|
||||||
|
checkLocationPermission,
|
||||||
|
requestLocationPermission,
|
||||||
|
getLocationWithPermission
|
||||||
|
} from '../utils/location.js'
|
||||||
|
|
||||||
|
// 获取当前位置
|
||||||
|
const location = await getCurrentLocation()
|
||||||
|
|
||||||
|
// 检查定位权限
|
||||||
|
const hasPermission = await checkLocationPermission()
|
||||||
|
|
||||||
|
// 申请定位权限
|
||||||
|
const granted = await requestLocationPermission()
|
||||||
|
|
||||||
|
// 获取位置并处理权限
|
||||||
|
const location = await getLocationWithPermission()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 位置信息模型
|
||||||
|
interface LocationInfo {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
accuracy: number
|
||||||
|
altitude: number
|
||||||
|
speed: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限状态
|
||||||
|
type PermissionStatus = true | false | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 字典组件 (Dict)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
数据字典管理组件,提供统一的字典数据获取和管理功能。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
// 获取单个字典数据
|
||||||
|
getDictData(type: string)
|
||||||
|
|
||||||
|
// 批量获取字典数据
|
||||||
|
getMultipleDictData(types: string[])
|
||||||
|
|
||||||
|
// 根据值获取文本
|
||||||
|
getDictText(dictData: DictItem[], value: string)
|
||||||
|
|
||||||
|
// 根据文本获取值
|
||||||
|
getDictValue(dictData: DictItem[], text: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 字典数据项
|
||||||
|
{
|
||||||
|
text: string, // 显示文本
|
||||||
|
value: string // 值
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```javascript
|
||||||
|
import { getDictData, DICT_TYPES, getDictText, getDictValue } from '../utils/dictUtils.js'
|
||||||
|
|
||||||
|
// 获取尾挂类型字典
|
||||||
|
const carBackTypes = await getDictData(DICT_TYPES.CAR_BACK_TYPE)
|
||||||
|
|
||||||
|
// 批量获取字典
|
||||||
|
const dicts = await getMultipleDictData([
|
||||||
|
DICT_TYPES.CAR_TYPE,
|
||||||
|
DICT_TYPES.COMPANY_TYPE
|
||||||
|
])
|
||||||
|
|
||||||
|
// 根据值获取文本
|
||||||
|
const text = getDictText(carBackTypes, 'flatbed')
|
||||||
|
|
||||||
|
// 根据文本获取值
|
||||||
|
const value = getDictValue(carBackTypes, '平板')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 字典项模型
|
||||||
|
interface DictItem {
|
||||||
|
text: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典类型常量
|
||||||
|
const DICT_TYPES = {
|
||||||
|
CAR_TYPE: 'car_type',
|
||||||
|
COMPANY_TYPE: 'company_type',
|
||||||
|
CAR_STATUS: 'car_status',
|
||||||
|
DRIVER_STATUS: 'driver_status',
|
||||||
|
GUFEI_ORDER_STATUS: 'gufei_order_status',
|
||||||
|
CAR_BACK_TYPE: 'car_back_type',
|
||||||
|
CAR_PACKAGE_TYPE: 'car_package_type',
|
||||||
|
GPS_STATUS: 'gps_status'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 加密组件 (Crypto)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
数据加密组件,提供AES加密功能用于密码加密。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
aesEncrypt(word: string, keyWord?: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 加密后的字符串
|
||||||
|
string
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```javascript
|
||||||
|
import { aesEncrypt } from '../utils/crypto.js'
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const encryptedPassword = aesEncrypt('123456', 'pigxpigxpigxpigx')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 加密参数模型
|
||||||
|
interface EncryptOptions {
|
||||||
|
word: string // 要加密的内容
|
||||||
|
keyWord?: string // 加密密钥,默认: 'pigxpigxpigxpigx'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API组件 (API)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
API接口管理组件,统一管理所有后端接口调用。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
// 认证API
|
||||||
|
authAPI.login(data: LoginData)
|
||||||
|
|
||||||
|
// 订单API
|
||||||
|
orderAPI.getWaitOrders(driverId: string)
|
||||||
|
orderAPI.createOrder(data: OrderData)
|
||||||
|
orderAPI.getTodayTotal()
|
||||||
|
|
||||||
|
// 车辆API
|
||||||
|
carAPI.getCarPage(params: CarPageParams)
|
||||||
|
|
||||||
|
// 司机API
|
||||||
|
driverAPI.getDriverInfo(userId: string)
|
||||||
|
|
||||||
|
// 文件API
|
||||||
|
fileAPI.upload(filePath: string, formData?: object)
|
||||||
|
|
||||||
|
// 字典API
|
||||||
|
dictAPI.getDictData(type: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 标准API响应
|
||||||
|
{
|
||||||
|
statusCode: number,
|
||||||
|
data: object
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```javascript
|
||||||
|
import { authAPI, orderAPI, carAPI, driverAPI, fileAPI, dictAPI } from '../api/index.js'
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const loginResult = await authAPI.login({ username: 'user', password: 'pass' })
|
||||||
|
|
||||||
|
// 获取待完成车次
|
||||||
|
const orders = await orderAPI.getWaitOrders('driver123')
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
const result = await orderAPI.createOrder(orderData)
|
||||||
|
|
||||||
|
// 获取今日统计
|
||||||
|
const stats = await orderAPI.getTodayTotal()
|
||||||
|
|
||||||
|
// 获取车辆数据
|
||||||
|
const cars = await carAPI.getCarPage({ current: 1, size: 10 })
|
||||||
|
|
||||||
|
// 获取司机信息
|
||||||
|
const driver = await driverAPI.getDriverInfo('user123')
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
const uploadResult = await fileAPI.upload(filePath)
|
||||||
|
|
||||||
|
// 获取字典数据
|
||||||
|
const dictData = await dictAPI.getDictData('car_type')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 登录数据模型
|
||||||
|
interface LoginData {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单数据模型
|
||||||
|
interface OrderData {
|
||||||
|
driverId: string
|
||||||
|
carId: string
|
||||||
|
totalWeight: number
|
||||||
|
companyId: string
|
||||||
|
carBackType: string
|
||||||
|
gufeiType: string
|
||||||
|
gufeiPackage: string
|
||||||
|
localImg: string
|
||||||
|
longitude: string
|
||||||
|
latitude: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 车辆分页参数模型
|
||||||
|
interface CarPageParams {
|
||||||
|
current: number
|
||||||
|
size: number
|
||||||
|
driverId: string
|
||||||
|
companyId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 今日统计模型
|
||||||
|
interface TodayStats {
|
||||||
|
count: number // 收货客户数
|
||||||
|
planWeight: number // 预计重量
|
||||||
|
realWeight: number // 实际重量
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 车辆表单组件 (CarForm)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
车辆信息录入表单组件,支持照片上传、位置获取和订单提交。
|
||||||
|
|
||||||
|
### 入参
|
||||||
|
```javascript
|
||||||
|
// 组件Props
|
||||||
|
{
|
||||||
|
visible: boolean, // 是否显示
|
||||||
|
carData: CarData, // 车辆数据
|
||||||
|
companyId: string // 供应商ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出参
|
||||||
|
```javascript
|
||||||
|
// 事件
|
||||||
|
{
|
||||||
|
submit: (formData) => void, // 表单提交事件
|
||||||
|
refresh: () => void // 刷新事件
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<CarForm
|
||||||
|
v-model:visible="showCarForm"
|
||||||
|
:car-data="selectedCarData"
|
||||||
|
:company-id="scannedCompanyId"
|
||||||
|
@submit="handleCarFormSubmit"
|
||||||
|
@refresh="handleCarFormRefresh"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CarForm from '../components/CarForm.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CarForm
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showCarForm: false,
|
||||||
|
selectedCarData: {
|
||||||
|
carNum: '',
|
||||||
|
carType: '',
|
||||||
|
id: '',
|
||||||
|
carWeight: '',
|
||||||
|
carVolume: ''
|
||||||
|
},
|
||||||
|
scannedCompanyId: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleCarFormSubmit(formData) {
|
||||||
|
console.log('表单提交:', formData)
|
||||||
|
},
|
||||||
|
handleCarFormRefresh() {
|
||||||
|
console.log('刷新数据')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
```javascript
|
||||||
|
// 车辆数据模型
|
||||||
|
interface CarData {
|
||||||
|
carNum: string // 车牌号
|
||||||
|
carType: string // 车辆类型
|
||||||
|
id: string // 车辆ID
|
||||||
|
carWeight: string // 车辆重量
|
||||||
|
carVolume: string // 车辆容积
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单数据模型
|
||||||
|
interface FormData {
|
||||||
|
carNum: string // 车牌号
|
||||||
|
carBackType: string // 尾挂类型
|
||||||
|
photos: string[] // 照片URL数组
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 使用示例
|
||||||
|
|
||||||
|
### 完整登录流程
|
||||||
|
```javascript
|
||||||
|
import { authAPI, driverAPI } from '../api/index.js'
|
||||||
|
import { saveUserInfo } from '../utils/auth.js'
|
||||||
|
import { aesEncrypt } from '../utils/crypto.js'
|
||||||
|
|
||||||
|
// 登录处理
|
||||||
|
const handleLogin = async (username, password) => {
|
||||||
|
try {
|
||||||
|
// 加密密码
|
||||||
|
const encryptedPassword = aesEncrypt(password)
|
||||||
|
|
||||||
|
// 调用登录API
|
||||||
|
const response = await authAPI.login({
|
||||||
|
username,
|
||||||
|
password: encryptedPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
// 保存用户信息
|
||||||
|
const userInfo = {
|
||||||
|
isLogin: true,
|
||||||
|
token: response.data.access_token,
|
||||||
|
userId: response.data.userId,
|
||||||
|
username: username
|
||||||
|
}
|
||||||
|
|
||||||
|
saveUserInfo(userInfo)
|
||||||
|
|
||||||
|
// 检查是否为司机
|
||||||
|
const driverResponse = await driverAPI.getDriverInfo(userInfo.userId)
|
||||||
|
if (driverResponse.statusCode === 200) {
|
||||||
|
userInfo.isDriver = true
|
||||||
|
userInfo.driverInfo = driverResponse.data
|
||||||
|
saveUserInfo(userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整订单创建流程
|
||||||
|
```javascript
|
||||||
|
import { orderAPI, fileAPI } from '../api/index.js'
|
||||||
|
import { getLocationWithPermission } from '../utils/location.js'
|
||||||
|
import { getUserInfo } from '../utils/auth.js'
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
const createOrder = async (formData, carData, companyId) => {
|
||||||
|
try {
|
||||||
|
// 获取位置
|
||||||
|
const location = await getLocationWithPermission()
|
||||||
|
|
||||||
|
// 获取司机信息
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
const driverInfo = userInfo.driverInfo.data
|
||||||
|
|
||||||
|
// 构建订单数据
|
||||||
|
const orderData = {
|
||||||
|
driverId: driverInfo.id,
|
||||||
|
carId: carData.id,
|
||||||
|
totalWeight: 1,
|
||||||
|
companyId: companyId,
|
||||||
|
carBackType: formData.carBackType,
|
||||||
|
gufeiType: '',
|
||||||
|
gufeiPackage: '',
|
||||||
|
localImg: formData.photos.join(','),
|
||||||
|
longitude: location.longitude.toString(),
|
||||||
|
latitude: location.latitude.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交订单
|
||||||
|
const response = await orderAPI.createOrder(orderData)
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
console.log('订单创建成功')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('订单创建失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 错误处理
|
||||||
|
|
||||||
|
### 统一错误处理
|
||||||
|
```javascript
|
||||||
|
// 用户凭证过期处理
|
||||||
|
if (response.data && response.data.code === 1 && response.data.msg === "用户凭证已过期") {
|
||||||
|
wx.showModal({
|
||||||
|
title: '登录过期',
|
||||||
|
content: '您的登录已过期,请重新登录',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '重新登录',
|
||||||
|
success: () => {
|
||||||
|
// 清除本地存储
|
||||||
|
wx.removeStorageSync('userInfo')
|
||||||
|
wx.removeStorageSync('isLogin')
|
||||||
|
wx.removeStorageSync('token')
|
||||||
|
|
||||||
|
// 跳转登录页
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '/pages/login/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 网络错误处理
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
const response = await api.getData()
|
||||||
|
// 处理成功响应
|
||||||
|
} catch (error) {
|
||||||
|
if (error.errMsg.includes('timeout')) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请求超时',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
wx.showToast({
|
||||||
|
title: '网络错误',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 调试功能
|
||||||
|
|
||||||
|
### 开发环境配置
|
||||||
|
```javascript
|
||||||
|
// 调试环境下的默认账号密码
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
loginForm.username = 'driver'
|
||||||
|
loginForm.password = '123456'
|
||||||
|
console.log('调试环境:已设置默认账号密码')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志输出
|
||||||
|
```javascript
|
||||||
|
// 请求日志
|
||||||
|
console.log(`API请求成功: ${method} ${url}`, response)
|
||||||
|
|
||||||
|
// 调试信息
|
||||||
|
console.log('开始获取待完成车次数据,司机ID:', driverId)
|
||||||
|
console.log('今日统计API响应:', response)
|
||||||
|
console.log('获取位置成功:', location)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 性能优化
|
||||||
|
|
||||||
|
### 请求优化
|
||||||
|
- 统一请求超时设置
|
||||||
|
- 自动token注入
|
||||||
|
- 请求去重处理
|
||||||
|
|
||||||
|
### 缓存策略
|
||||||
|
- 用户信息本地存储
|
||||||
|
- 字典数据缓存
|
||||||
|
- 位置信息缓存
|
||||||
|
|
||||||
|
### 错误恢复
|
||||||
|
- 自动重试机制
|
||||||
|
- 降级处理
|
||||||
|
- 用户引导
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 安全考虑
|
||||||
|
|
||||||
|
### 数据加密
|
||||||
|
- 密码AES加密
|
||||||
|
- 敏感信息保护
|
||||||
|
- 传输安全
|
||||||
|
|
||||||
|
### 权限控制
|
||||||
|
- 登录状态检查
|
||||||
|
- 角色权限验证
|
||||||
|
- 接口访问控制
|
||||||
|
|
||||||
|
### 数据验证
|
||||||
|
- 输入参数验证
|
||||||
|
- 响应数据校验
|
||||||
|
- 异常数据处理
|
||||||
14326
package-lock.json
generated
Normal file
78
package.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "my-mini-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"templateInfo": {
|
||||||
|
"name": "vue3-NutUI",
|
||||||
|
"typescript": false,
|
||||||
|
"css": "Less",
|
||||||
|
"framework": "Vue3"
|
||||||
|
},
|
||||||
|
"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: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:quickapp": "npm run build:quickapp -- --watch"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"last 3 versions",
|
||||||
|
"Android >= 4.1",
|
||||||
|
"ios >= 8"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.7.7",
|
||||||
|
"@nutui/icons-vue": "^0.1.1",
|
||||||
|
"@nutui/nutui-taro": "^4.2.8",
|
||||||
|
"@tarojs/components": "4.1.6",
|
||||||
|
"@tarojs/helper": "4.1.6",
|
||||||
|
"@tarojs/plugin-framework-vue3": "4.1.6",
|
||||||
|
"@tarojs/plugin-html": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-alipay": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-h5": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-jd": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-qq": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-swan": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-tt": "4.1.6",
|
||||||
|
"@tarojs/plugin-platform-weapp": "4.1.6",
|
||||||
|
"@tarojs/runtime": "4.1.6",
|
||||||
|
"@tarojs/shared": "4.1.6",
|
||||||
|
"@tarojs/taro": "4.1.6",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"echarts-for-weixin": "^1.0.2",
|
||||||
|
"vue": "^3.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.8.0",
|
||||||
|
"@nutui/auto-import-resolver": "^1.0.0",
|
||||||
|
"@tarojs/cli": "4.1.6",
|
||||||
|
"@tarojs/vite-runner": "4.1.6",
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||||
|
"babel-preset-taro": "4.1.6",
|
||||||
|
"eslint": "^8.12.0",
|
||||||
|
"eslint-config-taro": "4.1.6",
|
||||||
|
"eslint-plugin-vue": "^8.0.0",
|
||||||
|
"less": "^4.1.3",
|
||||||
|
"postcss": "^8.4.18",
|
||||||
|
"stylelint": "^14.4.0",
|
||||||
|
"terser": "^5.16.8",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"vite": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
project.config.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"miniprogramRoot": "./dist",
|
||||||
|
"projectname": "my-mini-app",
|
||||||
|
"description": "",
|
||||||
|
"appid": "touristappid",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"es6": false,
|
||||||
|
"enhance": false,
|
||||||
|
"compileHotReLoad": false,
|
||||||
|
"postcss": false,
|
||||||
|
"minified": false
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram"
|
||||||
|
}
|
||||||
13
project.tt.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"miniprogramRoot": "./",
|
||||||
|
"projectname": "my-mini-app",
|
||||||
|
"description": "",
|
||||||
|
"appid": "touristappid",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"es6": false,
|
||||||
|
"postcss": false,
|
||||||
|
"minified": false
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram"
|
||||||
|
}
|
||||||
64
resize_tabbar_icons.ps1
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 调整 TabBar 图标尺寸为 28x28 像素
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$sourceDir = "src/assets/images"
|
||||||
|
$targetSize = 28
|
||||||
|
|
||||||
|
# 需要调整的图标文件列表
|
||||||
|
$iconFiles = @(
|
||||||
|
"shouye.png",
|
||||||
|
"shouye_active.png",
|
||||||
|
"cheliang.png",
|
||||||
|
"cheliang_active.png",
|
||||||
|
"tongji.png",
|
||||||
|
"tongji_active.png",
|
||||||
|
"wode.png",
|
||||||
|
"wode_active.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($iconFile in $iconFiles) {
|
||||||
|
$sourcePath = Join-Path $sourceDir $iconFile
|
||||||
|
$backupPath = Join-Path $sourceDir ($iconFile -replace '\.png$', '_backup.png')
|
||||||
|
|
||||||
|
if (Test-Path $sourcePath) {
|
||||||
|
try {
|
||||||
|
# 创建备份
|
||||||
|
Copy-Item $sourcePath $backupPath -Force
|
||||||
|
Write-Host "已备份: $iconFile"
|
||||||
|
|
||||||
|
# 加载原始图片
|
||||||
|
$originalImage = [System.Drawing.Image]::FromFile((Resolve-Path $sourcePath))
|
||||||
|
|
||||||
|
# 创建新的 28x28 图片
|
||||||
|
$newImage = New-Object System.Drawing.Bitmap($targetSize, $targetSize)
|
||||||
|
$graphics = [System.Drawing.Graphics]::FromImage($newImage)
|
||||||
|
|
||||||
|
# 设置高质量缩放
|
||||||
|
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||||
|
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
||||||
|
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||||
|
$graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
||||||
|
|
||||||
|
# 绘制缩放后的图片
|
||||||
|
$graphics.DrawImage($originalImage, 0, 0, $targetSize, $targetSize)
|
||||||
|
|
||||||
|
# 保存新图片
|
||||||
|
$newImage.Save($sourcePath, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
|
||||||
|
# 释放资源
|
||||||
|
$graphics.Dispose()
|
||||||
|
$newImage.Dispose()
|
||||||
|
$originalImage.Dispose()
|
||||||
|
|
||||||
|
Write-Host "已调整: $iconFile -> 28x28 像素"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "调整失败: $iconFile - $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "文件不存在: $iconFile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "所有 TabBar 图标调整完成!"
|
||||||
94
src/api/index.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* API接口配置
|
||||||
|
*/
|
||||||
|
import { get, post, put, uploadFile } from '../utils/request.js'
|
||||||
|
|
||||||
|
// 认证相关API
|
||||||
|
export const authAPI = {
|
||||||
|
// 登录
|
||||||
|
login: (data) => post('/api/auth/oauth2/token?grant_type=password&scope=server', data, {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': 'Basic dGVzdDp0ZXN0'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典相关API
|
||||||
|
export const dictAPI = {
|
||||||
|
// 获取字典数据
|
||||||
|
getDictData: (type) => get(`/api/admin/dict/type/${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单相关API
|
||||||
|
export const orderAPI = {
|
||||||
|
// 获取订单分页数据
|
||||||
|
getOrderPage: (params) => get('/api/twopoint/zhxyOrder/page', params),
|
||||||
|
|
||||||
|
// 获取待完成车次
|
||||||
|
getWaitOrders: (driverId) => get(`/api/twopoint/zhxyOrder/waitOrder?driverId=${driverId}`),
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
createOrder: (data) => post('/api/twopoint/zhxyOrder', data),
|
||||||
|
|
||||||
|
// 保存订单(微信端)
|
||||||
|
saveByWx: (data) => post('/api/twopoint/zhxyOrder/saveByWx', data),
|
||||||
|
|
||||||
|
// 获取今日统计
|
||||||
|
getTodayTotal: () => get('/api/twopoint/zhxyOrder/getTodayTotal'),
|
||||||
|
|
||||||
|
// 获取图表数据
|
||||||
|
getChart: () => get('/api/twopoint/zhxyOrder/getChart'),
|
||||||
|
|
||||||
|
// 根据订单ID获取质检信息
|
||||||
|
getCheckInfoByOrderId: (orderId) => get(`/api/twopoint/zhxyOrderCheck/getByOrderId`, { orderId }),
|
||||||
|
|
||||||
|
// 司机确认质检结果
|
||||||
|
driverConfirm: (data) => post('/api/twopoint/zhxyOrderCheck/driverConfirm', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 车辆相关API
|
||||||
|
export const carAPI = {
|
||||||
|
// 获取车辆分页数据(支持orderType和orderNum参数)
|
||||||
|
getCarPage: (params) => get('/api/twopoint/zhxyCar/getZhxyCarPageByWx', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供应商相关API
|
||||||
|
export const companyAPI = {
|
||||||
|
// 根据供应商短号获取供应商信息
|
||||||
|
getByCompanyNo: (companyNo) => get(`/api/twopoint/zhxyCompany/getByCompanyNo?companyNo=${companyNo}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 司机相关API
|
||||||
|
export const driverAPI = {
|
||||||
|
// 获取司机信息
|
||||||
|
getDriverInfo: (userId) => get(`/api/twopoint/zhxyDriver/getDriverInfo?userId=${userId}`),
|
||||||
|
|
||||||
|
// 司机注册
|
||||||
|
registerDriver: (data) => post('/api/twopoint/zhxyDriver/registerByWx', data),
|
||||||
|
|
||||||
|
// 更新司机信息
|
||||||
|
updateDriver: (data) => put('/api/twopoint/zhxyDriver', data),
|
||||||
|
|
||||||
|
// 更新司机头像
|
||||||
|
updateDriverHead: (data) => post('/api/twopoint/zhxyDriver/updateHead', data),
|
||||||
|
|
||||||
|
// 上传身份证正面
|
||||||
|
updateIdCard: (data) => post('/api/twopoint/zhxyDriver/updateIdCard', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传API
|
||||||
|
export const fileAPI = {
|
||||||
|
// 上传文件
|
||||||
|
upload: (filePath, formData = {}) => uploadFile('/api/admin/sys-file/upload', filePath, 'file', formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户相关API
|
||||||
|
export const userAPI = {
|
||||||
|
// 修改密码
|
||||||
|
changePassword: (data) => put('/api/admin/user/personal/password', data),
|
||||||
|
|
||||||
|
// 用户注册
|
||||||
|
registerUser: (data) => post('/api/admin/register/user', data),
|
||||||
|
|
||||||
|
// 发送短信验证码
|
||||||
|
sendSmsCode: (data) => get(`/api/twopoint/zhxyDriver/getCode?tel=${data.phone}&type=1`)
|
||||||
|
}
|
||||||
51
src/app.config.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default {
|
||||||
|
pages: [
|
||||||
|
'pages/index/index',
|
||||||
|
'pages/vehicle/index',
|
||||||
|
'pages/statistics/index',
|
||||||
|
'pages/history/index',
|
||||||
|
'pages/profile/index',
|
||||||
|
'pages/login/index',
|
||||||
|
'pages/register/index',
|
||||||
|
'pages/password/index',
|
||||||
|
'pages/realname/index'
|
||||||
|
],
|
||||||
|
tabBar: {
|
||||||
|
color: '#999999',
|
||||||
|
selectedColor: '#1976d2',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderStyle: 'white',
|
||||||
|
list: [ // TabBar 列表,最少2项,最多5项
|
||||||
|
{
|
||||||
|
pagePath: 'pages/index/index',
|
||||||
|
text: '首页',
|
||||||
|
iconPath: "assets/images/shouye.png",
|
||||||
|
selectedIconPath: "assets/images/shouye_active.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pagePath: 'pages/statistics/index',
|
||||||
|
text: '统计',
|
||||||
|
iconPath: "assets/images/tongji.png",
|
||||||
|
selectedIconPath: "assets/images/tongji_active.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pagePath: 'pages/profile/index',
|
||||||
|
text: '我的',
|
||||||
|
iconPath: "assets/images/wode.png",
|
||||||
|
selectedIconPath: "assets/images/wode_active.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
backgroundTextStyle: 'light',
|
||||||
|
navigationBarBackgroundColor: '#fff',
|
||||||
|
navigationBarTitleText: 'WeChat',
|
||||||
|
navigationBarTextStyle: 'black'
|
||||||
|
},
|
||||||
|
permission: {
|
||||||
|
"scope.userLocation": {
|
||||||
|
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/app.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import '@nutui/nutui-taro/dist/style.css'
|
||||||
|
import '@nutui/icons-vue/dist/style_iconfont.css'
|
||||||
|
import { resetRedirectState } from './utils/auth.js'
|
||||||
|
|
||||||
|
import './app.less'
|
||||||
|
|
||||||
|
// 检查定位权限状态(不强制申请)
|
||||||
|
function checkLocationPermissionStatus() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
wx.getSetting({
|
||||||
|
success: (res) => {
|
||||||
|
const hasPermission = res.authSetting['scope.userLocation'] === true
|
||||||
|
resolve(hasPermission)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = createApp({
|
||||||
|
onLaunch(options) {
|
||||||
|
// 重置跳转状态
|
||||||
|
resetRedirectState()
|
||||||
|
// 不再强制检查登录状态,允许未登录用户查看功能
|
||||||
|
},
|
||||||
|
onShow(options) {
|
||||||
|
// 应用显示时的逻辑(不再强制申请定位权限)
|
||||||
|
},
|
||||||
|
// 入口组件不需要实现 render 方法,即使实现了也会被 taro 所覆盖
|
||||||
|
})
|
||||||
|
|
||||||
|
export default App
|
||||||
52
src/app.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"pages/index/index",
|
||||||
|
"pages/vehicle/index",
|
||||||
|
"pages/statistics/index",
|
||||||
|
"pages/profile/index",
|
||||||
|
"pages/login/index",
|
||||||
|
"pages/register/index",
|
||||||
|
"pages/password/index",
|
||||||
|
"pages/identityNew/index"
|
||||||
|
],
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#999999",
|
||||||
|
"selectedColor": "#1976d2",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"borderStyle": "white",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/index/index",
|
||||||
|
"text": "首页",
|
||||||
|
"iconPath": "assets/images/shouye.png",
|
||||||
|
"selectedIconPath": "assets/images/shouye_active.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/statistics/index",
|
||||||
|
"text": "统计",
|
||||||
|
"iconPath": "assets/images/tongji.png",
|
||||||
|
"selectedIconPath": "assets/images/tongji_active.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/profile/index",
|
||||||
|
"text": "我的",
|
||||||
|
"iconPath": "assets/images/wode.png",
|
||||||
|
"selectedIconPath": "assets/images/wode_active.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"backgroundTextStyle": "light",
|
||||||
|
"navigationBarBackgroundColor": "#fff",
|
||||||
|
"navigationBarTitleText": "WeChat",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"scope.userLocation": {
|
||||||
|
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requiredPrivateInfos": [
|
||||||
|
"getLocation"
|
||||||
|
]
|
||||||
|
}
|
||||||
151
src/app.less
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
@import '@nutui/nutui-taro/dist/style.css';
|
||||||
|
@import '@nutui/icons-vue/dist/style_iconfont.css';
|
||||||
|
|
||||||
|
/* 全局字体设置 */
|
||||||
|
page {
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用字体设置 */
|
||||||
|
* {
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保所有文本元素都使用全局字体 */
|
||||||
|
text,
|
||||||
|
view,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
label {
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NutUI 组件字体覆盖 */
|
||||||
|
.nut-button,
|
||||||
|
.nut-input,
|
||||||
|
.nut-form-item,
|
||||||
|
.nut-toast,
|
||||||
|
.nut-popup,
|
||||||
|
.nut-avatar {
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保图标字体不受影响 */
|
||||||
|
.nut-icon,
|
||||||
|
.nut-iconfont {
|
||||||
|
font-family: "nutui-iconfont" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TabBar 图标大小设置 */
|
||||||
|
.tabbar-icon {
|
||||||
|
width: 20rpx !important;
|
||||||
|
height: 20rpx !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 微信小程序 TabBar 样式覆盖 */
|
||||||
|
.taro-tabbar__icon {
|
||||||
|
width: 20rpx !important;
|
||||||
|
height: 20rpx !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taro-tabbar__icon img {
|
||||||
|
width: 20rpx !important;
|
||||||
|
height: 20rpx !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复真机input框样式问题 */
|
||||||
|
input {
|
||||||
|
/* 重置默认样式 */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
/* 修复真机显示问题 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: 80rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
/* 修复字体和间距 */
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
/* 修复内边距 */
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
/* 修复边框 */
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
|
||||||
|
/* 修复背景 */
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复占位符 */
|
||||||
|
input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复焦点状态 */
|
||||||
|
input:focus {
|
||||||
|
border-color: #1976d2;
|
||||||
|
background-color: #fff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复禁用状态 */
|
||||||
|
input:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复特定类型的input */
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="tel"],
|
||||||
|
input[type="email"] {
|
||||||
|
/* 确保所有input类型都应用修复样式 */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复NutUI input组件 */
|
||||||
|
.nut-input {
|
||||||
|
.nut-input__inner {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/images/cheliang.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/cheliang_active.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/images/icon-1.png
Normal file
|
After Width: | Height: | Size: 968 B |
13
src/assets/images/icon-1.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="48" height="48" rx="12" fill="#E3F2FD"/>
|
||||||
|
<rect x="14" y="12" width="20" height="24" rx="2" fill="#1976D2"/>
|
||||||
|
<rect x="16" y="14" width="16" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="16" y="17" width="12" height="1" rx="0.5" fill="white"/>
|
||||||
|
<rect x="16" y="19" width="14" height="1" rx="0.5" fill="white"/>
|
||||||
|
<rect x="16" y="21" width="10" height="1" rx="0.5" fill="white"/>
|
||||||
|
<rect x="16" y="23" width="16" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="16" y="26" width="8" height="1" rx="0.5" fill="white"/>
|
||||||
|
<rect x="16" y="28" width="12" height="1" rx="0.5" fill="white"/>
|
||||||
|
<circle cx="18" cy="32" r="2" fill="#42A5F5"/>
|
||||||
|
<path d="M16 32h4v4h-4v-4z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 793 B |
BIN
src/assets/images/icon-2.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
18
src/assets/images/icon-2.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="48" height="48" rx="12" fill="#E8F5E8"/>
|
||||||
|
<rect x="16" y="14" width="16" height="20" rx="2" fill="#4CAF50"/>
|
||||||
|
<rect x="18" y="16" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="19" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="22" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="25" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="28" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="31" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<circle cx="20" cy="17" r="1" fill="#4CAF50"/>
|
||||||
|
<circle cx="20" cy="20" r="1" fill="#4CAF50"/>
|
||||||
|
<circle cx="20" cy="23" r="1" fill="#4CAF50"/>
|
||||||
|
<circle cx="20" cy="26" r="1" fill="#4CAF50"/>
|
||||||
|
<circle cx="20" cy="29" r="1" fill="#4CAF50"/>
|
||||||
|
<circle cx="20" cy="32" r="1" fill="#4CAF50"/>
|
||||||
|
<path d="M20 36h8v2h-8v-2z" fill="#66BB6A"/>
|
||||||
|
<circle cx="18" cy="37" r="1" fill="#66BB6A"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1014 B |
BIN
src/assets/images/icon-3.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
src/assets/images/icon-3.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="48" height="48" rx="12" fill="#FFF3E0"/>
|
||||||
|
<rect x="16" y="12" width="16" height="24" rx="2" fill="#FF9800"/>
|
||||||
|
<rect x="18" y="14" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="17" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="20" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="23" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="26" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<rect x="18" y="29" width="12" height="2" rx="1" fill="white"/>
|
||||||
|
<circle cx="20" cy="15" r="1" fill="#FF9800"/>
|
||||||
|
<circle cx="20" cy="18" r="1" fill="#FF9800"/>
|
||||||
|
<circle cx="20" cy="21" r="1" fill="#FF9800"/>
|
||||||
|
<circle cx="20" cy="24" r="1" fill="#FF9800"/>
|
||||||
|
<circle cx="20" cy="27" r="1" fill="#FF9800"/>
|
||||||
|
<circle cx="20" cy="30" r="1" fill="#FF9800"/>
|
||||||
|
<path d="M20 36h8v2h-8v-2z" fill="#FFB74D"/>
|
||||||
|
<circle cx="18" cy="37" r="1" fill="#FFB74D"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1014 B |
BIN
src/assets/images/shouye.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/images/shouye_active.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/images/tongji.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/tongji_active.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/images/wode.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/images/wode_active.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
709
src/components/CarForm.vue
Normal file
@@ -0,0 +1,709 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 车辆信息表单弹窗 -->
|
||||||
|
<view v-if="visible" class="car-form-popup">
|
||||||
|
<view class="popup-mask" @click="closeForm"></view>
|
||||||
|
<view class="car-form-container">
|
||||||
|
<view class="form-header">
|
||||||
|
<text class="form-title">车辆信息录入</text>
|
||||||
|
<view class="close-btn" @click="closeForm">×</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-content">
|
||||||
|
<!-- 车牌号 -->
|
||||||
|
<view class="form-item" v-if="safeOrderType === ORDER_TYPES.COMPANY">
|
||||||
|
<text class="form-label">车牌号 <text class="required">*</text></text>
|
||||||
|
<input class="form-input disabled" :value="safeFormData.carNum" disabled placeholder="车牌号" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 重量输入 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">重量(千克) <text class="required">*</text></text>
|
||||||
|
<nut-input class="form-input" v-model="safeFormData.weight" type="number" placeholder="请输入重量"
|
||||||
|
:min="1" @input="onWeightInput" />
|
||||||
|
<text class="form-tip">最低1千克,不能小于1</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 照片上传 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">车辆照片 <text class="required">*</text></text>
|
||||||
|
<view class="photo-upload">
|
||||||
|
<view class="photo-list">
|
||||||
|
<view v-for="(photo, index) in safeFormData.photos" :key="index" class="photo-item">
|
||||||
|
<image :src="photo" class="photo-image" />
|
||||||
|
<text class="delete-btn" @click="removePhoto(index)">×</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="safeFormData.photos.length < 5" class="add-photo-btn" @click="choosePhoto">
|
||||||
|
<text class="add-icon">+</text>
|
||||||
|
<text class="add-text">添加照片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="photo-tip">请选择5张照片</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-actions">
|
||||||
|
<button class="btn-cancel" @click="closeForm">取消</button>
|
||||||
|
<button class="btn-submit" @click="submitForm">提交</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { getDictData, DICT_TYPES } from '../utils/dictUtils.js'
|
||||||
|
import { fileAPI, orderAPI } from '../api/index.js'
|
||||||
|
import { getLocationWithPermission, getLocationForOrder } from '../utils/location.js'
|
||||||
|
import { getUserInfo } from '../utils/auth.js'
|
||||||
|
import { API_CONFIG } from '../utils/request.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CarForm',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
carData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
companyId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
orderNum: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
orderType: {
|
||||||
|
type: String,
|
||||||
|
default: 'company' // 默认为公司类型
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:visible', 'submit', 'close'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
// 常量定义
|
||||||
|
const ORDER_TYPES = {
|
||||||
|
ORDER: 'order',
|
||||||
|
COMPANY: 'company'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用动态的API地址
|
||||||
|
const API_PREFIX = `${API_CONFIG.BASE_URL}api`
|
||||||
|
|
||||||
|
// 确保props有默认值
|
||||||
|
const safeOrderType = computed(() => props.orderType || ORDER_TYPES.COMPANY)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
carNum: '',
|
||||||
|
weight: '',
|
||||||
|
photos: []
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const safeFormData = computed(() => {
|
||||||
|
return {
|
||||||
|
carNum: (formData.value && formData.value.carNum) || '',
|
||||||
|
weight: (formData.value && formData.value.weight) || '',
|
||||||
|
photos: (formData.value && formData.value.photos) || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 重量输入处理
|
||||||
|
const onWeightInput = (e) => {
|
||||||
|
const value = e.detail.value
|
||||||
|
const numValue = parseFloat(value)
|
||||||
|
|
||||||
|
// 如果输入值小于1,设置为1
|
||||||
|
if (numValue < 1) {
|
||||||
|
formData.value.weight = '1'
|
||||||
|
wx.showToast({
|
||||||
|
title: '重量不能小于1千克',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
formData.value.weight = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const updateFormData = (field, value) => {
|
||||||
|
if (!formData.value) {
|
||||||
|
formData.value = {
|
||||||
|
carNum: '',
|
||||||
|
weight: '',
|
||||||
|
photos: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.value[field] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeForm = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
emit('close')
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
carNum: '',
|
||||||
|
weight: '',
|
||||||
|
photos: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const choosePhoto = () => {
|
||||||
|
const currentPhotos = (formData.value && formData.value.photos) || []
|
||||||
|
const remainingCount = 5 - currentPhotos.length
|
||||||
|
|
||||||
|
if (remainingCount <= 0) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '最多只能上传5张照片',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 chooseMedia API,支持一次完成选择和上传
|
||||||
|
wx.chooseMedia({
|
||||||
|
count: remainingCount,
|
||||||
|
mediaType: ['image'],
|
||||||
|
sourceType: ['camera'],
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
camera: 'back',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!formData.value) {
|
||||||
|
formData.value = {
|
||||||
|
carNum: '',
|
||||||
|
weight: '',
|
||||||
|
photos: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示上传进度
|
||||||
|
wx.showLoading({
|
||||||
|
title: '上传中...',
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取临时文件路径
|
||||||
|
const tempFilePaths = res.tempFiles.map(file => file.tempFilePath)
|
||||||
|
|
||||||
|
// 上传每张图片
|
||||||
|
await uploadPhotos(tempFilePaths, currentPhotos)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('选择照片失败:', error)
|
||||||
|
wx.showToast({
|
||||||
|
title: '选择照片失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传图片到服务器
|
||||||
|
const uploadPhotos = async (filePaths, existingPhotos) => {
|
||||||
|
const uploadPromises = filePaths.map(filePath => {
|
||||||
|
return fileAPI.upload(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadResults = await Promise.all(uploadPromises)
|
||||||
|
wx.hideLoading()
|
||||||
|
|
||||||
|
// 从上传结果中提取URL
|
||||||
|
const uploadedUrls = uploadResults.map(result => {
|
||||||
|
let url = null
|
||||||
|
|
||||||
|
// 尝试从不同路径获取URL
|
||||||
|
if (result.data && result.data.url) {
|
||||||
|
url = result.data.url
|
||||||
|
} else if (result.data && result.data.data && result.data.data.url) {
|
||||||
|
url = result.data.data.url
|
||||||
|
} else if (result.url) {
|
||||||
|
url = result.url
|
||||||
|
} else {
|
||||||
|
console.error('无法从上传结果中获取URL:', result)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果URL不是完整路径,添加前缀
|
||||||
|
if (url && !url.startsWith('http')) {
|
||||||
|
url = `${API_PREFIX}${url.startsWith('/') ? '' : '/'}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}).filter(url => url !== null)
|
||||||
|
|
||||||
|
// 检查是否有上传失败的图片
|
||||||
|
if (uploadedUrls.length !== filePaths.length) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '部分图片上传失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将上传成功的URL添加到照片列表
|
||||||
|
formData.value.photos = [...existingPhotos, ...uploadedUrls]
|
||||||
|
|
||||||
|
if (uploadedUrls.length > 0) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '上传成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
wx.hideLoading()
|
||||||
|
console.error('图片上传失败:', error)
|
||||||
|
wx.showToast({
|
||||||
|
title: '上传失败,请重试',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePhoto = (index) => {
|
||||||
|
const currentPhotos = [...((formData.value && formData.value.photos) || [])]
|
||||||
|
currentPhotos.splice(index, 1)
|
||||||
|
if (!formData.value) {
|
||||||
|
formData.value = {
|
||||||
|
carNum: '',
|
||||||
|
weight: '',
|
||||||
|
photos: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.value.photos = currentPhotos
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
const data = formData.value || {}
|
||||||
|
// 验证表单
|
||||||
|
// 只有公司类型才需要验证车牌号
|
||||||
|
if (safeOrderType.value === ORDER_TYPES.COMPANY && !data.carNum) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入车牌号',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!data.weight || data.weight === '') {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入重量',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const weightNum = parseFloat(data.weight)
|
||||||
|
if (isNaN(weightNum) || weightNum < 1) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '重量不能小于1千克',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.photos.length !== 5) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请选择5张照片',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单数据
|
||||||
|
|
||||||
|
wx.showLoading({
|
||||||
|
title: '提交中...'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取用户实时位置(主动申请权限)
|
||||||
|
let location = null
|
||||||
|
try {
|
||||||
|
// 主动申请权限并获取位置
|
||||||
|
location = await getLocationWithPermission(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取位置失败:', error)
|
||||||
|
wx.hideLoading()
|
||||||
|
|
||||||
|
// 用户拒绝定位权限,提供选择
|
||||||
|
wx.showModal({
|
||||||
|
title: '定位权限',
|
||||||
|
content: '提交订单需要获取您的位置信息,请在设置中开启定位权限',
|
||||||
|
showCancel: true,
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '去设置',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 用户点击去设置,打开设置页面
|
||||||
|
wx.openSetting({
|
||||||
|
success: (settingRes) => {
|
||||||
|
if (settingRes.authSetting['scope.userLocation']) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '定位权限已开启',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
// 权限开启后,可以重新尝试提交
|
||||||
|
this.submitForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取司机信息
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
if (!userInfo || !userInfo.driverInfo || !userInfo.driverInfo.data || !userInfo.driverInfo.data.id) {
|
||||||
|
wx.hideLoading()
|
||||||
|
wx.showModal({
|
||||||
|
title: '错误',
|
||||||
|
content: '无法获取司机信息,请重新登录',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '确定'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建提交数据
|
||||||
|
const submitData = {
|
||||||
|
driverId: userInfo.driverInfo.data.id, // 当前司机的ID
|
||||||
|
totalWeight: parseFloat(data.weight), // 总重量,使用用户输入的千克数
|
||||||
|
orderType: props.orderType, // 订单类型:order或company
|
||||||
|
gufeiType: '', // 固废类型,这里需要根据实际情况获取
|
||||||
|
gufeiPackage: '', // 包装方式,这里需要根据实际情况获取
|
||||||
|
localImg: data.photos.map(url => {
|
||||||
|
// 移除完整URL前缀,只保留相对路径
|
||||||
|
if (url.startsWith(API_PREFIX)) {
|
||||||
|
return url.substring(API_PREFIX.length)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}).join(','), // 现场照片,多个照片逗号隔开
|
||||||
|
longitude: location.longitude.toString(), // 经度
|
||||||
|
latitude: location.latitude.toString() // 纬度
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据订单类型添加不同的参数
|
||||||
|
const orderType = props.orderType || ORDER_TYPES.COMPANY
|
||||||
|
if (orderType === ORDER_TYPES.ORDER) {
|
||||||
|
// 订单类型,添加订单号
|
||||||
|
submitData.orderNum = props.orderNum || ''
|
||||||
|
} else if (orderType === ORDER_TYPES.COMPANY) {
|
||||||
|
// 公司类型,添加车辆ID、公司ID和车牌号
|
||||||
|
submitData.carId = (props.carData && (props.carData.id || props.carData.carId)) || ''
|
||||||
|
submitData.companyId = props.companyId || ''
|
||||||
|
submitData.carNum = (props.carData && props.carData.carNum) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用提交接口
|
||||||
|
try {
|
||||||
|
const response = await orderAPI.saveByWx(submitData)
|
||||||
|
wx.hideLoading()
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
// 检查业务状态码
|
||||||
|
if (response.data && response.data.code === 0) {
|
||||||
|
// 业务成功
|
||||||
|
wx.showToast({
|
||||||
|
title: '提交成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
emit('submit', data)
|
||||||
|
emit('refresh') // 触发刷新事件
|
||||||
|
closeForm()
|
||||||
|
} else {
|
||||||
|
// 业务失败,显示具体错误信息
|
||||||
|
const errorMsg = response.data?.msg || '提交失败'
|
||||||
|
console.error('创建订单业务失败:', response.data)
|
||||||
|
wx.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// HTTP状态码错误
|
||||||
|
wx.showToast({
|
||||||
|
title: '网络请求失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
wx.hideLoading()
|
||||||
|
console.error('提交失败:', error)
|
||||||
|
wx.showToast({
|
||||||
|
title: '提交失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听visible变化
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
|
||||||
|
if (props.carData && props.carData.carNum) {
|
||||||
|
// 只有公司类型才设置车牌号
|
||||||
|
if (safeOrderType.value === ORDER_TYPES.COMPANY) {
|
||||||
|
formData.value.carNum = props.carData.carNum || ''
|
||||||
|
}
|
||||||
|
formData.value.weight = ''
|
||||||
|
formData.value.photos = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
formData,
|
||||||
|
safeFormData,
|
||||||
|
safeOrderType,
|
||||||
|
ORDER_TYPES,
|
||||||
|
updateFormData,
|
||||||
|
onWeightInput,
|
||||||
|
closeForm,
|
||||||
|
resetForm,
|
||||||
|
choosePhoto,
|
||||||
|
removePhoto,
|
||||||
|
submitForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 车辆表单弹窗样式 */
|
||||||
|
.car-form-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.car-form-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx 20rpx 0 0;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #999;
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 30rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 1rpx solid #ddd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-picker {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 1rpx solid #ddd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-arrow {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120rpx;
|
||||||
|
border: 1rpx solid #ddd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-upload {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-item {
|
||||||
|
position: relative;
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -10rpx;
|
||||||
|
right: -10rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background: #ff4444;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-photo-btn {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-top: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel,
|
||||||
|
.btn-submit {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: #1976d2;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
src/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-touch-fullscreen" content="yes">
|
||||||
|
<meta name="format-detection" content="telephone=no,address=no">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
|
||||||
|
<title>my-mini-app</title>
|
||||||
|
<script><%= htmlWebpackPlugin.options.script %></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
src/pages/history/index.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '历史运送车次',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
onReachBottomDistance: 50
|
||||||
|
}
|
||||||
812
src/pages/history/index.vue
Normal file
@@ -0,0 +1,812 @@
|
|||||||
|
<template>
|
||||||
|
<view class="history-page">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">历史运送车次</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 骨架屏 -->
|
||||||
|
<view v-if="showSkeleton" class="skeleton-container">
|
||||||
|
<view v-for="n in 3" :key="n" class="skeleton-card">
|
||||||
|
<view class="skeleton-header">
|
||||||
|
<view class="skeleton-line skeleton-title"></view>
|
||||||
|
<view class="skeleton-line skeleton-status"></view>
|
||||||
|
</view>
|
||||||
|
<view class="skeleton-content">
|
||||||
|
<view class="skeleton-line skeleton-info"></view>
|
||||||
|
<view class="skeleton-line skeleton-info"></view>
|
||||||
|
<view class="skeleton-line skeleton-info"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空数据状态 -->
|
||||||
|
<view v-else-if="orderList.length === 0" class="empty-container">
|
||||||
|
<view class="empty-icon">📋</view>
|
||||||
|
<text class="empty-text">暂无历史数据</text>
|
||||||
|
<text class="empty-desc">您还没有历史运送记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 历史订单列表 -->
|
||||||
|
<view v-else class="orders-list">
|
||||||
|
<view v-for="(order, index) in orderList" :key="index" class="order-card" @click="handleOrderClick(order)">
|
||||||
|
<view class="order-header">
|
||||||
|
<text class="merchant-name">{{ order.merchant }}</text>
|
||||||
|
<view class="order-status" :class="order.status">
|
||||||
|
<text class="status-text">{{ order.statusText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="order-info">
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">地址:</text>
|
||||||
|
<text class="info-value">{{ order.address }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">重量:</text>
|
||||||
|
<text class="info-value">{{ order.estimatedWeight }}千克</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">联系人:</text>
|
||||||
|
<text class="info-value">{{ order.contact }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 质检信息弹窗 -->
|
||||||
|
<view v-if="showCheckModal" class="modal-overlay" @click="closeCheckModal">
|
||||||
|
<view class="modal-content" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">质检信息</text>
|
||||||
|
<text class="modal-close" @click="closeCheckModal">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<view v-if="loadingCheckInfo" class="loading-container">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="checkInfo" class="check-info">
|
||||||
|
<view class="info-row" v-if="checkInfo.checkStatus !== undefined">
|
||||||
|
<text class="info-label">质检状态:</text>
|
||||||
|
<text class="info-value">{{ getCheckStatusText(checkInfo.checkStatus) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="checkInfo.checkRemark">
|
||||||
|
<text class="info-label">备注:</text>
|
||||||
|
<text class="info-value">{{ checkInfo.checkRemark }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="checkInfo.checkTime">
|
||||||
|
<text class="info-label">质检时间:</text>
|
||||||
|
<text class="info-value">{{ checkInfo.checkTime }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="checkInfo.checkerName">
|
||||||
|
<text class="info-label">质检员:</text>
|
||||||
|
<text class="info-value">{{ checkInfo.checkerName }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-check-info">
|
||||||
|
<text>暂无质检信息</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<button class="btn-reject" @click="handleReject">驳回</button>
|
||||||
|
<button class="btn-approve" @click="handleApprove">通过</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="!showSkeleton && orderList.length > 0" class="load-more">
|
||||||
|
<view v-if="loadingMore" class="loading-more">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!hasMore" class="no-more">
|
||||||
|
<text class="no-more-text">没有更多数据了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { Location2 } from '@nutui/icons-vue'
|
||||||
|
import { checkLoginAndRedirect, getUserInfo } from '../../utils/auth.js'
|
||||||
|
import { orderAPI } from '../../api/index.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HistoryPage',
|
||||||
|
components: {
|
||||||
|
Location2
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
// 数据状态
|
||||||
|
const orderList = ref([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const showSkeleton = ref(true)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const driverId = ref('')
|
||||||
|
|
||||||
|
// 质检信息相关状态
|
||||||
|
const showCheckModal = ref(false)
|
||||||
|
const checkInfo = ref(null)
|
||||||
|
const loadingCheckInfo = ref(false)
|
||||||
|
const currentOrderId = ref('')
|
||||||
|
|
||||||
|
// 获取司机ID
|
||||||
|
const getDriverId = () => {
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
|
||||||
|
if (userInfo && userInfo.driverInfo && userInfo.driverInfo.data && userInfo.driverInfo.data.id) {
|
||||||
|
driverId.value = userInfo.driverInfo.data.id
|
||||||
|
} else {
|
||||||
|
console.error('无法获取司机ID')
|
||||||
|
|
||||||
|
wx.showModal({
|
||||||
|
title: '错误',
|
||||||
|
content: '无法获取司机信息,请重新登录',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '确定',
|
||||||
|
success: () => {
|
||||||
|
wx.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取历史订单数据
|
||||||
|
const fetchOrderData = async (isRefresh = false) => {
|
||||||
|
if (isRefresh) {
|
||||||
|
currentPage.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
orderList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMore.value) return
|
||||||
|
|
||||||
|
|
||||||
|
if (!driverId.value) {
|
||||||
|
console.error('司机ID为空,无法获取订单数据')
|
||||||
|
showSkeleton.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
showSkeleton.value = true
|
||||||
|
} else {
|
||||||
|
loadingMore.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await orderAPI.getOrderPage({
|
||||||
|
current: currentPage.value,
|
||||||
|
size: pageSize.value,
|
||||||
|
driverId: driverId.value
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
// 检查业务状态码
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
// 业务成功
|
||||||
|
const newOrders = response.data.data.records || response.data.data || []
|
||||||
|
|
||||||
|
// 确保newOrders是数组
|
||||||
|
if (!Array.isArray(newOrders)) {
|
||||||
|
console.error('API返回的数据不是数组格式:', newOrders)
|
||||||
|
hasMore.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地缓存中获取正在首页展示的待完成订单ID列表
|
||||||
|
let pendingOrderIds = []
|
||||||
|
try {
|
||||||
|
const storedIds = wx.getStorageSync('pendingOrderIds')
|
||||||
|
if (Array.isArray(storedIds)) {
|
||||||
|
pendingOrderIds = storedIds
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取待完成订单ID缓存失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤掉正在首页展示的订单
|
||||||
|
const filteredOrders = newOrders.filter(item => !pendingOrderIds.includes(item.id))
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
orderList.value = filteredOrders.map(item => ({
|
||||||
|
id: item.id || '',
|
||||||
|
merchant: item.companyName || '未知商户',
|
||||||
|
address: item.companyAddress || '地址未知',
|
||||||
|
estimatedWeight: item.realWeight || 0,
|
||||||
|
contact: item.contractPerson ? `${item.contractPerson} ${item.contractPhone}` : '未知',
|
||||||
|
status: `p${item.orderStatus}` || 'pending',
|
||||||
|
statusText: getStatusText(item.orderStatus)
|
||||||
|
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
orderList.value = [...orderList.value, ...filteredOrders.map(item => ({
|
||||||
|
id: item.id || '',
|
||||||
|
merchant: item.companyName || '未知商户',
|
||||||
|
address: item.companyAddress || '地址未知',
|
||||||
|
estimatedWeight: item.realWeight || 0,
|
||||||
|
contact: item.contractPerson ? `${item.contractPerson} ${item.contractPhone}` : '未知',
|
||||||
|
status: `p${item.orderStatus}` || 'pending',
|
||||||
|
statusText: getStatusText(item.orderStatus)
|
||||||
|
}))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否还有更多数据
|
||||||
|
hasMore.value = newOrders.length === pageSize.value
|
||||||
|
currentPage.value++
|
||||||
|
} else {
|
||||||
|
// 业务失败,显示具体错误信息
|
||||||
|
const errorMsg = response.data?.msg || '获取数据失败'
|
||||||
|
console.error('历史订单业务失败:', response.data)
|
||||||
|
wx.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
hasMore.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('API请求失败,状态码:', response.statusCode)
|
||||||
|
hasMore.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取历史订单数据失败:', error)
|
||||||
|
|
||||||
|
// 根据错误类型显示不同的提示
|
||||||
|
if (error.message && error.message.includes('用户凭证已过期')) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '登录已过期,请重新登录',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} else if (error.message && error.message.includes('网络')) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '网络连接失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore.value = false
|
||||||
|
} finally {
|
||||||
|
showSkeleton.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = ["运输中", "运输结束"]
|
||||||
|
return statusMap[status] || '未知状态'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取质检状态文本
|
||||||
|
const getCheckStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
0: '待质检',
|
||||||
|
1: '已通过',
|
||||||
|
2: '已驳回'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '未知状态'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击订单卡片
|
||||||
|
const handleOrderClick = async (order) => {
|
||||||
|
if (!order.id) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '订单ID不存在',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOrderId.value = order.id
|
||||||
|
showCheckModal.value = true
|
||||||
|
await fetchCheckInfo(order.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取质检信息
|
||||||
|
const fetchCheckInfo = async (orderId) => {
|
||||||
|
loadingCheckInfo.value = true
|
||||||
|
checkInfo.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await orderAPI.getCheckInfoByOrderId(orderId)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
checkInfo.value = response.data.data || null
|
||||||
|
} else {
|
||||||
|
console.error('获取质检信息失败:', response.data.msg)
|
||||||
|
wx.showToast({
|
||||||
|
title: response.data.msg || '获取质检信息失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('API请求失败,状态码:', response.statusCode)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取质检信息失败:', error)
|
||||||
|
wx.showToast({
|
||||||
|
title: '获取质检信息失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loadingCheckInfo.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭质检弹窗
|
||||||
|
const closeCheckModal = () => {
|
||||||
|
showCheckModal.value = false
|
||||||
|
checkInfo.value = null
|
||||||
|
currentOrderId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理通过
|
||||||
|
const handleApprove = () => {
|
||||||
|
wx.showModal({
|
||||||
|
title: '确认通过',
|
||||||
|
content: '确定要通过此订单的质检吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// TODO: 调用通过接口
|
||||||
|
wx.showToast({
|
||||||
|
title: '通过功能待实现',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理驳回
|
||||||
|
const handleReject = () => {
|
||||||
|
wx.showModal({
|
||||||
|
title: '确认驳回',
|
||||||
|
content: '确定要驳回此订单的质检吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// TODO: 调用驳回接口
|
||||||
|
wx.showToast({
|
||||||
|
title: '驳回功能待实现',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const loadMore = () => {
|
||||||
|
if (!loadingMore.value && hasMore.value) {
|
||||||
|
fetchOrderData(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const onPullDownRefresh = () => {
|
||||||
|
fetchOrderData(true).then(() => {
|
||||||
|
wx.stopPullDownRefresh()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触底加载
|
||||||
|
const onReachBottom = () => {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!checkLoginAndRedirect()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取司机ID
|
||||||
|
getDriverId()
|
||||||
|
|
||||||
|
// 获取历史订单数据
|
||||||
|
fetchOrderData(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理工作
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
orderList,
|
||||||
|
isLoading,
|
||||||
|
loadingMore,
|
||||||
|
showSkeleton,
|
||||||
|
hasMore,
|
||||||
|
getDriverId,
|
||||||
|
fetchOrderData,
|
||||||
|
getStatusText,
|
||||||
|
loadMore,
|
||||||
|
onPullDownRefresh,
|
||||||
|
onReachBottom,
|
||||||
|
showCheckModal,
|
||||||
|
checkInfo,
|
||||||
|
loadingCheckInfo,
|
||||||
|
handleOrderClick,
|
||||||
|
closeCheckModal,
|
||||||
|
handleApprove,
|
||||||
|
handleReject,
|
||||||
|
getCheckStatusText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.history-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 骨架屏样式 */
|
||||||
|
.skeleton-container {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-title {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-status {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-info {
|
||||||
|
width: 100%;
|
||||||
|
height: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-info:nth-child(2) {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-info:nth-child(3) {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空数据状态 */
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-desc {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单列表样式 */
|
||||||
|
.orders-list {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.p0 {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.p1 {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.completed {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status.cancelled {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多样式 */
|
||||||
|
.load-more {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border: 4rpx solid #f3f3f3;
|
||||||
|
border-top: 4rpx solid #1976d2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600rpx;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 30rpx;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx 0;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row .info-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
min-width: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row .info-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-check-info {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60rpx 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-top: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer button {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject {
|
||||||
|
background: #fff;
|
||||||
|
color: #f56c6c;
|
||||||
|
border: 1rpx solid #f56c6c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve {
|
||||||
|
background: #67c23a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject:active {
|
||||||
|
background: #fef0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve:active {
|
||||||
|
background: #5daf34;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
4
src/pages/identity/index.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '实名认证',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
}
|
||||||
445
src/pages/identity/index.vue
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
<template>
|
||||||
|
<view class="identity-page">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<view class="header">
|
||||||
|
<view class="back-btn" @click="goBack">
|
||||||
|
<Right size="32" color="white" style="transform: rotate(180deg);" />
|
||||||
|
</view>
|
||||||
|
<text class="title">实名认证</text>
|
||||||
|
<view class="placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 认证说明 -->
|
||||||
|
<view class="description-section">
|
||||||
|
<view class="description-card">
|
||||||
|
<view class="description-title">
|
||||||
|
<text class="title-text">请完成实名认证</text>
|
||||||
|
</view>
|
||||||
|
<view class="description-content">
|
||||||
|
<text class="content-text">为了保障您的账户安全,请上传身份证正面照片完成实名认证</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 身份证上传区域 -->
|
||||||
|
<view class="upload-section">
|
||||||
|
<view class="upload-card">
|
||||||
|
<view class="upload-title">
|
||||||
|
<text class="title-text">身份证正面</text>
|
||||||
|
<text class="required">*</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="upload-container">
|
||||||
|
<view class="id-card-preview" @click="chooseIdCard">
|
||||||
|
<image v-if="idCardFront" :src="idCardFront" class="id-card-image" />
|
||||||
|
<view v-else class="id-card-placeholder">
|
||||||
|
<view class="placeholder-icon">
|
||||||
|
<My size="60" color="#999" />
|
||||||
|
</view>
|
||||||
|
<text class="placeholder-text">点击上传身份证正面</text>
|
||||||
|
<text class="placeholder-tips">请确保身份证信息清晰可见</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 认证须知 -->
|
||||||
|
<view class="notice-section">
|
||||||
|
<view class="notice-card">
|
||||||
|
<view class="notice-title">
|
||||||
|
<text class="title-text">认证须知</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-content">
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 请确保身份证信息清晰可见</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 身份证需在有效期内</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 请勿上传模糊、反光或遮挡的照片</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 您的个人信息将严格保密</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-section">
|
||||||
|
<nut-button class="submit-btn" @click="handleSubmit" :disabled="isSubmitting || !idCardFront" type="primary"
|
||||||
|
size="large" block>
|
||||||
|
{{ isSubmitting ? '提交中...' : '提交认证' }}
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<view v-if="showToast" class="toast-overlay" @click="showToast = false">
|
||||||
|
<view class="toast-content">
|
||||||
|
<text class="toast-text">{{ toastMsg }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { My, Right } from '@nutui/icons-vue'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { driverAPI } from '../../api/index'
|
||||||
|
import { getUserInfo } from '../../utils/auth.js'
|
||||||
|
|
||||||
|
// 身份证图片
|
||||||
|
const idCardFront = ref('')
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// Toast 相关
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
|
||||||
|
// 显示提示信息
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择身份证照片
|
||||||
|
const chooseIdCard = () => {
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
showMessage('正在处理中,请稍候...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
const tempFilePath = res.tempFilePaths[0]
|
||||||
|
console.log('选择的身份证图片路径:', tempFilePath)
|
||||||
|
|
||||||
|
// 将图片转换为base64
|
||||||
|
Taro.getFileSystemManager().readFile({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: (fileRes) => {
|
||||||
|
const base64Data = `data:image/jpeg;base64,${fileRes.data}`
|
||||||
|
idCardFront.value = base64Data
|
||||||
|
console.log('身份证图片已转换为base64,长度:', base64Data.length)
|
||||||
|
showMessage('身份证照片选择成功')
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('读取身份证图片文件失败:', error)
|
||||||
|
showMessage('身份证照片处理失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('选择身份证图片失败:', error)
|
||||||
|
showMessage('选择身份证照片失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交认证
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!idCardFront.value) {
|
||||||
|
showMessage('请先上传身份证正面照片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前用户的司机ID
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
const driverId = storedUserInfo?.driverId || storedUserInfo?.id || storedUserInfo?.userId
|
||||||
|
|
||||||
|
if (!driverId) {
|
||||||
|
console.error('未找到司机ID,无法提交认证')
|
||||||
|
showMessage('用户信息不完整,请重新登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始提交实名认证')
|
||||||
|
const submitData = {
|
||||||
|
id: driverId,
|
||||||
|
idCardFront: idCardFront.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await driverAPI.updateIdCard(submitData)
|
||||||
|
console.log('实名认证提交响应:', response)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
console.log('实名认证提交成功')
|
||||||
|
showMessage('实名认证提交成功,请等待审核')
|
||||||
|
|
||||||
|
// 更新本地用户信息,标记已提交实名认证
|
||||||
|
if (storedUserInfo) {
|
||||||
|
storedUserInfo.identityVerified = true
|
||||||
|
storedUserInfo.identitySubmitted = true
|
||||||
|
Taro.setStorageSync('userInfo', storedUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '实名认证提交失败'
|
||||||
|
console.error('实名认证提交业务失败:', response.data)
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '实名认证提交失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('实名认证提交失败:', error)
|
||||||
|
showMessage('实名认证提交失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.identity-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 60rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证说明 */
|
||||||
|
.description-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4757;
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-preview {
|
||||||
|
width: 600rpx;
|
||||||
|
height: 400rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-preview:active {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-tips {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证须知 */
|
||||||
|
.notice-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-section {
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast样式 */
|
||||||
|
.toast-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin: 0 60rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
445
src/pages/identityNew/index.vue
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
<template>
|
||||||
|
<view class="identity-page">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<view class="header">
|
||||||
|
<view class="back-btn" @click="goBack">
|
||||||
|
<Right size="32" color="white" style="transform: rotate(180deg);" />
|
||||||
|
</view>
|
||||||
|
<text class="title">实名认证</text>
|
||||||
|
<view class="placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 认证说明 -->
|
||||||
|
<view class="description-section">
|
||||||
|
<view class="description-card">
|
||||||
|
<view class="description-title">
|
||||||
|
<text class="title-text">请完成实名认证</text>
|
||||||
|
</view>
|
||||||
|
<view class="description-content">
|
||||||
|
<text class="content-text">为了保障您的账户安全,请上传身份证正面照片完成实名认证</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 身份证上传区域 -->
|
||||||
|
<view class="upload-section">
|
||||||
|
<view class="upload-card">
|
||||||
|
<view class="upload-title">
|
||||||
|
<text class="title-text">身份证正面</text>
|
||||||
|
<text class="required">*</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="upload-container">
|
||||||
|
<view class="id-card-preview" @click="chooseIdCard">
|
||||||
|
<image v-if="idCardFront" :src="idCardFront" class="id-card-image" />
|
||||||
|
<view v-else class="id-card-placeholder">
|
||||||
|
<view class="placeholder-icon">
|
||||||
|
<My size="60" color="#999" />
|
||||||
|
</view>
|
||||||
|
<text class="placeholder-text">点击上传身份证正面</text>
|
||||||
|
<text class="placeholder-tips">请确保身份证信息清晰可见</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 认证须知 -->
|
||||||
|
<view class="notice-section">
|
||||||
|
<view class="notice-card">
|
||||||
|
<view class="notice-title">
|
||||||
|
<text class="title-text">认证须知</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-content">
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 请确保身份证信息清晰可见</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 身份证需在有效期内</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 请勿上传模糊、反光或遮挡的照片</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 您的个人信息将严格保密</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-section">
|
||||||
|
<nut-button class="submit-btn" @click="handleSubmit" :disabled="isSubmitting || !idCardFront" type="primary"
|
||||||
|
size="large" block>
|
||||||
|
{{ isSubmitting ? '提交中...' : '提交认证' }}
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<view v-if="showToast" class="toast-overlay" @click="showToast = false">
|
||||||
|
<view class="toast-content">
|
||||||
|
<text class="toast-text">{{ toastMsg }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { My, Right } from '@nutui/icons-vue'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { driverAPI } from '../../api/index'
|
||||||
|
import { getUserInfo } from '../../utils/auth.js'
|
||||||
|
|
||||||
|
// 身份证图片
|
||||||
|
const idCardFront = ref('')
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// Toast 相关
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
|
||||||
|
// 显示提示信息
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择身份证照片
|
||||||
|
const chooseIdCard = () => {
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
showMessage('正在处理中,请稍候...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
const tempFilePath = res.tempFilePaths[0]
|
||||||
|
console.log('选择的身份证图片路径:', tempFilePath)
|
||||||
|
|
||||||
|
// 将图片转换为base64
|
||||||
|
Taro.getFileSystemManager().readFile({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: (fileRes) => {
|
||||||
|
const base64Data = `data:image/jpeg;base64,${fileRes.data}`
|
||||||
|
idCardFront.value = base64Data
|
||||||
|
console.log('身份证图片已转换为base64,长度:', base64Data.length)
|
||||||
|
showMessage('身份证照片选择成功')
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('读取身份证图片文件失败:', error)
|
||||||
|
showMessage('身份证照片处理失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('选择身份证图片失败:', error)
|
||||||
|
showMessage('选择身份证照片失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交认证
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!idCardFront.value) {
|
||||||
|
showMessage('请先上传身份证正面照片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前用户的司机ID
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
const driverId = storedUserInfo?.driverId || storedUserInfo?.id || storedUserInfo?.userId
|
||||||
|
|
||||||
|
if (!driverId) {
|
||||||
|
console.error('未找到司机ID,无法提交认证')
|
||||||
|
showMessage('用户信息不完整,请重新登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始提交实名认证')
|
||||||
|
const submitData = {
|
||||||
|
id: driverId,
|
||||||
|
idCardFront: idCardFront.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await driverAPI.updateIdCard(submitData)
|
||||||
|
console.log('实名认证提交响应:', response)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
console.log('实名认证提交成功')
|
||||||
|
showMessage('实名认证提交成功,请等待审核')
|
||||||
|
|
||||||
|
// 更新本地用户信息,标记已提交实名认证
|
||||||
|
if (storedUserInfo) {
|
||||||
|
storedUserInfo.identityVerified = true
|
||||||
|
storedUserInfo.identitySubmitted = true
|
||||||
|
Taro.setStorageSync('userInfo', storedUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '实名认证提交失败'
|
||||||
|
console.error('实名认证提交业务失败:', response.data)
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '实名认证提交失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('实名认证提交失败:', error)
|
||||||
|
showMessage('实名认证提交失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.identity-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 60rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证说明 */
|
||||||
|
.description-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4757;
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-preview {
|
||||||
|
width: 600rpx;
|
||||||
|
height: 400rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-preview:active {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-tips {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证须知 */
|
||||||
|
.notice-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-section {
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast样式 */
|
||||||
|
.toast-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin: 0 60rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/pages/index/index.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '首页'
|
||||||
|
}
|
||||||
2215
src/pages/index/index.vue
Normal file
3
src/pages/login/index.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '登录'
|
||||||
|
}
|
||||||
499
src/pages/login/index.vue
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-page">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<view class="header">
|
||||||
|
<view class="logo">
|
||||||
|
<text class="logo-text">华锐绿能燃料调度</text>
|
||||||
|
</view>
|
||||||
|
<text class="welcome-text">欢迎登录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录方式切换 -->
|
||||||
|
<!-- <view class="login-mode-switch">
|
||||||
|
<view class="switch-tabs">
|
||||||
|
<view class="tab-item" :class="{ active: loginMode === 'password' }"
|
||||||
|
@click="switchLoginMode('password')">
|
||||||
|
<text class="tab-text">账号密码登录</text>
|
||||||
|
</view>
|
||||||
|
<view class="tab-item" :class="{ active: loginMode === 'phone' }" @click="switchLoginMode('phone')">
|
||||||
|
<text class="tab-text">手机号登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<view class="login-form">
|
||||||
|
<!-- 账号密码登录 -->
|
||||||
|
<view class="password-login">
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="input-label">
|
||||||
|
<My size="32" color="#999" />
|
||||||
|
<text class="label-text">手机号</text>
|
||||||
|
</view>
|
||||||
|
<nut-input v-model="loginForm.username" placeholder="请输入手机号" type="text" class="form-input" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="input-label">
|
||||||
|
<Setting size="32" color="#999" />
|
||||||
|
<text class="label-text">密码</text>
|
||||||
|
</view>
|
||||||
|
<nut-input v-model="loginForm.password" placeholder="请输入密码" type="password" class="form-input" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 忘记密码和手机验证码登录 -->
|
||||||
|
<view class="form-options">
|
||||||
|
<!-- <text class="forgot-password" @click="forgotPassword">忘记密码?</text> -->
|
||||||
|
<!-- <text class="phone-login-link" @click="switchToPhoneLogin">手机验证码登录</text> -->
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<button class="login-btn" @click="handleLogin" :disabled="isLoading">
|
||||||
|
{{ isLoading ? '登录中...' : '登录' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 注册链接 - 已关闭注册入口 -->
|
||||||
|
<!-- <view class="register-link" @click="goToRegister">
|
||||||
|
<text class="register-text">还没有账号?</text>
|
||||||
|
<text class="register-btn">立即注册</text>
|
||||||
|
</view> -->
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<nut-toast v-model:visible="showToast" :msg="toastMsg" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { Toast } from '@nutui/nutui-taro'
|
||||||
|
import { My, Setting } from '@nutui/icons-vue'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { aesEncrypt } from '../../utils/crypto.js'
|
||||||
|
import { saveUserInfo } from '../../utils/auth.js'
|
||||||
|
import { authAPI, driverAPI } from '../../api/index.js'
|
||||||
|
|
||||||
|
// 账号密码登录表单数据
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
|
||||||
|
// 显示提示信息
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 忘记密码
|
||||||
|
const forgotPassword = () => {
|
||||||
|
showMessage('忘记密码功能开发中...')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 账号密码登录验证
|
||||||
|
if (!loginForm.username.trim()) {
|
||||||
|
showMessage('请输入用户名')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loginForm.password.trim()) {
|
||||||
|
showMessage('请输入密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginForm.password.length < 6) {
|
||||||
|
showMessage('密码长度不能少于6位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用登录接口
|
||||||
|
// 对密码进行加密
|
||||||
|
const encryptedPassword = aesEncrypt(loginForm.password);
|
||||||
|
|
||||||
|
const loginData = {
|
||||||
|
username: loginForm.username,
|
||||||
|
password: encryptedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const response = await authAPI.login(loginData)
|
||||||
|
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
// 构建用户信息
|
||||||
|
const userInfo = {
|
||||||
|
isLogin: true,
|
||||||
|
token: response.data.access_token || response.data.token,
|
||||||
|
refreshToken: response.data.refresh_token,
|
||||||
|
expiresIn: response.data.expires_in,
|
||||||
|
tokenType: response.data.token_type || 'Bearer',
|
||||||
|
userId: response.data.userId || response.data.user_id,
|
||||||
|
username: loginForm.username,
|
||||||
|
avatar: response.data.avatar || '',
|
||||||
|
loginTime: new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先保存token到本地存储,供后续API调用使用
|
||||||
|
saveUserInfo(userInfo)
|
||||||
|
|
||||||
|
// 等待更长时间确保token完全生效
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// 检查用户是否为司机
|
||||||
|
try {
|
||||||
|
// 添加重试机制,避免token未生效的问题
|
||||||
|
let driverResponse = null
|
||||||
|
let retryCount = 0
|
||||||
|
const maxRetries = 3
|
||||||
|
|
||||||
|
while (retryCount < maxRetries) {
|
||||||
|
try {
|
||||||
|
driverResponse = await driverAPI.getDriverInfo(userInfo.userId)
|
||||||
|
break // 成功获取,跳出循环
|
||||||
|
} catch (error) {
|
||||||
|
retryCount++
|
||||||
|
console.warn(`获取司机信息失败,重试第${retryCount}次:`, error)
|
||||||
|
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
// 等待一段时间后重试
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
} else {
|
||||||
|
throw error // 重试次数用完,抛出错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driverResponse.statusCode === 200 && driverResponse.data) {
|
||||||
|
// 检查是否包含司机ID
|
||||||
|
const hasDriverId = driverResponse.data.id || driverResponse.data.driverId ||
|
||||||
|
(driverResponse.data.data && (driverResponse.data.data.id || driverResponse.data.data.driverId))
|
||||||
|
|
||||||
|
if (hasDriverId) {
|
||||||
|
// 用户是司机且有司机ID,登录成功
|
||||||
|
const updatedUserInfo = {
|
||||||
|
...userInfo,
|
||||||
|
isDriver: true,
|
||||||
|
driverInfo: driverResponse.data,
|
||||||
|
driverId: driverResponse.data.id || driverResponse.data.driverId ||
|
||||||
|
(driverResponse.data.data && (driverResponse.data.data.id || driverResponse.data.data.driverId))
|
||||||
|
}
|
||||||
|
saveUserInfo(updatedUserInfo)
|
||||||
|
|
||||||
|
showMessage('登录成功')
|
||||||
|
|
||||||
|
// 检查用户是否已完成实名认证
|
||||||
|
const driverStatus = driverResponse.data?.driverStatus ||
|
||||||
|
driverResponse.data?.data?.driverStatus
|
||||||
|
const isIdentityVerified = driverResponse.data?.identityVerified ||
|
||||||
|
driverResponse.data?.data?.identityVerified ||
|
||||||
|
updatedUserInfo.identityVerified
|
||||||
|
|
||||||
|
// 如果司机状态为1(正常)或者已经完成实名认证,则跳转到首页
|
||||||
|
if (driverStatus === '1' || isIdentityVerified) {
|
||||||
|
// 用户已完成实名认证,跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
// 用户未完成实名认证,跳转到实名认证页面
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({ url: '/pages/realname/index?from=login' })
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 用户不是司机或没有司机ID,登录失败
|
||||||
|
showMessage('登录失败:您不是司机用户')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 获取司机信息失败,登录失败
|
||||||
|
showMessage('登录失败:无法获取司机信息')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取司机信息失败:', error)
|
||||||
|
|
||||||
|
// 检查是否是token相关错误
|
||||||
|
if (error.message && error.message.includes('用户凭证已过期')) {
|
||||||
|
// 如果是token过期错误,可能是token还未完全生效,重试一次
|
||||||
|
console.log('检测到token过期错误,尝试重新获取司机信息...')
|
||||||
|
try {
|
||||||
|
// 再次等待并重试
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
const retryResponse = await driverAPI.getDriverInfo(userInfo.userId)
|
||||||
|
|
||||||
|
if (retryResponse.statusCode === 200 && retryResponse.data) {
|
||||||
|
// 处理重试成功的响应
|
||||||
|
const hasDriverId = retryResponse.data.id || retryResponse.data.driverId ||
|
||||||
|
(retryResponse.data.data && (retryResponse.data.data.id || retryResponse.data.data.driverId))
|
||||||
|
|
||||||
|
if (hasDriverId) {
|
||||||
|
const updatedUserInfo = {
|
||||||
|
...userInfo,
|
||||||
|
isDriver: true,
|
||||||
|
driverInfo: retryResponse.data,
|
||||||
|
driverId: retryResponse.data.id || retryResponse.data.driverId ||
|
||||||
|
(retryResponse.data.data && (retryResponse.data.data.id || retryResponse.data.data.driverId))
|
||||||
|
}
|
||||||
|
saveUserInfo(updatedUserInfo)
|
||||||
|
|
||||||
|
showMessage('登录成功')
|
||||||
|
|
||||||
|
// 检查认证状态并跳转
|
||||||
|
const driverStatus = retryResponse.data?.driverStatus ||
|
||||||
|
retryResponse.data?.data?.driverStatus
|
||||||
|
const isIdentityVerified = retryResponse.data?.identityVerified ||
|
||||||
|
retryResponse.data?.data?.identityVerified ||
|
||||||
|
updatedUserInfo.identityVerified
|
||||||
|
|
||||||
|
if (driverStatus === '1' || isIdentityVerified) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({ url: '/pages/realname/index?from=login' })
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (retryError) {
|
||||||
|
console.error('重试获取司机信息仍然失败:', retryError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage('登录失败:无法验证司机身份')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 登录失败
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '登录失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
showMessage('登录失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到注册页面 - 已关闭注册入口
|
||||||
|
// const goToRegister = () => {
|
||||||
|
// Taro.navigateTo({ url: '/pages/register/index' })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 页面初始化完成,不再设置默认账号密码
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 120rpx 0 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录方式切换 */
|
||||||
|
.login-mode-switch {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-text {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录表单 */
|
||||||
|
.login-form {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 2rpx solid #f0f0f0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
/* 修复真机显示问题 */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
/* 修复字体渲染 */
|
||||||
|
font-family: inherit;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 验证码输入 */
|
||||||
|
.verification-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-code-btn {
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-code-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单选项 */
|
||||||
|
.form-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-login-link {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册链接 */
|
||||||
|
.register-link {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-btn {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/pages/password/index.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '修改密码'
|
||||||
|
}
|
||||||
259
src/pages/password/index.vue
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<view class="password-page">
|
||||||
|
<!-- 修改密码表单 -->
|
||||||
|
<view class="password-form">
|
||||||
|
<!-- 当前密码 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">当前密码</text>
|
||||||
|
<input class="form-input" type="password" placeholder="请输入当前密码" v-model="passwordForm.oldPassword" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 新密码 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">新密码</text>
|
||||||
|
<input class="form-input" type="password" placeholder="请输入新密码" v-model="passwordForm.newPassword" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 确认新密码 -->
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">确认新密码</text>
|
||||||
|
<input class="form-input" type="password" placeholder="请再次输入新密码"
|
||||||
|
v-model="passwordForm.confirmPassword" />
|
||||||
|
<text class="password-hint">密码长度至少6位,建议包含字母和数字</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<button class="submit-btn" :class="{ 'disabled': isLoading }" :disabled="isLoading" @click="handleSubmit">
|
||||||
|
{{ isLoading ? '修改中...' : '确认修改' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { userAPI } from '../../api/index'
|
||||||
|
import { getUserInfo, logout } from '../../utils/auth'
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const passwordForm = reactive({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!passwordForm.oldPassword) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入当前密码',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordForm.newPassword) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '请输入新密码',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordForm.newPassword.length < 6) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '新密码至少6位',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '两次输入的新密码不一致',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordForm.oldPassword === passwordForm.newPassword) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '新密码不能与当前密码相同',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交修改密码
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await userAPI.changePassword({
|
||||||
|
password: passwordForm.oldPassword,
|
||||||
|
newpassword1: passwordForm.newPassword,
|
||||||
|
newpassword2: passwordForm.confirmPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '密码修改成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
passwordForm.oldPassword = ''
|
||||||
|
passwordForm.newPassword = ''
|
||||||
|
passwordForm.confirmPassword = ''
|
||||||
|
|
||||||
|
// 延迟返回上一页
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '修改失败'
|
||||||
|
wx.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败:', error)
|
||||||
|
wx.showToast({
|
||||||
|
title: '修改失败,请重试',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.password-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单区域 */
|
||||||
|
.password-form {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 2rpx solid #e9ecef;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: #f8f9fa;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
/* 修复真机显示问题 */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
/* 修复字体渲染 */
|
||||||
|
font-family: inherit;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: #1976d2;
|
||||||
|
background: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 密码提示 */
|
||||||
|
.password-hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #1976d2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:active {
|
||||||
|
background: #1565c0;
|
||||||
|
transform: translateY(2rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn.disabled {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
.password-page {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-form {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
font-size: 26rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
font-size: 26rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/pages/profile/index.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '个人中心'
|
||||||
|
}
|
||||||
793
src/pages/profile/index.vue
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
<template>
|
||||||
|
<view class="profile-page">
|
||||||
|
<!-- 用户信息区域 -->
|
||||||
|
<view class="user-info">
|
||||||
|
<view class="avatar-section">
|
||||||
|
<view class="avatar-container" @click="previewAvatar">
|
||||||
|
<image v-if="userInfo.driverInfo?.data?.driverHead"
|
||||||
|
:src="getImageUrl(userInfo.driverInfo.data.driverHead)" :key="avatarKey"
|
||||||
|
class="avatar-image" mode="aspectFill" />
|
||||||
|
<view v-else class="avatar-placeholder">
|
||||||
|
<My size="60" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-details">
|
||||||
|
<text class="username">{{ userInfo.driverInfo?.data?.driverName || userInfo.username || '点击登录' }}</text>
|
||||||
|
<text class="phone-number">{{ userInfo.driverInfo?.data?.driverTel || '未设置' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能菜单区域 -->
|
||||||
|
<view class="menu-section">
|
||||||
|
<!-- 我的车辆 -->
|
||||||
|
<!-- <view class="menu-item" @click="goToMyVehicles">
|
||||||
|
<view class="menu-icon">
|
||||||
|
<Cart size="40" color="#666" />
|
||||||
|
</view>
|
||||||
|
<text class="menu-text">我的车辆</text>
|
||||||
|
<view class="menu-arrow">
|
||||||
|
<Right size="32" color="#999" />
|
||||||
|
</view>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
|
<!-- 历史运送车次 -->
|
||||||
|
<view class="menu-item" @click="goToTrips">
|
||||||
|
<view class="menu-icon">
|
||||||
|
<Order size="40" color="#666" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-content">
|
||||||
|
<text class="menu-text">历史运送车次</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">
|
||||||
|
<Right size="32" color="#999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 修改头像 -->
|
||||||
|
<view class="menu-item" @click="changeAvatar" :class="{ 'disabled': isUploadingAvatar }">
|
||||||
|
<view class="menu-icon">
|
||||||
|
<My size="40" color="#666" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-content">
|
||||||
|
<text class="menu-text">{{ isUploadingAvatar ? '头像上传中...' : '修改头像' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow" v-if="!isUploadingAvatar">
|
||||||
|
<Right size="32" color="#999" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-loading" v-else>
|
||||||
|
<text class="loading-text">...</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 实名认证 -->
|
||||||
|
<view class="menu-item" @click="goToIdentity">
|
||||||
|
<view class="menu-icon">
|
||||||
|
<My size="40" color="#666" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-content">
|
||||||
|
<text class="menu-text">实名认证</text>
|
||||||
|
<view v-if="driverStatus" class="status-badge" :class="statusClass">
|
||||||
|
<text class="status-text">{{ statusText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">
|
||||||
|
<Right size="32" color="#999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 修改密码 -->
|
||||||
|
<view class="menu-item" @click="changePassword">
|
||||||
|
<view class="menu-icon">
|
||||||
|
<Setting size="40" color="#666" />
|
||||||
|
</view>
|
||||||
|
<view class="menu-content">
|
||||||
|
<text class="menu-text">修改密码</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-arrow">
|
||||||
|
<Right size="32" color="#999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录按钮 -->
|
||||||
|
<view class="logout-section">
|
||||||
|
<nut-button type="danger" size="large" @click="handleLogout" v-if="userInfo.isLogin">
|
||||||
|
退出登录
|
||||||
|
</nut-button>
|
||||||
|
<nut-button type="primary" size="large" @click="login" v-else>
|
||||||
|
立即登录
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<nut-toast v-model:visible="showToast" :msg="toastMsg" />
|
||||||
|
|
||||||
|
<!-- 头像预览弹窗 -->
|
||||||
|
<nut-popup v-model:visible="showAvatarPreview" position="center">
|
||||||
|
<view class="avatar-preview-dialog">
|
||||||
|
<view class="preview-title">头像预览</view>
|
||||||
|
<view class="avatar-preview">
|
||||||
|
<image v-if="userInfo.driverInfo?.data?.driverHead || userInfo.avatar"
|
||||||
|
:src="userInfo.driverInfo?.data?.driverHead ? getImageUrl(userInfo.driverInfo.data.driverHead) : userInfo.avatar"
|
||||||
|
class="preview-avatar-image" mode="aspectFill" />
|
||||||
|
<view v-else class="preview-avatar-placeholder">
|
||||||
|
<My size="100" color="#666" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="preview-buttons">
|
||||||
|
<nut-button @click="showAvatarPreview = false">关闭</nut-button>
|
||||||
|
<nut-button type="primary" @click="selectNewAvatar" :disabled="isUploadingAvatar">
|
||||||
|
{{ isUploadingAvatar ? '上传中...' : '选择新头像' }}
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</nut-popup>
|
||||||
|
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<nut-popup v-model:visible="showPasswordDialog" position="center">
|
||||||
|
<view class="password-dialog">
|
||||||
|
<view class="dialog-title">修改密码</view>
|
||||||
|
<nut-form>
|
||||||
|
<nut-form-item label="原密码">
|
||||||
|
<nut-input v-model="passwordForm.oldPassword" type="password" placeholder="请输入原密码" />
|
||||||
|
</nut-form-item>
|
||||||
|
<nut-form-item label="新密码">
|
||||||
|
<nut-input v-model="passwordForm.newPassword" type="password" placeholder="请输入新密码" />
|
||||||
|
</nut-form-item>
|
||||||
|
<nut-form-item label="确认密码">
|
||||||
|
<nut-input v-model="passwordForm.confirmPassword" type="password" placeholder="请再次输入新密码" />
|
||||||
|
</nut-form-item>
|
||||||
|
</nut-form>
|
||||||
|
<view class="dialog-buttons">
|
||||||
|
<nut-button @click="showPasswordDialog = false">取消</nut-button>
|
||||||
|
<nut-button type="primary" @click="submitPassword">确认</nut-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</nut-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
|
||||||
|
import { Button, Toast, Avatar, Popup, Form, FormItem, Input } from '@nutui/nutui-taro'
|
||||||
|
import { Cart, Order, Setting, Right, My } from '@nutui/icons-vue'
|
||||||
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
|
import { getUserInfo, logout, isLoggedIn, saveUserInfo, checkLoginAndRedirect } from '../../utils/auth.js'
|
||||||
|
import { driverAPI } from '../../api/index'
|
||||||
|
import { API_CONFIG } from '../../utils/request.js'
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
const userInfo = reactive({
|
||||||
|
isLogin: false,
|
||||||
|
username: '',
|
||||||
|
userId: '',
|
||||||
|
phone: '',
|
||||||
|
avatar: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查登录状态,未登录则跳转到登录页
|
||||||
|
if (!checkLoginAndRedirect()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录,获取用户信息
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
if (storedUserInfo) {
|
||||||
|
Object.assign(userInfo, storedUserInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面显示时重新获取用户信息
|
||||||
|
useDidShow(() => {
|
||||||
|
// 检查登录状态,未登录则跳转到登录页
|
||||||
|
if (!checkLoginAndRedirect()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录,重新获取用户信息
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
if (storedUserInfo) {
|
||||||
|
Object.assign(userInfo, storedUserInfo)
|
||||||
|
|
||||||
|
// 如果头像有变化,强制重新渲染
|
||||||
|
if (storedUserInfo.driverInfo?.data?.driverHead || storedUserInfo.avatar) {
|
||||||
|
avatarKey.value++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.assign(userInfo, {
|
||||||
|
isLogin: false,
|
||||||
|
username: '',
|
||||||
|
userId: '',
|
||||||
|
phone: '',
|
||||||
|
avatar: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新司机数据以获取最新的认证状态
|
||||||
|
refreshDriverData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Toast 相关
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
|
||||||
|
// 头像预览弹窗
|
||||||
|
const showAvatarPreview = ref(false)
|
||||||
|
|
||||||
|
// 修改密码弹窗
|
||||||
|
const showPasswordDialog = ref(false)
|
||||||
|
const passwordForm = reactive({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 头像上传状态
|
||||||
|
const isUploadingAvatar = ref(false)
|
||||||
|
|
||||||
|
// 头像key,用于强制重新渲染
|
||||||
|
const avatarKey = ref(0)
|
||||||
|
|
||||||
|
// 司机状态相关
|
||||||
|
const driverStatus = computed(() => {
|
||||||
|
return userInfo.driverInfo?.data?.driverStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const status = driverStatus.value
|
||||||
|
if (status === '0') return '待认证'
|
||||||
|
if (status === '1') return '正常'
|
||||||
|
if (status === '2') return '冻结'
|
||||||
|
return '未知状态'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取图片完整URL
|
||||||
|
const getImageUrl = (path) => {
|
||||||
|
if (!path) return ''
|
||||||
|
// 如果已经是完整URL,直接返回
|
||||||
|
if (path.startsWith('http')) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
// 拼接API地址
|
||||||
|
return `${API_CONFIG.BASE_URL}api${path.startsWith('/') ? path : '/' + path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = driverStatus.value
|
||||||
|
if (status === '0') return 'status-pending'
|
||||||
|
if (status === '1') return 'status-normal'
|
||||||
|
if (status === '2') return 'status-frozen'
|
||||||
|
return 'status-unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示提示
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新司机数据
|
||||||
|
const refreshDriverData = async () => {
|
||||||
|
try {
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
if (!storedUserInfo) {
|
||||||
|
console.error('无法获取用户信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
const userId = storedUserInfo.userId || storedUserInfo.id || storedUserInfo.user?.id
|
||||||
|
if (!userId) {
|
||||||
|
console.error('无法获取用户ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 调用API获取最新的司机信息
|
||||||
|
const response = await driverAPI.getDriverInfo(userId)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0 && response.data.data) {
|
||||||
|
// 更新本地用户信息中的司机数据
|
||||||
|
const updatedUserInfo = {
|
||||||
|
...storedUserInfo,
|
||||||
|
driverInfo: {
|
||||||
|
...storedUserInfo.driverInfo,
|
||||||
|
data: response.data.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新后的用户信息
|
||||||
|
saveUserInfo(updatedUserInfo)
|
||||||
|
|
||||||
|
// 更新当前页面的用户信息
|
||||||
|
Object.assign(userInfo, updatedUserInfo)
|
||||||
|
} else {
|
||||||
|
console.error('获取司机信息失败:', response.data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('获取司机信息API调用失败:', response)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新司机数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览头像
|
||||||
|
const previewAvatar = () => {
|
||||||
|
if (!userInfo.isLogin) {
|
||||||
|
showMessage('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showAvatarPreview.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更换头像
|
||||||
|
const changeAvatar = () => {
|
||||||
|
if (!userInfo.isLogin) {
|
||||||
|
showMessage('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectNewAvatar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择新头像
|
||||||
|
const selectNewAvatar = () => {
|
||||||
|
showAvatarPreview.value = false
|
||||||
|
|
||||||
|
if (isUploadingAvatar.value) {
|
||||||
|
showMessage('头像上传中,请稍候...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
const tempFilePath = res.tempFilePaths[0]
|
||||||
|
|
||||||
|
// 将图片转换为base64
|
||||||
|
Taro.getFileSystemManager().readFile({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: (fileRes) => {
|
||||||
|
const base64Data = `data:image/jpeg;base64,${fileRes.data}`
|
||||||
|
|
||||||
|
// 上传头像
|
||||||
|
uploadAvatar(base64Data)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('读取图片文件失败:', error)
|
||||||
|
showMessage('头像处理失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('选择图片失败:', error)
|
||||||
|
showMessage('选择头像失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传头像
|
||||||
|
const uploadAvatar = async (base64Data) => {
|
||||||
|
if (isUploadingAvatar.value) return
|
||||||
|
|
||||||
|
isUploadingAvatar.value = true
|
||||||
|
showMessage('正在上传头像...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// 获取当前用户的司机ID
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
const driverId = storedUserInfo?.driverId || storedUserInfo?.id || userInfo.userId
|
||||||
|
|
||||||
|
if (!driverId) {
|
||||||
|
console.error('未找到司机ID,无法上传头像')
|
||||||
|
showMessage('用户信息不完整,请重新登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
id: driverId,
|
||||||
|
driverHead: base64Data
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResponse = await driverAPI.updateDriverHead(updateData)
|
||||||
|
|
||||||
|
if (updateResponse.statusCode === 200 && updateResponse.data) {
|
||||||
|
if (updateResponse.data.code === 0) {
|
||||||
|
// 获取服务器返回的图片路径
|
||||||
|
const imagePath = updateResponse.data.data || updateResponse.data.path || updateResponse.data.url
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
|
// 更新driverInfo.data.driverHead字段为服务器返回的路径
|
||||||
|
if (userInfo.driverInfo && userInfo.driverInfo.data) {
|
||||||
|
userInfo.driverInfo.data.driverHead = imagePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制触发响应式更新
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 强制重新渲染头像
|
||||||
|
avatarKey.value++
|
||||||
|
|
||||||
|
// 更新本地存储的用户信息
|
||||||
|
if (storedUserInfo) {
|
||||||
|
// 同时更新driverInfo中的头像字段
|
||||||
|
if (storedUserInfo.driverInfo && storedUserInfo.driverInfo.data) {
|
||||||
|
storedUserInfo.driverInfo.data.driverHead = imagePath
|
||||||
|
}
|
||||||
|
Taro.setStorageSync('userInfo', storedUserInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage('头像更新成功')
|
||||||
|
} else {
|
||||||
|
console.error('服务器未返回图片路径')
|
||||||
|
showMessage('头像上传失败:未获取到图片路径')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = updateResponse.data?.msg || updateResponse.data?.message || '头像上传失败'
|
||||||
|
console.error('头像上传业务失败:', updateResponse.data)
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = updateResponse.data?.msg || updateResponse.data?.message || '头像上传失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('头像上传失败:', error)
|
||||||
|
showMessage('头像上传失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isUploadingAvatar.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 我的车辆
|
||||||
|
const goToMyVehicles = () => {
|
||||||
|
showMessage('跳转到我的车辆页面')
|
||||||
|
// 这里可以跳转到车辆页面
|
||||||
|
// Taro.navigateTo({ url: '/pages/vehicle/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史运送车次
|
||||||
|
const goToTrips = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/history/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实名认证
|
||||||
|
const goToIdentity = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/realname/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
const changePassword = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/password/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交密码修改
|
||||||
|
const submitPassword = () => {
|
||||||
|
if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
||||||
|
showMessage('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||||
|
showMessage('两次输入的密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordForm.newPassword.length < 6) {
|
||||||
|
showMessage('密码长度不能少于6位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里调用修改密码的API
|
||||||
|
showMessage('密码修改成功')
|
||||||
|
showPasswordDialog.value = false
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
passwordForm.oldPassword = ''
|
||||||
|
passwordForm.newPassword = ''
|
||||||
|
passwordForm.confirmPassword = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
const login = () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout() // 使用认证工具中的退出登录函数
|
||||||
|
showMessage('已退出登录')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.profile-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息区域 */
|
||||||
|
.user-info {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
margin-left: 40rpx;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: block;
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-number {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态徽章 */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: rgba(255, 193, 7, 0.2);
|
||||||
|
color: #ffc107;
|
||||||
|
border: 1rpx solid rgba(255, 193, 7, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-normal {
|
||||||
|
background: rgba(40, 167, 69, 0.2);
|
||||||
|
color: #28a745;
|
||||||
|
border: 1rpx solid rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-frozen {
|
||||||
|
background: rgba(220, 53, 69, 0.2);
|
||||||
|
color: #dc3545;
|
||||||
|
border: 1rpx solid rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-unknown {
|
||||||
|
background: rgba(108, 117, 125, 0.2);
|
||||||
|
color: #6c757d;
|
||||||
|
border: 1rpx solid rgba(108, 117, 125, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能菜单区域 */
|
||||||
|
.menu-section {
|
||||||
|
background: white;
|
||||||
|
margin: 40rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #667eea;
|
||||||
|
animation: loading 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
20% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出登录区域 */
|
||||||
|
.logout-section {
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像预览弹窗 */
|
||||||
|
.avatar-preview-dialog {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
width: 500rpx;
|
||||||
|
max-width: 90vw;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-avatar-image {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-avatar-placeholder {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-buttons .nut-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改密码弹窗 */
|
||||||
|
.password-dialog {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
width: 600rpx;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons .nut-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
4
src/pages/realname/index.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '实名认证',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
}
|
||||||
695
src/pages/realname/index.vue
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
<template>
|
||||||
|
<view class="identity-page">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<view class="header">
|
||||||
|
<view v-if="showBackBtn" class="back-btn" @click="goBack">
|
||||||
|
<Right size="32" color="white" style="transform: rotate(180deg);" />
|
||||||
|
</view>
|
||||||
|
<view v-else class="placeholder"></view>
|
||||||
|
<text class="title">实名认证</text>
|
||||||
|
<view class="placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 认证说明 -->
|
||||||
|
<view class="description-section">
|
||||||
|
<view class="description-card">
|
||||||
|
<view class="description-title">
|
||||||
|
<text class="title-text">{{ isAlreadyVerified ? '实名认证已完成' : '请完成实名认证' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="description-content">
|
||||||
|
<text class="content-text">{{ isAlreadyVerified ? '恭喜您,您已完成实名认证,可以正常使用所有功能' :
|
||||||
|
'为了保障您的账户安全,请上传身份证正面照片完成实名认证' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 身份证上传区域 -->
|
||||||
|
<view class="upload-section" v-if="!isAlreadyVerified">
|
||||||
|
<view class="upload-card">
|
||||||
|
<view class="upload-title">
|
||||||
|
<text class="title-text">身份证正面</text>
|
||||||
|
<text class="required">*</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="upload-container">
|
||||||
|
<view class="id-card-preview" @click="chooseIdCard">
|
||||||
|
<image v-if="idCardFront" :src="idCardFront" class="id-card-image" />
|
||||||
|
<view v-else class="id-card-placeholder">
|
||||||
|
<view class="placeholder-icon">
|
||||||
|
<My size="60" color="#999" />
|
||||||
|
</view>
|
||||||
|
<text class="placeholder-text">点击上传身份证正面</text>
|
||||||
|
<text class="placeholder-tips">请确保身份证信息清晰可见</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 已认证状态显示 -->
|
||||||
|
<view class="verified-section" v-if="isAlreadyVerified">
|
||||||
|
<view class="verified-card">
|
||||||
|
<view class="verified-icon">
|
||||||
|
<text class="check-icon">✓</text>
|
||||||
|
</view>
|
||||||
|
<view class="verified-content">
|
||||||
|
<text class="verified-title">实名认证已通过</text>
|
||||||
|
<text class="verified-desc">您的身份信息已验证,可以正常使用所有功能</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 认证须知 -->
|
||||||
|
<view class="notice-section" v-if="!isAlreadyVerified">
|
||||||
|
<view class="notice-card">
|
||||||
|
<view class="notice-title">
|
||||||
|
<text class="title-text">认证须知</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-content">
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 请确保身份证信息清晰可见</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 身份证需在有效期内</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 请勿上传模糊、反光或遮挡的照片</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-item">
|
||||||
|
<text class="notice-text">• 您的个人信息将严格保密</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="submit-section" v-if="!isAlreadyVerified">
|
||||||
|
<nut-button class="submit-btn" @click="handleSubmit" :disabled="isSubmitting || !idCardFront" type="primary"
|
||||||
|
size="large" block>
|
||||||
|
{{ isSubmitting ? '提交中...' : '提交认证' }}
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 已认证用户的返回按钮 -->
|
||||||
|
<view class="submit-section" v-if="isAlreadyVerified">
|
||||||
|
<nut-button class="back-to-home-btn" @click="goToHome" type="primary" size="large" block>
|
||||||
|
返回首页
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<view v-if="showToast" class="toast-overlay" @click="showToast = false">
|
||||||
|
<view class="toast-content">
|
||||||
|
<text class="toast-text">{{ toastMsg }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { My, Right } from '@nutui/icons-vue'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { driverAPI } from '../../api/index'
|
||||||
|
import { getUserInfo, saveUserInfo } from '../../utils/auth.js'
|
||||||
|
|
||||||
|
// 身份证图片
|
||||||
|
const idCardFront = ref('')
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// Toast 相关
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
const userInfo = ref({})
|
||||||
|
|
||||||
|
// 是否显示返回按钮
|
||||||
|
const showBackBtn = ref(true)
|
||||||
|
|
||||||
|
// 是否已经完成实名认证
|
||||||
|
const isAlreadyVerified = ref(false)
|
||||||
|
|
||||||
|
// 司机状态相关
|
||||||
|
const driverStatus = computed(() => {
|
||||||
|
return userInfo.value.driverInfo?.data?.driverStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const status = driverStatus.value
|
||||||
|
if (status === '0') return '待认证'
|
||||||
|
if (status === '1') return '正常'
|
||||||
|
if (status === '2') return '冻结'
|
||||||
|
return '未知状态'
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = driverStatus.value
|
||||||
|
if (status === '0') return 'status-pending'
|
||||||
|
if (status === '1') return 'status-normal'
|
||||||
|
if (status === '2') return 'status-frozen'
|
||||||
|
return 'status-unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示提示信息
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
const getUserInfoData = () => {
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
if (storedUserInfo) {
|
||||||
|
userInfo.value = storedUserInfo
|
||||||
|
|
||||||
|
// 检查用户是否已经完成实名认证
|
||||||
|
const driverStatus = storedUserInfo.driverInfo?.driverStatus ||
|
||||||
|
storedUserInfo.driverInfo?.data?.driverStatus
|
||||||
|
const isIdentityVerified = storedUserInfo.identityVerified ||
|
||||||
|
storedUserInfo.driverInfo?.identityVerified ||
|
||||||
|
storedUserInfo.driverInfo?.data?.identityVerified
|
||||||
|
|
||||||
|
// 如果司机状态为1(正常)或者已经完成实名认证,则显示已认证状态
|
||||||
|
isAlreadyVerified.value = driverStatus === '1' || !!isIdentityVerified
|
||||||
|
|
||||||
|
console.log('实名认证页面获取用户信息:', storedUserInfo)
|
||||||
|
console.log('用户是否已完成实名认证:', isAlreadyVerified.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取用户信息
|
||||||
|
getUserInfoData()
|
||||||
|
|
||||||
|
// 页面加载时判断来源
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取页面参数
|
||||||
|
const pages = Taro.getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const options = currentPage.options || {}
|
||||||
|
|
||||||
|
// 如果是从登录页面跳转过来的,不显示返回按钮
|
||||||
|
if (options.from === 'login') {
|
||||||
|
showBackBtn.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 也可以通过检查页面栈来判断
|
||||||
|
// 如果页面栈中只有2个页面(登录页面和当前页面),说明是从登录跳转过来的
|
||||||
|
if (pages.length === 2) {
|
||||||
|
const prevPage = pages[0]
|
||||||
|
if (prevPage.route === 'pages/login/index') {
|
||||||
|
showBackBtn.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
const goToHome = () => {
|
||||||
|
Taro.switchTab({ url: '/pages/index/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新司机数据
|
||||||
|
const refreshDriverData = async () => {
|
||||||
|
try {
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
if (!storedUserInfo) {
|
||||||
|
console.error('无法获取用户信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
const userId = storedUserInfo.userId || storedUserInfo.id || storedUserInfo.user?.id
|
||||||
|
if (!userId) {
|
||||||
|
console.error('无法获取用户ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始刷新司机数据,用户ID:', userId)
|
||||||
|
|
||||||
|
// 调用API获取最新的司机信息
|
||||||
|
const response = await driverAPI.getDriverInfo(userId)
|
||||||
|
console.log('刷新司机数据响应:', response)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0 && response.data.data) {
|
||||||
|
// 更新本地用户信息中的司机数据
|
||||||
|
const updatedUserInfo = {
|
||||||
|
...storedUserInfo,
|
||||||
|
driverInfo: {
|
||||||
|
...storedUserInfo.driverInfo,
|
||||||
|
data: response.data.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新后的用户信息
|
||||||
|
saveUserInfo(updatedUserInfo)
|
||||||
|
console.log('司机数据已更新:', response.data.data)
|
||||||
|
|
||||||
|
// 更新当前页面的用户信息
|
||||||
|
userInfo.value = updatedUserInfo
|
||||||
|
} else {
|
||||||
|
console.error('获取司机信息失败:', response.data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('获取司机信息API调用失败:', response)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新司机数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择身份证照片
|
||||||
|
const chooseIdCard = () => {
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
showMessage('正在处理中,请稍候...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
const tempFilePath = res.tempFilePaths[0]
|
||||||
|
console.log('选择的身份证图片路径:', tempFilePath)
|
||||||
|
|
||||||
|
// 将图片转换为base64
|
||||||
|
Taro.getFileSystemManager().readFile({
|
||||||
|
filePath: tempFilePath,
|
||||||
|
encoding: 'base64',
|
||||||
|
success: (fileRes) => {
|
||||||
|
const base64Data = `data:image/jpeg;base64,${fileRes.data}`
|
||||||
|
idCardFront.value = base64Data
|
||||||
|
console.log('身份证图片已转换为base64,长度:', base64Data.length)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('读取身份证图片文件失败:', error)
|
||||||
|
showMessage('身份证照片处理失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('选择身份证图片失败:', error)
|
||||||
|
showMessage('选择身份证照片失败,请重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交认证
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!idCardFront.value) {
|
||||||
|
showMessage('请先上传身份证正面照片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前用户的司机ID
|
||||||
|
const storedUserInfo = getUserInfo()
|
||||||
|
const driverId = storedUserInfo?.driverId || storedUserInfo?.id || storedUserInfo?.userId
|
||||||
|
|
||||||
|
if (!driverId) {
|
||||||
|
console.error('未找到司机ID,无法提交认证')
|
||||||
|
showMessage('用户信息不完整,请重新登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始提交实名认证')
|
||||||
|
const submitData = {
|
||||||
|
id: driverId,
|
||||||
|
idCardFront: idCardFront.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await driverAPI.updateIdCard(submitData)
|
||||||
|
console.log('实名认证提交响应:', response)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
console.log('实名认证提交成功')
|
||||||
|
showMessage('实名认证提交成功,请等待审核')
|
||||||
|
|
||||||
|
// 刷新司机数据以获取最新的认证状态
|
||||||
|
await refreshDriverData()
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/index/index' })
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '实名认证提交失败'
|
||||||
|
console.error('实名认证提交业务失败:', response.data)
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMsg = response.data?.msg || response.data?.message || '实名认证提交失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('实名认证提交失败:', error)
|
||||||
|
showMessage('实名认证提交失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.identity-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 60rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证说明 */
|
||||||
|
.description-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态徽章 */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: rgba(255, 193, 7, 0.2);
|
||||||
|
color: #ffc107;
|
||||||
|
border: 1rpx solid rgba(255, 193, 7, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-normal {
|
||||||
|
background: rgba(40, 167, 69, 0.2);
|
||||||
|
color: #28a745;
|
||||||
|
border: 1rpx solid rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-frozen {
|
||||||
|
background: rgba(220, 53, 69, 0.2);
|
||||||
|
color: #dc3545;
|
||||||
|
border: 1rpx solid rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-unknown {
|
||||||
|
background: rgba(108, 117, 125, 0.2);
|
||||||
|
color: #6c757d;
|
||||||
|
border: 1rpx solid rgba(108, 117, 125, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4757;
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-preview {
|
||||||
|
width: 600rpx;
|
||||||
|
height: 400rpx;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-preview:active {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-card-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-tips {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 认证须知 */
|
||||||
|
.notice-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-section {
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast样式 */
|
||||||
|
.toast-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin: 0 60rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已认证状态样式 */
|
||||||
|
.verified-section {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 2rpx solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #28a745;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #28a745;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-desc {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-to-home-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/pages/realname/test.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<view>
|
||||||
|
<text>test</text>
|
||||||
|
</view>
|
||||||
3
src/pages/register/index.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '注册'
|
||||||
|
}
|
||||||
843
src/pages/register/index.vue
Normal file
@@ -0,0 +1,843 @@
|
|||||||
|
<template>
|
||||||
|
<view class="register-page">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<view class="header">
|
||||||
|
<view class="back-btn" @click="goBack">
|
||||||
|
<Right size="32" color="white" style="transform: rotate(180deg);" />
|
||||||
|
</view>
|
||||||
|
<text class="title">注册账号</text>
|
||||||
|
<view class="placeholder"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 注册表单 -->
|
||||||
|
<view class="register-form">
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="input-label">
|
||||||
|
<My size="32" color="#999" />
|
||||||
|
<text class="label-text">手机号</text>
|
||||||
|
</view>
|
||||||
|
<nut-input v-model="registerForm.phone" placeholder="请输入手机号" type="number" maxlength="11"
|
||||||
|
class="form-input" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="input-label">
|
||||||
|
<text class="label-text">验证码</text>
|
||||||
|
</view>
|
||||||
|
<view class="verify-code-container">
|
||||||
|
<nut-input v-model="registerForm.code" placeholder="请输入验证码" type="number" maxlength="6"
|
||||||
|
class="form-input verify-input" />
|
||||||
|
<nut-button class="verify-btn" @click="sendVerifyCode" :disabled="!canSendCode || countdown > 0"
|
||||||
|
size="small" type="primary">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||||
|
</nut-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="input-label">
|
||||||
|
<Setting size="32" color="#999" />
|
||||||
|
<text class="label-text">设置密码</text>
|
||||||
|
</view>
|
||||||
|
<nut-input v-model="registerForm.password" placeholder="请输入密码(6-20位)" type="password"
|
||||||
|
class="form-input" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<view class="input-label">
|
||||||
|
<Setting size="32" color="#999" />
|
||||||
|
<text class="label-text">确认密码</text>
|
||||||
|
</view>
|
||||||
|
<nut-input v-model="registerForm.confirmPassword" placeholder="请再次输入密码" type="password"
|
||||||
|
class="form-input" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 用户协议 -->
|
||||||
|
<view class="agreement-section">
|
||||||
|
<view class="agreement-check">
|
||||||
|
<view class="checkbox" :class="{ 'checked': agreeAgreement }" @click="toggleAgreement">
|
||||||
|
<text v-if="agreeAgreement" class="checkmark">✓</text>
|
||||||
|
</view>
|
||||||
|
<view class="agreement-text">
|
||||||
|
我已阅读并同意
|
||||||
|
<view class="agreement-link" @click.stop="viewAgreement">《用户协议》</view>
|
||||||
|
和
|
||||||
|
<view class="agreement-link" @click.stop="viewPrivacy">《隐私政策》</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 注册按钮 -->
|
||||||
|
<nut-button class="register-btn" @click="handleRegister" :disabled="isLoading" type="primary" size="large"
|
||||||
|
block>
|
||||||
|
{{ isLoading ? '注册中...' : '立即注册' }}
|
||||||
|
</nut-button>
|
||||||
|
|
||||||
|
<!-- 登录链接 -->
|
||||||
|
<view class="login-link" @click="goToLogin">
|
||||||
|
<text class="login-text">已有账号?</text>
|
||||||
|
<text class="login-btn">立即登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户协议弹窗 -->
|
||||||
|
<view v-if="showAgreementModal" class="modal-overlay" @click="closeAgreementModal">
|
||||||
|
<view class="modal-content" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">用户协议</text>
|
||||||
|
<view class="modal-close" @click="closeAgreementModal">×</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="modal-body" scroll-y>
|
||||||
|
<text class="agreement-content">
|
||||||
|
欢迎使用我们的服务!在使用我们的服务前,请仔细阅读并同意以下条款:
|
||||||
|
|
||||||
|
【服务条款】
|
||||||
|
1. 您在使用我们的服务时,应当遵守相关法律法规,不得利用我们的服务从事违法违规活动。
|
||||||
|
2. 您应当保证所提供信息的真实性、准确性和完整性。
|
||||||
|
3. 我们有权根据业务需要调整服务内容,并提前通知用户。
|
||||||
|
|
||||||
|
【用户责任】
|
||||||
|
1. 您应当妥善保管账户信息,不得将账户借给他人使用。
|
||||||
|
2. 您应当对账户下的所有行为负责。
|
||||||
|
3. 如发现账户被盗用,请立即联系我们。
|
||||||
|
|
||||||
|
【服务限制】
|
||||||
|
1. 我们保留在必要时限制或终止服务的权利。
|
||||||
|
2. 对于违反本协议的行为,我们有权采取相应措施。
|
||||||
|
|
||||||
|
【免责声明】
|
||||||
|
1. 我们不对因使用服务而产生的任何直接或间接损失承担责任。
|
||||||
|
2. 我们不对第三方服务的内容、质量或可用性承担责任。
|
||||||
|
|
||||||
|
【协议修改】
|
||||||
|
1. 我们有权随时修改本协议,修改后的协议将在平台上公布。
|
||||||
|
2. 如您不同意修改后的协议,可以停止使用我们的服务。
|
||||||
|
|
||||||
|
【联系方式】
|
||||||
|
如有疑问,请联系我们的客服团队。
|
||||||
|
|
||||||
|
本协议自您同意之日起生效。
|
||||||
|
</text>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<nut-button class="modal-btn confirm-btn" @click="closeAgreementModal" type="primary" size="large"
|
||||||
|
block>我已阅读</nut-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 隐私政策弹窗 -->
|
||||||
|
<view v-if="showPrivacyModal" class="modal-overlay" @click="closePrivacyModal">
|
||||||
|
<view class="modal-content" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">隐私政策</text>
|
||||||
|
<view class="modal-close" @click="closePrivacyModal">×</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="modal-body" scroll-y>
|
||||||
|
<text class="privacy-content">
|
||||||
|
我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息:
|
||||||
|
|
||||||
|
【信息收集】
|
||||||
|
我们可能收集的信息包括:
|
||||||
|
• 个人身份信息(姓名、手机号等)
|
||||||
|
• 设备信息(设备型号、操作系统等)
|
||||||
|
• 使用信息(操作记录、偏好设置等)
|
||||||
|
• 位置信息(在您授权的情况下)
|
||||||
|
|
||||||
|
【信息使用】
|
||||||
|
我们使用收集的信息用于:
|
||||||
|
• 提供和改进我们的服务
|
||||||
|
• 与您沟通和提供客户支持
|
||||||
|
• 确保服务安全和防止欺诈
|
||||||
|
• 遵守法律法规要求
|
||||||
|
|
||||||
|
我们不会将您的个人信息出售给第三方。
|
||||||
|
|
||||||
|
【信息保护】
|
||||||
|
我们采用行业标准的安全措施保护您的信息,限制员工访问权限,定期审查和更新安全措施。
|
||||||
|
|
||||||
|
【信息共享】
|
||||||
|
我们仅在获得您的明确同意、法律法规要求或保护我们合法权益的情况下共享您的信息。
|
||||||
|
|
||||||
|
【您的权利】
|
||||||
|
您有权访问、更正或删除您的个人信息,有权撤回对信息处理的同意,有权要求我们停止处理您的个人信息。
|
||||||
|
|
||||||
|
【信息保留】
|
||||||
|
我们仅在必要期间内保留您的信息,当信息不再需要时,我们会安全删除或匿名化处理。
|
||||||
|
|
||||||
|
【政策更新】
|
||||||
|
我们可能会更新本隐私政策,重大变更将通过适当方式通知您。
|
||||||
|
|
||||||
|
【联系我们】
|
||||||
|
如有疑问,请联系我们的客服团队。
|
||||||
|
|
||||||
|
本隐私政策自发布之日起生效。
|
||||||
|
</text>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<nut-button class="modal-btn confirm-btn" @click="closePrivacyModal" type="primary" size="large"
|
||||||
|
block>我已阅读</nut-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<view v-if="showToast" class="toast-overlay" @click="showToast = false">
|
||||||
|
<view class="toast-content">
|
||||||
|
<text class="toast-text">{{ toastMsg }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { My, Setting, Right } from '@nutui/icons-vue'
|
||||||
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
|
import { userAPI, driverAPI } from '../../api/index'
|
||||||
|
// import { NutInput, NutButton, NutCheckbox } from '@nutui/nutui-taro'
|
||||||
|
|
||||||
|
// 注册表单数据
|
||||||
|
const registerForm = reactive({
|
||||||
|
phone: '',
|
||||||
|
code: '',
|
||||||
|
password: '',
|
||||||
|
type: 3,
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const agreeAgreement = ref(false)
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
const countdown = ref(0)
|
||||||
|
const showAgreementModal = ref(false)
|
||||||
|
const showPrivacyModal = ref(false)
|
||||||
|
const hasShownRegisterDisabled = ref(false) // 防止重复显示注册已关闭提示
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const canSendCode = computed(() => {
|
||||||
|
return /^1[3-9]\d{9}$/.test(registerForm.phone)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示提示信息
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查注册功能是否已关闭
|
||||||
|
onMounted(() => {
|
||||||
|
if (!hasShownRegisterDisabled.value) {
|
||||||
|
hasShownRegisterDisabled.value = true
|
||||||
|
// 注册功能已关闭,显示提示并跳转到登录页
|
||||||
|
Taro.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '注册功能已关闭,请联系管理员获取账号',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '返回登录',
|
||||||
|
success: () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面显示时也检查
|
||||||
|
useDidShow(() => {
|
||||||
|
if (!hasShownRegisterDisabled.value) {
|
||||||
|
hasShownRegisterDisabled.value = true
|
||||||
|
// 注册功能已关闭,显示提示并跳转到登录页
|
||||||
|
Taro.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '注册功能已关闭,请联系管理员获取账号',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '返回登录',
|
||||||
|
success: () => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送验证码
|
||||||
|
const sendVerifyCode = async () => {
|
||||||
|
// 注册功能已关闭
|
||||||
|
showMessage('注册功能已关闭,请联系管理员获取账号')
|
||||||
|
return
|
||||||
|
|
||||||
|
// 检查手机号是否为空
|
||||||
|
if (!String(registerForm.phone).trim()) {
|
||||||
|
showMessage('请输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号格式是否正确
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(String(registerForm.phone))) {
|
||||||
|
showMessage('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用发送短信验证码API
|
||||||
|
const smsData = {
|
||||||
|
phone: registerForm.phone
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await userAPI.sendSmsCode(smsData)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data?.code === 0) {
|
||||||
|
// 发送成功,不显示提示,直接开始倒计时
|
||||||
|
countdown.value = 60
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
showMessage(response.data?.msg || '发送失败,请重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送验证码失败:', error)
|
||||||
|
showMessage('发送失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换协议同意状态
|
||||||
|
const toggleAgreement = () => {
|
||||||
|
agreeAgreement.value = !agreeAgreement.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看用户协议
|
||||||
|
const viewAgreement = () => {
|
||||||
|
showAgreementModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看隐私政策
|
||||||
|
const viewPrivacy = () => {
|
||||||
|
showPrivacyModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭用户协议弹窗
|
||||||
|
const closeAgreementModal = () => {
|
||||||
|
showAgreementModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭隐私政策弹窗
|
||||||
|
const closePrivacyModal = () => {
|
||||||
|
showPrivacyModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理注册
|
||||||
|
const handleRegister = async () => {
|
||||||
|
// 注册功能已关闭
|
||||||
|
showMessage('注册功能已关闭,请联系管理员获取账号')
|
||||||
|
return
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
if (!String(registerForm.phone).trim()) {
|
||||||
|
showMessage('请输入手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(String(registerForm.phone))) {
|
||||||
|
showMessage('请输入正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(registerForm.code).trim()) {
|
||||||
|
showMessage('请输入验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(registerForm.code).length !== 6) {
|
||||||
|
showMessage('请输入6位验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(registerForm.password).trim()) {
|
||||||
|
showMessage('请输入密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(registerForm.password).length < 6 || String(registerForm.password).length > 20) {
|
||||||
|
showMessage('密码长度应为6-20位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(registerForm.password) !== String(registerForm.confirmPassword)) {
|
||||||
|
showMessage('两次输入的密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agreeAgreement.value) {
|
||||||
|
showMessage('请先同意用户协议和隐私政策')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 第一步:调用用户注册接口
|
||||||
|
const userData = {
|
||||||
|
username: String(registerForm.phone),
|
||||||
|
password: String(registerForm.password),
|
||||||
|
phone: String(registerForm.phone),
|
||||||
|
code: String(registerForm.code),
|
||||||
|
type: '3'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始用户注册,数据:', userData)
|
||||||
|
const userResponse = await userAPI.registerUser(userData)
|
||||||
|
console.log('用户注册响应:', userResponse)
|
||||||
|
|
||||||
|
if (userResponse.statusCode === 200 && userResponse.data) {
|
||||||
|
// 检查用户注册业务状态码
|
||||||
|
if (userResponse.data.code === 0) {
|
||||||
|
// 用户注册业务成功,继续司机注册
|
||||||
|
const driverData = {
|
||||||
|
driverTel: String(registerForm.phone),
|
||||||
|
password: String(registerForm.password), // 使用原始密码
|
||||||
|
type: 1,
|
||||||
|
code: String(registerForm.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('开始司机注册,数据:', driverData)
|
||||||
|
const driverResponse = await driverAPI.registerDriver(driverData)
|
||||||
|
console.log('司机注册响应:', driverResponse)
|
||||||
|
|
||||||
|
if (driverResponse.statusCode === 200 && driverResponse.data) {
|
||||||
|
// 检查司机注册业务状态码
|
||||||
|
if (driverResponse.data.code === 0) {
|
||||||
|
// 司机注册业务成功
|
||||||
|
showMessage('注册成功,请登录')
|
||||||
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
// 司机注册业务失败
|
||||||
|
const errorMsg = driverResponse.data?.msg || driverResponse.data?.message || '司机注册失败'
|
||||||
|
console.error('司机注册业务失败:', driverResponse.data)
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 司机注册HTTP请求失败
|
||||||
|
const errorMsg = driverResponse.data?.msg || driverResponse.data?.message || '司机注册失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 用户注册业务失败
|
||||||
|
const errorMsg = userResponse.data?.msg || userResponse.data?.message || '用户注册失败'
|
||||||
|
console.error('用户注册业务失败:', userResponse.data)
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 用户注册HTTP请求失败
|
||||||
|
const errorMsg = userResponse.data?.msg || userResponse.data?.message || '用户注册失败'
|
||||||
|
showMessage(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册失败:', error)
|
||||||
|
showMessage('注册失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
const goToLogin = () => {
|
||||||
|
Taro.navigateTo({ url: 'pages/login/index' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.register-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部区域 */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 60rpx 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册表单 */
|
||||||
|
.register-form {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
border: 2rpx solid #f0f0f0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
/* 修复真机显示问题 */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
/* 修复字体渲染 */
|
||||||
|
font-family: inherit;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NutUI Input 样式覆盖 */
|
||||||
|
.form-input .nut-input {
|
||||||
|
height: 80rpx;
|
||||||
|
border: 2rpx solid #f0f0f0;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input .nut-input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input .nut-input__inner {
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input .nut-input__inner::placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 验证码容器 */
|
||||||
|
.verify-code-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-btn {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 协议区域 */
|
||||||
|
.agreement-section {
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox.checked {
|
||||||
|
background: #1976d2;
|
||||||
|
border-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
color: white;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-text {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-text view {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注册按钮 */
|
||||||
|
.register-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NutUI Button 样式覆盖 */
|
||||||
|
.register-btn.nut-button {
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-btn.nut-button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录链接 */
|
||||||
|
.login-link {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast样式 */
|
||||||
|
.toast-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin: 0 60rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600rpx;
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 30rpx 30rpx 20rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:active {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
max-height: 65vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-content,
|
||||||
|
.privacy-content {
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
white-space: pre-line;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 20rpx 30rpx 30rpx;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
background: #1976d2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:active {
|
||||||
|
background: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NutUI Button 样式覆盖 */
|
||||||
|
.modal-btn.nut-button {
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #1976d2;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn.nut-button:active {
|
||||||
|
background: #1565c0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/pages/statistics/index.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '数据统计'
|
||||||
|
}
|
||||||
551
src/pages/statistics/index.vue
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
<template>
|
||||||
|
<view class="statistics-page">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">数据统计</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<view class="stats-cards">
|
||||||
|
<view class="stats-card">
|
||||||
|
<view class="stats-icon-container">
|
||||||
|
<image src="/src/assets/images/icon-1.svg" class="stats-icon" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view class="stats-content">
|
||||||
|
<text class="stats-value">{{ todayStats.count }}单</text>
|
||||||
|
<text class="stats-label">今日订单</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="stats-card">
|
||||||
|
<view class="stats-icon-container">
|
||||||
|
<image src="/src/assets/images/icon-2.svg" class="stats-icon" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view class="stats-content">
|
||||||
|
<text class="stats-value">{{ todayStats.planWeight }}千克</text>
|
||||||
|
<text class="stats-label">预计重量</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="stats-card">
|
||||||
|
<view class="stats-icon-container">
|
||||||
|
<image src="/src/assets/images/icon-3.svg" class="stats-icon" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view class="stats-content">
|
||||||
|
<text class="stats-value">{{ todayStats.realWeight }}千克</text>
|
||||||
|
<text class="stats-label">实际重量</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<view class="charts-section">
|
||||||
|
<!-- 重量趋势图 -->
|
||||||
|
<view class="chart-container">
|
||||||
|
<view class="chart-title">重量趋势</view>
|
||||||
|
<view v-if="chartLoading" class="chart-loading">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="chartData.length === 0" class="chart-empty">
|
||||||
|
<text>暂无数据</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="chart-data">
|
||||||
|
<view class="chart-summary">
|
||||||
|
<text class="summary-text">最近7天总重量: {{ getTotalWeight() }}千克</text>
|
||||||
|
</view>
|
||||||
|
<!-- ECharts折线图 -->
|
||||||
|
<canvas id="weightTrendChart" canvas-id="weightTrendChart" class="chart-canvas" @touchstart="touchStart"
|
||||||
|
@touchmove="touchMove" @touchend="touchEnd">
|
||||||
|
</canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 重量对比图 -->
|
||||||
|
<view class="chart-container">
|
||||||
|
<view class="chart-title">重量对比</view>
|
||||||
|
<canvas canvas-id="weightChart" class="chart-canvas" @touchstart="touchStart" @touchmove="touchMove"
|
||||||
|
@touchend="touchEnd"></canvas>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 状态分布图 -->
|
||||||
|
<view class="chart-container">
|
||||||
|
<view class="chart-title">状态分布</view>
|
||||||
|
<canvas canvas-id="statusChart" class="chart-canvas" @touchstart="touchStart" @touchmove="touchMove"
|
||||||
|
@touchend="touchEnd"></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
import { getUserInfo } from '../../utils/auth.js'
|
||||||
|
import { orderAPI } from '../../api/index.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StatisticsPage',
|
||||||
|
setup() {
|
||||||
|
// 统计数据
|
||||||
|
const todayStats = ref({
|
||||||
|
count: 0,
|
||||||
|
planWeight: 0,
|
||||||
|
realWeight: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图表数据
|
||||||
|
const chartData = ref([])
|
||||||
|
const chartLoading = ref(false)
|
||||||
|
|
||||||
|
// 获取今日统计数据
|
||||||
|
const fetchTodayStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await orderAPI.getTodayTotal()
|
||||||
|
console.log('今日统计API响应:', response)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
// 检查业务状态码
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
// 业务成功
|
||||||
|
todayStats.value = {
|
||||||
|
count: response.data.data.count || 0,
|
||||||
|
planWeight: response.data.data.planWeight || 0,
|
||||||
|
realWeight: response.data.data.realWeight || 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 业务失败,显示具体错误信息
|
||||||
|
const errorMsg = response.data?.msg || '获取统计数据失败'
|
||||||
|
console.error('今日统计业务失败:', response.data)
|
||||||
|
wx.showToast({
|
||||||
|
title: errorMsg,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取今日统计失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图表数据
|
||||||
|
const fetchChartData = async () => {
|
||||||
|
try {
|
||||||
|
chartLoading.value = true
|
||||||
|
const response = await orderAPI.getChart()
|
||||||
|
console.log('图表数据API响应:', response)
|
||||||
|
|
||||||
|
if (response.statusCode === 200 && response.data) {
|
||||||
|
if (response.data.code === 0 && Array.isArray(response.data.data)) {
|
||||||
|
// 处理图表数据
|
||||||
|
chartData.value = response.data.data.map(item => ({
|
||||||
|
day: item.day,
|
||||||
|
realWeight: parseFloat(item.realWeight) || 0
|
||||||
|
}))
|
||||||
|
console.log('图表数据处理完成:', chartData.value)
|
||||||
|
|
||||||
|
// 数据获取成功后初始化图表
|
||||||
|
await nextTick()
|
||||||
|
// 延迟一点时间确保DOM完全渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
initWeightTrendChart()
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
console.error('图表数据业务失败:', response.data)
|
||||||
|
wx.showToast({
|
||||||
|
title: response.data?.msg || '获取图表数据失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取图表数据失败:', error)
|
||||||
|
wx.showToast({
|
||||||
|
title: '获取图表数据失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
chartLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总重量
|
||||||
|
const getTotalWeight = () => {
|
||||||
|
return chartData.value.reduce((total, item) => total + item.realWeight, 0).toFixed(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
return `${month}月${day}日`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制图表函数
|
||||||
|
const drawChart = (ctx, weights, dates, canvasWidth, canvasHeight, padding, chartWidth, chartHeight, maxWeight, minWeight, weightRange) => {
|
||||||
|
// 绘制背景
|
||||||
|
ctx.setFillStyle('#f8f9fa')
|
||||||
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
|
||||||
|
// 绘制坐标轴
|
||||||
|
ctx.setStrokeStyle('#ddd')
|
||||||
|
ctx.setLineWidth(1)
|
||||||
|
|
||||||
|
// Y轴
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(padding, padding)
|
||||||
|
ctx.lineTo(padding, canvasHeight - padding)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// X轴
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(padding, canvasHeight - padding)
|
||||||
|
ctx.lineTo(canvasWidth - padding, canvasHeight - padding)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 绘制网格线
|
||||||
|
ctx.setStrokeStyle('#f0f0f0')
|
||||||
|
ctx.setLineWidth(0.5)
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
const y = padding + (chartHeight / 5) * i
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(padding, y)
|
||||||
|
ctx.lineTo(canvasWidth - padding, y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制折线
|
||||||
|
ctx.setStrokeStyle('#667eea')
|
||||||
|
ctx.setLineWidth(3)
|
||||||
|
ctx.beginPath()
|
||||||
|
|
||||||
|
weights.forEach((weight, index) => {
|
||||||
|
const x = padding + (chartWidth / (weights.length - 1)) * index
|
||||||
|
const y = canvasHeight - padding - ((weight - minWeight) / weightRange) * chartHeight
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
ctx.moveTo(x, y)
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 绘制数据点
|
||||||
|
ctx.setFillStyle('#667eea')
|
||||||
|
weights.forEach((weight, index) => {
|
||||||
|
const x = padding + (chartWidth / (weights.length - 1)) * index
|
||||||
|
const y = canvasHeight - padding - ((weight - minWeight) / weightRange) * chartHeight
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, 4, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 绘制Y轴标签
|
||||||
|
ctx.setFillStyle('#666')
|
||||||
|
ctx.setFontSize(10)
|
||||||
|
for (let i = 0; i <= 4; i++) {
|
||||||
|
const value = minWeight + (weightRange / 4) * (4 - i)
|
||||||
|
const y = padding + (chartHeight / 4) * i
|
||||||
|
ctx.fillText(value.toFixed(1), 5, y + 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制X轴标签
|
||||||
|
dates.forEach((date, index) => {
|
||||||
|
const x = padding + (chartWidth / (dates.length - 1)) * index
|
||||||
|
ctx.fillText(date, x - 10, canvasHeight - 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行绘制
|
||||||
|
ctx.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化重量趋势折线图
|
||||||
|
const initWeightTrendChart = () => {
|
||||||
|
try {
|
||||||
|
// 使用微信小程序原生canvas绘制折线图
|
||||||
|
const ctx = wx.createCanvasContext('weightTrendChart')
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const weights = chartData.value.map(item => item.realWeight)
|
||||||
|
const dates = chartData.value.map(item => formatDate(item.day))
|
||||||
|
|
||||||
|
if (weights.length === 0) {
|
||||||
|
console.warn('没有数据可绘制')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算数据范围
|
||||||
|
const maxWeight = Math.max(...weights)
|
||||||
|
const minWeight = Math.min(...weights)
|
||||||
|
const weightRange = maxWeight - minWeight || 1
|
||||||
|
|
||||||
|
// 获取canvas实际尺寸
|
||||||
|
const query = wx.createSelectorQuery()
|
||||||
|
query.select('#weightTrendChart')
|
||||||
|
.boundingClientRect()
|
||||||
|
.exec((res) => {
|
||||||
|
if (res[0]) {
|
||||||
|
const canvasWidth = res[0].width
|
||||||
|
const canvasHeight = res[0].height
|
||||||
|
const padding = 40
|
||||||
|
const chartWidth = canvasWidth - padding * 2
|
||||||
|
const chartHeight = canvasHeight - padding * 2
|
||||||
|
|
||||||
|
console.log('Canvas尺寸:', { canvasWidth, canvasHeight, chartWidth, chartHeight })
|
||||||
|
|
||||||
|
drawChart(ctx, weights, dates, canvasWidth, canvasHeight, padding, chartWidth, chartHeight, maxWeight, minWeight, weightRange)
|
||||||
|
|
||||||
|
console.log('重量趋势折线图绘制完成')
|
||||||
|
} else {
|
||||||
|
console.error('未找到canvas元素')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('绘制重量趋势折线图失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
const initCharts = () => {
|
||||||
|
console.log('初始化统计图表')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸事件处理
|
||||||
|
const touchStart = (e) => {
|
||||||
|
console.log('触摸开始', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const touchMove = (e) => {
|
||||||
|
console.log('触摸移动', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const touchEnd = (e) => {
|
||||||
|
console.log('触摸结束', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取统计数据
|
||||||
|
fetchTodayStats()
|
||||||
|
|
||||||
|
// 获取图表数据
|
||||||
|
fetchChartData()
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
initCharts()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面显示时重新获取数据
|
||||||
|
const onShow = () => {
|
||||||
|
// 重新获取统计数据
|
||||||
|
fetchTodayStats()
|
||||||
|
|
||||||
|
// 重新获取图表数据
|
||||||
|
fetchChartData()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
todayStats,
|
||||||
|
chartData,
|
||||||
|
chartLoading,
|
||||||
|
fetchTodayStats,
|
||||||
|
fetchChartData,
|
||||||
|
getTotalWeight,
|
||||||
|
formatDate,
|
||||||
|
initWeightTrendChart,
|
||||||
|
initCharts,
|
||||||
|
onShow,
|
||||||
|
touchStart,
|
||||||
|
touchMove,
|
||||||
|
touchEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.statistics-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片样式 */
|
||||||
|
.stats-cards {
|
||||||
|
display: flex;
|
||||||
|
padding: 20rpx;
|
||||||
|
gap: 12rpx;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-2rpx);
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon-container {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.2;
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏幕适配 */
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.stats-cards {
|
||||||
|
padding: 16rpx;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
min-width: 180rpx;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域样式 */
|
||||||
|
.charts-section {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 400rpx;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表数据样式 */
|
||||||
|
.chart-loading,
|
||||||
|
.chart-empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200rpx;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-data {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-summary {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-date {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-weight {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
src/pages/vehicle/index.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
navigationBarTitleText: '车辆定位',
|
||||||
|
navigationBarBackgroundColor: '#667eea',
|
||||||
|
navigationBarTextStyle: 'white'
|
||||||
|
}
|
||||||
549
src/pages/vehicle/index.vue
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
<template>
|
||||||
|
<view class="vehicle-page">
|
||||||
|
<view class="header">
|
||||||
|
<text class="title">车辆定位</text>
|
||||||
|
<text class="subtitle">获取当前车辆位置信息</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="content">
|
||||||
|
<!-- 获取位置按钮 -->
|
||||||
|
<button class="get-location-btn" @click="handleGetLocation" :disabled="isLoading">
|
||||||
|
{{ isLoading ? '获取中...' : '获取当前位置' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 位置信息显示 -->
|
||||||
|
<view v-if="locationInfo" class="location-info">
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="card-title">
|
||||||
|
<Location2 size="24" color="#667eea" />
|
||||||
|
<text class="title-text">位置信息</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">纬度:</text>
|
||||||
|
<text class="value">{{ locationInfo.latitude }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">经度:</text>
|
||||||
|
<text class="value">{{ locationInfo.longitude }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">高度:</text>
|
||||||
|
<text class="value">{{ locationInfo.altitude || '未知' }}m</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">精确度:</text>
|
||||||
|
<text class="value">{{ locationInfo.accuracy }}m</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">速度:</text>
|
||||||
|
<text class="value">{{ locationInfo.speed || 0 }}m/s</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">方向角:</text>
|
||||||
|
<text class="value">{{ locationInfo.heading || 0 }}°</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 地址信息显示 -->
|
||||||
|
<view v-if="addressInfo" class="address-info">
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="card-title">
|
||||||
|
<Location size="24" color="#667eea" />
|
||||||
|
<text class="title-text">地址信息</text>
|
||||||
|
</view>
|
||||||
|
<view class="address-item">
|
||||||
|
<text class="address-text">{{ addressInfo.address }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="addressInfo.formatted_addresses" class="address-item">
|
||||||
|
<text class="address-text">{{ addressInfo.formatted_addresses.recommend }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view v-if="locationInfo" class="action-buttons">
|
||||||
|
<button class="action-btn primary" @click="openMap">在地图中查看</button>
|
||||||
|
<button class="action-btn secondary" @click="copyLocation">复制坐标</button>
|
||||||
|
<button class="action-btn secondary" @click="shareLocation">分享位置</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 历史记录 -->
|
||||||
|
<view v-if="locationHistory.length > 0" class="history-section">
|
||||||
|
<view class="section-title">
|
||||||
|
<Clock size="20" color="#666" />
|
||||||
|
<text class="title-text">历史记录</text>
|
||||||
|
</view>
|
||||||
|
<view class="history-list">
|
||||||
|
<view v-for="(item, index) in locationHistory" :key="index" class="history-item">
|
||||||
|
<view class="history-time">{{ item.time }}</view>
|
||||||
|
<view class="history-coords">{{ item.latitude }}, {{ item.longitude }}</view>
|
||||||
|
<view class="history-actions">
|
||||||
|
<button class="mini-btn" @click="viewHistoryLocation(item)">查看</button>
|
||||||
|
<button class="mini-btn" @click="deleteHistoryItem(index)">删除</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Toast 提示 -->
|
||||||
|
<nut-toast v-model:visible="showToast" :msg="toastMsg" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Toast } from '@nutui/nutui-taro'
|
||||||
|
import { Location2, Location, Clock } from '@nutui/icons-vue'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { checkLoginAndRedirect } from '../../utils/auth.js'
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const locationInfo = ref(null)
|
||||||
|
const addressInfo = ref(null)
|
||||||
|
const showToast = ref(false)
|
||||||
|
const toastMsg = ref('')
|
||||||
|
const locationHistory = ref([])
|
||||||
|
|
||||||
|
// 显示提示信息
|
||||||
|
const showMessage = (msg) => {
|
||||||
|
toastMsg.value = msg
|
||||||
|
showToast.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查位置权限
|
||||||
|
const checkLocationPermission = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Taro.getSetting({
|
||||||
|
success: (res) => {
|
||||||
|
if (res.authSetting['scope.userLocation']) {
|
||||||
|
resolve(true)
|
||||||
|
} else {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求位置权限
|
||||||
|
const requestLocationPermission = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Taro.authorize({
|
||||||
|
scope: 'scope.userLocation',
|
||||||
|
success: () => {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开位置设置页面
|
||||||
|
const openLocationSetting = () => {
|
||||||
|
Taro.openSetting({
|
||||||
|
success: (res) => {
|
||||||
|
if (res.authSetting['scope.userLocation']) {
|
||||||
|
showMessage('位置权限已开启')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前位置
|
||||||
|
const getCurrentLocation = (options = {}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const defaultOptions = {
|
||||||
|
type: 'wgs84',
|
||||||
|
altitude: true,
|
||||||
|
isHighAccuracy: true,
|
||||||
|
highAccuracyExpireTime: 4000,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('获取位置成功:', res)
|
||||||
|
resolve(res)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('获取位置失败:', error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.getLocation({ ...defaultOptions, ...options })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取位置信息(包含权限检查)
|
||||||
|
const getLocationWithPermission = async (options = {}) => {
|
||||||
|
try {
|
||||||
|
// 检查权限
|
||||||
|
const hasPermission = await checkLocationPermission()
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
// 请求权限
|
||||||
|
const granted = await requestLocationPermission()
|
||||||
|
|
||||||
|
if (!granted) {
|
||||||
|
// 权限被拒绝,引导用户手动开启
|
||||||
|
Taro.showModal({
|
||||||
|
title: '位置权限',
|
||||||
|
content: '获取位置信息需要开启位置权限,请在设置中开启位置权限',
|
||||||
|
showCancel: true,
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '去设置',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
openLocationSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
throw new Error('位置权限被拒绝')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取位置信息
|
||||||
|
const location = await getCurrentLocation(options)
|
||||||
|
return location
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取位置信息失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理获取位置
|
||||||
|
const handleGetLocation = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取位置信息
|
||||||
|
const location = await getLocationWithPermission({
|
||||||
|
type: 'wgs84',
|
||||||
|
altitude: true,
|
||||||
|
isHighAccuracy: true
|
||||||
|
})
|
||||||
|
|
||||||
|
locationInfo.value = location
|
||||||
|
showMessage('获取位置成功')
|
||||||
|
|
||||||
|
// 保存到历史记录
|
||||||
|
const now = new Date()
|
||||||
|
const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
||||||
|
|
||||||
|
const historyItem = {
|
||||||
|
...location,
|
||||||
|
time: timeStr,
|
||||||
|
timestamp: now.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
locationHistory.value.unshift(historyItem)
|
||||||
|
|
||||||
|
// 限制历史记录数量
|
||||||
|
if (locationHistory.value.length > 10) {
|
||||||
|
locationHistory.value = locationHistory.value.slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
Taro.setStorageSync('locationHistory', locationHistory.value)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取位置失败:', error)
|
||||||
|
showMessage('获取位置失败:' + error.message)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在地图中查看位置
|
||||||
|
const openMap = () => {
|
||||||
|
if (!locationInfo.value) return
|
||||||
|
|
||||||
|
Taro.openLocation({
|
||||||
|
latitude: locationInfo.value.latitude,
|
||||||
|
longitude: locationInfo.value.longitude,
|
||||||
|
name: '当前位置',
|
||||||
|
address: addressInfo.value?.address || '未知地址',
|
||||||
|
scale: 18
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制坐标
|
||||||
|
const copyLocation = () => {
|
||||||
|
if (!locationInfo.value) return
|
||||||
|
|
||||||
|
const coordinate = `${locationInfo.value.latitude}, ${locationInfo.value.longitude}`
|
||||||
|
|
||||||
|
Taro.setClipboardData({
|
||||||
|
data: coordinate,
|
||||||
|
success: () => {
|
||||||
|
showMessage('坐标已复制到剪贴板')
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
showMessage('复制失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享位置
|
||||||
|
const shareLocation = () => {
|
||||||
|
if (!locationInfo.value) return
|
||||||
|
|
||||||
|
const shareText = `我的位置:${locationInfo.value.latitude}, ${locationInfo.value.longitude}`
|
||||||
|
|
||||||
|
Taro.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
success: () => {
|
||||||
|
showMessage('位置已分享')
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
showMessage('分享失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看历史位置
|
||||||
|
const viewHistoryLocation = (item) => {
|
||||||
|
locationInfo.value = item
|
||||||
|
showMessage('已加载历史位置')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除历史记录项
|
||||||
|
const deleteHistoryItem = (index) => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除这条历史记录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
locationHistory.value.splice(index, 1)
|
||||||
|
Taro.setStorageSync('locationHistory', locationHistory.value)
|
||||||
|
showMessage('删除成功')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时读取历史记录
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!checkLoginAndRedirect()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const history = Taro.getStorageSync('locationHistory')
|
||||||
|
if (history && Array.isArray(history)) {
|
||||||
|
locationHistory.value = history
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取历史记录失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vehicle-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-location-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-location-btn:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-info,
|
||||||
|
.address-info {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item {
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 72rpx;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.secondary {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-section {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .title-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
min-width: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-coords {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-btn {
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
280
src/utils/auth.js
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* 认证状态管理工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 存储键名常量
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
USER_INFO: 'userInfo',
|
||||||
|
TOKEN: 'token',
|
||||||
|
IS_LOGIN: 'isLogin',
|
||||||
|
TOKEN_EXPIRE_TIME: 'tokenExpireTime'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局状态管理
|
||||||
|
let isRedirectingToLogin = false
|
||||||
|
let tokenRefreshPromise = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存用户信息到本地存储
|
||||||
|
* @param {Object} userInfo 用户信息
|
||||||
|
*/
|
||||||
|
export function saveUserInfo(userInfo) {
|
||||||
|
try {
|
||||||
|
wx.setStorageSync(STORAGE_KEYS.USER_INFO, userInfo)
|
||||||
|
wx.setStorageSync(STORAGE_KEYS.TOKEN, userInfo.token)
|
||||||
|
wx.setStorageSync(STORAGE_KEYS.IS_LOGIN, true)
|
||||||
|
|
||||||
|
// 设置token过期时间(默认24小时)
|
||||||
|
const expireTime = Date.now() + (24 * 60 * 60 * 1000)
|
||||||
|
wx.setStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME, expireTime)
|
||||||
|
|
||||||
|
console.log('用户信息已保存')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存用户信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @returns {Object|null} 用户信息对象或null
|
||||||
|
*/
|
||||||
|
export function getUserInfo() {
|
||||||
|
try {
|
||||||
|
return wx.getStorageSync(STORAGE_KEYS.USER_INFO) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取token
|
||||||
|
* @returns {string|null} token或null
|
||||||
|
*/
|
||||||
|
export function getToken() {
|
||||||
|
try {
|
||||||
|
return wx.getStorageSync(STORAGE_KEYS.TOKEN) || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取token失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查token是否过期
|
||||||
|
* @returns {boolean} token是否过期
|
||||||
|
*/
|
||||||
|
export function isTokenExpired() {
|
||||||
|
try {
|
||||||
|
const expireTime = wx.getStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME)
|
||||||
|
if (!expireTime) {
|
||||||
|
return true // 没有过期时间记录,认为已过期
|
||||||
|
}
|
||||||
|
return Date.now() > expireTime
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查token过期时间失败:', error)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已登录
|
||||||
|
* @returns {boolean} 是否已登录
|
||||||
|
*/
|
||||||
|
export function isLoggedIn() {
|
||||||
|
try {
|
||||||
|
const isLogin = wx.getStorageSync(STORAGE_KEYS.IS_LOGIN)
|
||||||
|
const token = getToken()
|
||||||
|
const isExpired = isTokenExpired()
|
||||||
|
|
||||||
|
// 如果token过期,清除登录状态
|
||||||
|
if (isExpired && isLogin) {
|
||||||
|
console.log('Token已过期,清除登录状态')
|
||||||
|
clearUserInfo()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLogin && token && !isExpired
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查登录状态失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否为司机
|
||||||
|
* @returns {boolean} 是否为司机
|
||||||
|
*/
|
||||||
|
export function isDriver() {
|
||||||
|
try {
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
return userInfo && userInfo.isDriver === true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查司机身份失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取司机信息
|
||||||
|
* @returns {Object|null} 司机信息
|
||||||
|
*/
|
||||||
|
export function getDriverInfo() {
|
||||||
|
try {
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
return userInfo && userInfo.driverInfo ? userInfo.driverInfo : null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取司机信息失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除用户信息
|
||||||
|
*/
|
||||||
|
export function clearUserInfo() {
|
||||||
|
try {
|
||||||
|
wx.removeStorageSync(STORAGE_KEYS.USER_INFO)
|
||||||
|
wx.removeStorageSync(STORAGE_KEYS.TOKEN)
|
||||||
|
wx.removeStorageSync(STORAGE_KEYS.IS_LOGIN)
|
||||||
|
wx.removeStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME)
|
||||||
|
console.log('用户信息已清除')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除用户信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试刷新token
|
||||||
|
* @returns {Promise<boolean>} 是否刷新成功
|
||||||
|
*/
|
||||||
|
export async function refreshToken() {
|
||||||
|
// 如果已经在刷新中,等待结果
|
||||||
|
if (tokenRefreshPromise) {
|
||||||
|
return await tokenRefreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRefreshPromise = new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
if (!userInfo || !userInfo.userId) {
|
||||||
|
console.log('无法刷新token:用户信息不完整')
|
||||||
|
resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以调用刷新token的API
|
||||||
|
// 由于当前没有刷新token的API,我们直接返回false
|
||||||
|
console.log('当前没有token刷新API,需要重新登录')
|
||||||
|
resolve(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新token失败:', error)
|
||||||
|
resolve(false)
|
||||||
|
} finally {
|
||||||
|
tokenRefreshPromise = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return await tokenRefreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查登录状态,未登录则跳转到登录页
|
||||||
|
* @returns {boolean} 是否已登录
|
||||||
|
*/
|
||||||
|
export function checkLoginAndRedirect() {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
// 防止重复跳转
|
||||||
|
if (isRedirectingToLogin) {
|
||||||
|
console.log('正在跳转登录页,跳过重复跳转')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('用户未登录,跳转到登录页')
|
||||||
|
isRedirectingToLogin = true
|
||||||
|
|
||||||
|
// 检查当前页面是否为登录页
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const isLoginPage = currentPage && currentPage.route && currentPage.route.includes('login')
|
||||||
|
|
||||||
|
if (!isLoginPage) {
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '/pages/login/index',
|
||||||
|
success: () => {
|
||||||
|
console.log('成功跳转到登录页')
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('跳转登录页失败:', error)
|
||||||
|
isRedirectingToLogin = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('当前已在登录页,无需跳转')
|
||||||
|
isRedirectingToLogin = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否已完成实名认证
|
||||||
|
* @returns {boolean} 是否已完成实名认证
|
||||||
|
*/
|
||||||
|
export function isIdentityVerified() {
|
||||||
|
try {
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
if (!userInfo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查多种可能的实名认证状态字段
|
||||||
|
const driverStatus = userInfo.driverInfo?.data?.driverStatus ||
|
||||||
|
userInfo.driverInfo?.driverStatus
|
||||||
|
const identityVerified = userInfo.identityVerified ||
|
||||||
|
userInfo.driverInfo?.identityVerified ||
|
||||||
|
userInfo.driverInfo?.data?.identityVerified
|
||||||
|
|
||||||
|
// 司机状态为1(正常)或者明确标记为已认证
|
||||||
|
return driverStatus === '1' || !!identityVerified
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查实名认证状态失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查实名认证状态,未认证则跳转到实名认证页
|
||||||
|
* @returns {boolean} 是否已完成实名认证
|
||||||
|
*/
|
||||||
|
export function checkIdentityAndRedirect() {
|
||||||
|
if (!isIdentityVerified()) {
|
||||||
|
console.log('用户未完成实名认证,跳转到实名认证页')
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '/pages/realname/index?from=home'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置跳转状态
|
||||||
|
*/
|
||||||
|
export function resetRedirectState() {
|
||||||
|
isRedirectingToLogin = false
|
||||||
|
tokenRefreshPromise = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
export function logout() {
|
||||||
|
clearUserInfo()
|
||||||
|
resetRedirectState()
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '/pages/login/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
24
src/utils/crypto.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES加密函数
|
||||||
|
* @param {string} word 要加密的内容
|
||||||
|
* @param {string} keyWord 服务器随机返回的关键字,默认为 'XwKsGlMcdPMEhR1B'
|
||||||
|
* @returns {string} 加密后的字符串
|
||||||
|
*/
|
||||||
|
export function aesEncrypt(word, keyWord = 'pigxpigxpigxpigx') {
|
||||||
|
try {
|
||||||
|
const key = CryptoJS.enc.Utf8.parse(keyWord);
|
||||||
|
// 加密
|
||||||
|
var encrypted = CryptoJS.AES.encrypt(word, key, {
|
||||||
|
iv: key,
|
||||||
|
mode: CryptoJS.mode.CFB,
|
||||||
|
padding: CryptoJS.pad.NoPadding,
|
||||||
|
});
|
||||||
|
return encrypted.toString();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加密失败:', error);
|
||||||
|
return word; // 如果加密失败,返回原密码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
205
src/utils/dictUtils.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* 字典数据工具函数
|
||||||
|
* 用于获取不同类型的字典数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 字典类型常量
|
||||||
|
export const DICT_TYPES = {
|
||||||
|
CAR_TYPE: 'car_type', // 车辆类型
|
||||||
|
COMPANY_TYPE: 'company_type', // 单位类型
|
||||||
|
CAR_STATUS: 'car_status', // 车辆状态
|
||||||
|
DRIVER_STATUS: 'driver_status', // 司机状态
|
||||||
|
GUFEI_ORDER_STATUS: 'gufei_order_status', // 运输状态
|
||||||
|
CAR_BACK_TYPE: 'car_back_type', // 尾挂
|
||||||
|
CAR_PACKAGE_TYPE: 'car_package_type', // 包装方式
|
||||||
|
GPS_STATUS: 'gps_status' // gps状态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认字典数据
|
||||||
|
const DEFAULT_DICT_DATA = {
|
||||||
|
[DICT_TYPES.CAR_TYPE]: [
|
||||||
|
{ text: '货车', value: 'truck' },
|
||||||
|
{ text: '客车', value: 'bus' },
|
||||||
|
{ text: '轿车', value: 'car' },
|
||||||
|
{ text: 'SUV', value: 'suv' },
|
||||||
|
{ text: '面包车', value: 'van' },
|
||||||
|
{ text: '其他', value: 'other' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.COMPANY_TYPE]: [
|
||||||
|
{ text: '国有企业', value: 'state_owned' },
|
||||||
|
{ text: '民营企业', value: 'private' },
|
||||||
|
{ text: '外资企业', value: 'foreign' },
|
||||||
|
{ text: '合资企业', value: 'joint_venture' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.CAR_STATUS]: [
|
||||||
|
{ text: '正常', value: 'normal' },
|
||||||
|
{ text: '维修中', value: 'maintenance' },
|
||||||
|
{ text: '停用', value: 'disabled' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.DRIVER_STATUS]: [
|
||||||
|
{ text: '在职', value: 'active' },
|
||||||
|
{ text: '休假', value: 'vacation' },
|
||||||
|
{ text: '离职', value: 'inactive' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.GUFEI_ORDER_STATUS]: [
|
||||||
|
{ text: '待运输', value: 'pending' },
|
||||||
|
{ text: '运输中', value: 'in_transit' },
|
||||||
|
{ text: '已完成', value: 'completed' },
|
||||||
|
{ text: '已取消', value: 'cancelled' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.CAR_BACK_TYPE]: [
|
||||||
|
{ text: '平板', value: 'flat' },
|
||||||
|
{ text: '厢式', value: 'box' },
|
||||||
|
{ text: '罐式', value: 'tank' },
|
||||||
|
{ text: '其他', value: 'other' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.CAR_PACKAGE_TYPE]: [
|
||||||
|
{ text: '散装', value: 'bulk' },
|
||||||
|
{ text: '包装', value: 'packaged' },
|
||||||
|
{ text: '集装箱', value: 'container' }
|
||||||
|
],
|
||||||
|
[DICT_TYPES.GPS_STATUS]: [
|
||||||
|
{ text: '在线', value: 'online' },
|
||||||
|
{ text: '离线', value: 'offline' },
|
||||||
|
{ text: '故障', value: 'fault' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典数据
|
||||||
|
* @param {string} type - 字典类型
|
||||||
|
* @returns {Promise<Array>} 返回字典数据数组
|
||||||
|
*/
|
||||||
|
export const getDictData = (type) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 验证字典类型
|
||||||
|
if (!type) {
|
||||||
|
reject(new Error('字典类型不能为空'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取token
|
||||||
|
const token = wx.getStorageSync('token') || ''
|
||||||
|
if (!token) {
|
||||||
|
reject(new Error('未找到认证token'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'tenant-id': '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
|
wx.request({
|
||||||
|
url: `https://green.hrln.com.cn/api/admin/dict/type/${type}`,
|
||||||
|
method: 'GET',
|
||||||
|
header: headers,
|
||||||
|
success: (res) => {
|
||||||
|
console.log(`获取${type}字典数据:`, res.data)
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data) {
|
||||||
|
try {
|
||||||
|
let dictData = []
|
||||||
|
|
||||||
|
// 处理不同的返回数据格式
|
||||||
|
if (Array.isArray(res.data)) {
|
||||||
|
dictData = res.data
|
||||||
|
} else if (res.data.data && Array.isArray(res.data.data)) {
|
||||||
|
dictData = res.data.data
|
||||||
|
} else if (res.data.code === 0 && Array.isArray(res.data.data)) {
|
||||||
|
dictData = res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const formattedData = dictData.map(item => ({
|
||||||
|
text: item.dictLabel || item.label || item.name || item.text,
|
||||||
|
value: item.dictValue || item.value || item.code || item.key
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 如果获取到数据,返回格式化后的数据
|
||||||
|
if (formattedData.length > 0) {
|
||||||
|
resolve(formattedData)
|
||||||
|
} else {
|
||||||
|
// 如果没有数据,返回默认数据
|
||||||
|
console.warn(`未获取到${type}字典数据,使用默认数据`)
|
||||||
|
resolve(DEFAULT_DICT_DATA[type] || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`解析${type}字典数据失败:`, error)
|
||||||
|
// 解析失败时返回默认数据
|
||||||
|
resolve(DEFAULT_DICT_DATA[type] || [])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`获取${type}字典数据失败,使用默认数据`)
|
||||||
|
// 请求失败时返回默认数据
|
||||||
|
resolve(DEFAULT_DICT_DATA[type] || [])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`获取${type}字典数据失败:`, error)
|
||||||
|
// 网络请求失败时返回默认数据
|
||||||
|
resolve(DEFAULT_DICT_DATA[type] || [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取多个字典数据
|
||||||
|
* @param {Array<string>} types - 字典类型数组
|
||||||
|
* @param {string} token - 认证token,可选
|
||||||
|
* @returns {Promise<Object>} 返回包含所有字典数据的对象
|
||||||
|
*/
|
||||||
|
export const getMultipleDictData = async (types, token) => {
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const promises = types.map(type =>
|
||||||
|
getDictData(type, token).then(data => ({ type, data }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const results = await Promise.all(promises)
|
||||||
|
|
||||||
|
results.forEach(({ type, data }) => {
|
||||||
|
result[type] = data
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量获取字典数据失败:', error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据值获取字典文本
|
||||||
|
* @param {Array} dictData - 字典数据数组
|
||||||
|
* @param {string} value - 要查找的值
|
||||||
|
* @returns {string} 对应的文本,找不到时返回原值
|
||||||
|
*/
|
||||||
|
export const getDictText = (dictData, value) => {
|
||||||
|
if (!Array.isArray(dictData) || !value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = dictData.find(item => item.value === value)
|
||||||
|
return item ? item.text : value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文本获取字典值
|
||||||
|
* @param {Array} dictData - 字典数据数组
|
||||||
|
* @param {string} text - 要查找的文本
|
||||||
|
* @returns {string} 对应的值,找不到时返回原文本
|
||||||
|
*/
|
||||||
|
export const getDictValue = (dictData, text) => {
|
||||||
|
if (!Array.isArray(dictData) || !text) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = dictData.find(item => item.text === text)
|
||||||
|
return item ? item.value : text
|
||||||
|
}
|
||||||
211
src/utils/location.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* 位置工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户当前位置
|
||||||
|
* @returns {Promise<Object>} 位置信息 {latitude, longitude, accuracy}
|
||||||
|
*/
|
||||||
|
export function getCurrentLocation() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.getLocation({
|
||||||
|
type: 'wgs84', // 返回可以用于wx.openLocation的经纬度
|
||||||
|
altitude: true, // 传入 true 会返回高度信息
|
||||||
|
isHighAccuracy: true, // 开启高精度定位
|
||||||
|
highAccuracyExpireTime: 4000, // 高精度定位超时时间
|
||||||
|
success: (res) => {
|
||||||
|
console.log('获取位置成功:', res)
|
||||||
|
resolve({
|
||||||
|
latitude: res.latitude,
|
||||||
|
longitude: res.longitude,
|
||||||
|
accuracy: res.accuracy,
|
||||||
|
altitude: res.altitude,
|
||||||
|
speed: res.speed,
|
||||||
|
timestamp: res.timestamp
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('获取位置失败:', error)
|
||||||
|
// 开发环境下提供默认位置(北京天安门)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('使用默认位置进行测试')
|
||||||
|
resolve({
|
||||||
|
latitude: 39.9042,
|
||||||
|
longitude: 116.4074,
|
||||||
|
accuracy: 100,
|
||||||
|
altitude: 0,
|
||||||
|
speed: 0,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查定位权限
|
||||||
|
* @returns {Promise<boolean>} 是否有定位权限
|
||||||
|
*/
|
||||||
|
export function checkLocationPermission() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
wx.getSetting({
|
||||||
|
success: (res) => {
|
||||||
|
const hasPermission = res.authSetting['scope.userLocation'] === true
|
||||||
|
resolve(hasPermission)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请定位权限
|
||||||
|
* @returns {Promise<boolean>} 是否授权成功
|
||||||
|
*/
|
||||||
|
export function requestLocationPermission() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
wx.authorize({
|
||||||
|
scope: 'scope.userLocation',
|
||||||
|
success: () => {
|
||||||
|
console.log('定位权限授权成功')
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
console.log('定位权限授权失败')
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开设置页面
|
||||||
|
*/
|
||||||
|
export function openLocationSetting() {
|
||||||
|
wx.openSetting({
|
||||||
|
success: (res) => {
|
||||||
|
if (res.authSetting['scope.userLocation']) {
|
||||||
|
console.log('用户已开启定位权限')
|
||||||
|
wx.showToast({
|
||||||
|
title: '定位权限已开启',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取位置并处理权限(可选权限申请)
|
||||||
|
* @param {boolean} requestPermission 是否主动申请权限,默认为false
|
||||||
|
* @returns {Promise<Object>} 位置信息
|
||||||
|
*/
|
||||||
|
export async function getLocationWithPermission(requestPermission = false) {
|
||||||
|
try {
|
||||||
|
// 检查权限
|
||||||
|
const hasPermission = await checkLocationPermission()
|
||||||
|
console.log('当前定位权限状态:', hasPermission)
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
if (requestPermission) {
|
||||||
|
// 主动申请权限
|
||||||
|
const granted = await requestLocationPermission()
|
||||||
|
if (!granted) {
|
||||||
|
throw new Error('用户拒绝定位权限')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不主动申请权限,直接抛出错误
|
||||||
|
throw new Error('用户未授权定位权限,请先在设置中开启定位权限')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取位置
|
||||||
|
console.log('开始获取位置...')
|
||||||
|
const location = await getCurrentLocation()
|
||||||
|
console.log('获取位置成功:', location)
|
||||||
|
return location
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取位置失败:', error)
|
||||||
|
|
||||||
|
// 开发环境下提供默认位置
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('开发环境:使用默认位置')
|
||||||
|
return {
|
||||||
|
latitude: 39.9042,
|
||||||
|
longitude: 116.4074,
|
||||||
|
accuracy: 100,
|
||||||
|
altitude: 0,
|
||||||
|
speed: 0,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码时获取位置(主动申请权限)
|
||||||
|
* @returns {Promise<Object>} 位置信息
|
||||||
|
*/
|
||||||
|
export async function getLocationForScan() {
|
||||||
|
try {
|
||||||
|
// 主动申请权限并获取位置
|
||||||
|
const location = await getLocationWithPermission(true)
|
||||||
|
return location
|
||||||
|
} catch (error) {
|
||||||
|
console.error('扫码获取位置失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简化版定位获取(用于提交订单)
|
||||||
|
* @returns {Promise<Object>} 位置信息
|
||||||
|
*/
|
||||||
|
export async function getLocationForOrder() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('开始获取位置信息...')
|
||||||
|
|
||||||
|
wx.getLocation({
|
||||||
|
type: 'wgs84',
|
||||||
|
altitude: true,
|
||||||
|
isHighAccuracy: true,
|
||||||
|
highAccuracyExpireTime: 4000,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('获取位置成功:', res)
|
||||||
|
resolve({
|
||||||
|
latitude: res.latitude,
|
||||||
|
longitude: res.longitude,
|
||||||
|
accuracy: res.accuracy,
|
||||||
|
altitude: res.altitude,
|
||||||
|
speed: res.speed,
|
||||||
|
timestamp: res.timestamp
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('获取位置失败:', error)
|
||||||
|
|
||||||
|
// 开发环境下提供默认位置
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('开发环境:使用默认位置')
|
||||||
|
resolve({
|
||||||
|
latitude: 39.9042,
|
||||||
|
longitude: 116.4074,
|
||||||
|
accuracy: 100,
|
||||||
|
altitude: 0,
|
||||||
|
speed: 0,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
274
src/utils/request.js
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* 统一请求工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { checkLoginAndRedirect, isLoggedIn, clearUserInfo, resetRedirectState } from './auth.js'
|
||||||
|
|
||||||
|
// API基础配置
|
||||||
|
// 根据环境变量设置不同的API地址
|
||||||
|
const getBaseUrl = () => {
|
||||||
|
// 开发环境使用测试地址
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return 'https://green.hrln.com.cn/'//'https://green.cyweb.top/'
|
||||||
|
}
|
||||||
|
// 生产环境使用正式地址
|
||||||
|
return 'https://green.hrln.com.cn/' //'https://green.hrln.com.cn'
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_CONFIG = {
|
||||||
|
BASE_URL: getBaseUrl(),
|
||||||
|
TIMEOUT: 10000,
|
||||||
|
DEFAULT_HEADERS: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'tenant-id': '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复显示登录过期弹窗
|
||||||
|
let isShowingExpiredModal = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一处理token过期
|
||||||
|
*/
|
||||||
|
function handleTokenExpired() {
|
||||||
|
// 防止重复处理
|
||||||
|
if (isShowingExpiredModal) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowingExpiredModal = true
|
||||||
|
|
||||||
|
// 检查当前页面是否为登录页
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const isLoginPage = currentPage && currentPage.route && currentPage.route.includes('login')
|
||||||
|
|
||||||
|
if (isLoginPage) {
|
||||||
|
console.log('当前已在登录页,无需显示过期提示')
|
||||||
|
isShowingExpiredModal = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除用户信息
|
||||||
|
clearUserInfo()
|
||||||
|
resetRedirectState()
|
||||||
|
|
||||||
|
// 显示过期提示
|
||||||
|
wx.showModal({
|
||||||
|
title: '登录过期',
|
||||||
|
content: '您的登录已过期,请重新登录',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '重新登录',
|
||||||
|
success: () => {
|
||||||
|
// 跳转到登录页
|
||||||
|
checkLoginAndRedirect()
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// 重置状态
|
||||||
|
setTimeout(() => {
|
||||||
|
isShowingExpiredModal = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取token
|
||||||
|
const getToken = () => {
|
||||||
|
try {
|
||||||
|
const token = wx.getStorageSync('token') || ''
|
||||||
|
return token
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取token失败:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整URL
|
||||||
|
const buildUrl = (path) => {
|
||||||
|
if (path.startsWith('http')) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return `${API_CONFIG.BASE_URL}${path.startsWith('/') ? path : '/' + path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
const buildHeaders = (customHeaders = {}) => {
|
||||||
|
const token = getToken()
|
||||||
|
const headers = {
|
||||||
|
...API_CONFIG.DEFAULT_HEADERS,
|
||||||
|
...customHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
} else {
|
||||||
|
console.warn('未找到token,请求将不包含Authorization头')
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用请求方法
|
||||||
|
* @param {Object} options 请求配置
|
||||||
|
* @returns {Promise} 请求结果
|
||||||
|
*/
|
||||||
|
const request = (options) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
method = 'GET',
|
||||||
|
data = {},
|
||||||
|
headers = {},
|
||||||
|
timeout = API_CONFIG.TIMEOUT
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
url: buildUrl(url),
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
header: buildHeaders(headers),
|
||||||
|
timeout,
|
||||||
|
success: (res) => {
|
||||||
|
// 检查用户凭证是否过期
|
||||||
|
if (res.data && res.data.code === 1 && res.data.msg === "用户凭证已过期") {
|
||||||
|
console.warn('用户凭证已过期')
|
||||||
|
handleTokenExpired()
|
||||||
|
reject(new Error('用户凭证已过期'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(res)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`API请求失败: ${method} ${url}`, error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理不同请求方法的数据
|
||||||
|
if (method.toUpperCase() === 'GET') {
|
||||||
|
// GET请求将参数拼接到URL
|
||||||
|
if (Object.keys(data).length > 0) {
|
||||||
|
const params = new URLSearchParams(data).toString()
|
||||||
|
requestOptions.url += (requestOptions.url.includes('?') ? '&' : '?') + params
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// POST/PUT/DELETE请求将数据放在body中
|
||||||
|
requestOptions.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
wx.request(requestOptions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET请求
|
||||||
|
* @param {string} url 请求路径
|
||||||
|
* @param {Object} params 查询参数
|
||||||
|
* @param {Object} headers 自定义请求头
|
||||||
|
* @returns {Promise} 请求结果
|
||||||
|
*/
|
||||||
|
export const get = (url, params = {}, headers = {}) => {
|
||||||
|
return request({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
data: params,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST请求
|
||||||
|
* @param {string} url 请求路径
|
||||||
|
* @param {Object} data 请求数据
|
||||||
|
* @param {Object} headers 自定义请求头
|
||||||
|
* @returns {Promise} 请求结果
|
||||||
|
*/
|
||||||
|
export const post = (url, data = {}, headers = {}) => {
|
||||||
|
return request({
|
||||||
|
url,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT请求
|
||||||
|
* @param {string} url 请求路径
|
||||||
|
* @param {Object} data 请求数据
|
||||||
|
* @param {Object} headers 自定义请求头
|
||||||
|
* @returns {Promise} 请求结果
|
||||||
|
*/
|
||||||
|
export const put = (url, data = {}, headers = {}) => {
|
||||||
|
return request({
|
||||||
|
url,
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE请求
|
||||||
|
* @param {string} url 请求路径
|
||||||
|
* @param {Object} data 请求数据
|
||||||
|
* @param {Object} headers 自定义请求头
|
||||||
|
* @returns {Promise} 请求结果
|
||||||
|
*/
|
||||||
|
export const del = (url, data = {}, headers = {}) => {
|
||||||
|
return request({
|
||||||
|
url,
|
||||||
|
method: 'DELETE',
|
||||||
|
data,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传
|
||||||
|
* @param {string} url 上传路径
|
||||||
|
* @param {string} filePath 文件路径
|
||||||
|
* @param {string} name 文件字段名
|
||||||
|
* @param {Object} formData 额外表单数据
|
||||||
|
* @param {Object} headers 自定义请求头
|
||||||
|
* @returns {Promise} 上传结果
|
||||||
|
*/
|
||||||
|
export const uploadFile = (url, filePath, name = 'file', formData = {}, headers = {}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const uploadHeaders = buildHeaders(headers)
|
||||||
|
|
||||||
|
wx.uploadFile({
|
||||||
|
url: buildUrl(url),
|
||||||
|
filePath,
|
||||||
|
name,
|
||||||
|
formData,
|
||||||
|
header: uploadHeaders,
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
|
||||||
|
// 检查用户凭证是否过期
|
||||||
|
if (data && data.code === 1 && data.msg === "用户凭证已过期") {
|
||||||
|
console.warn('用户凭证已过期')
|
||||||
|
handleTokenExpired()
|
||||||
|
reject(new Error('用户凭证已过期'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ ...res, data })
|
||||||
|
} catch (error) {
|
||||||
|
resolve(res)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error(`文件上传失败: ${url}`, error)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出配置
|
||||||
|
export { API_CONFIG }
|
||||||
|
export default request
|
||||||