This commit is contained in:
2026-02-07 16:11:40 +08:00
parent 7998e702a1
commit 71314c5b12
63 changed files with 33553 additions and 0 deletions

12
.editorconfig Normal file
View 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
View File

@@ -0,0 +1,6 @@
// ESLint 检查 .vue 文件需要单独配置编辑器:
// https://eslint.vuejs.org/user-guide/#editor-integrations
{
"extends": ["taro/vue3"]
}

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
dist/
deploy_versions/
.temp/
.rn_temp/
node_modules/
.DS_Store
.swc
.history/

11
babel.config.js Normal file
View 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
View 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
View File

@@ -0,0 +1,5 @@
export default {
mini: {},
h5: {}
}

107
config/index.js Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

78
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
src/assets/images/wode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

709
src/components/CarForm.vue Normal file
View 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
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no,address=no">
<meta name="apple-mobile-web-app-status-bar-style" content="white">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
<title>my-mini-app</title>
<script><%= htmlWebpackPlugin.options.script %></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '历史运送车次',
enablePullDownRefresh: true,
onReachBottomDistance: 50
}

812
src/pages/history/index.vue Normal file
View 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>

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '实名认证',
navigationStyle: 'custom'
}

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

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

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '首页'
}

2215
src/pages/index/index.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '登录'
}

499
src/pages/login/index.vue Normal file
View 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>

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '修改密码'
}

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

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '个人中心'
}

793
src/pages/profile/index.vue Normal file
View 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>

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '实名认证',
navigationStyle: 'custom'
}

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

View File

@@ -0,0 +1,3 @@
<view>
<text>test</text>
</view>

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '注册'
}

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

View File

@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: '数据统计'
}

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

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '车辆定位',
navigationBarBackgroundColor: '#667eea',
navigationBarTextStyle: 'white'
}

549
src/pages/vehicle/index.vue Normal file
View 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
View 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
View 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
View 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
View 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
View 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

7814
yarn.lock Normal file

File diff suppressed because it is too large Load Diff