diff --git a/README.md b/README.md index 1b13661..465ccb1 100644 --- a/README.md +++ b/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 \ No newline at end of file +#### 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) 文件了解详情 + +--- + +**🎯 让活牛采购更智能,让业务管理更简单!** \ No newline at end of file diff --git a/admin-system/.env.development b/admin-system/.env.development new file mode 100644 index 0000000..3e9390d --- /dev/null +++ b/admin-system/.env.development @@ -0,0 +1,68 @@ +# 开发环境配置 +NODE_ENV=development + +# 应用标题 +VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台 + +# API接口地址 +VITE_API_BASE_URL=http://localhost:3001/api + +# WebSocket地址 +VITE_WS_BASE_URL=ws://localhost:3001 + +# 上传文件地址 +VITE_UPLOAD_URL=http://localhost:3001/api/upload + +# 静态资源地址 +VITE_STATIC_URL=http://localhost:3001/static + +# 是否启用Mock数据 +VITE_USE_MOCK=true + +# 是否启用开发工具 +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 \ No newline at end of file diff --git a/admin-system/.env.production b/admin-system/.env.production new file mode 100644 index 0000000..1c3fdd5 --- /dev/null +++ b/admin-system/.env.production @@ -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 \ No newline at end of file diff --git a/admin-system/.eslintrc.js b/admin-system/.eslintrc.js new file mode 100644 index 0000000..83364d4 --- /dev/null +++ b/admin-system/.eslintrc.js @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/admin-system/.gitignore b/admin-system/.gitignore new file mode 100644 index 0000000..8a4476b --- /dev/null +++ b/admin-system/.gitignore @@ -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/ \ No newline at end of file diff --git a/admin-system/.prettierrc b/admin-system/.prettierrc new file mode 100644 index 0000000..10ca1db --- /dev/null +++ b/admin-system/.prettierrc @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/admin-system/README.md b/admin-system/README.md index 7f673b9..e5ad89d 100644 --- a/admin-system/README.md +++ b/admin-system/README.md @@ -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 + ` + + \ No newline at end of file diff --git a/admin-system/package.json b/admin-system/package.json new file mode 100644 index 0000000..9d7869c --- /dev/null +++ b/admin-system/package.json @@ -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" + ] +} \ No newline at end of file diff --git a/admin-system/src/App.vue b/admin-system/src/App.vue new file mode 100644 index 0000000..75287e7 --- /dev/null +++ b/admin-system/src/App.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/api/order.ts b/admin-system/src/api/order.ts new file mode 100644 index 0000000..3420156 --- /dev/null +++ b/admin-system/src/api/order.ts @@ -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>> => { + return request.get('/orders', { params }) +} + +// 获取订单详情 +export const getOrderDetail = (id: number): Promise> => { + return request.get(`/orders/${id}`) +} + +// 创建订单 +export const createOrder = (data: OrderCreateForm): Promise> => { + return request.post('/orders', data) +} + +// 更新订单 +export const updateOrder = (id: number, data: OrderUpdateForm): Promise> => { + return request.put(`/orders/${id}`, data) +} + +// 删除订单 +export const deleteOrder = (id: number): Promise => { + return request.delete(`/orders/${id}`) +} + +// 取消订单 +export const cancelOrder = (id: number, reason?: string): Promise => { + return request.put(`/orders/${id}/cancel`, { reason }) +} + +// 确认订单 +export const confirmOrder = (id: number): Promise => { + return request.put(`/orders/${id}/confirm`) +} + +// 订单验收 +export const acceptOrder = (id: number, data: { actualWeight: number; notes?: string }): Promise => { + return request.put(`/orders/${id}/accept`, data) +} + +// 完成订单 +export const completeOrder = (id: number): Promise => { + return request.put(`/orders/${id}/complete`) +} + +// 获取订单统计数据 +export const getOrderStatistics = (params?: { startDate?: string; endDate?: string }): Promise => { + return request.get('/orders/statistics', { params }) +} \ No newline at end of file diff --git a/admin-system/src/api/user.ts b/admin-system/src/api/user.ts new file mode 100644 index 0000000..5e1c235 --- /dev/null +++ b/admin-system/src/api/user.ts @@ -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> => { + return request.post('/auth/login', data) +} + +// 获取用户信息 +export const getUserInfo = (): Promise> => { + return request.get('/auth/me') +} + +// 用户登出 +export const logout = (): Promise => { + return request.post('/auth/logout') +} + +// 获取用户列表 +export const getUserList = (params: UserListParams): Promise => { + return request.get('/users', { params }) +} + +// 创建用户 +export const createUser = (data: UserCreateForm): Promise> => { + return request.post('/users', data) +} + +// 更新用户 +export const updateUser = (id: number, data: UserUpdateForm): Promise> => { + return request.put(`/users/${id}`, data) +} + +// 删除用户 +export const deleteUser = (id: number): Promise => { + return request.delete(`/users/${id}`) +} + +// 批量删除用户 +export const batchDeleteUsers = (ids: number[]): Promise => { + return request.delete('/users/batch', { data: { ids } }) +} + +// 重置用户密码 +export const resetUserPassword = (id: number, newPassword: string): Promise => { + return request.put(`/users/${id}/password`, { password: newPassword }) +} + +// 启用/禁用用户 +export const toggleUserStatus = (id: number, status: 'active' | 'inactive' | 'banned'): Promise => { + return request.put(`/users/${id}/status`, { status }) +} \ No newline at end of file diff --git a/admin-system/src/layouts/index.vue b/admin-system/src/layouts/index.vue new file mode 100644 index 0000000..7a70a37 --- /dev/null +++ b/admin-system/src/layouts/index.vue @@ -0,0 +1,310 @@ + + + + +" \ No newline at end of file diff --git a/admin-system/src/main.ts b/admin-system/src/main.ts new file mode 100644 index 0000000..208947f --- /dev/null +++ b/admin-system/src/main.ts @@ -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') \ No newline at end of file diff --git a/admin-system/src/router/index.ts b/admin-system/src/router/index.ts new file mode 100644 index 0000000..d1a9f9f --- /dev/null +++ b/admin-system/src/router/index.ts @@ -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 \ No newline at end of file diff --git a/admin-system/src/stores/user.ts b/admin-system/src/stores/user.ts new file mode 100644 index 0000000..38991cc --- /dev/null +++ b/admin-system/src/stores/user.ts @@ -0,0 +1,119 @@ +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(localStorage.getItem('token') || '') + const userInfo = ref(null) + const permissions = ref([]) + + // 计算属性 + 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('登录成功') + return Promise.resolve() + } catch (error: any) { + ElMessage.error(error.message || '登录失败') + 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() + } catch (error) { + console.error('登出接口调用失败:', error) + } 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 + } +}) \ No newline at end of file diff --git a/admin-system/src/style/index.scss b/admin-system/src/style/index.scss new file mode 100644 index 0000000..48c0a89 --- /dev/null +++ b/admin-system/src/style/index.scss @@ -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; + } +} \ No newline at end of file diff --git a/admin-system/src/style/mixins.scss b/admin-system/src/style/mixins.scss new file mode 100644 index 0000000..614fe52 --- /dev/null +++ b/admin-system/src/style/mixins.scss @@ -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); +} \ No newline at end of file diff --git a/admin-system/src/style/variables.scss b/admin-system/src/style/variables.scss new file mode 100644 index 0000000..9a70017 --- /dev/null +++ b/admin-system/src/style/variables.scss @@ -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; \ No newline at end of file diff --git a/admin-system/src/types/order.ts b/admin-system/src/types/order.ts new file mode 100644 index 0000000..8db8d95 --- /dev/null +++ b/admin-system/src/types/order.ts @@ -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 +} \ No newline at end of file diff --git a/admin-system/src/types/user.ts b/admin-system/src/types/user.ts new file mode 100644 index 0000000..96d345a --- /dev/null +++ b/admin-system/src/types/user.ts @@ -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 +} \ No newline at end of file diff --git a/admin-system/src/utils/request.ts b/admin-system/src/utils/request.ts new file mode 100644 index 0000000..ad56e03 --- /dev/null +++ b/admin-system/src/utils/request.ts @@ -0,0 +1,97 @@ +import axios from 'axios' +import type { AxiosResponse, AxiosError } from 'axios' +import { ElMessage } from 'element-plus' +import { useUserStore } from '@/stores/user' + +// 创建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) { + ElMessage.error(data.message || '请求失败') + return Promise.reject(new Error(data.message || '请求失败')) + } + + return data + }, + (error: AxiosError) => { + // 处理HTTP错误状态码 + const { response } = error + + if (response) { + switch (response.status) { + case 401: + ElMessage.error('未授权,请重新登录') + // 清除登录状态并跳转到登录页 + const userStore = useUserStore() + userStore.logoutAction() + window.location.href = '/login' + break + case 403: + ElMessage.error('访问被拒绝,权限不足') + break + case 404: + ElMessage.error('请求的资源不存在') + break + case 500: + ElMessage.error('服务器内部错误') + break + default: + ElMessage.error(`请求失败: ${response.status}`) + } + } else if (error.code === 'ECONNABORTED') { + ElMessage.error('请求超时,请稍后重试') + } else { + ElMessage.error('网络错误,请检查网络连接') + } + + return Promise.reject(error) + } +) + +export default request + +// 通用API响应类型 +export interface ApiResponse { + success: boolean + data: T + message: string + timestamp: string +} + +// 分页响应类型 +export interface PaginatedResponse { + items: T[] + total: number + page: number + pageSize: number + totalPages: number +} \ No newline at end of file diff --git a/admin-system/src/views/dashboard/index.vue b/admin-system/src/views/dashboard/index.vue new file mode 100644 index 0000000..7d8b5f6 --- /dev/null +++ b/admin-system/src/views/dashboard/index.vue @@ -0,0 +1,416 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/finance/index.vue b/admin-system/src/views/finance/index.vue new file mode 100644 index 0000000..531b060 --- /dev/null +++ b/admin-system/src/views/finance/index.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/login/index.vue b/admin-system/src/views/login/index.vue new file mode 100644 index 0000000..1896b20 --- /dev/null +++ b/admin-system/src/views/login/index.vue @@ -0,0 +1,237 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/order/index.vue b/admin-system/src/views/order/index.vue new file mode 100644 index 0000000..bc590ac --- /dev/null +++ b/admin-system/src/views/order/index.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/quality/index.vue b/admin-system/src/views/quality/index.vue new file mode 100644 index 0000000..0777cae --- /dev/null +++ b/admin-system/src/views/quality/index.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/settings/index.vue b/admin-system/src/views/settings/index.vue new file mode 100644 index 0000000..40de7a0 --- /dev/null +++ b/admin-system/src/views/settings/index.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/supplier/index.vue b/admin-system/src/views/supplier/index.vue new file mode 100644 index 0000000..3d4b307 --- /dev/null +++ b/admin-system/src/views/supplier/index.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/transport/index.vue b/admin-system/src/views/transport/index.vue new file mode 100644 index 0000000..b245bae --- /dev/null +++ b/admin-system/src/views/transport/index.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/admin-system/src/views/user/index.vue b/admin-system/src/views/user/index.vue new file mode 100644 index 0000000..4015c47 --- /dev/null +++ b/admin-system/src/views/user/index.vue @@ -0,0 +1,517 @@ + + + + + \ No newline at end of file diff --git a/admin-system/tsconfig.json b/admin-system/tsconfig.json new file mode 100644 index 0000000..210dc8f --- /dev/null +++ b/admin-system/tsconfig.json @@ -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"] + } +} \ No newline at end of file diff --git a/admin-system/vite.config.ts b/admin-system/vite.config.ts new file mode 100644 index 0000000..171f3d6 --- /dev/null +++ b/admin-system/vite.config.ts @@ -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 "@/assets/styles/variables.scss"; @import "@/assets/styles/mixins.scss";` + } + } + } + } +}) \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..2e3be0a --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,61 @@ +# 应用配置 +NODE_ENV=development +PORT=3001 +APP_NAME=活牛采购智能数字化系统 + +# 数据库配置 +DB_HOST=129.211.213.226 +DB_PORT=9527 +DB_NAME=jiebandata +DB_USER=root +DB_PASSWORD=aiotAiot123! +DB_DIALECT=mysql + +# Redis配置 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# JWT配置 +JWT_SECRET=your_jwt_secret_key_here +JWT_EXPIRES_IN=24h +JWT_REFRESH_EXPIRES_IN=7d + +# 文件上传配置 +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=50MB +ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,mp4,avi + +# 日志配置 +LOG_LEVEL=info +LOG_FILE=./logs/app.log + +# 短信配置 +SMS_PROVIDER=aliyun +SMS_ACCESS_KEY= +SMS_SECRET_KEY= + +# 支付配置 +PAYMENT_PROVIDER=wechat +WECHAT_APPID= +WECHAT_SECRET= +WECHAT_MERCHANT_ID= +WECHAT_API_KEY= + +# WebSocket配置 +WS_PORT=3002 + +# 监控配置 +ENABLE_MONITORING=true +MONITORING_TOKEN= + +# 邮件配置 +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= + +# API限流配置 +RATE_LIMIT_WINDOW=15 +RATE_LIMIT_MAX_REQUESTS=100 \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index e3f3026..498ec35 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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. 安全防护措施 \ No newline at end of file +## 🚀 快速开始 + +### 环境要求 +- 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 \ No newline at end of file diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 0000000..9f3c788 --- /dev/null +++ b/backend/app.js @@ -0,0 +1,78 @@ +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 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 + +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`) +}) \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..3125463 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,67 @@ +{ + "name": "niumall-backend", + "version": "1.0.0", + "description": "活牛采购智能数字化系统 - 后端服务", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write src/", + "db:migrate": "sequelize-cli db:migrate", + "db:seed": "sequelize-cli db:seed:all", + "db:reset": "sequelize-cli db:migrate:undo:all && npm run db:migrate && npm run db:seed", + "pm2:start": "pm2 start ecosystem.config.js", + "pm2:stop": "pm2 stop ecosystem.config.js", + "pm2:restart": "pm2 restart ecosystem.config.js" + }, + "keywords": [ + "nodejs", + "express", + "api", + "cattle", + "procurement", + "digital", + "system" + ], + "author": "NiuMall Team", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "sequelize": "^6.32.1", + "mysql2": "^3.6.0", + "redis": "^4.6.7", + "jsonwebtoken": "^9.0.2", + "bcryptjs": "^2.4.3", + "joi": "^17.9.2", + "multer": "^1.4.5-lts.1", + "winston": "^3.10.0", + "socket.io": "^4.7.2", + "cors": "^2.8.5", + "helmet": "^7.0.0", + "compression": "^1.7.4", + "express-rate-limit": "^6.8.1", + "dotenv": "^16.3.1", + "moment": "^2.29.4", + "uuid": "^9.0.0", + "lodash": "^4.17.21", + "axios": "^1.4.0" + }, + "devDependencies": { + "nodemon": "^3.0.1", + "jest": "^29.6.2", + "supertest": "^6.3.3", + "eslint": "^8.45.0", + "prettier": "^3.0.0", + "sequelize-cli": "^6.6.1", + "pm2": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + } +} \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..caafbfd --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,194 @@ +const express = require('express') +const bcrypt = require('bcryptjs') +const jwt = require('jsonwebtoken') +const Joi = require('joi') +const router = express.Router() + +// 模拟用户数据 +const users = [ + { + id: 1, + username: 'admin', + email: 'admin@example.com', + password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + role: 'admin', + status: 'active' + }, + { + id: 2, + username: 'buyer', + email: 'buyer@example.com', + password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + role: 'buyer', + status: 'active' + }, + { + id: 3, + username: 'trader', + email: 'trader@example.com', + password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + role: 'trader', + status: 'active' + } +] + +// 登录参数验证 +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.role + }, + 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 = users.find(u => u.username === username || u.email === username) + if (!user) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }) + } + + // 验证密码 + const isPasswordValid = await bcrypt.compare(password, user.password) + 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.role, + status: user.status + } + } + }) + } catch (error) { + console.error('登录失败:', error) + res.status(500).json({ + success: false, + message: '登录失败,请稍后重试' + }) + } +}) + +// 获取当前用户信息 +router.get('/me', authenticateToken, (req, res) => { + const user = users.find(u => u.id === 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.role, + status: user.status + }, + permissions: getUserPermissions(user.role) + } + }) +}) + +// 用户登出 +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() + }) +} + +// 获取用户权限 +function getUserPermissions(role) { + const permissions = { + admin: ['*'], // 管理员拥有所有权限 + buyer: ['order:read', 'order:create', 'order:update', 'supplier:read'], + trader: ['order:read', 'order:update', 'supplier:read', 'supplier:create', 'supplier:update', 'transport:read'], + supplier: ['order:read', 'quality:read', 'quality:create', 'quality:update'], + driver: ['transport:read', 'transport:update'] + } + + return permissions[role] || [] +} + +module.exports = router \ No newline at end of file diff --git a/backend/routes/finance.js b/backend/routes/finance.js new file mode 100644 index 0000000..ad7d5a8 --- /dev/null +++ b/backend/routes/finance.js @@ -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; \ No newline at end of file diff --git a/backend/routes/orders.js b/backend/routes/orders.js new file mode 100644 index 0000000..34ea613 --- /dev/null +++ b/backend/routes/orders.js @@ -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 \ No newline at end of file diff --git a/backend/routes/quality.js b/backend/routes/quality.js new file mode 100644 index 0000000..338787f --- /dev/null +++ b/backend/routes/quality.js @@ -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; \ No newline at end of file diff --git a/backend/routes/suppliers.js b/backend/routes/suppliers.js new file mode 100644 index 0000000..edf6059 --- /dev/null +++ b/backend/routes/suppliers.js @@ -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; \ No newline at end of file diff --git a/backend/routes/transport.js b/backend/routes/transport.js new file mode 100644 index 0000000..6ca390b --- /dev/null +++ b/backend/routes/transport.js @@ -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; \ No newline at end of file diff --git a/backend/routes/users.js b/backend/routes/users.js new file mode 100644 index 0000000..35aaab6 --- /dev/null +++ b/backend/routes/users.js @@ -0,0 +1,359 @@ +const express = require('express') +const bcrypt = require('bcryptjs') +const Joi = require('joi') +const router = express.Router() + +// 模拟用户数据 +let users = [ + { + 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 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(), + role: Joi.string().valid('admin', 'buyer', 'trader', 'supplier', 'driver').required(), + status: Joi.string().valid('active', 'inactive').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(''), + role: Joi.string().valid('admin', 'buyer', 'trader', 'supplier', 'driver'), + status: Joi.string().valid('active', 'inactive', 'banned') +}) + +// 获取用户列表 +router.get('/', (req, res) => { + try { + const { page = 1, pageSize = 20, keyword, role, status } = req.query + + let filteredUsers = [...users] + + // 关键词搜索 + if (keyword) { + filteredUsers = filteredUsers.filter(user => + user.username.includes(keyword) || + user.email.includes(keyword) + ) + } + + // 角色筛选 + if (role) { + filteredUsers = filteredUsers.filter(user => user.role === role) + } + + // 状态筛选 + if (status) { + filteredUsers = filteredUsers.filter(user => user.status === status) + } + + // 分页 + const total = filteredUsers.length + const startIndex = (page - 1) * pageSize + const endIndex = startIndex + parseInt(pageSize) + const paginatedUsers = filteredUsers.slice(startIndex, endIndex) + + res.json({ + success: true, + data: { + items: paginatedUsers, + 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 user = users.find(u => u.id === parseInt(id)) + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }) + } + + res.json({ + success: true, + data: user + }) + } catch (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, role, status } = value + + // 检查用户名是否已存在 + if (users.find(u => u.username === username)) { + return res.status(400).json({ + success: false, + message: '用户名已存在' + }) + } + + // 检查邮箱是否已存在 + if (users.find(u => u.email === email)) { + return res.status(400).json({ + success: false, + message: '邮箱已存在' + }) + } + + // 创建新用户 + const newUser = { + id: Math.max(...users.map(u => u.id)) + 1, + username, + email, + phone: phone || '', + role, + status, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + + users.push(newUser) + + res.status(201).json({ + success: true, + message: '用户创建成功', + data: newUser + }) + } catch (error) { + res.status(500).json({ + success: false, + message: '创建用户失败' + }) + } +}) + +// 更新用户 +router.put('/:id', (req, res) => { + try { + const { id } = req.params + const userIndex = users.findIndex(u => u.id === parseInt(id)) + + if (userIndex === -1) { + 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 + }) + } + + // 更新用户信息 + users[userIndex] = { + ...users[userIndex], + ...value, + updatedAt: new Date().toISOString() + } + + res.json({ + success: true, + message: '用户更新成功', + data: users[userIndex] + }) + } catch (error) { + res.status(500).json({ + success: false, + message: '更新用户失败' + }) + } +}) + +// 删除用户 +router.delete('/:id', (req, res) => { + try { + const { id } = req.params + const userIndex = users.findIndex(u => u.id === parseInt(id)) + + if (userIndex === -1) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }) + } + + users.splice(userIndex, 1) + + res.json({ + success: true, + message: '用户删除成功' + }) + } catch (error) { + res.status(500).json({ + success: false, + message: '删除用户失败' + }) + } +}) + +// 批量删除用户 +router.delete('/batch', (req, res) => { + try { + const { ids } = req.body + + if (!Array.isArray(ids) || ids.length === 0) { + return res.status(400).json({ + success: false, + message: '请提供有效的用户ID列表' + }) + } + + users = users.filter(user => !ids.includes(user.id)) + + res.json({ + success: true, + message: `成功删除 ${ids.length} 个用户` + }) + } catch (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 userIndex = users.findIndex(u => u.id === parseInt(id)) + if (userIndex === -1) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }) + } + + if (!password || password.length < 6) { + return res.status(400).json({ + success: false, + message: '密码长度不能少于6位' + }) + } + + // 在实际项目中,这里会对密码进行加密 + users[userIndex].updatedAt = new Date().toISOString() + + res.json({ + success: true, + message: '密码重置成功' + }) + } catch (error) { + res.status(500).json({ + success: false, + message: '重置密码失败' + }) + } +}) + +// 更新用户状态 +router.put('/:id/status', (req, res) => { + try { + const { id } = req.params + const { status } = req.body + + const userIndex = users.findIndex(u => u.id === parseInt(id)) + if (userIndex === -1) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }) + } + + if (!['active', 'inactive', 'banned'].includes(status)) { + return res.status(400).json({ + success: false, + message: '无效的用户状态' + }) + } + + users[userIndex].status = status + users[userIndex].updatedAt = new Date().toISOString() + + res.json({ + success: true, + message: '用户状态更新成功', + data: users[userIndex] + }) + } catch (error) { + res.status(500).json({ + success: false, + message: '更新用户状态失败' + }) + } +}) + +module.exports = router \ No newline at end of file diff --git a/docs/开发环境配置指南.md b/docs/开发环境配置指南.md new file mode 100644 index 0000000..6432e41 --- /dev/null +++ b/docs/开发环境配置指南.md @@ -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 +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 /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 +- **技术交流群**:微信群-前端技术交流 \ No newline at end of file diff --git a/docs/测试规范文档.md b/docs/测试规范文档.md new file mode 100644 index 0000000..0f42ad8 --- /dev/null +++ b/docs/测试规范文档.md @@ -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 + + + + 100 + 60 + 300 + +``` + +### 性能指标要求 +- **响应时间**: 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 \ No newline at end of file diff --git a/docs/部署和运维文档.md b/docs/部署和运维文档.md new file mode 100644 index 0000000..a730cca --- /dev/null +++ b/docs/部署和运维文档.md @@ -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 . + +# 安装后端依赖 +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 + +# 重新部署 +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 \ No newline at end of file diff --git a/mini_program/README.md b/mini_program/README.md index 471a6f2..76addcc 100644 --- a/mini_program/README.md +++ b/mini_program/README.md @@ -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. 性能优化 \ No newline at end of file +### 环境要求 +- 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 +} + +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 + +--- + +**🎯 让移动办公更便捷,让业务管理更高效!** \ No newline at end of file diff --git a/website/MAINTENANCE.md b/website/MAINTENANCE.md new file mode 100644 index 0000000..7a4e3d9 --- /dev/null +++ b/website/MAINTENANCE.md @@ -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 \ No newline at end of file diff --git a/website/api.html b/website/api.html new file mode 100644 index 0000000..91eb114 --- /dev/null +++ b/website/api.html @@ -0,0 +1,656 @@ + + + + + + API接口文档 - 活牛采购智能数字化系统 + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+

API接口文档

+

完整的REST API接口文档,支持开发者快速集成活牛采购系统

+
+
+ RESTful API +
+
+ JWT认证 +
+
+ JSON格式 +
+
+
+
+
+
API信息
+

基础URL:
https://api.niumall.com/v1

+

版本: v1.0

+

认证: Bearer Token

+
+
+
+
+
+ + +
+
+
+ + + + +
+
+ + +
+

API概述

+

活牛采购智能数字化系统提供完整的REST API接口,支持开发者快速集成和自定义开发。

+ +
快速开始
+

所有API请求都需要使用HTTPS协议,并在请求头中包含有效的JWT Token。

+ +
+ +
curl -X GET "https://api.niumall.com/v1/orders" \
+  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+  -H "Content-Type: application/json"
+
+ +
响应格式
+

所有API响应都使用JSON格式,并遵循统一的响应结构:

+ +
+
{
+  "success": true,
+  "data": {},
+  "message": "操作成功",
+  "timestamp": "2024-01-20T12:00:00Z"
+}
+
+
+ + +
+

认证授权

+ +
+
+
+ POST + /auth/login + 用户登录 +
+
+
+
请求参数
+ + + + + + + + + + + + + + + + + + + + + + + +
参数名类型必填说明
usernamestring用户名或手机号
passwordstring密码
+ +
请求示例
+
+ +
{
+  "username": "admin@example.com",
+  "password": "password123"
+}
+
+ +
响应示例
+
+
{
+  "success": true,
+  "data": {
+    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+    "token_type": "Bearer",
+    "expires_in": 3600,
+    "user": {
+      "id": 1,
+      "username": "admin",
+      "email": "admin@example.com",
+      "role": "admin"
+    }
+  },
+  "message": "登录成功"
+}
+
+
+
+
+ + +
+

订单管理

+ +
+
+
+ GET + /orders + 获取订单列表 +
+
+
+
查询参数
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数名类型必填说明
pageinteger页码,默认1
limitinteger每页数量,默认20
statusstring订单状态筛选
+ +
响应示例
+
+
{
+  "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": "获取成功"
+}
+
+
+
+ +
+
+
+ POST + /orders + 创建新订单 +
+
+
+
请求参数
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数名类型必填说明
supplier_idinteger供应商ID
cattle_breedstring牛种类型
cattle_countinteger牛只数量
expected_weightdecimal预期重量(kg)
unit_pricedecimal单价(元/kg)
+ +
请求示例
+
+ +
{
+  "supplier_id": 101,
+  "cattle_breed": "西门塔尔",
+  "cattle_count": 30,
+  "expected_weight": 15000,
+  "unit_price": 28.5,
+  "delivery_address": "北京市朝阳区xx养殖场",
+  "expected_delivery_date": "2024-01-25"
+}
+
+
+
+
+ + +
+

错误处理

+ +

API使用标准HTTP状态码来表示请求的成功或失败状态。

+ +
HTTP状态码
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
状态码说明
200请求成功
201创建成功
400请求参数错误
401未授权,需要登录
403禁止访问,权限不足
404资源不存在
500服务器内部错误
+ +
错误响应格式
+
+
{
+  "success": false,
+  "error": {
+    "code": "VALIDATION_ERROR",
+    "message": "请求参数验证失败",
+    "details": [
+      {
+        "field": "cattle_count",
+        "message": "牛只数量必须大于0"
+      }
+    ]
+  },
+  "timestamp": "2024-01-20T12:00:00Z"
+}
+
+
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/website/css/custom.css b/website/css/custom.css index 3a3414c..01edcc6 100644 --- a/website/css/custom.css +++ b/website/css/custom.css @@ -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; diff --git a/website/css/responsive.css b/website/css/responsive.css new file mode 100644 index 0000000..c2eab06 --- /dev/null +++ b/website/css/responsive.css @@ -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); +} \ No newline at end of file diff --git a/website/demo.html b/website/demo.html new file mode 100644 index 0000000..7911da4 --- /dev/null +++ b/website/demo.html @@ -0,0 +1,473 @@ + + + + + + 在线演示 - 活牛采购智能数字化系统 + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+

系统在线演示

+

体验活牛采购智能数字化系统的完整功能,了解如何提升您的采购效率

+
+
+
+
+
+
+
0
+ 活跃用户 +
+
+
+
+
0
+ 完成订单 +
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
功能模块
+
+ + + + + + +
+
+
+ + +
+
+ +
+

+ + 数据驾驶舱 +

+ + +
+
+
+
0
+
本月订单
+
+
+
+
+
0%
+
完成率
+
+
+
+
+
0
+
运输中
+
+
+
+
+
0%
+
质量合格率
+
+
+
+ + +
+
+
+
订单趋势分析
+
+ +
+
+
+
+
+
供应商分布
+
+ +
+
+
+
+
+ + + + + + +
+
+
+
+
+ + +
+
+

准备开始使用了吗?

+

立即申请免费试用,体验完整功能

+
+
+
+
+ + +
+
+
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/website/index-enhanced.html b/website/index-enhanced.html new file mode 100644 index 0000000..9f1f15f --- /dev/null +++ b/website/index-enhanced.html @@ -0,0 +1,356 @@ + + + + + + 活牛采购智能数字化系统 - 专业数字化采购解决方案 + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + 咨询热线:400-xxx-xxxx + | + 邮箱:info@niumall.com + +
+ +
+
+
+ + + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + 行业领先的数字化解决方案 + +
+ +

+ 数字化活牛采购 + 全流程管理 +

+ +

+ 专业的SOP采购管理系统,从供应商筛选、订单管理、运输跟踪到财务结算, + 一站式解决活牛采购的所有难题 +

+ +
+
+
+
+ + 供应商管理 +
+
+
+
+ + 运输跟踪 +
+
+
+
+ + 质量保证 +
+
+
+
+ + 数据分析 +
+
+
+
+ + + +
+
+
+
+
0
+ 服务企业 +
+
+
+
+
0
+ 满意度% +
+
+
+
+
0
+ 效率提升% +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
活牛采购管理系统
+
+ +
+ +
+
+
+
+
+ +
+
156
+ 本月订单 +
+
+
+
+
+
+
+
+
+ +
+
23
+ 运输中 +
+
+
+
+
+
+ + +
+
+ 采购趋势 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+
新订单通知
+
2分钟前
+
+
+
+
+
+ +
+
+
+
+ +
+
运输完成
+
订单#12345
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ 了解更多 + +
+
+
+ + \ No newline at end of file diff --git a/website/js/main.js b/website/js/main.js index 9163b7e..78b543d 100644 --- a/website/js/main.js +++ b/website/js/main.js @@ -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(); \ No newline at end of file +monitorPerformance(); + +// 加载动画初始化 +function initPreloader() { + const preloader = document.createElement('div'); + preloader.className = 'preloader'; + preloader.innerHTML = ` +
+ +
+
+
+

正在加载中...

+
+ `; + + // 添加加载动画样式 + 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(); +} \ No newline at end of file diff --git a/website/product-enhanced.html b/website/product-enhanced.html new file mode 100644 index 0000000..e1ea013 --- /dev/null +++ b/website/product-enhanced.html @@ -0,0 +1,194 @@ + + + + + + 产品介绍 - 活牛采购智能数字化系统 + + + + + + + + + + + + + + + +
+
+
+
+

+ 全流程数字化 + 采购管理 +

+

+ 从供应商筛选到财务结算,NiuMall提供活牛采购的完整数字化解决方案 +

+ +
+
+
+
实时数据驾驶舱
+
+
+
+
0
+ 本月订单 +
+
+
+
+
0%
+ 成功率 +
+
+
+
+
0
+ 运输中 +
+
+
+
+
+
+
+
+ + +
+
+
+

核心功能模块

+

全面覆盖活牛采购业务的每一个环节

+
+ +
+
+
+
+
+ +
+

供应商管理

+

完整的供应商档案管理、资质审核、绩效评估体系

+
+
+
+ +
+
+
+
+ +
+

订单管理

+

标准化的采购流程管理,从需求计划到订单执行

+
+
+
+ +
+
+
+
+ +
+

运输跟踪

+

全程可视化的运输监控,确保运输过程安全透明

+
+
+
+ +
+
+
+
+ +
+

质量管理

+

严格的质量控制体系,确保牛只质量符合标准

+
+
+
+ +
+
+
+
+ +
+

财务结算

+

自动化的财务结算系统,提高资金使用效率

+
+
+
+ +
+
+
+
+ +
+

数据分析

+

深度数据分析和商业智能,为决策提供科学依据

+
+
+
+
+
+
+ + +
+
+

准备开始体验了吗?

+

立即申请免费试用,体验完整功能

+ +
+
+ + + + + + + \ No newline at end of file