新增CSS动画效果和交互样式优化

This commit is contained in:
2025-09-18 10:54:25 +08:00
parent 7b6dd95fa5
commit 018d8fb8f2
26 changed files with 3182 additions and 31 deletions

View File

@@ -0,0 +1,3 @@
# 开发环境配置
VUE_APP_API_BASE_URL=http://localhost:3002/api/driver
VUE_APP_USE_MOCK=false

View File

@@ -0,0 +1,3 @@
# 生产环境配置
VUE_APP_API_BASE_URL=https://yourdomain.com/api/driver
VUE_APP_USE_MOCK=false

View File

@@ -0,0 +1,122 @@
# 司机端小程序
活牛运输司机小程序,用于活牛采购智能数字化系统中的运输环节管理。
## 项目介绍
司机端小程序是活牛采购智能数字化系统的重要组成部分,主要服务于活牛运输司机,提供任务接收、位置上报、状态监控、交付确认等功能。
## 功能特性
- 用户认证:手机号验证码登录
- 任务管理:查看、接受、拒绝运输任务
- 运输监控:实时位置上报、牛只状态监控
- 车辆管理:车辆信息维护
- 交付确认:完成运输任务并确认交付
## 技术栈
- 框架uni-app + Vue 3
- 状态管理Pinia
- 语言JavaScript/TypeScript
- 构建工具Vite
- UIuni-ui
## 目录结构
```
src/
├── components/ # 公共组件
├── pages/ # 页面
│ ├── auth/ # 认证相关页面
│ ├── index/ # 首页
│ ├── task/ # 任务相关页面
│ ├── transport/ # 运输相关页面
│ ├── delivery/ # 交付相关页面
│ └── vehicle/ # 车辆相关页面
├── static/ # 静态资源
├── stores/ # 状态管理
├── utils/ # 工具函数
├── App.vue # 根组件
├── main.js # 入口文件
├── manifest.json # 项目配置
└── pages.json # 页面配置
```
## 开发环境
### 环境要求
- Node.js >= 16.0.0
- 微信开发者工具
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
# 启动微信小程序开发环境
npm run dev
# 启动H5开发环境
npm run dev:h5
```
### 构建生产版本
```bash
# 构建微信小程序生产版本
npm run build
# 构建H5生产版本
npm run build:h5
```
## 页面说明
| 页面路径 | 功能说明 |
|---------|---------|
| /pages/auth/login | 登录页面 |
| /pages/index/index | 首页 |
| /pages/task/task-list | 任务列表 |
| /pages/task/task-detail | 任务详情 |
| /pages/transport/location-report | 位置上报 |
| /pages/transport/status-report | 状态上报 |
| /pages/delivery/delivery-confirm | 交付确认 |
| /pages/vehicle/vehicle-info | 车辆信息 |
## 状态管理
使用 Pinia 进行状态管理,包含以下模块:
- user: 用户信息管理
- task: 任务信息管理
## 网络请求
封装了统一的网络请求模块,包含:
- request.js: 基础请求封装
- api.js: 接口封装
## 工具函数
包含常用的工具函数:
- formatDate: 时间格式化
- debounce: 防抖函数
- throttle: 节流函数
- deepClone: 深拷贝
- validatePhone: 手机号验证
- validatePlateNumber: 车牌号验证
## 注意事项
1. 开发时请遵循项目代码规范
2. 新增页面需要在 pages.json 中注册
3. 接口请求统一使用封装的 request 模块
4. 状态管理使用 Pinia避免直接操作全局数据

View File

@@ -2,12 +2,25 @@
"name": "driver-mp",
"version": "1.0.0",
"description": "活牛运输司机小程序",
"main": "main.js",
"main": "src/main.js",
"scripts": {
"dev": "uni -p mp-weixin"
"dev": "uni -p mp-weixin",
"build": "uni build -p mp-weixin",
"dev:h5": "uni -p h5",
"build:h5": "uni build -p h5"
},
"dependencies": {
"@dcloudio/uni-app": "^3.0.0",
"pinia": "^2.0.0"
"@dcloudio/uni-ui": "^1.4.0",
"pinia": "^2.0.0",
"@dcloudio/uni-components": "^3.0.0"
},
"devDependencies": {
"@dcloudio/uni-cli-shared": "^3.0.0",
"@dcloudio/uni-template-compiler": "^3.0.0",
"@dcloudio/types": "^3.3.0",
"typescript": "^5.0.0",
"vite": "^4.4.0",
"@vue/tsconfig": "^0.1.3"
}
}

View File

@@ -0,0 +1,16 @@
<script setup>
// app.vue
</script>
<template>
<view>
<router-view />
</view>
</template>
<style>
/* 全局样式 */
page {
background-color: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,11 @@
import { createSSRApp } from 'vue';
import App from './App.vue';
import store from './stores';
export function createApp() {
const app = createSSRApp(App);
app.use(store);
return {
app
};
}

View File

@@ -0,0 +1,19 @@
{
"name": "driver-mp",
"appid": "__UNI__DRIVERMP",
"description": "活牛运输司机小程序",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"mp-weixin": {
"appid": "wx-your-driver-appid-here",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true
},
"vueVersion": "3"
}

View File

@@ -0,0 +1,84 @@
{
"pages": [
{
"path": "pages/auth/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "司机首页"
}
},
{
"path": "pages/task/task-list",
"style": {
"navigationBarTitleText": "任务列表"
}
},
{
"path": "pages/task/task-detail",
"style": {
"navigationBarTitleText": "任务详情"
}
},
{
"path": "pages/transport/location-report",
"style": {
"navigationBarTitleText": "位置上报"
}
},
{
"path": "pages/transport/status-report",
"style": {
"navigationBarTitleText": "状态上报"
}
},
{
"path": "pages/delivery/delivery-confirm",
"style": {
"navigationBarTitleText": "交付确认"
}
},
{
"path": "pages/vehicle/vehicle-info",
"style": {
"navigationBarTitleText": "车辆信息"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "司机端",
"navigationBarBackgroundColor": "#f8f8f8",
"backgroundColor": "#f8f8f8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#1989fa",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/task/task-list",
"iconPath": "static/tabbar/task.png",
"selectedIconPath": "static/tabbar/task-active.png",
"text": "任务"
},
{
"pagePath": "pages/transport/location-report",
"iconPath": "static/tabbar/location.png",
"selectedIconPath": "static/tabbar/location-active.png",
"text": "运输"
}
]
}
}

View File

@@ -0,0 +1,241 @@
<script setup>
import { ref } from 'vue';
import { useUserStore } from '../../stores/user';
import { userApi } from '../../utils/api';
import { validatePhone } from '../../utils';
// 用户状态管理
const userStore = useUserStore();
// 表单数据
const loginForm = ref({
phone: '',
code: ''
});
// 验证码倒计时
const countdown = ref(0);
let timer = null;
// 发送验证码
const sendCode = () => {
if (!loginForm.value.phone) {
uni.showToast({
title: '请输入手机号',
icon: 'none'
});
return;
}
if (!validatePhone(loginForm.value.phone)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
// 实际开发中这里会调用接口发送验证码
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 启动倒计时
startCountdown();
};
// 启动倒计时
const startCountdown = () => {
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
};
// 登录
const login = async () => {
if (!loginForm.value.phone) {
uni.showToast({
title: '请输入手机号',
icon: 'none'
});
return;
}
if (!validatePhone(loginForm.value.phone)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
if (!loginForm.value.code) {
uni.showToast({
title: '请输入验证码',
icon: 'none'
});
return;
}
try {
// 调用登录接口
const res = await userApi.login({
phone: loginForm.value.phone,
code: loginForm.value.code
});
// 保存用户信息和token
userStore.login(res.data.user, res.data.token);
// 保存token到本地存储
uni.setStorageSync('driver_token', res.data.token);
uni.showToast({
title: '登录成功',
icon: 'success'
});
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 1000);
} catch (err) {
uni.showToast({
title: err.message || '登录失败',
icon: 'none'
});
}
};
</script>
<template>
<view class="container">
<view class="login-header">
<text class="title">司机登录</text>
<text class="subtitle">活牛运输管理系统</text>
</view>
<view class="login-form">
<view class="form-item">
<input
v-model="loginForm.phone"
class="form-input"
type="number"
placeholder="请输入手机号"
maxlength="11"
/>
</view>
<view class="form-item code-item">
<input
v-model="loginForm.code"
class="form-input"
type="number"
placeholder="请输入验证码"
maxlength="6"
/>
<button
class="code-btn"
:disabled="countdown > 0"
@click="sendCode"
>
{{ countdown > 0 ? `${countdown}s后重新获取` : '获取验证码' }}
</button>
</view>
<button class="login-btn" @click="login">登录</button>
</view>
<view class="agreement">
<text class="agreement-text">
登录即表示您同意用户协议隐私政策
</text>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 0 40rpx;
}
.login-header {
text-align: center;
padding: 200rpx 0 100rpx;
.title {
display: block;
font-size: 48rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.subtitle {
font-size: 28rpx;
color: #666;
}
}
.login-form {
.form-item {
margin-bottom: 30rpx;
.form-input {
height: 90rpx;
padding: 0 30rpx;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 28rpx;
}
}
.code-item {
display: flex;
gap: 20rpx;
.form-input {
flex: 1;
}
.code-btn {
width: 220rpx;
height: 90rpx;
background: #1989fa;
color: #fff;
border-radius: 12rpx;
font-size: 26rpx;
&:disabled {
background: #cccccc;
}
}
}
.login-btn {
height: 90rpx;
line-height: 90rpx;
background: #1989fa;
color: #fff;
border-radius: 12rpx;
font-size: 32rpx;
margin-top: 50rpx;
}
}
.agreement {
margin-top: 50rpx;
text-align: center;
.agreement-text {
font-size: 24rpx;
color: #999;
}
}
</style>

View File

@@ -0,0 +1,342 @@
<script setup>
import { ref } from 'vue';
// 交付信息
const deliveryInfo = ref({
orderId: '20250001',
supplier: '某某牛场',
destination: '某某屠宰场',
plannedArrival: '2025-09-15 14:30',
actualArrival: '',
cattleInfo: {
breed: '西门塔尔',
quantity: 50,
weight: '25000公斤'
}
});
// 签收人信息
const recipientInfo = ref({
name: '',
phone: '',
signature: ''
});
// 表单验证
const formValid = ref(false);
// 图片上传
const imageList = ref([]);
// 上传图片
const uploadImage = () => {
uni.chooseImage({
count: 9 - imageList.value.length,
success: (res) => {
imageList.value = [...imageList.value, ...res.tempFilePaths];
uni.showToast({
title: '图片上传成功',
icon: 'success'
});
},
fail: () => {
uni.showToast({
title: '图片上传失败',
icon: 'none'
});
}
});
};
// 删除图片
const removeImage = (index) => {
imageList.value.splice(index, 1);
};
// 确认交付
const confirmDelivery = () => {
if (!recipientInfo.value.name) {
uni.showToast({
title: '请输入签收人姓名',
icon: 'none'
});
return;
}
if (!recipientInfo.value.phone) {
uni.showToast({
title: '请输入签收人电话',
icon: 'none'
});
return;
}
// 实际开发中这里会调用接口提交交付信息
uni.showLoading({
title: '提交中...'
});
setTimeout(() => {
uni.hideLoading();
uni.showModal({
title: '交付成功',
content: '交付确认已完成,订单状态已更新',
showCancel: false,
success: () => {
// 返回上一页或跳转到任务列表
uni.navigateBack();
}
});
}, 1000);
};
</script>
<template>
<view class="container">
<!-- 订单信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">订单信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">订单号:</text>
<text class="value">{{ deliveryInfo.orderId }}</text>
</view>
<view class="info-row">
<text class="label">供应商:</text>
<text class="value">{{ deliveryInfo.supplier }}</text>
</view>
<view class="info-row">
<text class="label">目的地:</text>
<text class="value">{{ deliveryInfo.destination }}</text>
</view>
<view class="info-row">
<text class="label">计划到达:</text>
<text class="value">{{ deliveryInfo.plannedArrival }}</text>
</view>
<view class="info-row">
<text class="label">实际到达:</text>
<input
v-model="deliveryInfo.actualArrival"
class="value-input"
type="text"
placeholder="请输入实际到达时间"
/>
</view>
</view>
</view>
<!-- 牛只信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">牛只信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">品种:</text>
<text class="value">{{ deliveryInfo.cattleInfo.breed }}</text>
</view>
<view class="info-row">
<text class="label">数量:</text>
<text class="value">{{ deliveryInfo.cattleInfo.quantity }}</text>
</view>
<view class="info-row">
<text class="label">重量:</text>
<text class="value">{{ deliveryInfo.cattleInfo.weight }}</text>
</view>
</view>
</view>
<!-- 签收信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">签收信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">签收人:</text>
<input
v-model="recipientInfo.name"
class="value-input"
type="text"
placeholder="请输入签收人姓名"
/>
</view>
<view class="info-row">
<text class="label">联系电话:</text>
<input
v-model="recipientInfo.phone"
class="value-input"
type="text"
placeholder="请输入签收人电话"
/>
</view>
</view>
</view>
<!-- 上传照片 -->
<view class="section">
<view class="section-header">
<text class="section-title">交付照片</text>
</view>
<view class="section-content">
<view class="image-upload-area">
<view
v-for="(image, index) in imageList"
:key="index"
class="image-preview"
>
<image :src="image" class="preview-image" mode="aspectFill" />
<view
class="remove-btn"
@click.stop="removeImage(index)"
>
×
</view>
</view>
<view
v-if="imageList.length < 9"
class="upload-btn"
@click="uploadImage"
>
<text class="icon">+</text>
<text class="text">上传照片</text>
</view>
</view>
</view>
</view>
<!-- 确认按钮 -->
<view class="confirm-section">
<button class="confirm-btn" @click="confirmDelivery">确认交付</button>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.section {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.section-header {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
.section-title {
font-size: 28rpx;
font-weight: bold;
}
}
.section-content {
padding: 24rpx;
.info-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.label {
width: 150rpx;
color: #666;
font-size: 26rpx;
}
.value {
flex: 1;
font-size: 26rpx;
color: #333;
}
.value-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
box-sizing: border-box;
}
}
.image-upload-area {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.image-preview {
position: relative;
width: 150rpx;
height: 150rpx;
.preview-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.remove-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background: #ff4d4f;
border-radius: 50%;
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.upload-btn {
width: 150rpx;
height: 150rpx;
border: 1rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.icon {
font-size: 50rpx;
line-height: 1;
}
.text {
font-size: 22rpx;
color: #666;
margin-top: 10rpx;
}
}
}
}
}
.confirm-section {
padding: 20rpx 0 40rpx;
.confirm-btn {
height: 80rpx;
line-height: 80rpx;
background: #1989fa;
color: #fff;
font-size: 28rpx;
border-radius: 12rpx;
}
}
</style>

View File

@@ -0,0 +1,420 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useUserStore } from '../../stores/user';
import { useTaskStore } from '../../stores/task';
// 用户状态管理
const userStore = useUserStore();
// 任务状态管理
const taskStore = useTaskStore();
// 当前任务信息
const currentTask = ref({
orderId: '20250001',
supplier: '某某牛场',
destination: '某某屠宰场',
estimatedTime: '2小时30分钟',
status: '运输中'
});
// 快捷操作
const quickActions = ref([
{
icon: 'location',
title: '位置上报',
path: '/pages/transport/location-report'
},
{
icon: 'status',
title: '状态上报',
path: '/pages/transport/status-report'
},
{
icon: 'task',
title: '任务列表',
path: '/pages/task/task-list'
},
{
icon: 'delivery',
title: '交付确认',
path: '/pages/delivery/delivery-confirm'
}
]);
// 页面加载时检查登录状态
onMounted(() => {
// 如果未登录,跳转到登录页
if (!userStore.loggedIn) {
uni.redirectTo({
url: '/pages/auth/login'
});
}
});
// 导航到指定页面
const navigateTo = (path) => {
uni.navigateTo({
url: path
});
};
// 拨打电话
const makePhoneCall = (phoneNumber) => {
uni.makePhoneCall({
phoneNumber: phoneNumber
});
};
// 退出登录
const logout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清除用户信息
userStore.logout();
// 清除本地存储的token
uni.removeStorageSync('driver_token');
// 跳转到登录页
uni.redirectTo({
url: '/pages/auth/login'
});
}
}
});
};
</script>
<template>
<view class="container">
<view class="header">
<text class="welcome">司机工作台</text>
<text class="subtitle">您好{{ userStore.userName || '司机师傅' }}</text>
</view>
<!-- 当前任务卡片 -->
<view class="current-task">
<view class="task-header">
<text class="task-title">当前任务</text>
<text class="task-status">{{ currentTask.status }}</text>
</view>
<view class="task-info">
<view class="info-row">
<text class="label">订单号:</text>
<text class="value">{{ currentTask.orderId }}</text>
</view>
<view class="info-row">
<text class="label">供应商:</text>
<text class="value">{{ currentTask.supplier }}</text>
</view>
<view class="info-row">
<text class="label">目的地:</text>
<text class="value">{{ currentTask.destination }}</text>
</view>
<view class="info-row">
<text class="label">预计时间:</text>
<text class="value">{{ currentTask.estimatedTime }}</text>
</view>
</view>
<view class="task-actions">
<button class="action-btn primary" @click="navigateTo('/pages/task/task-detail')">查看详情</button>
<button class="action-btn" @click="makePhoneCall('13800138000')">联系客服</button>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="section-title">快捷操作</view>
<view class="actions-grid">
<view
v-for="(action, index) in quickActions"
:key="index"
class="action-card"
@click="navigateTo(action.path)"
>
<view class="action-icon">
<text class="icon">{{ action.icon }}</text>
</view>
<text class="action-title">{{ action.title }}</text>
</view>
</view>
</view>
<!-- 个人信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">个人信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">姓名:</text>
<text class="value">{{ userStore.userInfo.name }}</text>
</view>
<view class="info-row">
<text class="label">手机号:</text>
<text class="value">{{ userStore.userInfo.phone }}</text>
</view>
<view class="info-row">
<text class="label">车牌号:</text>
<text class="value">{{ userStore.userInfo.licensePlate || '未设置' }}</text>
</view>
<view class="info-row">
<button class="edit-btn" @click="navigateTo('/pages/vehicle/vehicle-info')">
{{ userStore.userInfo.licensePlate ? '编辑' : '设置' }}车辆信息
</button>
</view>
</view>
</view>
<!-- 通知提醒 -->
<view class="notification-section">
<view class="section-title">系统通知</view>
<view class="notification-list">
<view class="notification-item">
<text class="notification-text">您有1个新任务待接受</text>
<text class="notification-time">5分钟前</text>
</view>
<view class="notification-item">
<text class="notification-text">请记得每30分钟上报一次位置</text>
<text class="notification-time">1小时前</text>
</view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.header {
text-align: center;
padding: 40rpx 0;
.welcome {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
color: #666;
font-size: 24rpx;
}
}
.current-task {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.task-title {
font-size: 32rpx;
font-weight: bold;
}
.task-status {
background: #1989fa;
color: #fff;
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
}
}
.task-info {
.info-row {
display: flex;
margin-bottom: 15rpx;
.label {
width: 150rpx;
color: #666;
font-size: 26rpx;
}
.value {
flex: 1;
font-size: 26rpx;
color: #333;
}
}
}
.task-actions {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
.action-btn {
flex: 1;
height: 70rpx;
line-height: 70rpx;
font-size: 26rpx;
&.primary {
background: #1989fa;
color: #fff;
}
}
}
}
.section-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.quick-actions {
margin-bottom: 30rpx;
.actions-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.action-card {
background: #fff;
padding: 30rpx;
border-radius: 12rpx;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.action-icon {
width: 80rpx;
height: 80rpx;
background: #1989fa;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 20rpx;
.icon {
color: #fff;
font-size: 36rpx;
}
}
.action-title {
font-size: 24rpx;
color: #333;
}
}
}
}
.section {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.section-header {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
.section-title {
font-size: 28rpx;
font-weight: bold;
}
}
.section-content {
padding: 24rpx;
.info-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.label {
width: 150rpx;
color: #666;
font-size: 26rpx;
}
.value {
flex: 1;
font-size: 26rpx;
color: #333;
}
.edit-btn {
height: 60rpx;
line-height: 60rpx;
padding: 0 30rpx;
background: #1989fa;
color: #fff;
border-radius: 8rpx;
font-size: 24rpx;
}
}
}
}
.notification-section {
margin-bottom: 30rpx;
.notification-list {
background: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.notification-item {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
&:last-child {
border-bottom: none;
}
.notification-text {
font-size: 26rpx;
}
.notification-time {
color: #999;
font-size: 22rpx;
}
}
}
}
.logout-section {
.logout-btn {
height: 80rpx;
line-height: 80rpx;
background: #ff4d4f;
color: #fff;
font-size: 28rpx;
border-radius: 12rpx;
}
}
</style>

View File

@@ -0,0 +1,287 @@
<script setup>
import { ref } from 'vue';
// 任务详情数据
const taskDetail = ref({
orderId: '20250001',
status: 'ongoing',
statusText: '进行中',
supplier: '某某牛场',
supplierContact: '李先生 13800138000',
destination: '某某屠宰场',
destinationContact: '王先生 13900139000',
cattleInfo: {
breed: '西门塔尔',
quantity: 50,
weight: '25000公斤'
},
createTime: '2025-09-15 08:30',
estimatedDeparture: '2025-09-15 10:00',
actualDeparture: '2025-09-15 10:15',
estimatedArrival: '2025-09-15 14:30',
vehicleInfo: {
plateNumber: '京A12345',
driver: '张师傅',
phone: '13700137000'
}
});
// 操作按钮
const actions = ref([
{
name: '位置上报',
path: '/pages/transport/location-report'
},
{
name: '状态上报',
path: '/pages/transport/status-report'
},
{
name: '交付确认',
path: '/pages/delivery/delivery-confirm'
}
]);
// 导航操作
const navigateTo = (path) => {
uni.navigateTo({
url: path
});
};
// 拨打电话
const makePhoneCall = (phoneNumber) => {
uni.makePhoneCall({
phoneNumber: phoneNumber
});
};
</script>
<template>
<view class="container">
<!-- 任务状态 -->
<view class="status-banner" :class="taskDetail.status">
<text class="status-text">{{ taskDetail.statusText }}</text>
</view>
<!-- 订单信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">订单信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">订单号:</text>
<text class="value">{{ taskDetail.orderId }}</text>
</view>
<view class="info-row">
<text class="label">创建时间:</text>
<text class="value">{{ taskDetail.createTime }}</text>
</view>
</view>
</view>
<!-- 牛只信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">牛只信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">品种:</text>
<text class="value">{{ taskDetail.cattleInfo.breed }}</text>
</view>
<view class="info-row">
<text class="label">数量:</text>
<text class="value">{{ taskDetail.cattleInfo.quantity }}</text>
</view>
<view class="info-row">
<text class="label">重量:</text>
<text class="value">{{ taskDetail.cattleInfo.weight }}</text>
</view>
</view>
</view>
<!-- 供应商信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">供应商信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">名称:</text>
<text class="value">{{ taskDetail.supplier }}</text>
</view>
<view class="info-row">
<text class="label">联系人:</text>
<text class="value">{{ taskDetail.supplierContact }}</text>
<text class="contact-btn" @click="makePhoneCall('13800138000')">拨号</text>
</view>
</view>
</view>
<!-- 目的地信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">目的地信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">地址:</text>
<text class="value">{{ taskDetail.destination }}</text>
</view>
<view class="info-row">
<text class="label">联系人:</text>
<text class="value">{{ taskDetail.destinationContact }}</text>
<text class="contact-btn" @click="makePhoneCall('13900139000')">拨号</text>
</view>
</view>
</view>
<!-- 运输信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">运输信息</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">车牌号:</text>
<text class="value">{{ taskDetail.vehicleInfo.plateNumber }}</text>
</view>
<view class="info-row">
<text class="label">司机:</text>
<text class="value">{{ taskDetail.vehicleInfo.driver }}</text>
</view>
<view class="info-row">
<text class="label">预计发车:</text>
<text class="value">{{ taskDetail.estimatedDeparture }}</text>
</view>
<view class="info-row">
<text class="label">实际发车:</text>
<text class="value">{{ taskDetail.actualDeparture }}</text>
</view>
<view class="info-row">
<text class="label">预计到达:</text>
<text class="value">{{ taskDetail.estimatedArrival }}</text>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="actions">
<button
v-for="(action, index) in actions"
:key="index"
class="action-btn"
@click="navigateTo(action.path)"
>
{{ action.name }}
</button>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.status-banner {
background: #1989fa;
color: #fff;
text-align: center;
padding: 30rpx;
border-radius: 12rpx;
margin-bottom: 30rpx;
&.pending {
background: #fff3cd;
color: #856404;
}
&.ongoing {
background: #1989fa;
color: #fff;
}
&.completed {
background: #d4edda;
color: #155724;
}
.status-text {
font-size: 32rpx;
font-weight: bold;
}
}
.section {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.section-header {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
.section-title {
font-size: 28rpx;
font-weight: bold;
}
}
.section-content {
padding: 24rpx;
.info-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.label {
width: 150rpx;
color: #666;
font-size: 26rpx;
}
.value {
flex: 1;
font-size: 26rpx;
color: #333;
}
.contact-btn {
color: #1989fa;
font-size: 24rpx;
padding: 10rpx 20rpx;
border: 1rpx solid #1989fa;
border-radius: 6rpx;
}
}
}
}
.actions {
display: flex;
flex-direction: column;
gap: 20rpx;
padding: 20rpx 0;
.action-btn {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
background: #1989fa;
color: #fff;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@@ -0,0 +1,210 @@
<script setup>
import { ref } from 'vue';
// 任务状态选项
const taskStatus = ref([
{ name: '全部', value: 'all' },
{ name: '待接受', value: 'pending' },
{ name: '进行中', value: 'ongoing' },
{ name: '已完成', value: 'completed' }
]);
// 当前选中的状态
const currentStatus = ref('all');
// 任务列表数据
const taskList = ref([
{
id: '1',
orderId: '20250001',
supplier: '某某牛场',
destination: '某某屠宰场',
createTime: '2025-09-15 08:30',
status: 'ongoing',
statusText: '进行中'
},
{
id: '2',
orderId: '20250002',
supplier: '某某牧场',
destination: '某某加工厂',
createTime: '2025-09-16 10:15',
status: 'pending',
statusText: '待接受'
},
{
id: '3',
orderId: '20250003',
supplier: '某某养殖基地',
destination: '某某配送中心',
createTime: '2025-09-17 14:20',
status: 'completed',
statusText: '已完成'
}
]);
// 切换任务状态
const switchStatus = (status) => {
currentStatus.value = status;
// 实际开发中这里会重新请求数据
};
// 查看任务详情
const viewTaskDetail = (taskId) => {
uni.navigateTo({
url: `/pages/task/task-detail?id=${taskId}`
});
};
</script>
<template>
<view class="container">
<!-- 状态筛选 -->
<scroll-view class="status-tabs" scroll-x>
<view class="tab-list">
<view
v-for="status in taskStatus"
:key="status.value"
class="tab-item"
:class="{ active: currentStatus === status.value }"
@click="switchStatus(status.value)"
>
{{ status.name }}
</view>
</view>
</scroll-view>
<!-- 任务列表 -->
<view class="task-list">
<view
v-for="task in taskList"
:key="task.id"
class="task-item"
@click="viewTaskDetail(task.id)"
>
<view class="task-header">
<text class="order-id">订单号: {{ task.orderId }}</text>
<text class="task-status" :class="task.status">{{ task.statusText }}</text>
</view>
<view class="task-content">
<view class="info-row">
<text class="label">供应商:</text>
<text class="value">{{ task.supplier }}</text>
</view>
<view class="info-row">
<text class="label">目的地:</text>
<text class="value">{{ task.destination }}</text>
</view>
<view class="info-row">
<text class="label">创建时间:</text>
<text class="value">{{ task.createTime }}</text>
</view>
</view>
<view class="task-footer">
<text class="arrow">></text>
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.status-tabs {
background: #fff;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.tab-list {
display: flex;
white-space: nowrap;
.tab-item {
padding: 24rpx 32rpx;
display: inline-block;
font-size: 26rpx;
&.active {
color: #1989fa;
font-weight: bold;
border-bottom: 4rpx solid #1989fa;
}
}
}
}
.task-list {
.task-item {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.order-id {
font-size: 28rpx;
font-weight: bold;
}
.task-status {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 6rpx;
&.pending {
background: #fff3cd;
color: #856404;
}
&.ongoing {
background: #d1ecf1;
color: #0c5460;
}
&.completed {
background: #d4edda;
color: #155724;
}
}
}
.task-content {
.info-row {
display: flex;
margin-bottom: 12rpx;
.label {
width: 120rpx;
color: #666;
font-size: 24rpx;
}
.value {
flex: 1;
font-size: 24rpx;
color: #333;
}
}
}
.task-footer {
text-align: right;
padding-top: 10rpx;
.arrow {
color: #999;
}
}
}
}
</style>

View File

@@ -0,0 +1,266 @@
<script setup>
import { ref } from 'vue';
// 位置信息
const locationInfo = ref({
latitude: '',
longitude: '',
address: '',
reportTime: ''
});
// 上报记录
const reportHistory = ref([
{
id: '1',
time: '2025-09-15 10:15',
address: '北京市朝阳区某某路'
},
{
id: '2',
time: '2025-09-15 10:30',
address: '北京市朝阳区某某街'
},
{
id: '3',
time: '2025-09-15 10:45',
address: '北京市朝阳区某某大道'
}
]);
// 获取当前位置
const getLocation = () => {
uni.getLocation({
type: 'gcj02',
success: function (res) {
locationInfo.value.latitude = res.latitude;
locationInfo.value.longitude = res.longitude;
locationInfo.value.reportTime = new Date().toLocaleString('zh-CN');
// 通过经纬度获取地址信息实际开发中需要调用地图API
locationInfo.value.address = '北京市朝阳区某某位置';
uni.showToast({
title: '获取位置成功',
icon: 'success'
});
},
fail: function () {
uni.showToast({
title: '获取位置失败',
icon: 'none'
});
}
});
};
// 上报位置
const reportLocation = () => {
if (!locationInfo.value.latitude || !locationInfo.value.longitude) {
uni.showToast({
title: '请先获取当前位置',
icon: 'none'
});
return;
}
// 实际开发中这里会调用接口上报位置信息
uni.showLoading({
title: '上报中...'
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '位置上报成功',
icon: 'success'
});
// 添加到上报历史
reportHistory.value.unshift({
id: Date.now().toString(),
time: new Date().toLocaleString('zh-CN'),
address: locationInfo.value.address
});
}, 1000);
};
</script>
<template>
<view class="container">
<!-- 地图区域 -->
<view class="map-container">
<view class="map-placeholder">
<text>地图区域</text>
<text class="desc">显示当前位置和行驶轨迹</text>
</view>
</view>
<!-- 位置信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">当前位置</text>
</view>
<view class="section-content">
<view class="info-row">
<text class="label">经度:</text>
<text class="value">{{ locationInfo.longitude || '未获取' }}</text>
</view>
<view class="info-row">
<text class="label">纬度:</text>
<text class="value">{{ locationInfo.latitude || '未获取' }}</text>
</view>
<view class="info-row">
<text class="label">地址:</text>
<text class="value">{{ locationInfo.address || '未获取' }}</text>
</view>
<view class="info-row">
<text class="label">获取时间:</text>
<text class="value">{{ locationInfo.reportTime || '未获取' }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="actions">
<button class="action-btn" @click="getLocation">获取当前位置</button>
<button class="action-btn primary" @click="reportLocation">上报位置信息</button>
</view>
<!-- 上报历史 -->
<view class="section">
<view class="section-header">
<text class="section-title">上报历史</text>
</view>
<view class="section-content">
<view
v-for="record in reportHistory"
:key="record.id"
class="history-item"
>
<view class="history-time">{{ record.time }}</view>
<view class="history-address">{{ record.address }}</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.map-container {
height: 400rpx;
background: #f0f0f0;
border-radius: 12rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: center;
align-items: center;
.map-placeholder {
text-align: center;
text {
display: block;
&:first-child {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
&.desc {
color: #666;
font-size: 24rpx;
}
}
}
}
.section {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.section-header {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
.section-title {
font-size: 28rpx;
font-weight: bold;
}
}
.section-content {
padding: 24rpx;
.info-row {
display: flex;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.label {
width: 150rpx;
color: #666;
font-size: 26rpx;
}
.value {
flex: 1;
font-size: 26rpx;
color: #333;
}
}
.history-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
&:last-child {
border-bottom: none;
}
.history-time {
font-size: 24rpx;
color: #1989fa;
margin-bottom: 10rpx;
}
.history-address {
font-size: 26rpx;
color: #333;
}
}
}
}
.actions {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 30rpx;
.action-btn {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
background: #fff;
color: #333;
border: 1rpx solid #ddd;
&.primary {
background: #1989fa;
color: #fff;
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,354 @@
<script setup>
import { ref } from 'vue';
// 牛只状态选项
const cattleStatusOptions = ref([
{ label: '健康良好', value: 'good' },
{ label: '轻微应激', value: 'stress' },
{ label: '部分异常', value: 'abnormal' },
{ label: '严重异常', value: 'serious' }
]);
// 表单数据
const formData = ref({
cattleStatus: 'good',
description: '',
videoPath: '',
imagePaths: []
});
// 选择牛只状态
const selectCattleStatus = (value) => {
formData.value.cattleStatus = value;
};
// 选择视频
const chooseVideo = () => {
uni.chooseVideo({
success: (res) => {
formData.value.videoPath = res.tempFilePath;
uni.showToast({
title: '视频选择成功',
icon: 'success'
});
},
fail: () => {
uni.showToast({
title: '视频选择失败',
icon: 'none'
});
}
});
};
// 选择图片
const chooseImage = () => {
uni.chooseImage({
count: 9,
success: (res) => {
formData.value.imagePaths = res.tempFilePaths;
uni.showToast({
title: '图片选择成功',
icon: 'success'
});
},
fail: () => {
uni.showToast({
title: '图片选择失败',
icon: 'none'
});
}
});
};
// 提交状态报告
const submitReport = () => {
if (!formData.value.cattleStatus) {
uni.showToast({
title: '请选择牛只状态',
icon: 'none'
});
return;
}
// 实际开发中这里会调用接口提交数据
uni.showLoading({
title: '提交中...'
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '状态上报成功',
icon: 'success'
});
// 清空表单
formData.value = {
cattleStatus: 'good',
description: '',
videoPath: '',
imagePaths: []
};
}, 1000);
};
</script>
<template>
<view class="container">
<view class="section">
<view class="section-header">
<text class="section-title">牛只状态</text>
</view>
<view class="section-content">
<view class="radio-group">
<label
v-for="option in cattleStatusOptions"
:key="option.value"
class="radio-item"
>
<radio
:value="option.value"
:checked="formData.cattleStatus === option.value"
@click="selectCattleStatus(option.value)"
/>
<text>{{ option.label }}</text>
</label>
</view>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">情况描述</text>
</view>
<view class="section-content">
<textarea
v-model="formData.description"
class="description-textarea"
placeholder="请详细描述牛只当前状态..."
maxlength="500"
/>
<view class="textarea-footer">
<text>{{ formData.description.length }}/500</text>
</view>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">上传视频</text>
</view>
<view class="section-content">
<view class="upload-area" @click="chooseVideo">
<view v-if="!formData.videoPath" class="upload-placeholder">
<text class="icon">📹</text>
<text class="text">点击上传视频</text>
</view>
<video
v-else
:src="formData.videoPath"
class="preview-video"
controls
/>
</view>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">上传图片</text>
</view>
<view class="section-content">
<view class="image-upload-area">
<view
v-for="(image, index) in formData.imagePaths"
:key="index"
class="image-preview"
>
<image :src="image" class="preview-image" mode="aspectFill" />
<view
class="remove-btn"
@click.stop="removeImage(index)"
>
×
</view>
</view>
<view
v-if="formData.imagePaths.length < 9"
class="upload-btn"
@click="chooseImage"
>
<text class="icon">+</text>
<text class="text">上传图片</text>
</view>
</view>
</view>
</view>
<view class="submit-section">
<button class="submit-btn" @click="submitReport">提交状态报告</button>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.section {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.section-header {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
.section-title {
font-size: 28rpx;
font-weight: bold;
}
}
.section-content {
padding: 24rpx;
.radio-group {
.radio-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
radio {
margin-right: 10rpx;
transform: scale(0.8);
}
text {
font-size: 26rpx;
color: #333;
}
}
}
.description-textarea {
width: 100%;
height: 200rpx;
padding: 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
box-sizing: border-box;
}
.textarea-footer {
text-align: right;
padding-top: 10rpx;
font-size: 22rpx;
color: #999;
}
.upload-area {
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200rpx;
border: 1rpx dashed #ddd;
border-radius: 8rpx;
.icon {
font-size: 60rpx;
margin-bottom: 10rpx;
}
.text {
font-size: 26rpx;
color: #666;
}
}
.preview-video {
width: 100%;
height: 300rpx;
}
}
.image-upload-area {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.image-preview {
position: relative;
width: 150rpx;
height: 150rpx;
.preview-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.remove-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background: #ff4d4f;
border-radius: 50%;
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.upload-btn {
width: 150rpx;
height: 150rpx;
border: 1rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.icon {
font-size: 50rpx;
line-height: 1;
}
.text {
font-size: 22rpx;
color: #666;
margin-top: 10rpx;
}
}
}
}
}
.submit-section {
padding: 20rpx 0 40rpx;
.submit-btn {
height: 80rpx;
line-height: 80rpx;
background: #1989fa;
color: #fff;
font-size: 28rpx;
border-radius: 12rpx;
}
}
</style>

View File

@@ -0,0 +1,321 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useUserStore } from '../../stores/user';
import { userApi } from '../../utils/api';
// 用户状态管理
const userStore = useUserStore();
// 车辆信息表单
const vehicleForm = ref({
plateNumber: '',
vehicleType: '',
loadCapacity: '',
vehiclePhotos: []
});
// 车辆类型选项
const vehicleTypes = ref([
{ label: '厢式货车', value: 'van' },
{ label: '冷藏车', value: 'refrigerator' },
{ label: '平板车', value: 'flatbed' },
{ label: '其他', value: 'other' }
]);
// 图片上传
const uploadImage = () => {
uni.chooseImage({
count: 3 - vehicleForm.value.vehiclePhotos.length,
success: (res) => {
vehicleForm.value.vehiclePhotos = [...vehicleForm.value.vehiclePhotos, ...res.tempFilePaths];
uni.showToast({
title: '图片上传成功',
icon: 'success'
});
},
fail: () => {
uni.showToast({
title: '图片上传失败',
icon: 'none'
});
}
});
};
// 删除图片
const removeImage = (index) => {
vehicleForm.value.vehiclePhotos.splice(index, 1);
};
// 保存车辆信息
const saveVehicleInfo = async () => {
if (!vehicleForm.value.plateNumber) {
uni.showToast({
title: '请输入车牌号',
icon: 'none'
});
return;
}
if (!vehicleForm.value.vehicleType) {
uni.showToast({
title: '请选择车辆类型',
icon: 'none'
});
return;
}
if (!vehicleForm.value.loadCapacity) {
uni.showToast({
title: '请输入载重量',
icon: 'none'
});
return;
}
try {
// 调用接口保存车辆信息
const res = await userApi.uploadVehicleInfo(vehicleForm.value);
// 更新用户状态中的车辆信息
userStore.setUserInfo({
licensePlate: vehicleForm.value.plateNumber,
vehicleType: vehicleForm.value.vehicleType
});
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1000);
} catch (err) {
uni.showToast({
title: err.message || '保存失败',
icon: 'none'
});
}
};
// 页面加载时获取用户车辆信息
onMounted(() => {
if (userStore.userInfo.licensePlate) {
vehicleForm.value.plateNumber = userStore.userInfo.licensePlate;
}
if (userStore.userInfo.vehicleType) {
vehicleForm.value.vehicleType = userStore.userInfo.vehicleType;
}
});
</script>
<template>
<view class="container">
<view class="section">
<view class="section-header">
<text class="section-title">车辆基本信息</text>
</view>
<view class="section-content">
<view class="form-item">
<text class="label">车牌号</text>
<input
v-model="vehicleForm.plateNumber"
class="form-input"
type="text"
placeholder="请输入车牌号"
/>
</view>
<view class="form-item">
<text class="label">车辆类型</text>
<picker
:range="vehicleTypes"
range-key="label"
@change="e => vehicleForm.vehicleType = vehicleTypes[e.detail.value].value"
>
<view class="picker">
{{ vehicleTypes.find(item => item.value === vehicleForm.vehicleType)?.label || '请选择车辆类型' }}
</view>
</picker>
</view>
<view class="form-item">
<text class="label">载重量()</text>
<input
v-model="vehicleForm.loadCapacity"
class="form-input"
type="digit"
placeholder="请输入载重量"
/>
</view>
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">车辆照片</text>
</view>
<view class="section-content">
<view class="image-upload-area">
<view
v-for="(image, index) in vehicleForm.vehiclePhotos"
:key="index"
class="image-preview"
>
<image :src="image" class="preview-image" mode="aspectFill" />
<view
class="remove-btn"
@click.stop="removeImage(index)"
>
×
</view>
</view>
<view
v-if="vehicleForm.vehiclePhotos.length < 3"
class="upload-btn"
@click="uploadImage"
>
<text class="icon">+</text>
<text class="text">上传照片</text>
</view>
</view>
</view>
</view>
<view class="save-section">
<button class="save-btn" @click="saveVehicleInfo">保存车辆信息</button>
</view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.section {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.section-header {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
.section-title {
font-size: 28rpx;
font-weight: bold;
}
}
.section-content {
padding: 24rpx;
.form-item {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
.label {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 15rpx;
}
.form-input {
height: 70rpx;
padding: 0 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
box-sizing: border-box;
}
.picker {
height: 70rpx;
padding: 0 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
display: flex;
align-items: center;
color: #666;
}
}
.image-upload-area {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.image-preview {
position: relative;
width: 150rpx;
height: 150rpx;
.preview-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.remove-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background: #ff4d4f;
border-radius: 50%;
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.upload-btn {
width: 150rpx;
height: 150rpx;
border: 1rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.icon {
font-size: 50rpx;
line-height: 1;
}
.text {
font-size: 22rpx;
color: #666;
margin-top: 10rpx;
}
}
}
}
}
.save-section {
padding: 20rpx 0 40rpx;
.save-btn {
height: 80rpx;
line-height: 80rpx;
background: #1989fa;
color: #fff;
font-size: 28rpx;
border-radius: 12rpx;
}
}
</style>

View File

@@ -0,0 +1,5 @@
import { createPinia } from 'pinia';
const store = createPinia();
export default store;

View File

@@ -0,0 +1,62 @@
import { defineStore } from 'pinia';
export const useTaskStore = defineStore('task', {
state: () => ({
// 任务列表
taskList: [],
// 当前任务
currentTask: null,
// 任务状态筛选
taskFilter: 'all'
}),
getters: {
// 根据状态筛选的任务列表
filteredTasks: (state) => {
if (state.taskFilter === 'all') {
return state.taskList;
}
return state.taskList.filter(task => task.status === state.taskFilter);
},
// 当前进行中的任务
ongoingTask: (state) => {
return state.taskList.find(task => task.status === 'ongoing') || null;
}
},
actions: {
// 设置任务列表
setTaskList(tasks) {
this.taskList = tasks;
},
// 添加任务
addTask(task) {
this.taskList.unshift(task);
},
// 更新任务
updateTask(updatedTask) {
const index = this.taskList.findIndex(task => task.id === updatedTask.id);
if (index !== -1) {
this.taskList[index] = { ...this.taskList[index], ...updatedTask };
}
},
// 设置当前任务
setCurrentTask(task) {
this.currentTask = task;
},
// 设置任务筛选状态
setTaskFilter(filter) {
this.taskFilter = filter;
},
// 根据ID获取任务详情
getTaskById(id) {
return this.taskList.find(task => task.id === id) || null;
}
}
});

View File

@@ -0,0 +1,62 @@
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
// 用户信息
userInfo: {
id: '',
name: '',
phone: '',
licensePlate: '',
vehicleType: ''
},
// 登录状态
isLogin: false,
// token
token: ''
}),
getters: {
// 是否已登录
loggedIn: (state) => state.isLogin,
// 用户姓名
userName: (state) => state.userInfo.name
},
actions: {
// 设置用户信息
setUserInfo(userInfo) {
this.userInfo = { ...this.userInfo, ...userInfo };
},
// 设置登录状态
setLoginStatus(status) {
this.isLogin = status;
},
// 设置token
setToken(token) {
this.token = token;
},
// 登录
login(userInfo, token) {
this.setUserInfo(userInfo);
this.setToken(token);
this.setLoginStatus(true);
},
// 登出
logout() {
this.userInfo = {
id: '',
name: '',
phone: '',
licensePlate: '',
vehicleType: ''
};
this.token = '';
this.isLogin = false;
}
}
});

View File

@@ -0,0 +1,58 @@
import request from './request';
// 用户相关接口
export const userApi = {
// 登录
login: (data) => request.post('/auth/login', data),
// 获取用户信息
getUserInfo: () => request.get('/user/info'),
// 更新用户信息
updateUserInfo: (data) => request.put('/user/info', data),
// 上传车辆信息
uploadVehicleInfo: (data) => request.post('/user/vehicle', data)
};
// 任务相关接口
export const taskApi = {
// 获取任务列表
getTaskList: (params) => request.get('/tasks', params),
// 获取任务详情
getTaskDetail: (id) => request.get(`/tasks/${id}`),
// 接受任务
acceptTask: (id) => request.post(`/tasks/${id}/accept`),
// 拒绝任务
rejectTask: (id, reason) => request.post(`/tasks/${id}/reject`, { reason }),
// 开始任务
startTask: (id) => request.post(`/tasks/${id}/start`),
// 完成任务
completeTask: (id) => request.post(`/tasks/${id}/complete`)
};
// 运输相关接口
export const transportApi = {
// 上报位置
reportLocation: (data) => request.post('/transport/location', data),
// 上报状态
reportStatus: (data) => request.post('/transport/status', data),
// 获取运输轨迹
getTrack: (taskId) => request.get(`/transport/track/${taskId}`)
};
// 交付相关接口
export const deliveryApi = {
// 确认交付
confirmDelivery: (data) => request.post('/delivery/confirm', data),
// 获取交付记录
getDeliveryRecords: (params) => request.get('/delivery/records', params)
};

View File

@@ -0,0 +1,100 @@
// 工具函数集合
/**
* 格式化时间
* @param {Date|string} date - 时间
* @param {string} format - 格式化字符串
* @returns {string} 格式化后的时间
*/
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} delay - 延迟时间(ms)
* @returns {Function} 防抖后的函数
*/
export function debounce(func, delay) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
/**
* 节流函数
* @param {Function} func - 要节流的函数
* @param {number} delay - 延迟时间(ms)
* @returns {Function} 节流后的函数
*/
export function throttle(func, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime > delay) {
lastTime = now;
func.apply(this, args);
}
};
}
/**
* 深拷贝对象
* @param {any} obj - 要拷贝的对象
* @returns {any} 拷贝后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(item => deepClone(item));
if (obj instanceof Object) {
const clonedObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
}
/**
* 验证手机号
* @param {string} phone - 手机号
* @returns {boolean} 是否为有效手机号
*/
export function validatePhone(phone) {
const reg = /^1[3-9]\d{9}$/;
return reg.test(phone);
}
/**
* 验证车牌号
* @param {string} plate - 车牌号
* @returns {boolean} 是否为有效车牌号
*/
export function validatePlateNumber(plate) {
const reg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;
return reg.test(plate);
}

View File

@@ -0,0 +1,144 @@
// 网络请求封装
class Request {
constructor(baseURL = '') {
this.baseURL = baseURL;
}
// 请求拦截器
interceptors = {
request: (config) => config,
response: (response) => response
};
// 设置请求拦截器
setRequestInterceptor(interceptor) {
this.interceptors.request = interceptor;
}
// 设置响应拦截器
setResponseInterceptor(interceptor) {
this.interceptors.response = interceptor;
}
// 通用请求方法
request(options) {
return new Promise((resolve, reject) => {
// 应用请求拦截器
const config = this.interceptors.request({
url: this.baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: options.header || {},
...options
});
// 发起请求
uni.request({
...config,
success: (res) => {
// 应用响应拦截器
const response = this.interceptors.response(res);
resolve(response);
},
fail: (err) => {
reject(err);
}
});
});
}
// GET请求
get(url, data = {}, options = {}) {
return this.request({
url,
method: 'GET',
data,
...options
});
}
// POST请求
post(url, data = {}, options = {}) {
return this.request({
url,
method: 'POST',
data,
...options
});
}
// PUT请求
put(url, data = {}, options = {}) {
return this.request({
url,
method: 'PUT',
data,
...options
});
}
// DELETE请求
delete(url, data = {}, options = {}) {
return this.request({
url,
method: 'DELETE',
data,
...options
});
}
}
// 创建请求实例
const request = new Request('http://localhost:3002/api/driver');
// 设置默认请求头
request.setRequestInterceptor((config) => {
// 可以在这里添加token等认证信息
const token = uni.getStorageSync('driver_token');
if (token) {
config.header = {
...config.header,
'Authorization': `Bearer ${token}`
};
}
// 设置Content-Type
config.header = {
...config.header,
'Content-Type': 'application/json'
};
return config;
});
// 设置响应拦截器
request.setResponseInterceptor((response) => {
const { data, statusCode } = response;
// 根据状态码处理响应
if (statusCode >= 200 && statusCode < 300) {
return data;
} else if (statusCode === 401) {
// token过期或未授权
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
});
// 可以在这里执行登出操作
setTimeout(() => {
uni.redirectTo({
url: '/pages/auth/login'
});
}, 1500);
return Promise.reject(data);
} else {
// 其他错误
uni.showToast({
title: data.message || '请求失败',
icon: 'none'
});
return Promise.reject(data);
}
});
export default request;

View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"@dcloudio/types",
"miniprogram-api-typings",
"mini-types"
],
"paths": {
"@/*": [
"./src/*"
]
},
"lib": [
"esnext",
"dom"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,13 +0,0 @@
{
"name": "sales-mp",
"version": "1.0.0",
"description": "活牛销售小程序",
"main": "main.js",
"scripts": {
"dev": "uni -p mp-weixin"
},
"dependencies": {
"@dcloudio/uni-app": "^3.0.0",
"pinia": "^2.0.0"
}
}

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
// 活牛销售首页
</script>
<template>
<view class="container">
<text>活牛销售小程序首页</text>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
</style>