Merge remote-tracking branch 'origin/main'
# Conflicts: # backend/.env.example # backend/package-lock.json # backend/package.json
This commit is contained in:
251
README.md
251
README.md
@@ -1,47 +1,240 @@
|
||||
# NiuMall 项目
|
||||
# 活牛采购智能数字化系统 (NiuMall)
|
||||
|
||||
## 项目结构
|
||||
## 📋 项目概述
|
||||
|
||||
本项目采用模块化架构设计,包含以下主要目录:
|
||||
活牛采购智能数字化系统是一个专业的活牛采购全流程数字化管理解决方案,采用模块化设计架构,支持多端协同工作,实现从采购计划到最终结算的全链路数字化管理。
|
||||
|
||||
### 📁 backend - 后端服务
|
||||
后端API服务,基于Node.js/Java/Python等技术栈开发
|
||||
**项目特色:**
|
||||
- 🔄 **模块化架构**:前后端分离,各模块独立开发部署
|
||||
- 📱 **多端支持**:官网、管理后台、小程序矩阵全覆盖
|
||||
- 🔒 **统一认证**:单点登录,统一用户中心
|
||||
- 📊 **实时数据**:WebSocket实时数据同步
|
||||
- 🎯 **专业化**:专注活牛采购行业需求
|
||||
|
||||
### 📁 admin-system - 管理后台
|
||||
基于Vue/React的管理后台前端项目
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 📁 website - 官网
|
||||
公司官网或电商平台前端项目
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Website │ │ Admin System │ │ Mini Programs │
|
||||
│ (HTML5+CSS3) │ │ (Vue 3) │ │ (uni-app) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└──────────┬───────────┴──────────┬───────────┘
|
||||
│ │
|
||||
┌────────┴─────────┐ ┌──────┴───────┐
|
||||
│ API Gateway │ │ 统一用户中心 │
|
||||
│ (Authentication)│ │(Single SSO) │
|
||||
└────────┬─────────┘ └──────┬───────┘
|
||||
│ │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ Backend Services │
|
||||
│ (Node.js) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ Unified Database │
|
||||
│ (MySQL + Redis) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 📁 mini_program - 微信小程序矩阵
|
||||
包含多个微信小程序项目的目录
|
||||
## 📁 项目结构
|
||||
|
||||
### 📁 docs - 文档目录
|
||||
项目文档、API文档、设计文档等
|
||||
```
|
||||
niumall/
|
||||
├── 📂 docs/ # 📚 项目文档
|
||||
│ ├── 活牛采购智能数字化系统PRD.md
|
||||
│ ├── 技术实施方案.md
|
||||
│ ├── 官网需求文档.md
|
||||
│ └── Live-Cattle-Procurement-SOP-System-PRD.md
|
||||
├── 📂 website/ # 🌐 企业官网
|
||||
│ ├── index.html # 首页
|
||||
│ ├── css/custom.css # 自定义样式
|
||||
│ ├── js/main.js # 主要逻辑
|
||||
│ └── ...
|
||||
├── 📂 admin-system/ # 🔧 管理后台
|
||||
│ └── README.md # Vue 3 + TypeScript + Element Plus
|
||||
├── 📂 backend/ # ⚙️ 后端服务
|
||||
│ └── README.md # Node.js + Express + MySQL
|
||||
├── 📂 mini_program/ # 📱 小程序矩阵
|
||||
│ └── README.md # uni-app 跨平台开发
|
||||
└── 📂 test/ # 🧪 测试目录
|
||||
```
|
||||
|
||||
### 📁 test - 测试文件目录
|
||||
单元测试、集成测试、端到端测试等
|
||||
### 🌐 Website - 企业官网
|
||||
**技术栈**:HTML5 + Bootstrap 5 + 原生JavaScript
|
||||
- 企业品牌展示和产品介绍
|
||||
- 响应式设计,SEO优化
|
||||
- 客户案例和解决方案展示
|
||||
- 在线咨询和试用申请
|
||||
|
||||
## 开发规范
|
||||
### 🔧 Admin System - 管理后台
|
||||
**技术栈**:Vue 3 + TypeScript + Element Plus + Vite + Pinia
|
||||
- 用户管理和权限控制
|
||||
- 订单管理和流程监控
|
||||
- 数据统计和分析报表
|
||||
- 系统配置和维护
|
||||
|
||||
1. 每个目录下应有独立的package.json和开发配置
|
||||
2. 遵循统一的代码规范和提交规范
|
||||
3. 文档及时更新,保持与代码同步
|
||||
### 📱 Mini Program - 小程序矩阵
|
||||
**技术栈**:uni-app + Vue 3 + TypeScript
|
||||
- **客户端小程序**:采购订单创建和跟踪
|
||||
- **供应商小程序**:牛只管理和装车操作
|
||||
- **司机小程序**:运输跟踪和状态上报
|
||||
- **内部员工小程序**:内部操作和管理
|
||||
|
||||
## 快速开始
|
||||
### ⚙️ Backend - 后端服务
|
||||
**技术栈**:Node.js + Express + MySQL + Redis
|
||||
- 微服务架构设计
|
||||
- 统一API接口服务
|
||||
- 实时数据同步
|
||||
- 文件存储和处理
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 16.0.0
|
||||
- MySQL >= 5.7
|
||||
- Redis >= 6.0
|
||||
- 微信开发者工具(小程序开发)
|
||||
|
||||
### 数据库配置
|
||||
```bash
|
||||
# 安装依赖(根据具体项目)
|
||||
npm install
|
||||
# 数据库连接信息
|
||||
主机: 129.211.213.226
|
||||
端口: 9527
|
||||
用户名: root
|
||||
密码: aiotAiot123!
|
||||
数据库: jiebandata
|
||||
```
|
||||
|
||||
# 启动开发服务
|
||||
### 启动步骤
|
||||
|
||||
#### 1. 启动后端服务
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
#### 2. 启动管理后台
|
||||
```bash
|
||||
cd admin-system
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支
|
||||
3. 提交更改
|
||||
4. 推送到分支
|
||||
5. 开启Pull Request
|
||||
#### 3. 启动企业官网
|
||||
```bash
|
||||
cd website
|
||||
# 直接用浏览器打开 index.html 或使用本地服务器
|
||||
python -m http.server 8080 # Python方式
|
||||
# 或
|
||||
npx serve . # Node.js方式
|
||||
```
|
||||
|
||||
#### 4. 小程序开发
|
||||
```bash
|
||||
cd mini_program
|
||||
npm install
|
||||
# 使用微信开发者工具打开对应小程序目录
|
||||
```
|
||||
|
||||
## 👥 用户角色
|
||||
|
||||
| 角色 | 职责 | 主要功能 |
|
||||
|------|------|----------|
|
||||
| 🏭 **采购人** | 发起采购需求,验收确认 | 订单创建、进度跟踪、验收支付 |
|
||||
| 🤝 **贸易商** | 订单转发,供应商管理 | 订单管理、供应商资质审核、结算处理 |
|
||||
| 🐄 **供应商** | 牛只准备,装车管理 | 牛只信息维护、证件上传、装车监控 |
|
||||
| 🚛 **司机** | 运输执行,状态上报 | 实时定位、运输跟踪、状态报告 |
|
||||
| 👨💼 **内部员工** | 系统管理,业务监督 | 用户管理、数据分析、异常处理 |
|
||||
|
||||
## 📊 核心功能
|
||||
|
||||
### 1. 采购订单管理
|
||||
- ✅ 订单创建和审核流程
|
||||
- ✅ 多级审批和权限控制
|
||||
- ✅ 订单状态实时跟踪
|
||||
- ✅ 异常处理和风险控制
|
||||
|
||||
### 2. 运输跟踪管理
|
||||
- 🚛 实时GPS定位跟踪
|
||||
- 📹 装车卸车视频监控
|
||||
- 📱 移动端状态上报
|
||||
- ⏰ 运输时效监控
|
||||
|
||||
### 3. 质检验收管理
|
||||
- 🔍 标准化质检流程
|
||||
- 📋 检疫证明管理
|
||||
- ⚖️ 称重数据记录
|
||||
- 🎯 质量标准配置
|
||||
|
||||
### 4. 结算支付管理
|
||||
- 💰 自动结算计算
|
||||
- 💳 在线支付支持
|
||||
- 📊 财务报表生成
|
||||
- 🔒 资金安全保障
|
||||
|
||||
## 🛠️ 开发规范
|
||||
|
||||
### 代码规范
|
||||
- **JavaScript/TypeScript**:遵循 ESLint + Prettier 规范
|
||||
- **Vue**:遵循 Vue 3 Composition API 最佳实践
|
||||
- **CSS**:使用 BEM 命名规范
|
||||
- **提交规范**:遵循 Conventional Commits
|
||||
|
||||
### 分支管理
|
||||
- `main`:主分支,生产环境代码
|
||||
- `develop`:开发分支,集成测试
|
||||
- `feature/*`:功能分支
|
||||
- `hotfix/*`:紧急修复分支
|
||||
|
||||
## 📈 部署方案
|
||||
|
||||
### 生产环境
|
||||
- **Web服务器**:Nginx + PM2
|
||||
- **数据库**:MySQL 主从复制
|
||||
- **缓存**:Redis 集群
|
||||
- **文件存储**:MinIO/阿里云OSS
|
||||
- **负载均衡**:Nginx Load Balancer
|
||||
|
||||
### 开发环境
|
||||
- **容器化**:Docker + Docker Compose
|
||||
- **CI/CD**:GitHub Actions
|
||||
- **监控**:Prometheus + Grafana
|
||||
- **日志**:ELK Stack
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. **Fork** 本仓库
|
||||
2. **创建**特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. **提交**更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. **推送**到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. **开启** Pull Request
|
||||
|
||||
### 提交信息规范
|
||||
```
|
||||
feat: 新功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
chore: 其他修改
|
||||
```
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- **产品经理**:product@niumall.com
|
||||
- **技术支持**:tech@niumall.com
|
||||
- **商务合作**:business@niumall.com
|
||||
- **客服热线**:400-xxx-xxxx
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
|
||||
|
||||
---
|
||||
|
||||
**🎯 让活牛采购更智能,让业务管理更简单!**
|
||||
68
admin-system/.env.development
Normal file
68
admin-system/.env.development
Normal file
@@ -0,0 +1,68 @@
|
||||
# 开发环境配置
|
||||
NODE_ENV=development
|
||||
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台
|
||||
|
||||
# API接口地址
|
||||
VITE_API_BASE_URL=http://localhost:3002/api
|
||||
|
||||
# WebSocket地址
|
||||
VITE_WS_BASE_URL=ws://localhost:3002
|
||||
|
||||
# 上传文件地址
|
||||
VITE_UPLOAD_URL=http://localhost:3002/api/upload
|
||||
|
||||
# 静态资源地址
|
||||
VITE_STATIC_URL=http://localhost:3002/static
|
||||
|
||||
# 是否启用Mock数据
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否启用开发工具
|
||||
VITE_DEV_TOOLS=true
|
||||
|
||||
# 路由模式 hash | history
|
||||
VITE_ROUTER_MODE=history
|
||||
|
||||
# 应用端口
|
||||
VITE_PORT=3000
|
||||
|
||||
# 代理前缀
|
||||
VITE_API_PREFIX=/api
|
||||
|
||||
# Token密钥
|
||||
VITE_TOKEN_KEY=admin_token
|
||||
|
||||
# Token过期时间(小时)
|
||||
VITE_TOKEN_EXPIRES=24
|
||||
|
||||
# 分页大小
|
||||
VITE_PAGE_SIZE=20
|
||||
|
||||
# 上传文件最大大小(MB)
|
||||
VITE_UPLOAD_MAX_SIZE=10
|
||||
|
||||
# 是否显示设置按钮
|
||||
VITE_SHOW_SETTINGS=true
|
||||
|
||||
# 是否显示标签页
|
||||
VITE_SHOW_TABS=true
|
||||
|
||||
# 是否显示面包屑
|
||||
VITE_SHOW_BREADCRUMB=true
|
||||
|
||||
# 是否固定头部
|
||||
VITE_FIXED_HEADER=true
|
||||
|
||||
# 侧边栏Logo
|
||||
VITE_SIDEBAR_LOGO=true
|
||||
|
||||
# 默认主题色
|
||||
VITE_THEME_COLOR=#409eff
|
||||
|
||||
# 默认布局
|
||||
VITE_LAYOUT=default
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION=1.0.0
|
||||
77
admin-system/.env.production
Normal file
77
admin-system/.env.production
Normal file
@@ -0,0 +1,77 @@
|
||||
# 生产环境配置
|
||||
NODE_ENV=production
|
||||
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台
|
||||
|
||||
# API接口地址
|
||||
VITE_API_BASE_URL=https://api.niumall.com/api
|
||||
|
||||
# WebSocket地址
|
||||
VITE_WS_BASE_URL=wss://api.niumall.com
|
||||
|
||||
# 上传文件地址
|
||||
VITE_UPLOAD_URL=https://api.niumall.com/api/upload
|
||||
|
||||
# 静态资源地址
|
||||
VITE_STATIC_URL=https://static.niumall.com
|
||||
|
||||
# 是否启用Mock数据
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否启用开发工具
|
||||
VITE_DEV_TOOLS=false
|
||||
|
||||
# 路由模式 hash | history
|
||||
VITE_ROUTER_MODE=history
|
||||
|
||||
# 应用端口
|
||||
VITE_PORT=80
|
||||
|
||||
# 代理前缀
|
||||
VITE_API_PREFIX=/api
|
||||
|
||||
# Token密钥
|
||||
VITE_TOKEN_KEY=admin_token
|
||||
|
||||
# Token过期时间(小时)
|
||||
VITE_TOKEN_EXPIRES=24
|
||||
|
||||
# 分页大小
|
||||
VITE_PAGE_SIZE=20
|
||||
|
||||
# 上传文件最大大小(MB)
|
||||
VITE_UPLOAD_MAX_SIZE=10
|
||||
|
||||
# 是否显示设置按钮
|
||||
VITE_SHOW_SETTINGS=false
|
||||
|
||||
# 是否显示标签页
|
||||
VITE_SHOW_TABS=true
|
||||
|
||||
# 是否显示面包屑
|
||||
VITE_SHOW_BREADCRUMB=true
|
||||
|
||||
# 是否固定头部
|
||||
VITE_FIXED_HEADER=true
|
||||
|
||||
# 侧边栏Logo
|
||||
VITE_SIDEBAR_LOGO=true
|
||||
|
||||
# 默认主题色
|
||||
VITE_THEME_COLOR=#409eff
|
||||
|
||||
# 默认布局
|
||||
VITE_LAYOUT=default
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# CDN地址
|
||||
VITE_CDN_URL=https://cdn.niumall.com
|
||||
|
||||
# 错误日志上报地址
|
||||
VITE_ERROR_LOG_URL=https://api.niumall.com/api/error-log
|
||||
|
||||
# 性能监控地址
|
||||
VITE_PERFORMANCE_URL=https://api.niumall.com/api/performance
|
||||
91
admin-system/.eslintrc-auto-import.json
Normal file
91
admin-system/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"acceptHMRUpdate": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"createPinia": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"defineStore": true,
|
||||
"effectScope": true,
|
||||
"getActivePinia": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"mapActions": true,
|
||||
"mapGetters": true,
|
||||
"mapState": true,
|
||||
"mapStores": true,
|
||||
"mapWritableState": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"setActivePinia": true,
|
||||
"setMapStoreSuffix": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"storeToRefs": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useId": true,
|
||||
"useLink": true,
|
||||
"useModel": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"useTemplateRef": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
107
admin-system/.eslintrc.js
Normal file
107
admin-system/.eslintrc.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:vue/vue3-strongly-recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
globals: {
|
||||
ElMessage: 'readonly',
|
||||
ElMessageBox: 'readonly',
|
||||
ElNotification: 'readonly',
|
||||
ElLoading: 'readonly'
|
||||
},
|
||||
rules: {
|
||||
// Vue规则
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/component-tags-order': [
|
||||
'error',
|
||||
{
|
||||
order: ['script', 'template', 'style']
|
||||
}
|
||||
],
|
||||
'vue/component-name-in-template-casing': [
|
||||
'error',
|
||||
'PascalCase',
|
||||
{
|
||||
registeredComponentsOnly: false
|
||||
}
|
||||
],
|
||||
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
||||
'vue/define-emits-declaration': 'error',
|
||||
'vue/define-props-declaration': 'error',
|
||||
'vue/html-button-has-type': 'error',
|
||||
'vue/no-unused-refs': 'error',
|
||||
'vue/no-useless-v-bind': 'error',
|
||||
'vue/prefer-separate-static-class': 'error',
|
||||
'vue/prefer-true-attribute-shorthand': 'error',
|
||||
'vue/block-order': [
|
||||
'error',
|
||||
{
|
||||
order: ['script', 'template', 'style']
|
||||
}
|
||||
],
|
||||
|
||||
// TypeScript规则
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
|
||||
// JavaScript规则
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'prefer-template': 'error',
|
||||
'template-curly-spacing': 'error',
|
||||
'yield-star-spacing': 'error',
|
||||
'prefer-rest-params': 'error',
|
||||
'no-useless-escape': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
'no-fallthrough': 'error',
|
||||
'no-extra-boolean-cast': 'error',
|
||||
'no-case-declarations': 'error',
|
||||
'no-async-promise-executor': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'],
|
||||
extends: ['plugin:cypress/recommended']
|
||||
},
|
||||
{
|
||||
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
99
admin-system/.gitignore
vendored
Normal file
99
admin-system/.gitignore
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Auto-generated files
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
|
||||
# Local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Test
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
40
admin-system/.prettierrc
Normal file
40
admin-system/.prettierrc
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.vue",
|
||||
"options": {
|
||||
"parser": "vue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"options": {
|
||||
"parser": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json"],
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.scss", "*.css"],
|
||||
"options": {
|
||||
"parser": "scss"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,38 +1,447 @@
|
||||
# Admin System 管理后台
|
||||
# Admin System - 活牛采购智能数字化系统管理后台
|
||||
|
||||
## 技术栈
|
||||
- Vue 3 + TypeScript
|
||||
- Element Plus / Ant Design Vue
|
||||
- Vue Router
|
||||
- Pinia状态管理
|
||||
- Axios HTTP客户端
|
||||
## 📋 项目概述
|
||||
|
||||
活牛采购智能数字化系统管理后台是基于Vue 3 + TypeScript的现代化Web应用,为系统管理员、内部员工提供全面的业务管理和数据分析功能。
|
||||
|
||||
**核心功能:**
|
||||
- 👥 用户权限管理(采购人、贸易商、供应商、司机)
|
||||
- 📦 订单全流程管理和监控
|
||||
- 🚛 运输跟踪和状态监控
|
||||
- 💰 结算财务管理
|
||||
- 📊 数据统计和分析报表
|
||||
- ⚙️ 系统配置和维护
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
| 类别 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| **前端框架** | Vue 3 | ^3.3.0 | Composition API + `<script setup>` |
|
||||
| **开发语言** | TypeScript | ^5.0.0 | 类型安全和智能提示 |
|
||||
| **构建工具** | Vite | ^4.4.0 | 快速开发和构建 |
|
||||
| **UI组件库** | Element Plus | ^2.3.0 | 企业级组件库 |
|
||||
| **状态管理** | Pinia | ^2.1.0 | Vue 3官方推荐 |
|
||||
| **路由管理** | Vue Router | ^4.2.0 | 单页面应用路由 |
|
||||
| **HTTP客户端** | Axios | ^1.4.0 | API请求处理 |
|
||||
| **代码规范** | ESLint + Prettier | latest | 代码质量保证 |
|
||||
| **CSS预处理** | Sass/SCSS | ^1.64.0 | 样式管理 |
|
||||
| **图标库** | @element-plus/icons-vue | ^2.1.0 | 图标组件 |
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
admin-system/
|
||||
├── public/ # 公共资源
|
||||
│ ├── favicon.ico
|
||||
│ └── index.html
|
||||
├── src/
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── store/ # 状态管理
|
||||
│ ├── api/ # API接口
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── assets/ # 静态资源
|
||||
├── public/ # 公共文件
|
||||
├── tests/ # 测试文件
|
||||
├── package.json
|
||||
└── README.md
|
||||
│ ├── api/ # API接口定义
|
||||
│ │ ├── auth.ts # 认证相关API
|
||||
│ │ ├── user.ts # 用户管理API
|
||||
│ │ ├── order.ts # 订单管理API
|
||||
│ │ ├── transport.ts # 运输跟踪API
|
||||
│ │ └── settlement.ts # 结算管理API
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ ├── images/
|
||||
│ │ ├── icons/
|
||||
│ │ └── styles/
|
||||
│ │ ├── index.scss # 全局样式
|
||||
│ │ ├── variables.scss # SCSS变量
|
||||
│ │ └── mixins.scss # SCSS混入
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── common/ # 通用组件
|
||||
│ │ │ ├── AppHeader.vue
|
||||
│ │ │ ├── AppSidebar.vue
|
||||
│ │ │ ├── AppBreadcrumb.vue
|
||||
│ │ │ └── PageContainer.vue
|
||||
│ │ ├── charts/ # 图表组件
|
||||
│ │ │ ├── LineChart.vue
|
||||
│ │ │ ├── BarChart.vue
|
||||
│ │ │ └── PieChart.vue
|
||||
│ │ └── forms/ # 表单组件
|
||||
│ │ ├── UserForm.vue
|
||||
│ │ ├── OrderForm.vue
|
||||
│ │ └── SearchForm.vue
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ │ ├── useAuth.ts # 认证逻辑
|
||||
│ │ ├── useTable.ts # 表格通用逻辑
|
||||
│ │ ├── useDialog.ts # 弹窗逻辑
|
||||
│ │ └── usePermission.ts # 权限控制
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ ├── index.ts # 路由主文件
|
||||
│ │ ├── routes.ts # 路由定义
|
||||
│ │ └── guards.ts # 路由守卫
|
||||
│ ├── stores/ # Pinia状态管理
|
||||
│ │ ├── auth.ts # 认证状态
|
||||
│ │ ├── user.ts # 用户状态
|
||||
│ │ ├── order.ts # 订单状态
|
||||
│ │ └── app.ts # 应用全局状态
|
||||
│ ├── types/ # TypeScript类型定义
|
||||
│ │ ├── api.ts # API响应类型
|
||||
│ │ ├── user.ts # 用户类型
|
||||
│ │ ├── order.ts # 订单类型
|
||||
│ │ └── common.ts # 通用类型
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── request.ts # 请求封装
|
||||
│ │ ├── storage.ts # 本地存储
|
||||
│ │ ├── format.ts # 格式化工具
|
||||
│ │ ├── validator.ts # 表单验证
|
||||
│ │ └── constants.ts # 常量定义
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── auth/ # 认证页面
|
||||
│ │ │ ├── Login.vue
|
||||
│ │ │ └── ForgotPassword.vue
|
||||
│ │ ├── dashboard/ # 仪表盘
|
||||
│ │ │ └── Dashboard.vue
|
||||
│ │ ├── user/ # 用户管理
|
||||
│ │ │ ├── UserList.vue
|
||||
│ │ │ ├── UserDetail.vue
|
||||
│ │ │ └── UserRoles.vue
|
||||
│ │ ├── order/ # 订单管理
|
||||
│ │ │ ├── OrderList.vue
|
||||
│ │ │ ├── OrderDetail.vue
|
||||
│ │ │ ├── OrderFlow.vue
|
||||
│ │ │ └── OrderTracking.vue
|
||||
│ │ ├── transport/ # 运输管理
|
||||
│ │ │ ├── TransportList.vue
|
||||
│ │ │ ├── TransportMap.vue
|
||||
│ │ │ └── VehicleManage.vue
|
||||
│ │ ├── settlement/ # 结算管理
|
||||
│ │ │ ├── SettlementList.vue
|
||||
│ │ │ ├── PaymentRecord.vue
|
||||
│ │ │ └── FinancialReport.vue
|
||||
│ │ ├── system/ # 系统管理
|
||||
│ │ │ ├── SystemConfig.vue
|
||||
│ │ │ ├── LogManage.vue
|
||||
│ │ │ └── DataBackup.vue
|
||||
│ │ └── error/ # 错误页面
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 应用入口
|
||||
├── tests/ # 测试文件
|
||||
│ ├── unit/ # 单元测试
|
||||
│ └── e2e/ # 端到端测试
|
||||
├── .env.development # 开发环境配置
|
||||
├── .env.production # 生产环境配置
|
||||
├── .eslintrc.js # ESLint配置
|
||||
├── .prettierrc # Prettier配置
|
||||
├── tsconfig.json # TypeScript配置
|
||||
├── vite.config.ts # Vite配置
|
||||
├── package.json # 项目依赖
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
- 用户管理
|
||||
- 商品管理
|
||||
- 订单管理
|
||||
- 数据统计
|
||||
- 系统设置
|
||||
## 🚀 快速开始
|
||||
|
||||
## 开发规范
|
||||
1. 使用Composition API
|
||||
2. TypeScript严格模式
|
||||
3. 组件命名规范
|
||||
4. 代码分割和懒加载
|
||||
### 环境要求
|
||||
- Node.js >= 16.0.0
|
||||
- npm >= 8.0.0 或 yarn >= 1.22.0
|
||||
- 现代浏览器(Chrome 90+, Firefox 88+, Safari 14+)
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd admin-system
|
||||
npm install
|
||||
# 或
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 开发环境启动
|
||||
```bash
|
||||
npm run dev
|
||||
# 或
|
||||
yarn dev
|
||||
```
|
||||
|
||||
访问地址:http://localhost:3000
|
||||
|
||||
### 生产环境构建
|
||||
```bash
|
||||
npm run build
|
||||
# 或
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 代码检查和格式化
|
||||
```bash
|
||||
# ESLint检查
|
||||
npm run lint
|
||||
|
||||
# 自动修复
|
||||
npm run lint:fix
|
||||
|
||||
# Prettier格式化
|
||||
npm run format
|
||||
```
|
||||
|
||||
### 测试
|
||||
```bash
|
||||
# 单元测试
|
||||
npm run test:unit
|
||||
|
||||
# 端到端测试
|
||||
npm run test:e2e
|
||||
|
||||
# 测试覆盖率
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## 🔐 认证和权限
|
||||
|
||||
### 用户角色
|
||||
| 角色 | 权限描述 | 功能范围 |
|
||||
|------|----------|----------|
|
||||
| **super_admin** | 超级管理员 | 所有功能和系统配置 |
|
||||
| **admin** | 系统管理员 | 用户管理、订单管理、数据查看 |
|
||||
| **operator** | 业务操作员 | 订单处理、运输跟踪 |
|
||||
| **viewer** | 只读用户 | 数据查看和报表导出 |
|
||||
|
||||
### 权限控制
|
||||
- **路由级权限**:基于用户角色控制页面访问
|
||||
- **组件级权限**:控制按钮、菜单项的显示
|
||||
- **数据级权限**:控制数据的增删改查权限
|
||||
- **API级权限**:后端接口权限验证
|
||||
|
||||
### 默认账号
|
||||
```
|
||||
管理员账号:admin / admin123
|
||||
操作员账号:operator / operator123
|
||||
查看员账号:viewer / viewer123
|
||||
```
|
||||
|
||||
## 📊 功能模块
|
||||
|
||||
### 1. 用户管理模块
|
||||
- **用户列表**:查看所有系统用户
|
||||
- **用户详情**:查看和编辑用户信息
|
||||
- **角色管理**:分配和修改用户角色
|
||||
- **权限配置**:细粒度权限控制
|
||||
|
||||
### 2. 订单管理模块
|
||||
- **订单列表**:所有采购订单管理
|
||||
- **订单详情**:订单完整信息展示
|
||||
- **流程监控**:订单状态流转跟踪
|
||||
- **异常处理**:订单异常情况处理
|
||||
|
||||
### 3. 运输跟踪模块
|
||||
- **运输列表**:所有运输任务
|
||||
- **实时地图**:GPS位置实时显示
|
||||
- **车辆管理**:运输车辆信息
|
||||
- **轨迹回放**:历史运输轨迹
|
||||
|
||||
### 4. 结算管理模块
|
||||
- **结算列表**:所有结算记录
|
||||
- **支付记录**:支付流水查询
|
||||
- **财务报表**:各类财务统计
|
||||
- **对账管理**:自动对账功能
|
||||
|
||||
### 5. 数据分析模块
|
||||
- **业务仪表盘**:核心指标展示
|
||||
- **统计报表**:多维度数据分析
|
||||
- **图表可视化**:数据图表展示
|
||||
- **报表导出**:Excel/PDF导出
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 代码规范
|
||||
- **命名规范**:
|
||||
- 组件名:PascalCase(如 `UserList.vue`)
|
||||
- 文件名:kebab-case(如 `user-list.vue`)
|
||||
- 变量名:camelCase(如 `userName`)
|
||||
- 常量名:UPPER_SNAKE_CASE(如 `API_BASE_URL`)
|
||||
|
||||
- **组件开发**:
|
||||
- 使用 Composition API + `<script setup>`
|
||||
- 统一使用 TypeScript
|
||||
- 组件props必须定义类型
|
||||
- 使用defineEmits定义事件
|
||||
|
||||
- **样式规范**:
|
||||
- 使用SCSS语法
|
||||
- 遵循BEM命名规范
|
||||
- 使用CSS变量定义主题色彩
|
||||
- 响应式设计适配
|
||||
|
||||
### Git提交规范
|
||||
```
|
||||
feat: 新增功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
chore: 其他修改
|
||||
```
|
||||
|
||||
### 目录命名规范
|
||||
- 页面组件放在 `views/` 目录
|
||||
- 公共组件放在 `components/` 目录
|
||||
- API接口放在 `api/` 目录
|
||||
- 工具函数放在 `utils/` 目录
|
||||
- 类型定义放在 `types/` 目录
|
||||
|
||||
## 🌐 API接口
|
||||
|
||||
### 接口基础配置
|
||||
```typescript
|
||||
// 开发环境
|
||||
VITE_API_BASE_URL=http://localhost:3001/api
|
||||
VITE_WS_BASE_URL=ws://localhost:3001
|
||||
|
||||
// 生产环境
|
||||
VITE_API_BASE_URL=https://api.niumall.com/api
|
||||
VITE_WS_BASE_URL=wss://api.niumall.com
|
||||
```
|
||||
|
||||
### 核心接口模块
|
||||
- **认证接口**:登录、登出、刷新token
|
||||
- **用户接口**:用户CRUD、角色管理
|
||||
- **订单接口**:订单管理、状态更新
|
||||
- **运输接口**:GPS跟踪、状态上报
|
||||
- **结算接口**:支付、对账、报表
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
### 断点定义
|
||||
```scss
|
||||
$breakpoints: (
|
||||
xs: 0,
|
||||
sm: 576px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1200px,
|
||||
xxl: 1400px
|
||||
);
|
||||
```
|
||||
|
||||
### 适配策略
|
||||
- **桌面端**:1200px+ 完整功能
|
||||
- **平板端**:768px-1199px 简化布局
|
||||
- **手机端**:<768px 移动优化
|
||||
|
||||
## 🔍 性能优化
|
||||
|
||||
### 构建优化
|
||||
- **代码分割**:路由级别的懒加载
|
||||
- **资源压缩**:Gzip/Brotli压缩
|
||||
- **缓存策略**:浏览器缓存配置
|
||||
- **CDN加速**:静态资源CDN
|
||||
|
||||
### 运行时优化
|
||||
- **虚拟列表**:大数据列表优化
|
||||
- **图片懒加载**:减少初始加载
|
||||
- **防抖节流**:高频操作优化
|
||||
- **内存管理**:避免内存泄漏
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
### 单元测试
|
||||
- **组件测试**:Vue组件功能测试
|
||||
- **工具函数测试**:纯函数测试
|
||||
- **Store测试**:状态管理测试
|
||||
- **覆盖率要求**:>80%
|
||||
|
||||
### 集成测试
|
||||
- **API集成测试**:接口联调测试
|
||||
- **路由测试**:页面跳转测试
|
||||
- **权限测试**:权限控制测试
|
||||
|
||||
### E2E测试
|
||||
- **关键流程测试**:用户核心操作路径
|
||||
- **浏览器兼容性测试**:多浏览器测试
|
||||
- **性能测试**:页面加载性能
|
||||
|
||||
## 🚀 部署指南
|
||||
|
||||
### 构建命令
|
||||
```bash
|
||||
# 开发环境构建
|
||||
npm run build:dev
|
||||
|
||||
# 测试环境构建
|
||||
npm run build:test
|
||||
|
||||
# 生产环境构建
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
### Nginx配置
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name admin.niumall.com;
|
||||
|
||||
location / {
|
||||
root /var/www/admin-system;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend-server;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker部署
|
||||
```dockerfile
|
||||
FROM nginx:alpine
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
## 🛡️ 安全措施
|
||||
|
||||
### 前端安全
|
||||
- **XSS防护**:输入输出转义
|
||||
- **CSRF防护**:Token验证
|
||||
- **Content Security Policy**:内容安全策略
|
||||
- **敏感信息保护**:避免敏感数据泄露
|
||||
|
||||
### 认证安全
|
||||
- **JWT Token**:无状态认证
|
||||
- **Token刷新**:自动刷新机制
|
||||
- **权限验证**:多级权限控制
|
||||
- **会话管理**:安全会话处理
|
||||
|
||||
## 📈 监控和日志
|
||||
|
||||
### 前端监控
|
||||
- **错误监控**:JavaScript错误捕获
|
||||
- **性能监控**:页面加载性能
|
||||
- **用户行为**:操作行为统计
|
||||
- **接口监控**:API调用监控
|
||||
|
||||
### 日志管理
|
||||
- **操作日志**:用户操作记录
|
||||
- **错误日志**:错误信息收集
|
||||
- **性能日志**:性能指标记录
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. **Fork** 仓库
|
||||
2. **创建**特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. **提交**更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. **推送**到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. **开启** Pull Request
|
||||
|
||||
### 开发流程
|
||||
1. 领取任务并创建分支
|
||||
2. 本地开发和测试
|
||||
3. 代码审查和合并
|
||||
4. 部署测试环境
|
||||
5. 上线生产环境
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
- **技术负责人**:张工程师 (zhang@niumall.com)
|
||||
- **产品经理**:李经理 (li@niumall.com)
|
||||
- **技术群组**:微信群-前端技术交流
|
||||
- **问题反馈**:GitHub Issues
|
||||
|
||||
---
|
||||
|
||||
**🎯 让管理更智能,让业务更高效!**
|
||||
14
admin-system/index.html
Normal file
14
admin-system/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>活牛采购智能数字化系统 - 管理后台</title>
|
||||
<meta name="description" content="活牛采购智能数字化系统管理后台,提供用户管理、订单管理、供应商管理等功能。">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
7693
admin-system/package-lock.json
generated
Normal file
7693
admin-system/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
admin-system/package.json
Normal file
69
admin-system/package.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "niumall-admin-system",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "活牛采购智能数字化系统 - 管理后台",
|
||||
"author": "NiuMall Team",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"build:dev": "vue-tsc && vite build --mode development",
|
||||
"build:test": "vue-tsc && vite build --mode test",
|
||||
"build:prod": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "cypress run",
|
||||
"test:e2e:dev": "cypress open",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "^2.3.8",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.4",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/node": "^20.4.5",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^12.17.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.64.1",
|
||||
"typescript": "~5.1.6",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.6",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
4
admin-system/public/logo.svg
Normal file
4
admin-system/public/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" rx="20" fill="#4CAF50" />
|
||||
<text x="50" y="65" font-family="Arial, sans-serif" font-size="50" font-weight="bold" text-anchor="middle" fill="white">N</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
38
admin-system/src/App.vue
Normal file
38
admin-system/src/App.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from './stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 应用初始化时检查登录状态
|
||||
userStore.checkLoginStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 全局样式重置
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
</style>
|
||||
53
admin-system/src/api/order.ts
Normal file
53
admin-system/src/api/order.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse, PaginatedResponse } from '@/utils/request'
|
||||
import type { Order, OrderListParams, OrderCreateForm, OrderUpdateForm } from '@/types/order'
|
||||
|
||||
// 获取订单列表
|
||||
export const getOrderList = (params: OrderListParams): Promise<ApiResponse<PaginatedResponse<Order>>> => {
|
||||
return request.get('/orders', { params })
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
export const getOrderDetail = (id: number): Promise<ApiResponse<Order>> => {
|
||||
return request.get(`/orders/${id}`)
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
export const createOrder = (data: OrderCreateForm): Promise<ApiResponse<Order>> => {
|
||||
return request.post('/orders', data)
|
||||
}
|
||||
|
||||
// 更新订单
|
||||
export const updateOrder = (id: number, data: OrderUpdateForm): Promise<ApiResponse<Order>> => {
|
||||
return request.put(`/orders/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
export const deleteOrder = (id: number): Promise<ApiResponse> => {
|
||||
return request.delete(`/orders/${id}`)
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
export const cancelOrder = (id: number, reason?: string): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/cancel`, { reason })
|
||||
}
|
||||
|
||||
// 确认订单
|
||||
export const confirmOrder = (id: number): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/confirm`)
|
||||
}
|
||||
|
||||
// 订单验收
|
||||
export const acceptOrder = (id: number, data: { actualWeight: number; notes?: string }): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/accept`, data)
|
||||
}
|
||||
|
||||
// 完成订单
|
||||
export const completeOrder = (id: number): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/complete`)
|
||||
}
|
||||
|
||||
// 获取订单统计数据
|
||||
export const getOrderStatistics = (params?: { startDate?: string; endDate?: string }): Promise<ApiResponse> => {
|
||||
return request.get('/orders/statistics', { params })
|
||||
}
|
||||
53
admin-system/src/api/user.ts
Normal file
53
admin-system/src/api/user.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse } from '@/utils/request'
|
||||
import type { User, LoginForm, LoginResponse, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
|
||||
|
||||
// 用户登录
|
||||
export const login = (data: LoginForm): Promise<ApiResponse<LoginResponse>> => {
|
||||
return request.post('/auth/login', data)
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const getUserInfo = (): Promise<ApiResponse<{ user: User; permissions: string[] }>> => {
|
||||
return request.get('/auth/me')
|
||||
}
|
||||
|
||||
// 用户登出
|
||||
export const logout = (): Promise<ApiResponse> => {
|
||||
return request.post('/auth/logout')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
export const getUserList = (params: UserListParams): Promise<ApiResponse> => {
|
||||
return request.get('/users', { params })
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export const createUser = (data: UserCreateForm): Promise<ApiResponse<User>> => {
|
||||
return request.post('/users', data)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
export const updateUser = (id: number, data: UserUpdateForm): Promise<ApiResponse<User>> => {
|
||||
return request.put(`/users/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (id: number): Promise<ApiResponse> => {
|
||||
return request.delete(`/users/${id}`)
|
||||
}
|
||||
|
||||
// 批量删除用户
|
||||
export const batchDeleteUsers = (ids: number[]): Promise<ApiResponse> => {
|
||||
return request.delete('/users/batch', { data: { ids } })
|
||||
}
|
||||
|
||||
// 重置用户密码
|
||||
export const resetUserPassword = (id: number, newPassword: string): Promise<ApiResponse> => {
|
||||
return request.put(`/users/${id}/password`, { password: newPassword })
|
||||
}
|
||||
|
||||
// 启用/禁用用户
|
||||
export const toggleUserStatus = (id: number, status: 'active' | 'inactive' | 'banned'): Promise<ApiResponse> => {
|
||||
return request.put(`/users/${id}/status`, { status })
|
||||
}
|
||||
310
admin-system/src/layouts/index.vue
Normal file
310
admin-system/src/layouts/index.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="isCollapse ? '64px' : '240px'" class="layout-aside">
|
||||
<div class="logo-container">
|
||||
<img v-if="!isCollapse" src="/logo.svg" alt="Logo" class="logo" />
|
||||
<span v-if="!isCollapse" class="logo-text">NiuMall</span>
|
||||
<img v-else src="/logo.svg" alt="Logo" class="logo-mini" />
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:unique-opened="true"
|
||||
class="layout-menu"
|
||||
router
|
||||
>
|
||||
<template v-for="route in menuRoutes" :key="route.path">
|
||||
<el-menu-item :index="route.path" v-if="!route.children">
|
||||
<el-icon><component :is="route.meta?.icon" /></el-icon>
|
||||
<template #title>{{ route.meta?.title }}</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu :index="route.path" v-else>
|
||||
<template #title>
|
||||
<el-icon><component :is="route.meta?.icon" /></el-icon>
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in route.children"
|
||||
:key="child.path"
|
||||
:index="child.path"
|
||||
>
|
||||
{{ child.meta?.title }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<el-container class="layout-main">
|
||||
<!-- 头部 -->
|
||||
<el-header class="layout-header">
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
type="text"
|
||||
:icon="isCollapse ? 'Expand' : 'Fold'"
|
||||
@click="toggleCollapse"
|
||||
/>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item
|
||||
v-for="item in breadcrumbs"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
>
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<div class="user-info">
|
||||
<el-avatar :src="userStore.avatar" :size="32" />
|
||||
<span class="username">{{ userStore.username }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><User /></el-icon>
|
||||
个人中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="settings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
系统设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-main class="layout-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 菜单折叠状态
|
||||
const isCollapse = ref(false)
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
// 菜单路由配置
|
||||
const menuRoutes = computed(() => {
|
||||
return router.getRoutes()
|
||||
.find(r => r.path === '/')
|
||||
?.children?.filter(child => child.meta?.title) || []
|
||||
})
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumbs = computed(() => {
|
||||
const matched = route.matched.filter(item => item.meta?.title)
|
||||
return matched.map(item => ({
|
||||
path: item.path,
|
||||
title: item.meta?.title
|
||||
}))
|
||||
})
|
||||
|
||||
// 切换菜单折叠状态
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
|
||||
// 处理用户下拉菜单命令
|
||||
const handleCommand = async (command: string) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
router.push('/settings')
|
||||
break
|
||||
case 'logout':
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要退出登录吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
await userStore.logoutAction()
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化,在移动端自动收起菜单
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
if (window.innerWidth <= 768) {
|
||||
isCollapse.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.layout-aside {
|
||||
background: #304156;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
.logo-container {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20px;
|
||||
background: #263445;
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.logo-mini {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-menu {
|
||||
border: none;
|
||||
background: #304156;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
color: #bfcbd9;
|
||||
|
||||
&:hover {
|
||||
background: #48576a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #4CAF50;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
color: #bfcbd9;
|
||||
|
||||
&:hover {
|
||||
background: #48576a;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
background: #f0f2f5;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.layout-aside {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
padding: 0 15px;
|
||||
|
||||
.header-left {
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>"
|
||||
25
admin-system/src/main.ts
Normal file
25
admin-system/src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style/index.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册Element Plus图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
132
admin-system/src/router/index.ts
Normal file
132
admin-system/src/router/index.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 布局组件
|
||||
import Layout from '@/layouts/index.vue'
|
||||
|
||||
// 页面组件
|
||||
import Login from '@/views/login/index.vue'
|
||||
import Dashboard from '@/views/dashboard/index.vue'
|
||||
import UserManagement from '@/views/user/index.vue'
|
||||
import OrderManagement from '@/views/order/index.vue'
|
||||
import SupplierManagement from '@/views/supplier/index.vue'
|
||||
import TransportManagement from '@/views/transport/index.vue'
|
||||
import FinanceManagement from '@/views/finance/index.vue'
|
||||
import QualityManagement from '@/views/quality/index.vue'
|
||||
import Settings from '@/views/settings/index.vue'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: {
|
||||
title: '登录',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard,
|
||||
meta: {
|
||||
title: '数据驾驶舱',
|
||||
icon: 'DataAnalysis'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'UserManagement',
|
||||
component: UserManagement,
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'User'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
name: 'OrderManagement',
|
||||
component: OrderManagement,
|
||||
meta: {
|
||||
title: '订单管理',
|
||||
icon: 'ShoppingCart'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'supplier',
|
||||
name: 'SupplierManagement',
|
||||
component: SupplierManagement,
|
||||
meta: {
|
||||
title: '供应商管理',
|
||||
icon: 'OfficeBuilding'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'transport',
|
||||
name: 'TransportManagement',
|
||||
component: TransportManagement,
|
||||
meta: {
|
||||
title: '运输管理',
|
||||
icon: 'Truck'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'quality',
|
||||
name: 'QualityManagement',
|
||||
component: QualityManagement,
|
||||
meta: {
|
||||
title: '质量管理',
|
||||
icon: 'Medal'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'FinanceManagement',
|
||||
component: FinanceManagement,
|
||||
meta: {
|
||||
title: '财务管理',
|
||||
icon: 'Money'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
component: Settings,
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'Setting'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 设置页面标题
|
||||
document.title = to.meta?.title ? `${to.meta.title} - 活牛采购智能数字化系统` : '活牛采购智能数字化系统'
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.path !== '/login' && !userStore.isLoggedIn) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && userStore.isLoggedIn) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
128
admin-system/src/stores/user.ts
Normal file
128
admin-system/src/stores/user.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { User, LoginForm } from '@/types/user'
|
||||
import { login, getUserInfo, logout } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const token = ref<string>(localStorage.getItem('token') || '')
|
||||
const userInfo = ref<User | null>(null)
|
||||
const permissions = ref<string[]>([])
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
const avatar = computed(() => userInfo.value?.avatar || '/default-avatar.png')
|
||||
const username = computed(() => userInfo.value?.username || '')
|
||||
const role = computed(() => userInfo.value?.role || '')
|
||||
|
||||
// 登录
|
||||
const loginAction = async (loginForm: LoginForm) => {
|
||||
try {
|
||||
const response = await login(loginForm)
|
||||
const { access_token, user } = response.data
|
||||
|
||||
token.value = access_token
|
||||
userInfo.value = user
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', access_token)
|
||||
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||
|
||||
ElMessage.success({
|
||||
message: '登录成功',
|
||||
grouping: true,
|
||||
duration: 3000
|
||||
})
|
||||
return Promise.resolve()
|
||||
} catch (error: any) {
|
||||
// 错误信息已在request拦截器中统一处理
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfoAction = async () => {
|
||||
try {
|
||||
const response = await getUserInfo()
|
||||
userInfo.value = response.data.user
|
||||
permissions.value = response.data.permissions || []
|
||||
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.data.user))
|
||||
localStorage.setItem('permissions', JSON.stringify(response.data.permissions))
|
||||
} catch (error) {
|
||||
// 如果获取用户信息失败,清除登录状态
|
||||
logoutAction()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logoutAction = async () => {
|
||||
try {
|
||||
await logout()
|
||||
ElMessage.success({
|
||||
message: '已退出登录',
|
||||
grouping: true,
|
||||
duration: 3000
|
||||
})
|
||||
} catch (error: any) {
|
||||
// 错误信息已在request拦截器中统一处理
|
||||
} finally {
|
||||
// 清除状态和本地存储
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
permissions.value = []
|
||||
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
|
||||
ElMessage.success('已退出登录')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
const checkLoginStatus = () => {
|
||||
const storedToken = localStorage.getItem('token')
|
||||
const storedUserInfo = localStorage.getItem('userInfo')
|
||||
const storedPermissions = localStorage.getItem('permissions')
|
||||
|
||||
if (storedToken && storedUserInfo) {
|
||||
token.value = storedToken
|
||||
userInfo.value = JSON.parse(storedUserInfo)
|
||||
permissions.value = storedPermissions ? JSON.parse(storedPermissions) : []
|
||||
}
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
const hasPermission = (permission: string) => {
|
||||
return permissions.value.includes(permission) || permissions.value.includes('*')
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
const hasRole = (roleName: string) => {
|
||||
return userInfo.value?.role === roleName
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
userInfo,
|
||||
permissions,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
avatar,
|
||||
username,
|
||||
role,
|
||||
|
||||
// 方法
|
||||
loginAction,
|
||||
getUserInfoAction,
|
||||
logoutAction,
|
||||
checkLoginStatus,
|
||||
hasPermission,
|
||||
hasRole
|
||||
}
|
||||
})
|
||||
191
admin-system/src/style/index.scss
Normal file
191
admin-system/src/style/index.scss
Normal file
@@ -0,0 +1,191 @@
|
||||
// 全局样式文件
|
||||
@import './variables.scss';
|
||||
@import './mixins.scss';
|
||||
|
||||
// 全局重置样式
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
// 清除默认样式
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// 通用工具类
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
|
||||
// 间距工具类
|
||||
@for $i from 0 through 40 {
|
||||
.mt-#{$i} { margin-top: #{$i}px; }
|
||||
.mb-#{$i} { margin-bottom: #{$i}px; }
|
||||
.ml-#{$i} { margin-left: #{$i}px; }
|
||||
.mr-#{$i} { margin-right: #{$i}px; }
|
||||
.pt-#{$i} { padding-top: #{$i}px; }
|
||||
.pb-#{$i} { padding-bottom: #{$i}px; }
|
||||
.pl-#{$i} { padding-left: #{$i}px; }
|
||||
.pr-#{$i} { padding-right: #{$i}px; }
|
||||
}
|
||||
|
||||
// Element Plus 自定义样式
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-select .el-input__wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-table__header-wrapper {
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
&:hover > td {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 20px 20px 10px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.mobile-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
width: 90% !important;
|
||||
margin: 5vh auto !important;
|
||||
}
|
||||
}
|
||||
184
admin-system/src/style/mixins.scss
Normal file
184
admin-system/src/style/mixins.scss
Normal file
@@ -0,0 +1,184 @@
|
||||
// SCSS Mixins
|
||||
|
||||
// 清除浮动
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本省略
|
||||
@mixin ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 多行文本省略
|
||||
@mixin ellipsis-multiline($lines: 2) {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// 绝对居中
|
||||
@mixin absolute-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
// Flex 居中
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
@mixin mobile {
|
||||
@media (max-width: 767px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tablet {
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: 1024px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
@mixin card-style {
|
||||
background: white;
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: $box-shadow-light;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
// 按钮基础样式
|
||||
@mixin button-base {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: none;
|
||||
border-radius: $border-radius-base;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: $transition-base;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 主要按钮样式
|
||||
@mixin button-primary {
|
||||
@include button-base;
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $primary-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 次要按钮样式
|
||||
@mixin button-secondary {
|
||||
@include button-base;
|
||||
background-color: transparent;
|
||||
color: $primary-color;
|
||||
border: 1px solid $primary-color;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
@mixin table-style {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-radius: $border-radius-large;
|
||||
overflow: hidden;
|
||||
box-shadow: $box-shadow-light;
|
||||
|
||||
th, td {
|
||||
padding: $spacing-md;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid $border-lighter;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: $background-base;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: $background-light;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
@mixin input-style {
|
||||
width: 100%;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid $border-base;
|
||||
border-radius: $border-radius-base;
|
||||
font-size: $font-size-small;
|
||||
color: $text-primary;
|
||||
background-color: white;
|
||||
transition: $transition-border;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $text-placeholder;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: $background-base;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
@mixin loading-spin {
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
// 渐变背景
|
||||
@mixin gradient-background($start-color, $end-color, $direction: to right) {
|
||||
background: linear-gradient($direction, $start-color, $end-color);
|
||||
}
|
||||
65
admin-system/src/style/variables.scss
Normal file
65
admin-system/src/style/variables.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
// SCSS 变量定义
|
||||
|
||||
// 颜色变量
|
||||
$primary-color: #4CAF50;
|
||||
$primary-light: #81C784;
|
||||
$primary-dark: #388E3C;
|
||||
|
||||
$success-color: #67C23A;
|
||||
$warning-color: #E6A23C;
|
||||
$danger-color: #F56C6C;
|
||||
$info-color: #409EFF;
|
||||
|
||||
$text-primary: #303133;
|
||||
$text-regular: #606266;
|
||||
$text-secondary: #909399;
|
||||
$text-placeholder: #C0C4CC;
|
||||
|
||||
$border-base: #DCDFE6;
|
||||
$border-light: #E4E7ED;
|
||||
$border-lighter: #EBEEF5;
|
||||
$border-extra-light: #F2F6FC;
|
||||
|
||||
$background-base: #F5F7FA;
|
||||
$background-light: #FAFCFF;
|
||||
|
||||
// 尺寸变量
|
||||
$header-height: 60px;
|
||||
$sidebar-width: 240px;
|
||||
$sidebar-collapsed-width: 64px;
|
||||
|
||||
// 字体大小
|
||||
$font-size-extra-small: 12px;
|
||||
$font-size-small: 14px;
|
||||
$font-size-base: 16px;
|
||||
$font-size-medium: 18px;
|
||||
$font-size-large: 20px;
|
||||
$font-size-extra-large: 24px;
|
||||
|
||||
// 间距
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 16px;
|
||||
$spacing-lg: 24px;
|
||||
$spacing-xl: 32px;
|
||||
|
||||
// 圆角
|
||||
$border-radius-small: 4px;
|
||||
$border-radius-base: 6px;
|
||||
$border-radius-large: 8px;
|
||||
$border-radius-round: 20px;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
$box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
|
||||
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 过渡动画
|
||||
$transition-base: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
$transition-fade: opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
$transition-border: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
// Z-index 层级
|
||||
$z-index-normal: 1;
|
||||
$z-index-top: 1000;
|
||||
$z-index-popper: 2000;
|
||||
75
admin-system/src/types/order.ts
Normal file
75
admin-system/src/types/order.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// 订单相关类型定义
|
||||
|
||||
export interface Order {
|
||||
id: number
|
||||
orderNo: string
|
||||
buyerId: number
|
||||
buyerName: string
|
||||
supplierId: number
|
||||
supplierName: string
|
||||
traderId?: number
|
||||
traderName?: string
|
||||
cattleBreed: string
|
||||
cattleCount: number
|
||||
expectedWeight: number
|
||||
actualWeight?: number
|
||||
unitPrice: number
|
||||
totalAmount: number
|
||||
paidAmount: number
|
||||
remainingAmount: number
|
||||
status: OrderStatus
|
||||
deliveryAddress: string
|
||||
expectedDeliveryDate: string
|
||||
actualDeliveryDate?: string
|
||||
notes?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type OrderStatus =
|
||||
| 'pending' // 待确认
|
||||
| 'confirmed' // 已确认
|
||||
| 'preparing' // 准备中
|
||||
| 'shipping' // 运输中
|
||||
| 'delivered' // 已送达
|
||||
| 'accepted' // 已验收
|
||||
| 'completed' // 已完成
|
||||
| 'cancelled' // 已取消
|
||||
| 'refunded' // 已退款
|
||||
|
||||
export interface OrderListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
orderNo?: string
|
||||
buyerId?: number
|
||||
supplierId?: number
|
||||
status?: OrderStatus
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface OrderCreateForm {
|
||||
buyerId: number
|
||||
supplierId: number
|
||||
traderId?: number
|
||||
cattleBreed: string
|
||||
cattleCount: number
|
||||
expectedWeight: number
|
||||
unitPrice: number
|
||||
deliveryAddress: string
|
||||
expectedDeliveryDate: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface OrderUpdateForm {
|
||||
cattleBreed?: string
|
||||
cattleCount?: number
|
||||
expectedWeight?: number
|
||||
actualWeight?: number
|
||||
unitPrice?: number
|
||||
deliveryAddress?: string
|
||||
expectedDeliveryDate?: string
|
||||
actualDeliveryDate?: string
|
||||
notes?: string
|
||||
status?: OrderStatus
|
||||
}
|
||||
52
admin-system/src/types/user.ts
Normal file
52
admin-system/src/types/user.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// 用户相关类型定义
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
avatar?: string
|
||||
role: string
|
||||
status: 'active' | 'inactive' | 'banned'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface LoginForm {
|
||||
username: string
|
||||
password: string
|
||||
captcha?: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
user: User
|
||||
}
|
||||
|
||||
export interface UserListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
role?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface UserCreateForm {
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
password: string
|
||||
role: string
|
||||
status: 'active' | 'inactive'
|
||||
}
|
||||
|
||||
export interface UserUpdateForm {
|
||||
username?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
role?: string
|
||||
status?: 'active' | 'inactive' | 'banned'
|
||||
avatar?: string
|
||||
}
|
||||
147
admin-system/src/utils/request.ts
Normal file
147
admin-system/src/utils/request.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosResponse, AxiosError } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 错误代码映射表
|
||||
const ERROR_CODES: Record<string, string> = {
|
||||
'AUTH_INVALID_CREDENTIALS': '用户名或密码错误',
|
||||
'AUTH_ACCOUNT_LOCKED': '账户已被锁定,请联系管理员',
|
||||
'AUTH_ACCOUNT_DISABLED': '账户已被禁用',
|
||||
'AUTH_TOKEN_EXPIRED': '登录已过期,请重新登录',
|
||||
'AUTH_INVALID_TOKEN': '无效的登录凭证',
|
||||
'NETWORK_ERROR': '网络错误,请检查网络连接',
|
||||
'TIMEOUT_ERROR': '请求超时,请稍后重试',
|
||||
'SERVER_ERROR': '服务器内部错误',
|
||||
'UNKNOWN_ERROR': '未知错误,请联系管理员'
|
||||
}
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加认证token
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
ElMessage.error('请求配置错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data } = response
|
||||
|
||||
// 检查业务状态码
|
||||
if (data.success === false) {
|
||||
const errorCode = data.code || 'UNKNOWN_ERROR'
|
||||
const errorMsg = ERROR_CODES[errorCode] || data.message || '请求失败'
|
||||
ElMessage.error({
|
||||
message: errorMsg,
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
return Promise.reject(new Error(errorMsg))
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// 处理HTTP错误状态码
|
||||
const { response } = error
|
||||
|
||||
if (response) {
|
||||
const errorCode = (response.data as any)?.code
|
||||
const errorMsg = errorCode ? ERROR_CODES[errorCode] : undefined
|
||||
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
ElMessage.error({
|
||||
message: errorMsg || '未授权,请重新登录',
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
// 清除登录状态并跳转到登录页
|
||||
const userStore = useUserStore()
|
||||
userStore.logoutAction()
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error({
|
||||
message: errorMsg || '访问被拒绝,权限不足',
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error({
|
||||
message: errorMsg || '请求的资源不存在',
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error({
|
||||
message: errorMsg || '服务器内部错误',
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
break
|
||||
default:
|
||||
ElMessage.error({
|
||||
message: errorMsg || `请求失败: ${response.status}`,
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
ElMessage.error({
|
||||
message: '请求超时,请稍后重试',
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
} else {
|
||||
ElMessage.error({
|
||||
message: '网络错误,请检查网络连接',
|
||||
grouping: true,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
|
||||
// 通用API响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
data: T
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
// 分页响应类型
|
||||
export interface PaginatedResponse<T = any> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
416
admin-system/src/views/dashboard/index.vue
Normal file
416
admin-system/src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-cards">
|
||||
<el-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.key">
|
||||
<el-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" :style="{ backgroundColor: stat.color }">
|
||||
<el-icon :size="24"><component :is="stat.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
|
||||
<el-icon><component :is="stat.trend > 0 ? 'TrendCharts' : 'Bottom'" /></el-icon>
|
||||
{{ Math.abs(stat.trend) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<el-col :lg="16">
|
||||
<el-card title="订单趋势" class="chart-card">
|
||||
<div ref="orderTrendChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="8">
|
||||
<el-card title="订单状态分布" class="chart-card">
|
||||
<div ref="orderStatusChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格区域 -->
|
||||
<el-row :gutter="20" class="tables-section">
|
||||
<el-col :lg="12">
|
||||
<el-card title="最近订单" class="table-card">
|
||||
<el-table :data="recentOrders" size="small">
|
||||
<el-table-column prop="orderNo" label="订单号" width="120" />
|
||||
<el-table-column prop="supplierName" label="供应商" />
|
||||
<el-table-column prop="cattleCount" label="数量" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="12">
|
||||
<el-card title="供应商排行" class="table-card">
|
||||
<el-table :data="topSuppliers" size="small">
|
||||
<el-table-column type="index" label="排名" width="60" />
|
||||
<el-table-column prop="name" label="供应商名称" />
|
||||
<el-table-column prop="orderCount" label="订单数" width="80" />
|
||||
<el-table-column prop="totalAmount" label="总金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.totalAmount.toLocaleString() }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref([
|
||||
{
|
||||
key: 'totalOrders',
|
||||
label: '总订单数',
|
||||
value: '1,234',
|
||||
icon: 'ShoppingCart',
|
||||
color: '#409EFF',
|
||||
trend: 12.5
|
||||
},
|
||||
{
|
||||
key: 'completedOrders',
|
||||
label: '已完成订单',
|
||||
value: '856',
|
||||
icon: 'CircleCheck',
|
||||
color: '#67C23A',
|
||||
trend: 8.3
|
||||
},
|
||||
{
|
||||
key: 'totalAmount',
|
||||
label: '总交易额',
|
||||
value: '¥2.34M',
|
||||
icon: 'Money',
|
||||
color: '#E6A23C',
|
||||
trend: 15.7
|
||||
},
|
||||
{
|
||||
key: 'activeSuppliers',
|
||||
label: '活跃供应商',
|
||||
value: '168',
|
||||
icon: 'OfficeBuilding',
|
||||
color: '#F56C6C',
|
||||
trend: -2.1
|
||||
}
|
||||
])
|
||||
|
||||
// 最近订单
|
||||
const recentOrders = ref([
|
||||
{
|
||||
orderNo: 'ORD20240101001',
|
||||
supplierName: '山东畜牧合作社',
|
||||
cattleCount: 50,
|
||||
status: 'shipping'
|
||||
},
|
||||
{
|
||||
orderNo: 'ORD20240101002',
|
||||
supplierName: '河北养殖基地',
|
||||
cattleCount: 30,
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
orderNo: 'ORD20240101003',
|
||||
supplierName: '内蒙古牧场',
|
||||
cattleCount: 80,
|
||||
status: 'pending'
|
||||
}
|
||||
])
|
||||
|
||||
// 供应商排行
|
||||
const topSuppliers = ref([
|
||||
{
|
||||
name: '山东畜牧合作社',
|
||||
orderCount: 45,
|
||||
totalAmount: 1250000
|
||||
},
|
||||
{
|
||||
name: '河北养殖基地',
|
||||
orderCount: 38,
|
||||
totalAmount: 980000
|
||||
},
|
||||
{
|
||||
name: '内蒙古牧场',
|
||||
orderCount: 32,
|
||||
totalAmount: 850000
|
||||
}
|
||||
])
|
||||
|
||||
// 图表元素引用
|
||||
const orderTrendChart = ref<HTMLElement>()
|
||||
const orderStatusChart = ref<HTMLElement>()
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
confirmed: 'info',
|
||||
shipping: 'primary',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待确认',
|
||||
confirmed: '已确认',
|
||||
shipping: '运输中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 初始化订单趋势图表
|
||||
const initOrderTrendChart = () => {
|
||||
if (!orderTrendChart.value) return
|
||||
|
||||
const chart = echarts.init(orderTrendChart.value)
|
||||
const option = {
|
||||
title: {
|
||||
text: '近30天订单趋势',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['订单数量', '完成订单']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'line',
|
||||
data: [120, 132, 101, 134, 90, 230, 210, 180, 160, 190, 200, 220],
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#409EFF'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '完成订单',
|
||||
type: 'line',
|
||||
data: [100, 120, 90, 120, 80, 200, 190, 160, 140, 170, 180, 200],
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#67C23A'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化订单状态图表
|
||||
const initOrderStatusChart = () => {
|
||||
if (!orderStatusChart.value) return
|
||||
|
||||
const chart = echarts.init(orderStatusChart.value)
|
||||
const option = {
|
||||
title: {
|
||||
text: '订单状态分布',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单状态',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
data: [
|
||||
{ value: 335, name: '已完成', itemStyle: { color: '#67C23A' } },
|
||||
{ value: 210, name: '运输中', itemStyle: { color: '#409EFF' } },
|
||||
{ value: 180, name: '已确认', itemStyle: { color: '#E6A23C' } },
|
||||
{ value: 130, name: '待确认', itemStyle: { color: '#F56C6C' } },
|
||||
{ value: 45, name: '已取消', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initOrderTrendChart()
|
||||
initOrderStatusChart()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
.stats-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
&.up {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #F56C6C;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.tables-section {
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-card__header) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 15px 20px;
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
.stats-cards {
|
||||
.stat-card {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.stat-content {
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/finance/index.vue
Normal file
34
admin-system/src/views/finance/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="finance-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>财务管理</h2>
|
||||
<p>订单结算、支付管理和财务报表</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="财务管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 财务管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.finance-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
237
admin-system/src/views/login/index.vue
Normal file
237
admin-system/src/views/login/index.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<img src="/logo.svg" alt="Logo" class="logo-img" />
|
||||
<h1 class="title">活牛采购智能数字化系统</h1>
|
||||
</div>
|
||||
<p class="subtitle">管理后台</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
prefix-icon="User"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
prefix-icon="Lock"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
class="login-btn"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p class="demo-account">
|
||||
<strong>演示账号:</strong>
|
||||
</p>
|
||||
<div class="demo-list">
|
||||
<el-tag @click="setDemoAccount('admin', 'admin123')">管理员: admin / admin123</el-tag>
|
||||
<el-tag type="success" @click="setDemoAccount('buyer', 'buyer123')">采购人: buyer / buyer123</el-tag>
|
||||
<el-tag type="warning" @click="setDemoAccount('trader', 'trader123')">贸易商: trader / trader123</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { LoginForm } from '@/types/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const loginForm = reactive<LoginForm>({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
await loginFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
await userStore.loginAction(loginForm)
|
||||
|
||||
ElMessage.success('登录成功!')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置演示账号
|
||||
const setDemoAccount = (username: string, password: string) => {
|
||||
loginForm.username = username
|
||||
loginForm.password = password
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
padding: 40px 40px 30px;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||
color: white;
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.logo-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 40px;
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
padding: 0 40px 40px;
|
||||
text-align: center;
|
||||
|
||||
.demo-account {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.el-tag {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding: 30px 30px 20px;
|
||||
|
||||
.logo .title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
padding: 0 30px 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/order/index.vue
Normal file
34
admin-system/src/views/order/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="order-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>订单管理</h2>
|
||||
<p>管理活牛采购订单的全生命周期流程</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="订单管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 订单管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/quality/index.vue
Normal file
34
admin-system/src/views/quality/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="quality-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>质量管理</h2>
|
||||
<p>牛只质量检验、检疫证明管理和质量追溯</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="质量管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 质量管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quality-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/settings/index.vue
Normal file
34
admin-system/src/views/settings/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>系统设置</h2>
|
||||
<p>系统配置、权限管理和参数设置</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="系统设置功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 系统设置页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/supplier/index.vue
Normal file
34
admin-system/src/views/supplier/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="supplier-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>供应商管理</h2>
|
||||
<p>管理供应商信息、资质认证和绩效评估</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="供应商管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 供应商管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.supplier-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/transport/index.vue
Normal file
34
admin-system/src/views/transport/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="transport-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>运输管理</h2>
|
||||
<p>实时跟踪运输过程,监控车辆位置和牛只状态</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="运输管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 运输管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.transport-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
517
admin-system/src/views/user/index.vue
Normal file
517
admin-system/src/views/user/index.vue
Normal file
@@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<el-card class="search-card">
|
||||
<div class="search-form">
|
||||
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.keyword" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="采购人" value="buyer" />
|
||||
<el-option label="贸易商" value="trader" />
|
||||
<el-option label="供应商" value="supplier" />
|
||||
<el-option label="司机" value="driver" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="正常" value="active" />
|
||||
<el-option label="禁用" value="inactive" />
|
||||
<el-option label="封禁" value="banned" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户列表</span>
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column prop="role" label="角色">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRoleType(row.role)">
|
||||
{{ getRoleText(row.role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户表单弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="采购人" value="buyer" />
|
||||
<el-option label="贸易商" value="trader" />
|
||||
<el-option label="供应商" value="supplier" />
|
||||
<el-option label="司机" value="driver" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="正常" value="active" />
|
||||
<el-option label="禁用" value="inactive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.id" label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { User, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive<UserListParams>({
|
||||
keyword: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<User[]>([])
|
||||
const selectedUsers = ref<User[]>([])
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<UserCreateForm & { id?: number }>({
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
role: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = ref('新增用户')
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockUsers: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'buyer01',
|
||||
email: 'buyer01@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'buyer',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'supplier01',
|
||||
email: 'supplier01@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'supplier',
|
||||
status: 'inactive',
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
updatedAt: '2024-01-03T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取用户列表
|
||||
const getUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 简单的过滤逻辑
|
||||
let filteredUsers = [...mockUsers]
|
||||
|
||||
if (searchForm.keyword) {
|
||||
filteredUsers = filteredUsers.filter(user =>
|
||||
user.username.includes(searchForm.keyword!) ||
|
||||
user.email.includes(searchForm.keyword!)
|
||||
)
|
||||
}
|
||||
|
||||
if (searchForm.role) {
|
||||
filteredUsers = filteredUsers.filter(user => user.role === searchForm.role)
|
||||
}
|
||||
|
||||
if (searchForm.status) {
|
||||
filteredUsers = filteredUsers.filter(user => user.status === searchForm.status)
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const start = (pagination.page - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
|
||||
tableData.value = filteredUsers.slice(start, end)
|
||||
pagination.total = filteredUsers.length
|
||||
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增用户'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (row: User) => {
|
||||
dialogTitle.value = '编辑用户'
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
email: row.email,
|
||||
phone: row.phone,
|
||||
role: row.role,
|
||||
status: row.status
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (row: User) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户 "${row.username}" 吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 模拟删除
|
||||
ElMessage.success('删除成功')
|
||||
getUserList()
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = async (row: User) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要重置用户 "${row.username}" 的密码吗?`,
|
||||
'重置密码确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 模拟重置密码
|
||||
ElMessage.success('密码重置成功,新密码为:123456')
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 表格选择变化
|
||||
const handleSelectionChange = (selection: User[]) => {
|
||||
selectedUsers.value = selection
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(form.id ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
getUserList()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
role: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取角色类型
|
||||
const getRoleType = (role: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
admin: 'danger',
|
||||
buyer: 'primary',
|
||||
trader: 'success',
|
||||
supplier: 'warning',
|
||||
driver: 'info'
|
||||
}
|
||||
return roleMap[role] || 'info'
|
||||
}
|
||||
|
||||
// 获取角色文本
|
||||
const getRoleText = (role: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
admin: '管理员',
|
||||
buyer: '采购人',
|
||||
trader: '贸易商',
|
||||
supplier: '供应商',
|
||||
driver: '司机'
|
||||
}
|
||||
return roleMap[role] || role
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: 'success',
|
||||
inactive: 'warning',
|
||||
banned: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: '正常',
|
||||
inactive: '禁用',
|
||||
banned: '封禁'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-management {
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-form {
|
||||
.demo-form-inline {
|
||||
.el-form-item {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.user-management {
|
||||
.search-form {
|
||||
.demo-form-inline {
|
||||
.el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
.el-table-column {
|
||||
&:nth-child(n+4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
admin-system/tsconfig.json
Normal file
53
admin-system/tsconfig.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"auto-imports.d.ts",
|
||||
"components.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/*",
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/views/*": ["./src/views/*"],
|
||||
"@/utils/*": ["./src/utils/*"],
|
||||
"@/api/*": ["./src/api/*"],
|
||||
"@/stores/*": ["./src/stores/*"],
|
||||
"@/router/*": ["./src/router/*"],
|
||||
"@/types/*": ["./src/types/*"],
|
||||
"@/assets/*": ["./src/assets/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"useDefineForClassFields": true,
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"types": ["vite/client", "element-plus/global", "node"]
|
||||
}
|
||||
}
|
||||
103
admin-system/vite.config.ts
Normal file
103
admin-system/vite.config.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: true,
|
||||
eslintrc: {
|
||||
enabled: true
|
||||
}
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: true
|
||||
})
|
||||
],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@/components': resolve(__dirname, 'src/components'),
|
||||
'@/views': resolve(__dirname, 'src/views'),
|
||||
'@/utils': resolve(__dirname, 'src/utils'),
|
||||
'@/api': resolve(__dirname, 'src/api'),
|
||||
'@/stores': resolve(__dirname, 'src/stores'),
|
||||
'@/router': resolve(__dirname, 'src/router'),
|
||||
'@/types': resolve(__dirname, 'src/types'),
|
||||
'@/assets': resolve(__dirname, 'src/assets')
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_BASE_URL || 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
build: {
|
||||
target: 'es2015',
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
sourcemap: mode === 'development',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
chunkFileNames: 'js/[name]-[hash].js',
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name?.endsWith('.css')) {
|
||||
return 'css/[name]-[hash].css'
|
||||
}
|
||||
return 'assets/[name]-[hash].[ext]'
|
||||
},
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'element-plus': ['element-plus', '@element-plus/icons-vue'],
|
||||
'utils': ['axios', 'nprogress']
|
||||
}
|
||||
}
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: mode === 'production',
|
||||
drop_debugger: mode === 'production'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'vue-router', 'pinia', 'element-plus', 'axios']
|
||||
},
|
||||
|
||||
define: {
|
||||
__VUE_OPTIONS_API__: false,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@import "@/style/variables.scss"; @import "@/style/mixins.scss";`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
23
backend/.env
Normal file
23
backend/.env
Normal file
@@ -0,0 +1,23 @@
|
||||
# 数据库配置
|
||||
DB_HOST=129.211.213.226
|
||||
DB_PORT=9527
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=aiotAiot123!
|
||||
DB_NAME=jiebandata
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=niumall_jwt_secret_key_2024
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# 应用配置
|
||||
NODE_ENV=development
|
||||
PORT=3002
|
||||
API_PREFIX=/api
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
@@ -1,30 +1,104 @@
|
||||
# Backend 后端服务
|
||||
# Backend - 活牛采购智能数字化系统后端服务
|
||||
|
||||
## 技术栈
|
||||
- Node.js/Express/Nest.js
|
||||
- 数据库:MySQL/MongoDB/Redis
|
||||
- 消息队列:RabbitMQ/Kafka
|
||||
- 缓存:Redis
|
||||
- 文件存储:MinIO/阿里云OSS
|
||||
## 📋 项目概述
|
||||
|
||||
活牛采购智能数字化系统后端服务采用Node.js + Express框架构建,为前端应用、管理后台和小程序提供统一的API接口服务。系统采用微服务架构设计,支持高并发、高可用的业务处理。
|
||||
|
||||
**核心特性:**
|
||||
- 🛠️ **微服务架构**:模块化服务设计,易于维护和扩展
|
||||
- 🔐 **统一认证**:JWT + RBAC权限控制系统
|
||||
- 📊 **实时数据**:WebSocket实时数据推送
|
||||
- 📹 **文件管理**:支持大文件上传和视频处理
|
||||
- 💰 **支付集成**:支持多种支付方式
|
||||
- 📈 **监控日志**:完善的日志和监控体系
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
| 类别 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| **运行时** | Node.js | ^18.17.0 | 服务器运行环境 |
|
||||
| **Web框架** | Express.js | ^4.18.0 | 轻量级Web框架 |
|
||||
| **数据库ORM** | Sequelize | ^6.32.0 | 关系型数据库ORM |
|
||||
| **数据库** | MySQL | ^8.0 | 主数据库 |
|
||||
| **缓存** | Redis | ^7.0 | 内存缓存和会话存储 |
|
||||
| **认证** | jsonwebtoken | ^9.0.0 | JWT认证 |
|
||||
| **参数验证** | joi | ^17.9.0 | 请求参数验证 |
|
||||
| **文件上传** | multer | ^1.4.5 | 文件上传中间件 |
|
||||
| **日志** | winston | ^3.10.0 | 日志管理 |
|
||||
| **实时通信** | Socket.io | ^4.7.0 | WebSocket实时通信 |
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── services/ # 服务层
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── config/ # 配置文件
|
||||
├── tests/ # 测试文件
|
||||
├── package.json
|
||||
└── README.md
|
||||
├── src/ # 源代码目录
|
||||
│ ├── app.js # 应用入口文件
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── controllers/ # 控制器层
|
||||
│ ├── services/ # 服务层
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── routes/ # 路由定义
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── validators/ # 参数验证器
|
||||
│ ├── jobs/ # 后台任务
|
||||
│ └── database/ # 数据库相关
|
||||
├── tests/ # 测试文件
|
||||
├── docs/ # 文档目录
|
||||
├── uploads/ # 上传文件目录
|
||||
├── logs/ # 日志目录
|
||||
└── package.json # 项目依赖
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
1. 使用ES6+语法
|
||||
2. 遵循RESTful API设计规范
|
||||
3. 错误处理统一格式
|
||||
4. 日志记录规范
|
||||
5. 安全防护措施
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 18.0.0
|
||||
- MySQL >= 8.0
|
||||
- Redis >= 7.0
|
||||
- npm >= 8.0.0
|
||||
|
||||
### 数据库配置
|
||||
```bash
|
||||
# 数据库连接信息
|
||||
主机: 129.211.213.226
|
||||
端口: 9527
|
||||
用户名: root
|
||||
密码: aiotAiot123!
|
||||
数据库: jiebandata
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 环境配置
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.example .env.development
|
||||
|
||||
# 编辑配置文件
|
||||
vim .env.development
|
||||
```
|
||||
|
||||
### 数据库初始化
|
||||
```bash
|
||||
# 执行数据库迁移
|
||||
npm run db:migrate
|
||||
|
||||
# 执行数据填充
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
# 开发环境
|
||||
npm run dev
|
||||
|
||||
# 生产环境
|
||||
npm start
|
||||
```
|
||||
|
||||
服务将运行在 http://localhost:3001
|
||||
101
backend/app.js
Normal file
101
backend/app.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const helmet = require('helmet')
|
||||
const morgan = require('morgan')
|
||||
const rateLimit = require('express-rate-limit')
|
||||
const compression = require('compression')
|
||||
require('dotenv').config()
|
||||
|
||||
// 数据库连接
|
||||
const { testConnection, syncModels } = require('./models')
|
||||
|
||||
const app = express()
|
||||
|
||||
// 中间件配置
|
||||
app.use(helmet()) // 安全头
|
||||
app.use(cors()) // 跨域
|
||||
app.use(compression()) // 压缩
|
||||
app.use(morgan('combined')) // 日志
|
||||
app.use(express.json({ limit: '10mb' }))
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
||||
|
||||
// 限流
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 分钟
|
||||
max: 100, // 限制每个IP最多100个请求
|
||||
message: {
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后重试'
|
||||
}
|
||||
})
|
||||
app.use('/api', limiter)
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '服务运行正常',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
})
|
||||
})
|
||||
|
||||
// API 路由
|
||||
app.use('/api/auth', require('./routes/auth'))
|
||||
app.use('/api/users', require('./routes/users'))
|
||||
app.use('/api/orders', require('./routes/orders'))
|
||||
app.use('/api/suppliers', require('./routes/suppliers'))
|
||||
app.use('/api/transport', require('./routes/transport'))
|
||||
app.use('/api/finance', require('./routes/finance'))
|
||||
app.use('/api/quality', require('./routes/quality'))
|
||||
|
||||
// 404 处理
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '接口不存在',
|
||||
path: req.path
|
||||
})
|
||||
})
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err)
|
||||
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
message: err.message || '服务器内部错误',
|
||||
timestamp: new Date().toISOString(),
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
})
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 测试数据库连接
|
||||
const dbConnected = await testConnection();
|
||||
if (!dbConnected) {
|
||||
console.error('❌ 数据库连接失败,服务器启动终止');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 同步数据库模型
|
||||
await syncModels();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 服务器启动成功`)
|
||||
console.log(`📱 运行环境: ${process.env.NODE_ENV || 'development'}`)
|
||||
console.log(`🌐 访问地址: http://localhost:${PORT}`)
|
||||
console.log(`📊 健康检查: http://localhost:${PORT}/health`)
|
||||
console.log(`📚 API文档: http://localhost:${PORT}/api/docs`)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
startServer()
|
||||
58
backend/config/database.js
Normal file
58
backend/config/database.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// 数据库配置文件
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
development: {
|
||||
username: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || 'aiotAiot123!',
|
||||
database: process.env.DB_NAME || 'jiebandata',
|
||||
host: process.env.DB_HOST || '129.211.213.226',
|
||||
port: process.env.DB_PORT || 9527,
|
||||
dialect: 'mysql',
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4',
|
||||
dateStrings: true,
|
||||
typeCast: true
|
||||
},
|
||||
timezone: '+08:00',
|
||||
logging: console.log,
|
||||
pool: {
|
||||
max: 20,
|
||||
min: 0,
|
||||
acquire: 60000,
|
||||
idle: 10000
|
||||
}
|
||||
},
|
||||
test: {
|
||||
username: process.env.TEST_DB_USERNAME || 'root',
|
||||
password: process.env.TEST_DB_PASSWORD || 'aiotAiot123!',
|
||||
database: process.env.TEST_DB_NAME || 'jiebandata_test',
|
||||
host: process.env.TEST_DB_HOST || '129.211.213.226',
|
||||
port: process.env.TEST_DB_PORT || 9527,
|
||||
dialect: 'mysql',
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4'
|
||||
},
|
||||
timezone: '+08:00',
|
||||
logging: false
|
||||
},
|
||||
production: {
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: 'mysql',
|
||||
dialectOptions: {
|
||||
charset: 'utf8mb4'
|
||||
},
|
||||
timezone: '+08:00',
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 50,
|
||||
min: 5,
|
||||
acquire: 60000,
|
||||
idle: 10000
|
||||
}
|
||||
}
|
||||
};
|
||||
140
backend/models/index.js
Normal file
140
backend/models/index.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// 数据库连接和模型定义
|
||||
const { Sequelize } = require('sequelize');
|
||||
const config = require('../config/database.js');
|
||||
|
||||
// 根据环境变量选择配置
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const dbConfig = config[env];
|
||||
|
||||
// 创建Sequelize实例
|
||||
const sequelize = new Sequelize(
|
||||
dbConfig.database,
|
||||
dbConfig.username,
|
||||
dbConfig.password,
|
||||
{
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
dialect: dbConfig.dialect,
|
||||
dialectOptions: dbConfig.dialectOptions,
|
||||
timezone: dbConfig.timezone,
|
||||
logging: dbConfig.logging,
|
||||
pool: dbConfig.pool
|
||||
}
|
||||
);
|
||||
|
||||
// 测试数据库连接
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义模型
|
||||
const models = {
|
||||
sequelize,
|
||||
Sequelize,
|
||||
|
||||
// 用户模型(匹配实际数据库结构)
|
||||
User: sequelize.define('User', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
openid: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
nickname: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
avatar: {
|
||||
type: Sequelize.STRING(255)
|
||||
},
|
||||
gender: {
|
||||
type: Sequelize.ENUM('male', 'female', 'other')
|
||||
},
|
||||
birthday: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
phone: {
|
||||
type: Sequelize.STRING(20),
|
||||
unique: true
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING(100),
|
||||
unique: true
|
||||
},
|
||||
uuid: {
|
||||
type: Sequelize.STRING(36),
|
||||
unique: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}),
|
||||
|
||||
// 为了兼容现有API,创建一个简化版的用户模型
|
||||
ApiUser: sequelize.define('ApiUser', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password_hash: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
phone: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING(100)
|
||||
},
|
||||
user_type: {
|
||||
type: Sequelize.ENUM('client', 'supplier', 'driver', 'staff', 'admin'),
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.ENUM('active', 'inactive', 'locked'),
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
tableName: 'api_users',
|
||||
timestamps: true
|
||||
})
|
||||
};
|
||||
|
||||
// 同步数据库模型
|
||||
const syncModels = async () => {
|
||||
try {
|
||||
// 只同步API用户表(如果不存在则创建)
|
||||
await models.ApiUser.sync({ alter: true });
|
||||
console.log('✅ API用户表同步成功');
|
||||
console.log('✅ 数据库模型同步完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库模型同步失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...models,
|
||||
testConnection,
|
||||
syncModels
|
||||
};
|
||||
172
backend/routes/auth.js
Normal file
172
backend/routes/auth.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const express = require('express')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 引入数据库模型
|
||||
const { ApiUser } = require('../models')
|
||||
|
||||
// 登录参数验证
|
||||
const loginSchema = Joi.object({
|
||||
username: Joi.string().min(2).max(50).required(),
|
||||
password: Joi.string().min(6).max(100).required()
|
||||
})
|
||||
|
||||
// 生成JWT token
|
||||
const generateToken = (user) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.user_type
|
||||
},
|
||||
process.env.JWT_SECRET || 'niumall-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
)
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = loginSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
const { username, password } = value
|
||||
|
||||
// 查找用户
|
||||
const user = await ApiUser.findOne({
|
||||
where: {
|
||||
[require('sequelize').Op.or]: [
|
||||
{ username: username },
|
||||
{ phone: username },
|
||||
{ email: username }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password_hash)
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.status !== 'active') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被禁用,请联系管理员'
|
||||
})
|
||||
}
|
||||
|
||||
// 生成token
|
||||
const token = generateToken(user)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
access_token: token,
|
||||
token_type: 'Bearer',
|
||||
expires_in: 86400, // 24小时
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.user_type,
|
||||
status: user.status
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '登录失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前用户信息
|
||||
router.get('/me', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const user = await ApiUser.findByPk(req.user.id)
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.user_type,
|
||||
status: user.status
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户信息失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 用户登出
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
// 在实际项目中,可以将token加入黑名单
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登出成功'
|
||||
})
|
||||
})
|
||||
|
||||
// JWT token验证中间件
|
||||
function authenticateToken(req, res, next) {
|
||||
const authHeader = req.headers['authorization']
|
||||
const token = authHeader && authHeader.split(' ')[1]
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '访问令牌缺失'
|
||||
})
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'niumall-secret-key', (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '访问令牌无效或已过期'
|
||||
})
|
||||
}
|
||||
req.user = user
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = router
|
||||
490
backend/routes/finance.js
Normal file
490
backend/routes/finance.js
Normal file
@@ -0,0 +1,490 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟财务数据
|
||||
let settlements = [
|
||||
{
|
||||
id: 1,
|
||||
orderId: 1,
|
||||
settlementCode: 'SET001',
|
||||
supplierName: '山东优质牲畜合作社',
|
||||
buyerName: '北京肉类加工有限公司',
|
||||
cattleCount: 50,
|
||||
unitPrice: 25000,
|
||||
totalAmount: 1250000,
|
||||
paymentMethod: 'bank_transfer',
|
||||
paymentStatus: 'paid',
|
||||
settlementDate: '2024-01-20',
|
||||
paymentDate: '2024-01-22',
|
||||
invoiceNumber: 'INV001',
|
||||
invoiceStatus: 'issued',
|
||||
taxAmount: 125000,
|
||||
actualPayment: 1125000,
|
||||
bankAccount: '1234567890123456789',
|
||||
bankName: '中国农业银行',
|
||||
createdAt: new Date('2024-01-20'),
|
||||
updatedAt: new Date('2024-01-22')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderId: 2,
|
||||
settlementCode: 'SET002',
|
||||
supplierName: '内蒙古草原牲畜有限公司',
|
||||
buyerName: '天津屠宰加工厂',
|
||||
cattleCount: 80,
|
||||
unitPrice: 24000,
|
||||
totalAmount: 1920000,
|
||||
paymentMethod: 'cash',
|
||||
paymentStatus: 'pending',
|
||||
settlementDate: '2024-01-25',
|
||||
paymentDate: null,
|
||||
invoiceNumber: 'INV002',
|
||||
invoiceStatus: 'pending',
|
||||
taxAmount: 192000,
|
||||
actualPayment: 1728000,
|
||||
bankAccount: '9876543210987654321',
|
||||
bankName: '中国建设银行',
|
||||
createdAt: new Date('2024-01-25'),
|
||||
updatedAt: new Date('2024-01-25')
|
||||
}
|
||||
];
|
||||
|
||||
let payments = [
|
||||
{
|
||||
id: 1,
|
||||
settlementId: 1,
|
||||
paymentCode: 'PAY001',
|
||||
amount: 1125000,
|
||||
paymentMethod: 'bank_transfer',
|
||||
status: 'success',
|
||||
transactionId: 'TXN20240122001',
|
||||
paidAt: '2024-01-22T10:30:00Z',
|
||||
createdAt: new Date('2024-01-22T10:30:00Z')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const settlementCreateSchema = Joi.object({
|
||||
orderId: Joi.number().integer().required(),
|
||||
cattleCount: Joi.number().integer().min(1).required(),
|
||||
unitPrice: Joi.number().min(0).required(),
|
||||
paymentMethod: Joi.string().valid('bank_transfer', 'cash', 'check', 'online').required(),
|
||||
settlementDate: Joi.date().iso().required(),
|
||||
invoiceNumber: Joi.string().min(3).max(50)
|
||||
});
|
||||
|
||||
const paymentCreateSchema = Joi.object({
|
||||
settlementId: Joi.number().integer().required(),
|
||||
amount: Joi.number().min(0).required(),
|
||||
paymentMethod: Joi.string().valid('bank_transfer', 'cash', 'check', 'online').required(),
|
||||
transactionId: Joi.string().max(100)
|
||||
});
|
||||
|
||||
// 获取结算列表
|
||||
router.get('/settlements', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
paymentStatus,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
let filteredSettlements = [...settlements];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement =>
|
||||
settlement.settlementCode.includes(keyword) ||
|
||||
settlement.supplierName.includes(keyword) ||
|
||||
settlement.buyerName.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 支付状态筛选
|
||||
if (paymentStatus) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement => settlement.paymentStatus === paymentStatus);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement =>
|
||||
new Date(settlement.settlementDate) >= new Date(startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredSettlements = filteredSettlements.filter(settlement =>
|
||||
new Date(settlement.settlementDate) <= new Date(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedSettlements = filteredSettlements.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedSettlements,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredSettlements.length,
|
||||
totalPages: Math.ceil(filteredSettlements.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取结算列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取结算详情
|
||||
router.get('/settlements/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const settlement = settlements.find(s => s.id === parseInt(id));
|
||||
|
||||
if (!settlement) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '结算记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取相关支付记录
|
||||
const relatedPayments = payments.filter(p => p.settlementId === settlement.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...settlement,
|
||||
payments: relatedPayments
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取结算详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建结算记录
|
||||
router.post('/settlements', (req, res) => {
|
||||
try {
|
||||
const { error, value } = settlementCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const settlementCode = `SET${String(Date.now()).slice(-6)}`;
|
||||
const totalAmount = value.cattleCount * value.unitPrice;
|
||||
const taxAmount = totalAmount * 0.1; // 假设税率10%
|
||||
const actualPayment = totalAmount - taxAmount;
|
||||
|
||||
const newSettlement = {
|
||||
id: Math.max(...settlements.map(s => s.id)) + 1,
|
||||
...value,
|
||||
settlementCode,
|
||||
totalAmount,
|
||||
taxAmount,
|
||||
actualPayment,
|
||||
paymentStatus: 'pending',
|
||||
paymentDate: null,
|
||||
invoiceStatus: 'pending',
|
||||
supplierName: '供应商名称', // 实际应从订单获取
|
||||
buyerName: '采购商名称', // 实际应从订单获取
|
||||
bankAccount: '',
|
||||
bankName: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
settlements.push(newSettlement);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '结算记录创建成功',
|
||||
data: newSettlement
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建结算记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新结算状态
|
||||
router.put('/settlements/:id/status', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { paymentStatus, invoiceStatus } = req.body;
|
||||
|
||||
const settlementIndex = settlements.findIndex(s => s.id === parseInt(id));
|
||||
if (settlementIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '结算记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (paymentStatus) {
|
||||
settlements[settlementIndex].paymentStatus = paymentStatus;
|
||||
if (paymentStatus === 'paid') {
|
||||
settlements[settlementIndex].paymentDate = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (invoiceStatus) {
|
||||
settlements[settlementIndex].invoiceStatus = invoiceStatus;
|
||||
}
|
||||
|
||||
settlements[settlementIndex].updatedAt = new Date();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '结算状态更新成功',
|
||||
data: settlements[settlementIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新结算状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取支付记录列表
|
||||
router.get('/payments', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
settlementId,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
let filteredPayments = [...payments];
|
||||
|
||||
// 按结算单筛选
|
||||
if (settlementId) {
|
||||
filteredPayments = filteredPayments.filter(payment => payment.settlementId === parseInt(settlementId));
|
||||
}
|
||||
|
||||
// 按状态筛选
|
||||
if (status) {
|
||||
filteredPayments = filteredPayments.filter(payment => payment.status === status);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedPayments = filteredPayments.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedPayments,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredPayments.length,
|
||||
totalPages: Math.ceil(filteredPayments.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取支付记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建支付记录
|
||||
router.post('/payments', (req, res) => {
|
||||
try {
|
||||
const { error, value } = paymentCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const paymentCode = `PAY${String(Date.now()).slice(-6)}`;
|
||||
|
||||
const newPayment = {
|
||||
id: Math.max(...payments.map(p => p.id)) + 1,
|
||||
...value,
|
||||
paymentCode,
|
||||
status: 'processing',
|
||||
paidAt: null,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
payments.push(newPayment);
|
||||
|
||||
// 模拟支付处理
|
||||
setTimeout(() => {
|
||||
const paymentIndex = payments.findIndex(p => p.id === newPayment.id);
|
||||
if (paymentIndex !== -1) {
|
||||
payments[paymentIndex].status = 'success';
|
||||
payments[paymentIndex].paidAt = new Date().toISOString();
|
||||
payments[paymentIndex].transactionId = `TXN${Date.now()}`;
|
||||
|
||||
// 更新对应结算单状态
|
||||
const settlementIndex = settlements.findIndex(s => s.id === value.settlementId);
|
||||
if (settlementIndex !== -1) {
|
||||
settlements[settlementIndex].paymentStatus = 'paid';
|
||||
settlements[settlementIndex].paymentDate = new Date().toISOString().split('T')[0];
|
||||
settlements[settlementIndex].updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
}, 3000); // 3秒后处理完成
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '支付申请已提交',
|
||||
data: newPayment
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建支付记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取财务统计
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalSettlements = settlements.length;
|
||||
const paidCount = settlements.filter(s => s.paymentStatus === 'paid').length;
|
||||
const pendingCount = settlements.filter(s => s.paymentStatus === 'pending').length;
|
||||
|
||||
const totalAmount = settlements.reduce((sum, s) => sum + s.totalAmount, 0);
|
||||
const paidAmount = settlements
|
||||
.filter(s => s.paymentStatus === 'paid')
|
||||
.reduce((sum, s) => sum + s.actualPayment, 0);
|
||||
const pendingAmount = settlements
|
||||
.filter(s => s.paymentStatus === 'pending')
|
||||
.reduce((sum, s) => sum + s.actualPayment, 0);
|
||||
|
||||
const totalTaxAmount = settlements.reduce((sum, s) => sum + s.taxAmount, 0);
|
||||
|
||||
// 本月统计
|
||||
const currentMonth = new Date().getMonth();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const monthlySettlements = settlements.filter(s => {
|
||||
const settleDate = new Date(s.settlementDate);
|
||||
return settleDate.getMonth() === currentMonth && settleDate.getFullYear() === currentYear;
|
||||
});
|
||||
const monthlyAmount = monthlySettlements.reduce((sum, s) => sum + s.totalAmount, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSettlements,
|
||||
paidCount,
|
||||
pendingCount,
|
||||
totalAmount,
|
||||
paidAmount,
|
||||
pendingAmount,
|
||||
totalTaxAmount,
|
||||
monthlyAmount,
|
||||
paymentRate: totalSettlements > 0 ? Math.round((paidCount / totalSettlements) * 100) : 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取财务统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取财务报表
|
||||
router.get('/reports/monthly', (req, res) => {
|
||||
try {
|
||||
const { year = new Date().getFullYear(), month } = req.query;
|
||||
|
||||
let targetSettlements = settlements;
|
||||
|
||||
// 筛选指定年份
|
||||
targetSettlements = targetSettlements.filter(s => {
|
||||
const settleDate = new Date(s.settlementDate);
|
||||
return settleDate.getFullYear() === parseInt(year);
|
||||
});
|
||||
|
||||
// 如果指定了月份,进一步筛选
|
||||
if (month) {
|
||||
targetSettlements = targetSettlements.filter(s => {
|
||||
const settleDate = new Date(s.settlementDate);
|
||||
return settleDate.getMonth() === parseInt(month) - 1;
|
||||
});
|
||||
}
|
||||
|
||||
// 按月份分组统计
|
||||
const monthlyStats = {};
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
monthlyStats[i] = {
|
||||
month: i,
|
||||
settlementCount: 0,
|
||||
totalAmount: 0,
|
||||
paidAmount: 0,
|
||||
pendingAmount: 0
|
||||
};
|
||||
}
|
||||
|
||||
targetSettlements.forEach(settlement => {
|
||||
const settleMonth = new Date(settlement.settlementDate).getMonth() + 1;
|
||||
monthlyStats[settleMonth].settlementCount++;
|
||||
monthlyStats[settleMonth].totalAmount += settlement.totalAmount;
|
||||
|
||||
if (settlement.paymentStatus === 'paid') {
|
||||
monthlyStats[settleMonth].paidAmount += settlement.actualPayment;
|
||||
} else {
|
||||
monthlyStats[settleMonth].pendingAmount += settlement.actualPayment;
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
year: parseInt(year),
|
||||
monthlyStats: Object.values(monthlyStats)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取财务报表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
539
backend/routes/orders.js
Normal file
539
backend/routes/orders.js
Normal file
@@ -0,0 +1,539 @@
|
||||
const express = require('express')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 模拟订单数据
|
||||
let orders = [
|
||||
{
|
||||
id: 1,
|
||||
orderNo: 'ORD20240101001',
|
||||
buyerId: 2,
|
||||
buyerName: '山东养殖场',
|
||||
supplierId: 3,
|
||||
supplierName: '河北供应商',
|
||||
traderId: 1,
|
||||
traderName: '北京贸易公司',
|
||||
cattleBreed: '西门塔尔',
|
||||
cattleCount: 50,
|
||||
expectedWeight: 25000,
|
||||
actualWeight: 24800,
|
||||
unitPrice: 28.5,
|
||||
totalAmount: 712500,
|
||||
paidAmount: 200000,
|
||||
remainingAmount: 512500,
|
||||
status: 'shipping',
|
||||
deliveryAddress: '山东省济南市某养殖场',
|
||||
expectedDeliveryDate: '2024-01-15',
|
||||
actualDeliveryDate: null,
|
||||
notes: '优质西门塔尔牛',
|
||||
createdAt: '2024-01-10T00:00:00Z',
|
||||
updatedAt: '2024-01-12T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderNo: 'ORD20240101002',
|
||||
buyerId: 2,
|
||||
buyerName: '山东养殖场',
|
||||
supplierId: 4,
|
||||
supplierName: '内蒙古牧场',
|
||||
traderId: 1,
|
||||
traderName: '北京贸易公司',
|
||||
cattleBreed: '安格斯',
|
||||
cattleCount: 30,
|
||||
expectedWeight: 18000,
|
||||
actualWeight: 18200,
|
||||
unitPrice: 30.0,
|
||||
totalAmount: 540000,
|
||||
paidAmount: 540000,
|
||||
remainingAmount: 0,
|
||||
status: 'completed',
|
||||
deliveryAddress: '山东省济南市某养殖场',
|
||||
expectedDeliveryDate: '2024-01-08',
|
||||
actualDeliveryDate: '2024-01-08',
|
||||
notes: '',
|
||||
createdAt: '2024-01-05T00:00:00Z',
|
||||
updatedAt: '2024-01-08T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
||||
// 订单状态枚举
|
||||
const ORDER_STATUS = {
|
||||
PENDING: 'pending',
|
||||
CONFIRMED: 'confirmed',
|
||||
PREPARING: 'preparing',
|
||||
SHIPPING: 'shipping',
|
||||
DELIVERED: 'delivered',
|
||||
ACCEPTED: 'accepted',
|
||||
COMPLETED: 'completed',
|
||||
CANCELLED: 'cancelled',
|
||||
REFUNDED: 'refunded'
|
||||
}
|
||||
|
||||
// 验证模式
|
||||
const createOrderSchema = Joi.object({
|
||||
buyerId: Joi.number().integer().positive().required(),
|
||||
supplierId: Joi.number().integer().positive().required(),
|
||||
traderId: Joi.number().integer().positive(),
|
||||
cattleBreed: Joi.string().min(1).max(50).required(),
|
||||
cattleCount: Joi.number().integer().positive().required(),
|
||||
expectedWeight: Joi.number().positive().required(),
|
||||
unitPrice: Joi.number().positive().required(),
|
||||
deliveryAddress: Joi.string().min(1).max(200).required(),
|
||||
expectedDeliveryDate: Joi.date().iso().required(),
|
||||
notes: Joi.string().max(500).allow('')
|
||||
})
|
||||
|
||||
const updateOrderSchema = Joi.object({
|
||||
cattleBreed: Joi.string().min(1).max(50),
|
||||
cattleCount: Joi.number().integer().positive(),
|
||||
expectedWeight: Joi.number().positive(),
|
||||
actualWeight: Joi.number().positive(),
|
||||
unitPrice: Joi.number().positive(),
|
||||
deliveryAddress: Joi.string().min(1).max(200),
|
||||
expectedDeliveryDate: Joi.date().iso(),
|
||||
actualDeliveryDate: Joi.date().iso(),
|
||||
notes: Joi.string().max(500).allow(''),
|
||||
status: Joi.string().valid(...Object.values(ORDER_STATUS))
|
||||
})
|
||||
|
||||
// 获取订单列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
orderNo,
|
||||
buyerId,
|
||||
supplierId,
|
||||
status,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query
|
||||
|
||||
let filteredOrders = [...orders]
|
||||
|
||||
// 订单号搜索
|
||||
if (orderNo) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.orderNo.includes(orderNo)
|
||||
)
|
||||
}
|
||||
|
||||
// 买方筛选
|
||||
if (buyerId) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.buyerId === parseInt(buyerId)
|
||||
)
|
||||
}
|
||||
|
||||
// 供应商筛选
|
||||
if (supplierId) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
order.supplierId === parseInt(supplierId)
|
||||
)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredOrders = filteredOrders.filter(order => order.status === status)
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if (startDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) >= new Date(startDate)
|
||||
)
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) <= new Date(endDate)
|
||||
)
|
||||
}
|
||||
|
||||
// 分页
|
||||
const total = filteredOrders.length
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const endIndex = startIndex + parseInt(pageSize)
|
||||
const paginatedOrders = filteredOrders.slice(startIndex, endIndex)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: paginatedOrders,
|
||||
total: total,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单列表失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取订单详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const order = orders.find(o => o.id === parseInt(id))
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: order
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单详情失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 创建订单
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = createOrderSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
buyerId,
|
||||
supplierId,
|
||||
traderId,
|
||||
cattleBreed,
|
||||
cattleCount,
|
||||
expectedWeight,
|
||||
unitPrice,
|
||||
deliveryAddress,
|
||||
expectedDeliveryDate,
|
||||
notes
|
||||
} = value
|
||||
|
||||
// 生成订单号
|
||||
const orderNo = `ORD${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(orders.length + 1).padStart(3, '0')}`
|
||||
|
||||
// 计算总金额
|
||||
const totalAmount = expectedWeight * unitPrice
|
||||
|
||||
// 创建新订单
|
||||
const newOrder = {
|
||||
id: Math.max(...orders.map(o => o.id)) + 1,
|
||||
orderNo,
|
||||
buyerId,
|
||||
buyerName: '买方名称', // 实际项目中需要从数据库获取
|
||||
supplierId,
|
||||
supplierName: '供应商名称', // 实际项目中需要从数据库获取
|
||||
traderId: traderId || null,
|
||||
traderName: traderId ? '贸易商名称' : null,
|
||||
cattleBreed,
|
||||
cattleCount,
|
||||
expectedWeight,
|
||||
actualWeight: null,
|
||||
unitPrice,
|
||||
totalAmount,
|
||||
paidAmount: 0,
|
||||
remainingAmount: totalAmount,
|
||||
status: ORDER_STATUS.PENDING,
|
||||
deliveryAddress,
|
||||
expectedDeliveryDate,
|
||||
actualDeliveryDate: null,
|
||||
notes: notes || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
orders.push(newOrder)
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '订单创建成功',
|
||||
data: newOrder
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新订单
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
const { error, value } = updateOrderSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
// 更新订单信息
|
||||
orders[orderIndex] = {
|
||||
...orders[orderIndex],
|
||||
...value,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 如果更新了实际重量,重新计算总金额
|
||||
if (value.actualWeight && orders[orderIndex].unitPrice) {
|
||||
orders[orderIndex].totalAmount = value.actualWeight * orders[orderIndex].unitPrice
|
||||
orders[orderIndex].remainingAmount = orders[orderIndex].totalAmount - orders[orderIndex].paidAmount
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单更新成功',
|
||||
data: orders[orderIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除订单
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
orders.splice(orderIndex, 1)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 确认订单
|
||||
router.put('/:id/confirm', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (orders[orderIndex].status !== ORDER_STATUS.PENDING) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '只有待确认的订单才能确认'
|
||||
})
|
||||
}
|
||||
|
||||
orders[orderIndex].status = ORDER_STATUS.CONFIRMED
|
||||
orders[orderIndex].updatedAt = new Date().toISOString()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单确认成功',
|
||||
data: orders[orderIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '确认订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 取消订单
|
||||
router.put('/:id/cancel', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { reason } = req.body
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
orders[orderIndex].status = ORDER_STATUS.CANCELLED
|
||||
orders[orderIndex].notes = reason ? `取消原因: ${reason}` : '订单已取消'
|
||||
orders[orderIndex].updatedAt = new Date().toISOString()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单取消成功',
|
||||
data: orders[orderIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '取消订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 订单验收
|
||||
router.put('/:id/accept', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { actualWeight, notes } = req.body
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (!actualWeight || actualWeight <= 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供有效的实际重量'
|
||||
})
|
||||
}
|
||||
|
||||
orders[orderIndex].status = ORDER_STATUS.ACCEPTED
|
||||
orders[orderIndex].actualWeight = actualWeight
|
||||
orders[orderIndex].totalAmount = actualWeight * orders[orderIndex].unitPrice
|
||||
orders[orderIndex].remainingAmount = orders[orderIndex].totalAmount - orders[orderIndex].paidAmount
|
||||
orders[orderIndex].actualDeliveryDate = new Date().toISOString()
|
||||
if (notes) {
|
||||
orders[orderIndex].notes = notes
|
||||
}
|
||||
orders[orderIndex].updatedAt = new Date().toISOString()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单验收成功',
|
||||
data: orders[orderIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '订单验收失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 完成订单
|
||||
router.put('/:id/complete', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orderIndex = orders.findIndex(o => o.id === parseInt(id))
|
||||
|
||||
if (orderIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '订单不存在'
|
||||
})
|
||||
}
|
||||
|
||||
orders[orderIndex].status = ORDER_STATUS.COMPLETED
|
||||
orders[orderIndex].paidAmount = orders[orderIndex].totalAmount
|
||||
orders[orderIndex].remainingAmount = 0
|
||||
orders[orderIndex].updatedAt = new Date().toISOString()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '订单完成成功',
|
||||
data: orders[orderIndex]
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '完成订单失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取订单统计数据
|
||||
router.get('/statistics', (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query
|
||||
|
||||
let filteredOrders = [...orders]
|
||||
|
||||
if (startDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) >= new Date(startDate)
|
||||
)
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredOrders = filteredOrders.filter(order =>
|
||||
new Date(order.createdAt) <= new Date(endDate)
|
||||
)
|
||||
}
|
||||
|
||||
const statistics = {
|
||||
totalOrders: filteredOrders.length,
|
||||
completedOrders: filteredOrders.filter(o => o.status === ORDER_STATUS.COMPLETED).length,
|
||||
pendingOrders: filteredOrders.filter(o => o.status === ORDER_STATUS.PENDING).length,
|
||||
cancelledOrders: filteredOrders.filter(o => o.status === ORDER_STATUS.CANCELLED).length,
|
||||
totalAmount: filteredOrders.reduce((sum, order) => sum + order.totalAmount, 0),
|
||||
totalCattle: filteredOrders.reduce((sum, order) => sum + order.cattleCount, 0),
|
||||
statusDistribution: Object.values(ORDER_STATUS).reduce((acc, status) => {
|
||||
acc[status] = filteredOrders.filter(o => o.status === status).length
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statistics
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取订单统计失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
548
backend/routes/quality.js
Normal file
548
backend/routes/quality.js
Normal file
@@ -0,0 +1,548 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟质量检测数据
|
||||
let qualityRecords = [
|
||||
{
|
||||
id: 1,
|
||||
orderId: 1,
|
||||
inspectionCode: 'QC001',
|
||||
inspectorName: '张检验员',
|
||||
inspectionDate: '2024-01-15',
|
||||
inspectionLocation: '山东省济南市历城区牲畜养殖基地',
|
||||
cattleCount: 50,
|
||||
samplingCount: 5,
|
||||
inspectionType: 'pre_transport',
|
||||
healthStatus: 'healthy',
|
||||
quarantineCertificate: 'QC001_certificate.pdf',
|
||||
vaccineRecords: [
|
||||
{
|
||||
vaccineName: '口蹄疫疫苗',
|
||||
vaccineDate: '2024-01-01',
|
||||
batchNumber: 'VAC20240101'
|
||||
}
|
||||
],
|
||||
diseaseTests: [
|
||||
{
|
||||
testName: '布鲁氏菌病检测',
|
||||
result: 'negative',
|
||||
testDate: '2024-01-10'
|
||||
},
|
||||
{
|
||||
testName: '结核病检测',
|
||||
result: 'negative',
|
||||
testDate: '2024-01-10'
|
||||
}
|
||||
],
|
||||
weightCheck: {
|
||||
averageWeight: 450,
|
||||
weightRange: '420-480',
|
||||
weightVariance: 15
|
||||
},
|
||||
qualityGrade: 'A',
|
||||
qualityScore: 95,
|
||||
issues: [],
|
||||
recommendations: [
|
||||
'建议继续保持当前饲养标准',
|
||||
'注意观察牲畜健康状况'
|
||||
],
|
||||
photos: [
|
||||
'inspection_001_1.jpg',
|
||||
'inspection_001_2.jpg'
|
||||
],
|
||||
status: 'passed',
|
||||
createdAt: new Date('2024-01-15'),
|
||||
updatedAt: new Date('2024-01-15')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderId: 2,
|
||||
inspectionCode: 'QC002',
|
||||
inspectorName: '李检验员',
|
||||
inspectionDate: '2024-01-16',
|
||||
inspectionLocation: '内蒙古呼和浩特市草原牧场',
|
||||
cattleCount: 80,
|
||||
samplingCount: 8,
|
||||
inspectionType: 'pre_transport',
|
||||
healthStatus: 'healthy',
|
||||
quarantineCertificate: 'QC002_certificate.pdf',
|
||||
vaccineRecords: [
|
||||
{
|
||||
vaccineName: '口蹄疫疫苗',
|
||||
vaccineDate: '2023-12-15',
|
||||
batchNumber: 'VAC20231215'
|
||||
}
|
||||
],
|
||||
diseaseTests: [
|
||||
{
|
||||
testName: '布鲁氏菌病检测',
|
||||
result: 'negative',
|
||||
testDate: '2024-01-12'
|
||||
}
|
||||
],
|
||||
weightCheck: {
|
||||
averageWeight: 480,
|
||||
weightRange: '450-520',
|
||||
weightVariance: 20
|
||||
},
|
||||
qualityGrade: 'A',
|
||||
qualityScore: 92,
|
||||
issues: [
|
||||
{
|
||||
type: 'minor',
|
||||
description: '个别牲畜体重偏轻',
|
||||
solution: '加强营养补充'
|
||||
}
|
||||
],
|
||||
recommendations: [
|
||||
'对体重偏轻的牲畜进行重点关注',
|
||||
'适当调整饲料配比'
|
||||
],
|
||||
photos: [
|
||||
'inspection_002_1.jpg',
|
||||
'inspection_002_2.jpg',
|
||||
'inspection_002_3.jpg'
|
||||
],
|
||||
status: 'passed',
|
||||
createdAt: new Date('2024-01-16'),
|
||||
updatedAt: new Date('2024-01-16')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const inspectionCreateSchema = Joi.object({
|
||||
orderId: Joi.number().integer().required(),
|
||||
inspectorName: Joi.string().min(2).max(50).required(),
|
||||
inspectionDate: Joi.date().iso().required(),
|
||||
inspectionLocation: Joi.string().min(5).max(200).required(),
|
||||
cattleCount: Joi.number().integer().min(1).required(),
|
||||
samplingCount: Joi.number().integer().min(1).required(),
|
||||
inspectionType: Joi.string().valid('pre_transport', 'during_transport', 'post_transport', 'arrival').required()
|
||||
});
|
||||
|
||||
const qualityResultSchema = Joi.object({
|
||||
healthStatus: Joi.string().valid('healthy', 'sick', 'quarantine').required(),
|
||||
qualityGrade: Joi.string().valid('A+', 'A', 'B+', 'B', 'C', 'D').required(),
|
||||
qualityScore: Joi.number().min(0).max(100).required(),
|
||||
weightCheck: Joi.object({
|
||||
averageWeight: Joi.number().min(0),
|
||||
weightRange: Joi.string(),
|
||||
weightVariance: Joi.number().min(0)
|
||||
}),
|
||||
diseaseTests: Joi.array().items(Joi.object({
|
||||
testName: Joi.string().required(),
|
||||
result: Joi.string().valid('positive', 'negative', 'inconclusive').required(),
|
||||
testDate: Joi.date().iso().required()
|
||||
})),
|
||||
issues: Joi.array().items(Joi.object({
|
||||
type: Joi.string().valid('critical', 'major', 'minor').required(),
|
||||
description: Joi.string().required(),
|
||||
solution: Joi.string()
|
||||
})),
|
||||
recommendations: Joi.array().items(Joi.string())
|
||||
});
|
||||
|
||||
// 获取质量检测列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
inspectionType,
|
||||
qualityGrade,
|
||||
status,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
let filteredRecords = [...qualityRecords];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredRecords = filteredRecords.filter(record =>
|
||||
record.inspectionCode.includes(keyword) ||
|
||||
record.inspectorName.includes(keyword) ||
|
||||
record.inspectionLocation.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 检测类型筛选
|
||||
if (inspectionType) {
|
||||
filteredRecords = filteredRecords.filter(record => record.inspectionType === inspectionType);
|
||||
}
|
||||
|
||||
// 质量等级筛选
|
||||
if (qualityGrade) {
|
||||
filteredRecords = filteredRecords.filter(record => record.qualityGrade === qualityGrade);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredRecords = filteredRecords.filter(record => record.status === status);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate) {
|
||||
filteredRecords = filteredRecords.filter(record =>
|
||||
new Date(record.inspectionDate) >= new Date(startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredRecords = filteredRecords.filter(record =>
|
||||
new Date(record.inspectionDate) <= new Date(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedRecords = filteredRecords.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedRecords,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredRecords.length,
|
||||
totalPages: Math.ceil(filteredRecords.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量检测列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取质量检测详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const record = qualityRecords.find(r => r.id === parseInt(id));
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '质量检测记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量检测详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建质量检测记录
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { error, value } = inspectionCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const inspectionCode = `QC${String(Date.now()).slice(-6)}`;
|
||||
|
||||
const newRecord = {
|
||||
id: Math.max(...qualityRecords.map(r => r.id)) + 1,
|
||||
...value,
|
||||
inspectionCode,
|
||||
healthStatus: 'pending',
|
||||
quarantineCertificate: '',
|
||||
vaccineRecords: [],
|
||||
diseaseTests: [],
|
||||
weightCheck: null,
|
||||
qualityGrade: '',
|
||||
qualityScore: 0,
|
||||
issues: [],
|
||||
recommendations: [],
|
||||
photos: [],
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
qualityRecords.push(newRecord);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '质量检测记录创建成功',
|
||||
data: newRecord
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建质量检测记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新质量检测结果
|
||||
router.put('/:id/result', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = qualityResultSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const recordIndex = qualityRecords.findIndex(r => r.id === parseInt(id));
|
||||
if (recordIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '质量检测记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 根据检测结果确定状态
|
||||
let status = 'passed';
|
||||
if (value.healthStatus === 'sick' || value.qualityScore < 60) {
|
||||
status = 'failed';
|
||||
} else if (value.healthStatus === 'quarantine' || value.issues.some(issue => issue.type === 'critical')) {
|
||||
status = 'quarantine';
|
||||
}
|
||||
|
||||
qualityRecords[recordIndex] = {
|
||||
...qualityRecords[recordIndex],
|
||||
...value,
|
||||
status,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '质量检测结果更新成功',
|
||||
data: qualityRecords[recordIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新质量检测结果失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上传检测照片
|
||||
router.post('/:id/photos', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { photos } = req.body;
|
||||
|
||||
if (!Array.isArray(photos) || photos.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '照片列表不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
const recordIndex = qualityRecords.findIndex(r => r.id === parseInt(id));
|
||||
if (recordIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '质量检测记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
qualityRecords[recordIndex].photos = [...qualityRecords[recordIndex].photos, ...photos];
|
||||
qualityRecords[recordIndex].updatedAt = new Date();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '照片上传成功',
|
||||
data: {
|
||||
photos: qualityRecords[recordIndex].photos
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '上传照片失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取质量统计
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalInspections = qualityRecords.length;
|
||||
const passedCount = qualityRecords.filter(r => r.status === 'passed').length;
|
||||
const failedCount = qualityRecords.filter(r => r.status === 'failed').length;
|
||||
const quarantineCount = qualityRecords.filter(r => r.status === 'quarantine').length;
|
||||
const pendingCount = qualityRecords.filter(r => r.status === 'pending').length;
|
||||
|
||||
// 平均质量分数
|
||||
const completedRecords = qualityRecords.filter(r => r.qualityScore > 0);
|
||||
const averageScore = completedRecords.length > 0
|
||||
? completedRecords.reduce((sum, r) => sum + r.qualityScore, 0) / completedRecords.length
|
||||
: 0;
|
||||
|
||||
// 质量等级分布
|
||||
const gradeDistribution = qualityRecords
|
||||
.filter(r => r.qualityGrade)
|
||||
.reduce((dist, record) => {
|
||||
dist[record.qualityGrade] = (dist[record.qualityGrade] || 0) + 1;
|
||||
return dist;
|
||||
}, {});
|
||||
|
||||
// 检测类型分布
|
||||
const typeDistribution = qualityRecords.reduce((dist, record) => {
|
||||
dist[record.inspectionType] = (dist[record.inspectionType] || 0) + 1;
|
||||
return dist;
|
||||
}, {});
|
||||
|
||||
// 合格率
|
||||
const passRate = totalInspections > 0 ? Math.round((passedCount / totalInspections) * 100) : 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalInspections,
|
||||
passedCount,
|
||||
failedCount,
|
||||
quarantineCount,
|
||||
pendingCount,
|
||||
averageScore: Math.round(averageScore * 10) / 10,
|
||||
passRate,
|
||||
gradeDistribution,
|
||||
typeDistribution
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取质量趋势报告
|
||||
router.get('/reports/trend', (req, res) => {
|
||||
try {
|
||||
const { period = 'month' } = req.query;
|
||||
|
||||
// 按时间分组统计
|
||||
const now = new Date();
|
||||
const trends = [];
|
||||
|
||||
if (period === 'month') {
|
||||
// 最近12个月
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const monthRecords = qualityRecords.filter(r => {
|
||||
const recordDate = new Date(r.inspectionDate);
|
||||
return recordDate.getMonth() === date.getMonth() &&
|
||||
recordDate.getFullYear() === date.getFullYear();
|
||||
});
|
||||
|
||||
const passed = monthRecords.filter(r => r.status === 'passed').length;
|
||||
const total = monthRecords.length;
|
||||
const averageScore = monthRecords.length > 0
|
||||
? monthRecords.reduce((sum, r) => sum + (r.qualityScore || 0), 0) / monthRecords.length
|
||||
: 0;
|
||||
|
||||
trends.push({
|
||||
period: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
totalInspections: total,
|
||||
passedCount: passed,
|
||||
passRate: total > 0 ? Math.round((passed / total) * 100) : 0,
|
||||
averageScore: Math.round(averageScore * 10) / 10
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
period,
|
||||
trends
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取质量趋势报告失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取检测标准配置
|
||||
router.get('/standards', (req, res) => {
|
||||
try {
|
||||
const standards = {
|
||||
weightStandards: {
|
||||
cattle: {
|
||||
min: 400,
|
||||
max: 600,
|
||||
optimal: 500
|
||||
}
|
||||
},
|
||||
healthRequirements: [
|
||||
{
|
||||
name: '口蹄疫疫苗',
|
||||
required: true,
|
||||
validityDays: 365
|
||||
},
|
||||
{
|
||||
name: '布鲁氏菌病检测',
|
||||
required: true,
|
||||
validityDays: 30
|
||||
},
|
||||
{
|
||||
name: '结核病检测',
|
||||
required: true,
|
||||
validityDays: 30
|
||||
}
|
||||
],
|
||||
gradingCriteria: {
|
||||
'A+': { minScore: 95, description: '优质级' },
|
||||
'A': { minScore: 85, description: '良好级' },
|
||||
'B+': { minScore: 75, description: '合格级' },
|
||||
'B': { minScore: 65, description: '基本合格级' },
|
||||
'C': { minScore: 50, description: '待改进级' },
|
||||
'D': { minScore: 0, description: '不合格级' }
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: standards
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取检测标准失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
406
backend/routes/suppliers.js
Normal file
406
backend/routes/suppliers.js
Normal file
@@ -0,0 +1,406 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟供应商数据
|
||||
let suppliers = [
|
||||
{
|
||||
id: 1,
|
||||
name: '山东优质牲畜合作社',
|
||||
code: 'SUP001',
|
||||
contact: '李经理',
|
||||
phone: '15888888888',
|
||||
address: '山东省济南市历城区牲畜养殖基地',
|
||||
businessLicense: 'SUP001_license.pdf',
|
||||
qualificationLevel: 'A',
|
||||
certifications: ['动物防疫合格证', '饲料生产许可证'],
|
||||
cattleTypes: ['肉牛', '奶牛'],
|
||||
capacity: 5000,
|
||||
rating: 4.8,
|
||||
cooperationStartDate: '2022-01-15',
|
||||
status: 'active',
|
||||
region: 'east',
|
||||
createdAt: new Date('2022-01-15'),
|
||||
updatedAt: new Date('2024-01-15')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '内蒙古草原牲畜有限公司',
|
||||
code: 'SUP002',
|
||||
contact: '王总',
|
||||
phone: '13999999999',
|
||||
address: '内蒙古呼和浩特市草原牧场',
|
||||
businessLicense: 'SUP002_license.pdf',
|
||||
qualificationLevel: 'A+',
|
||||
certifications: ['有机认证', '绿色食品认证'],
|
||||
cattleTypes: ['草原牛', '黄牛'],
|
||||
capacity: 8000,
|
||||
rating: 4.9,
|
||||
cooperationStartDate: '2021-08-20',
|
||||
status: 'active',
|
||||
region: 'north',
|
||||
createdAt: new Date('2021-08-20'),
|
||||
updatedAt: new Date('2024-01-20')
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '四川高原牲畜养殖场',
|
||||
code: 'SUP003',
|
||||
contact: '张场长',
|
||||
phone: '18777777777',
|
||||
address: '四川省成都市高原养殖区',
|
||||
businessLicense: 'SUP003_license.pdf',
|
||||
qualificationLevel: 'B+',
|
||||
certifications: ['无公害产品认证'],
|
||||
cattleTypes: ['高原牛'],
|
||||
capacity: 3000,
|
||||
rating: 4.5,
|
||||
cooperationStartDate: '2022-06-10',
|
||||
status: 'active',
|
||||
region: 'southwest',
|
||||
createdAt: new Date('2022-06-10'),
|
||||
updatedAt: new Date('2024-01-10')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const supplierCreateSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100).required(),
|
||||
code: Joi.string().min(3).max(20).required(),
|
||||
contact: Joi.string().min(2).max(50).required(),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(),
|
||||
address: Joi.string().min(5).max(200).required(),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C').required(),
|
||||
cattleTypes: Joi.array().items(Joi.string()).min(1).required(),
|
||||
capacity: Joi.number().integer().min(1).required(),
|
||||
region: Joi.string().valid('north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest', 'central').required()
|
||||
});
|
||||
|
||||
const supplierUpdateSchema = Joi.object({
|
||||
name: Joi.string().min(2).max(100),
|
||||
contact: Joi.string().min(2).max(50),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/),
|
||||
address: Joi.string().min(5).max(200),
|
||||
qualificationLevel: Joi.string().valid('A+', 'A', 'B+', 'B', 'C'),
|
||||
cattleTypes: Joi.array().items(Joi.string()).min(1),
|
||||
capacity: Joi.number().integer().min(1),
|
||||
region: Joi.string().valid('north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest', 'central'),
|
||||
status: Joi.string().valid('active', 'inactive', 'suspended')
|
||||
});
|
||||
|
||||
// 获取供应商列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
region,
|
||||
qualificationLevel,
|
||||
status = 'active'
|
||||
} = req.query;
|
||||
|
||||
let filteredSuppliers = [...suppliers];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier =>
|
||||
supplier.name.includes(keyword) ||
|
||||
supplier.code.includes(keyword) ||
|
||||
supplier.contact.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 区域筛选
|
||||
if (region) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.region === region);
|
||||
}
|
||||
|
||||
// 资质等级筛选
|
||||
if (qualificationLevel) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.qualificationLevel === qualificationLevel);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredSuppliers = filteredSuppliers.filter(supplier => supplier.status === status);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedSuppliers = filteredSuppliers.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedSuppliers,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredSuppliers.length,
|
||||
totalPages: Math.ceil(filteredSuppliers.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const supplier = suppliers.find(s => s.id === parseInt(id));
|
||||
|
||||
if (!supplier) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: supplier
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建供应商
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { error, value } = supplierCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
// 检查编码是否重复
|
||||
const existingSupplier = suppliers.find(s => s.code === value.code);
|
||||
if (existingSupplier) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '供应商编码已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const newSupplier = {
|
||||
id: Math.max(...suppliers.map(s => s.id)) + 1,
|
||||
...value,
|
||||
businessLicense: '',
|
||||
certifications: [],
|
||||
rating: 0,
|
||||
cooperationStartDate: new Date().toISOString().split('T')[0],
|
||||
status: 'active',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
suppliers.push(newSupplier);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '供应商创建成功',
|
||||
data: newSupplier
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新供应商
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = supplierUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const supplierIndex = suppliers.findIndex(s => s.id === parseInt(id));
|
||||
if (supplierIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers[supplierIndex] = {
|
||||
...suppliers[supplierIndex],
|
||||
...value,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商更新成功',
|
||||
data: suppliers[supplierIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 删除供应商
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const supplierIndex = suppliers.findIndex(s => s.id === parseInt(id));
|
||||
|
||||
if (supplierIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '供应商不存在'
|
||||
});
|
||||
}
|
||||
|
||||
suppliers.splice(supplierIndex, 1);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '供应商删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除供应商失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取供应商统计信息
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalSuppliers = suppliers.length;
|
||||
const activeSuppliers = suppliers.filter(s => s.status === 'active').length;
|
||||
const averageRating = suppliers.reduce((sum, s) => sum + s.rating, 0) / totalSuppliers;
|
||||
const totalCapacity = suppliers.reduce((sum, s) => sum + s.capacity, 0);
|
||||
|
||||
// 按等级统计
|
||||
const levelStats = suppliers.reduce((stats, supplier) => {
|
||||
stats[supplier.qualificationLevel] = (stats[supplier.qualificationLevel] || 0) + 1;
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
// 按区域统计
|
||||
const regionStats = suppliers.reduce((stats, supplier) => {
|
||||
stats[supplier.region] = (stats[supplier.region] || 0) + 1;
|
||||
return stats;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSuppliers,
|
||||
activeSuppliers,
|
||||
averageRating: Math.round(averageRating * 10) / 10,
|
||||
totalCapacity,
|
||||
levelStats,
|
||||
regionStats
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取供应商统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 批量操作
|
||||
router.post('/batch', (req, res) => {
|
||||
try {
|
||||
const { action, ids } = req.body;
|
||||
|
||||
if (!action || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数错误'
|
||||
});
|
||||
}
|
||||
|
||||
let affectedCount = 0;
|
||||
|
||||
switch (action) {
|
||||
case 'activate':
|
||||
suppliers.forEach(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
supplier.status = 'active';
|
||||
supplier.updatedAt = new Date();
|
||||
affectedCount++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'deactivate':
|
||||
suppliers.forEach(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
supplier.status = 'inactive';
|
||||
supplier.updatedAt = new Date();
|
||||
affectedCount++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
suppliers = suppliers.filter(supplier => {
|
||||
if (ids.includes(supplier.id)) {
|
||||
affectedCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '不支持的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `批量${action}成功`,
|
||||
data: { affectedCount }
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量操作失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
467
backend/routes/transport.js
Normal file
467
backend/routes/transport.js
Normal file
@@ -0,0 +1,467 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
|
||||
// 模拟运输数据
|
||||
let transports = [
|
||||
{
|
||||
id: 1,
|
||||
orderId: 1,
|
||||
transportCode: 'TRP001',
|
||||
driverName: '张师傅',
|
||||
driverPhone: '13800001111',
|
||||
vehicleNumber: '鲁A12345',
|
||||
vehicleType: '厢式货车',
|
||||
startLocation: '山东省济南市历城区牲畜养殖基地',
|
||||
endLocation: '北京市朝阳区肉类加工厂',
|
||||
plannedDepartureTime: '2024-01-15T08:00:00Z',
|
||||
actualDepartureTime: '2024-01-15T08:30:00Z',
|
||||
estimatedArrivalTime: '2024-01-15T18:00:00Z',
|
||||
actualArrivalTime: null,
|
||||
distance: 450,
|
||||
status: 'in_transit',
|
||||
currentLocation: {
|
||||
lat: 36.8012,
|
||||
lng: 117.1120,
|
||||
address: '山东省济南市天桥区',
|
||||
updateTime: '2024-01-15T14:30:00Z'
|
||||
},
|
||||
route: [
|
||||
{ lat: 36.6512, lng: 117.1201, time: '2024-01-15T08:30:00Z' },
|
||||
{ lat: 36.7012, lng: 117.1001, time: '2024-01-15T10:30:00Z' },
|
||||
{ lat: 36.8012, lng: 117.1120, time: '2024-01-15T14:30:00Z' }
|
||||
],
|
||||
cattleCount: 50,
|
||||
temperature: 18,
|
||||
humidity: 65,
|
||||
alerts: [],
|
||||
createdAt: new Date('2024-01-15T08:00:00Z'),
|
||||
updatedAt: new Date('2024-01-15T14:30:00Z')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderId: 2,
|
||||
transportCode: 'TRP002',
|
||||
driverName: '李师傅',
|
||||
driverPhone: '13800002222',
|
||||
vehicleNumber: '蒙B67890',
|
||||
vehicleType: '专用运牛车',
|
||||
startLocation: '内蒙古呼和浩特市草原牧场',
|
||||
endLocation: '天津市滨海新区屠宰场',
|
||||
plannedDepartureTime: '2024-01-16T06:00:00Z',
|
||||
actualDepartureTime: '2024-01-16T06:15:00Z',
|
||||
estimatedArrivalTime: '2024-01-16T20:00:00Z',
|
||||
actualArrivalTime: '2024-01-16T19:45:00Z',
|
||||
distance: 680,
|
||||
status: 'completed',
|
||||
currentLocation: {
|
||||
lat: 39.3434,
|
||||
lng: 117.3616,
|
||||
address: '天津市滨海新区',
|
||||
updateTime: '2024-01-16T19:45:00Z'
|
||||
},
|
||||
route: [
|
||||
{ lat: 40.8420, lng: 111.7520, time: '2024-01-16T06:15:00Z' },
|
||||
{ lat: 40.1420, lng: 114.7520, time: '2024-01-16T12:15:00Z' },
|
||||
{ lat: 39.3434, lng: 117.3616, time: '2024-01-16T19:45:00Z' }
|
||||
],
|
||||
cattleCount: 80,
|
||||
temperature: 15,
|
||||
humidity: 70,
|
||||
alerts: [
|
||||
{
|
||||
type: 'temperature',
|
||||
message: '车厢温度偏高',
|
||||
time: '2024-01-16T14:30:00Z',
|
||||
resolved: true
|
||||
}
|
||||
],
|
||||
createdAt: new Date('2024-01-16T06:00:00Z'),
|
||||
updatedAt: new Date('2024-01-16T19:45:00Z')
|
||||
}
|
||||
];
|
||||
|
||||
// 验证schemas
|
||||
const transportCreateSchema = Joi.object({
|
||||
orderId: Joi.number().integer().required(),
|
||||
driverName: Joi.string().min(2).max(50).required(),
|
||||
driverPhone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(),
|
||||
vehicleNumber: Joi.string().min(5).max(20).required(),
|
||||
vehicleType: Joi.string().min(2).max(50).required(),
|
||||
startLocation: Joi.string().min(5).max(200).required(),
|
||||
endLocation: Joi.string().min(5).max(200).required(),
|
||||
plannedDepartureTime: Joi.date().iso().required(),
|
||||
estimatedArrivalTime: Joi.date().iso().required(),
|
||||
distance: Joi.number().min(1).required(),
|
||||
cattleCount: Joi.number().integer().min(1).required()
|
||||
});
|
||||
|
||||
const locationUpdateSchema = Joi.object({
|
||||
lat: Joi.number().min(-90).max(90).required(),
|
||||
lng: Joi.number().min(-180).max(180).required(),
|
||||
address: Joi.string().max(200),
|
||||
temperature: Joi.number().min(-50).max(50),
|
||||
humidity: Joi.number().min(0).max(100)
|
||||
});
|
||||
|
||||
const statusUpdateSchema = Joi.object({
|
||||
status: Joi.string().valid('pending', 'loading', 'in_transit', 'arrived', 'completed', 'cancelled').required(),
|
||||
actualTime: Joi.date().iso()
|
||||
});
|
||||
|
||||
// 获取运输列表
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
keyword,
|
||||
status,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
let filteredTransports = [...transports];
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
filteredTransports = filteredTransports.filter(transport =>
|
||||
transport.transportCode.includes(keyword) ||
|
||||
transport.driverName.includes(keyword) ||
|
||||
transport.vehicleNumber.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredTransports = filteredTransports.filter(transport => transport.status === status);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startDate) {
|
||||
filteredTransports = filteredTransports.filter(transport =>
|
||||
new Date(transport.plannedDepartureTime) >= new Date(startDate)
|
||||
);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filteredTransports = filteredTransports.filter(transport =>
|
||||
new Date(transport.plannedDepartureTime) <= new Date(endDate)
|
||||
);
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + parseInt(pageSize);
|
||||
const paginatedTransports = filteredTransports.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: paginatedTransports,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: filteredTransports.length,
|
||||
totalPages: Math.ceil(filteredTransports.length / pageSize)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取运输详情
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const transport = transports.find(t => t.id === parseInt(id));
|
||||
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: transport
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 创建运输任务
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { error, value } = transportCreateSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const transportCode = `TRP${String(Date.now()).slice(-6)}`;
|
||||
|
||||
const newTransport = {
|
||||
id: Math.max(...transports.map(t => t.id)) + 1,
|
||||
...value,
|
||||
transportCode,
|
||||
actualDepartureTime: null,
|
||||
actualArrivalTime: null,
|
||||
status: 'pending',
|
||||
currentLocation: null,
|
||||
route: [],
|
||||
temperature: null,
|
||||
humidity: null,
|
||||
alerts: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
transports.push(newTransport);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '运输任务创建成功',
|
||||
data: newTransport
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建运输任务失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新位置信息
|
||||
router.post('/:id/location', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = locationUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const transportIndex = transports.findIndex(t => t.id === parseInt(id));
|
||||
if (transportIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
const locationData = {
|
||||
...value,
|
||||
updateTime: currentTime.toISOString()
|
||||
};
|
||||
|
||||
// 更新当前位置
|
||||
transports[transportIndex].currentLocation = locationData;
|
||||
|
||||
// 添加到路径轨迹
|
||||
transports[transportIndex].route.push({
|
||||
lat: value.lat,
|
||||
lng: value.lng,
|
||||
time: currentTime.toISOString()
|
||||
});
|
||||
|
||||
// 更新温度和湿度
|
||||
if (value.temperature !== undefined) {
|
||||
transports[transportIndex].temperature = value.temperature;
|
||||
}
|
||||
if (value.humidity !== undefined) {
|
||||
transports[transportIndex].humidity = value.humidity;
|
||||
}
|
||||
|
||||
transports[transportIndex].updatedAt = currentTime;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '位置更新成功',
|
||||
data: transports[transportIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新位置失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 更新运输状态
|
||||
router.put('/:id/status', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { error, value } = statusUpdateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
errors: error.details.map(detail => detail.message)
|
||||
});
|
||||
}
|
||||
|
||||
const transportIndex = transports.findIndex(t => t.id === parseInt(id));
|
||||
if (transportIndex === -1) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
transports[transportIndex].status = value.status;
|
||||
|
||||
// 根据状态更新实际时间
|
||||
if (value.status === 'in_transit' && !transports[transportIndex].actualDepartureTime) {
|
||||
transports[transportIndex].actualDepartureTime = value.actualTime || currentTime.toISOString();
|
||||
} else if (value.status === 'completed' && !transports[transportIndex].actualArrivalTime) {
|
||||
transports[transportIndex].actualArrivalTime = value.actualTime || currentTime.toISOString();
|
||||
}
|
||||
|
||||
transports[transportIndex].updatedAt = currentTime;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '状态更新成功',
|
||||
data: transports[transportIndex]
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取运输统计
|
||||
router.get('/stats/overview', (req, res) => {
|
||||
try {
|
||||
const totalTransports = transports.length;
|
||||
const inTransitCount = transports.filter(t => t.status === 'in_transit').length;
|
||||
const completedCount = transports.filter(t => t.status === 'completed').length;
|
||||
const pendingCount = transports.filter(t => t.status === 'pending').length;
|
||||
|
||||
// 平均运输时间(已完成的订单)
|
||||
const completedTransports = transports.filter(t => t.status === 'completed' && t.actualDepartureTime && t.actualArrivalTime);
|
||||
const averageTransitTime = completedTransports.length > 0
|
||||
? completedTransports.reduce((sum, t) => {
|
||||
const departureTime = new Date(t.actualDepartureTime);
|
||||
const arrivalTime = new Date(t.actualArrivalTime);
|
||||
return sum + (arrivalTime - departureTime);
|
||||
}, 0) / completedTransports.length / (1000 * 60 * 60) // 转换为小时
|
||||
: 0;
|
||||
|
||||
// 总运输距离
|
||||
const totalDistance = transports.reduce((sum, t) => sum + t.distance, 0);
|
||||
|
||||
// 总运输牲畜数量
|
||||
const totalCattleCount = transports.reduce((sum, t) => sum + t.cattleCount, 0);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalTransports,
|
||||
inTransitCount,
|
||||
completedCount,
|
||||
pendingCount,
|
||||
averageTransitTime: Math.round(averageTransitTime * 10) / 10,
|
||||
totalDistance,
|
||||
totalCattleCount
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实时运输地图数据
|
||||
router.get('/map/realtime', (req, res) => {
|
||||
try {
|
||||
const activeTransports = transports
|
||||
.filter(t => t.status === 'in_transit' && t.currentLocation)
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
transportCode: t.transportCode,
|
||||
driverName: t.driverName,
|
||||
vehicleNumber: t.vehicleNumber,
|
||||
currentLocation: t.currentLocation,
|
||||
destination: t.endLocation,
|
||||
cattleCount: t.cattleCount,
|
||||
estimatedArrivalTime: t.estimatedArrivalTime
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: activeTransports
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取实时地图数据失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取运输轨迹
|
||||
router.get('/:id/route', (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const transport = transports.find(t => t.id === parseInt(id));
|
||||
|
||||
if (!transport) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '运输记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
transportCode: transport.transportCode,
|
||||
startLocation: transport.startLocation,
|
||||
endLocation: transport.endLocation,
|
||||
route: transport.route,
|
||||
currentLocation: transport.currentLocation
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取运输轨迹失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
336
backend/routes/users.js
Normal file
336
backend/routes/users.js
Normal file
@@ -0,0 +1,336 @@
|
||||
const express = require('express')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const Joi = require('joi')
|
||||
const router = express.Router()
|
||||
|
||||
// 引入数据库模型
|
||||
const { ApiUser } = require('../models')
|
||||
const sequelize = require('sequelize')
|
||||
|
||||
// 验证模式
|
||||
const createUserSchema = Joi.object({
|
||||
username: Joi.string().min(2).max(50).required(),
|
||||
email: Joi.string().email().required(),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).allow(''),
|
||||
password: Joi.string().min(6).max(100).required(),
|
||||
user_type: Joi.string().valid('client', 'supplier', 'driver', 'staff', 'admin').required(),
|
||||
status: Joi.string().valid('active', 'inactive', 'locked').default('active')
|
||||
})
|
||||
|
||||
const updateUserSchema = Joi.object({
|
||||
username: Joi.string().min(2).max(50),
|
||||
email: Joi.string().email(),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).allow(''),
|
||||
user_type: Joi.string().valid('client', 'supplier', 'driver', 'staff', 'admin'),
|
||||
status: Joi.string().valid('active', 'inactive', 'locked')
|
||||
})
|
||||
|
||||
// 获取用户列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, keyword, user_type, status } = req.query
|
||||
|
||||
// 构建查询条件
|
||||
const where = {}
|
||||
if (keyword) {
|
||||
where[sequelize.Op.or] = [
|
||||
{ username: { [sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ email: { [sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ phone: { [sequelize.Op.like]: `%${keyword}%` } }
|
||||
]
|
||||
}
|
||||
if (user_type) where.user_type = user_type
|
||||
if (status) where.status = status
|
||||
|
||||
// 分页查询
|
||||
const result = await ApiUser.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(pageSize),
|
||||
offset: (parseInt(page) - 1) * parseInt(pageSize),
|
||||
order: [['createdAt', 'DESC']]
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: result.rows,
|
||||
total: result.count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
totalPages: Math.ceil(result.count / parseInt(pageSize))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户列表失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取用户详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const user = await ApiUser.findByPk(id)
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取用户详情失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取用户详情失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 创建用户
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// 参数验证
|
||||
const { error, value } = createUserSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
const { username, email, phone, password, user_type, status } = value
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await ApiUser.findOne({
|
||||
where: {
|
||||
[sequelize.Op.or]: [
|
||||
{ username: username },
|
||||
{ email: email },
|
||||
{ phone: phone }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名、邮箱或手机号已存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
const saltRounds = 10
|
||||
const password_hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
// 创建新用户
|
||||
const newUser = await ApiUser.create({
|
||||
username,
|
||||
email,
|
||||
phone: phone || '',
|
||||
password_hash,
|
||||
user_type,
|
||||
status,
|
||||
})
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '用户创建成功',
|
||||
data: newUser
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const user = await ApiUser.findByPk(id)
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
const { error, value } = updateUserSchema.validate(req.body)
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '参数验证失败',
|
||||
details: error.details[0].message
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
await user.update(value)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户更新成功',
|
||||
data: user
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除用户
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const user = await ApiUser.findByPk(id)
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
await user.destroy()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 批量删除用户
|
||||
router.delete('/batch', async (req, res) => {
|
||||
try {
|
||||
const { ids } = req.body
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供有效的用户ID列表'
|
||||
})
|
||||
}
|
||||
|
||||
await ApiUser.destroy({
|
||||
where: {
|
||||
id: ids
|
||||
}
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功删除 ${ids.length} 个用户`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('批量删除用户失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量删除用户失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 重置用户密码
|
||||
router.put('/:id/password', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { password } = req.body
|
||||
|
||||
const user = await ApiUser.findByPk(id)
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '密码长度不能少于6位'
|
||||
})
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
const saltRounds = 10
|
||||
const password_hash = await bcrypt.hash(password, saltRounds)
|
||||
|
||||
// 更新密码
|
||||
await user.update({ password_hash })
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码重置成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '重置密码失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户状态
|
||||
router.put('/:id/status', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { status } = req.body
|
||||
|
||||
const user = await ApiUser.findByPk(id)
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
})
|
||||
}
|
||||
|
||||
if (!['active', 'inactive', 'locked'].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '无效的用户状态'
|
||||
})
|
||||
}
|
||||
|
||||
await user.update({ status })
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户状态更新成功',
|
||||
data: user
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('更新用户状态失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新用户状态失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
203
docs/开发环境配置指南.md
Normal file
203
docs/开发环境配置指南.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 开发环境配置指南
|
||||
|
||||
## 🖥️ 系统要求
|
||||
|
||||
### 基础环境
|
||||
- **操作系统**:Windows 10+, macOS 10.15+, Ubuntu 18.04+
|
||||
- **Node.js**:>= 18.0.0
|
||||
- **npm**:>= 8.0.0
|
||||
- **Git**:>= 2.20.0
|
||||
|
||||
### 数据库环境
|
||||
- **MySQL**:>= 8.0
|
||||
- **Redis**:>= 7.0
|
||||
|
||||
### 开发工具
|
||||
- **IDE**:VS Code(推荐)/ WebStorm / HBuilderX
|
||||
- **浏览器**:Chrome 90+ (开发调试)
|
||||
- **微信开发者工具**:最新版本(小程序开发)
|
||||
|
||||
## 🔧 环境安装
|
||||
|
||||
### 1. Node.js 安装
|
||||
```bash
|
||||
# 使用 nvm 管理版本(推荐)
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
|
||||
# 或直接下载安装
|
||||
# https://nodejs.org/
|
||||
```
|
||||
|
||||
### 2. 数据库配置
|
||||
```bash
|
||||
# MySQL 8.0
|
||||
# 连接信息:
|
||||
HOST: 129.211.213.226
|
||||
PORT: 9527
|
||||
USERNAME: root
|
||||
PASSWORD: aiotAiot123!
|
||||
DATABASE: jiebandata
|
||||
|
||||
# Redis(本地开发)
|
||||
HOST: localhost
|
||||
PORT: 6379
|
||||
```
|
||||
|
||||
### 3. 开发工具安装
|
||||
```bash
|
||||
# VS Code 推荐插件
|
||||
ext install ms-vscode.vscode-typescript-next
|
||||
ext install Vue.volar
|
||||
ext install bradlc.vscode-tailwindcss
|
||||
ext install esbenp.prettier-vscode
|
||||
ext install ms-vscode.vscode-eslint
|
||||
```
|
||||
|
||||
## 🚀 项目启动
|
||||
|
||||
### 全局依赖安装
|
||||
```bash
|
||||
# 安装全局工具
|
||||
npm install -g @vue/cli
|
||||
npm install -g serve
|
||||
npm install -g pm2
|
||||
npm install -g sequelize-cli
|
||||
```
|
||||
|
||||
### 项目克隆和初始化
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
cd niumall
|
||||
|
||||
# 安装各模块依赖
|
||||
cd admin-system && npm install
|
||||
cd ../backend && npm install
|
||||
cd ../mini_program && npm install
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
|
||||
#### 1. 后端服务
|
||||
```bash
|
||||
cd backend
|
||||
cp .env.example .env.development
|
||||
# 编辑环境变量
|
||||
npm run dev
|
||||
# 服务运行在 http://localhost:3001
|
||||
```
|
||||
|
||||
#### 2. 管理后台
|
||||
```bash
|
||||
cd admin-system
|
||||
npm run dev
|
||||
# 服务运行在 http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. 官网
|
||||
```bash
|
||||
cd website
|
||||
python -m http.server 8080
|
||||
# 或 npx serve . -p 8080
|
||||
# 访问 http://localhost:8080
|
||||
```
|
||||
|
||||
#### 4. 小程序
|
||||
```bash
|
||||
cd mini_program/client-mp
|
||||
npm run dev:mp-weixin
|
||||
# 使用微信开发者工具打开 dist/dev/mp-weixin
|
||||
```
|
||||
|
||||
## 🔗 开发服务地址
|
||||
|
||||
| 服务 | 地址 | 说明 |
|
||||
|------|------|------|
|
||||
| 后端API | http://localhost:3001 | Express服务 |
|
||||
| 管理后台 | http://localhost:3000 | Vue3应用 |
|
||||
| 企业官网 | http://localhost:8080 | 静态网站 |
|
||||
| 小程序 | 微信开发者工具 | uni-app应用 |
|
||||
|
||||
## 🛠️ 开发工具配置
|
||||
|
||||
### VS Code 配置
|
||||
```json
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"eslint.format.enable": true,
|
||||
"vetur.validation.template": false
|
||||
}
|
||||
```
|
||||
|
||||
### Git 配置
|
||||
```bash
|
||||
# 配置用户信息
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.email "your.email@example.com"
|
||||
|
||||
# 配置提交模板
|
||||
git config --global commit.template .gitmessage
|
||||
```
|
||||
|
||||
## 🔧 常见问题解决
|
||||
|
||||
### Node.js 版本问题
|
||||
```bash
|
||||
# 切换Node版本
|
||||
nvm use 18
|
||||
npm install
|
||||
```
|
||||
|
||||
### 端口冲突
|
||||
```bash
|
||||
# 检查端口占用
|
||||
netstat -ano | findstr :3000
|
||||
# 杀死进程
|
||||
taskkill /PID <PID> /F
|
||||
```
|
||||
|
||||
### 数据库连接失败
|
||||
1. 检查数据库服务是否启动
|
||||
2. 验证连接参数
|
||||
3. 检查防火墙设置
|
||||
4. 确认网络连通性
|
||||
|
||||
### 依赖安装失败
|
||||
```bash
|
||||
# 清除缓存重新安装
|
||||
npm cache clean --force
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
## 📝 开发规范
|
||||
|
||||
### 代码提交规范
|
||||
```bash
|
||||
# 提交格式
|
||||
feat: 新功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
chore: 其他修改
|
||||
```
|
||||
|
||||
### 分支管理
|
||||
- `main`: 主分支,生产环境代码
|
||||
- `develop`: 开发分支,集成测试
|
||||
- `feature/*`: 功能分支
|
||||
- `hotfix/*`: 紧急修复分支
|
||||
|
||||
## 📧 技术支持
|
||||
|
||||
- **开发环境问题**:dev@niumall.com
|
||||
- **数据库相关**:db@niumall.com
|
||||
- **部署相关**:ops@niumall.com
|
||||
- **技术交流群**:微信群-前端技术交流
|
||||
226
docs/测试规范文档.md
Normal file
226
docs/测试规范文档.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 测试规范文档
|
||||
|
||||
## 📋 测试策略
|
||||
|
||||
### 测试层次
|
||||
- **单元测试**: 覆盖率 >= 80%
|
||||
- **集成测试**: 覆盖核心业务流程
|
||||
- **接口测试**: 覆盖所有API端点
|
||||
- **端到端测试**: 覆盖用户关键路径
|
||||
|
||||
### 测试框架
|
||||
- **后端**: Jest + Supertest
|
||||
- **前端**: Vitest + Vue Test Utils
|
||||
- **接口**: Postman + Newman
|
||||
- **性能**: JMeter
|
||||
|
||||
## 🧪 单元测试规范
|
||||
|
||||
### 测试文件命名
|
||||
```
|
||||
src/services/user.service.js -> tests/unit/services/user.service.test.js
|
||||
src/utils/validator.js -> tests/unit/utils/validator.test.js
|
||||
src/components/UserForm.vue -> tests/unit/components/UserForm.spec.ts
|
||||
```
|
||||
|
||||
### 测试结构
|
||||
```javascript
|
||||
describe('模块名称', () => {
|
||||
beforeEach(() => {
|
||||
// 测试前置操作
|
||||
});
|
||||
|
||||
describe('方法名称', () => {
|
||||
it('should 预期行为 when 条件', () => {
|
||||
// Arrange - 准备
|
||||
// Act - 执行
|
||||
// Assert - 断言
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 示例代码
|
||||
```javascript
|
||||
// 后端单元测试
|
||||
describe('UserService', () => {
|
||||
describe('createUser', () => {
|
||||
it('should create user successfully', async () => {
|
||||
const userData = { phone: '13800138000' };
|
||||
const mockUser = { id: 1, ...userData };
|
||||
User.create = jest.fn().mockResolvedValue(mockUser);
|
||||
|
||||
const result = await UserService.createUser(userData);
|
||||
|
||||
expect(User.create).toHaveBeenCalledWith(userData);
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 前端组件测试
|
||||
describe('UserForm', () => {
|
||||
it('validates required fields', async () => {
|
||||
const wrapper = mount(UserForm);
|
||||
await wrapper.find('form').trigger('submit');
|
||||
expect(wrapper.text()).toContain('请输入真实姓名');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🔗 接口测试规范
|
||||
|
||||
### Postman测试集合
|
||||
```javascript
|
||||
// 认证接口测试
|
||||
pm.test("登录成功", function () {
|
||||
pm.response.to.have.status(200);
|
||||
const response = pm.response.json();
|
||||
pm.expect(response.code).to.eql(200);
|
||||
pm.expect(response.data.token).to.not.be.empty;
|
||||
|
||||
// 保存token用于后续测试
|
||||
pm.environment.set("authToken", response.data.token);
|
||||
});
|
||||
|
||||
// 业务接口测试
|
||||
pm.test("创建订单成功", function () {
|
||||
pm.response.to.have.status(200);
|
||||
const response = pm.response.json();
|
||||
pm.expect(response.data.id).to.be.a('number');
|
||||
pm.environment.set("orderId", response.data.id);
|
||||
});
|
||||
```
|
||||
|
||||
### 自动化测试脚本
|
||||
```bash
|
||||
# 运行Postman测试
|
||||
newman run tests/api/niumall-api.postman_collection.json \
|
||||
-e tests/api/test-environment.json \
|
||||
--reporters cli,html \
|
||||
--reporter-html-export reports/api-test-report.html
|
||||
```
|
||||
|
||||
## 🎯 端到端测试
|
||||
|
||||
### 关键用户路径
|
||||
1. **用户注册登录流程**
|
||||
2. **订单创建到完成流程**
|
||||
3. **运输跟踪流程**
|
||||
4. **支付结算流程**
|
||||
|
||||
### Cypress测试示例
|
||||
```javascript
|
||||
describe('订单管理流程', () => {
|
||||
beforeEach(() => {
|
||||
cy.login('client'); // 自定义命令
|
||||
});
|
||||
|
||||
it('客户可以创建订单', () => {
|
||||
cy.visit('/orders/create');
|
||||
cy.get('[data-cy=supplier-select]').select('供应商A');
|
||||
cy.get('[data-cy=cattle-type]').type('肉牛');
|
||||
cy.get('[data-cy=quantity]').type('10');
|
||||
cy.get('[data-cy=submit-btn]').click();
|
||||
|
||||
cy.url().should('include', '/orders/');
|
||||
cy.contains('订单创建成功');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 📊 性能测试
|
||||
|
||||
### JMeter测试计划
|
||||
```xml
|
||||
<!-- 并发用户登录测试 -->
|
||||
<ThreadGroup>
|
||||
<elementProp name="ThreadGroup.arguments" elementType="Arguments"/>
|
||||
<stringProp name="ThreadGroup.num_threads">100</stringProp>
|
||||
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
|
||||
<stringProp name="ThreadGroup.duration">300</stringProp>
|
||||
</ThreadGroup>
|
||||
```
|
||||
|
||||
### 性能指标要求
|
||||
- **响应时间**: API < 200ms, 页面 < 3s
|
||||
- **并发用户**: 支持1000并发
|
||||
- **错误率**: < 0.1%
|
||||
- **TPS**: >= 500
|
||||
|
||||
## 🔍 质量检查
|
||||
|
||||
### 代码覆盖率
|
||||
```bash
|
||||
# 后端覆盖率
|
||||
npm run test:coverage
|
||||
|
||||
# 前端覆盖率
|
||||
npm run test:unit -- --coverage
|
||||
```
|
||||
|
||||
### 测试执行
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm test
|
||||
|
||||
# 运行单元测试
|
||||
npm run test:unit
|
||||
|
||||
# 运行集成测试
|
||||
npm run test:integration
|
||||
|
||||
# 运行E2E测试
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### CI/CD集成
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test Pipeline
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
```
|
||||
|
||||
## 📋 测试检查清单
|
||||
|
||||
### 提交前检查
|
||||
- [ ] 所有单元测试通过
|
||||
- [ ] 代码覆盖率 >= 80%
|
||||
- [ ] 集成测试通过
|
||||
- [ ] API测试通过
|
||||
- [ ] 无ESLint错误
|
||||
|
||||
### 发布前检查
|
||||
- [ ] 端到端测试通过
|
||||
- [ ] 性能测试达标
|
||||
- [ ] 安全扫描通过
|
||||
- [ ] 浏览器兼容性测试
|
||||
- [ ] 移动端适配测试
|
||||
|
||||
## 📞 测试支持
|
||||
|
||||
- **测试负责人**: test@niumall.com
|
||||
- **质量保证**: qa@niumall.com
|
||||
- **性能测试**: perf@niumall.com
|
||||
518
docs/部署和运维文档.md
Normal file
518
docs/部署和运维文档.md
Normal file
@@ -0,0 +1,518 @@
|
||||
# 部署和运维文档
|
||||
|
||||
## 🏗️ 部署架构
|
||||
|
||||
### 生产环境架构
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 用户访问 │
|
||||
└─────────────┘
|
||||
│
|
||||
┌─────────────┐
|
||||
│ CDN/负载均衡 │
|
||||
└─────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 官网服务 │ │ 管理后台 │ │ 小程序 │
|
||||
│ (Nginx) │ │ (Nginx) │ │ (CDN) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
┌─────────────┐
|
||||
│ API网关 │
|
||||
│ (Nginx) │
|
||||
└─────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 后端服务1 │ │ 后端服务2 │ │ 后端服务N │
|
||||
│ (PM2) │ │ (PM2) │ │ (PM2) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ MySQL │ │ Redis │ │ 文件存储 │
|
||||
│ (主从复制) │ │ (集群) │ │ (MinIO) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## 🚀 部署流程
|
||||
|
||||
### 1. 服务器准备
|
||||
|
||||
#### 基础环境
|
||||
```bash
|
||||
# 更新系统
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 安装基础软件
|
||||
sudo apt install -y nginx nodejs npm mysql-server redis-server git
|
||||
|
||||
# 安装Node.js 18
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# 安装PM2
|
||||
sudo npm install -g pm2
|
||||
```
|
||||
|
||||
#### 目录结构
|
||||
```bash
|
||||
# 创建项目目录
|
||||
sudo mkdir -p /var/www/niumall
|
||||
sudo mkdir -p /var/www/niumall/website
|
||||
sudo mkdir -p /var/www/niumall/admin
|
||||
sudo mkdir -p /var/www/niumall/backend
|
||||
sudo mkdir -p /var/www/niumall/logs
|
||||
sudo mkdir -p /var/www/niumall/uploads
|
||||
|
||||
# 设置权限
|
||||
sudo chown -R www-data:www-data /var/www/niumall
|
||||
sudo chmod -R 755 /var/www/niumall
|
||||
```
|
||||
|
||||
### 2. 数据库部署
|
||||
|
||||
#### MySQL配置
|
||||
```bash
|
||||
# 安全配置
|
||||
sudo mysql_secure_installation
|
||||
|
||||
# 创建数据库和用户
|
||||
mysql -u root -p
|
||||
```
|
||||
|
||||
```sql
|
||||
-- 创建数据库
|
||||
CREATE DATABASE jiebandata CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 创建用户
|
||||
CREATE USER 'niumall'@'localhost' IDENTIFIED BY 'your_secure_password';
|
||||
GRANT ALL PRIVILEGES ON jiebandata.* TO 'niumall'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
#### Redis配置
|
||||
```bash
|
||||
# 编辑Redis配置
|
||||
sudo vim /etc/redis/redis.conf
|
||||
|
||||
# 关键配置项
|
||||
bind 127.0.0.1
|
||||
port 6379
|
||||
requirepass your_redis_password
|
||||
maxmemory 2gb
|
||||
maxmemory-policy allkeys-lru
|
||||
|
||||
# 重启Redis
|
||||
sudo systemctl restart redis-server
|
||||
sudo systemctl enable redis-server
|
||||
```
|
||||
|
||||
### 3. 后端服务部署
|
||||
|
||||
#### 代码部署
|
||||
```bash
|
||||
# 克隆代码
|
||||
cd /var/www/niumall
|
||||
sudo git clone <repository-url> .
|
||||
|
||||
# 安装后端依赖
|
||||
cd backend
|
||||
sudo npm install --production
|
||||
|
||||
# 复制环境配置
|
||||
sudo cp .env.example .env.production
|
||||
sudo vim .env.production
|
||||
```
|
||||
|
||||
#### 环境配置
|
||||
```bash
|
||||
# .env.production
|
||||
NODE_ENV=production
|
||||
PORT=3001
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=jiebandata
|
||||
DB_USER=niumall
|
||||
DB_PASSWORD=your_secure_password
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
JWT_SECRET=your_jwt_secret_key
|
||||
```
|
||||
|
||||
#### PM2配置
|
||||
```javascript
|
||||
// ecosystem.config.js
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'niumall-backend',
|
||||
script: 'src/app.js',
|
||||
cwd: '/var/www/niumall/backend',
|
||||
instances: 'max',
|
||||
exec_mode: 'cluster',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3001
|
||||
},
|
||||
error_file: '/var/www/niumall/logs/backend-error.log',
|
||||
out_file: '/var/www/niumall/logs/backend-out.log',
|
||||
log_file: '/var/www/niumall/logs/backend.log',
|
||||
time: true
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 启动后端服务
|
||||
```bash
|
||||
# 数据库迁移
|
||||
cd /var/www/niumall/backend
|
||||
sudo npm run db:migrate
|
||||
sudo npm run db:seed
|
||||
|
||||
# 启动服务
|
||||
sudo pm2 start ecosystem.config.js
|
||||
sudo pm2 save
|
||||
sudo pm2 startup
|
||||
```
|
||||
|
||||
### 4. 前端部署
|
||||
|
||||
#### 管理后台构建
|
||||
```bash
|
||||
cd /var/www/niumall/admin-system
|
||||
sudo npm install
|
||||
sudo npm run build:prod
|
||||
|
||||
# 复制构建文件
|
||||
sudo cp -r dist/* /var/www/niumall/admin/
|
||||
```
|
||||
|
||||
#### 官网部署
|
||||
```bash
|
||||
# 直接复制静态文件
|
||||
sudo cp -r website/* /var/www/niumall/website/
|
||||
```
|
||||
|
||||
### 5. Nginx配置
|
||||
|
||||
#### 主配置文件
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/niumall
|
||||
server {
|
||||
listen 80;
|
||||
server_name niumall.com www.niumall.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name niumall.com www.niumall.com;
|
||||
|
||||
ssl_certificate /path/to/ssl/certificate.crt;
|
||||
ssl_certificate_key /path/to/ssl/private.key;
|
||||
|
||||
# 官网
|
||||
location / {
|
||||
root /var/www/niumall/website;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 管理后台
|
||||
location /admin {
|
||||
alias /var/www/niumall/admin;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
|
||||
# API接口
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 文件上传
|
||||
location /uploads {
|
||||
alias /var/www/niumall/uploads;
|
||||
expires 1M;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 启用配置
|
||||
```bash
|
||||
# 创建软链接
|
||||
sudo ln -s /etc/nginx/sites-available/niumall /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试配置
|
||||
sudo nginx -t
|
||||
|
||||
# 重启Nginx
|
||||
sudo systemctl restart nginx
|
||||
sudo systemctl enable nginx
|
||||
```
|
||||
|
||||
## 🔧 运维管理
|
||||
|
||||
### 1. 监控配置
|
||||
|
||||
#### 系统监控
|
||||
```bash
|
||||
# 安装监控工具
|
||||
sudo npm install -g pm2-logrotate
|
||||
sudo pm2 install pm2-server-monit
|
||||
|
||||
# 配置日志轮转
|
||||
sudo pm2 set pm2-logrotate:max_size 10M
|
||||
sudo pm2 set pm2-logrotate:retain 30
|
||||
```
|
||||
|
||||
#### 健康检查脚本
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# health-check.sh
|
||||
|
||||
# 检查后端服务
|
||||
if curl -f http://localhost:3001/api/health > /dev/null 2>&1; then
|
||||
echo "✓ Backend service is healthy"
|
||||
else
|
||||
echo "✗ Backend service is down"
|
||||
# 重启服务
|
||||
pm2 restart niumall-backend
|
||||
fi
|
||||
|
||||
# 检查数据库
|
||||
if mysqladmin ping -h localhost -u niumall -p'password' --silent; then
|
||||
echo "✓ MySQL is healthy"
|
||||
else
|
||||
echo "✗ MySQL is down"
|
||||
fi
|
||||
|
||||
# 检查Redis
|
||||
if redis-cli ping > /dev/null 2>&1; then
|
||||
echo "✓ Redis is healthy"
|
||||
else
|
||||
echo "✗ Redis is down"
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. 备份策略
|
||||
|
||||
#### 数据库备份
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/var/backups/niumall"
|
||||
|
||||
# 创建备份目录
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# MySQL备份
|
||||
mysqldump -u niumall -p'password' jiebandata > $BACKUP_DIR/mysql_$DATE.sql
|
||||
|
||||
# 压缩备份
|
||||
gzip $BACKUP_DIR/mysql_$DATE.sql
|
||||
|
||||
# 文件备份
|
||||
tar -czf $BACKUP_DIR/uploads_$DATE.tar.gz /var/www/niumall/uploads
|
||||
|
||||
# 清理老备份(保留30天)
|
||||
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete
|
||||
|
||||
echo "Backup completed: $DATE"
|
||||
```
|
||||
|
||||
#### 定时任务
|
||||
```bash
|
||||
# 编辑crontab
|
||||
sudo crontab -e
|
||||
|
||||
# 添加任务
|
||||
# 每日凌晨2点备份
|
||||
0 2 * * * /path/to/backup.sh
|
||||
|
||||
# 每小时健康检查
|
||||
0 * * * * /path/to/health-check.sh
|
||||
|
||||
# 每日凌晨重启PM2(可选)
|
||||
0 3 * * 0 pm2 restart all
|
||||
```
|
||||
|
||||
### 3. 日志管理
|
||||
|
||||
#### 日志配置
|
||||
```bash
|
||||
# 创建日志目录
|
||||
sudo mkdir -p /var/log/niumall
|
||||
|
||||
# 配置logrotate
|
||||
sudo vim /etc/logrotate.d/niumall
|
||||
```
|
||||
|
||||
```
|
||||
/var/log/niumall/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 www-data www-data
|
||||
}
|
||||
```
|
||||
|
||||
#### 日志查看命令
|
||||
```bash
|
||||
# 查看后端日志
|
||||
sudo pm2 logs niumall-backend
|
||||
|
||||
# 查看Nginx日志
|
||||
sudo tail -f /var/log/nginx/access.log
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
|
||||
# 查看系统日志
|
||||
sudo journalctl -u nginx
|
||||
sudo journalctl -u mysql
|
||||
```
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
#### 数据库优化
|
||||
```sql
|
||||
-- 查看慢查询
|
||||
SHOW VARIABLES LIKE 'slow_query_log';
|
||||
SET GLOBAL slow_query_log = 'ON';
|
||||
SET GLOBAL long_query_time = 1;
|
||||
|
||||
-- 分析查询性能
|
||||
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
CREATE INDEX idx_orders_created_at ON orders(created_at);
|
||||
```
|
||||
|
||||
#### Redis优化
|
||||
```bash
|
||||
# 监控Redis性能
|
||||
redis-cli --latency-history -i 1
|
||||
|
||||
# 查看内存使用
|
||||
redis-cli info memory
|
||||
|
||||
# 清理过期key
|
||||
redis-cli --scan --pattern "expired:*" | xargs redis-cli del
|
||||
```
|
||||
|
||||
## 🚨 故障处理
|
||||
|
||||
### 常见问题排查
|
||||
|
||||
#### 服务无法启动
|
||||
```bash
|
||||
# 检查端口占用
|
||||
sudo netstat -tlnp | grep :3001
|
||||
|
||||
# 检查进程状态
|
||||
sudo pm2 status
|
||||
|
||||
# 查看错误日志
|
||||
sudo pm2 logs niumall-backend --err
|
||||
|
||||
# 重启服务
|
||||
sudo pm2 restart niumall-backend
|
||||
```
|
||||
|
||||
#### 数据库连接失败
|
||||
```bash
|
||||
# 检查MySQL状态
|
||||
sudo systemctl status mysql
|
||||
|
||||
# 检查连接数
|
||||
mysql -u root -p -e "SHOW PROCESSLIST;"
|
||||
|
||||
# 重启MySQL
|
||||
sudo systemctl restart mysql
|
||||
```
|
||||
|
||||
#### 内存不足
|
||||
```bash
|
||||
# 查看内存使用
|
||||
free -h
|
||||
sudo ps aux --sort=-%mem | head
|
||||
|
||||
# 清理缓存
|
||||
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
|
||||
|
||||
# 重启占用内存大的进程
|
||||
sudo pm2 restart all
|
||||
```
|
||||
|
||||
### 紧急恢复流程
|
||||
|
||||
#### 数据库恢复
|
||||
```bash
|
||||
# 停止应用
|
||||
sudo pm2 stop all
|
||||
|
||||
# 恢复数据库
|
||||
mysql -u root -p jiebandata < /var/backups/niumall/mysql_20240120.sql
|
||||
|
||||
# 重启应用
|
||||
sudo pm2 start all
|
||||
```
|
||||
|
||||
#### 代码回滚
|
||||
```bash
|
||||
# 查看提交历史
|
||||
cd /var/www/niumall
|
||||
sudo git log --oneline -10
|
||||
|
||||
# 回滚到指定版本
|
||||
sudo git reset --hard <commit-hash>
|
||||
|
||||
# 重新部署
|
||||
cd backend && sudo npm run build
|
||||
sudo pm2 restart all
|
||||
```
|
||||
|
||||
## 📊 监控指标
|
||||
|
||||
### 关键指标
|
||||
- **服务可用性**: > 99.9%
|
||||
- **响应时间**: < 200ms (API), < 3s (页面)
|
||||
- **错误率**: < 0.1%
|
||||
- **CPU使用率**: < 70%
|
||||
- **内存使用率**: < 80%
|
||||
- **磁盘使用率**: < 85%
|
||||
|
||||
### 告警配置
|
||||
```bash
|
||||
# CPU使用率告警
|
||||
if [ $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1) > 70 ]; then
|
||||
echo "High CPU usage detected" | mail -s "Server Alert" admin@niumall.com
|
||||
fi
|
||||
|
||||
# 磁盘空间告警
|
||||
if [ $(df / | tail -1 | awk '{print $5}' | cut -d'%' -f1) > 85 ]; then
|
||||
echo "Low disk space" | mail -s "Storage Alert" admin@niumall.com
|
||||
fi
|
||||
```
|
||||
|
||||
## 📞 运维联系方式
|
||||
|
||||
- **运维负责人**: ops@niumall.com
|
||||
- **紧急联系**: +86 138-xxxx-xxxx
|
||||
- **技术支持**: tech@niumall.com
|
||||
- **监控告警**: alert@niumall.com
|
||||
@@ -1,37 +1,351 @@
|
||||
# Mini Program 微信小程序矩阵
|
||||
# Mini Program - 活牛采购智能数字化系统小程序矩阵
|
||||
|
||||
## 技术栈
|
||||
- 微信小程序原生开发
|
||||
- Taro多端框架(可选)
|
||||
- WeUI / Vant Weapp UI组件库
|
||||
## 📱 项目概述
|
||||
|
||||
活牛采购智能数字化系统小程序矩阵基于uni-app框架开发,支持微信小程序、支付宝小程序、H5等多端发布。系统包含四个独立的小程序应用,分别服务于不同角色用户,实现活牛采购全流程的移动端操作。
|
||||
|
||||
**核心特性:**
|
||||
- 🎯 **多角色支持**:客户端、供应商、司机、内部员工四大角色
|
||||
- 🔄 **跨平台兼容**:支持微信、支付宝、H5等多平台
|
||||
- 📡 **实时同步**:WebSocket实时数据更新
|
||||
- 📍 **位置服务**:GPS定位和轨迹跟踪
|
||||
- 📸 **媒体支持**:图片、视频上传和播放
|
||||
- 💼 **离线缓存**:关键数据本地缓存
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
| 类别 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| **跨端框架** | uni-app | ^3.0.0 | 跨平台开发框架 |
|
||||
| **前端框架** | Vue 3 | ^3.3.0 | 响应式前端框架 |
|
||||
| **开发语言** | TypeScript | ^5.0.0 | 类型安全开发 |
|
||||
| **状态管理** | Pinia | ^2.1.0 | 状态管理库 |
|
||||
| **UI组件库** | uni-ui | ^1.4.0 | uni-app官方组件库 |
|
||||
| **HTTP客户端** | uni.request | 内置 | 网络请求封装 |
|
||||
| **地图服务** | 腾讯地图 | latest | 位置和导航服务 |
|
||||
| **构建工具** | Vite | ^4.4.0 | 快速构建工具 |
|
||||
| **代码规范** | ESLint + Prettier | latest | 代码质量保证 |
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
mini_program/
|
||||
├── app-one/ # 小程序项目1
|
||||
│ ├── pages/ # 页面文件
|
||||
│ ├── components/ # 自定义组件
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── app.js # 小程序入口
|
||||
│ ├── app.json # 小程序配置
|
||||
│ └── app.wxss # 全局样式
|
||||
├── app-two/ # 小程序项目2
|
||||
├── common/ # 公共资源
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── utils/ # 公共工具
|
||||
│ └── api/ # 公共API
|
||||
├── docs/ # 开发文档
|
||||
└── README.md
|
||||
├── client-mp/ # 客户端小程序
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # 页面文件
|
||||
│ │ │ ├── index/ # 首页
|
||||
│ │ │ ├── order/ # 订单相关
|
||||
│ │ │ ├── profile/ # 个人中心
|
||||
│ │ │ └── auth/ # 认证相关
|
||||
│ │ ├── components/ # 组件
|
||||
│ │ ├── stores/ # 状态管理
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── api/ # API接口
|
||||
│ │ ├── static/ # 静态资源
|
||||
│ │ ├── App.vue # 应用入口
|
||||
│ │ └── main.ts # 主文件
|
||||
│ ├── manifest.json # 应用配置
|
||||
│ ├── pages.json # 页面配置
|
||||
│ └── package.json # 依赖配置
|
||||
├── supplier-mp/ # 供应商小程序
|
||||
│ └── ...
|
||||
├── driver-mp/ # 司机小程序
|
||||
│ └── ...
|
||||
├── staff-mp/ # 内部员工小程序
|
||||
│ └── ...
|
||||
├── common/ # 公共资源
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── utils/ # 公共工具
|
||||
│ ├── api/ # 公共API
|
||||
│ ├── types/ # 类型定义
|
||||
│ └── styles/ # 公共样式
|
||||
├── docs/ # 开发文档
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 小程序类型
|
||||
- 电商小程序
|
||||
- 工具类小程序
|
||||
- 内容类小程序
|
||||
- 服务类小程序
|
||||
## 🚀 快速开始
|
||||
|
||||
## 开发规范
|
||||
1. 遵循微信小程序开发规范
|
||||
2. 组件化开发
|
||||
3. 代码复用和模块化
|
||||
4. 性能优化
|
||||
### 环境要求
|
||||
- Node.js >= 16.0.0
|
||||
- HBuilderX >= 3.8.0 或 VS Code + uni-app插件
|
||||
- 微信开发者工具
|
||||
- 支付宝小程序开发工具(可选)
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd mini_program/client-mp
|
||||
npm install
|
||||
|
||||
# 安装其他小程序依赖
|
||||
cd ../supplier-mp && npm install
|
||||
cd ../driver-mp && npm install
|
||||
cd ../staff-mp && npm install
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
```bash
|
||||
# 客户端小程序
|
||||
cd client-mp
|
||||
npm run dev:mp-weixin # 微信小程序
|
||||
npm run dev:mp-alipay # 支付宝小程序
|
||||
npm run dev:h5 # H5
|
||||
|
||||
# 其他小程序类似
|
||||
```
|
||||
|
||||
### 构建发布
|
||||
```bash
|
||||
# 生产环境构建
|
||||
npm run build:mp-weixin
|
||||
npm run build:mp-alipay
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
## 📱 小程序功能模块
|
||||
|
||||
### 1. 客户端小程序 (Client MP)
|
||||
**目标用户**:采购人、采购企业
|
||||
**核心功能**:
|
||||
- 📋 **订单管理**:创建采购订单、查看订单状态
|
||||
- 🚛 **运输跟踪**:实时查看运输进度和位置
|
||||
- ✅ **验收确认**:到货验收、质量确认
|
||||
- 💰 **支付结算**:查看结算单、在线支付
|
||||
- 📊 **数据统计**:采购数据分析和报表
|
||||
|
||||
### 2. 供应商小程序 (Supplier MP)
|
||||
**目标用户**:牲畜供应商
|
||||
**核心功能**:
|
||||
- 📦 **订单接收**:接收和确认采购订单
|
||||
- 🐄 **牲畜管理**:牲畜信息录入和管理
|
||||
- 📋 **检疫证明**:上传检疫合格证明
|
||||
- 🚚 **装车管理**:装车过程记录和监控
|
||||
- 📸 **现场拍照**:装车现场照片和视频
|
||||
|
||||
### 3. 司机小程序 (Driver MP)
|
||||
**目标用户**:运输司机
|
||||
**核心功能**:
|
||||
- 🚛 **车辆登记**:车辆信息和证件管理
|
||||
- 📍 **位置上报**:实时GPS位置上报
|
||||
- 📱 **状态更新**:运输状态实时更新
|
||||
- 🎥 **视频上报**:运输过程视频记录
|
||||
- 📄 **单据管理**:运输单据电子化管理
|
||||
|
||||
### 4. 内部员工小程序 (Staff MP)
|
||||
**目标用户**:企业内部员工
|
||||
**核心功能**:
|
||||
- 👥 **用户管理**:用户信息查看和管理
|
||||
- 📊 **数据监控**:业务数据实时监控
|
||||
- ⚠️ **异常处理**:异常情况处理和跟进
|
||||
- 📋 **审核管理**:订单和流程审核
|
||||
- 📈 **统计报表**:业务统计和分析
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 命名规范
|
||||
- **页面文件**:kebab-case(如 `order-list.vue`)
|
||||
- **组件名**:PascalCase(如 `OrderCard.vue`)
|
||||
- **变量名**:camelCase(如 `orderInfo`)
|
||||
- **常量名**:UPPER_SNAKE_CASE(如 `API_BASE_URL`)
|
||||
|
||||
### 目录规范
|
||||
- `pages/`:页面文件,按功能模块分组
|
||||
- `components/`:组件文件,按类型分组
|
||||
- `stores/`:状态管理,按业务模块分组
|
||||
- `api/`:接口定义,按业务模块分组
|
||||
- `utils/`:工具函数,按功能分组
|
||||
|
||||
### 代码规范
|
||||
- 使用TypeScript进行类型约束
|
||||
- 使用Composition API编写组件
|
||||
- 遵循ESLint和Prettier规范
|
||||
- 组件props必须定义类型
|
||||
- 使用defineEmits定义事件
|
||||
|
||||
## 🌐 API接口
|
||||
|
||||
### 接口配置
|
||||
```typescript
|
||||
// 环境配置
|
||||
const config = {
|
||||
development: {
|
||||
baseURL: 'http://localhost:3001/api',
|
||||
wsURL: 'ws://localhost:3001'
|
||||
},
|
||||
production: {
|
||||
baseURL: 'https://api.niumall.com/api',
|
||||
wsURL: 'wss://api.niumall.com'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 请求封装
|
||||
```typescript
|
||||
// 统一请求封装
|
||||
interface RequestOptions {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
data?: any
|
||||
header?: Record<string, string>
|
||||
}
|
||||
|
||||
const request = (options: RequestOptions) => {
|
||||
return uni.request({
|
||||
...options,
|
||||
header: {
|
||||
'Authorization': `Bearer ${getToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options.header
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 📍 位置服务
|
||||
|
||||
### GPS定位
|
||||
```typescript
|
||||
// 获取当前位置
|
||||
const getCurrentLocation = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 实时位置上报
|
||||
const startLocationTracking = () => {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const location = await getCurrentLocation()
|
||||
await reportLocation(location)
|
||||
} catch (error) {
|
||||
console.error('位置上报失败:', error)
|
||||
}
|
||||
}, 10000) // 每10秒上报一次
|
||||
}
|
||||
```
|
||||
|
||||
## 📸 媒体处理
|
||||
|
||||
### 图片上传
|
||||
```typescript
|
||||
// 选择并上传图片
|
||||
const uploadImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 9,
|
||||
success: (res) => {
|
||||
res.tempFilePaths.forEach(filePath => {
|
||||
uni.uploadFile({
|
||||
url: `${config.baseURL}/upload`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
'Authorization': `Bearer ${getToken()}`
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
console.log('上传成功:', uploadRes)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 💾 数据缓存
|
||||
|
||||
### 本地存储
|
||||
```typescript
|
||||
// 缓存管理
|
||||
class CacheManager {
|
||||
static set(key: string, value: any, expire?: number) {
|
||||
const data = {
|
||||
value,
|
||||
expire: expire ? Date.now() + expire : null
|
||||
}
|
||||
uni.setStorageSync(key, JSON.stringify(data))
|
||||
}
|
||||
|
||||
static get(key: string) {
|
||||
try {
|
||||
const data = JSON.parse(uni.getStorageSync(key))
|
||||
if (data.expire && Date.now() > data.expire) {
|
||||
this.remove(key)
|
||||
return null
|
||||
}
|
||||
return data.value
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static remove(key: string) {
|
||||
uni.removeStorageSync(key)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 权限管理
|
||||
|
||||
### 登录状态管理
|
||||
```typescript
|
||||
// 登录状态检查
|
||||
const checkAuth = () => {
|
||||
const token = CacheManager.get('token')
|
||||
if (!token) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/auth/login'
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 页面访问权限
|
||||
const checkPagePermission = (userRole: string, requiredRole: string[]) => {
|
||||
return requiredRole.includes(userRole)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
### 单元测试
|
||||
- 工具函数测试
|
||||
- 组件功能测试
|
||||
- API接口测试
|
||||
- 状态管理测试
|
||||
|
||||
### 真机测试
|
||||
- 多设备兼容性测试
|
||||
- 网络环境测试
|
||||
- 性能压力测试
|
||||
- 用户体验测试
|
||||
|
||||
## 📦 发布部署
|
||||
|
||||
### 微信小程序发布
|
||||
1. 使用微信开发者工具打开项目
|
||||
2. 点击「上传」按钮上传代码
|
||||
3. 登录微信公众平台提交审核
|
||||
4. 审核通过后发布上线
|
||||
|
||||
### 支付宝小程序发布
|
||||
1. 使用支付宝小程序开发工具打开项目
|
||||
2. 点击「上传」按钮上传代码
|
||||
3. 登录支付宝开放平台提交审核
|
||||
4. 审核通过后发布上线
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. **Fork** 仓库
|
||||
2. **创建**特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. **提交**更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. **推送**到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. **开启** Pull Request
|
||||
|
||||
---
|
||||
|
||||
**🎯 让移动办公更便捷,让业务管理更高效!**
|
||||
84
website/MAINTENANCE.md
Normal file
84
website/MAINTENANCE.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Website 维护指南
|
||||
|
||||
## 📁 文件结构
|
||||
```
|
||||
website/
|
||||
├── css/custom.css # 自定义样式
|
||||
├── js/main.js # 主要脚本
|
||||
├── images/ # 图片资源
|
||||
├── index.html # 首页
|
||||
├── product.html # 产品介绍
|
||||
├── solutions.html # 解决方案
|
||||
├── cases.html # 客户案例
|
||||
├── news.html # 新闻动态
|
||||
├── about.html # 关于我们
|
||||
└── contact.html # 联系我们
|
||||
```
|
||||
|
||||
## 🔧 维护任务
|
||||
|
||||
### 内容更新
|
||||
1. **新闻更新**:编辑 `news.html`,添加新内容
|
||||
2. **产品信息**:更新 `product.html` 产品功能
|
||||
3. **客户案例**:在 `cases.html` 添加成功案例
|
||||
4. **联系信息**:修改 `contact.html` 联系方式
|
||||
|
||||
### 图片管理
|
||||
- 使用WebP格式优化加载速度
|
||||
- 图片大小控制在500KB以内
|
||||
- 添加适当的alt属性
|
||||
|
||||
### SEO优化
|
||||
- 更新页面title和meta描述
|
||||
- 检查关键词密度(2-3%)
|
||||
- 保持H标签层次结构
|
||||
- 添加结构化数据标记
|
||||
|
||||
## 🚀 部署流程
|
||||
|
||||
### 本地测试
|
||||
```bash
|
||||
# Python服务器
|
||||
python -m http.server 8080
|
||||
|
||||
# Node.js服务器
|
||||
npx serve . -p 8080
|
||||
```
|
||||
|
||||
### 生产部署
|
||||
```bash
|
||||
# 上传文件到服务器
|
||||
scp -r website/* user@server:/var/www/html/
|
||||
|
||||
# 检查文件权限
|
||||
chmod -R 644 /var/www/html/*
|
||||
chmod 755 /var/www/html/
|
||||
```
|
||||
|
||||
## 📊 性能监控
|
||||
|
||||
### 关键指标
|
||||
- 页面加载时间 < 3秒
|
||||
- 首屏渲染时间 < 1.5秒
|
||||
- Google PageSpeed 分数 > 90
|
||||
|
||||
### 优化建议
|
||||
- 启用Gzip压缩
|
||||
- 配置浏览器缓存
|
||||
- 使用CDN加速
|
||||
- 图片懒加载
|
||||
|
||||
## 🔍 SEO检查清单
|
||||
|
||||
- [ ] 页面标题包含目标关键词
|
||||
- [ ] Meta描述控制在150字符内
|
||||
- [ ] 图片添加alt属性
|
||||
- [ ] 内链和外链正常
|
||||
- [ ] 移动端适配良好
|
||||
- [ ] 页面加载速度优化
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
- **前端开发**:frontend@niumall.com
|
||||
- **SEO优化**:seo@niumall.com
|
||||
- **服务器运维**:ops@niumall.com
|
||||
656
website/api.html
Normal file
656
website/api.html
Normal file
@@ -0,0 +1,656 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API接口文档 - 活牛采购智能数字化系统</title>
|
||||
<meta name="description" content="活牛采购智能数字化系统API接口文档,提供完整的接口说明和示例代码。">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome Icons -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Prism.js 代码高亮 -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
||||
<!-- 自定义样式 -->
|
||||
<link href="css/custom.css" rel="stylesheet">
|
||||
<link href="css/responsive.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
.api-container {
|
||||
background: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.api-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4rem 0;
|
||||
margin-top: -100px;
|
||||
padding-top: 150px;
|
||||
}
|
||||
|
||||
.api-sidebar {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
position: sticky;
|
||||
top: 120px;
|
||||
max-height: calc(100vh - 140px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.api-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.api-nav-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0.25rem 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.api-nav-item:hover {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: var(--primary-color);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.api-nav-item.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.endpoint-card {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.endpoint-header {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.method-get { background: #28a745; color: white; }
|
||||
.method-post { background: #007bff; color: white; }
|
||||
.method-put { background: #ffc107; color: black; }
|
||||
.method-delete { background: #dc3545; color: white; }
|
||||
|
||||
.endpoint-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.param-table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.param-table th {
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.param-table td {
|
||||
border: 1px solid #e9ecef;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.response-example {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding: 1rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.api-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.api-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="index.html">
|
||||
<div class="logo-container me-2">
|
||||
<i class="fas fa-cow text-primary fs-2"></i>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-name fw-bold">NiuMall</div>
|
||||
<div class="brand-subtitle">活牛采购智能系统</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a href="index.html" class="nav-link">
|
||||
<i class="fas fa-home me-1"></i>返回首页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- API文档页面 -->
|
||||
<div class="api-container">
|
||||
<!-- 页面标题 -->
|
||||
<section class="api-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3">API接口文档</h1>
|
||||
<p class="lead">完整的REST API接口文档,支持开发者快速集成活牛采购系统</p>
|
||||
<div class="d-flex gap-3 mt-4">
|
||||
<div class="badge bg-primary fs-6 px-3 py-2">
|
||||
<i class="fas fa-code me-2"></i>RESTful API
|
||||
</div>
|
||||
<div class="badge bg-success fs-6 px-3 py-2">
|
||||
<i class="fas fa-shield-alt me-2"></i>JWT认证
|
||||
</div>
|
||||
<div class="badge bg-info fs-6 px-3 py-2">
|
||||
<i class="fas fa-database me-2"></i>JSON格式
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 text-center">
|
||||
<div class="api-info">
|
||||
<h5 class="mb-3">API信息</h5>
|
||||
<p><strong>基础URL:</strong><br>https://api.niumall.com/v1</p>
|
||||
<p><strong>版本:</strong> v1.0</p>
|
||||
<p><strong>认证:</strong> Bearer Token</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API文档内容 -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="col-lg-3">
|
||||
<div class="api-sidebar">
|
||||
<h5 class="mb-3">接口分类</h5>
|
||||
<div class="api-nav">
|
||||
<a href="#overview" class="api-nav-item active" onclick="showSection('overview')">
|
||||
<i class="fas fa-info-circle me-2"></i>概述
|
||||
</a>
|
||||
<a href="#auth" class="api-nav-item" onclick="showSection('auth')">
|
||||
<i class="fas fa-key me-2"></i>认证授权
|
||||
</a>
|
||||
<a href="#users" class="api-nav-item" onclick="showSection('users')">
|
||||
<i class="fas fa-users me-2"></i>用户管理
|
||||
</a>
|
||||
<a href="#orders" class="api-nav-item" onclick="showSection('orders')">
|
||||
<i class="fas fa-shopping-cart me-2"></i>订单管理
|
||||
</a>
|
||||
<a href="#suppliers" class="api-nav-item" onclick="showSection('suppliers')">
|
||||
<i class="fas fa-store me-2"></i>供应商管理
|
||||
</a>
|
||||
<a href="#transport" class="api-nav-item" onclick="showSection('transport')">
|
||||
<i class="fas fa-truck me-2"></i>运输管理
|
||||
</a>
|
||||
<a href="#finance" class="api-nav-item" onclick="showSection('finance')">
|
||||
<i class="fas fa-chart-line me-2"></i>财务管理
|
||||
</a>
|
||||
<a href="#errors" class="api-nav-item" onclick="showSection('errors')">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>错误处理
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API文档内容区域 -->
|
||||
<div class="col-lg-9">
|
||||
<div class="api-content">
|
||||
|
||||
<!-- 概述 -->
|
||||
<div id="overview" class="api-section active">
|
||||
<h3 class="mb-4">API概述</h3>
|
||||
<p class="text-muted mb-4">活牛采购智能数字化系统提供完整的REST API接口,支持开发者快速集成和自定义开发。</p>
|
||||
|
||||
<h5>快速开始</h5>
|
||||
<p>所有API请求都需要使用HTTPS协议,并在请求头中包含有效的JWT Token。</p>
|
||||
|
||||
<div class="code-block position-relative">
|
||||
<button class="copy-btn" onclick="copyCode(this)">复制</button>
|
||||
<pre><code class="language-bash">curl -X GET "https://api.niumall.com/v1/orders" \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json"</code></pre>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-4">响应格式</h5>
|
||||
<p>所有API响应都使用JSON格式,并遵循统一的响应结构:</p>
|
||||
|
||||
<div class="response-example">
|
||||
<pre><code class="language-json">{
|
||||
"success": true,
|
||||
"data": {},
|
||||
"message": "操作成功",
|
||||
"timestamp": "2024-01-20T12:00:00Z"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 认证授权 -->
|
||||
<div id="auth" class="api-section">
|
||||
<h3 class="mb-4">认证授权</h3>
|
||||
|
||||
<div class="endpoint-card">
|
||||
<div class="endpoint-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="method-badge method-post">POST</span>
|
||||
<span class="ms-3 fw-bold">/auth/login</span>
|
||||
<span class="ms-auto text-muted">用户登录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="endpoint-content">
|
||||
<h6>请求参数</h6>
|
||||
<table class="table param-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>必填</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>username</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>用户名或手机号</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>password</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>密码</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 class="mt-3">请求示例</h6>
|
||||
<div class="code-block position-relative">
|
||||
<button class="copy-btn" onclick="copyCode(this)">复制</button>
|
||||
<pre><code class="language-json">{
|
||||
"username": "admin@example.com",
|
||||
"password": "password123"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-3">响应示例</h6>
|
||||
<div class="response-example">
|
||||
<pre><code class="language-json">{
|
||||
"success": true,
|
||||
"data": {
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"role": "admin"
|
||||
}
|
||||
},
|
||||
"message": "登录成功"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单管理 -->
|
||||
<div id="orders" class="api-section">
|
||||
<h3 class="mb-4">订单管理</h3>
|
||||
|
||||
<div class="endpoint-card">
|
||||
<div class="endpoint-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="method-badge method-get">GET</span>
|
||||
<span class="ms-3 fw-bold">/orders</span>
|
||||
<span class="ms-auto text-muted">获取订单列表</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="endpoint-content">
|
||||
<h6>查询参数</h6>
|
||||
<table class="table param-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>必填</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>page</td>
|
||||
<td>integer</td>
|
||||
<td>否</td>
|
||||
<td>页码,默认1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>limit</td>
|
||||
<td>integer</td>
|
||||
<td>否</td>
|
||||
<td>每页数量,默认20</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>status</td>
|
||||
<td>string</td>
|
||||
<td>否</td>
|
||||
<td>订单状态筛选</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 class="mt-3">响应示例</h6>
|
||||
<div class="response-example">
|
||||
<pre><code class="language-json">{
|
||||
"success": true,
|
||||
"data": {
|
||||
"orders": [
|
||||
{
|
||||
"id": 1,
|
||||
"order_no": "ORD20240120001",
|
||||
"supplier_id": 101,
|
||||
"supplier_name": "山东畜牧合作社",
|
||||
"cattle_count": 50,
|
||||
"total_weight": 25000,
|
||||
"unit_price": 28.5,
|
||||
"total_amount": 712500,
|
||||
"status": "shipping",
|
||||
"created_at": "2024-01-20T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"total_pages": 5,
|
||||
"total_count": 100
|
||||
}
|
||||
},
|
||||
"message": "获取成功"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-card">
|
||||
<div class="endpoint-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="method-badge method-post">POST</span>
|
||||
<span class="ms-3 fw-bold">/orders</span>
|
||||
<span class="ms-auto text-muted">创建新订单</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="endpoint-content">
|
||||
<h6>请求参数</h6>
|
||||
<table class="table param-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>必填</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>supplier_id</td>
|
||||
<td>integer</td>
|
||||
<td>是</td>
|
||||
<td>供应商ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cattle_breed</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>牛种类型</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>cattle_count</td>
|
||||
<td>integer</td>
|
||||
<td>是</td>
|
||||
<td>牛只数量</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>expected_weight</td>
|
||||
<td>decimal</td>
|
||||
<td>是</td>
|
||||
<td>预期重量(kg)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>unit_price</td>
|
||||
<td>decimal</td>
|
||||
<td>是</td>
|
||||
<td>单价(元/kg)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 class="mt-3">请求示例</h6>
|
||||
<div class="code-block position-relative">
|
||||
<button class="copy-btn" onclick="copyCode(this)">复制</button>
|
||||
<pre><code class="language-json">{
|
||||
"supplier_id": 101,
|
||||
"cattle_breed": "西门塔尔",
|
||||
"cattle_count": 30,
|
||||
"expected_weight": 15000,
|
||||
"unit_price": 28.5,
|
||||
"delivery_address": "北京市朝阳区xx养殖场",
|
||||
"expected_delivery_date": "2024-01-25"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误处理 -->
|
||||
<div id="errors" class="api-section">
|
||||
<h3 class="mb-4">错误处理</h3>
|
||||
|
||||
<p class="text-muted mb-4">API使用标准HTTP状态码来表示请求的成功或失败状态。</p>
|
||||
|
||||
<h5>HTTP状态码</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>状态码</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="badge bg-success">200</span></td>
|
||||
<td>请求成功</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge bg-primary">201</span></td>
|
||||
<td>创建成功</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge bg-warning">400</span></td>
|
||||
<td>请求参数错误</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge bg-danger">401</span></td>
|
||||
<td>未授权,需要登录</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge bg-danger">403</span></td>
|
||||
<td>禁止访问,权限不足</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge bg-danger">404</span></td>
|
||||
<td>资源不存在</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge bg-danger">500</span></td>
|
||||
<td>服务器内部错误</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5 class="mt-4">错误响应格式</h5>
|
||||
<div class="response-example">
|
||||
<pre><code class="language-json">{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "请求参数验证失败",
|
||||
"details": [
|
||||
{
|
||||
"field": "cattle_count",
|
||||
"message": "牛只数量必须大于0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"timestamp": "2024-01-20T12:00:00Z"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他部分省略,实际项目中会包含所有API接口 -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Prism.js 代码高亮 -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
<!-- 自定义脚本 -->
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<script>
|
||||
// 显示指定的API部分
|
||||
function showSection(sectionId) {
|
||||
// 隐藏所有部分
|
||||
document.querySelectorAll('.api-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
});
|
||||
|
||||
// 移除所有导航项的active类
|
||||
document.querySelectorAll('.api-nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 显示选中的部分
|
||||
document.getElementById(sectionId).classList.add('active');
|
||||
|
||||
// 激活对应的导航项
|
||||
event.target.classList.add('active');
|
||||
|
||||
// 更新URL hash
|
||||
window.location.hash = sectionId;
|
||||
}
|
||||
|
||||
// 复制代码功能
|
||||
function copyCode(button) {
|
||||
const codeBlock = button.nextElementSibling;
|
||||
const code = codeBlock.textContent;
|
||||
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '已复制';
|
||||
button.style.background = 'rgba(40, 167, 69, 0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.style.background = 'rgba(255, 255, 255, 0.1)';
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时根据hash显示对应部分
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash && document.getElementById(hash)) {
|
||||
showSection(hash);
|
||||
}
|
||||
|
||||
// 初始化代码高亮
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
});
|
||||
|
||||
// 平滑滚动到锚点
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -32,11 +32,630 @@
|
||||
body {
|
||||
font-family: var(--font-family-base);
|
||||
line-height: 1.6;
|
||||
color: var(--text-light);
|
||||
background-color: var(--dark-color);
|
||||
color: var(--text-dark);
|
||||
background-color: #ffffff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 顶部联系栏 */
|
||||
.top-bar {
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-bar a:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* 导航栏样式增强 */
|
||||
.navbar {
|
||||
padding: 0.5rem 0;
|
||||
transition: var(--transition);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 品牌logo样式 */
|
||||
.logo-container {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.brand-text .brand-name {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.brand-text .brand-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(248, 249, 250, 0.8);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 导航链接增强 */
|
||||
.navbar-nav .nav-link {
|
||||
font-weight: 500;
|
||||
color: var(--text-light) !important;
|
||||
transition: var(--transition);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link.active {
|
||||
color: var(--primary-color) !important;
|
||||
background: rgba(76, 175, 80, 0.15);
|
||||
box-shadow: 0 2px 10px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
/* 下拉菜单 */
|
||||
.dropdown-menu {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
padding: 0.5rem 0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: var(--transition);
|
||||
border-radius: 8px;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: var(--primary-color);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
/* 英雄区域现代化设计 */
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-section .container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 英雄区域动画元素 */
|
||||
.hero-bg-animation {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floating-element {
|
||||
position: absolute;
|
||||
font-size: 2rem;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
animation-delay: var(--delay);
|
||||
left: var(--x);
|
||||
top: var(--y);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(5deg); }
|
||||
}
|
||||
|
||||
/* 英雄内容样式 */
|
||||
.hero-content {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hero-badge .badge {
|
||||
border-radius: 25px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* 英雄区域功能特性 */
|
||||
.hero-features .feature-item {
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.hero-features .feature-item:hover {
|
||||
transform: translateY(-5px);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 按钮增强样式 */
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0.875rem 2rem;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, var(--primary-light), var(--primary-color));
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.6);
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-3px);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 英雄视觉区域 */
|
||||
.hero-visual {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.hero-image-container {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.dashboard-mockup {
|
||||
transform: rotateY(-5deg) rotateX(5deg);
|
||||
transition: var(--transition);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-mockup:hover {
|
||||
transform: rotateY(0deg) rotateX(0deg) scale(1.02);
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
animation: floatCard 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.floating-card:nth-child(2) {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
@keyframes floatCard {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
/* 滚动指示器 */
|
||||
.scroll-indicator {
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-10px); }
|
||||
60% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
/* 统计数据样式 */
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 响应式设计优化 */
|
||||
@media (max-width: 768px) {
|
||||
.top-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.hero-actions .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-mockup {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.hero-features .col-6 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-stats .col-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.brand-text .brand-name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.brand-text .brand-subtitle {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 工具类 */
|
||||
.text-shadow {
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.backdrop-blur {
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.2em solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spinner-border 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 动画效果增强 */
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 图片优化 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 返回顶部按钮 */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.6);
|
||||
}
|
||||
|
||||
/* 导航栏动画 */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮点击效果 */
|
||||
.btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 卡片悬停增强 */
|
||||
.card {
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress {
|
||||
height: 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
||||
}
|
||||
|
||||
/* 数字统计增强 */
|
||||
.count-up {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
text-shadow: 0 2px 4px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
/* 模态框动画 */
|
||||
.modal-dialog {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 懒加载图片 */
|
||||
img.lazy {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
img.lazy.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 表单增强样式 */
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(76, 175, 80, 0.25);
|
||||
}
|
||||
|
||||
.form-control.is-valid {
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.form-control.is-invalid {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* 提示框样式 */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 打字机效果 */
|
||||
.typewriter {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 粒子效果容器 */
|
||||
.particles-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.back-to-top {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.floating-element {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面切换动画 */
|
||||
.page-transition {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.page-transition.loaded {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 高级动画效果 */
|
||||
@keyframes pulseGlow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(76, 175, 80, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-glow {
|
||||
animation: pulseGlow 2s infinite;
|
||||
}
|
||||
|
||||
/* 文字渐变效果 */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 毛玻璃效果 */
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 3D变换效果 */
|
||||
.transform-3d {
|
||||
transform-style: preserve-3d;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.transform-3d:hover {
|
||||
transform: rotateY(5deg) rotateX(5deg);
|
||||
}
|
||||
|
||||
/* 无障碍优化 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 焦点可见性 */
|
||||
.btn:focus-visible,
|
||||
.nav-link:focus-visible,
|
||||
.form-control:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.navbar,
|
||||
.back-to-top,
|
||||
.floating-element {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航栏样式 */
|
||||
.navbar {
|
||||
padding: 1rem 0;
|
||||
|
||||
438
website/css/responsive.css
Normal file
438
website/css/responsive.css
Normal file
@@ -0,0 +1,438 @@
|
||||
/* 移动端响应式优化样式 */
|
||||
|
||||
/* 移动设备优先设计 */
|
||||
@media (max-width: 767.98px) {
|
||||
/* 导航栏优化 */
|
||||
.navbar {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.navbar-brand .brand-text .brand-name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand .brand-text .brand-subtitle {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.25rem 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 英雄区域移动端优化 */
|
||||
.hero-section {
|
||||
min-height: 90vh;
|
||||
padding: 100px 0 50px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-features .col-6 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-features .feature-item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.hero-actions .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* 隐藏复杂动画元素 */
|
||||
.floating-element,
|
||||
.floating-card,
|
||||
.particles-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.dashboard-mockup {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* 统计数据移动端优化 */
|
||||
.hero-stats .col-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* 卡片组件优化 */
|
||||
.card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
/* 按钮优化 */
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
/* 表单优化 */
|
||||
.form-control {
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 模态框优化 */
|
||||
.modal-dialog {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
/* 演示页面移动端优化 */
|
||||
.demo-sidebar {
|
||||
margin-bottom: 2rem;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.demo-nav-item {
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板设备优化 */
|
||||
@media (min-width: 768px) and (max-width: 991.98px) {
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.hero-actions .btn {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.demo-sidebar {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕优化 */
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 触摸设备优化 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
/* 移除悬停效果 */
|
||||
.card:hover,
|
||||
.btn:hover,
|
||||
.nav-link:hover,
|
||||
.dashboard-card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: inherit !important;
|
||||
}
|
||||
|
||||
/* 增大点击区域 */
|
||||
.btn {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 优化表单控件 */
|
||||
.form-control,
|
||||
.form-select {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式 */
|
||||
@media (prefers-contrast: high) {
|
||||
.hero-section {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #000;
|
||||
border-color: #fff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 2px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--text-color: #ffffff;
|
||||
--card-bg: #1e1e1e;
|
||||
--border-color: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: rgba(0, 0, 0, 0.9) !important;
|
||||
}
|
||||
|
||||
.demo-sidebar,
|
||||
.demo-content,
|
||||
.dashboard-card {
|
||||
background-color: var(--card-bg);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画效果 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
.floating-element,
|
||||
.floating-card {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏模式优化 */
|
||||
@media (orientation: landscape) and (max-height: 500px) {
|
||||
.hero-section {
|
||||
min-height: 100vh;
|
||||
padding: 80px 0 20px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-features {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕设备 */
|
||||
@media (max-width: 575.98px) {
|
||||
.container {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 80px 0 30px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-features .col-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.hero-stats .col-4 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.hero-stats .col-4:nth-child(3) {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 2rem 0;
|
||||
padding-top: 120px;
|
||||
}
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.demo-nav-item {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 打印样式优化 */
|
||||
@media print {
|
||||
.navbar,
|
||||
.back-to-top,
|
||||
.floating-element,
|
||||
.demo-sidebar,
|
||||
.hero-bg-animation {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.demo-content,
|
||||
.dashboard-card {
|
||||
background: white !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.card {
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #ddd !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 可访问性增强 */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.smooth-scroll {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高分辨率屏幕优化 */
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
.logo-container i {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
}
|
||||
|
||||
/* 键盘导航优化 */
|
||||
.btn:focus-visible,
|
||||
.nav-link:focus-visible,
|
||||
.form-control:focus-visible,
|
||||
.demo-nav-item:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.25);
|
||||
}
|
||||
|
||||
/* 错误状态优化 */
|
||||
.is-invalid:focus {
|
||||
border-color: var(--danger-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
/* 成功状态优化 */
|
||||
.is-valid:focus {
|
||||
border-color: var(--success-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
473
website/demo.html
Normal file
473
website/demo.html
Normal file
@@ -0,0 +1,473 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>在线演示 - 活牛采购智能数字化系统</title>
|
||||
<meta name="description" content="体验活牛采购智能数字化系统的强大功能,包括供应商管理、运输跟踪、质量控制等核心模块。">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome Icons -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
||||
<!-- 自定义样式 -->
|
||||
<link href="css/custom.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
.demo-container {
|
||||
background: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4rem 0;
|
||||
margin-top: -100px;
|
||||
padding-top: 150px;
|
||||
}
|
||||
|
||||
.demo-sidebar {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
position: sticky;
|
||||
top: 120px;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.demo-nav-item {
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.25rem 0;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.demo-nav-item:hover {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.demo-nav-item.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.metric-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.process-step {
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
background: #f8f9fa;
|
||||
margin-bottom: 1rem;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.process-step.completed {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-left-color: var(--success-color);
|
||||
}
|
||||
|
||||
.process-step.active {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border-left-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="index.html">
|
||||
<div class="logo-container me-2">
|
||||
<i class="fas fa-cow text-primary fs-2"></i>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-name fw-bold">NiuMall</div>
|
||||
<div class="brand-subtitle">活牛采购智能系统</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a href="index.html" class="nav-link">
|
||||
<i class="fas fa-home me-1"></i>返回首页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 演示页面 -->
|
||||
<div class="demo-container">
|
||||
<!-- 页面标题 -->
|
||||
<section class="demo-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-3">系统在线演示</h1>
|
||||
<p class="lead">体验活牛采购智能数字化系统的完整功能,了解如何提升您的采购效率</p>
|
||||
</div>
|
||||
<div class="col-lg-4 text-center">
|
||||
<div class="demo-stats">
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<div class="stat-item text-center">
|
||||
<div class="stat-number count-up" data-target="1000">0</div>
|
||||
<small>活跃用户</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-item text-center">
|
||||
<div class="stat-number count-up" data-target="50000">0</div>
|
||||
<small>完成订单</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 演示内容 -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="col-lg-3">
|
||||
<div class="demo-sidebar">
|
||||
<h5 class="mb-3">功能模块</h5>
|
||||
<div class="demo-nav">
|
||||
<button class="demo-nav-item active" onclick="showDemo('dashboard')">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>
|
||||
数据驾驶舱
|
||||
</button>
|
||||
<button class="demo-nav-item" onclick="showDemo('orders')">
|
||||
<i class="fas fa-shopping-cart me-2"></i>
|
||||
订单管理
|
||||
</button>
|
||||
<button class="demo-nav-item" onclick="showDemo('suppliers')">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
供应商管理
|
||||
</button>
|
||||
<button class="demo-nav-item" onclick="showDemo('transport')">
|
||||
<i class="fas fa-truck me-2"></i>
|
||||
运输跟踪
|
||||
</button>
|
||||
<button class="demo-nav-item" onclick="showDemo('quality')">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
质量管理
|
||||
</button>
|
||||
<button class="demo-nav-item" onclick="showDemo('finance')">
|
||||
<i class="fas fa-chart-line me-2"></i>
|
||||
财务结算
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 演示内容区域 -->
|
||||
<div class="col-lg-9">
|
||||
<div class="demo-content">
|
||||
<!-- 数据驾驶舱 -->
|
||||
<div id="dashboard-demo" class="demo-section">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-tachometer-alt text-primary me-2"></i>
|
||||
数据驾驶舱
|
||||
</h3>
|
||||
|
||||
<!-- 关键指标 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="dashboard-card metric-card">
|
||||
<div class="metric-number count-up" data-target="156">0</div>
|
||||
<div class="metric-label">本月订单</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="dashboard-card metric-card">
|
||||
<div class="metric-number count-up" data-target="89" data-suffix="%">0%</div>
|
||||
<div class="metric-label">完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="dashboard-card metric-card">
|
||||
<div class="metric-number count-up" data-target="2356">0</div>
|
||||
<div class="metric-label">运输中</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="dashboard-card metric-card">
|
||||
<div class="metric-number count-up" data-target="98" data-suffix="%">0%</div>
|
||||
<div class="metric-label">质量合格率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="dashboard-card">
|
||||
<h5 class="mb-3">订单趋势分析</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="ordersChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="dashboard-card">
|
||||
<h5 class="mb-3">供应商分布</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="suppliersChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他演示模块 -->
|
||||
<div id="orders-demo" class="demo-section" style="display: none;">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-shopping-cart text-primary me-2"></i>
|
||||
订单管理系统
|
||||
</h3>
|
||||
<p class="text-muted mb-4">完整的订单生命周期管理,从创建到完成的全流程跟踪</p>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h5 class="mb-3">订单处理流程</h5>
|
||||
<div class="process-step completed">
|
||||
<strong>1. 订单创建</strong> - 采购人员创建采购需求
|
||||
<small class="text-success d-block">✓ 已完成</small>
|
||||
</div>
|
||||
<div class="process-step completed">
|
||||
<strong>2. 供应商确认</strong> - 供应商接受订单并确认交期
|
||||
<small class="text-success d-block">✓ 已完成</small>
|
||||
</div>
|
||||
<div class="process-step active">
|
||||
<strong>3. 质量检验</strong> - 牛只质量检验和证件审核
|
||||
<small class="text-warning d-block">⏳ 进行中</small>
|
||||
</div>
|
||||
<div class="process-step">
|
||||
<strong>4. 运输配送</strong> - 专业运输队伍配送
|
||||
<small class="text-muted d-block">○ 待执行</small>
|
||||
</div>
|
||||
<div class="process-step">
|
||||
<strong>5. 到货验收</strong> - 买方验收确认
|
||||
<small class="text-muted d-block">○ 待执行</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更多演示模块将在这里添加 -->
|
||||
<div id="suppliers-demo" class="demo-section" style="display: none;">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-users text-primary me-2"></i>
|
||||
供应商管理
|
||||
</h3>
|
||||
<p class="text-muted mb-4">全面的供应商资质管理和评估体系</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="dashboard-card text-center">
|
||||
<i class="fas fa-certificate text-primary fs-1 mb-3"></i>
|
||||
<h5>资质认证</h5>
|
||||
<p class="text-muted">营业执照、检疫证明等资质文件管理</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="dashboard-card text-center">
|
||||
<i class="fas fa-star text-warning fs-1 mb-3"></i>
|
||||
<h5>评级管理</h5>
|
||||
<p class="text-muted">基于历史交易的供应商信用评级</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="dashboard-card text-center">
|
||||
<i class="fas fa-chart-bar text-success fs-1 mb-3"></i>
|
||||
<h5>绩效分析</h5>
|
||||
<p class="text-muted">交付准时率、质量合格率统计</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 试用申请区域 -->
|
||||
<section class="py-5 bg-primary text-white">
|
||||
<div class="container text-center">
|
||||
<h2 class="mb-3">准备开始使用了吗?</h2>
|
||||
<p class="lead mb-4">立即申请免费试用,体验完整功能</p>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<form class="trial-form">
|
||||
<div class="input-group input-group-lg">
|
||||
<input type="email" class="form-control" placeholder="请输入您的邮箱地址" required>
|
||||
<button class="btn btn-light btn-lg" type="submit">
|
||||
<i class="fas fa-rocket me-2"></i>立即试用
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- 自定义脚本 -->
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<script>
|
||||
// 演示模块切换
|
||||
function showDemo(module) {
|
||||
// 隐藏所有演示模块
|
||||
document.querySelectorAll('.demo-section').forEach(section => {
|
||||
section.style.display = 'none';
|
||||
});
|
||||
|
||||
// 移除所有导航项的active类
|
||||
document.querySelectorAll('.demo-nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 显示选中的模块
|
||||
document.getElementById(module + '-demo').style.display = 'block';
|
||||
|
||||
// 激活对应的导航项
|
||||
event.target.classList.add('active');
|
||||
|
||||
// 重新初始化图表(如果是仪表盘)
|
||||
if (module === 'dashboard') {
|
||||
setTimeout(initCharts, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
function initCharts() {
|
||||
// 订单趋势图
|
||||
const ordersCtx = document.getElementById('ordersChart');
|
||||
if (ordersCtx) {
|
||||
new Chart(ordersCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
datasets: [{
|
||||
label: '订单数量',
|
||||
data: [120, 190, 300, 250, 200, 300],
|
||||
borderColor: '#4CAF50',
|
||||
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 供应商分布图
|
||||
const suppliersCtx = document.getElementById('suppliersChart');
|
||||
if (suppliersCtx) {
|
||||
new Chart(suppliersCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['华北', '华东', '华南', '西部'],
|
||||
datasets: [{
|
||||
data: [30, 45, 20, 15],
|
||||
backgroundColor: [
|
||||
'#4CAF50',
|
||||
'#2196F3',
|
||||
'#FF9800',
|
||||
'#9C27B0'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(initCharts, 500);
|
||||
|
||||
// 试用表单提交
|
||||
document.querySelector('.trial-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const email = this.querySelector('input[type="email"]').value;
|
||||
showAlert('success', '申请提交成功!我们会尽快与您联系。');
|
||||
this.reset();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
356
website/index-enhanced.html
Normal file
356
website/index-enhanced.html
Normal file
@@ -0,0 +1,356 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>活牛采购智能数字化系统 - 专业数字化采购解决方案</title>
|
||||
<meta name="description" content="专业的活牛采购数字化管理系统,提供从采购、运输到结算的全流程数字化解决方案,助力养殖企业提升采购效率。">
|
||||
<meta name="keywords" content="活牛采购,畜牧管理,数字化系统,SOP系统,农业数字化,养殖业">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome Icons -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- AOS Animation -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
|
||||
<!-- 自定义样式 -->
|
||||
<link href="css/custom.css" rel="stylesheet">
|
||||
<!-- 响应式样式 -->
|
||||
<link href="css/responsive.css" rel="stylesheet">
|
||||
|
||||
<!-- 结构化数据 -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "活牛采购智能数字化系统",
|
||||
"description": "专业的活牛采购数字化管理系统,提供从采购、运输到结算的全流程数字化解决方案",
|
||||
"applicationCategory": "BusinessApplication",
|
||||
"operatingSystem": "Web",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "CNY"
|
||||
},
|
||||
"provider": {
|
||||
"@type": "Organization",
|
||||
"name": "NiuMall",
|
||||
"url": "https://www.niumall.com"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 顶部联系栏 -->
|
||||
<div class="top-bar bg-primary text-white py-2 d-none d-md-block">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<small>
|
||||
<i class="fas fa-phone me-2"></i>咨询热线:400-xxx-xxxx
|
||||
<span class="mx-3">|</span>
|
||||
<i class="fas fa-envelope me-2"></i>邮箱:info@niumall.com
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<small>
|
||||
<a href="#" class="text-white text-decoration-none me-3">
|
||||
<i class="fab fa-weixin me-1"></i>微信
|
||||
</a>
|
||||
<a href="#" class="text-white text-decoration-none">
|
||||
<i class="fab fa-qq me-1"></i>QQ
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top shadow-sm" id="mainNavbar">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="index.html">
|
||||
<div class="logo-container me-2">
|
||||
<i class="fas fa-cow text-primary fs-2"></i>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-name fw-bold">NiuMall</div>
|
||||
<div class="brand-subtitle">活牛采购智能系统</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav mx-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="index.html">
|
||||
<i class="fas fa-home me-1"></i>首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-cube me-1"></i>产品中心
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="product.html">产品介绍</a></li>
|
||||
<li><a class="dropdown-item" href="solutions.html">解决方案</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="demo.html">在线演示</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="cases.html">
|
||||
<i class="fas fa-trophy me-1"></i>客户案例
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="news.html">
|
||||
<i class="fas fa-newspaper me-1"></i>新闻动态
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="about.html">
|
||||
<i class="fas fa-info-circle me-1"></i>关于我们
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="contact.html">
|
||||
<i class="fas fa-envelope me-1"></i>联系我们
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-actions d-flex gap-2">
|
||||
<a href="contact.html" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-phone me-1"></i>咨询
|
||||
</a>
|
||||
<a href="contact.html" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-rocket me-1"></i>免费试用
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 英雄区域 -->
|
||||
<section class="hero-section position-relative overflow-hidden" id="hero">
|
||||
<!-- 背景动画元素 -->
|
||||
<div class="hero-bg-animation">
|
||||
<div class="floating-element" style="--delay: 0s; --x: 10%; --y: 20%;">
|
||||
<i class="fas fa-cow text-primary opacity-25"></i>
|
||||
</div>
|
||||
<div class="floating-element" style="--delay: 1s; --x: 80%; --y: 10%;">
|
||||
<i class="fas fa-truck text-success opacity-25"></i>
|
||||
</div>
|
||||
<div class="floating-element" style="--delay: 2s; --x: 20%; --y: 80%;">
|
||||
<i class="fas fa-chart-line text-info opacity-25"></i>
|
||||
</div>
|
||||
<div class="floating-element" style="--delay: 3s; --x: 70%; --y: 70%;">
|
||||
<i class="fas fa-shield-alt text-warning opacity-25"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container position-relative">
|
||||
<div class="row align-items-center min-vh-100">
|
||||
<div class="col-lg-6">
|
||||
<div class="hero-content" data-aos="fade-right">
|
||||
<div class="hero-badge mb-3">
|
||||
<span class="badge bg-primary fs-6 px-3 py-2">
|
||||
<i class="fas fa-star me-2"></i>行业领先的数字化解决方案
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="hero-title display-3 fw-bold mb-4">
|
||||
数字化活牛采购
|
||||
<span class="text-primary">全流程管理</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-subtitle lead mb-4 text-muted">
|
||||
专业的SOP采购管理系统,从供应商筛选、订单管理、运输跟踪到财务结算,
|
||||
<strong class="text-primary">一站式解决</strong>活牛采购的所有难题
|
||||
</p>
|
||||
|
||||
<div class="hero-features mb-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="feature-item text-center">
|
||||
<i class="fas fa-users text-primary fs-4 mb-2"></i>
|
||||
<small class="d-block">供应商管理</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="feature-item text-center">
|
||||
<i class="fas fa-truck text-success fs-4 mb-2"></i>
|
||||
<small class="d-block">运输跟踪</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="feature-item text-center">
|
||||
<i class="fas fa-shield-alt text-warning fs-4 mb-2"></i>
|
||||
<small class="d-block">质量保证</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="feature-item text-center">
|
||||
<i class="fas fa-chart-line text-info fs-4 mb-2"></i>
|
||||
<small class="d-block">数据分析</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-actions d-flex flex-wrap gap-3">
|
||||
<a href="contact.html" class="btn btn-primary btn-lg px-4 py-3">
|
||||
<i class="fas fa-rocket me-2"></i>立即试用
|
||||
<small class="d-block mt-1">免费体验30天</small>
|
||||
</a>
|
||||
<a href="product.html" class="btn btn-outline-secondary btn-lg px-4 py-3">
|
||||
<i class="fas fa-play-circle me-2"></i>观看演示
|
||||
<small class="d-block mt-1">3分钟了解产品</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="hero-stats mt-5">
|
||||
<div class="row g-4">
|
||||
<div class="col-4">
|
||||
<div class="stat-item text-center">
|
||||
<div class="stat-number h4 fw-bold text-primary mb-1" data-target="500">0</div>
|
||||
<small class="text-muted">服务企业</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item text-center">
|
||||
<div class="stat-number h4 fw-bold text-success mb-1" data-target="99">0</div>
|
||||
<small class="text-muted">满意度%</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item text-center">
|
||||
<div class="stat-number h4 fw-bold text-warning mb-1" data-target="40">0</div>
|
||||
<small class="text-muted">效率提升%</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="hero-visual" data-aos="fade-left">
|
||||
<div class="hero-image-container position-relative">
|
||||
<!-- 主要展示图 -->
|
||||
<div class="main-visual">
|
||||
<div class="dashboard-mockup bg-white rounded-3 shadow-lg p-4">
|
||||
<div class="mockup-header d-flex align-items-center mb-3">
|
||||
<div class="d-flex gap-2">
|
||||
<div class="dot bg-danger rounded-circle" style="width: 12px; height: 12px;"></div>
|
||||
<div class="dot bg-warning rounded-circle" style="width: 12px; height: 12px;"></div>
|
||||
<div class="dot bg-success rounded-circle" style="width: 12px; height: 12px;"></div>
|
||||
</div>
|
||||
<div class="ms-3 text-muted small">活牛采购管理系统</div>
|
||||
</div>
|
||||
|
||||
<div class="mockup-content">
|
||||
<!-- 模拟仪表盘 -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6">
|
||||
<div class="card border-0 bg-primary bg-opacity-10">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-shopping-cart text-primary fs-4"></i>
|
||||
<div class="ms-3">
|
||||
<div class="h6 mb-0">156</div>
|
||||
<small class="text-muted">本月订单</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card border-0 bg-success bg-opacity-10">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-truck text-success fs-4"></i>
|
||||
<div class="ms-3">
|
||||
<div class="h6 mb-0">23</div>
|
||||
<small class="text-muted">运输中</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模拟图表 -->
|
||||
<div class="chart-area bg-light rounded p-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<small class="text-muted">采购趋势</small>
|
||||
<div class="progress" style="width: 100px; height: 4px;">
|
||||
<div class="progress-bar bg-primary" style="width: 75%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-chart d-flex align-items-end gap-1" style="height: 60px;">
|
||||
<div class="bar bg-primary rounded-top" style="height: 30%; width: 8px;"></div>
|
||||
<div class="bar bg-primary rounded-top" style="height: 60%; width: 8px;"></div>
|
||||
<div class="bar bg-primary rounded-top" style="height: 40%; width: 8px;"></div>
|
||||
<div class="bar bg-primary rounded-top" style="height: 80%; width: 8px;"></div>
|
||||
<div class="bar bg-primary rounded-top" style="height: 100%; width: 8px;"></div>
|
||||
<div class="bar bg-primary rounded-top" style="height: 70%; width: 8px;"></div>
|
||||
<div class="bar bg-primary rounded-top" style="height: 90%; width: 8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 浮动元素 -->
|
||||
<div class="floating-card position-absolute" style="top: -20px; right: -20px;">
|
||||
<div class="card border-0 shadow-sm" style="width: 200px;">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-bell text-warning fs-5"></i>
|
||||
<div class="ms-2">
|
||||
<div class="small fw-bold">新订单通知</div>
|
||||
<div class="tiny text-muted">2分钟前</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="floating-card position-absolute" style="bottom: -20px; left: -20px;">
|
||||
<div class="card border-0 shadow-sm" style="width: 180px;">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle text-success fs-5"></i>
|
||||
<div class="ms-2">
|
||||
<div class="small fw-bold">运输完成</div>
|
||||
<div class="tiny text-muted">订单#12345</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动提示 -->
|
||||
<div class="scroll-indicator position-absolute bottom-0 start-50 translate-middle-x mb-4">
|
||||
<div class="text-center">
|
||||
<small class="text-muted d-block mb-2">了解更多</small>
|
||||
<i class="fas fa-chevron-down text-primary animate-bounce"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 其他部分保持不变... -->
|
||||
@@ -9,6 +9,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
initBackToTop();
|
||||
initImageLazyLoading();
|
||||
initFormValidation();
|
||||
initParticleEffect();
|
||||
initTypewriter();
|
||||
initProgressBars();
|
||||
initCountUp();
|
||||
initModalEffects();
|
||||
initSmoothScrolling();
|
||||
initPreloader();
|
||||
|
||||
console.log('官网初始化完成 - 活牛采购智能数字化系统');
|
||||
});
|
||||
@@ -17,23 +24,47 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function initNavigation() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
const navbarToggler = document.querySelector('.navbar-toggler');
|
||||
const navbarCollapse = document.querySelector('.navbar-collapse');
|
||||
|
||||
// 滚动时导航栏样式变化
|
||||
window.addEventListener('scroll', function() {
|
||||
window.addEventListener('scroll', throttle(function() {
|
||||
if (window.scrollY > 100) {
|
||||
navbar.classList.add('navbar-scrolled');
|
||||
navbar.style.backgroundColor = 'rgba(255, 255, 255, 0.95)';
|
||||
navbar.style.boxShadow = '0 2px 20px rgba(0, 0, 0, 0.1)';
|
||||
navbar.classList.add('navbar-scrolled', 'scrolled');
|
||||
navbar.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';
|
||||
navbar.style.boxShadow = '0 2px 20px rgba(0, 0, 0, 0.3)';
|
||||
navbar.style.backdropFilter = 'blur(10px)';
|
||||
} else {
|
||||
navbar.classList.remove('navbar-scrolled');
|
||||
navbar.style.backgroundColor = 'rgba(255, 255, 255, 1)';
|
||||
navbar.classList.remove('navbar-scrolled', 'scrolled');
|
||||
navbar.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
navbar.style.boxShadow = 'none';
|
||||
}
|
||||
});
|
||||
}, 100));
|
||||
|
||||
// 移动端导航切换
|
||||
if (navbarToggler && navbarCollapse) {
|
||||
navbarToggler.addEventListener('click', function() {
|
||||
navbarCollapse.classList.toggle('show');
|
||||
this.classList.toggle('collapsed');
|
||||
|
||||
// 动画效果
|
||||
if (navbarCollapse.classList.contains('show')) {
|
||||
navbarCollapse.style.animation = 'slideDown 0.3s ease-out';
|
||||
} else {
|
||||
navbarCollapse.style.animation = 'slideUp 0.3s ease-out';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导航链接点击效果
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
// 添加点击动画
|
||||
this.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
|
||||
// 移除所有active类
|
||||
navLinks.forEach(l => l.classList.remove('active'));
|
||||
// 添加当前active类
|
||||
@@ -45,13 +76,36 @@ function initNavigation() {
|
||||
e.preventDefault();
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
const offsetTop = targetElement.offsetTop - 80;
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// 关闭移动端菜单
|
||||
if (navbarCollapse.classList.contains('show')) {
|
||||
navbarToggler.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 悬停效果
|
||||
link.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-2px)';
|
||||
});
|
||||
|
||||
link.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
|
||||
// 自动高亮当前页面对应的导航项
|
||||
const currentPage = window.location.pathname.split('/').pop();
|
||||
navLinks.forEach(link => {
|
||||
if (link.getAttribute('href') === currentPage) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,4 +361,346 @@ function monitorPerformance() {
|
||||
}
|
||||
|
||||
// 初始化性能监控
|
||||
monitorPerformance();
|
||||
monitorPerformance();
|
||||
|
||||
// 加载动画初始化
|
||||
function initPreloader() {
|
||||
const preloader = document.createElement('div');
|
||||
preloader.className = 'preloader';
|
||||
preloader.innerHTML = `
|
||||
<div class="preloader-inner">
|
||||
<div class="preloader-logo">
|
||||
<i class="fas fa-cow text-primary fs-1"></i>
|
||||
<div class="brand-name mt-2">NiuMall</div>
|
||||
</div>
|
||||
<div class="loading-bar">
|
||||
<div class="loading-progress"></div>
|
||||
</div>
|
||||
<p class="loading-text">正在加载中...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加加载动画样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.preloader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
transition: opacity 0.5s ease, visibility 0.5s ease;
|
||||
}
|
||||
.preloader-inner {
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
.brand-name {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.loading-bar {
|
||||
width: 200px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.loading-progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #81C784);
|
||||
width: 0;
|
||||
animation: loadingProgress 2s ease-in-out;
|
||||
}
|
||||
.loading-text {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
@keyframes loadingProgress {
|
||||
0% { width: 0%; }
|
||||
100% { width: 100%; }
|
||||
}
|
||||
.preloader.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(preloader);
|
||||
|
||||
// 页面加载完成后隐藏加载动画
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(() => {
|
||||
preloader.classList.add('hide');
|
||||
setTimeout(() => {
|
||||
preloader.remove();
|
||||
style.remove();
|
||||
}, 500);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// 打字机效果
|
||||
function initTypewriter() {
|
||||
const typewriterElements = document.querySelectorAll('.typewriter');
|
||||
|
||||
typewriterElements.forEach(element => {
|
||||
const text = element.textContent;
|
||||
element.textContent = '';
|
||||
element.style.borderRight = '2px solid #4CAF50';
|
||||
element.style.animation = 'typewriter-cursor 1s infinite';
|
||||
|
||||
let i = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (i < text.length) {
|
||||
element.textContent += text.charAt(i);
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
setTimeout(() => {
|
||||
element.style.borderRight = 'none';
|
||||
}, 1000);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 添加打字机样式
|
||||
if (!document.querySelector('#typewriter-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'typewriter-style';
|
||||
style.textContent = `
|
||||
@keyframes typewriter-cursor {
|
||||
0%, 50% { border-right-color: transparent; }
|
||||
51%, 100% { border-right-color: #4CAF50; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条动画
|
||||
function initProgressBars() {
|
||||
const progressBars = document.querySelectorAll('.progress-bar');
|
||||
|
||||
const progressObserver = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const progressBar = entry.target;
|
||||
const targetWidth = progressBar.getAttribute('data-width') || '100%';
|
||||
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.style.transition = 'width 2s ease-in-out';
|
||||
|
||||
setTimeout(() => {
|
||||
progressBar.style.width = targetWidth;
|
||||
}, 100);
|
||||
|
||||
progressObserver.unobserve(progressBar);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
progressBars.forEach(bar => progressObserver.observe(bar));
|
||||
}
|
||||
|
||||
// 数字递增动画优化
|
||||
function initCountUp() {
|
||||
const countElements = document.querySelectorAll('.count-up');
|
||||
|
||||
const countObserver = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const element = entry.target;
|
||||
const target = parseInt(element.getAttribute('data-target') || element.textContent);
|
||||
const duration = parseInt(element.getAttribute('data-duration') || '2000');
|
||||
const suffix = element.getAttribute('data-suffix') || '';
|
||||
|
||||
animateNumber(element, 0, target, duration, suffix);
|
||||
countObserver.unobserve(element);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
countElements.forEach(el => countObserver.observe(el));
|
||||
}
|
||||
|
||||
// 数字动画函数
|
||||
function animateNumber(element, start, end, duration, suffix) {
|
||||
const startTime = performance.now();
|
||||
|
||||
function update(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const current = Math.floor(start + (end - start) * easeProgress);
|
||||
|
||||
element.textContent = current.toLocaleString() + suffix;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
// 模态框效果
|
||||
function initModalEffects() {
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
|
||||
modals.forEach(modal => {
|
||||
modal.addEventListener('show.bs.modal', function() {
|
||||
this.querySelector('.modal-dialog').style.transform = 'scale(0.8)';
|
||||
this.querySelector('.modal-dialog').style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
this.querySelector('.modal-dialog').style.transition = 'all 0.3s ease';
|
||||
this.querySelector('.modal-dialog').style.transform = 'scale(1)';
|
||||
this.querySelector('.modal-dialog').style.opacity = '1';
|
||||
}, 10);
|
||||
});
|
||||
|
||||
modal.addEventListener('hide.bs.modal', function() {
|
||||
this.querySelector('.modal-dialog').style.transform = 'scale(0.8)';
|
||||
this.querySelector('.modal-dialog').style.opacity = '0';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 平滑滚动优化
|
||||
function initSmoothScrolling() {
|
||||
// 禁用默认滚动行为
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
|
||||
if (target) {
|
||||
const offsetTop = target.getBoundingClientRect().top + window.pageYOffset - 80;
|
||||
|
||||
// 使用自定义平滑滚动
|
||||
smoothScrollTo(offsetTop, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 自定义平滑滚动函数
|
||||
function smoothScrollTo(target, duration) {
|
||||
const start = window.pageYOffset;
|
||||
const distance = target - start;
|
||||
const startTime = performance.now();
|
||||
|
||||
function scroll(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
window.scrollTo(0, start + distance * easeProgress);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(scroll);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(scroll);
|
||||
}
|
||||
|
||||
// 粒子效果
|
||||
function initParticleEffect() {
|
||||
const heroSection = document.querySelector('.hero-section');
|
||||
if (!heroSection) return;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.style.top = '0';
|
||||
canvas.style.left = '0';
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.pointerEvents = 'none';
|
||||
canvas.style.zIndex = '1';
|
||||
|
||||
heroSection.appendChild(canvas);
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = heroSection.offsetWidth;
|
||||
canvas.height = heroSection.offsetHeight;
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
const particles = [];
|
||||
|
||||
// 创建粒子
|
||||
function createParticle() {
|
||||
return {
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
vx: (Math.random() - 0.5) * 0.5,
|
||||
vy: (Math.random() - 0.5) * 0.5,
|
||||
radius: Math.random() * 2 + 1,
|
||||
opacity: Math.random() * 0.5 + 0.2
|
||||
};
|
||||
}
|
||||
|
||||
// 初始化粒子
|
||||
for (let i = 0; i < 50; i++) {
|
||||
particles.push(createParticle());
|
||||
}
|
||||
|
||||
function animate() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
particles.forEach((particle, index) => {
|
||||
// 更新位置
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
// 边界检测
|
||||
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
||||
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
||||
|
||||
// 绘制粒子
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(76, 175, 80, ${particle.opacity})`;
|
||||
ctx.fill();
|
||||
|
||||
// 连接粒子
|
||||
particles.forEach((otherParticle, otherIndex) => {
|
||||
if (index !== otherIndex) {
|
||||
const dx = particle.x - otherParticle.x;
|
||||
const dy = particle.y - otherParticle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 100) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particle.x, particle.y);
|
||||
ctx.lineTo(otherParticle.x, otherParticle.y);
|
||||
ctx.strokeStyle = `rgba(76, 175, 80, ${0.1 * (1 - distance / 100)})`;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
}
|
||||
194
website/product-enhanced.html
Normal file
194
website/product-enhanced.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>产品介绍 - 活牛采购智能数字化系统</title>
|
||||
<meta name="description" content="了解活牛采购智能数字化系统的核心功能,包括供应商管理、订单管理、运输跟踪等全流程功能。">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome Icons -->
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- 自定义样式 -->
|
||||
<link href="css/custom.css" rel="stylesheet">
|
||||
<link href="css/responsive.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="index.html">
|
||||
<div class="logo-container me-2">
|
||||
<i class="fas fa-cow text-primary fs-2"></i>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-name fw-bold">NiuMall</div>
|
||||
<div class="brand-subtitle">活牛采购智能系统</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a href="index.html" class="nav-link">
|
||||
<i class="fas fa-home me-1"></i>返回首页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 产品英雄区域 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<div class="row align-items-center min-vh-100">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="display-3 fw-bold mb-4 text-white">
|
||||
全流程数字化
|
||||
<span class="text-primary">采购管理</span>
|
||||
</h1>
|
||||
<p class="lead mb-4 text-white-50">
|
||||
从供应商筛选到财务结算,NiuMall提供活牛采购的完整数字化解决方案
|
||||
</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="demo.html" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-play me-2"></i>观看演示
|
||||
</a>
|
||||
<a href="contact.html" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-download me-2"></i>下载手册
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="bg-white rounded-3 shadow-lg p-4">
|
||||
<h5 class="mb-3">实时数据驾驶舱</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-4">
|
||||
<div class="text-center p-2 bg-primary bg-opacity-10 rounded">
|
||||
<div class="h4 text-primary mb-0 count-up" data-target="156">0</div>
|
||||
<small>本月订单</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="text-center p-2 bg-success bg-opacity-10 rounded">
|
||||
<div class="h4 text-success mb-0 count-up" data-target="98" data-suffix="%">0%</div>
|
||||
<small>成功率</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="text-center p-2 bg-warning bg-opacity-10 rounded">
|
||||
<div class="h4 text-warning mb-0 count-up" data-target="24">0</div>
|
||||
<small>运输中</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 核心功能模块 -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="display-5 fw-bold mb-3">核心功能模块</h2>
|
||||
<p class="lead text-muted">全面覆盖活牛采购业务的每一个环节</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100 border-0 shadow">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="feature-icon bg-primary bg-opacity-10 p-3 rounded-circle mx-auto mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="fas fa-users text-primary fs-2"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">供应商管理</h4>
|
||||
<p class="text-muted">完整的供应商档案管理、资质审核、绩效评估体系</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100 border-0 shadow">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="feature-icon bg-success bg-opacity-10 p-3 rounded-circle mx-auto mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="fas fa-shopping-cart text-success fs-2"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">订单管理</h4>
|
||||
<p class="text-muted">标准化的采购流程管理,从需求计划到订单执行</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100 border-0 shadow">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="feature-icon bg-warning bg-opacity-10 p-3 rounded-circle mx-auto mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="fas fa-truck text-warning fs-2"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">运输跟踪</h4>
|
||||
<p class="text-muted">全程可视化的运输监控,确保运输过程安全透明</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100 border-0 shadow">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="feature-icon bg-info bg-opacity-10 p-3 rounded-circle mx-auto mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="fas fa-shield-alt text-info fs-2"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">质量管理</h4>
|
||||
<p class="text-muted">严格的质量控制体系,确保牛只质量符合标准</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100 border-0 shadow">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="feature-icon bg-danger bg-opacity-10 p-3 rounded-circle mx-auto mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="fas fa-chart-line text-danger fs-2"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">财务结算</h4>
|
||||
<p class="text-muted">自动化的财务结算系统,提高资金使用效率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100 border-0 shadow">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="feature-icon bg-secondary bg-opacity-10 p-3 rounded-circle mx-auto mb-3" style="width: 80px; height: 80px;">
|
||||
<i class="fas fa-chart-bar text-secondary fs-2"></i>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3">数据分析</h4>
|
||||
<p class="text-muted">深度数据分析和商业智能,为决策提供科学依据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 体验申请区域 -->
|
||||
<section class="py-5 bg-primary text-white">
|
||||
<div class="container text-center">
|
||||
<h2 class="mb-3">准备开始体验了吗?</h2>
|
||||
<p class="lead mb-4">立即申请免费试用,体验完整功能</p>
|
||||
<div class="d-flex gap-3 justify-content-center">
|
||||
<a href="demo.html" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-play me-2"></i>在线演示
|
||||
</a>
|
||||
<a href="contact.html" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-phone me-2"></i>联系咨询
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- 自定义脚本 -->
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user