添加 IntelliJ IDEA 项目配置文件

This commit is contained in:
ylweng
2025-09-02 21:59:27 +08:00
parent 59cfe620fe
commit 501c218a83
56 changed files with 11886 additions and 126 deletions

View File

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

View File

@@ -0,0 +1,77 @@
# 生产环境配置
NODE_ENV=production
# 应用标题
VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台
# API接口地址
VITE_API_BASE_URL=https://api.niumall.com/api
# WebSocket地址
VITE_WS_BASE_URL=wss://api.niumall.com
# 上传文件地址
VITE_UPLOAD_URL=https://api.niumall.com/api/upload
# 静态资源地址
VITE_STATIC_URL=https://static.niumall.com
# 是否启用Mock数据
VITE_USE_MOCK=false
# 是否启用开发工具
VITE_DEV_TOOLS=false
# 路由模式 hash | history
VITE_ROUTER_MODE=history
# 应用端口
VITE_PORT=80
# 代理前缀
VITE_API_PREFIX=/api
# Token密钥
VITE_TOKEN_KEY=admin_token
# Token过期时间(小时)
VITE_TOKEN_EXPIRES=24
# 分页大小
VITE_PAGE_SIZE=20
# 上传文件最大大小(MB)
VITE_UPLOAD_MAX_SIZE=10
# 是否显示设置按钮
VITE_SHOW_SETTINGS=false
# 是否显示标签页
VITE_SHOW_TABS=true
# 是否显示面包屑
VITE_SHOW_BREADCRUMB=true
# 是否固定头部
VITE_FIXED_HEADER=true
# 侧边栏Logo
VITE_SIDEBAR_LOGO=true
# 默认主题色
VITE_THEME_COLOR=#409eff
# 默认布局
VITE_LAYOUT=default
# 应用版本
VITE_APP_VERSION=1.0.0
# CDN地址
VITE_CDN_URL=https://cdn.niumall.com
# 错误日志上报地址
VITE_ERROR_LOG_URL=https://api.niumall.com/api/error-log
# 性能监控地址
VITE_PERFORMANCE_URL=https://api.niumall.com/api/performance

107
admin-system/.eslintrc.js Normal file
View File

@@ -0,0 +1,107 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2021: true
},
extends: [
'plugin:vue/vue3-essential',
'plugin:vue/vue3-strongly-recommended',
'plugin:vue/vue3-recommended',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser'
},
plugins: ['vue', '@typescript-eslint'],
globals: {
ElMessage: 'readonly',
ElMessageBox: 'readonly',
ElNotification: 'readonly',
ElLoading: 'readonly'
},
rules: {
// Vue规则
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off',
'vue/component-tags-order': [
'error',
{
order: ['script', 'template', 'style']
}
],
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: false
}
],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-emits-declaration': 'error',
'vue/define-props-declaration': 'error',
'vue/html-button-has-type': 'error',
'vue/no-unused-refs': 'error',
'vue/no-useless-v-bind': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/prefer-true-attribute-shorthand': 'error',
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style']
}
],
// TypeScript规则
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/ban-ts-comment': 'warn',
// JavaScript规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': 'off',
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
'template-curly-spacing': 'error',
'yield-star-spacing': 'error',
'prefer-rest-params': 'error',
'no-useless-escape': 'error',
'no-irregular-whitespace': 'error',
'no-prototype-builtins': 'error',
'no-fallthrough': 'error',
'no-extra-boolean-cast': 'error',
'no-case-declarations': 'error',
'no-async-promise-executor': 'error'
},
overrides: [
{
files: ['cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'],
extends: ['plugin:cypress/recommended']
},
{
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
env: {
jest: true
}
}
]
}

99
admin-system/.gitignore vendored Normal file
View File

@@ -0,0 +1,99 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env.local
.env.*.local
# TypeScript
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Auto-generated files
auto-imports.d.ts
components.d.ts
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
Thumbs.db
# Local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# Test
test-results/
playwright-report/
playwright/.cache/

40
admin-system/.prettierrc Normal file
View File

@@ -0,0 +1,40 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore",
"overrides": [
{
"files": "*.vue",
"options": {
"parser": "vue"
}
},
{
"files": ["*.ts", "*.tsx"],
"options": {
"parser": "typescript"
}
},
{
"files": ["*.json"],
"options": {
"parser": "json"
}
},
{
"files": ["*.scss", "*.css"],
"options": {
"parser": "scss"
}
}
]
}

View File

@@ -1,38 +1,447 @@
# Admin System 管理后台
# Admin System - 活牛采购智能数字化系统管理后台
## 技术栈
- Vue 3 + TypeScript
- Element Plus / Ant Design Vue
- Vue Router
- Pinia状态管理
- Axios HTTP客户端
## 📋 项目概述
活牛采购智能数字化系统管理后台是基于Vue 3 + TypeScript的现代化Web应用为系统管理员、内部员工提供全面的业务管理和数据分析功能。
**核心功能:**
- 👥 用户权限管理(采购人、贸易商、供应商、司机)
- 📦 订单全流程管理和监控
- 🚛 运输跟踪和状态监控
- 💰 结算财务管理
- 📊 数据统计和分析报表
- ⚙️ 系统配置和维护
## 🛠 技术栈
| 类别 | 技术选型 | 版本 | 说明 |
|------|----------|------|------|
| **前端框架** | Vue 3 | ^3.3.0 | Composition API + `<script setup>` |
| **开发语言** | TypeScript | ^5.0.0 | 类型安全和智能提示 |
| **构建工具** | Vite | ^4.4.0 | 快速开发和构建 |
| **UI组件库** | Element Plus | ^2.3.0 | 企业级组件库 |
| **状态管理** | Pinia | ^2.1.0 | Vue 3官方推荐 |
| **路由管理** | Vue Router | ^4.2.0 | 单页面应用路由 |
| **HTTP客户端** | Axios | ^1.4.0 | API请求处理 |
| **代码规范** | ESLint + Prettier | latest | 代码质量保证 |
| **CSS预处理** | Sass/SCSS | ^1.64.0 | 样式管理 |
| **图标库** | @element-plus/icons-vue | ^2.1.0 | 图标组件 |
## 📂 项目结构
## 项目结构
```
admin-system/
├── public/ # 公共资源
│ ├── favicon.ico
│ └── index.html
├── src/
│ ├── views/ # 页面组件
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── api/ # API接口
├── utils/ # 工具函数
── assets/ # 静态资源
├── public/ # 公共文件
├── tests/ # 测试文件
├── package.json
└── README.md
│ ├── api/ # API接口定义
│ ├── auth.ts # 认证相关API
│ ├── user.ts # 用户管理API
│ ├── order.ts # 订单管理API
│ ├── transport.ts # 运输跟踪API
│ └── settlement.ts # 结算管理API
── assets/ # 静态资源
├── images/
├── icons/
│ │ └── styles/
│ │ ├── index.scss # 全局样式
│ │ ├── variables.scss # SCSS变量
│ │ └── mixins.scss # SCSS混入
│ ├── components/ # 公共组件
│ │ ├── common/ # 通用组件
│ │ │ ├── AppHeader.vue
│ │ │ ├── AppSidebar.vue
│ │ │ ├── AppBreadcrumb.vue
│ │ │ └── PageContainer.vue
│ │ ├── charts/ # 图表组件
│ │ │ ├── LineChart.vue
│ │ │ ├── BarChart.vue
│ │ │ └── PieChart.vue
│ │ └── forms/ # 表单组件
│ │ ├── UserForm.vue
│ │ ├── OrderForm.vue
│ │ └── SearchForm.vue
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts # 认证逻辑
│ │ ├── useTable.ts # 表格通用逻辑
│ │ ├── useDialog.ts # 弹窗逻辑
│ │ └── usePermission.ts # 权限控制
│ ├── router/ # 路由配置
│ │ ├── index.ts # 路由主文件
│ │ ├── routes.ts # 路由定义
│ │ └── guards.ts # 路由守卫
│ ├── stores/ # Pinia状态管理
│ │ ├── auth.ts # 认证状态
│ │ ├── user.ts # 用户状态
│ │ ├── order.ts # 订单状态
│ │ └── app.ts # 应用全局状态
│ ├── types/ # TypeScript类型定义
│ │ ├── api.ts # API响应类型
│ │ ├── user.ts # 用户类型
│ │ ├── order.ts # 订单类型
│ │ └── common.ts # 通用类型
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 请求封装
│ │ ├── storage.ts # 本地存储
│ │ ├── format.ts # 格式化工具
│ │ ├── validator.ts # 表单验证
│ │ └── constants.ts # 常量定义
│ ├── views/ # 页面组件
│ │ ├── auth/ # 认证页面
│ │ │ ├── Login.vue
│ │ │ └── ForgotPassword.vue
│ │ ├── dashboard/ # 仪表盘
│ │ │ └── Dashboard.vue
│ │ ├── user/ # 用户管理
│ │ │ ├── UserList.vue
│ │ │ ├── UserDetail.vue
│ │ │ └── UserRoles.vue
│ │ ├── order/ # 订单管理
│ │ │ ├── OrderList.vue
│ │ │ ├── OrderDetail.vue
│ │ │ ├── OrderFlow.vue
│ │ │ └── OrderTracking.vue
│ │ ├── transport/ # 运输管理
│ │ │ ├── TransportList.vue
│ │ │ ├── TransportMap.vue
│ │ │ └── VehicleManage.vue
│ │ ├── settlement/ # 结算管理
│ │ │ ├── SettlementList.vue
│ │ │ ├── PaymentRecord.vue
│ │ │ └── FinancialReport.vue
│ │ ├── system/ # 系统管理
│ │ │ ├── SystemConfig.vue
│ │ │ ├── LogManage.vue
│ │ │ └── DataBackup.vue
│ │ └── error/ # 错误页面
│ │ ├── 404.vue
│ │ └── 500.vue
│ ├── App.vue # 根组件
│ └── main.ts # 应用入口
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ └── e2e/ # 端到端测试
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
├── .eslintrc.js # ESLint配置
├── .prettierrc # Prettier配置
├── tsconfig.json # TypeScript配置
├── vite.config.ts # Vite配置
├── package.json # 项目依赖
└── README.md # 项目文档
```
## 功能模块
- 用户管理
- 商品管理
- 订单管理
- 数据统计
- 系统设置
## 🚀 快速开始
## 开发规范
1. 使用Composition API
2. TypeScript严格模式
3. 组件命名规范
4. 代码分割和懒加载
### 环境要求
- Node.js >= 16.0.0
- npm >= 8.0.0 或 yarn >= 1.22.0
- 现代浏览器Chrome 90+, Firefox 88+, Safari 14+
### 安装依赖
```bash
cd admin-system
npm install
# 或
yarn install
```
### 开发环境启动
```bash
npm run dev
# 或
yarn dev
```
访问地址http://localhost:3000
### 生产环境构建
```bash
npm run build
# 或
yarn build
```
### 代码检查和格式化
```bash
# ESLint检查
npm run lint
# 自动修复
npm run lint:fix
# Prettier格式化
npm run format
```
### 测试
```bash
# 单元测试
npm run test:unit
# 端到端测试
npm run test:e2e
# 测试覆盖率
npm run test:coverage
```
## 🔐 认证和权限
### 用户角色
| 角色 | 权限描述 | 功能范围 |
|------|----------|----------|
| **super_admin** | 超级管理员 | 所有功能和系统配置 |
| **admin** | 系统管理员 | 用户管理、订单管理、数据查看 |
| **operator** | 业务操作员 | 订单处理、运输跟踪 |
| **viewer** | 只读用户 | 数据查看和报表导出 |
### 权限控制
- **路由级权限**:基于用户角色控制页面访问
- **组件级权限**:控制按钮、菜单项的显示
- **数据级权限**:控制数据的增删改查权限
- **API级权限**:后端接口权限验证
### 默认账号
```
管理员账号admin / admin123
操作员账号operator / operator123
查看员账号viewer / viewer123
```
## 📊 功能模块
### 1. 用户管理模块
- **用户列表**:查看所有系统用户
- **用户详情**:查看和编辑用户信息
- **角色管理**:分配和修改用户角色
- **权限配置**:细粒度权限控制
### 2. 订单管理模块
- **订单列表**:所有采购订单管理
- **订单详情**:订单完整信息展示
- **流程监控**:订单状态流转跟踪
- **异常处理**:订单异常情况处理
### 3. 运输跟踪模块
- **运输列表**:所有运输任务
- **实时地图**GPS位置实时显示
- **车辆管理**:运输车辆信息
- **轨迹回放**:历史运输轨迹
### 4. 结算管理模块
- **结算列表**:所有结算记录
- **支付记录**:支付流水查询
- **财务报表**:各类财务统计
- **对账管理**:自动对账功能
### 5. 数据分析模块
- **业务仪表盘**:核心指标展示
- **统计报表**:多维度数据分析
- **图表可视化**:数据图表展示
- **报表导出**Excel/PDF导出
## 🔧 开发规范
### 代码规范
- **命名规范**
- 组件名PascalCase`UserList.vue`
- 文件名kebab-case`user-list.vue`
- 变量名camelCase`userName`
- 常量名UPPER_SNAKE_CASE`API_BASE_URL`
- **组件开发**
- 使用 Composition API + `<script setup>`
- 统一使用 TypeScript
- 组件props必须定义类型
- 使用defineEmits定义事件
- **样式规范**
- 使用SCSS语法
- 遵循BEM命名规范
- 使用CSS变量定义主题色彩
- 响应式设计适配
### Git提交规范
```
feat: 新增功能
fix: 修复bug
docs: 文档更新
style: 代码格式调整
refactor: 代码重构
test: 测试相关
chore: 其他修改
```
### 目录命名规范
- 页面组件放在 `views/` 目录
- 公共组件放在 `components/` 目录
- API接口放在 `api/` 目录
- 工具函数放在 `utils/` 目录
- 类型定义放在 `types/` 目录
## 🌐 API接口
### 接口基础配置
```typescript
// 开发环境
VITE_API_BASE_URL=http://localhost:3001/api
VITE_WS_BASE_URL=ws://localhost:3001
// 生产环境
VITE_API_BASE_URL=https://api.niumall.com/api
VITE_WS_BASE_URL=wss://api.niumall.com
```
### 核心接口模块
- **认证接口**登录、登出、刷新token
- **用户接口**用户CRUD、角色管理
- **订单接口**:订单管理、状态更新
- **运输接口**GPS跟踪、状态上报
- **结算接口**:支付、对账、报表
## 📱 响应式设计
### 断点定义
```scss
$breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1400px
);
```
### 适配策略
- **桌面端**1200px+ 完整功能
- **平板端**768px-1199px 简化布局
- **手机端**<768px 移动优化
## 🔍 性能优化
### 构建优化
- **代码分割**路由级别的懒加载
- **资源压缩**Gzip/Brotli压缩
- **缓存策略**浏览器缓存配置
- **CDN加速**静态资源CDN
### 运行时优化
- **虚拟列表**大数据列表优化
- **图片懒加载**减少初始加载
- **防抖节流**高频操作优化
- **内存管理**避免内存泄漏
## 🧪 测试策略
### 单元测试
- **组件测试**Vue组件功能测试
- **工具函数测试**纯函数测试
- **Store测试**状态管理测试
- **覆盖率要求**>80%
### 集成测试
- **API集成测试**:接口联调测试
- **路由测试**:页面跳转测试
- **权限测试**:权限控制测试
### E2E测试
- **关键流程测试**:用户核心操作路径
- **浏览器兼容性测试**:多浏览器测试
- **性能测试**:页面加载性能
## 🚀 部署指南
### 构建命令
```bash
# 开发环境构建
npm run build:dev
# 测试环境构建
npm run build:test
# 生产环境构建
npm run build:prod
```
### Nginx配置
```nginx
server {
listen 80;
server_name admin.niumall.com;
location / {
root /var/www/admin-system;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend-server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Docker部署
```dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
## 🛡️ 安全措施
### 前端安全
- **XSS防护**:输入输出转义
- **CSRF防护**Token验证
- **Content Security Policy**:内容安全策略
- **敏感信息保护**:避免敏感数据泄露
### 认证安全
- **JWT Token**:无状态认证
- **Token刷新**:自动刷新机制
- **权限验证**:多级权限控制
- **会话管理**:安全会话处理
## 📈 监控和日志
### 前端监控
- **错误监控**JavaScript错误捕获
- **性能监控**:页面加载性能
- **用户行为**:操作行为统计
- **接口监控**API调用监控
### 日志管理
- **操作日志**:用户操作记录
- **错误日志**:错误信息收集
- **性能日志**:性能指标记录
## 🤝 贡献指南
1. **Fork** 仓库
2. **创建**特性分支 (`git checkout -b feature/AmazingFeature`)
3. **提交**更改 (`git commit -m 'Add some AmazingFeature'`)
4. **推送**到分支 (`git push origin feature/AmazingFeature`)
5. **开启** Pull Request
### 开发流程
1. 领取任务并创建分支
2. 本地开发和测试
3. 代码审查和合并
4. 部署测试环境
5. 上线生产环境
## 📞 技术支持
- **技术负责人**:张工程师 (zhang@niumall.com)
- **产品经理**:李经理 (li@niumall.com)
- **技术群组**:微信群-前端技术交流
- **问题反馈**GitHub Issues
---
**🎯 让管理更智能,让业务更高效!**

14
admin-system/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>活牛采购智能数字化系统 - 管理后台</title>
<meta name="description" content="活牛采购智能数字化系统管理后台,提供用户管理、订单管理、供应商管理等功能。">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

69
admin-system/package.json Normal file
View File

@@ -0,0 +1,69 @@
{
"name": "niumall-admin-system",
"version": "1.0.0",
"type": "module",
"description": "活牛采购智能数字化系统 - 管理后台",
"author": "NiuMall Team",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:dev": "vue-tsc && vite build --mode development",
"build:test": "vue-tsc && vite build --mode test",
"build:prod": "vue-tsc && vite build --mode production",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/",
"type-check": "vue-tsc --noEmit",
"test:unit": "vitest",
"test:e2e": "cypress run",
"test:e2e:dev": "cypress open",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.4.0",
"echarts": "^5.4.2",
"element-plus": "^2.3.8",
"nprogress": "^0.2.0",
"pinia": "^2.1.4",
"pinia-plugin-persistedstate": "^3.2.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
"@types/nprogress": "^0.2.0",
"@types/node": "^20.4.5",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^2.4.0",
"@vue/tsconfig": "^0.4.0",
"cypress": "^12.17.1",
"eslint": "^8.45.0",
"eslint-plugin-cypress": "^2.13.3",
"eslint-plugin-vue": "^9.15.1",
"jsdom": "^22.1.0",
"prettier": "^3.0.0",
"sass": "^1.64.1",
"typescript": "~5.1.6",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.6",
"vitest": "^0.33.0",
"vue-tsc": "^1.8.5"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

38
admin-system/src/App.vue Normal file
View File

@@ -0,0 +1,38 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from './stores/user'
const userStore = useUserStore()
onMounted(() => {
// 应用初始化时检查登录状态
userStore.checkLoginStatus()
})
</script>
<style lang="scss">
#app {
height: 100vh;
overflow: hidden;
}
// 全局样式重置
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
font-size: 14px;
color: #333;
background-color: #f0f2f5;
}
</style>

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
import type { ApiResponse, PaginatedResponse } from '@/utils/request'
import type { Order, OrderListParams, OrderCreateForm, OrderUpdateForm } from '@/types/order'
// 获取订单列表
export const getOrderList = (params: OrderListParams): Promise<ApiResponse<PaginatedResponse<Order>>> => {
return request.get('/orders', { params })
}
// 获取订单详情
export const getOrderDetail = (id: number): Promise<ApiResponse<Order>> => {
return request.get(`/orders/${id}`)
}
// 创建订单
export const createOrder = (data: OrderCreateForm): Promise<ApiResponse<Order>> => {
return request.post('/orders', data)
}
// 更新订单
export const updateOrder = (id: number, data: OrderUpdateForm): Promise<ApiResponse<Order>> => {
return request.put(`/orders/${id}`, data)
}
// 删除订单
export const deleteOrder = (id: number): Promise<ApiResponse> => {
return request.delete(`/orders/${id}`)
}
// 取消订单
export const cancelOrder = (id: number, reason?: string): Promise<ApiResponse> => {
return request.put(`/orders/${id}/cancel`, { reason })
}
// 确认订单
export const confirmOrder = (id: number): Promise<ApiResponse> => {
return request.put(`/orders/${id}/confirm`)
}
// 订单验收
export const acceptOrder = (id: number, data: { actualWeight: number; notes?: string }): Promise<ApiResponse> => {
return request.put(`/orders/${id}/accept`, data)
}
// 完成订单
export const completeOrder = (id: number): Promise<ApiResponse> => {
return request.put(`/orders/${id}/complete`)
}
// 获取订单统计数据
export const getOrderStatistics = (params?: { startDate?: string; endDate?: string }): Promise<ApiResponse> => {
return request.get('/orders/statistics', { params })
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
import type { ApiResponse } from '@/utils/request'
import type { User, LoginForm, LoginResponse, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
// 用户登录
export const login = (data: LoginForm): Promise<ApiResponse<LoginResponse>> => {
return request.post('/auth/login', data)
}
// 获取用户信息
export const getUserInfo = (): Promise<ApiResponse<{ user: User; permissions: string[] }>> => {
return request.get('/auth/me')
}
// 用户登出
export const logout = (): Promise<ApiResponse> => {
return request.post('/auth/logout')
}
// 获取用户列表
export const getUserList = (params: UserListParams): Promise<ApiResponse> => {
return request.get('/users', { params })
}
// 创建用户
export const createUser = (data: UserCreateForm): Promise<ApiResponse<User>> => {
return request.post('/users', data)
}
// 更新用户
export const updateUser = (id: number, data: UserUpdateForm): Promise<ApiResponse<User>> => {
return request.put(`/users/${id}`, data)
}
// 删除用户
export const deleteUser = (id: number): Promise<ApiResponse> => {
return request.delete(`/users/${id}`)
}
// 批量删除用户
export const batchDeleteUsers = (ids: number[]): Promise<ApiResponse> => {
return request.delete('/users/batch', { data: { ids } })
}
// 重置用户密码
export const resetUserPassword = (id: number, newPassword: string): Promise<ApiResponse> => {
return request.put(`/users/${id}/password`, { password: newPassword })
}
// 启用/禁用用户
export const toggleUserStatus = (id: number, status: 'active' | 'inactive' | 'banned'): Promise<ApiResponse> => {
return request.put(`/users/${id}/status`, { status })
}

View File

@@ -0,0 +1,310 @@
<template>
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '240px'" class="layout-aside">
<div class="logo-container">
<img v-if="!isCollapse" src="/logo.png" alt="Logo" class="logo" />
<span v-if="!isCollapse" class="logo-text">NiuMall</span>
<img v-else src="/logo.png" alt="Logo" class="logo-mini" />
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:unique-opened="true"
class="layout-menu"
router
>
<template v-for="route in menuRoutes" :key="route.path">
<el-menu-item :index="route.path" v-if="!route.children">
<el-icon><component :is="route.meta?.icon" /></el-icon>
<template #title>{{ route.meta?.title }}</template>
</el-menu-item>
<el-sub-menu :index="route.path" v-else>
<template #title>
<el-icon><component :is="route.meta?.icon" /></el-icon>
<span>{{ route.meta?.title }}</span>
</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="child.path"
>
{{ child.meta?.title }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
<!-- 主要内容区域 -->
<el-container class="layout-main">
<!-- 头部 -->
<el-header class="layout-header">
<div class="header-left">
<el-button
type="text"
:icon="isCollapse ? 'Expand' : 'Fold'"
@click="toggleCollapse"
/>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="item in breadcrumbs"
:key="item.path"
:to="item.path"
>
{{ item.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<div class="user-info">
<el-avatar :src="userStore.avatar" :size="32" />
<span class="username">{{ userStore.username }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item command="settings">
<el-icon><Setting /></el-icon>
系统设置
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 内容区域 -->
<el-main class="layout-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// 菜单折叠状态
const isCollapse = ref(false)
// 当前激活的菜单
const activeMenu = computed(() => route.path)
// 菜单路由配置
const menuRoutes = computed(() => {
return router.getRoutes()
.find(r => r.path === '/')
?.children?.filter(child => child.meta?.title) || []
})
// 面包屑导航
const breadcrumbs = computed(() => {
const matched = route.matched.filter(item => item.meta?.title)
return matched.map(item => ({
path: item.path,
title: item.meta?.title
}))
})
// 切换菜单折叠状态
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
// 处理用户下拉菜单命令
const handleCommand = async (command: string) => {
switch (command) {
case 'profile':
router.push('/profile')
break
case 'settings':
router.push('/settings')
break
case 'logout':
try {
await ElMessageBox.confirm(
'确定要退出登录吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await userStore.logoutAction()
router.push('/login')
} catch (error) {
// 用户取消操作
}
break
}
}
// 监听路由变化,在移动端自动收起菜单
watch(
() => route.path,
() => {
if (window.innerWidth <= 768) {
isCollapse.value = true
}
}
)
</script>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
}
.layout-aside {
background: #304156;
transition: width 0.3s ease;
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
background: #263445;
.logo {
width: 32px;
height: 32px;
margin-right: 10px;
}
.logo-mini {
width: 32px;
height: 32px;
}
.logo-text {
color: white;
font-size: 18px;
font-weight: 600;
}
}
}
.layout-menu {
border: none;
background: #304156;
:deep(.el-menu-item) {
color: #bfcbd9;
&:hover {
background: #48576a;
color: #ffffff;
}
&.is-active {
background: #4CAF50;
color: #ffffff;
}
}
:deep(.el-sub-menu__title) {
color: #bfcbd9;
&:hover {
background: #48576a;
color: #ffffff;
}
}
}
.layout-main {
display: flex;
flex-direction: column;
}
.layout-header {
background: white;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.header-right {
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: background-color 0.3s ease;
&:hover {
background-color: #f5f5f5;
}
.username {
font-size: 14px;
color: #333;
}
}
}
}
.layout-content {
background: #f0f2f5;
padding: 20px;
overflow-y: auto;
}
@media (max-width: 768px) {
.layout-aside {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
height: 100vh;
}
.layout-main {
margin-left: 0;
}
.layout-header {
padding: 0 15px;
.header-left {
gap: 15px;
}
}
.layout-content {
padding: 15px;
}
}
</style>"

25
admin-system/src/main.ts Normal file
View File

@@ -0,0 +1,25 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import './style/index.scss'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')

View File

@@ -0,0 +1,132 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 布局组件
import Layout from '@/layouts/index.vue'
// 页面组件
import Login from '@/views/login/index.vue'
import Dashboard from '@/views/dashboard/index.vue'
import UserManagement from '@/views/user/index.vue'
import OrderManagement from '@/views/order/index.vue'
import SupplierManagement from '@/views/supplier/index.vue'
import TransportManagement from '@/views/transport/index.vue'
import FinanceManagement from '@/views/finance/index.vue'
import QualityManagement from '@/views/quality/index.vue'
import Settings from '@/views/settings/index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: Login,
meta: {
title: '登录',
requiresAuth: false
}
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
title: '数据驾驶舱',
icon: 'DataAnalysis'
}
},
{
path: 'user',
name: 'UserManagement',
component: UserManagement,
meta: {
title: '用户管理',
icon: 'User'
}
},
{
path: 'order',
name: 'OrderManagement',
component: OrderManagement,
meta: {
title: '订单管理',
icon: 'ShoppingCart'
}
},
{
path: 'supplier',
name: 'SupplierManagement',
component: SupplierManagement,
meta: {
title: '供应商管理',
icon: 'OfficeBuilding'
}
},
{
path: 'transport',
name: 'TransportManagement',
component: TransportManagement,
meta: {
title: '运输管理',
icon: 'Truck'
}
},
{
path: 'quality',
name: 'QualityManagement',
component: QualityManagement,
meta: {
title: '质量管理',
icon: 'Medal'
}
},
{
path: 'finance',
name: 'FinanceManagement',
component: FinanceManagement,
meta: {
title: '财务管理',
icon: 'Money'
}
},
{
path: 'settings',
name: 'Settings',
component: Settings,
meta: {
title: '系统设置',
icon: 'Setting'
}
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 设置页面标题
document.title = to.meta?.title ? `${to.meta.title} - 活牛采购智能数字化系统` : '活牛采购智能数字化系统'
// 检查是否需要登录
if (to.path !== '/login' && !userStore.isLoggedIn) {
next('/login')
} else if (to.path === '/login' && userStore.isLoggedIn) {
next('/')
} else {
next()
}
})
export default router

View File

@@ -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<string>(localStorage.getItem('token') || '')
const userInfo = ref<User | null>(null)
const permissions = ref<string[]>([])
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const avatar = computed(() => userInfo.value?.avatar || '/default-avatar.png')
const username = computed(() => userInfo.value?.username || '')
const role = computed(() => userInfo.value?.role || '')
// 登录
const loginAction = async (loginForm: LoginForm) => {
try {
const response = await login(loginForm)
const { access_token, user } = response.data
token.value = access_token
userInfo.value = user
// 保存到本地存储
localStorage.setItem('token', access_token)
localStorage.setItem('userInfo', JSON.stringify(user))
ElMessage.success('登录成功')
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
}
})

View File

@@ -0,0 +1,191 @@
// 全局样式文件
@import './variables.scss';
@import './mixins.scss';
// 全局重置样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
font-size: 14px;
color: #333;
background-color: #f0f2f5;
}
// 清除默认样式
ul, ol {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
// 通用工具类
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
.w-full { width: 100%; }
.h-full { height: 100%; }
// 间距工具类
@for $i from 0 through 40 {
.mt-#{$i} { margin-top: #{$i}px; }
.mb-#{$i} { margin-bottom: #{$i}px; }
.ml-#{$i} { margin-left: #{$i}px; }
.mr-#{$i} { margin-right: #{$i}px; }
.pt-#{$i} { padding-top: #{$i}px; }
.pb-#{$i} { padding-bottom: #{$i}px; }
.pl-#{$i} { padding-left: #{$i}px; }
.pr-#{$i} { padding-right: #{$i}px; }
}
// Element Plus 自定义样式
.el-card {
border-radius: 8px;
border: none;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-button {
border-radius: 6px;
}
.el-input__wrapper {
border-radius: 6px;
}
.el-select .el-input__wrapper {
border-radius: 6px;
}
.el-table {
border-radius: 8px;
overflow: hidden;
.el-table__header-wrapper {
th {
background-color: #fafafa;
color: #333;
font-weight: 600;
}
}
.el-table__row {
&:hover > td {
background-color: #f5f7fa;
}
}
}
.el-pagination {
margin-top: 20px;
justify-content: center;
}
.el-dialog {
border-radius: 12px;
.el-dialog__header {
padding: 20px 20px 10px;
border-bottom: 1px solid #e6e6e6;
}
.el-dialog__body {
padding: 20px;
}
}
.el-form {
.el-form-item__label {
color: #333;
font-weight: 500;
}
}
// 自定义滚动条
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
// 动画效果
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
// 响应式设计
@media (max-width: 768px) {
.mobile-hidden {
display: none !important;
}
.el-table {
font-size: 12px;
}
.el-dialog {
width: 90% !important;
margin: 5vh auto !important;
}
}

View File

@@ -0,0 +1,184 @@
// SCSS Mixins
// 清除浮动
@mixin clearfix {
&::after {
content: "";
display: table;
clear: both;
}
}
// 文本省略
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 多行文本省略
@mixin ellipsis-multiline($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
// 绝对居中
@mixin absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
// Flex 居中
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// 响应式断点
@mixin mobile {
@media (max-width: 767px) {
@content;
}
}
@mixin tablet {
@media (min-width: 768px) and (max-width: 1023px) {
@content;
}
}
@mixin desktop {
@media (min-width: 1024px) {
@content;
}
}
// 卡片样式
@mixin card-style {
background: white;
border-radius: $border-radius-large;
box-shadow: $box-shadow-light;
padding: $spacing-lg;
}
// 按钮基础样式
@mixin button-base {
display: inline-flex;
align-items: center;
justify-content: center;
padding: $spacing-sm $spacing-md;
border: none;
border-radius: $border-radius-base;
font-size: $font-size-small;
font-weight: 500;
cursor: pointer;
transition: $transition-base;
text-decoration: none;
outline: none;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
// 主要按钮样式
@mixin button-primary {
@include button-base;
background-color: $primary-color;
color: white;
&:hover:not(:disabled) {
background-color: $primary-light;
}
&:active {
background-color: $primary-dark;
}
}
// 次要按钮样式
@mixin button-secondary {
@include button-base;
background-color: transparent;
color: $primary-color;
border: 1px solid $primary-color;
&:hover:not(:disabled) {
background-color: $primary-color;
color: white;
}
}
// 表格样式
@mixin table-style {
width: 100%;
border-collapse: collapse;
border-radius: $border-radius-large;
overflow: hidden;
box-shadow: $box-shadow-light;
th, td {
padding: $spacing-md;
text-align: left;
border-bottom: 1px solid $border-lighter;
}
th {
background-color: $background-base;
font-weight: 600;
color: $text-primary;
}
tr:hover {
background-color: $background-light;
}
}
// 输入框样式
@mixin input-style {
width: 100%;
padding: $spacing-sm $spacing-md;
border: 1px solid $border-base;
border-radius: $border-radius-base;
font-size: $font-size-small;
color: $text-primary;
background-color: white;
transition: $transition-border;
&:focus {
outline: none;
border-color: $primary-color;
box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
}
&::placeholder {
color: $text-placeholder;
}
&:disabled {
background-color: $background-base;
cursor: not-allowed;
}
}
// 加载动画
@mixin loading-spin {
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
animation: spin 1s linear infinite;
}
// 渐变背景
@mixin gradient-background($start-color, $end-color, $direction: to right) {
background: linear-gradient($direction, $start-color, $end-color);
}

View File

@@ -0,0 +1,65 @@
// SCSS 变量定义
// 颜色变量
$primary-color: #4CAF50;
$primary-light: #81C784;
$primary-dark: #388E3C;
$success-color: #67C23A;
$warning-color: #E6A23C;
$danger-color: #F56C6C;
$info-color: #409EFF;
$text-primary: #303133;
$text-regular: #606266;
$text-secondary: #909399;
$text-placeholder: #C0C4CC;
$border-base: #DCDFE6;
$border-light: #E4E7ED;
$border-lighter: #EBEEF5;
$border-extra-light: #F2F6FC;
$background-base: #F5F7FA;
$background-light: #FAFCFF;
// 尺寸变量
$header-height: 60px;
$sidebar-width: 240px;
$sidebar-collapsed-width: 64px;
// 字体大小
$font-size-extra-small: 12px;
$font-size-small: 14px;
$font-size-base: 16px;
$font-size-medium: 18px;
$font-size-large: 20px;
$font-size-extra-large: 24px;
// 间距
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
// 圆角
$border-radius-small: 4px;
$border-radius-base: 6px;
$border-radius-large: 8px;
$border-radius-round: 20px;
// 阴影
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
$box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
// 过渡动画
$transition-base: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
$transition-fade: opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
$transition-border: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
// Z-index 层级
$z-index-normal: 1;
$z-index-top: 1000;
$z-index-popper: 2000;

View File

@@ -0,0 +1,75 @@
// 订单相关类型定义
export interface Order {
id: number
orderNo: string
buyerId: number
buyerName: string
supplierId: number
supplierName: string
traderId?: number
traderName?: string
cattleBreed: string
cattleCount: number
expectedWeight: number
actualWeight?: number
unitPrice: number
totalAmount: number
paidAmount: number
remainingAmount: number
status: OrderStatus
deliveryAddress: string
expectedDeliveryDate: string
actualDeliveryDate?: string
notes?: string
createdAt: string
updatedAt: string
}
export type OrderStatus =
| 'pending' // 待确认
| 'confirmed' // 已确认
| 'preparing' // 准备中
| 'shipping' // 运输中
| 'delivered' // 已送达
| 'accepted' // 已验收
| 'completed' // 已完成
| 'cancelled' // 已取消
| 'refunded' // 已退款
export interface OrderListParams {
page?: number
pageSize?: number
orderNo?: string
buyerId?: number
supplierId?: number
status?: OrderStatus
startDate?: string
endDate?: string
}
export interface OrderCreateForm {
buyerId: number
supplierId: number
traderId?: number
cattleBreed: string
cattleCount: number
expectedWeight: number
unitPrice: number
deliveryAddress: string
expectedDeliveryDate: string
notes?: string
}
export interface OrderUpdateForm {
cattleBreed?: string
cattleCount?: number
expectedWeight?: number
actualWeight?: number
unitPrice?: number
deliveryAddress?: string
expectedDeliveryDate?: string
actualDeliveryDate?: string
notes?: string
status?: OrderStatus
}

View File

@@ -0,0 +1,52 @@
// 用户相关类型定义
export interface User {
id: number
username: string
email: string
phone?: string
avatar?: string
role: string
status: 'active' | 'inactive' | 'banned'
createdAt: string
updatedAt: string
}
export interface LoginForm {
username: string
password: string
captcha?: string
}
export interface LoginResponse {
access_token: string
token_type: string
expires_in: number
user: User
}
export interface UserListParams {
page?: number
pageSize?: number
keyword?: string
role?: string
status?: string
}
export interface UserCreateForm {
username: string
email: string
phone?: string
password: string
role: string
status: 'active' | 'inactive'
}
export interface UserUpdateForm {
username?: string
email?: string
phone?: string
role?: string
status?: 'active' | 'inactive' | 'banned'
avatar?: string
}

View File

@@ -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<T = any> {
success: boolean
data: T
message: string
timestamp: string
}
// 分页响应类型
export interface PaginatedResponse<T = any> {
items: T[]
total: number
page: number
pageSize: number
totalPages: number
}

View File

@@ -0,0 +1,416 @@
<template>
<div class="dashboard">
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-cards">
<el-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.key">
<el-card class="stat-card" :body-style="{ padding: '20px' }">
<div class="stat-content">
<div class="stat-icon" :style="{ backgroundColor: stat.color }">
<el-icon :size="24"><component :is="stat.icon" /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
<el-icon><component :is="stat.trend > 0 ? 'TrendCharts' : 'Bottom'" /></el-icon>
{{ Math.abs(stat.trend) }}%
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" class="charts-section">
<el-col :lg="16">
<el-card title="订单趋势" class="chart-card">
<div ref="orderTrendChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :lg="8">
<el-card title="订单状态分布" class="chart-card">
<div ref="orderStatusChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格区域 -->
<el-row :gutter="20" class="tables-section">
<el-col :lg="12">
<el-card title="最近订单" class="table-card">
<el-table :data="recentOrders" size="small">
<el-table-column prop="orderNo" label="订单号" width="120" />
<el-table-column prop="supplierName" label="供应商" />
<el-table-column prop="cattleCount" label="数量" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :lg="12">
<el-card title="供应商排行" class="table-card">
<el-table :data="topSuppliers" size="small">
<el-table-column type="index" label="排名" width="60" />
<el-table-column prop="name" label="供应商名称" />
<el-table-column prop="orderCount" label="订单数" width="80" />
<el-table-column prop="totalAmount" label="总金额" width="120">
<template #default="{ row }">
¥{{ row.totalAmount.toLocaleString() }}
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
// 统计数据
const stats = ref([
{
key: 'totalOrders',
label: '总订单数',
value: '1,234',
icon: 'ShoppingCart',
color: '#409EFF',
trend: 12.5
},
{
key: 'completedOrders',
label: '已完成订单',
value: '856',
icon: 'CircleCheck',
color: '#67C23A',
trend: 8.3
},
{
key: 'totalAmount',
label: '总交易额',
value: '¥2.34M',
icon: 'Money',
color: '#E6A23C',
trend: 15.7
},
{
key: 'activeSuppliers',
label: '活跃供应商',
value: '168',
icon: 'OfficeBuilding',
color: '#F56C6C',
trend: -2.1
}
])
// 最近订单
const recentOrders = ref([
{
orderNo: 'ORD20240101001',
supplierName: '山东畜牧合作社',
cattleCount: 50,
status: 'shipping'
},
{
orderNo: 'ORD20240101002',
supplierName: '河北养殖基地',
cattleCount: 30,
status: 'completed'
},
{
orderNo: 'ORD20240101003',
supplierName: '内蒙古牧场',
cattleCount: 80,
status: 'pending'
}
])
// 供应商排行
const topSuppliers = ref([
{
name: '山东畜牧合作社',
orderCount: 45,
totalAmount: 1250000
},
{
name: '河北养殖基地',
orderCount: 38,
totalAmount: 980000
},
{
name: '内蒙古牧场',
orderCount: 32,
totalAmount: 850000
}
])
// 图表元素引用
const orderTrendChart = ref<HTMLElement>()
const orderStatusChart = ref<HTMLElement>()
// 获取状态类型
const getStatusType = (status: string) => {
const statusMap: Record<string, string> = {
pending: 'warning',
confirmed: 'info',
shipping: 'primary',
completed: 'success',
cancelled: 'danger'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
pending: '待确认',
confirmed: '已确认',
shipping: '运输中',
completed: '已完成',
cancelled: '已取消'
}
return statusMap[status] || status
}
// 初始化订单趋势图表
const initOrderTrendChart = () => {
if (!orderTrendChart.value) return
const chart = echarts.init(orderTrendChart.value)
const option = {
title: {
text: '近30天订单趋势',
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['订单数量', '完成订单']
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '订单数量',
type: 'line',
data: [120, 132, 101, 134, 90, 230, 210, 180, 160, 190, 200, 220],
smooth: true,
itemStyle: {
color: '#409EFF'
}
},
{
name: '完成订单',
type: 'line',
data: [100, 120, 90, 120, 80, 200, 190, 160, 140, 170, 180, 200],
smooth: true,
itemStyle: {
color: '#67C23A'
}
}
]
}
chart.setOption(option)
// 响应式调整
window.addEventListener('resize', () => {
chart.resize()
})
}
// 初始化订单状态图表
const initOrderStatusChart = () => {
if (!orderStatusChart.value) return
const chart = echarts.init(orderStatusChart.value)
const option = {
title: {
text: '订单状态分布',
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [
{
name: '订单状态',
type: 'pie',
radius: ['50%', '70%'],
data: [
{ value: 335, name: '已完成', itemStyle: { color: '#67C23A' } },
{ value: 210, name: '运输中', itemStyle: { color: '#409EFF' } },
{ value: 180, name: '已确认', itemStyle: { color: '#E6A23C' } },
{ value: 130, name: '待确认', itemStyle: { color: '#F56C6C' } },
{ value: 45, name: '已取消', itemStyle: { color: '#909399' } }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
chart.setOption(option)
// 响应式调整
window.addEventListener('resize', () => {
chart.resize()
})
}
onMounted(() => {
nextTick(() => {
initOrderTrendChart()
initOrderStatusChart()
})
})
</script>
<style lang="scss" scoped>
.dashboard {
.stats-cards {
margin-bottom: 20px;
}
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-right: 15px;
}
.stat-info {
flex: 1;
.stat-number {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.stat-trend {
font-size: 12px;
display: flex;
align-items: center;
gap: 2px;
&.up {
color: #67C23A;
}
&.down {
color: #F56C6C;
}
}
}
}
}
.charts-section {
margin-bottom: 20px;
}
.chart-card {
border-radius: 8px;
.chart-container {
height: 300px;
}
}
.tables-section {
.table-card {
border-radius: 8px;
:deep(.el-card__header) {
border-bottom: 1px solid #f0f0f0;
padding: 15px 20px;
.card-header {
font-size: 16px;
font-weight: 500;
color: #333;
}
}
}
}
}
@media (max-width: 768px) {
.dashboard {
.stats-cards {
.stat-card {
margin-bottom: 15px;
.stat-content {
.stat-icon {
width: 50px;
height: 50px;
margin-right: 10px;
}
.stat-info {
.stat-number {
font-size: 20px;
}
}
}
}
}
.chart-container {
height: 250px !important;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="finance-management">
<el-card>
<div class="page-header">
<h2>财务管理</h2>
<p>订单结算支付管理和财务报表</p>
</div>
<el-empty description="财务管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 财务管理页面
</script>
<style lang="scss" scoped>
.finance-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<h1 class="title">活牛采购智能数字化系统</h1>
</div>
<p class="subtitle">管理后台</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="rules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
size="large"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-btn"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<p class="demo-account">
<strong>演示账号</strong>
</p>
<div class="demo-list">
<el-tag @click="setDemoAccount('admin', 'admin123')">管理员: admin / admin123</el-tag>
<el-tag type="success" @click="setDemoAccount('buyer', 'buyer123')">采购人: buyer / buyer123</el-tag>
<el-tag type="warning" @click="setDemoAccount('trader', 'trader123')">贸易商: trader / trader123</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import type { LoginForm } from '@/types/user'
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref<FormInstance>()
const loading = ref(false)
// 表单数据
const loginForm = reactive<LoginForm>({
username: '',
password: ''
})
// 表单验证规则
const rules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
// 登录处理
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
loading.value = true
await userStore.loginAction(loginForm)
ElMessage.success('登录成功!')
router.push('/')
} catch (error) {
console.error('登录失败:', error)
} finally {
loading.value = false
}
}
// 设置演示账号
const setDemoAccount = (username: string, password: string) => {
loginForm.username = username
loginForm.password = password
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-box {
width: 100%;
max-width: 400px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.login-header {
text-align: center;
padding: 40px 40px 30px;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
.logo-img {
width: 40px;
height: 40px;
margin-right: 10px;
}
.title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
}
.subtitle {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
}
.login-form {
padding: 40px;
.login-btn {
width: 100%;
height: 44px;
font-size: 16px;
border-radius: 6px;
}
}
.login-footer {
padding: 0 40px 40px;
text-align: center;
.demo-account {
font-size: 14px;
color: #666;
margin-bottom: 15px;
}
.demo-list {
display: flex;
flex-direction: column;
gap: 10px;
.el-tag {
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
}
}
@media (max-width: 480px) {
.login-container {
padding: 10px;
}
.login-header {
padding: 30px 30px 20px;
.logo .title {
font-size: 16px;
}
}
.login-form {
padding: 30px;
}
.login-footer {
padding: 0 30px 30px;
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="order-management">
<el-card>
<div class="page-header">
<h2>订单管理</h2>
<p>管理活牛采购订单的全生命周期流程</p>
</div>
<el-empty description="订单管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 订单管理页面
</script>
<style lang="scss" scoped>
.order-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="quality-management">
<el-card>
<div class="page-header">
<h2>质量管理</h2>
<p>牛只质量检验检疫证明管理和质量追溯</p>
</div>
<el-empty description="质量管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 质量管理页面
</script>
<style lang="scss" scoped>
.quality-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="settings">
<el-card>
<div class="page-header">
<h2>系统设置</h2>
<p>系统配置权限管理和参数设置</p>
</div>
<el-empty description="系统设置功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 系统设置页面
</script>
<style lang="scss" scoped>
.settings {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="supplier-management">
<el-card>
<div class="page-header">
<h2>供应商管理</h2>
<p>管理供应商信息资质认证和绩效评估</p>
</div>
<el-empty description="供应商管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 供应商管理页面
</script>
<style lang="scss" scoped>
.supplier-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="transport-management">
<el-card>
<div class="page-header">
<h2>运输管理</h2>
<p>实时跟踪运输过程监控车辆位置和牛只状态</p>
</div>
<el-empty description="运输管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 运输管理页面
</script>
<style lang="scss" scoped>
.transport-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,517 @@
<template>
<div class="user-management">
<el-card class="search-card">
<div class="search-form">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="用户名">
<el-input v-model="searchForm.keyword" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
<el-option label="管理员" value="admin" />
<el-option label="采购人" value="buyer" />
<el-option label="贸易商" value="trader" />
<el-option label="供应商" value="supplier" />
<el-option label="司机" value="driver" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="正常" value="active" />
<el-option label="禁用" value="inactive" />
<el-option label="封禁" value="banned" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>用户列表</span>
<el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="role" label="角色">
<template #default="{ row }">
<el-tag :type="getRoleType(row.role)">
{{ getRoleText(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<!-- 用户表单弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色">
<el-option label="管理员" value="admin" />
<el-option label="采购人" value="buyer" />
<el-option label="贸易商" value="trader" />
<el-option label="供应商" value="supplier" />
<el-option label="司机" value="driver" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="正常" value="active" />
<el-option label="禁用" value="inactive" />
</el-select>
</el-form-item>
<el-form-item v-if="!form.id" label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { User, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 搜索表单
const searchForm = reactive<UserListParams>({
keyword: '',
role: '',
status: ''
})
// 分页配置
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<User[]>([])
const selectedUsers = ref<User[]>([])
// 表单数据
const form = reactive<UserCreateForm & { id?: number }>({
username: '',
email: '',
phone: '',
password: '',
role: '',
status: 'active'
})
// 对话框标题
const dialogTitle = ref('新增用户')
// 表单验证规则
const rules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
// 模拟数据
const mockUsers: User[] = [
{
id: 1,
username: 'admin',
email: 'admin@example.com',
phone: '13800138000',
role: 'admin',
status: 'active',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
},
{
id: 2,
username: 'buyer01',
email: 'buyer01@example.com',
phone: '13800138001',
role: 'buyer',
status: 'active',
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z'
},
{
id: 3,
username: 'supplier01',
email: 'supplier01@example.com',
phone: '13800138002',
role: 'supplier',
status: 'inactive',
createdAt: '2024-01-03T00:00:00Z',
updatedAt: '2024-01-03T00:00:00Z'
}
]
// 获取用户列表
const getUserList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
// 简单的过滤逻辑
let filteredUsers = [...mockUsers]
if (searchForm.keyword) {
filteredUsers = filteredUsers.filter(user =>
user.username.includes(searchForm.keyword!) ||
user.email.includes(searchForm.keyword!)
)
}
if (searchForm.role) {
filteredUsers = filteredUsers.filter(user => user.role === searchForm.role)
}
if (searchForm.status) {
filteredUsers = filteredUsers.filter(user => user.status === searchForm.status)
}
// 分页处理
const start = (pagination.page - 1) * pagination.pageSize
const end = start + pagination.pageSize
tableData.value = filteredUsers.slice(start, end)
pagination.total = filteredUsers.length
} catch (error) {
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getUserList()
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
role: '',
status: ''
})
handleSearch()
}
// 新增用户
const handleAdd = () => {
dialogTitle.value = '新增用户'
resetForm()
dialogVisible.value = true
}
// 编辑用户
const handleEdit = (row: User) => {
dialogTitle.value = '编辑用户'
Object.assign(form, {
id: row.id,
username: row.username,
email: row.email,
phone: row.phone,
role: row.role,
status: row.status
})
dialogVisible.value = true
}
// 删除用户
const handleDelete = async (row: User) => {
try {
await ElMessageBox.confirm(
`确定要删除用户 "${row.username}" 吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 模拟删除
ElMessage.success('删除成功')
getUserList()
} catch (error) {
// 用户取消操作
}
}
// 重置密码
const handleResetPassword = async (row: User) => {
try {
await ElMessageBox.confirm(
`确定要重置用户 "${row.username}" 的密码吗?`,
'重置密码确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 模拟重置密码
ElMessage.success('密码重置成功新密码为123456')
} catch (error) {
// 用户取消操作
}
}
// 表格选择变化
const handleSelectionChange = (selection: User[]) => {
selectedUsers.value = selection
}
// 分页大小变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
pagination.page = 1
getUserList()
}
// 当前页变化
const handleCurrentChange = (page: number) => {
pagination.page = page
getUserList()
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
submitLoading.value = true
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success(form.id ? '更新成功' : '创建成功')
dialogVisible.value = false
getUserList()
} catch (error) {
console.error('表单验证失败:', error)
} finally {
submitLoading.value = false
}
}
// 关闭对话框
const handleDialogClose = () => {
formRef.value?.resetFields()
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: undefined,
username: '',
email: '',
phone: '',
password: '',
role: '',
status: 'active'
})
}
// 获取角色类型
const getRoleType = (role: string) => {
const roleMap: Record<string, string> = {
admin: 'danger',
buyer: 'primary',
trader: 'success',
supplier: 'warning',
driver: 'info'
}
return roleMap[role] || 'info'
}
// 获取角色文本
const getRoleText = (role: string) => {
const roleMap: Record<string, string> = {
admin: '管理员',
buyer: '采购人',
trader: '贸易商',
supplier: '供应商',
driver: '司机'
}
return roleMap[role] || role
}
// 获取状态类型
const getStatusType = (status: string) => {
const statusMap: Record<string, string> = {
active: 'success',
inactive: 'warning',
banned: 'danger'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
active: '正常',
inactive: '禁用',
banned: '封禁'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
onMounted(() => {
getUserList()
})
</script>
<style lang="scss" scoped>
.user-management {
.search-card {
margin-bottom: 20px;
.search-form {
.demo-form-inline {
.el-form-item {
margin-right: 20px;
margin-bottom: 0;
}
}
}
}
.table-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
@media (max-width: 768px) {
.user-management {
.search-form {
.demo-form-inline {
.el-form-item {
margin-right: 0;
margin-bottom: 15px;
width: 100%;
}
}
}
.el-table {
.el-table-column {
&:nth-child(n+4) {
display: none;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,53 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue",
"auto-imports.d.ts",
"components.d.ts"
],
"exclude": [
"src/**/__tests__/*",
"node_modules",
"dist"
],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/views/*": ["./src/views/*"],
"@/utils/*": ["./src/utils/*"],
"@/api/*": ["./src/api/*"],
"@/stores/*": ["./src/stores/*"],
"@/router/*": ["./src/router/*"],
"@/types/*": ["./src/types/*"],
"@/assets/*": ["./src/assets/*"]
},
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": false,
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"skipLibCheck": true,
"isolatedModules": true,
"noEmit": true,
"useDefineForClassFields": true,
"resolveJsonModule": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"removeComments": true,
"types": ["vite/client", "element-plus/global", "node"]
}
}

103
admin-system/vite.config.ts Normal file
View File

@@ -0,0 +1,103 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [ElementPlusResolver()],
dts: true,
eslintrc: {
enabled: true
}
}),
Components({
resolvers: [ElementPlusResolver()],
dts: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@/components': resolve(__dirname, 'src/components'),
'@/views': resolve(__dirname, 'src/views'),
'@/utils': resolve(__dirname, 'src/utils'),
'@/api': resolve(__dirname, 'src/api'),
'@/stores': resolve(__dirname, 'src/stores'),
'@/router': resolve(__dirname, 'src/router'),
'@/types': resolve(__dirname, 'src/types'),
'@/assets': resolve(__dirname, 'src/assets')
}
},
server: {
host: '0.0.0.0',
port: 3000,
open: true,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:3001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: mode === 'development',
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.css')) {
return 'css/[name]-[hash].css'
}
return 'assets/[name]-[hash].[ext]'
},
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus', '@element-plus/icons-vue'],
'utils': ['axios', 'nprogress']
}
}
},
terserOptions: {
compress: {
drop_console: mode === 'production',
drop_debugger: mode === 'production'
}
}
},
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'element-plus', 'axios']
},
define: {
__VUE_OPTIONS_API__: false,
__VUE_PROD_DEVTOOLS__: false
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/styles/variables.scss"; @import "@/assets/styles/mixins.scss";`
}
}
}
}
})