添加 IntelliJ IDEA 项目配置文件
This commit is contained in:
68
admin-system/.env.development
Normal file
68
admin-system/.env.development
Normal file
@@ -0,0 +1,68 @@
|
||||
# 开发环境配置
|
||||
NODE_ENV=development
|
||||
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台
|
||||
|
||||
# API接口地址
|
||||
VITE_API_BASE_URL=http://localhost: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
|
||||
77
admin-system/.env.production
Normal file
77
admin-system/.env.production
Normal file
@@ -0,0 +1,77 @@
|
||||
# 生产环境配置
|
||||
NODE_ENV=production
|
||||
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台
|
||||
|
||||
# API接口地址
|
||||
VITE_API_BASE_URL=https://api.niumall.com/api
|
||||
|
||||
# WebSocket地址
|
||||
VITE_WS_BASE_URL=wss://api.niumall.com
|
||||
|
||||
# 上传文件地址
|
||||
VITE_UPLOAD_URL=https://api.niumall.com/api/upload
|
||||
|
||||
# 静态资源地址
|
||||
VITE_STATIC_URL=https://static.niumall.com
|
||||
|
||||
# 是否启用Mock数据
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否启用开发工具
|
||||
VITE_DEV_TOOLS=false
|
||||
|
||||
# 路由模式 hash | history
|
||||
VITE_ROUTER_MODE=history
|
||||
|
||||
# 应用端口
|
||||
VITE_PORT=80
|
||||
|
||||
# 代理前缀
|
||||
VITE_API_PREFIX=/api
|
||||
|
||||
# Token密钥
|
||||
VITE_TOKEN_KEY=admin_token
|
||||
|
||||
# Token过期时间(小时)
|
||||
VITE_TOKEN_EXPIRES=24
|
||||
|
||||
# 分页大小
|
||||
VITE_PAGE_SIZE=20
|
||||
|
||||
# 上传文件最大大小(MB)
|
||||
VITE_UPLOAD_MAX_SIZE=10
|
||||
|
||||
# 是否显示设置按钮
|
||||
VITE_SHOW_SETTINGS=false
|
||||
|
||||
# 是否显示标签页
|
||||
VITE_SHOW_TABS=true
|
||||
|
||||
# 是否显示面包屑
|
||||
VITE_SHOW_BREADCRUMB=true
|
||||
|
||||
# 是否固定头部
|
||||
VITE_FIXED_HEADER=true
|
||||
|
||||
# 侧边栏Logo
|
||||
VITE_SIDEBAR_LOGO=true
|
||||
|
||||
# 默认主题色
|
||||
VITE_THEME_COLOR=#409eff
|
||||
|
||||
# 默认布局
|
||||
VITE_LAYOUT=default
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# CDN地址
|
||||
VITE_CDN_URL=https://cdn.niumall.com
|
||||
|
||||
# 错误日志上报地址
|
||||
VITE_ERROR_LOG_URL=https://api.niumall.com/api/error-log
|
||||
|
||||
# 性能监控地址
|
||||
VITE_PERFORMANCE_URL=https://api.niumall.com/api/performance
|
||||
107
admin-system/.eslintrc.js
Normal file
107
admin-system/.eslintrc.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:vue/vue3-strongly-recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint'],
|
||||
globals: {
|
||||
ElMessage: 'readonly',
|
||||
ElMessageBox: 'readonly',
|
||||
ElNotification: 'readonly',
|
||||
ElLoading: 'readonly'
|
||||
},
|
||||
rules: {
|
||||
// Vue规则
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/component-tags-order': [
|
||||
'error',
|
||||
{
|
||||
order: ['script', 'template', 'style']
|
||||
}
|
||||
],
|
||||
'vue/component-name-in-template-casing': [
|
||||
'error',
|
||||
'PascalCase',
|
||||
{
|
||||
registeredComponentsOnly: false
|
||||
}
|
||||
],
|
||||
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
||||
'vue/define-emits-declaration': 'error',
|
||||
'vue/define-props-declaration': 'error',
|
||||
'vue/html-button-has-type': 'error',
|
||||
'vue/no-unused-refs': 'error',
|
||||
'vue/no-useless-v-bind': 'error',
|
||||
'vue/prefer-separate-static-class': 'error',
|
||||
'vue/prefer-true-attribute-shorthand': 'error',
|
||||
'vue/block-order': [
|
||||
'error',
|
||||
{
|
||||
order: ['script', 'template', 'style']
|
||||
}
|
||||
],
|
||||
|
||||
// TypeScript规则
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
|
||||
// JavaScript规则
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'prefer-template': 'error',
|
||||
'template-curly-spacing': 'error',
|
||||
'yield-star-spacing': 'error',
|
||||
'prefer-rest-params': 'error',
|
||||
'no-useless-escape': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
'no-fallthrough': 'error',
|
||||
'no-extra-boolean-cast': 'error',
|
||||
'no-case-declarations': 'error',
|
||||
'no-async-promise-executor': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'],
|
||||
extends: ['plugin:cypress/recommended']
|
||||
},
|
||||
{
|
||||
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
99
admin-system/.gitignore
vendored
Normal file
99
admin-system/.gitignore
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Auto-generated files
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
|
||||
# Local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Test
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
40
admin-system/.prettierrc
Normal file
40
admin-system/.prettierrc
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.vue",
|
||||
"options": {
|
||||
"parser": "vue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"options": {
|
||||
"parser": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json"],
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.scss", "*.css"],
|
||||
"options": {
|
||||
"parser": "scss"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,38 +1,447 @@
|
||||
# Admin System 管理后台
|
||||
# Admin System - 活牛采购智能数字化系统管理后台
|
||||
|
||||
## 技术栈
|
||||
- Vue 3 + TypeScript
|
||||
- Element Plus / Ant Design Vue
|
||||
- Vue Router
|
||||
- Pinia状态管理
|
||||
- Axios HTTP客户端
|
||||
## 📋 项目概述
|
||||
|
||||
活牛采购智能数字化系统管理后台是基于Vue 3 + TypeScript的现代化Web应用,为系统管理员、内部员工提供全面的业务管理和数据分析功能。
|
||||
|
||||
**核心功能:**
|
||||
- 👥 用户权限管理(采购人、贸易商、供应商、司机)
|
||||
- 📦 订单全流程管理和监控
|
||||
- 🚛 运输跟踪和状态监控
|
||||
- 💰 结算财务管理
|
||||
- 📊 数据统计和分析报表
|
||||
- ⚙️ 系统配置和维护
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
| 类别 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| **前端框架** | Vue 3 | ^3.3.0 | Composition API + `<script setup>` |
|
||||
| **开发语言** | TypeScript | ^5.0.0 | 类型安全和智能提示 |
|
||||
| **构建工具** | Vite | ^4.4.0 | 快速开发和构建 |
|
||||
| **UI组件库** | Element Plus | ^2.3.0 | 企业级组件库 |
|
||||
| **状态管理** | Pinia | ^2.1.0 | Vue 3官方推荐 |
|
||||
| **路由管理** | Vue Router | ^4.2.0 | 单页面应用路由 |
|
||||
| **HTTP客户端** | Axios | ^1.4.0 | API请求处理 |
|
||||
| **代码规范** | ESLint + Prettier | latest | 代码质量保证 |
|
||||
| **CSS预处理** | Sass/SCSS | ^1.64.0 | 样式管理 |
|
||||
| **图标库** | @element-plus/icons-vue | ^2.1.0 | 图标组件 |
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
admin-system/
|
||||
├── public/ # 公共资源
|
||||
│ ├── favicon.ico
|
||||
│ └── index.html
|
||||
├── src/
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── store/ # 状态管理
|
||||
│ ├── api/ # API接口
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── assets/ # 静态资源
|
||||
├── public/ # 公共文件
|
||||
├── tests/ # 测试文件
|
||||
├── package.json
|
||||
└── README.md
|
||||
│ ├── api/ # API接口定义
|
||||
│ │ ├── auth.ts # 认证相关API
|
||||
│ │ ├── user.ts # 用户管理API
|
||||
│ │ ├── order.ts # 订单管理API
|
||||
│ │ ├── transport.ts # 运输跟踪API
|
||||
│ │ └── settlement.ts # 结算管理API
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ ├── images/
|
||||
│ │ ├── icons/
|
||||
│ │ └── styles/
|
||||
│ │ ├── index.scss # 全局样式
|
||||
│ │ ├── variables.scss # SCSS变量
|
||||
│ │ └── mixins.scss # SCSS混入
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ ├── common/ # 通用组件
|
||||
│ │ │ ├── AppHeader.vue
|
||||
│ │ │ ├── AppSidebar.vue
|
||||
│ │ │ ├── AppBreadcrumb.vue
|
||||
│ │ │ └── PageContainer.vue
|
||||
│ │ ├── charts/ # 图表组件
|
||||
│ │ │ ├── LineChart.vue
|
||||
│ │ │ ├── BarChart.vue
|
||||
│ │ │ └── PieChart.vue
|
||||
│ │ └── forms/ # 表单组件
|
||||
│ │ ├── UserForm.vue
|
||||
│ │ ├── OrderForm.vue
|
||||
│ │ └── SearchForm.vue
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ │ ├── useAuth.ts # 认证逻辑
|
||||
│ │ ├── useTable.ts # 表格通用逻辑
|
||||
│ │ ├── useDialog.ts # 弹窗逻辑
|
||||
│ │ └── usePermission.ts # 权限控制
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ ├── index.ts # 路由主文件
|
||||
│ │ ├── routes.ts # 路由定义
|
||||
│ │ └── guards.ts # 路由守卫
|
||||
│ ├── stores/ # Pinia状态管理
|
||||
│ │ ├── auth.ts # 认证状态
|
||||
│ │ ├── user.ts # 用户状态
|
||||
│ │ ├── order.ts # 订单状态
|
||||
│ │ └── app.ts # 应用全局状态
|
||||
│ ├── types/ # TypeScript类型定义
|
||||
│ │ ├── api.ts # API响应类型
|
||||
│ │ ├── user.ts # 用户类型
|
||||
│ │ ├── order.ts # 订单类型
|
||||
│ │ └── common.ts # 通用类型
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── request.ts # 请求封装
|
||||
│ │ ├── storage.ts # 本地存储
|
||||
│ │ ├── format.ts # 格式化工具
|
||||
│ │ ├── validator.ts # 表单验证
|
||||
│ │ └── constants.ts # 常量定义
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── auth/ # 认证页面
|
||||
│ │ │ ├── Login.vue
|
||||
│ │ │ └── ForgotPassword.vue
|
||||
│ │ ├── dashboard/ # 仪表盘
|
||||
│ │ │ └── Dashboard.vue
|
||||
│ │ ├── user/ # 用户管理
|
||||
│ │ │ ├── UserList.vue
|
||||
│ │ │ ├── UserDetail.vue
|
||||
│ │ │ └── UserRoles.vue
|
||||
│ │ ├── order/ # 订单管理
|
||||
│ │ │ ├── OrderList.vue
|
||||
│ │ │ ├── OrderDetail.vue
|
||||
│ │ │ ├── OrderFlow.vue
|
||||
│ │ │ └── OrderTracking.vue
|
||||
│ │ ├── transport/ # 运输管理
|
||||
│ │ │ ├── TransportList.vue
|
||||
│ │ │ ├── TransportMap.vue
|
||||
│ │ │ └── VehicleManage.vue
|
||||
│ │ ├── settlement/ # 结算管理
|
||||
│ │ │ ├── SettlementList.vue
|
||||
│ │ │ ├── PaymentRecord.vue
|
||||
│ │ │ └── FinancialReport.vue
|
||||
│ │ ├── system/ # 系统管理
|
||||
│ │ │ ├── SystemConfig.vue
|
||||
│ │ │ ├── LogManage.vue
|
||||
│ │ │ └── DataBackup.vue
|
||||
│ │ └── error/ # 错误页面
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 应用入口
|
||||
├── tests/ # 测试文件
|
||||
│ ├── unit/ # 单元测试
|
||||
│ └── e2e/ # 端到端测试
|
||||
├── .env.development # 开发环境配置
|
||||
├── .env.production # 生产环境配置
|
||||
├── .eslintrc.js # ESLint配置
|
||||
├── .prettierrc # Prettier配置
|
||||
├── tsconfig.json # TypeScript配置
|
||||
├── vite.config.ts # Vite配置
|
||||
├── package.json # 项目依赖
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
- 用户管理
|
||||
- 商品管理
|
||||
- 订单管理
|
||||
- 数据统计
|
||||
- 系统设置
|
||||
## 🚀 快速开始
|
||||
|
||||
## 开发规范
|
||||
1. 使用Composition API
|
||||
2. TypeScript严格模式
|
||||
3. 组件命名规范
|
||||
4. 代码分割和懒加载
|
||||
### 环境要求
|
||||
- Node.js >= 16.0.0
|
||||
- npm >= 8.0.0 或 yarn >= 1.22.0
|
||||
- 现代浏览器(Chrome 90+, Firefox 88+, Safari 14+)
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
cd admin-system
|
||||
npm install
|
||||
# 或
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 开发环境启动
|
||||
```bash
|
||||
npm run dev
|
||||
# 或
|
||||
yarn dev
|
||||
```
|
||||
|
||||
访问地址:http://localhost:3000
|
||||
|
||||
### 生产环境构建
|
||||
```bash
|
||||
npm run build
|
||||
# 或
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 代码检查和格式化
|
||||
```bash
|
||||
# ESLint检查
|
||||
npm run lint
|
||||
|
||||
# 自动修复
|
||||
npm run lint:fix
|
||||
|
||||
# Prettier格式化
|
||||
npm run format
|
||||
```
|
||||
|
||||
### 测试
|
||||
```bash
|
||||
# 单元测试
|
||||
npm run test:unit
|
||||
|
||||
# 端到端测试
|
||||
npm run test:e2e
|
||||
|
||||
# 测试覆盖率
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## 🔐 认证和权限
|
||||
|
||||
### 用户角色
|
||||
| 角色 | 权限描述 | 功能范围 |
|
||||
|------|----------|----------|
|
||||
| **super_admin** | 超级管理员 | 所有功能和系统配置 |
|
||||
| **admin** | 系统管理员 | 用户管理、订单管理、数据查看 |
|
||||
| **operator** | 业务操作员 | 订单处理、运输跟踪 |
|
||||
| **viewer** | 只读用户 | 数据查看和报表导出 |
|
||||
|
||||
### 权限控制
|
||||
- **路由级权限**:基于用户角色控制页面访问
|
||||
- **组件级权限**:控制按钮、菜单项的显示
|
||||
- **数据级权限**:控制数据的增删改查权限
|
||||
- **API级权限**:后端接口权限验证
|
||||
|
||||
### 默认账号
|
||||
```
|
||||
管理员账号:admin / admin123
|
||||
操作员账号:operator / operator123
|
||||
查看员账号:viewer / viewer123
|
||||
```
|
||||
|
||||
## 📊 功能模块
|
||||
|
||||
### 1. 用户管理模块
|
||||
- **用户列表**:查看所有系统用户
|
||||
- **用户详情**:查看和编辑用户信息
|
||||
- **角色管理**:分配和修改用户角色
|
||||
- **权限配置**:细粒度权限控制
|
||||
|
||||
### 2. 订单管理模块
|
||||
- **订单列表**:所有采购订单管理
|
||||
- **订单详情**:订单完整信息展示
|
||||
- **流程监控**:订单状态流转跟踪
|
||||
- **异常处理**:订单异常情况处理
|
||||
|
||||
### 3. 运输跟踪模块
|
||||
- **运输列表**:所有运输任务
|
||||
- **实时地图**:GPS位置实时显示
|
||||
- **车辆管理**:运输车辆信息
|
||||
- **轨迹回放**:历史运输轨迹
|
||||
|
||||
### 4. 结算管理模块
|
||||
- **结算列表**:所有结算记录
|
||||
- **支付记录**:支付流水查询
|
||||
- **财务报表**:各类财务统计
|
||||
- **对账管理**:自动对账功能
|
||||
|
||||
### 5. 数据分析模块
|
||||
- **业务仪表盘**:核心指标展示
|
||||
- **统计报表**:多维度数据分析
|
||||
- **图表可视化**:数据图表展示
|
||||
- **报表导出**:Excel/PDF导出
|
||||
|
||||
## 🔧 开发规范
|
||||
|
||||
### 代码规范
|
||||
- **命名规范**:
|
||||
- 组件名:PascalCase(如 `UserList.vue`)
|
||||
- 文件名:kebab-case(如 `user-list.vue`)
|
||||
- 变量名:camelCase(如 `userName`)
|
||||
- 常量名:UPPER_SNAKE_CASE(如 `API_BASE_URL`)
|
||||
|
||||
- **组件开发**:
|
||||
- 使用 Composition API + `<script setup>`
|
||||
- 统一使用 TypeScript
|
||||
- 组件props必须定义类型
|
||||
- 使用defineEmits定义事件
|
||||
|
||||
- **样式规范**:
|
||||
- 使用SCSS语法
|
||||
- 遵循BEM命名规范
|
||||
- 使用CSS变量定义主题色彩
|
||||
- 响应式设计适配
|
||||
|
||||
### Git提交规范
|
||||
```
|
||||
feat: 新增功能
|
||||
fix: 修复bug
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 测试相关
|
||||
chore: 其他修改
|
||||
```
|
||||
|
||||
### 目录命名规范
|
||||
- 页面组件放在 `views/` 目录
|
||||
- 公共组件放在 `components/` 目录
|
||||
- API接口放在 `api/` 目录
|
||||
- 工具函数放在 `utils/` 目录
|
||||
- 类型定义放在 `types/` 目录
|
||||
|
||||
## 🌐 API接口
|
||||
|
||||
### 接口基础配置
|
||||
```typescript
|
||||
// 开发环境
|
||||
VITE_API_BASE_URL=http://localhost:3001/api
|
||||
VITE_WS_BASE_URL=ws://localhost:3001
|
||||
|
||||
// 生产环境
|
||||
VITE_API_BASE_URL=https://api.niumall.com/api
|
||||
VITE_WS_BASE_URL=wss://api.niumall.com
|
||||
```
|
||||
|
||||
### 核心接口模块
|
||||
- **认证接口**:登录、登出、刷新token
|
||||
- **用户接口**:用户CRUD、角色管理
|
||||
- **订单接口**:订单管理、状态更新
|
||||
- **运输接口**:GPS跟踪、状态上报
|
||||
- **结算接口**:支付、对账、报表
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
### 断点定义
|
||||
```scss
|
||||
$breakpoints: (
|
||||
xs: 0,
|
||||
sm: 576px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1200px,
|
||||
xxl: 1400px
|
||||
);
|
||||
```
|
||||
|
||||
### 适配策略
|
||||
- **桌面端**:1200px+ 完整功能
|
||||
- **平板端**:768px-1199px 简化布局
|
||||
- **手机端**:<768px 移动优化
|
||||
|
||||
## 🔍 性能优化
|
||||
|
||||
### 构建优化
|
||||
- **代码分割**:路由级别的懒加载
|
||||
- **资源压缩**:Gzip/Brotli压缩
|
||||
- **缓存策略**:浏览器缓存配置
|
||||
- **CDN加速**:静态资源CDN
|
||||
|
||||
### 运行时优化
|
||||
- **虚拟列表**:大数据列表优化
|
||||
- **图片懒加载**:减少初始加载
|
||||
- **防抖节流**:高频操作优化
|
||||
- **内存管理**:避免内存泄漏
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
### 单元测试
|
||||
- **组件测试**:Vue组件功能测试
|
||||
- **工具函数测试**:纯函数测试
|
||||
- **Store测试**:状态管理测试
|
||||
- **覆盖率要求**:>80%
|
||||
|
||||
### 集成测试
|
||||
- **API集成测试**:接口联调测试
|
||||
- **路由测试**:页面跳转测试
|
||||
- **权限测试**:权限控制测试
|
||||
|
||||
### E2E测试
|
||||
- **关键流程测试**:用户核心操作路径
|
||||
- **浏览器兼容性测试**:多浏览器测试
|
||||
- **性能测试**:页面加载性能
|
||||
|
||||
## 🚀 部署指南
|
||||
|
||||
### 构建命令
|
||||
```bash
|
||||
# 开发环境构建
|
||||
npm run build:dev
|
||||
|
||||
# 测试环境构建
|
||||
npm run build:test
|
||||
|
||||
# 生产环境构建
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
### Nginx配置
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name admin.niumall.com;
|
||||
|
||||
location / {
|
||||
root /var/www/admin-system;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend-server;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker部署
|
||||
```dockerfile
|
||||
FROM nginx:alpine
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
## 🛡️ 安全措施
|
||||
|
||||
### 前端安全
|
||||
- **XSS防护**:输入输出转义
|
||||
- **CSRF防护**:Token验证
|
||||
- **Content Security Policy**:内容安全策略
|
||||
- **敏感信息保护**:避免敏感数据泄露
|
||||
|
||||
### 认证安全
|
||||
- **JWT Token**:无状态认证
|
||||
- **Token刷新**:自动刷新机制
|
||||
- **权限验证**:多级权限控制
|
||||
- **会话管理**:安全会话处理
|
||||
|
||||
## 📈 监控和日志
|
||||
|
||||
### 前端监控
|
||||
- **错误监控**:JavaScript错误捕获
|
||||
- **性能监控**:页面加载性能
|
||||
- **用户行为**:操作行为统计
|
||||
- **接口监控**:API调用监控
|
||||
|
||||
### 日志管理
|
||||
- **操作日志**:用户操作记录
|
||||
- **错误日志**:错误信息收集
|
||||
- **性能日志**:性能指标记录
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. **Fork** 仓库
|
||||
2. **创建**特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. **提交**更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. **推送**到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. **开启** Pull Request
|
||||
|
||||
### 开发流程
|
||||
1. 领取任务并创建分支
|
||||
2. 本地开发和测试
|
||||
3. 代码审查和合并
|
||||
4. 部署测试环境
|
||||
5. 上线生产环境
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
- **技术负责人**:张工程师 (zhang@niumall.com)
|
||||
- **产品经理**:李经理 (li@niumall.com)
|
||||
- **技术群组**:微信群-前端技术交流
|
||||
- **问题反馈**:GitHub Issues
|
||||
|
||||
---
|
||||
|
||||
**🎯 让管理更智能,让业务更高效!**
|
||||
14
admin-system/index.html
Normal file
14
admin-system/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>活牛采购智能数字化系统 - 管理后台</title>
|
||||
<meta name="description" content="活牛采购智能数字化系统管理后台,提供用户管理、订单管理、供应商管理等功能。">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
69
admin-system/package.json
Normal file
69
admin-system/package.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "niumall-admin-system",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "活牛采购智能数字化系统 - 管理后台",
|
||||
"author": "NiuMall Team",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"build:dev": "vue-tsc && vite build --mode development",
|
||||
"build:test": "vue-tsc && vite build --mode test",
|
||||
"build:prod": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "cypress run",
|
||||
"test:e2e:dev": "cypress open",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "^2.3.8",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.4",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/node": "^20.4.5",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^12.17.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.64.1",
|
||||
"typescript": "~5.1.6",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.6",
|
||||
"vitest": "^0.33.0",
|
||||
"vue-tsc": "^1.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
38
admin-system/src/App.vue
Normal file
38
admin-system/src/App.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useUserStore } from './stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 应用初始化时检查登录状态
|
||||
userStore.checkLoginStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 全局样式重置
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
</style>
|
||||
53
admin-system/src/api/order.ts
Normal file
53
admin-system/src/api/order.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse, PaginatedResponse } from '@/utils/request'
|
||||
import type { Order, OrderListParams, OrderCreateForm, OrderUpdateForm } from '@/types/order'
|
||||
|
||||
// 获取订单列表
|
||||
export const getOrderList = (params: OrderListParams): Promise<ApiResponse<PaginatedResponse<Order>>> => {
|
||||
return request.get('/orders', { params })
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
export const getOrderDetail = (id: number): Promise<ApiResponse<Order>> => {
|
||||
return request.get(`/orders/${id}`)
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
export const createOrder = (data: OrderCreateForm): Promise<ApiResponse<Order>> => {
|
||||
return request.post('/orders', data)
|
||||
}
|
||||
|
||||
// 更新订单
|
||||
export const updateOrder = (id: number, data: OrderUpdateForm): Promise<ApiResponse<Order>> => {
|
||||
return request.put(`/orders/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
export const deleteOrder = (id: number): Promise<ApiResponse> => {
|
||||
return request.delete(`/orders/${id}`)
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
export const cancelOrder = (id: number, reason?: string): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/cancel`, { reason })
|
||||
}
|
||||
|
||||
// 确认订单
|
||||
export const confirmOrder = (id: number): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/confirm`)
|
||||
}
|
||||
|
||||
// 订单验收
|
||||
export const acceptOrder = (id: number, data: { actualWeight: number; notes?: string }): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/accept`, data)
|
||||
}
|
||||
|
||||
// 完成订单
|
||||
export const completeOrder = (id: number): Promise<ApiResponse> => {
|
||||
return request.put(`/orders/${id}/complete`)
|
||||
}
|
||||
|
||||
// 获取订单统计数据
|
||||
export const getOrderStatistics = (params?: { startDate?: string; endDate?: string }): Promise<ApiResponse> => {
|
||||
return request.get('/orders/statistics', { params })
|
||||
}
|
||||
53
admin-system/src/api/user.ts
Normal file
53
admin-system/src/api/user.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResponse } from '@/utils/request'
|
||||
import type { User, LoginForm, LoginResponse, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
|
||||
|
||||
// 用户登录
|
||||
export const login = (data: LoginForm): Promise<ApiResponse<LoginResponse>> => {
|
||||
return request.post('/auth/login', data)
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const getUserInfo = (): Promise<ApiResponse<{ user: User; permissions: string[] }>> => {
|
||||
return request.get('/auth/me')
|
||||
}
|
||||
|
||||
// 用户登出
|
||||
export const logout = (): Promise<ApiResponse> => {
|
||||
return request.post('/auth/logout')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
export const getUserList = (params: UserListParams): Promise<ApiResponse> => {
|
||||
return request.get('/users', { params })
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export const createUser = (data: UserCreateForm): Promise<ApiResponse<User>> => {
|
||||
return request.post('/users', data)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
export const updateUser = (id: number, data: UserUpdateForm): Promise<ApiResponse<User>> => {
|
||||
return request.put(`/users/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (id: number): Promise<ApiResponse> => {
|
||||
return request.delete(`/users/${id}`)
|
||||
}
|
||||
|
||||
// 批量删除用户
|
||||
export const batchDeleteUsers = (ids: number[]): Promise<ApiResponse> => {
|
||||
return request.delete('/users/batch', { data: { ids } })
|
||||
}
|
||||
|
||||
// 重置用户密码
|
||||
export const resetUserPassword = (id: number, newPassword: string): Promise<ApiResponse> => {
|
||||
return request.put(`/users/${id}/password`, { password: newPassword })
|
||||
}
|
||||
|
||||
// 启用/禁用用户
|
||||
export const toggleUserStatus = (id: number, status: 'active' | 'inactive' | 'banned'): Promise<ApiResponse> => {
|
||||
return request.put(`/users/${id}/status`, { status })
|
||||
}
|
||||
310
admin-system/src/layouts/index.vue
Normal file
310
admin-system/src/layouts/index.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="isCollapse ? '64px' : '240px'" class="layout-aside">
|
||||
<div class="logo-container">
|
||||
<img v-if="!isCollapse" src="/logo.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
25
admin-system/src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style/index.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册Element Plus图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
132
admin-system/src/router/index.ts
Normal file
132
admin-system/src/router/index.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 布局组件
|
||||
import Layout from '@/layouts/index.vue'
|
||||
|
||||
// 页面组件
|
||||
import Login from '@/views/login/index.vue'
|
||||
import Dashboard from '@/views/dashboard/index.vue'
|
||||
import UserManagement from '@/views/user/index.vue'
|
||||
import OrderManagement from '@/views/order/index.vue'
|
||||
import SupplierManagement from '@/views/supplier/index.vue'
|
||||
import TransportManagement from '@/views/transport/index.vue'
|
||||
import FinanceManagement from '@/views/finance/index.vue'
|
||||
import QualityManagement from '@/views/quality/index.vue'
|
||||
import Settings from '@/views/settings/index.vue'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: {
|
||||
title: '登录',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard,
|
||||
meta: {
|
||||
title: '数据驾驶舱',
|
||||
icon: 'DataAnalysis'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'UserManagement',
|
||||
component: UserManagement,
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
icon: 'User'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
name: 'OrderManagement',
|
||||
component: OrderManagement,
|
||||
meta: {
|
||||
title: '订单管理',
|
||||
icon: 'ShoppingCart'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'supplier',
|
||||
name: 'SupplierManagement',
|
||||
component: SupplierManagement,
|
||||
meta: {
|
||||
title: '供应商管理',
|
||||
icon: 'OfficeBuilding'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'transport',
|
||||
name: 'TransportManagement',
|
||||
component: TransportManagement,
|
||||
meta: {
|
||||
title: '运输管理',
|
||||
icon: 'Truck'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'quality',
|
||||
name: 'QualityManagement',
|
||||
component: QualityManagement,
|
||||
meta: {
|
||||
title: '质量管理',
|
||||
icon: 'Medal'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'FinanceManagement',
|
||||
component: FinanceManagement,
|
||||
meta: {
|
||||
title: '财务管理',
|
||||
icon: 'Money'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
component: Settings,
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
icon: 'Setting'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 设置页面标题
|
||||
document.title = to.meta?.title ? `${to.meta.title} - 活牛采购智能数字化系统` : '活牛采购智能数字化系统'
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.path !== '/login' && !userStore.isLoggedIn) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && userStore.isLoggedIn) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
119
admin-system/src/stores/user.ts
Normal file
119
admin-system/src/stores/user.ts
Normal 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
|
||||
}
|
||||
})
|
||||
191
admin-system/src/style/index.scss
Normal file
191
admin-system/src/style/index.scss
Normal file
@@ -0,0 +1,191 @@
|
||||
// 全局样式文件
|
||||
@import './variables.scss';
|
||||
@import './mixins.scss';
|
||||
|
||||
// 全局重置样式
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
// 清除默认样式
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// 通用工具类
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
|
||||
// 间距工具类
|
||||
@for $i from 0 through 40 {
|
||||
.mt-#{$i} { margin-top: #{$i}px; }
|
||||
.mb-#{$i} { margin-bottom: #{$i}px; }
|
||||
.ml-#{$i} { margin-left: #{$i}px; }
|
||||
.mr-#{$i} { margin-right: #{$i}px; }
|
||||
.pt-#{$i} { padding-top: #{$i}px; }
|
||||
.pb-#{$i} { padding-bottom: #{$i}px; }
|
||||
.pl-#{$i} { padding-left: #{$i}px; }
|
||||
.pr-#{$i} { padding-right: #{$i}px; }
|
||||
}
|
||||
|
||||
// Element Plus 自定义样式
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-select .el-input__wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-table__header-wrapper {
|
||||
th {
|
||||
background-color: #fafafa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
&:hover > td {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 20px 20px 10px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.mobile-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
width: 90% !important;
|
||||
margin: 5vh auto !important;
|
||||
}
|
||||
}
|
||||
184
admin-system/src/style/mixins.scss
Normal file
184
admin-system/src/style/mixins.scss
Normal file
@@ -0,0 +1,184 @@
|
||||
// SCSS Mixins
|
||||
|
||||
// 清除浮动
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本省略
|
||||
@mixin ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 多行文本省略
|
||||
@mixin ellipsis-multiline($lines: 2) {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// 绝对居中
|
||||
@mixin absolute-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
// Flex 居中
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
@mixin mobile {
|
||||
@media (max-width: 767px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tablet {
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: 1024px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
@mixin card-style {
|
||||
background: white;
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: $box-shadow-light;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
// 按钮基础样式
|
||||
@mixin button-base {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: none;
|
||||
border-radius: $border-radius-base;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: $transition-base;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 主要按钮样式
|
||||
@mixin button-primary {
|
||||
@include button-base;
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $primary-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 次要按钮样式
|
||||
@mixin button-secondary {
|
||||
@include button-base;
|
||||
background-color: transparent;
|
||||
color: $primary-color;
|
||||
border: 1px solid $primary-color;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
@mixin table-style {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-radius: $border-radius-large;
|
||||
overflow: hidden;
|
||||
box-shadow: $box-shadow-light;
|
||||
|
||||
th, td {
|
||||
padding: $spacing-md;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid $border-lighter;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: $background-base;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: $background-light;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
@mixin input-style {
|
||||
width: 100%;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid $border-base;
|
||||
border-radius: $border-radius-base;
|
||||
font-size: $font-size-small;
|
||||
color: $text-primary;
|
||||
background-color: white;
|
||||
transition: $transition-border;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $text-placeholder;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: $background-base;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
@mixin loading-spin {
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
// 渐变背景
|
||||
@mixin gradient-background($start-color, $end-color, $direction: to right) {
|
||||
background: linear-gradient($direction, $start-color, $end-color);
|
||||
}
|
||||
65
admin-system/src/style/variables.scss
Normal file
65
admin-system/src/style/variables.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
// SCSS 变量定义
|
||||
|
||||
// 颜色变量
|
||||
$primary-color: #4CAF50;
|
||||
$primary-light: #81C784;
|
||||
$primary-dark: #388E3C;
|
||||
|
||||
$success-color: #67C23A;
|
||||
$warning-color: #E6A23C;
|
||||
$danger-color: #F56C6C;
|
||||
$info-color: #409EFF;
|
||||
|
||||
$text-primary: #303133;
|
||||
$text-regular: #606266;
|
||||
$text-secondary: #909399;
|
||||
$text-placeholder: #C0C4CC;
|
||||
|
||||
$border-base: #DCDFE6;
|
||||
$border-light: #E4E7ED;
|
||||
$border-lighter: #EBEEF5;
|
||||
$border-extra-light: #F2F6FC;
|
||||
|
||||
$background-base: #F5F7FA;
|
||||
$background-light: #FAFCFF;
|
||||
|
||||
// 尺寸变量
|
||||
$header-height: 60px;
|
||||
$sidebar-width: 240px;
|
||||
$sidebar-collapsed-width: 64px;
|
||||
|
||||
// 字体大小
|
||||
$font-size-extra-small: 12px;
|
||||
$font-size-small: 14px;
|
||||
$font-size-base: 16px;
|
||||
$font-size-medium: 18px;
|
||||
$font-size-large: 20px;
|
||||
$font-size-extra-large: 24px;
|
||||
|
||||
// 间距
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 16px;
|
||||
$spacing-lg: 24px;
|
||||
$spacing-xl: 32px;
|
||||
|
||||
// 圆角
|
||||
$border-radius-small: 4px;
|
||||
$border-radius-base: 6px;
|
||||
$border-radius-large: 8px;
|
||||
$border-radius-round: 20px;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
$box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
|
||||
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 过渡动画
|
||||
$transition-base: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
$transition-fade: opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
$transition-border: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
// Z-index 层级
|
||||
$z-index-normal: 1;
|
||||
$z-index-top: 1000;
|
||||
$z-index-popper: 2000;
|
||||
75
admin-system/src/types/order.ts
Normal file
75
admin-system/src/types/order.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// 订单相关类型定义
|
||||
|
||||
export interface Order {
|
||||
id: number
|
||||
orderNo: string
|
||||
buyerId: number
|
||||
buyerName: string
|
||||
supplierId: number
|
||||
supplierName: string
|
||||
traderId?: number
|
||||
traderName?: string
|
||||
cattleBreed: string
|
||||
cattleCount: number
|
||||
expectedWeight: number
|
||||
actualWeight?: number
|
||||
unitPrice: number
|
||||
totalAmount: number
|
||||
paidAmount: number
|
||||
remainingAmount: number
|
||||
status: OrderStatus
|
||||
deliveryAddress: string
|
||||
expectedDeliveryDate: string
|
||||
actualDeliveryDate?: string
|
||||
notes?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type OrderStatus =
|
||||
| 'pending' // 待确认
|
||||
| 'confirmed' // 已确认
|
||||
| 'preparing' // 准备中
|
||||
| 'shipping' // 运输中
|
||||
| 'delivered' // 已送达
|
||||
| 'accepted' // 已验收
|
||||
| 'completed' // 已完成
|
||||
| 'cancelled' // 已取消
|
||||
| 'refunded' // 已退款
|
||||
|
||||
export interface OrderListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
orderNo?: string
|
||||
buyerId?: number
|
||||
supplierId?: number
|
||||
status?: OrderStatus
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface OrderCreateForm {
|
||||
buyerId: number
|
||||
supplierId: number
|
||||
traderId?: number
|
||||
cattleBreed: string
|
||||
cattleCount: number
|
||||
expectedWeight: number
|
||||
unitPrice: number
|
||||
deliveryAddress: string
|
||||
expectedDeliveryDate: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface OrderUpdateForm {
|
||||
cattleBreed?: string
|
||||
cattleCount?: number
|
||||
expectedWeight?: number
|
||||
actualWeight?: number
|
||||
unitPrice?: number
|
||||
deliveryAddress?: string
|
||||
expectedDeliveryDate?: string
|
||||
actualDeliveryDate?: string
|
||||
notes?: string
|
||||
status?: OrderStatus
|
||||
}
|
||||
52
admin-system/src/types/user.ts
Normal file
52
admin-system/src/types/user.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// 用户相关类型定义
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
avatar?: string
|
||||
role: string
|
||||
status: 'active' | 'inactive' | 'banned'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface LoginForm {
|
||||
username: string
|
||||
password: string
|
||||
captcha?: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
user: User
|
||||
}
|
||||
|
||||
export interface UserListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
role?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface UserCreateForm {
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
password: string
|
||||
role: string
|
||||
status: 'active' | 'inactive'
|
||||
}
|
||||
|
||||
export interface UserUpdateForm {
|
||||
username?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
role?: string
|
||||
status?: 'active' | 'inactive' | 'banned'
|
||||
avatar?: string
|
||||
}
|
||||
97
admin-system/src/utils/request.ts
Normal file
97
admin-system/src/utils/request.ts
Normal 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
|
||||
}
|
||||
416
admin-system/src/views/dashboard/index.vue
Normal file
416
admin-system/src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-cards">
|
||||
<el-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.key">
|
||||
<el-card class="stat-card" :body-style="{ padding: '20px' }">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" :style="{ backgroundColor: stat.color }">
|
||||
<el-icon :size="24"><component :is="stat.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
|
||||
<el-icon><component :is="stat.trend > 0 ? 'TrendCharts' : 'Bottom'" /></el-icon>
|
||||
{{ Math.abs(stat.trend) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<el-col :lg="16">
|
||||
<el-card title="订单趋势" class="chart-card">
|
||||
<div ref="orderTrendChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="8">
|
||||
<el-card title="订单状态分布" class="chart-card">
|
||||
<div ref="orderStatusChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格区域 -->
|
||||
<el-row :gutter="20" class="tables-section">
|
||||
<el-col :lg="12">
|
||||
<el-card title="最近订单" class="table-card">
|
||||
<el-table :data="recentOrders" size="small">
|
||||
<el-table-column prop="orderNo" label="订单号" width="120" />
|
||||
<el-table-column prop="supplierName" label="供应商" />
|
||||
<el-table-column prop="cattleCount" label="数量" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="12">
|
||||
<el-card title="供应商排行" class="table-card">
|
||||
<el-table :data="topSuppliers" size="small">
|
||||
<el-table-column type="index" label="排名" width="60" />
|
||||
<el-table-column prop="name" label="供应商名称" />
|
||||
<el-table-column prop="orderCount" label="订单数" width="80" />
|
||||
<el-table-column prop="totalAmount" label="总金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.totalAmount.toLocaleString() }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref([
|
||||
{
|
||||
key: 'totalOrders',
|
||||
label: '总订单数',
|
||||
value: '1,234',
|
||||
icon: 'ShoppingCart',
|
||||
color: '#409EFF',
|
||||
trend: 12.5
|
||||
},
|
||||
{
|
||||
key: 'completedOrders',
|
||||
label: '已完成订单',
|
||||
value: '856',
|
||||
icon: 'CircleCheck',
|
||||
color: '#67C23A',
|
||||
trend: 8.3
|
||||
},
|
||||
{
|
||||
key: 'totalAmount',
|
||||
label: '总交易额',
|
||||
value: '¥2.34M',
|
||||
icon: 'Money',
|
||||
color: '#E6A23C',
|
||||
trend: 15.7
|
||||
},
|
||||
{
|
||||
key: 'activeSuppliers',
|
||||
label: '活跃供应商',
|
||||
value: '168',
|
||||
icon: 'OfficeBuilding',
|
||||
color: '#F56C6C',
|
||||
trend: -2.1
|
||||
}
|
||||
])
|
||||
|
||||
// 最近订单
|
||||
const recentOrders = ref([
|
||||
{
|
||||
orderNo: 'ORD20240101001',
|
||||
supplierName: '山东畜牧合作社',
|
||||
cattleCount: 50,
|
||||
status: 'shipping'
|
||||
},
|
||||
{
|
||||
orderNo: 'ORD20240101002',
|
||||
supplierName: '河北养殖基地',
|
||||
cattleCount: 30,
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
orderNo: 'ORD20240101003',
|
||||
supplierName: '内蒙古牧场',
|
||||
cattleCount: 80,
|
||||
status: 'pending'
|
||||
}
|
||||
])
|
||||
|
||||
// 供应商排行
|
||||
const topSuppliers = ref([
|
||||
{
|
||||
name: '山东畜牧合作社',
|
||||
orderCount: 45,
|
||||
totalAmount: 1250000
|
||||
},
|
||||
{
|
||||
name: '河北养殖基地',
|
||||
orderCount: 38,
|
||||
totalAmount: 980000
|
||||
},
|
||||
{
|
||||
name: '内蒙古牧场',
|
||||
orderCount: 32,
|
||||
totalAmount: 850000
|
||||
}
|
||||
])
|
||||
|
||||
// 图表元素引用
|
||||
const orderTrendChart = ref<HTMLElement>()
|
||||
const orderStatusChart = ref<HTMLElement>()
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
confirmed: 'info',
|
||||
shipping: 'primary',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待确认',
|
||||
confirmed: '已确认',
|
||||
shipping: '运输中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 初始化订单趋势图表
|
||||
const initOrderTrendChart = () => {
|
||||
if (!orderTrendChart.value) return
|
||||
|
||||
const chart = echarts.init(orderTrendChart.value)
|
||||
const option = {
|
||||
title: {
|
||||
text: '近30天订单趋势',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['订单数量', '完成订单']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'line',
|
||||
data: [120, 132, 101, 134, 90, 230, 210, 180, 160, 190, 200, 220],
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#409EFF'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '完成订单',
|
||||
type: 'line',
|
||||
data: [100, 120, 90, 120, 80, 200, 190, 160, 140, 170, 180, 200],
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: '#67C23A'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化订单状态图表
|
||||
const initOrderStatusChart = () => {
|
||||
if (!orderStatusChart.value) return
|
||||
|
||||
const chart = echarts.init(orderStatusChart.value)
|
||||
const option = {
|
||||
title: {
|
||||
text: '订单状态分布',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单状态',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
data: [
|
||||
{ value: 335, name: '已完成', itemStyle: { color: '#67C23A' } },
|
||||
{ value: 210, name: '运输中', itemStyle: { color: '#409EFF' } },
|
||||
{ value: 180, name: '已确认', itemStyle: { color: '#E6A23C' } },
|
||||
{ value: 130, name: '待确认', itemStyle: { color: '#F56C6C' } },
|
||||
{ value: 45, name: '已取消', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initOrderTrendChart()
|
||||
initOrderStatusChart()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
.stats-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
&.up {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #F56C6C;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.tables-section {
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.el-card__header) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 15px 20px;
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
.stats-cards {
|
||||
.stat-card {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.stat-content {
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/finance/index.vue
Normal file
34
admin-system/src/views/finance/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="finance-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>财务管理</h2>
|
||||
<p>订单结算、支付管理和财务报表</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="财务管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 财务管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.finance-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
237
admin-system/src/views/login/index.vue
Normal file
237
admin-system/src/views/login/index.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<img src="/logo.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>
|
||||
34
admin-system/src/views/order/index.vue
Normal file
34
admin-system/src/views/order/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="order-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>订单管理</h2>
|
||||
<p>管理活牛采购订单的全生命周期流程</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="订单管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 订单管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/quality/index.vue
Normal file
34
admin-system/src/views/quality/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="quality-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>质量管理</h2>
|
||||
<p>牛只质量检验、检疫证明管理和质量追溯</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="质量管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 质量管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quality-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/settings/index.vue
Normal file
34
admin-system/src/views/settings/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>系统设置</h2>
|
||||
<p>系统配置、权限管理和参数设置</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="系统设置功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 系统设置页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/supplier/index.vue
Normal file
34
admin-system/src/views/supplier/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="supplier-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>供应商管理</h2>
|
||||
<p>管理供应商信息、资质认证和绩效评估</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="供应商管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 供应商管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.supplier-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-system/src/views/transport/index.vue
Normal file
34
admin-system/src/views/transport/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="transport-management">
|
||||
<el-card>
|
||||
<div class="page-header">
|
||||
<h2>运输管理</h2>
|
||||
<p>实时跟踪运输过程,监控车辆位置和牛只状态</p>
|
||||
</div>
|
||||
|
||||
<el-empty description="运输管理功能开发中..." />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 运输管理页面
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.transport-management {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
517
admin-system/src/views/user/index.vue
Normal file
517
admin-system/src/views/user/index.vue
Normal file
@@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<el-card class="search-card">
|
||||
<div class="search-form">
|
||||
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.keyword" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="采购人" value="buyer" />
|
||||
<el-option label="贸易商" value="trader" />
|
||||
<el-option label="供应商" value="supplier" />
|
||||
<el-option label="司机" value="driver" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="正常" value="active" />
|
||||
<el-option label="禁用" value="inactive" />
|
||||
<el-option label="封禁" value="banned" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户列表</span>
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column prop="role" label="角色">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRoleType(row.role)">
|
||||
{{ getRoleText(row.role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户表单弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="采购人" value="buyer" />
|
||||
<el-option label="贸易商" value="trader" />
|
||||
<el-option label="供应商" value="supplier" />
|
||||
<el-option label="司机" value="driver" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="正常" value="active" />
|
||||
<el-option label="禁用" value="inactive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.id" label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import type { User, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive<UserListParams>({
|
||||
keyword: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<User[]>([])
|
||||
const selectedUsers = ref<User[]>([])
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<UserCreateForm & { id?: number }>({
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
role: '',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = ref('新增用户')
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const mockUsers: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
phone: '13800138000',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'buyer01',
|
||||
email: 'buyer01@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'buyer',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'supplier01',
|
||||
email: 'supplier01@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'supplier',
|
||||
status: 'inactive',
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
updatedAt: '2024-01-03T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取用户列表
|
||||
const getUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 简单的过滤逻辑
|
||||
let filteredUsers = [...mockUsers]
|
||||
|
||||
if (searchForm.keyword) {
|
||||
filteredUsers = filteredUsers.filter(user =>
|
||||
user.username.includes(searchForm.keyword!) ||
|
||||
user.email.includes(searchForm.keyword!)
|
||||
)
|
||||
}
|
||||
|
||||
if (searchForm.role) {
|
||||
filteredUsers = filteredUsers.filter(user => user.role === searchForm.role)
|
||||
}
|
||||
|
||||
if (searchForm.status) {
|
||||
filteredUsers = filteredUsers.filter(user => user.status === searchForm.status)
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const start = (pagination.page - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
|
||||
tableData.value = filteredUsers.slice(start, end)
|
||||
pagination.total = filteredUsers.length
|
||||
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
role: '',
|
||||
status: ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增用户'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (row: User) => {
|
||||
dialogTitle.value = '编辑用户'
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
email: row.email,
|
||||
phone: row.phone,
|
||||
role: row.role,
|
||||
status: row.status
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (row: User) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户 "${row.username}" 吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 模拟删除
|
||||
ElMessage.success('删除成功')
|
||||
getUserList()
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = async (row: User) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要重置用户 "${row.username}" 的密码吗?`,
|
||||
'重置密码确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 模拟重置密码
|
||||
ElMessage.success('密码重置成功,新密码为:123456')
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 表格选择变化
|
||||
const handleSelectionChange = (selection: User[]) => {
|
||||
selectedUsers.value = selection
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
pagination.page = 1
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
ElMessage.success(form.id ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
getUserList()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleDialogClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
role: '',
|
||||
status: 'active'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取角色类型
|
||||
const getRoleType = (role: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
admin: 'danger',
|
||||
buyer: 'primary',
|
||||
trader: 'success',
|
||||
supplier: 'warning',
|
||||
driver: 'info'
|
||||
}
|
||||
return roleMap[role] || 'info'
|
||||
}
|
||||
|
||||
// 获取角色文本
|
||||
const getRoleText = (role: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
admin: '管理员',
|
||||
buyer: '采购人',
|
||||
trader: '贸易商',
|
||||
supplier: '供应商',
|
||||
driver: '司机'
|
||||
}
|
||||
return roleMap[role] || role
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: 'success',
|
||||
inactive: 'warning',
|
||||
banned: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
active: '正常',
|
||||
inactive: '禁用',
|
||||
banned: '封禁'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-management {
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-form {
|
||||
.demo-form-inline {
|
||||
.el-form-item {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.user-management {
|
||||
.search-form {
|
||||
.demo-form-inline {
|
||||
.el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
.el-table-column {
|
||||
&:nth-child(n+4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
admin-system/tsconfig.json
Normal file
53
admin-system/tsconfig.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"auto-imports.d.ts",
|
||||
"components.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/*",
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/views/*": ["./src/views/*"],
|
||||
"@/utils/*": ["./src/utils/*"],
|
||||
"@/api/*": ["./src/api/*"],
|
||||
"@/stores/*": ["./src/stores/*"],
|
||||
"@/router/*": ["./src/router/*"],
|
||||
"@/types/*": ["./src/types/*"],
|
||||
"@/assets/*": ["./src/assets/*"]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"useDefineForClassFields": true,
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"types": ["vite/client", "element-plus/global", "node"]
|
||||
}
|
||||
}
|
||||
103
admin-system/vite.config.ts
Normal file
103
admin-system/vite.config.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: true,
|
||||
eslintrc: {
|
||||
enabled: true
|
||||
}
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: true
|
||||
})
|
||||
],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@/components': resolve(__dirname, 'src/components'),
|
||||
'@/views': resolve(__dirname, 'src/views'),
|
||||
'@/utils': resolve(__dirname, 'src/utils'),
|
||||
'@/api': resolve(__dirname, 'src/api'),
|
||||
'@/stores': resolve(__dirname, 'src/stores'),
|
||||
'@/router': resolve(__dirname, 'src/router'),
|
||||
'@/types': resolve(__dirname, 'src/types'),
|
||||
'@/assets': resolve(__dirname, 'src/assets')
|
||||
}
|
||||
},
|
||||
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_BASE_URL || 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
build: {
|
||||
target: 'es2015',
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
sourcemap: mode === 'development',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
chunkFileNames: 'js/[name]-[hash].js',
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name?.endsWith('.css')) {
|
||||
return 'css/[name]-[hash].css'
|
||||
}
|
||||
return 'assets/[name]-[hash].[ext]'
|
||||
},
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'element-plus': ['element-plus', '@element-plus/icons-vue'],
|
||||
'utils': ['axios', 'nprogress']
|
||||
}
|
||||
}
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: mode === 'production',
|
||||
drop_debugger: mode === 'production'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'vue-router', 'pinia', 'element-plus', 'axios']
|
||||
},
|
||||
|
||||
define: {
|
||||
__VUE_OPTIONS_API__: false,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@import "@/assets/styles/variables.scss"; @import "@/assets/styles/mixins.scss";`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user