更新项目文档,明确小程序独立架构和技术选型

This commit is contained in:
ylweng
2025-09-17 23:01:14 +08:00
parent aa1e08baa5
commit eeedbebb9c
20 changed files with 2020 additions and 116 deletions

View File

@@ -0,0 +1,34 @@
{
"name": "client-mp",
"version": "1.0.0",
"description": "活牛采购智能数字化系统 - 客户端小程序",
"main": "main.js",
"scripts": {
"dev": "hbuilderx",
"build": "hbuilderx build"
},
"keywords": [
"活牛采购",
"小程序",
"uni-app"
],
"author": "",
"license": "ISC",
"dependencies": {
"@dcloudio/uni-app": "^3.0.0-alpha-3070320230701001",
"pinia": "^2.1.0",
"vue": "^3.3.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^8.0.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^2.0.0",
"typescript": "^5.0.0"
}
}

View File

View File

@@ -0,0 +1,30 @@
import { request } from '@/utils/request';
import type { IOrder, IOrderStats } from '@/types/order';
// 获取订单统计
export const fetchOrderStats = async (): Promise<IOrderStats> => {
try {
const res = await request({
url: '/order/stats',
method: 'GET'
});
return res.data;
} catch (error) {
console.error('获取订单统计失败:', error);
throw error;
}
};
// 获取最近订单
export const fetchRecentOrders = async (): Promise<IOrder[]> => {
try {
const res = await request({
url: '/order/recent',
method: 'GET'
});
return res.data;
} catch (error) {
console.error('获取最近订单失败:', error);
throw error;
}
};

View File

@@ -0,0 +1,16 @@
import { request } from '@/utils/request';
import type { IUser } from '@/types/user';
// 获取用户信息
export const fetchUserInfo = async (): Promise<IUser> => {
try {
const res = await request({
url: '/user/info',
method: 'GET'
});
return res.data;
} catch (error) {
console.error('获取用户信息失败:', error);
throw error;
}
};

View File

@@ -0,0 +1,30 @@
import { createSSRApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
import * as Pinia from 'pinia';
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
// 配置Pinia
pinia.use(({ store }) => {
// 持久化存储
const storageKey = `pinia_${store.$id}`;
const savedState = uni.getStorageSync(storageKey);
if (savedState) {
store.$patch(JSON.parse(savedState));
}
store.$subscribe((mutation, state) => {
uni.setStorageSync(storageKey, JSON.stringify(state));
});
});
app.use(pinia);
return {
app,
pinia
};
}

View File

@@ -0,0 +1,84 @@
{
"name": "活牛采购-客户端",
"appid": "",
"description": "活牛采购智能数字化系统客户端小程序",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
]
},
"ios": {
"dSYMs": false,
"privacyDescription": {
"NSLocationWhenInUseUsageDescription": "此应用需要访问您的位置信息,以便提供运输跟踪服务",
"NSCameraUsageDescription": "此应用需要访问您的相机,以便上传照片和视频",
"NSPhotoLibraryUsageDescription": "此应用需要访问您的相册,以便选择照片和视频",
"NSMicrophoneUsageDescription": "此应用需要访问您的麦克风,以便录制视频"
}
}
}
},
"mp-weixin": {
"appid": "wx0123456789abcdef",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "您的位置信息将用于运输跟踪服务"
}
},
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
]
},
"mp-alipay": {
"usingComponents": true,
"appid": "2021000000000000"
},
"vueVersion": "3",
"h5": {
"router": {
"mode": "hash",
"base": "/"
},
"title": "活牛采购-客户端",
"optimization": {
"treeShaking": {
"enable": true
}
}
}
}

View File

@@ -0,0 +1,140 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "活牛采购",
"enablePullDownRefresh": true
}
},
{
"path": "pages/order/order-list",
"style": {
"navigationBarTitleText": "订单管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/order/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "pages/order/order-create",
"style": {
"navigationBarTitleText": "创建订单"
}
},
{
"path": "pages/tracking/tracking-list",
"style": {
"navigationBarTitleText": "运输跟踪"
}
},
{
"path": "pages/tracking/tracking-detail",
"style": {
"navigationBarTitleText": "运输详情"
}
},
{
"path": "pages/acceptance/acceptance-list",
"style": {
"navigationBarTitleText": "验收管理"
}
},
{
"path": "pages/acceptance/acceptance-detail",
"style": {
"navigationBarTitleText": "验收详情"
}
},
{
"path": "pages/payment/payment-list",
"style": {
"navigationBarTitleText": "结算支付"
}
},
{
"path": "pages/payment/payment-detail",
"style": {
"navigationBarTitleText": "结算详情"
}
},
{
"path": "pages/statistics/statistics",
"style": {
"navigationBarTitleText": "数据统计"
}
},
{
"path": "pages/auth/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/auth/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人中心"
}
},
{
"path": "pages/user/settings",
"style": {
"navigationBarTitleText": "设置"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "活牛采购",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/order/order-list",
"iconPath": "static/images/tabbar/order.png",
"selectedIconPath": "static/images/tabbar/order-active.png",
"text": "订单"
},
{
"pagePath": "pages/tracking/tracking-list",
"iconPath": "static/images/tabbar/tracking.png",
"selectedIconPath": "static/images/tabbar/tracking-active.png",
"text": "跟踪"
},
{
"pagePath": "pages/user/profile",
"iconPath": "static/images/tabbar/user.png",
"selectedIconPath": "static/images/tabbar/user-active.png",
"text": "我的"
}
]
},
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
}

View File

@@ -0,0 +1,289 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useOrderStore } from '@/stores/order';
import { useUserStore } from '@/stores/user';
import type { IHomeData } from './types';
// 状态管理
const orderStore = useOrderStore();
const userStore = useUserStore();
// 页面数据
const homeData = ref<IHomeData>({
pendingOrders: 0,
inTransitOrders: 0,
pendingAcceptance: 0,
pendingPayment: 0,
recentOrders: [],
statistics: {
totalAmount: 0,
totalWeight: 0,
averagePrice: 0
}
});
// 加载数据
const loading = ref(false);
const loadData = async () => {
try {
loading.value = true;
await Promise.all([
orderStore.fetchOrderStats(),
orderStore.fetchRecentOrders(),
userStore.fetchUserInfo()
]);
homeData.value = {
pendingOrders: orderStore.pendingCount,
inTransitOrders: orderStore.inTransitCount,
pendingAcceptance: orderStore.pendingAcceptanceCount,
pendingPayment: orderStore.pendingPaymentCount,
recentOrders: orderStore.recentOrders.slice(0, 5),
statistics: {
totalAmount: orderStore.totalAmount,
totalWeight: orderStore.totalWeight,
averagePrice: orderStore.averagePrice
}
};
} catch (error) {
uni.showToast({
title: '加载数据失败',
icon: 'none'
});
console.error('加载首页数据失败:', error);
} finally {
loading.value = false;
}
};
// 页面生命周期
onMounted(() => {
loadData();
});
// 下拉刷新
const onRefresh = () => {
loadData();
};
// 跳转到订单列表
const navigateToOrderList = (type: string) => {
uni.navigateTo({
url: `/pages/order/order-list?type=${type}`
});
};
// 跳转到订单详情
const navigateToOrderDetail = (id: string) => {
uni.navigateTo({
url: `/pages/order/order-detail?id=${id}`
});
};
</script>
<template>
<view class="container">
<!-- 下拉刷新 -->
<scroll-view
scroll-y
refresher-enabled
:refresher-triggered="loading"
@refresherrefresh="onRefresh"
>
<!-- 用户信息 -->
<view class="user-info card">
<view class="flex-between">
<view>
<text class="text-bold">欢迎回来{{ userStore.userInfo.name }}</text>
<view class="margin-top-sm text-gray">
<text>上次登录: {{ userStore.userInfo.lastLogin }}</text>
</view>
</view>
<image
:src="userStore.userInfo.avatar"
mode="aspectFill"
class="avatar"
/>
</view>
</view>
<!-- 订单概览 -->
<view class="order-overview card">
<view class="flex-between">
<text class="text-bold">订单概览</text>
<text class="text-primary" @tap="navigateToOrderList('all')">查看全部</text>
</view>
<view class="flex-between margin-top">
<view
class="overview-item"
@tap="navigateToOrderList('pending')"
>
<text class="overview-count">{{ homeData.pendingOrders }}</text>
<text class="overview-label">待处理</text>
</view>
<view
class="overview-item"
@tap="navigateToOrderList('in_transit')"
>
<text class="overview-count">{{ homeData.inTransitOrders }}</text>
<text class="overview-label">运输中</text>
</view>
<view
class="overview-item"
@tap="navigateToOrderList('pending_acceptance')"
>
<text class="overview-count">{{ homeData.pendingAcceptance }}</text>
<text class="overview-label">待验收</text>
</view>
<view
class="overview-item"
@tap="navigateToOrderList('pending_payment')"
>
<text class="overview-count">{{ homeData.pendingPayment }}</text>
<text class="overview-label">待支付</text>
</view>
</view>
</view>
<!-- 统计信息 -->
<view class="statistics card">
<view class="flex-between">
<text class="text-bold">采购统计</text>
<text class="text-primary">本月</text>
</view>
<view class="flex-between margin-top">
<view class="stat-item">
<text class="stat-value">¥{{ homeData.statistics.totalAmount }}</text>
<text class="stat-label">总金额</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ homeData.statistics.totalWeight }}kg</text>
<text class="stat-label">总重量</text>
</view>
<view class="stat-item">
<text class="stat-value">¥{{ homeData.statistics.averagePrice }}</text>
<text class="stat-label">均价/kg</text>
</view>
</view>
</view>
<!-- 最近订单 -->
<view class="recent-orders card">
<view class="flex-between">
<text class="text-bold">最近订单</text>
<text class="text-primary" @tap="navigateToOrderList('all')">查看全部</text>
</view>
<view class="margin-top">
<view
v-for="order in homeData.recentOrders"
:key="order.id"
class="order-item"
@tap="navigateToOrderDetail(order.id)"
>
<view class="flex-between">
<text class="text-bold">订单号: {{ order.orderNo }}</text>
<text :class="`status-${order.status}`">{{ order.statusText }}</text>
</view>
<view class="flex-between margin-top-sm">
<text>供应商: {{ order.supplierName }}</text>
<text>{{ order.createTime }}</text>
</view>
<view class="flex-between margin-top-sm">
<text>数量: {{ order.quantity }}</text>
<text>重量: {{ order.weight }}kg</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<style lang="scss">
.container {
padding: 20rpx;
}
.card {
background-color: #fff;
border-radius: 8rpx;
box-shadow: 0 2rpx 12rpx 0 rgba(0, 0, 0, 0.1);
margin-bottom: 20rpx;
padding: 20rpx;
}
.user-info {
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
}
}
.overview-item {
display: flex;
flex-direction: column;
align-items: center;
.overview-count {
font-size: 36rpx;
font-weight: bold;
color: #3cc51f;
}
.overview-label {
font-size: 24rpx;
color: #666;
margin-top: 10rpx;
}
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
.stat-value {
font-size: 28rpx;
font-weight: bold;
}
.stat-label {
font-size: 24rpx;
color: #666;
margin-top: 10rpx;
}
}
.order-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
&:last-child {
border-bottom: none;
}
.status-pending {
color: #ff9900;
}
.status-in_transit {
color: #1989fa;
}
.status-pending_acceptance {
color: #ff9900;
}
.status-completed {
color: #3cc51f;
}
.status-canceled {
color: #999;
}
}
</style>

View File

@@ -0,0 +1,25 @@
// 首页数据类型定义
export interface IHomeData {
pendingOrders: number;
inTransitOrders: number;
pendingAcceptance: number;
pendingPayment: number;
recentOrders: IRecentOrder[];
statistics: {
totalAmount: number;
totalWeight: number;
averagePrice: number;
};
}
// 最近订单类型定义
export interface IRecentOrder {
id: string;
orderNo: string;
supplierName: string;
quantity: number;
weight: number;
status: string;
statusText: string;
createTime: string;
}

View File

@@ -0,0 +1,59 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { IOrder, IOrderStats } from '@/types/order';
import { fetchOrderStats, fetchRecentOrders } from '@/api/order';
export const useOrderStore = defineStore('order', () => {
// 订单统计
const pendingCount = ref(0);
const inTransitCount = ref(0);
const pendingAcceptanceCount = ref(0);
const pendingPaymentCount = ref(0);
const totalAmount = ref(0);
const totalWeight = ref(0);
const averagePrice = ref(0);
// 最近订单
const recentOrders = ref<IOrder[]>([]);
// 获取订单统计
const fetchOrderStats = async () => {
try {
const stats = await fetchOrderStats();
pendingCount.value = stats.pendingCount;
inTransitCount.value = stats.inTransitCount;
pendingAcceptanceCount.value = stats.pendingAcceptanceCount;
pendingPaymentCount.value = stats.pendingPaymentCount;
totalAmount.value = stats.totalAmount;
totalWeight.value = stats.totalWeight;
averagePrice.value = stats.averagePrice;
} catch (error) {
console.error('获取订单统计失败:', error);
throw error;
}
};
// 获取最近订单
const fetchRecentOrders = async () => {
try {
const orders = await fetchRecentOrders();
recentOrders.value = orders;
} catch (error) {
console.error('获取最近订单失败:', error);
throw error;
}
};
return {
pendingCount,
inTransitCount,
pendingAcceptanceCount,
pendingPaymentCount,
totalAmount,
totalWeight,
averagePrice,
recentOrders,
fetchOrderStats,
fetchRecentOrders
};
});

View File

@@ -0,0 +1,33 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { IUser } from '@/types/user';
import { fetchUserInfo } from '@/api/user';
export const useUserStore = defineStore('user', () => {
// 用户信息
const userInfo = ref<IUser>({
id: '',
name: '加载中...',
avatar: '/static/images/default-avatar.png',
lastLogin: '',
phone: '',
company: '',
role: 'client'
});
// 获取用户信息
const fetchUserInfo = async () => {
try {
const info = await fetchUserInfo();
userInfo.value = info;
} catch (error) {
console.error('获取用户信息失败:', error);
throw error;
}
};
return {
userInfo,
fetchUserInfo
};
});

View File

@@ -0,0 +1,35 @@
// 订单状态类型
export type OrderStatus =
| 'pending'
| 'confirmed'
| 'in_transit'
| 'pending_acceptance'
| 'completed'
| 'canceled';
// 订单基础信息
export interface IOrder {
id: string;
orderNo: string;
supplierId: string;
supplierName: string;
quantity: number;
weight: number;
price: number;
totalAmount: number;
status: OrderStatus;
statusText: string;
createTime: string;
updateTime: string;
}
// 订单统计信息
export interface IOrderStats {
pendingCount: number;
inTransitCount: number;
pendingAcceptanceCount: number;
pendingPaymentCount: number;
totalAmount: number;
totalWeight: number;
averagePrice: number;
}

View File

@@ -0,0 +1,14 @@
// 请求选项
export interface RequestOptions {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: any;
header?: Record<string, string>;
}
// 响应数据
export interface ResponseData<T = any> {
code: number;
message: string;
data: T;
}

View File

@@ -0,0 +1,17 @@
// 用户角色类型
export type UserRole =
| 'client'
| 'supplier'
| 'driver'
| 'staff';
// 用户信息
export interface IUser {
id: string;
name: string;
avatar: string;
lastLogin: string;
phone: string;
company: string;
role: UserRole;
}

View File

@@ -0,0 +1,86 @@
import type { RequestOptions } from '@/types/request';
// 环境配置
const config = {
development: {
baseURL: 'http://localhost:3001/api',
wsURL: 'ws://localhost:3001'
},
production: {
baseURL: 'https://api.niumall.com/api',
wsURL: 'wss://api.niumall.com'
}
};
// 当前环境
const env = process.env.NODE_ENV === 'production' ? 'production' : 'development';
const { baseURL, wsURL } = config[env];
// 请求封装
export const request = (options: RequestOptions) => {
return new Promise((resolve, reject) => {
uni.request({
url: baseURL + options.url,
method: options.method || 'GET',
data: options.data,
header: {
'Authorization': `Bearer ${getToken()}`,
'Content-Type': 'application/json',
...options.header
},
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data);
} else {
handleError(res);
reject(res);
}
},
fail: (err) => {
handleError(err);
reject(err);
}
});
});
};
// WebSocket连接
export const connectWebSocket = () => {
return uni.connectSocket({
url: wsURL,
success: () => {
console.log('WebSocket连接成功');
},
fail: (err) => {
console.error('WebSocket连接失败:', err);
}
});
};
// 获取token
const getToken = (): string => {
try {
const token = uni.getStorageSync('token');
return token || '';
} catch (e) {
return '';
}
};
// 错误处理
const handleError = (error: any) => {
console.error('请求错误:', error);
// 未授权
if (error.statusCode === 401) {
uni.redirectTo({
url: '/pages/auth/login'
});
}
// 其他错误
uni.showToast({
title: error.data?.message || '请求失败',
icon: 'none'
});
};