1.0
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# 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?
|
||||
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,29 @@
|
||||
# untitled
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,206 @@
|
||||
# 管理员端前端项目
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于Vue 3 + Element Plus的管理员端前端项目,用于管理虚拟商品交易平台。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: Vue 3 (Composition API)
|
||||
- **UI库**: Element Plus
|
||||
- **状态管理**: Pinia
|
||||
- **路由**: Vue Router 4
|
||||
- **HTTP客户端**: Axios
|
||||
- **图表**: ECharts
|
||||
- **构建工具**: Vite
|
||||
- **样式**: CSS3 + Tailwind CSS
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── layouts/ # 布局组件
|
||||
│ └── AdminLayout.vue # 管理端主布局
|
||||
├── views/ # 页面组件
|
||||
│ ├── Login.vue # 登录页面
|
||||
│ └── admin/ # 管理端页面
|
||||
│ ├── Dashboard.vue # 仪表盘
|
||||
│ ├── Users.vue # 用户管理
|
||||
│ ├── Scores.vue # 积分管理
|
||||
│ └── orders/ # 订单管理
|
||||
│ ├── PurchaseOrders.vue # 买单管理
|
||||
│ └── SalesOrders.vue # 卖单管理
|
||||
├── stores/ # Pinia状态管理
|
||||
│ └── auth.js # 认证状态
|
||||
├── router/ # 路由配置
|
||||
│ └── index.js # 主路由文件
|
||||
└── main.js # 应用入口
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🔐 认证系统
|
||||
- 支持管理员和代理商登录
|
||||
- JWT Token认证
|
||||
- 自动路由守卫
|
||||
- 权限控制
|
||||
|
||||
### 👥 用户管理
|
||||
- 用户列表查看(支持搜索、筛选、分页)
|
||||
- 用户详情查看
|
||||
- 用户创建、编辑、删除(仅管理员)
|
||||
- 用户状态管理
|
||||
- 权限级别显示
|
||||
|
||||
### 📋 订单管理
|
||||
- 买单管理(查看、处理订单状态)
|
||||
- 卖单管理(查看、处理订单状态)
|
||||
- 订单详情查看
|
||||
- 支持多条件筛选
|
||||
- 订单状态流转
|
||||
|
||||
### ⭐ 积分管理
|
||||
- 积分记录查看(支持搜索、筛选)
|
||||
- 积分记录创建、编辑、删除(仅管理员)
|
||||
- 积分变化统计
|
||||
- 用户积分查询
|
||||
|
||||
### 📊 数据统计
|
||||
- 仪表盘数据概览
|
||||
- 用户角色分布图表
|
||||
- 订单趋势图表
|
||||
- 实时统计数据
|
||||
|
||||
### 🎨 界面特性
|
||||
- 响应式设计,支持移动端
|
||||
- 现代化UI设计
|
||||
- 暗色侧边栏
|
||||
- 面包屑导航
|
||||
- 页面切换动画
|
||||
- 数据加载状态
|
||||
|
||||
## 权限说明
|
||||
|
||||
### 管理员权限(system_role=0)
|
||||
- ✅ 访问所有功能模块
|
||||
- ✅ 用户管理(查看、创建、编辑、删除)
|
||||
- ✅ 订单管理(查看、处理)
|
||||
- ✅ 积分管理(查看、创建、编辑、删除)
|
||||
- ✅ 商品管理(计划中)
|
||||
- ✅ 系统配置(计划中)
|
||||
|
||||
### 代理商权限(system_role=1)
|
||||
- ✅ 仪表盘查看(仅自己邀请的用户数据)
|
||||
- ✅ 用户管理(仅查看自己邀请的用户)
|
||||
- ✅ 订单管理(仅查看相关用户的订单)
|
||||
- ✅ 积分管理(仅查看相关用户的积分记录)
|
||||
- ❌ 无创建、编辑、删除权限
|
||||
- ❌ 无商品管理权限
|
||||
- ❌ 无系统配置权限
|
||||
|
||||
## 安装和运行
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
cd front
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 启动开发服务器
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 构建生产版本
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 默认测试账户
|
||||
|
||||
### 管理员账户
|
||||
- 手机号:`13800138000`
|
||||
- 密码:`admin123`
|
||||
- 权限:全部功能
|
||||
|
||||
### 代理商账户
|
||||
- 手机号:`13800138001`
|
||||
- 密码:`agent123`
|
||||
- 权限:受限查看
|
||||
|
||||
## API配置
|
||||
|
||||
项目已配置axios默认baseURL为`/back`,确保后端API正确运行在对应端口。
|
||||
|
||||
请求拦截器会自动添加token到请求头:
|
||||
```javascript
|
||||
headers: {
|
||||
token: localStorage.getItem('token')
|
||||
}
|
||||
```
|
||||
|
||||
响应拦截器会自动处理401未授权错误,跳转到登录页面。
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 组件命名
|
||||
- 页面组件:PascalCase(如:`Users.vue`)
|
||||
- 布局组件:PascalCase + Layout后缀(如:`AdminLayout.vue`)
|
||||
- 通用组件:PascalCase(如:`DataTable.vue`)
|
||||
|
||||
### 状态管理
|
||||
- 使用Pinia进行状态管理
|
||||
- 按功能模块划分store
|
||||
- 使用Composition API风格
|
||||
|
||||
### 样式规范
|
||||
- 优先使用Element Plus组件样式
|
||||
- 使用scoped CSS避免样式污染
|
||||
- 响应式设计,移动端适配
|
||||
- 使用CSS变量统一主题色彩
|
||||
|
||||
### 代码规范
|
||||
- 使用Composition API
|
||||
- 使用TypeScript类型注解(计划中)
|
||||
- 函数和变量使用camelCase命名
|
||||
- 常量使用UPPER_SNAKE_CASE命名
|
||||
|
||||
## 待开发功能
|
||||
|
||||
- [ ] 商品管理页面
|
||||
- [ ] 系统配置管理
|
||||
- [ ] 数据导出功能
|
||||
- [ ] 批量操作功能
|
||||
- [ ] 个人信息管理
|
||||
- [ ] 操作日志记录
|
||||
- [ ] 系统通知功能
|
||||
- [ ] 文件上传组件
|
||||
- [ ] 富文本编辑器
|
||||
- [ ] 国际化支持
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**:代理商只能看到自己邀请的用户相关数据
|
||||
2. **数据过滤**:前端路由守卫 + 后端API权限双重保护
|
||||
3. **错误处理**:统一的错误提示和处理机制
|
||||
4. **性能优化**:按需加载、虚拟滚动等优化措施
|
||||
5. **安全性**:Token自动过期处理、XSS防护
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
- Chrome >= 87
|
||||
- Firefox >= 78
|
||||
- Safari >= 14
|
||||
- Edge >= 88
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork项目
|
||||
2. 创建特性分支
|
||||
3. 提交更改
|
||||
4. 推送到分支
|
||||
5. 创建Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/icon.jpg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+9670
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "untitled",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@antv/g6": "^5.0.47",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@kangc/v-md-editor": "^2.3.18",
|
||||
"axios": "^1.6.7",
|
||||
"chart.js": "^4.4.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.5.6",
|
||||
"gojs": "^3.0.22",
|
||||
"markdown-it": "^14.0.0",
|
||||
"neo4j-driver": "^5.28.1",
|
||||
"pinia": "^2.1.7",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-awesome-swiper": "^5.0.1",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"less": "^4.2.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-proxy": "^0.5.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#409eff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#67c23a;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景圆形 -->
|
||||
<circle cx="16" cy="16" r="15" fill="url(#logoGradient)" stroke="#fff" stroke-width="1"/>
|
||||
|
||||
<!-- 商品图标 -->
|
||||
<g fill="#fff">
|
||||
<!-- 购物袋主体 -->
|
||||
<path d="M9 12h14v12a2 2 0 0 1-2 2H11a2 2 0 0 1-2-2V12z"/>
|
||||
<!-- 购物袋手柄 -->
|
||||
<path d="M12 8a4 4 0 0 1 8 0v4h-2V8a2 2 0 0 0-4 0v4h-2V8z"/>
|
||||
<!-- 装饰点 -->
|
||||
<circle cx="13" cy="18" r="1"/>
|
||||
<circle cx="19" cy="18" r="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 798 B |
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,3 @@
|
||||
/*@tailwind base;*/
|
||||
/*@tailwind components;*/
|
||||
/*@tailwind utilities;*/
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="fileList"
|
||||
:action="uploadUrl"
|
||||
:headers="headers"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:on-remove="onRemove"
|
||||
:limit="limit"
|
||||
:multiple="multiple"
|
||||
:show-file-list="false"
|
||||
:auto-upload="autoUpload"
|
||||
accept="image/*"
|
||||
class="upload-demo"
|
||||
>
|
||||
<div v-if="imageUrls.length < limit && !(props.previewSrcList && props.previewSrcList.length > 0 && !props.multiple)" class="upload-item">
|
||||
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||
<div class="upload-text">上传图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<div v-if="imageUrls.length > 0 || (props.previewSrcList && props.previewSrcList.length > 0)" class="image-list">
|
||||
<!-- 显示已有图片 -->
|
||||
<div
|
||||
v-for="(url, index) in imageUrls"
|
||||
:key="`uploaded-${index}`"
|
||||
class="image-item"
|
||||
>
|
||||
<el-image
|
||||
:src="getImageUrl(url)"
|
||||
:preview-src-list="props.previewSrcList.length > 0 ? props.previewSrcList : imageUrls.map(u => getImageUrl(u))"
|
||||
:initial-index="index"
|
||||
fit="cover"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div class="image-actions">
|
||||
<el-icon class="action-icon" @click="previewImage(index)">
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
<el-icon class="action-icon" @click="removeImage(index)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示外部传入的预览图片(当没有上传图片时) -->
|
||||
<div
|
||||
v-if="imageUrls.length === 0 && props.previewSrcList && props.previewSrcList.length > 0"
|
||||
v-for="(url, index) in props.previewSrcList"
|
||||
:key="`preview-${index}`"
|
||||
class="image-item"
|
||||
>
|
||||
<el-image
|
||||
:src="url"
|
||||
:preview-src-list="props.previewSrcList"
|
||||
:initial-index="index"
|
||||
fit="cover"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div class="image-actions">
|
||||
<el-icon class="action-icon" @click="previewImage(index)">
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
<el-icon class="action-icon" @click="removeExternalImage(index)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<el-progress
|
||||
v-if="uploading"
|
||||
:percentage="uploadProgress"
|
||||
:show-text="false"
|
||||
class="upload-progress"
|
||||
/>
|
||||
|
||||
<!-- 图片预览器 -->
|
||||
<el-image-viewer
|
||||
v-if="showViewer"
|
||||
:url-list="viewerUrls"
|
||||
:initial-index="viewerIndex"
|
||||
@close="closeViewer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: ''
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 9
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10 // MB
|
||||
},
|
||||
previewSrcList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const uploadRef = ref()
|
||||
const fileList = ref([])
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const showViewer = ref(false)
|
||||
const viewerIndex = ref(0)
|
||||
const viewerUrls = ref([])
|
||||
|
||||
const uploadUrl = '/back/upload'
|
||||
const headers = computed(() => ({
|
||||
token: localStorage.getItem('token') || ''
|
||||
}))
|
||||
|
||||
// 图片URL列表
|
||||
const imageUrls = computed({
|
||||
get() {
|
||||
if (!props.modelValue) return []
|
||||
if (typeof props.modelValue === 'string') {
|
||||
return props.modelValue ? props.modelValue.split(',').filter(url => url.trim()) : []
|
||||
}
|
||||
return props.modelValue || []
|
||||
},
|
||||
set(value) {
|
||||
const newValue = props.multiple ? value.join(',') : (value[0] || '')
|
||||
emit('update:modelValue', newValue)
|
||||
emit('change', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取完整图片URL
|
||||
const getImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${window.location.origin}${url}`
|
||||
}
|
||||
|
||||
// 上传前检查
|
||||
const beforeUpload = (file) => {
|
||||
// 检查文件类型
|
||||
const isImage = file.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
const isLtMaxSize = file.size / 1024 / 1024 < props.maxSize
|
||||
if (!isLtMaxSize) {
|
||||
ElMessage.error(`图片大小不能超过 ${props.maxSize}MB!`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (imageUrls.value.length >= props.limit) {
|
||||
ElMessage.error(`最多只能上传 ${props.limit} 张图片!`)
|
||||
return false
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
return true
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
const onSuccess = (response, file) => {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 100
|
||||
|
||||
if (response.success) {
|
||||
const newUrls = [...imageUrls.value, response.data.url]
|
||||
imageUrls.value = newUrls
|
||||
ElMessage.success('图片上传成功')
|
||||
|
||||
// 清空上传组件的文件列表,避免下次上传时出现问题
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const onError = (error, file) => {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error('图片上传失败,请重试')
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const onRemove = (file) => {
|
||||
// 这里可以实现从服务器删除文件的逻辑
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
const removeImage = (index) => {
|
||||
const newUrls = [...imageUrls.value]
|
||||
newUrls.splice(index, 1)
|
||||
imageUrls.value = newUrls
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (index) => {
|
||||
// 设置预览图片列表和初始索引
|
||||
if (props.previewSrcList && props.previewSrcList.length > 0) {
|
||||
viewerUrls.value = props.previewSrcList
|
||||
} else {
|
||||
viewerUrls.value = imageUrls.value.map(url => getImageUrl(url))
|
||||
}
|
||||
viewerIndex.value = index
|
||||
showViewer.value = true
|
||||
}
|
||||
|
||||
// 关闭图片预览
|
||||
const closeViewer = () => {
|
||||
showViewer.value = false
|
||||
}
|
||||
|
||||
// 删除外部传入的图片
|
||||
const removeExternalImage = (index) => {
|
||||
// 通知父组件删除外部图片
|
||||
emit('update:modelValue', '')
|
||||
emit('change', '')
|
||||
}
|
||||
|
||||
// 手动上传
|
||||
const submit = () => {
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.submit()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空文件
|
||||
const clearFiles = () => {
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
imageUrls.value = []
|
||||
}
|
||||
|
||||
// 监听外部传入的值变化,同步重置内部状态
|
||||
watch(() => props.modelValue, (newValue, oldValue) => {
|
||||
// 当外部值从有值变为空值时,清空内部状态
|
||||
if ((oldValue && !newValue) || (oldValue && newValue === '')) {
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
fileList.value = []
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
}, { immediate: false })
|
||||
|
||||
// 暴露方法供父组件调用
|
||||
defineExpose({
|
||||
submit,
|
||||
clearFiles
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.upload-item:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #8c939d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.image-item:hover .image-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.action-icon:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<el-container>
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="collapsed ? '64px' : '250px'" class="sidebar">
|
||||
<div class="logo">
|
||||
<img v-if="!collapsed" src="/logo.svg" alt="Logo" class="logo-img">
|
||||
<span v-if="!collapsed" class="logo-text">管理后台</span>
|
||||
<el-icon v-else class="logo-icon"><Menu /></el-icon>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
:collapse="collapsed"
|
||||
:unique-opened="true"
|
||||
router
|
||||
class="sidebar-menu"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409eff"
|
||||
>
|
||||
<!-- 仪表盘 -->
|
||||
<el-menu-item index="/admin/dashboard">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 用户管理 -->
|
||||
<el-sub-menu index="users">
|
||||
<template #title>
|
||||
<el-icon><User /></el-icon>
|
||||
<span>用户管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/admin/users">用户列表</el-menu-item>
|
||||
<el-menu-item v-if="authStore.isAdmin" index="/admin/warehouse/users">库存管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 订单管理 -->
|
||||
<el-sub-menu index="orders" v-if="authStore.isAdmin">
|
||||
<template #title>
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>订单管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/admin/orders/purchase">买单管理</el-menu-item>
|
||||
<el-menu-item index="/admin/orders/sales">卖单管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 商品管理 (仅管理员) -->
|
||||
<el-sub-menu v-if="authStore.isAdmin" index="products">
|
||||
<template #title>
|
||||
<el-icon><Box /></el-icon>
|
||||
<span>商品管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/admin/products/categories">商品分类</el-menu-item>
|
||||
<el-menu-item index="/admin/products/primary">一级商品</el-menu-item>
|
||||
<el-menu-item index="/admin/products/secondary">二级商品</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 积分管理 -->
|
||||
<el-sub-menu index="scores">
|
||||
<template #title>
|
||||
<el-icon><Star /></el-icon>
|
||||
<span>积分管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/admin/scores">积分记录</el-menu-item>
|
||||
<!-- <el-menu-item v-if="authStore.isAdmin" index="/admin/scores/create">添加积分</el-menu-item>-->
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 系统配置 (仅管理员) -->
|
||||
<el-menu-item v-if="authStore.isAdmin" index="/admin/configs">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统配置</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 轮播图管理 (仅管理员) -->
|
||||
<el-menu-item v-if="authStore.isAdmin" index="/admin/banners">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>轮播图管理</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 个人信息 -->
|
||||
<el-menu-item index="/admin/profile">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>个人信息</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container>
|
||||
<!-- 顶部导航 -->
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
type="text"
|
||||
class="collapse-btn"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<el-icon><Expand v-if="collapsed" /><Fold v-else /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item
|
||||
v-for="item in breadcrumbs"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- 用户信息 -->
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="user-info">
|
||||
<el-avatar :size="32" :src="authStore.user?.avatar">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</el-avatar>
|
||||
<span class="username">{{ authStore.user?.customer_name || authStore.user?.real_name }}</span>
|
||||
<span class="user-role">{{ authStore.userRole }}</span>
|
||||
<el-icon class="arrow-down"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">个人信息</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<el-main class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const collapsed = ref(false)
|
||||
|
||||
// 切换侧边栏
|
||||
const toggleSidebar = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumbs = computed(() => {
|
||||
const matched = route.matched.filter(item => item.meta && item.meta.title)
|
||||
const first = matched[0]
|
||||
|
||||
if (!first || first.name !== 'Admin') {
|
||||
matched.unshift({ path: '/admin/dashboard', meta: { title: '首页' } })
|
||||
}
|
||||
|
||||
return matched.map(item => ({
|
||||
name: item.meta.title,
|
||||
path: item.path
|
||||
}))
|
||||
})
|
||||
|
||||
// 处理用户下拉菜单命令
|
||||
const handleCommand = async (command) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
router.push('/admin/profile')
|
||||
break
|
||||
case 'logout':
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
authStore.logout()
|
||||
ElMessage.success('退出登录成功')
|
||||
router.push('/login')
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化,在小屏幕下自动收起侧边栏
|
||||
watch(route, () => {
|
||||
if (window.innerWidth < 992) {
|
||||
collapsed.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #304156;
|
||||
transition: width 0.28s;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #2b3a4b;
|
||||
border-bottom: 1px solid #1d2b3b;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-menu:not(.el-menu--collapse) {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
margin-right: 20px;
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-left: 8px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
background-color: #f0f9ff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b3d8ff;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 页面切换动画 */
|
||||
.fade-transform-enter-active,
|
||||
.fade-transform-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 992px) {
|
||||
.header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from "./router/index.js";
|
||||
import ElementPlus, {ElMessage} from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import axios from 'axios'
|
||||
import VueMarkdownEditor from '@kangc/v-md-editor';
|
||||
import '@kangc/v-md-editor/lib/style/base-editor.css';
|
||||
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
|
||||
import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
|
||||
import Prism from 'prismjs';
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import "./assets/main.css"
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
VueMarkdownEditor.use(vuepressTheme, {
|
||||
Prism,
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
app.use(VueMarkdownEditor);
|
||||
axios.defaults.baseURL = '/back';
|
||||
axios.loadData = async function (url) {
|
||||
const resp = await axios.get(url);
|
||||
return resp.data;
|
||||
};
|
||||
axios.interceptors.request.use(function (config) {
|
||||
|
||||
if(localStorage.getItem("token"))
|
||||
config.headers.token = localStorage.getItem("token");
|
||||
return config;
|
||||
}, function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
// 响应拦截器
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
|
||||
if(response.data.code==401){
|
||||
ElMessage.error( "登陆异常")
|
||||
router.push("/login")
|
||||
response.status.code=401
|
||||
return response;
|
||||
}
|
||||
|
||||
if( response.data.success== false){
|
||||
ElMessage.error( response.data.message)
|
||||
}
|
||||
// 对响应数据做点什么
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
app.config.globalProperties.$http = axios;
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(ElementPlus);
|
||||
app.mount("#app");
|
||||
@@ -0,0 +1,158 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
// 导入页面组件
|
||||
import Login from "@/views/Login.vue";
|
||||
import AdminLayout from "@/layouts/AdminLayout.vue";
|
||||
import Dashboard from "@/views/admin/Dashboard.vue";
|
||||
import Users from "@/views/admin/Users.vue";
|
||||
import PurchaseOrders from "@/views/admin/orders/PurchaseOrders.vue";
|
||||
import SalesOrders from "@/views/admin/orders/SalesOrders.vue";
|
||||
import Scores from "@/views/admin/Scores.vue";
|
||||
import Categories from "@/views/admin/products/Categories.vue";
|
||||
import PrimaryProducts from "@/views/admin/products/PrimaryProducts.vue";
|
||||
import SecondaryProducts from "@/views/admin/products/SecondaryProducts.vue";
|
||||
import SystemConfigs from "@/views/admin/SystemConfigs.vue";
|
||||
import Banners from "@/views/admin/Banners.vue";
|
||||
import Profile from "@/views/Profile.vue";
|
||||
import UserWarehouses from "@/views/admin/UserWarehouses.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/admin/dashboard",
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
meta: { title: "登录" },
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "Admin",
|
||||
component: AdminLayout,
|
||||
redirect: "/admin/dashboard",
|
||||
meta: {
|
||||
title: "管理后台",
|
||||
requiresAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "dashboard",
|
||||
name: "Dashboard",
|
||||
component: Dashboard,
|
||||
meta: { title: "仪表盘", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
name: "Users",
|
||||
component: Users,
|
||||
meta: { title: "用户管理", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "warehouse/users",
|
||||
name: "UserWarehouses",
|
||||
component: UserWarehouses,
|
||||
meta: { title: "用户库存管理", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "orders/purchase",
|
||||
name: "PurchaseOrders",
|
||||
component: PurchaseOrders,
|
||||
meta: { title: "买单管理", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "orders/sales",
|
||||
name: "SalesOrders",
|
||||
component: SalesOrders,
|
||||
meta: { title: "卖单管理", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "scores",
|
||||
name: "Scores",
|
||||
component: Scores,
|
||||
meta: { title: "积分管理", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "products/categories",
|
||||
name: "ProductCategories",
|
||||
component: Categories,
|
||||
meta: { title: "商品分类", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "products/primary",
|
||||
name: "PrimaryProducts",
|
||||
component: PrimaryProducts,
|
||||
meta: { title: "一级商品", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "products/secondary",
|
||||
name: "SecondaryProducts",
|
||||
component: SecondaryProducts,
|
||||
meta: { title: "二级商品", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "configs",
|
||||
name: "SystemConfigs",
|
||||
component: SystemConfigs,
|
||||
meta: { title: "系统配置", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "banners",
|
||||
name: "Banners",
|
||||
component: Banners,
|
||||
meta: { title: "轮播图管理", requiresAuth: true, adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
name: "Profile",
|
||||
component: Profile,
|
||||
meta: { title: "个人信息", requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||
next("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已登录且访问登录页,重定向到管理后台
|
||||
if (to.path === "/login" && authStore.isLoggedIn) {
|
||||
next("/admin/dashboard");
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
if (to.meta.requiresAuth && authStore.isLoggedIn) {
|
||||
// 检查是否为管理员或代理商
|
||||
if (authStore.user?.system_role > 1) {
|
||||
// 普通用户无权访问管理后台
|
||||
next("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否为管理员专用页面
|
||||
if (to.meta.adminOnly && !authStore.isAdmin) {
|
||||
// 非管理员无权访问管理员专用页面
|
||||
next("/admin/dashboard");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
const isAdmin = computed(() => user.value?.system_role === 0)
|
||||
const isAgent = computed(() => user.value?.system_role === 1)
|
||||
const userRole = computed(() => {
|
||||
if (!user.value) return ''
|
||||
switch (user.value.system_role) {
|
||||
case 0: return '管理员'
|
||||
case 1: return '代理商'
|
||||
case 2: return '普通用户'
|
||||
default: return '未知'
|
||||
}
|
||||
})
|
||||
|
||||
// 登录
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
const response = await axios.post('/auth/login', credentials)
|
||||
|
||||
if (response.data.success) {
|
||||
const { token: newToken, user: userData } = response.data.data
|
||||
|
||||
token.value = newToken
|
||||
user.value = userData
|
||||
|
||||
localStorage.setItem('token', newToken)
|
||||
localStorage.setItem('user', JSON.stringify(userData))
|
||||
|
||||
return userData
|
||||
} else {
|
||||
throw new Error(response.data.message || '登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
token.value = ''
|
||||
user.value = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
const response = await axios.get('/auth/current')
|
||||
|
||||
if (response.data.success) {
|
||||
user.value = response.data.data
|
||||
localStorage.setItem('user', JSON.stringify(response.data.data))
|
||||
return response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
logout() // 如果获取用户信息失败,清除登录状态
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
const hasPermission = (requiredRole = null) => {
|
||||
if (!user.value) return false
|
||||
|
||||
// 管理员拥有所有权限
|
||||
if (user.value.system_role === 0) return true
|
||||
|
||||
// 如果没有指定角色要求,只要登录即可
|
||||
if (requiredRole === null) return true
|
||||
|
||||
// 检查特定角色权限
|
||||
return user.value.system_role === requiredRole
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
const updateUserInfo = (userData) => {
|
||||
user.value = userData
|
||||
localStorage.setItem('user', JSON.stringify(userData))
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isLoggedIn,
|
||||
isAdmin,
|
||||
isAgent,
|
||||
userRole,
|
||||
login,
|
||||
logout,
|
||||
getCurrentUser,
|
||||
hasPermission,
|
||||
updateUserInfo
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1>管理员登录</h1>
|
||||
<p>虚拟商品交易平台管理系统</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
class="login-form"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<el-form-item prop="phone">
|
||||
<el-input
|
||||
v-model="loginForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
prefix-icon="Phone"
|
||||
size="large"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-button"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- <div class="login-footer">-->
|
||||
<!-- <div class="demo-accounts">-->
|
||||
<!-- <h4>测试账户:</h4>-->
|
||||
<!-- <div class="account-item">-->
|
||||
<!-- <span class="label">管理员:</span>-->
|
||||
<!-- <el-button type="text" @click="fillAccount('admin')">13800138000 / admin123</el-button>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="account-item">-->
|
||||
<!-- <span class="label">代理商:</span>-->
|
||||
<!-- <el-button type="text" @click="fillAccount('agent')">13800138001 / agent123</el-button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loginFormRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
phone: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 填充测试账户
|
||||
const fillAccount = (type) => {
|
||||
if (type === 'admin') {
|
||||
loginForm.phone = '13800138000'
|
||||
loginForm.password = 'admin123'
|
||||
} else if (type === 'agent') {
|
||||
loginForm.phone = '13800138001'
|
||||
loginForm.password = 'agent123'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
const valid = await loginFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
await authStore.login(loginForm)
|
||||
ElMessage.success('登录成功')
|
||||
if (authStore.isAdmin) {
|
||||
router.push('/admin/dashboard')
|
||||
}else {
|
||||
router.push('/admin/users')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style 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 {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 28px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #7f8c8d;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
border-top: 1px solid #ebeef5;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.demo-accounts h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.account-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.account-item .label {
|
||||
color: #909399;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.account-item .el-button {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.account-item .el-button:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<div class="page-header">
|
||||
<h2>个人信息</h2>
|
||||
</div>
|
||||
|
||||
<div class="profile-content">
|
||||
<!-- 个人信息卡片 -->
|
||||
<el-card class="profile-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>基本信息</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="profileFormRef"
|
||||
:model="profileForm"
|
||||
:rules="profileRules"
|
||||
label-width="100px"
|
||||
class="profile-form"
|
||||
>
|
||||
<!-- 头像上传 -->
|
||||
<el-form-item label="头像">
|
||||
<div class="avatar-upload">
|
||||
<div class="avatar-container">
|
||||
<el-image
|
||||
v-if="profileForm.avatar"
|
||||
:src="getImageUrl(profileForm.avatar)"
|
||||
fit="cover"
|
||||
class="avatar-preview"
|
||||
/>
|
||||
<div v-else class="avatar-placeholder">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<ImageUpload
|
||||
v-model="profileForm.avatar"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
class="avatar-uploader"
|
||||
>
|
||||
<el-button type="primary" size="small">
|
||||
<el-icon><Upload /></el-icon>
|
||||
更换头像
|
||||
</el-button>
|
||||
</ImageUpload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<el-form-item label="手机号">
|
||||
<el-input
|
||||
v-model="profileForm.phone"
|
||||
disabled
|
||||
placeholder="手机号码"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户姓名" prop="customer_name">
|
||||
<el-input
|
||||
v-model="profileForm.customer_name"
|
||||
placeholder="请输入客户姓名"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="真实姓名" prop="real_name">
|
||||
<el-input
|
||||
v-model="profileForm.real_name"
|
||||
placeholder="请输入真实姓名"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公司全称" prop="company_name">
|
||||
<el-input
|
||||
v-model="profileForm.company_name"
|
||||
placeholder="请输入公司全称"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="个人介绍" prop="personal_intro">
|
||||
<el-input
|
||||
v-model="profileForm.personal_intro"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入个人介绍"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身份码">
|
||||
<el-input
|
||||
v-model="profileForm.identity_code"
|
||||
disabled
|
||||
placeholder="系统自动生成"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="推荐人身份码">
|
||||
<el-input
|
||||
v-model="profileForm.referrer_identity_code"
|
||||
disabled
|
||||
placeholder="推荐人身份码"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 证件图片 -->
|
||||
<el-form-item label="营业执照">
|
||||
<ImageUpload
|
||||
v-model="profileForm.business_license"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身份证正面">
|
||||
<ImageUpload
|
||||
v-model="profileForm.id_card_front"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身份证反面">
|
||||
<ImageUpload
|
||||
v-model="profileForm.id_card_back"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 收款码 -->
|
||||
<el-form-item label="主收款码">
|
||||
<ImageUpload
|
||||
v-model="profileForm.main_payment_code"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="副收款码">
|
||||
<ImageUpload
|
||||
v-model="profileForm.backup_payment_code"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="updateProfile">
|
||||
保存修改
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 修改密码卡片 -->
|
||||
<el-card class="password-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>修改密码</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-width="100px"
|
||||
class="password-form"
|
||||
>
|
||||
<el-form-item label="当前密码" prop="current_password">
|
||||
<el-input
|
||||
v-model="passwordForm.current_password"
|
||||
type="password"
|
||||
placeholder="请输入当前密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="new_password">
|
||||
<el-input
|
||||
v-model="passwordForm.new_password"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirm_password">
|
||||
<el-input
|
||||
v-model="passwordForm.confirm_password"
|
||||
type="password"
|
||||
placeholder="请确认新密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="passwordSubmitting" @click="updatePassword">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button @click="resetPasswordForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { User, Upload } from '@element-plus/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const submitting = ref(false)
|
||||
const passwordSubmitting = ref(false)
|
||||
const profileFormRef = ref()
|
||||
const passwordFormRef = ref()
|
||||
|
||||
// 个人信息表单
|
||||
const profileForm = reactive({
|
||||
phone: '',
|
||||
customer_name: '',
|
||||
real_name: '',
|
||||
avatar: '',
|
||||
company_name: '',
|
||||
personal_intro: '',
|
||||
identity_code: '',
|
||||
referrer_identity_code: '',
|
||||
business_license: '',
|
||||
id_card_front: '',
|
||||
id_card_back: '',
|
||||
main_payment_code: '',
|
||||
backup_payment_code: ''
|
||||
})
|
||||
|
||||
// 密码修改表单
|
||||
const passwordForm = reactive({
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
// 个人信息验证规则
|
||||
const profileRules = {
|
||||
customer_name: [
|
||||
{ required: true, message: '请输入客户姓名', trigger: 'blur' },
|
||||
{ max: 50, message: '客户姓名不能超过50个字符', trigger: 'blur' }
|
||||
],
|
||||
real_name: [
|
||||
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
|
||||
{ max: 50, message: '真实姓名不能超过50个字符', trigger: 'blur' }
|
||||
],
|
||||
company_name: [
|
||||
{ max: 100, message: '公司名称不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
personal_intro: [
|
||||
{ max: 500, message: '个人介绍不能超过500个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 密码验证规则
|
||||
const passwordRules = {
|
||||
current_password: [
|
||||
{ required: true, message: '请输入当前密码', trigger: 'blur' }
|
||||
],
|
||||
new_password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6个字符', trigger: 'blur' },
|
||||
{ max: 20, message: '密码长度不能超过20个字符', trigger: 'blur' }
|
||||
],
|
||||
confirm_password: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value !== passwordForm.new_password) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
const response = await axios.get('/current-user')
|
||||
|
||||
if (response.data.success) {
|
||||
const userData = response.data.data
|
||||
Object.assign(profileForm, {
|
||||
phone: userData.phone || '',
|
||||
customer_name: userData.customer_name || '',
|
||||
real_name: userData.real_name || '',
|
||||
avatar: userData.avatar || '',
|
||||
company_name: userData.company_name || '',
|
||||
personal_intro: userData.personal_intro || '',
|
||||
identity_code: userData.identity_code || '',
|
||||
referrer_identity_code: userData.referrer_identity_code || '',
|
||||
business_license: userData.business_license_image || '',
|
||||
id_card_front: userData.id_card_front_image || '',
|
||||
id_card_back: userData.id_card_back_image || '',
|
||||
main_payment_code: userData.main_payment_qr_image || '',
|
||||
backup_payment_code: userData.sub_payment_qr_image || ''
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
ElMessage.error('获取用户信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${window.location.origin}${url}`
|
||||
}
|
||||
|
||||
// 更新个人信息
|
||||
const updateProfile = async () => {
|
||||
if (!profileFormRef.value) return
|
||||
|
||||
const valid = await profileFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const response = await axios.put('/update-profile', {
|
||||
customer_name: profileForm.customer_name,
|
||||
real_name: profileForm.real_name,
|
||||
avatar: profileForm.avatar,
|
||||
company_name: profileForm.company_name,
|
||||
personal_intro: profileForm.personal_intro,
|
||||
business_license: profileForm.business_license,
|
||||
id_card_front: profileForm.id_card_front,
|
||||
id_card_back: profileForm.id_card_back,
|
||||
main_payment_code: profileForm.main_payment_code,
|
||||
backup_payment_code: profileForm.backup_payment_code
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('个人信息更新成功')
|
||||
// 更新全局用户信息
|
||||
authStore.updateUserInfo(response.data.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新个人信息失败:', error)
|
||||
ElMessage.error('更新个人信息失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const updatePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
|
||||
const valid = await passwordFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
passwordSubmitting.value = true
|
||||
|
||||
try {
|
||||
const response = await axios.put('/change-password', {
|
||||
current_password: passwordForm.current_password,
|
||||
new_password: passwordForm.new_password
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('密码修改成功')
|
||||
resetPasswordForm()
|
||||
|
||||
// 提醒用户重新登录
|
||||
await ElMessageBox.confirm(
|
||||
'密码已修改成功,为了安全起见,请重新登录。',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '重新登录',
|
||||
cancelButtonText: '稍后再说',
|
||||
type: 'success'
|
||||
}
|
||||
)
|
||||
|
||||
// 退出登录
|
||||
authStore.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
const message = error.response?.data?.message || '修改密码失败'
|
||||
ElMessage.error(message)
|
||||
} finally {
|
||||
passwordSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码表单
|
||||
const resetPasswordForm = () => {
|
||||
Object.assign(passwordForm, {
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
if (passwordFormRef.value) {
|
||||
passwordFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取用户信息
|
||||
onMounted(() => {
|
||||
getCurrentUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.profile-card,
|
||||
.password-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.profile-form,
|
||||
.password-form {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 2px dashed #dcdfe6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
color: #c0c4cc;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.profile-form,
|
||||
.password-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div class="banners-page">
|
||||
<div class="page-header">
|
||||
<h2>轮播图管理</h2>
|
||||
<el-button type="primary" @click="showCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加轮播图
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 轮播图列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="banners"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="图片" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.image_url"
|
||||
:src="(row.image_url)"
|
||||
|
||||
class="banner-preview"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
/>
|
||||
<span v-else class="no-image">无图片</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column prop="link_url" label="链接地址" min-width="200" show-overflow-tooltip />-->
|
||||
<!-- -->
|
||||
<!-- <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />-->
|
||||
<!-- -->
|
||||
<el-table-column prop="sort_order" label="排序" width="80" />
|
||||
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 1 ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="danger-text"
|
||||
@click="deleteBanner(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getBanners"
|
||||
@current-change="getBanners"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑轮播图对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogMode === 'create' ? '添加轮播图' : '编辑轮播图'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="bannerFormRef"
|
||||
:model="bannerForm"
|
||||
:rules="bannerRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input
|
||||
v-model="bannerForm.title"
|
||||
placeholder="请输入轮播图标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片" prop="image_url">
|
||||
<ImageUpload
|
||||
v-model="bannerForm.image_url"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
list-type="picture-card"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item label="链接地址" prop="link_url">-->
|
||||
<!-- <el-input-->
|
||||
<!-- v-model="bannerForm.link_url"-->
|
||||
<!-- placeholder="请输入跳转链接(可选)"-->
|
||||
<!-- />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- -->
|
||||
<!-- <el-form-item label="描述" prop="description">-->
|
||||
<!-- <el-input-->
|
||||
<!-- v-model="bannerForm.description"-->
|
||||
<!-- type="textarea"-->
|
||||
<!-- :rows="3"-->
|
||||
<!-- placeholder="请输入描述信息(可选)"-->
|
||||
<!-- />-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<el-form-item label="排序" prop="sort_order">
|
||||
<el-input-number
|
||||
v-model="bannerForm.sort_order"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="bannerForm.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitBanner">
|
||||
{{ dialogMode === 'create' ? '添加' : '保存' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import axios from 'axios'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const banners = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogMode = ref('create') // create | edit
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 轮播图表单
|
||||
const bannerForm = reactive({
|
||||
id: null,
|
||||
title: '',
|
||||
image_url: '',
|
||||
link_url: '',
|
||||
description: '',
|
||||
sort_order: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const bannerFormRef = ref()
|
||||
|
||||
// 表单验证规则
|
||||
const bannerRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入轮播图标题', trigger: 'blur' }
|
||||
],
|
||||
image_url: [
|
||||
{ required: true, message: '请上传轮播图图片', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取轮播图列表
|
||||
const getBanners = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await axios.get('/admin/banners', {
|
||||
params: {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
banners.value = data.data.list || []
|
||||
pagination.total = data.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(data.message || '获取轮播图列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取轮播图列表失败:', error)
|
||||
ElMessage.error('获取轮播图列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogMode.value = 'create'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (banner) => {
|
||||
dialogMode.value = 'edit'
|
||||
Object.assign(bannerForm, {
|
||||
id: banner.id,
|
||||
title: banner.title,
|
||||
image_url: banner.image_url,
|
||||
link_url: banner.link_url || '',
|
||||
description: banner.description || '',
|
||||
sort_order: banner.sort_order || 0,
|
||||
status: banner.status
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(bannerForm, {
|
||||
id: null,
|
||||
title: '',
|
||||
image_url: '',
|
||||
link_url: '',
|
||||
description: '',
|
||||
sort_order: 0,
|
||||
status: 1
|
||||
})
|
||||
bannerFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 提交轮播图
|
||||
const submitBanner = async () => {
|
||||
if (!bannerFormRef.value) return
|
||||
|
||||
const valid = await bannerFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const url = dialogMode.value === 'create'
|
||||
? '/admin/banners'
|
||||
: `/admin/banners/${bannerForm.id}`
|
||||
|
||||
const method = dialogMode.value === 'create' ? 'post' : 'put'
|
||||
|
||||
const { data } = await axios[method](url, {
|
||||
title: bannerForm.title,
|
||||
image_url: bannerForm.image_url,
|
||||
link_url: bannerForm.link_url,
|
||||
description: bannerForm.description,
|
||||
sort_order: bannerForm.sort_order,
|
||||
status: bannerForm.status
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
ElMessage.success(dialogMode.value === 'create' ? '轮播图添加成功' : '轮播图更新成功')
|
||||
dialogVisible.value = false
|
||||
getBanners()
|
||||
} else {
|
||||
ElMessage.error(data.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交轮播图失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除轮播图
|
||||
const deleteBanner = async (banner) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除轮播图 "${banner.title}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const { data } = await axios.delete(`/admin/banners/${banner.id}`)
|
||||
|
||||
if (data.success) {
|
||||
ElMessage.success('轮播图删除成功')
|
||||
getBanners()
|
||||
} else {
|
||||
ElMessage.error(data.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除轮播图失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取完整图片URL
|
||||
const getFullImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${axios.defaults.baseURL}${url}`
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
getBanners()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banners-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.banner-preview {
|
||||
width: 80px;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.danger-text:hover {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,606 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="dashboard-header">
|
||||
<h2>仪表盘</h2>
|
||||
<p>欢迎回来,{{ authStore.user?.customer_name || authStore.user?.real_name }}!</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon user-icon">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stats.totalUsers || 0 }}</div>
|
||||
<div class="stat-label">总用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon order-icon">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stats.totalOrders || 0 }}</div>
|
||||
<div class="stat-label">总订单数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon product-icon">
|
||||
<el-icon><Box /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">{{ stats.totalProducts || 0 }}</div>
|
||||
<div class="stat-label">商品总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon money-icon">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number">¥{{ formatAmount(stats.totalAmount) }}</div>
|
||||
<div class="stat-label">交易总额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 用户搜索统计 -->
|
||||
<div class="user-search-section">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户统计查询</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="search-form">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-input
|
||||
v-model="userSearchQuery"
|
||||
placeholder="输入手机号或用户姓名"
|
||||
clearable
|
||||
@keyup.enter="searchUserStats"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="primary" @click="searchUserStats" :loading="userSearchLoading">
|
||||
查询统计
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 用户统计结果 -->
|
||||
<div v-if="selectedUserStats" class="user-stats-result">
|
||||
<el-divider content-position="left">用户信息</el-divider>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="客户姓名">{{ selectedUserStats.user.customer_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实姓名">{{ selectedUserStats.user.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ selectedUserStats.user.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户角色">{{ getUserRoleName(selectedUserStats.user.system_role) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ formatTime(selectedUserStats.user.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="公司名称">{{ selectedUserStats.user.company_name || '未填写' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">
|
||||
统计数据
|
||||
<span v-if="dateRange && dateRange.length === 2" style="font-size: 12px; color: #909399; margin-left: 10px;">
|
||||
({{ dateRange[0] }} 至 {{ dateRange[1] }})
|
||||
</span>
|
||||
</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="采购订单数" :value="selectedUserStats.stats.purchase_orders_count || 0" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="销售订单数" :value="selectedUserStats.stats.sales_orders_count || 0" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="采购总金额" :value="selectedUserStats.stats.total_purchase_amount || 0" prefix="¥" :precision="2" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="销售总金额" :value="selectedUserStats.stats.total_sales_amount || 0" prefix="¥" :precision="2" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="库存商品种类" :value="selectedUserStats.stats.warehouse_items_count || 0" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="库存商品总数" :value="selectedUserStats.stats.total_warehouse_quantity || 0" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="积分余额" :value="selectedUserStats.stats.score_balance || 0" />
|
||||
</el-col>
|
||||
<!-- <el-col :span="6">-->
|
||||
<!-- <el-statistic title="最后活动时间" :value="formatTime(selectedUserStats.stats.last_activity_time)" />-->
|
||||
<!-- </el-col>-->
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div v-else-if="userSearched && !selectedUserStats" class="no-user-found">
|
||||
<el-empty description="未找到匹配的用户" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-section" v-if="authStore.isAdmin">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card title="用户角色分布">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户角色分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="roleChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card title="订单趋势">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最近7天订单趋势</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="orderTrendChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div v-if="authStore.isAdmin" class="recent-activities">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最近活动</span>
|
||||
<el-button type="text" @click="refreshActivities">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
:timestamp="activity.time"
|
||||
placement="top"
|
||||
>
|
||||
<el-card>
|
||||
<p>{{ activity.description }}</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<div v-if="recentActivities.length === 0" class="no-data">
|
||||
<el-empty description="暂无活动记录" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div >
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import * as echarts from 'echarts'
|
||||
import axios from 'axios'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const stats = ref({})
|
||||
const recentActivities = ref([])
|
||||
const roleChartRef = ref(null)
|
||||
const orderTrendChartRef = ref(null)
|
||||
const userInfo=ref( null)
|
||||
// 用户搜索相关
|
||||
const userSearchQuery = ref('')
|
||||
const dateRange = ref([])
|
||||
const selectedUserStats = ref(null)
|
||||
const userSearchLoading = ref(false)
|
||||
const userSearched = ref(false)
|
||||
|
||||
let roleChart = null
|
||||
let orderTrendChart = null
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0.00'
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const [userStats, orderStats, productStats, orderTrendData] = await Promise.all([
|
||||
axios.get('/admin/users/stats'),
|
||||
axios.get('/admin/orders/purchase/stats'),
|
||||
authStore.isAdmin ? axios.get('/admin/products/primary/stats') : Promise.resolve({ data: { data: {} } }),
|
||||
axios.get('/admin/orders/purchase/trend')
|
||||
])
|
||||
|
||||
// 处理订单趋势数据
|
||||
const trendData = orderTrendData.data.data.trend || []
|
||||
const days = trendData.map(item => item.date)
|
||||
const orders = trendData.map(item => item.count)
|
||||
|
||||
stats.value = {
|
||||
totalUsers: userStats.data.data.total_users || 0,
|
||||
totalOrders: (orderStats.data.data.total_purchase_orders || 0) + (orderStats.data.data.total_sales_orders || 0),
|
||||
totalProducts: productStats.data.data.total_primary_products || 0,
|
||||
totalAmount: (orderStats.data.data.total_purchase_amount || 0) + (orderStats.data.data.total_sales_amount || 0),
|
||||
roleStats: userStats.data.data.role_counts || [],
|
||||
orderTrend: { days, orders }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化角色分布图表
|
||||
const initRoleChart = () => {
|
||||
if (!roleChartRef.value) return
|
||||
|
||||
roleChart = echarts.init(roleChartRef.value)
|
||||
|
||||
const roleNames = { 0: '管理员', 1: '代理商', 2: '普通用户' }
|
||||
const data = (stats.value.roleStats || []).map(item => ({
|
||||
name: roleNames[item.system_role] || '未知',
|
||||
value: item.count
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
bottom: 10,
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用户角色',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
roleChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化订单趋势图表
|
||||
const initOrderTrendChart = () => {
|
||||
if (!orderTrendChartRef.value) return
|
||||
|
||||
orderTrendChart = echarts.init(orderTrendChartRef.value)
|
||||
|
||||
const trendData = stats.value.orderTrend || { days: [], orders: [] }
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: trendData.days
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '订单数',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
areaStyle: {},
|
||||
data: trendData.orders
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
orderTrendChart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取最近活动
|
||||
const getRecentActivities = async () => {
|
||||
try {
|
||||
const response = await axios.get('/admin/orders/purchase/activities')
|
||||
if (response.data.success) {
|
||||
// 格式化时间
|
||||
recentActivities.value = (response.data.data.activities || []).map(activity => ({
|
||||
id: activity.id,
|
||||
description: activity.description,
|
||||
time: new Date(activity.time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新活动记录
|
||||
const refreshActivities = () => {
|
||||
getRecentActivities()
|
||||
}
|
||||
|
||||
// 获取用户角色名称
|
||||
const getUserRoleName = (role) => {
|
||||
const roleNames = { 0: '管理员', 1: '代理商', 2: '普通用户' }
|
||||
return roleNames[role] || '未知'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '未知'
|
||||
return new Date(timeStr).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索用户统计
|
||||
const searchUserStats = async () => {
|
||||
if (!userSearchQuery.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
userSearchLoading.value = true
|
||||
userSearched.value = true
|
||||
selectedUserStats.value = null
|
||||
|
||||
try {
|
||||
const params = {
|
||||
query: userSearchQuery.value.trim()
|
||||
}
|
||||
|
||||
// 添加时间范围参数
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.start_date = dateRange.value[0]
|
||||
params.end_date = dateRange.value[1]
|
||||
}
|
||||
|
||||
const response = await axios.get('/admin/users/search-stats', {
|
||||
params: params
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
selectedUserStats.value = response.data.data
|
||||
} else {
|
||||
selectedUserStats.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索用户统计失败:', error)
|
||||
selectedUserStats.value = null
|
||||
} finally {
|
||||
userSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后初始化
|
||||
onMounted(async () => {
|
||||
await getStats()
|
||||
await getRecentActivities()
|
||||
|
||||
nextTick(() => {
|
||||
initRoleChart()
|
||||
initOrderTrendChart()
|
||||
})
|
||||
})
|
||||
|
||||
// 响应式图表
|
||||
window.addEventListener('resize', () => {
|
||||
if (roleChart) roleChart.resize()
|
||||
if (orderTrendChart) orderTrendChart.resize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #303133;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
margin: 0;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.order-icon {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.product-icon {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.money-icon {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recent-activities {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.user-search-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-stats-result {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.no-user-found {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.charts-section .el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="scores-page">
|
||||
<div class="page-header">
|
||||
<h2>积分管理</h2>
|
||||
<el-button v-if="authStore.isAdmin" type="primary" @click="showCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加积分记录
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input
|
||||
v-model="searchForm.user_id"
|
||||
placeholder="请输入用户ID"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号">
|
||||
<el-input
|
||||
v-model="searchForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="变化类型">
|
||||
<el-select
|
||||
v-model="searchForm.change_type"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="增加" value="positive" />
|
||||
<el-option label="减少" value="negative" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getScoreRecords">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 积分记录列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="scoreRecords"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||
|
||||
<el-table-column label="用户信息" width="160">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.user?.customer_name || row.user?.phone }}</div>
|
||||
<div class="text-xs text-gray-500">ID: {{ row.user_id }}</div>
|
||||
<div class="text-xs text-gray-500">{{ row.user?.phone }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="change_number" label="积分变化" width="120" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<span :class="getChangeNumberClass(row.change_number)">
|
||||
{{ row.change_number > 0 ? '+' : '' }}{{ row.change_number }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="当前积分" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold">{{ row.user?.current_points || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="note" label="说明" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showScoreDetail(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<!-- <el-button-->
|
||||
<!-- v-if="authStore.isAdmin"-->
|
||||
<!-- type="text"-->
|
||||
<!-- size="small"-->
|
||||
<!-- @click="showEditDialog(row)"-->
|
||||
<!-- >-->
|
||||
<!-- 编辑-->
|
||||
<!-- </el-button>-->
|
||||
<el-button
|
||||
v-if="authStore.isAdmin"
|
||||
type="text"
|
||||
size="small"
|
||||
class="danger-text"
|
||||
@click="deleteScoreRecord(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getScoreRecords"
|
||||
@current-change="getScoreRecords"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑积分记录对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogMode === 'create' ? '添加积分记录' : '编辑积分记录'"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="scoreFormRef"
|
||||
:model="scoreForm"
|
||||
:rules="scoreRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<el-select
|
||||
v-model="scoreForm.user_id"
|
||||
placeholder="请选择用户"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="searchUsers"
|
||||
:loading="userSearchLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.id"
|
||||
:label="`${user.customer_name || user.real_name} (${user.phone})`"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="积分变化" prop="change_number">
|
||||
<el-input-number
|
||||
v-model="scoreForm.change_number"
|
||||
:min="-999999"
|
||||
:max="999999"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="说明" prop="note">
|
||||
<el-input
|
||||
v-model="scoreForm.note"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入变化说明"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitScore">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 积分记录详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="积分记录详情"
|
||||
width="600px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="记录ID">{{ currentRecord.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ currentRecord.user_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户姓名">{{ currentRecord.user?.customer_name || currentRecord.user?.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户手机">{{ currentRecord.user?.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="积分变化">
|
||||
<span :class="getChangeNumberClass(currentRecord.change_number)">
|
||||
{{ currentRecord.change_number > 0 ? '+' : '' }}{{ currentRecord.change_number }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前积分">{{ currentRecord.user?.current_points || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="说明" :span="2">{{ currentRecord.note }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentRecord.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentRecord.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const scoreRecords = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const dialogMode = ref('create')
|
||||
const submitting = ref(false)
|
||||
const scoreFormRef = ref()
|
||||
const userSearchLoading = ref(false)
|
||||
const userOptions = ref([])
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
user_id: '',
|
||||
phone: '',
|
||||
change_type: ''
|
||||
})
|
||||
|
||||
// 积分记录表单
|
||||
const scoreForm = reactive({
|
||||
user_id: '',
|
||||
change_number: 0,
|
||||
note: ''
|
||||
})
|
||||
|
||||
// 当前查看的记录
|
||||
const currentRecord = ref({})
|
||||
|
||||
// 表单验证规则
|
||||
const scoreRules = {
|
||||
user_id: [
|
||||
{ required: true, message: '请选择用户', trigger: 'change' }
|
||||
],
|
||||
change_number: [
|
||||
{ required: true, message: '请输入积分变化', trigger: 'blur' },
|
||||
{ type: 'number', message: '积分变化必须为数字' }
|
||||
],
|
||||
note: [
|
||||
{ required: true, message: '请输入变化说明', trigger: 'blur' },
|
||||
{ max: 200, message: '说明不能超过200个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取积分记录列表
|
||||
const getScoreRecords = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/scores', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
scoreRecords.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取积分记录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
user_id: '',
|
||||
phone: '',
|
||||
change_type: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getScoreRecords()
|
||||
}
|
||||
|
||||
// 处理排序
|
||||
const handleSortChange = ({ prop, order }) => {
|
||||
// 实现排序逻辑
|
||||
console.log('排序:', prop, order)
|
||||
}
|
||||
|
||||
// 获取积分变化样式
|
||||
const getChangeNumberClass = (changeNumber) => {
|
||||
if (changeNumber > 0) {
|
||||
return 'text-green-500 font-semibold'
|
||||
} else if (changeNumber < 0) {
|
||||
return 'text-red-500 font-semibold'
|
||||
}
|
||||
return 'text-gray-500'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
const searchUsers = async (query) => {
|
||||
if (!query) {
|
||||
userOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
userSearchLoading.value = true
|
||||
try {
|
||||
const response = await axios.get('/admin/users', {
|
||||
params: {
|
||||
phone: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
userOptions.value = response.data.data.list || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索用户失败:', error)
|
||||
} finally {
|
||||
userSearchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogMode.value = 'create'
|
||||
resetScoreForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (record) => {
|
||||
dialogMode.value = 'edit'
|
||||
Object.assign(scoreForm, {
|
||||
user_id: record.user_id,
|
||||
change_number: record.change_number,
|
||||
note: record.note
|
||||
})
|
||||
// 预填充用户选项
|
||||
userOptions.value = [record.user]
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示积分记录详情
|
||||
const showScoreDetail = (record) => {
|
||||
currentRecord.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 重置积分记录表单
|
||||
const resetScoreForm = () => {
|
||||
Object.assign(scoreForm, {
|
||||
user_id: '',
|
||||
change_number: 0,
|
||||
note: ''
|
||||
})
|
||||
userOptions.value = []
|
||||
if (scoreFormRef.value) {
|
||||
scoreFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交积分记录表单
|
||||
const submitScore = async () => {
|
||||
if (!scoreFormRef.value) return
|
||||
|
||||
const valid = await scoreFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
if (dialogMode.value === 'create') {
|
||||
await axios.post('/admin/scores', scoreForm)
|
||||
ElMessage.success('积分记录创建成功')
|
||||
} else {
|
||||
await axios.put(`/admin/scores/${currentRecord.value.id}`, scoreForm)
|
||||
ElMessage.success('积分记录更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getScoreRecords()
|
||||
} catch (error) {
|
||||
console.error('提交积分记录失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除积分记录
|
||||
const deleteScoreRecord = async (record) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除这条积分记录吗?删除后用户积分将会恢复。`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await axios.delete(`/admin/scores/${record.id}`)
|
||||
ElMessage.success('积分记录删除成功')
|
||||
getScoreRecords()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除积分记录失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getScoreRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scores-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.danger-text:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,692 @@
|
||||
<template>
|
||||
<div class="system-configs-page">
|
||||
<div class="page-header">
|
||||
<h2>系统配置管理</h2>
|
||||
<el-button type="primary" @click="showCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加配置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="配置键">
|
||||
<el-input
|
||||
v-model="searchForm.config_key"
|
||||
placeholder="请输入配置键"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="配置类型">
|
||||
<el-select
|
||||
v-model="searchForm.config_type"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="字符串" value="string" />
|
||||
<el-option label="数字" value="number" />
|
||||
<el-option label="布尔值" value="boolean" />
|
||||
<el-option label="JSON" value="json" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="系统配置">
|
||||
<el-select
|
||||
v-model="searchForm.is_system"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="是" value="1" />
|
||||
<el-option label="否" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getConfigs">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 配置列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="configs"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column prop="config_key" label="配置键" width="200" />
|
||||
|
||||
<el-table-column prop="config_name" label="配置名称" width="150" />
|
||||
|
||||
<el-table-column prop="config_value" label="配置值" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div class="config-value">
|
||||
<span v-if="row.config_type === 'boolean'">
|
||||
<el-tag :type="row.config_value === 'true' ? 'success' : 'danger'" size="small">
|
||||
{{ row.config_value === 'true' ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span v-else-if="row.config_type === 'json'" class="json-value">
|
||||
{{ formatJsonValue(row.config_value) }}
|
||||
</span>
|
||||
<span v-else>{{ row.config_value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="config_type" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagColor(row.config_type)" size="small">
|
||||
{{ getTypeName(row.config_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="is_system" label="系统配置" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_system === 1 ? 'warning' : 'info'" size="small">
|
||||
{{ row.is_system === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="config_description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showConfigDetail(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.is_system !== 1"
|
||||
type="text"
|
||||
size="small"
|
||||
class="danger-text"
|
||||
@click="deleteConfig(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getConfigs"
|
||||
@current-change="getConfigs"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑配置对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogMode === 'create' ? '添加配置' : '编辑配置'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="configFormRef"
|
||||
:model="configForm"
|
||||
:rules="configRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="配置键" prop="config_key">
|
||||
<el-input
|
||||
v-model="configForm.config_key"
|
||||
placeholder="请输入配置键(英文字母、数字、下划线)"
|
||||
:disabled="dialogMode === 'edit' && configForm.is_system === 1"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="配置名称" prop="config_name">
|
||||
<el-input
|
||||
v-model="configForm.config_name"
|
||||
placeholder="请输入配置名称"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="配置类型" prop="config_type">
|
||||
<el-select
|
||||
v-model="configForm.config_type"
|
||||
placeholder="请选择配置类型"
|
||||
style="width: 100%"
|
||||
@change="onTypeChange"
|
||||
>
|
||||
<el-option label="字符串" value="string" />
|
||||
<el-option label="数字" value="number" />
|
||||
<el-option label="布尔值" value="boolean" />
|
||||
<el-option label="JSON" value="json" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="配置值" prop="config_value">
|
||||
<!-- 字符串类型 -->
|
||||
<el-input
|
||||
v-if="configForm.config_type === 'string'"
|
||||
v-model="configForm.config_value"
|
||||
placeholder="请输入配置值"
|
||||
maxlength="1000"
|
||||
/>
|
||||
|
||||
<!-- 数字类型 -->
|
||||
<el-input-number
|
||||
v-else-if="configForm.config_type === 'number'"
|
||||
v-model="configForm.config_value"
|
||||
:precision="6"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
|
||||
<!-- 布尔值类型 -->
|
||||
<el-radio-group
|
||||
v-else-if="configForm.config_type === 'boolean'"
|
||||
v-model="configForm.config_value"
|
||||
>
|
||||
<el-radio label="true">是</el-radio>
|
||||
<el-radio label="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<!-- JSON类型 -->
|
||||
<el-input
|
||||
v-else-if="configForm.config_type === 'json'"
|
||||
v-model="configForm.config_value"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入有效的JSON格式"
|
||||
/>
|
||||
|
||||
<!-- 默认 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="configForm.config_value"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="配置描述">
|
||||
<el-input
|
||||
v-model="configForm.config_description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置描述"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitConfig">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 配置详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="配置详情"
|
||||
width="600px"
|
||||
>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="配置ID">{{ currentConfig.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="配置键">{{ currentConfig.config_key }}</el-descriptions-item>
|
||||
<el-descriptions-item label="配置名称">{{ currentConfig.config_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="配置类型">
|
||||
<el-tag :type="getTypeTagColor(currentConfig.config_type)">
|
||||
{{ getTypeName(currentConfig.config_type) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="系统配置">
|
||||
<el-tag :type="currentConfig.is_system === 1 ? 'warning' : 'info'">
|
||||
{{ currentConfig.is_system === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="配置值">
|
||||
<div class="config-value-detail">
|
||||
<span v-if="currentConfig.config_type === 'boolean'">
|
||||
<el-tag :type="currentConfig.config_value === 'true' ? 'success' : 'danger'">
|
||||
{{ currentConfig.config_value === 'true' ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<pre v-else-if="currentConfig.config_type === 'json'" class="json-preview">{{ formatJson(currentConfig.config_value) }}</pre>
|
||||
<span v-else>{{ currentConfig.config_value }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="配置描述">{{ currentConfig.config_description || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentConfig.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentConfig.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const configs = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const dialogMode = ref('create')
|
||||
const submitting = ref(false)
|
||||
const configFormRef = ref()
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
config_key: '',
|
||||
config_type: '',
|
||||
is_system: ''
|
||||
})
|
||||
|
||||
// 配置表单
|
||||
const configForm = reactive({
|
||||
config_key: '',
|
||||
config_name: '',
|
||||
config_value: '',
|
||||
config_type: 'string',
|
||||
config_description: '',
|
||||
is_system: 0
|
||||
})
|
||||
|
||||
// 当前查看的配置
|
||||
const currentConfig = ref({})
|
||||
|
||||
// 表单验证规则
|
||||
const configRules = {
|
||||
config_key: [
|
||||
{ required: true, message: '请输入配置键', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '配置键只能包含字母、数字和下划线', trigger: 'blur' },
|
||||
{ max: 100, message: '配置键不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
config_name: [
|
||||
{ required: true, message: '请输入配置名称', trigger: 'blur' },
|
||||
{ max: 200, message: '配置名称不能超过200个字符', trigger: 'blur' }
|
||||
],
|
||||
config_type: [
|
||||
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||||
],
|
||||
config_value: [
|
||||
{ required: true, message: '请输入配置值', trigger: 'blur' },
|
||||
{ validator: validateConfigValue, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 配置值验证器
|
||||
function validateConfigValue(rule, value, callback) {
|
||||
if (!value && value !== 0 && value !== false) {
|
||||
callback(new Error('请输入配置值'))
|
||||
return
|
||||
}
|
||||
|
||||
const type = configForm.config_type
|
||||
|
||||
if (type === 'number') {
|
||||
if (isNaN(Number(value))) {
|
||||
callback(new Error('请输入有效的数字'))
|
||||
return
|
||||
}
|
||||
} else if (type === 'boolean') {
|
||||
if (value !== 'true' && value !== 'false') {
|
||||
callback(new Error('请选择布尔值'))
|
||||
return
|
||||
}
|
||||
} else if (type === 'json') {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
} catch (error) {
|
||||
callback(new Error('请输入有效的JSON格式'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
// 获取配置列表
|
||||
const getConfigs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/configs', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
configs.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
config_key: '',
|
||||
config_type: '',
|
||||
is_system: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getConfigs()
|
||||
}
|
||||
|
||||
// 获取类型名称
|
||||
const getTypeName = (type) => {
|
||||
const typeMap = {
|
||||
string: '字符串',
|
||||
number: '数字',
|
||||
boolean: '布尔值',
|
||||
json: 'JSON'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取类型标签颜色
|
||||
const getTypeTagColor = (type) => {
|
||||
const colorMap = {
|
||||
string: '',
|
||||
number: 'success',
|
||||
boolean: 'warning',
|
||||
json: 'danger'
|
||||
}
|
||||
return colorMap[type] || ''
|
||||
}
|
||||
|
||||
// 格式化JSON值(简化显示)
|
||||
const formatJsonValue = (value) => {
|
||||
try {
|
||||
const obj = JSON.parse(value)
|
||||
return typeof obj === 'object' ? `{${Object.keys(obj).length} 项}` : value
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化JSON(详细显示)
|
||||
const formatJson = (value) => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 类型变化处理
|
||||
const onTypeChange = (type) => {
|
||||
if (type === 'boolean') {
|
||||
configForm.config_value = 'true'
|
||||
} else if (type === 'number') {
|
||||
configForm.config_value = 0
|
||||
} else if (type === 'json') {
|
||||
configForm.config_value = '{}'
|
||||
} else {
|
||||
configForm.config_value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogMode.value = 'create'
|
||||
resetConfigForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (config) => {
|
||||
dialogMode.value = 'edit'
|
||||
Object.assign(configForm, {
|
||||
id: config.id,
|
||||
config_key: config.config_key,
|
||||
config_name: config.config_name,
|
||||
config_value: config.config_value,
|
||||
config_type: config.config_type,
|
||||
config_description: config.config_description,
|
||||
is_system: config.is_system
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示配置详情
|
||||
const showConfigDetail = (config) => {
|
||||
currentConfig.value = config
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 重置配置表单
|
||||
const resetConfigForm = () => {
|
||||
Object.assign(configForm, {
|
||||
config_key: '',
|
||||
config_name: '',
|
||||
config_value: '',
|
||||
config_type: 'string',
|
||||
config_description: '',
|
||||
is_system: 0
|
||||
})
|
||||
if (configFormRef.value) {
|
||||
configFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交配置表单
|
||||
const submitConfig = async () => {
|
||||
if (!configFormRef.value) return
|
||||
|
||||
const valid = await configFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 根据类型转换配置值
|
||||
let submitData = { ...configForm }
|
||||
if (configForm.config_type === 'number') {
|
||||
submitData.config_value = String(Number(configForm.config_value))
|
||||
}
|
||||
|
||||
if (dialogMode.value === 'create') {
|
||||
await axios.post('/admin/configs', submitData)
|
||||
ElMessage.success('配置创建成功')
|
||||
} else {
|
||||
const { id, ...updateData } = submitData
|
||||
await axios.put(`/admin/configs/${id}`, updateData)
|
||||
ElMessage.success('配置更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getConfigs()
|
||||
} catch (error) {
|
||||
console.error('提交配置失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const deleteConfig = async (config) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除配置 "${config.config_name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await axios.delete(`/admin/configs/${config.id}`)
|
||||
ElMessage.success('配置删除成功')
|
||||
getConfigs()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除配置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.system-configs-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.config-value {
|
||||
max-width: 200px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.json-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-value-detail {
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.danger-text:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<div class="user-warehouses-page">
|
||||
<div class="page-header">
|
||||
<h2>用户库存管理</h2>
|
||||
<el-button type="primary" @click="refreshData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="搜索用户">
|
||||
<el-input
|
||||
v-model="searchForm.search"
|
||||
placeholder="请输入手机号或用户姓名"
|
||||
clearable
|
||||
style="width: 250px"
|
||||
@keyup.enter="getUserWarehouses"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item label="商品类型">-->
|
||||
<!-- <el-select-->
|
||||
<!-- v-model="searchForm.product_type"-->
|
||||
<!-- placeholder="请选择商品类型"-->
|
||||
<!-- clearable-->
|
||||
<!-- style="width: 150px"-->
|
||||
<!-- >-->
|
||||
<!-- <el-option label="一级商品" :value="1" />-->
|
||||
<!-- <el-option label="二级商品" :value="2" />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<el-form-item label="库存状态">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="库存中" :value="1" />
|
||||
<el-option label="已出售" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getUserWarehouses" :loading="loading">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户库存统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<el-statistic title="有库存用户数" :value="totalUsers || 0" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<el-statistic title="库存商品总数" :value="totalQuantity || 0" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- <el-col :span="6">-->
|
||||
<!-- <el-card class="stat-card">-->
|
||||
<!-- <el-statistic title="库存总价值" :value="totalValue || 0" prefix="¥" :precision="2" />-->
|
||||
<!-- </el-card>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="6">-->
|
||||
<!-- <el-card class="stat-card">-->
|
||||
<!-- <el-statistic title="已出售数量" :value="soldCount || 0" />-->
|
||||
<!-- </el-card>-->
|
||||
<!-- </el-col>-->
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 用户库存列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="userWarehouses"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||
|
||||
<el-table-column label="用户信息" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ row.customer_name || row.real_name || '未知用户' }}</div>
|
||||
<div class="user-phone">{{ row.phone }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_type" label="商品类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.product_type === 1 ? 'primary' : 'success'" size="small">
|
||||
{{ row.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="total_quantity" label="库存数量" width="100" sortable>
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'low-stock': row.total_quantity < 10 }">{{ row.total_quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="total_value" label="库存价值" width="120" sortable>
|
||||
<template #default="{ row }">
|
||||
¥{{ formatAmount(row.total_value) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '库存中' : '已出售' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column label="统计信息" width="200">-->
|
||||
<!-- <template #default="{ row }">-->
|
||||
<!-- <div class="stats-info">-->
|
||||
<!-- <div>一级: {{ row.primary_product_count }}</div>-->
|
||||
<!-- <div>二级: {{ row.secondary_product_count }}</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="viewUserDetail(row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getUserWarehouses"
|
||||
@current-change="getUserWarehouses"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户库存详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
:title="`用户库存详情 - ${currentUser?.customer_name || currentUser?.real_name || ''}`"
|
||||
width="800px"
|
||||
>
|
||||
<div v-if="currentUserWarehouses.length > 0" class="warehouse-detail">
|
||||
<el-descriptions :column="2" border class="mb-4">
|
||||
<el-descriptions-item label="客户姓名">{{ currentUser.customer_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实姓名">{{ currentUser.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentUser.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存总价值">
|
||||
<span class="text-primary">¥{{ formatAmount(currentUserTotalValue) }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-table :data="currentUserWarehouses" stripe>
|
||||
<el-table-column prop="product_type" label="商品类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.product_type === 1 ? 'primary' : 'success'" size="small">
|
||||
{{ row.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="200" />
|
||||
|
||||
<el-table-column prop="total_quantity" label="数量" width="80" />
|
||||
|
||||
<el-table-column prop="total_value" label="价值" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ formatAmount(row.total_value) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '库存中' : '已出售' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<el-empty description="暂无库存数据" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import axios from 'axios'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const userWarehouses = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const currentUser = ref({})
|
||||
const currentUserWarehouses = ref([])
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
search: '',
|
||||
product_type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 统计信息
|
||||
const totalUsers = ref(0)
|
||||
const totalQuantity = ref(0)
|
||||
const totalValue = ref(0)
|
||||
const soldCount = ref(0)
|
||||
|
||||
// 计算当前用户库存总价值
|
||||
const currentUserTotalValue = computed(() => {
|
||||
return currentUserWarehouses.value.reduce((sum, item) => {
|
||||
return sum + (item.total_value || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0.00'
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户库存列表
|
||||
const getUserWarehouses = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/warehouse/users', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
const data = response.data.data
|
||||
// 如果后端返回的数据超过pageSize,只显示pageSize条
|
||||
const list = data.list || []
|
||||
if (list.length > pagination.pageSize) {
|
||||
userWarehouses.value = list.slice(0, pagination.pageSize)
|
||||
} else {
|
||||
userWarehouses.value = list
|
||||
}
|
||||
pagination.total = data.total || 0
|
||||
|
||||
// 计算统计数据
|
||||
calculateStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户库存列表失败:', error)
|
||||
ElMessage.error('获取用户库存列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算统计数据
|
||||
const calculateStats = () => {
|
||||
const warehouses = userWarehouses.value
|
||||
|
||||
// 去重计算用户数(根据 user_id)
|
||||
const uniqueUsers = new Set(warehouses.map(w => w.user_id))
|
||||
totalUsers.value = uniqueUsers.size
|
||||
|
||||
// 计算总数量(只统计库存中的)
|
||||
const stockItems = warehouses.filter(w => w.status === 1)
|
||||
totalQuantity.value = stockItems.reduce((sum, w) => sum + (w.total_quantity || 0), 0)
|
||||
|
||||
// 计算总价值(只统计库存中的)
|
||||
totalValue.value = stockItems.reduce((sum, w) => sum + (w.total_value || 0), 0)
|
||||
|
||||
// 计算已出售数量
|
||||
soldCount.value = warehouses
|
||||
.filter(w => w.status === 0)
|
||||
.reduce((sum, w) => sum + (w.total_quantity || 0), 0)
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
search: '',
|
||||
product_type: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getUserWarehouses()
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
getUserWarehouses()
|
||||
ElMessage.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 查看用户详情
|
||||
const viewUserDetail = async (warehouse) => {
|
||||
currentUser.value = {
|
||||
user_id: warehouse.user_id,
|
||||
customer_name: warehouse.customer_name,
|
||||
real_name: warehouse.real_name,
|
||||
phone: warehouse.phone
|
||||
}
|
||||
|
||||
// 获取该用户的所有库存(不区分商品类型和状态)
|
||||
try {
|
||||
const params = {
|
||||
search: warehouse.phone,
|
||||
page: 1,
|
||||
page_size: 1000
|
||||
}
|
||||
// 清除筛选条件,获取该用户的所有库存
|
||||
const searchKey = searchForm.search
|
||||
const productType = searchForm.product_type
|
||||
const status = searchForm.status
|
||||
|
||||
searchForm.search = warehouse.phone
|
||||
searchForm.product_type = ''
|
||||
searchForm.status = ''
|
||||
|
||||
const response = await axios.get('/admin/warehouse/users', {
|
||||
params: {
|
||||
search: warehouse.phone,
|
||||
page: 1,
|
||||
page_size: 1000
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
currentUserWarehouses.value = response.data.data.list || []
|
||||
}
|
||||
|
||||
// 恢复原有的搜索条件
|
||||
searchForm.search = searchKey
|
||||
searchForm.product_type = productType
|
||||
searchForm.status = status
|
||||
|
||||
detailVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取用户库存详情失败:', error)
|
||||
ElMessage.error('获取用户库存详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getUserWarehouses()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-warehouses-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.low-stock {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-cards .el-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,950 @@
|
||||
<template>
|
||||
<div class="users-page">
|
||||
<div class="page-header">
|
||||
<h2>用户管理</h2>
|
||||
<el-button v-if="authStore.isAdmin" type="primary" @click="showCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="手机号">
|
||||
<el-input
|
||||
v-model="searchForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="真实姓名">
|
||||
<el-input
|
||||
v-model="searchForm.real_name"
|
||||
placeholder="请输入真实姓名"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户角色">
|
||||
<el-select
|
||||
v-model="searchForm.system_role"
|
||||
placeholder="请选择角色"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<!-- <el-option label="管理员" value="0" />-->
|
||||
<el-option label="代理商" value="1" />
|
||||
<el-option label="普通用户" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身份码">
|
||||
<el-input
|
||||
v-model="searchForm.identity_code"
|
||||
placeholder="请输入身份码"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getUsers">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="users"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||
|
||||
<el-table-column prop="phone" label="手机号" width="140" />
|
||||
|
||||
<el-table-column prop="customer_name" label="客户姓名" width="120" />
|
||||
|
||||
<el-table-column prop="real_name" label="真实姓名" width="120" />
|
||||
|
||||
<el-table-column prop="system_role" label="角色" width="100" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getRoleTagType(row.system_role)"
|
||||
size="small"
|
||||
>
|
||||
{{ getRoleName(row.system_role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="current_points" label="当前积分" width="100" sortable="custom" />
|
||||
|
||||
<el-table-column prop="identity_code" label="身份码" width="120" />
|
||||
|
||||
<el-table-column prop="company_name" label="公司名称" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="referrer_identity_code" label="推荐人身份码" width="140" />
|
||||
|
||||
<el-table-column prop="status" label="状态" width="80" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 1 ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="expiry_date" label="过期时间" width="160" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.expiry_date" :class="{ 'expired-text': isExpired(row.expiry_date) }">
|
||||
{{ formatDate(row.expiry_date) }}
|
||||
</span>
|
||||
<el-tag v-else type="success" size="small">永不过期</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showUserDetail(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="authStore.isAdmin"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="showEditDialog(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="authStore.isAdmin"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="showPaymentDialog(row)"
|
||||
>
|
||||
付款码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="authStore.isAdmin && row.system_role !== 0"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="showExpiryDialog(row)"
|
||||
>
|
||||
设置过期
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="authStore.isAdmin && row.system_role !== 0"
|
||||
type="text"
|
||||
size="small"
|
||||
class="danger-text"
|
||||
@click="deleteUser(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getUsers"
|
||||
@current-change="getUsers"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogMode === 'create' ? '添加用户' : '编辑用户'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="userFormRef"
|
||||
:model="userForm"
|
||||
:rules="userRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="userForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
:disabled="dialogMode === 'edit'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="userForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户角色" prop="system_role">
|
||||
<el-select v-model="userForm.system_role" placeholder="请选择角色">
|
||||
<!-- <el-option label="管理员" :value="0" />-->
|
||||
<el-option label="代理商" :value="1" />
|
||||
<el-option label="普通用户" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户姓名" prop="customer_name">
|
||||
<el-input v-model="userForm.customer_name" placeholder="请输入客户姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="真实姓名" prop="real_name">
|
||||
<el-input v-model="userForm.real_name" placeholder="请输入真实姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公司名称">
|
||||
<el-input v-model="userForm.company_name" placeholder="请输入公司名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="推荐人身份码">
|
||||
<el-input v-model="userForm.referrer_identity_code" placeholder="请输入推荐人身份码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="个人介绍">
|
||||
<el-input
|
||||
v-model="userForm.personal_intro"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入个人介绍"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="userForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitUser">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="用户详情"
|
||||
width="600px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="用户ID">{{ currentUser.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentUser.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户角色">
|
||||
<el-tag :type="getRoleTagType(currentUser.system_role)">
|
||||
{{ getRoleName(currentUser.system_role) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentUser.status === 1 ? 'success' : 'danger'">
|
||||
{{ currentUser.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户姓名">{{ currentUser.customer_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实姓名">{{ currentUser.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前积分">{{ currentUser.current_points }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份码">{{ currentUser.identity_code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="公司名称" :span="2">{{ currentUser.company_name || '未填写' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="推荐人身份码" :span="2">{{ currentUser.referrer_identity_code || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="个人介绍" :span="2">{{ currentUser.personal_intro || '未填写' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间" :span="2">
|
||||
<span v-if="currentUser.expiry_date" :class="{ 'expired-text': isExpired(currentUser.expiry_date) }">
|
||||
{{ formatDate(currentUser.expiry_date) }}
|
||||
</span>
|
||||
<el-tag v-else type="success" size="small">永不过期</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="营业执照" :span="2">
|
||||
|
||||
<el-image
|
||||
:src="currentUser.business_license_image"
|
||||
fit="cover"
|
||||
:preview-src-list="[currentUser.business_license_image]"
|
||||
style="width: 248px"
|
||||
/>
|
||||
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证正面" :span="2">
|
||||
|
||||
<el-image
|
||||
:src="currentUser.id_card_front_image"
|
||||
:preview-src-list="[currentUser.id_card_front_image]"
|
||||
fit="cover"
|
||||
style="width: 248px"
|
||||
/>
|
||||
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证反面" :span="2">
|
||||
|
||||
<el-image
|
||||
:src="currentUser.id_card_back_image"
|
||||
:preview-src-list="[currentUser.id_card_back_image]"
|
||||
fit="cover"
|
||||
style="width: 248px"
|
||||
/>
|
||||
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ formatDate(currentUser.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ formatDate(currentUser.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 设置用户过期时间对话框 -->
|
||||
<el-dialog
|
||||
v-model="expiryVisible"
|
||||
title="设置用户过期时间"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="expiryFormRef"
|
||||
:model="expiryForm"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="用户">
|
||||
<span>{{ expiryUser.customer_name || expiryUser.real_name || expiryUser.phone }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="过期设置">
|
||||
<el-radio-group v-model="expiryForm.type">
|
||||
<el-radio label="never">永不过期</el-radio>
|
||||
<el-radio label="custom">设置过期时间</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="expiryForm.type === 'custom'" label="过期时间">
|
||||
<el-date-picker
|
||||
v-model="expiryForm.expiryDate"
|
||||
type="datetime"
|
||||
placeholder="选择过期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||||
:disabled-date="disabledDate"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="expiryVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="expirySubmitting" @click="submitExpiry">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户付款码管理对话框 -->
|
||||
<el-dialog
|
||||
v-model="paymentVisible"
|
||||
title="用户付款码管理"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="payment-info">
|
||||
<p><strong>用户:</strong>{{ paymentUser.customer_name || paymentUser.real_name || paymentUser.phone }}</p>
|
||||
<p><strong>手机号:</strong>{{ paymentUser.phone }}</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="paymentFormRef"
|
||||
:model="paymentForm"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="主收款码">
|
||||
<ImageUpload
|
||||
v-model="paymentForm.main_payment_qr_image"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
accept="image/*"
|
||||
list-type="picture-card"
|
||||
:preview-src-list="paymentForm.main_payment_qr_image ? [getFullImageUrl(paymentForm.main_payment_qr_image)] : []"
|
||||
/>
|
||||
<div class="upload-tip">支持jpg、png格式,建议尺寸200x200px</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="副收款码">
|
||||
<ImageUpload
|
||||
v-model="paymentForm.sub_payment_qr_image"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
accept="image/*"
|
||||
list-type="picture-card"
|
||||
:preview-src-list="paymentForm.sub_payment_qr_image ? [getFullImageUrl(paymentForm.sub_payment_qr_image)] : []"
|
||||
/>
|
||||
<div class="upload-tip">支持jpg、png格式,建议尺寸200x200px</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="paymentVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="paymentSubmitting" @click="updatePaymentInfo">
|
||||
保存
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const users = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const expiryVisible = ref(false)
|
||||
const paymentVisible = ref(false)
|
||||
const dialogMode = ref('create')
|
||||
const submitting = ref(false)
|
||||
const expirySubmitting = ref(false)
|
||||
const paymentSubmitting = ref(false)
|
||||
const userFormRef = ref()
|
||||
const expiryFormRef = ref()
|
||||
const paymentFormRef = ref()
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
phone: '',
|
||||
real_name: '',
|
||||
system_role: '',
|
||||
identity_code: ''
|
||||
})
|
||||
|
||||
// 用户表单
|
||||
const userForm = reactive({
|
||||
phone: '',
|
||||
password: '',
|
||||
system_role: 2,
|
||||
customer_name: '',
|
||||
real_name: '',
|
||||
company_name: '',
|
||||
referrer_identity_code: '',
|
||||
personal_intro: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 当前查看的用户
|
||||
const currentUser = ref({})
|
||||
|
||||
// 当前设置过期时间的用户
|
||||
const expiryUser = ref({})
|
||||
|
||||
// 过期时间表单
|
||||
const expiryForm = reactive({
|
||||
type: 'never',
|
||||
expiryDate: null
|
||||
})
|
||||
|
||||
// 当前编辑付款码的用户
|
||||
const paymentUser = ref({})
|
||||
|
||||
// 付款码表单
|
||||
const paymentForm = reactive({
|
||||
main_payment_qr_image: '',
|
||||
sub_payment_qr_image: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const userRules = {
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
|
||||
system_role: [
|
||||
{ required: true, message: '请选择用户角色', trigger: 'change' }
|
||||
],
|
||||
customer_name: [
|
||||
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const getUsers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/users', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
users.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
phone: '',
|
||||
real_name: '',
|
||||
system_role: '',
|
||||
identity_code: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getUsers()
|
||||
}
|
||||
|
||||
// 处理排序
|
||||
const handleSortChange = ({ prop, order }) => {
|
||||
if (!prop) return
|
||||
|
||||
// 复制用户数组进行排序
|
||||
const sortedUsers = [...users.value]
|
||||
|
||||
sortedUsers.sort((a, b) => {
|
||||
let aVal = a[prop]
|
||||
let bVal = b[prop]
|
||||
|
||||
// 处理不同类型的字段排序
|
||||
switch (prop) {
|
||||
case 'system_role':
|
||||
// 角色排序:管理员(0) < 代理商(1) < 普通用户(2)
|
||||
aVal = aVal || 999
|
||||
bVal = bVal || 999
|
||||
break
|
||||
|
||||
case 'status':
|
||||
// 状态排序:禁用(0) < 启用(1)
|
||||
aVal = aVal || 0
|
||||
bVal = bVal || 0
|
||||
break
|
||||
|
||||
case 'expiry_date':
|
||||
// 过期时间排序:永不过期(null) < 已过期 < 未过期
|
||||
if (!aVal && !bVal) {
|
||||
aVal = bVal = 0 // 都是永不过期,相等
|
||||
} else if (!aVal) {
|
||||
aVal = -1 // 永不过期排在前面
|
||||
} else if (!bVal) {
|
||||
bVal = -1
|
||||
} else {
|
||||
const aExpired = isExpired(aVal)
|
||||
const bExpired = isExpired(bVal)
|
||||
if (aExpired && !bExpired) {
|
||||
aVal = 1 // 已过期排后面
|
||||
bVal = 0 // 未过期排前面
|
||||
} else if (!aExpired && bExpired) {
|
||||
aVal = 0
|
||||
bVal = 1
|
||||
} else {
|
||||
// 都是已过期或都是未过期,按时间排序
|
||||
aVal = new Date(aVal).getTime()
|
||||
bVal = new Date(bVal).getTime()
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'current_points':
|
||||
// 积分排序:null转为0
|
||||
aVal = aVal || 0
|
||||
bVal = bVal || 0
|
||||
break
|
||||
|
||||
default:
|
||||
// 默认排序处理
|
||||
if (aVal == null) aVal = ''
|
||||
if (bVal == null) bVal = ''
|
||||
break
|
||||
}
|
||||
|
||||
// 比较值
|
||||
if (aVal < bVal) {
|
||||
return order === 'ascending' ? -1 : 1
|
||||
} else if (aVal > bVal) {
|
||||
return order === 'ascending' ? 1 : -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户列表
|
||||
users.value = sortedUsers
|
||||
|
||||
// 提示用户
|
||||
if (order) {
|
||||
const orderText = order === 'ascending' ? '升序' : '降序'
|
||||
console.log(`已按${prop}进行${orderText}排序`)
|
||||
} else {
|
||||
console.log('已取消排序')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色名称
|
||||
const getRoleName = (role) => {
|
||||
const roleMap = { 0: '管理员', 1: '代理商', 2: '普通用户' }
|
||||
return roleMap[role] || '未知'
|
||||
}
|
||||
|
||||
// 获取角色标签类型
|
||||
const getRoleTagType = (role) => {
|
||||
const typeMap = { 0: '', 1: 'warning', 2: 'info' }
|
||||
return typeMap[role] || 'info'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 判断是否已过期
|
||||
const isExpired = (expiryDate) => {
|
||||
if (!expiryDate) return false
|
||||
return dayjs(expiryDate).isBefore(dayjs())
|
||||
}
|
||||
|
||||
// 禁用过去的日期
|
||||
const disabledDate = (time) => {
|
||||
return time.getTime() < Date.now() - 8.64e7 // 禁用昨天之前的日期
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogMode.value = 'create'
|
||||
resetUserForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (user) => {
|
||||
dialogMode.value = 'edit'
|
||||
Object.assign(userForm, {
|
||||
...user,
|
||||
password: '' // 编辑时不显示密码
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示用户详情
|
||||
const showUserDetail = (user) => {
|
||||
currentUser.value = user
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 显示设置过期时间对话框
|
||||
const showExpiryDialog = (user) => {
|
||||
expiryUser.value = user
|
||||
expiryForm.type = user.expiry_date ? 'custom' : 'never'
|
||||
expiryForm.expiryDate = user.expiry_date ? dayjs(user.expiry_date).format('YYYY-MM-DDTHH:mm:ssZ') : null
|
||||
expiryVisible.value = true
|
||||
}
|
||||
|
||||
// 重置用户表单
|
||||
const resetUserForm = () => {
|
||||
Object.assign(userForm, {
|
||||
phone: '',
|
||||
password: '',
|
||||
system_role: 2,
|
||||
customer_name: '',
|
||||
real_name: '',
|
||||
company_name: '',
|
||||
referrer_identity_code: '',
|
||||
personal_intro: '',
|
||||
status: 1
|
||||
})
|
||||
if (userFormRef.value) {
|
||||
userFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交用户表单
|
||||
const submitUser = async () => {
|
||||
if (!userFormRef.value) return
|
||||
|
||||
const valid = await userFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
if (dialogMode.value === 'create') {
|
||||
Object.assign(userForm, {
|
||||
id: null,
|
||||
status: 0
|
||||
})
|
||||
await axios.post('/admin/users', userForm)
|
||||
ElMessage.success('用户创建成功')
|
||||
} else {
|
||||
const { id, ...updateData } = userForm
|
||||
if (!updateData.password) {
|
||||
delete updateData.password
|
||||
}
|
||||
await axios.put(`/admin/users/${id}`, updateData)
|
||||
ElMessage.success('用户更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getUsers()
|
||||
} catch (error) {
|
||||
console.error('提交用户失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交过期时间设置
|
||||
const submitExpiry = async () => {
|
||||
expirySubmitting.value = true
|
||||
|
||||
try {
|
||||
const expiryDate = expiryForm.type === 'never' ? null : expiryForm.expiryDate
|
||||
|
||||
await axios.put(`/admin/users/${expiryUser.value.id}/expiry`, {
|
||||
expiry_date: expiryDate
|
||||
})
|
||||
|
||||
ElMessage.success('用户过期时间设置成功')
|
||||
expiryVisible.value = false
|
||||
getUsers() // 刷新列表
|
||||
} catch (error) {
|
||||
console.error('设置过期时间失败:', error)
|
||||
ElMessage.error('设置过期时间失败')
|
||||
} finally {
|
||||
expirySubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示付款码管理对话框
|
||||
const showPaymentDialog = async (user) => {
|
||||
paymentUser.value = user
|
||||
|
||||
// 获取用户当前的付款信息(直接从用户信息中获取)
|
||||
Object.assign(paymentForm, {
|
||||
main_payment_qr_image: user.main_payment_qr_image || '',
|
||||
sub_payment_qr_image: user.sub_payment_qr_image || ''
|
||||
})
|
||||
|
||||
paymentVisible.value = true
|
||||
}
|
||||
|
||||
// 重置付款表单
|
||||
const resetPaymentForm = () => {
|
||||
Object.assign(paymentForm, {
|
||||
main_payment_qr_image: '',
|
||||
sub_payment_qr_image: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户付款信息
|
||||
const updatePaymentInfo = async () => {
|
||||
paymentSubmitting.value = true
|
||||
try {
|
||||
paymentUser.value.main_payment_qr_image= paymentForm.main_payment_qr_image
|
||||
paymentUser.value.sub_payment_qr_image= paymentForm.sub_payment_qr_image
|
||||
const { data } = await axios.put(`/admin/users/${paymentUser.value.id}`, paymentUser.value)
|
||||
|
||||
if (data.success) {
|
||||
ElMessage.success('用户付款信息更新成功')
|
||||
paymentVisible.value = false
|
||||
getUsers() // 刷新用户列表
|
||||
} else {
|
||||
ElMessage.error(data.message || '更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用户付款信息失败:', error)
|
||||
ElMessage.error('更新失败')
|
||||
} finally {
|
||||
paymentSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取完整图片URL
|
||||
const getFullImageUrl = (url) => {
|
||||
return url
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (user) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户 "${user.customer_name || user.phone}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await axios.delete(`/admin/users/${user.id}`)
|
||||
ElMessage.success('用户删除成功')
|
||||
getUsers()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.danger-text:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.expired-text {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
background-color: #f5f7fa;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.payment-info p {
|
||||
margin: 0 0 8px 0;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.payment-info p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,788 @@
|
||||
<template>
|
||||
<div class="purchase-orders-page">
|
||||
<div class="page-header">
|
||||
<h2>买单管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="订单编号">
|
||||
<el-input
|
||||
v-model="searchForm.order_no"
|
||||
placeholder="请输入订单编号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="订单状态">
|
||||
<el-select
|
||||
v-model="searchForm.order_status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="待付款" value="0" />
|
||||
<el-option label="待确认" value="1" />
|
||||
<el-option label="已完成" value="2" />
|
||||
<el-option label="已取消" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商品类型">
|
||||
<el-select
|
||||
v-model="searchForm.product_type"
|
||||
placeholder="请选择类型"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="一级商品" value="1" />
|
||||
<el-option label="二级商品" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="卖家名字">
|
||||
<el-input
|
||||
v-model="searchForm.seller_name"
|
||||
placeholder="请输入卖家名字"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="买家名字">
|
||||
<el-input
|
||||
v-model="searchForm.buyer_name"
|
||||
placeholder="请输入买家名字"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getOrders">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="orders"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column prop="order_no" label="订单编号" width="180" />
|
||||
|
||||
<el-table-column label="买家信息" width="140">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.buyer?.real_name || row.buyer?.phone }}</div>
|
||||
<div class="text-xs text-gray-500">{{ row.buyer?.phone }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="卖家信息" width="140">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.seller">
|
||||
{{ row.seller?.real_name || row.seller?.phone }}
|
||||
<div class="text-xs text-gray-500">{{ row.seller?.phone }}</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">平台商品</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="product_type" label="商品类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.product_type === 1 ? 'primary' : 'warning'" size="small">
|
||||
{{ row.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="unit_price" label="单价" width="100">
|
||||
<template #default="{ row }">
|
||||
¥{{ formatAmount(row.unit_price) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
|
||||
<el-table-column prop="total_amount" label="总金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-red-500">¥{{ formatAmount(row.total_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="order_status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.order_status)" size="small">
|
||||
{{ getStatusName(row.order_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showOrderDetail(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="showMessages(row)">
|
||||
留言
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="authStore.isAdmin && row.order_status === 1"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="showStatusDialog(row)"
|
||||
>
|
||||
处理
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getOrders"
|
||||
@current-change="getOrders"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="买单详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="订单编号" :span="2">{{ currentOrder.order_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="买家姓名">{{ currentOrder.buyer?.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="买家手机">{{ currentOrder.buyer?.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家姓名">{{ currentOrder.seller?.real_name || '平台' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家手机">{{ currentOrder.seller?.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品名称" :span="2">{{ currentOrder.product_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品类型">
|
||||
<el-tag :type="currentOrder.product_type === 1 ? 'primary' : 'warning'">
|
||||
{{ currentOrder.product_type === 1 ? '一级商品' : '二级商品' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态">
|
||||
<el-tag :type="getStatusTagType(currentOrder.order_status)">
|
||||
{{ getStatusName(currentOrder.order_status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="单价">¥{{ formatAmount(currentOrder.unit_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ currentOrder.quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总金额" :span="2">
|
||||
<span class="text-red-500 font-semibold text-lg">¥{{ formatAmount(currentOrder.total_amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址" :span="2">
|
||||
<div v-if="currentOrder.address">
|
||||
{{ currentOrder.address.consignee_name }} {{ currentOrder.address.consignee_phone }}<br>
|
||||
{{ currentOrder.address.province }}{{ currentOrder.address.city }}{{ currentOrder.address.district }}{{ currentOrder.address.detail_address }}
|
||||
</div>
|
||||
<span v-else>暂无</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ currentOrder.remark || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="确认时间">{{ formatDate(currentOrder.confirm_time) || '未确认' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<div v-if="currentOrder.product_images" class="mt-4">
|
||||
<h4>商品图片</h4>
|
||||
<div class="image-gallery">
|
||||
<el-image
|
||||
v-for="(image, index) in getProductImages(currentOrder.product_images)"
|
||||
:key="index"
|
||||
:src="image"
|
||||
:preview-src-list="getProductImages(currentOrder.product_images)"
|
||||
fit="cover"
|
||||
class="product-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h4>付款凭证</h4>
|
||||
<div v-if="currentOrder.payment_proof_image" class="image-gallery">
|
||||
<el-image
|
||||
|
||||
:src="currentOrder.payment_proof_image"
|
||||
alt="无"
|
||||
fit="cover"
|
||||
:preview-src-list="[currentOrder.payment_proof_image]"
|
||||
class="product-image"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
无
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 订单状态处理对话框 -->
|
||||
<el-dialog
|
||||
v-model="statusVisible"
|
||||
title="处理订单"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="statusFormRef"
|
||||
:model="statusForm"
|
||||
:rules="statusRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="处理结果" prop="order_status">
|
||||
<el-radio-group v-model="statusForm.order_status">
|
||||
<el-radio :label="2">确认完成</el-radio>
|
||||
<el-radio :label="3">取消订单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="处理备注" prop="remark">
|
||||
<el-input
|
||||
v-model="statusForm.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入处理备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="statusVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitStatus">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 订单留言对话框 -->
|
||||
<el-dialog
|
||||
v-model="messagesVisible"
|
||||
:title="`订单留言 - ${currentOrder.order_no}`"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 留言列表 -->
|
||||
<div class="messages-container">
|
||||
<div class="messages-list">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
:class="{ 'admin-message': message.user?.system_role === 0 || message.user?.system_role === 1 }"
|
||||
>
|
||||
<div class="message-header">
|
||||
<span class="user-name">
|
||||
{{ message.user?.real_name || message.user?.customer_name }}
|
||||
<el-tag
|
||||
v-if="message.user?.system_role === 0"
|
||||
type="danger"
|
||||
size="small"
|
||||
>
|
||||
管理员
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="message.user?.system_role === 1"
|
||||
type="warning"
|
||||
size="small"
|
||||
>
|
||||
代理商
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="message-time">{{ formatDate(message.created_at) }}</span>
|
||||
</div>
|
||||
<div class="message-content">{{ message.content }}</div>
|
||||
<!-- 留言图片 -->
|
||||
<div v-if="message.images" class="message-images">
|
||||
<el-image
|
||||
v-for="(image, index) in getMessageImages(message.images)"
|
||||
:key="index"
|
||||
:src="(image)"
|
||||
:preview-src-list="getMessageImages(message.images)"
|
||||
:initial-index="index"
|
||||
class="message-image"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="messages.length === 0" class="empty-messages">
|
||||
暂无留言
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送留言 -->
|
||||
<div class="send-message">
|
||||
<el-form :model="messageForm" @submit.prevent="sendMessage">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="messageForm.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入留言内容(最多500字)"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="sendingMessage"
|
||||
@click="sendMessage"
|
||||
:disabled="!messageForm.content.trim()"
|
||||
>
|
||||
发送留言
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="messagesVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const orders = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const statusVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const statusFormRef = ref()
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
order_no: '',
|
||||
order_status: '',
|
||||
product_type: '',
|
||||
seller_name: '',
|
||||
buyer_name: ''
|
||||
})
|
||||
|
||||
// 当前查看的订单
|
||||
const currentOrder = ref({})
|
||||
|
||||
// 状态处理表单
|
||||
const statusForm = reactive({
|
||||
order_status: 2,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 状态处理验证规则
|
||||
const statusRules = {
|
||||
order_status: [
|
||||
{ required: true, message: '请选择处理结果', trigger: 'change' }
|
||||
],
|
||||
remark: [
|
||||
{ required: true, message: '请输入处理备注', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 留言相关数据
|
||||
const messagesVisible = ref(false)
|
||||
const messages = ref([])
|
||||
const sendingMessage = ref(false)
|
||||
const messageForm = reactive({
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 获取订单列表
|
||||
const getOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/orders/purchase', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
orders.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取买单列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
order_no: '',
|
||||
order_status: '',
|
||||
product_type: '',
|
||||
seller_name: '',
|
||||
buyer_name: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getOrders()
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = { 0: '待付款', 1: '待确认', 2: '已完成', 3: '已取消' }
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status) => {
|
||||
const typeMap = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'danger' }
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0.00'
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 获取留言图片列表
|
||||
const getMessageImages = (images) => {
|
||||
if (!images) return []
|
||||
return images.split(',').filter(img => img.trim())
|
||||
}
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imageUrl) => {
|
||||
if (!imageUrl) return ''
|
||||
if (imageUrl.startsWith('http')) return imageUrl
|
||||
return `${axios.defaults.baseURL.replace('/back', '')}/file/${imageUrl}`
|
||||
}
|
||||
|
||||
// 获取商品图片列表
|
||||
const getProductImages = (images) => {
|
||||
if (!images) return []
|
||||
return images.split(',').filter(img => img.trim())
|
||||
}
|
||||
|
||||
// 显示订单详情
|
||||
const showOrderDetail = (order) => {
|
||||
currentOrder.value = order
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 显示状态处理对话框
|
||||
const showStatusDialog = (order) => {
|
||||
currentOrder.value = order
|
||||
statusForm.order_status = 2
|
||||
statusForm.remark = ''
|
||||
statusVisible.value = true
|
||||
}
|
||||
|
||||
// 提交状态处理
|
||||
const submitStatus = async () => {
|
||||
if (!statusFormRef.value) return
|
||||
|
||||
const valid = await statusFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
await axios.put(`/admin/orders/purchase/${currentOrder.value.id}/status`, statusForm)
|
||||
ElMessage.success('订单处理成功')
|
||||
statusVisible.value = false
|
||||
getOrders()
|
||||
} catch (error) {
|
||||
console.error('处理订单失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示留言对话框
|
||||
const showMessages = async (order) => {
|
||||
currentOrder.value = order
|
||||
messagesVisible.value = true
|
||||
await getMessages()
|
||||
}
|
||||
|
||||
// 获取订单留言
|
||||
const getMessages = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||
params: {
|
||||
type: 'purchase'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
messages.value = response.data.data.list || []
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '获取留言失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取留言失败:', error)
|
||||
ElMessage.error('获取留言失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 发送留言
|
||||
const sendMessage = async () => {
|
||||
if (!messageForm.content.trim()) {
|
||||
ElMessage.warning('请输入留言内容')
|
||||
return
|
||||
}
|
||||
|
||||
sendingMessage.value = true
|
||||
try {
|
||||
const response = await axios.post(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||
content: messageForm.content.trim()
|
||||
}, {
|
||||
params: {
|
||||
type: 'purchase'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('留言发送成功')
|
||||
messageForm.content = ''
|
||||
await getMessages() // 刷新留言列表
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '发送留言失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送留言失败:', error)
|
||||
ElMessage.error('发送留言失败')
|
||||
} finally {
|
||||
sendingMessage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getOrders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.purchase-orders-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 留言相关样式 */
|
||||
.messages-container {
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
flex: 1;
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.message-item.admin-message {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.send-message {
|
||||
border-top: 1px solid #e4e7ed;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* 留言图片样式 */
|
||||
.message-images {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<div class="sales-orders-page">
|
||||
<div class="page-header">
|
||||
<h2>卖单管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="订单编号">
|
||||
<el-input
|
||||
v-model="searchForm.order_no"
|
||||
placeholder="请输入订单编号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="订单状态">
|
||||
<el-select
|
||||
v-model="searchForm.order_status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="待付款" value="0" />
|
||||
<el-option label="待发货" value="1" />
|
||||
<el-option label="确认完成" value="2" />
|
||||
<el-option label="已取消" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="卖家名字">
|
||||
<el-input
|
||||
v-model="searchForm.seller_name"
|
||||
placeholder="请输入卖家名字"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="买家名字">
|
||||
<el-input
|
||||
v-model="searchForm.buyer_name"
|
||||
placeholder="请输入买家名字"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getOrders">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="orders"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column prop="order_no" label="订单编号" width="180" />
|
||||
|
||||
<el-table-column label="卖家信息" width="140">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.seller?.real_name || row.seller?.phone }}</div>
|
||||
<div class="text-xs text-gray-500">{{ row.seller?.phone }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="买家信息" width="140">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.buyer?.real_name || row.buyer?.phone }}</div>
|
||||
<div class="text-xs text-gray-500">{{ row.buyer?.phone }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="selling_price" label="售价" width="100">
|
||||
<template #default="{ row }">
|
||||
¥{{ formatAmount(row.selling_price) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
|
||||
<el-table-column prop="total_amount" label="总金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-red-500">¥{{ formatAmount(row.total_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="order_status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.order_status)" size="small">
|
||||
{{ getStatusName(row.order_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showOrderDetail(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="showMessages(row)">
|
||||
留言
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="authStore.isAdmin && row.order_status === 1"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="showStatusDialog(row)"
|
||||
>
|
||||
处理
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getOrders"
|
||||
@current-change="getOrders"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="卖单详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="订单编号" :span="2">{{ currentOrder.order_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家姓名">{{ currentOrder.seller?.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家手机">{{ currentOrder.seller?.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="买家姓名">{{ currentOrder.buyer?.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="买家手机">{{ currentOrder.buyer?.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品名称" :span="2">{{ currentOrder.product_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单状态" :span="2">
|
||||
<el-tag :type="getStatusTagType(currentOrder.order_status)">
|
||||
{{ getStatusName(currentOrder.order_status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="售价">¥{{ formatAmount(currentOrder.selling_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ currentOrder.quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总金额" :span="2">
|
||||
<span class="text-red-500 font-semibold text-lg">¥{{ formatAmount(currentOrder.total_amount) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址" :span="2">
|
||||
<div v-if="currentOrder.address">
|
||||
{{ currentOrder.address.consignee_name }} {{ currentOrder.address.consignee_phone }}<br>
|
||||
{{ currentOrder.address.province }}{{ currentOrder.address.city }}{{ currentOrder.address.district }}{{ currentOrder.address.detail_address }}
|
||||
</div>
|
||||
<span v-else>暂无</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ currentOrder.remark || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentOrder.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成时间">{{ formatDate(currentOrder.complete_time) || '未完成' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 商品信息 -->
|
||||
<div v-if="currentOrder.secondary_product" class="mt-4">
|
||||
<h4>关联商品信息</h4>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="商品ID">{{ currentOrder.secondary_product.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成本价">¥{{ formatAmount(currentOrder.secondary_product.cost_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品描述" :span="2">{{ currentOrder.secondary_product.product_description || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<div v-if="currentOrder.product_images" class="mt-4">
|
||||
<h4>商品图片</h4>
|
||||
<div class="image-gallery">
|
||||
<el-image
|
||||
v-for="(image, index) in getProductImages(currentOrder.product_images)"
|
||||
:key="index"
|
||||
:src="image"
|
||||
:preview-src-list="getProductImages(currentOrder.product_images)"
|
||||
fit="cover"
|
||||
class="product-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4>付款凭证</h4>
|
||||
<div v-if="currentOrder.payment_proof_image" class="image-gallery">
|
||||
<el-image
|
||||
:preview-src-list="[currentOrder.payment_proof_image]"
|
||||
:src="currentOrder.payment_proof_image"
|
||||
alt="无"
|
||||
fit="cover"
|
||||
class="product-image"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
无
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 订单状态处理对话框 -->
|
||||
<el-dialog
|
||||
v-model="statusVisible"
|
||||
title="处理订单"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="statusFormRef"
|
||||
:model="statusForm"
|
||||
:rules="statusRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="处理结果" prop="order_status">
|
||||
<el-radio-group v-model="statusForm.order_status">
|
||||
<el-radio :label="2">确认完成</el-radio>
|
||||
<el-radio :label="3">取消订单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="处理备注" prop="remark">
|
||||
<el-input
|
||||
v-model="statusForm.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入处理备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="statusVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitStatus">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 订单留言对话框 -->
|
||||
<el-dialog
|
||||
v-model="messagesVisible"
|
||||
:title="`订单留言 - ${currentOrder.order_no}`"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 留言列表 -->
|
||||
<div class="messages-container">
|
||||
<div class="messages-list">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
:class="{ 'admin-message': message.user?.system_role === 0 || message.user?.system_role === 1 }"
|
||||
>
|
||||
<div class="message-header">
|
||||
<span class="user-name">
|
||||
{{ message.user?.real_name || message.user?.customer_name }}
|
||||
<el-tag
|
||||
v-if="message.user?.system_role === 0"
|
||||
type="danger"
|
||||
size="small"
|
||||
>
|
||||
管理员
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="message.user?.system_role === 1"
|
||||
type="warning"
|
||||
size="small"
|
||||
>
|
||||
代理商
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="message-time">{{ formatDate(message.created_at) }}</span>
|
||||
</div>
|
||||
<div class="message-content">{{ message.content }}</div>
|
||||
<!-- 留言图片 -->
|
||||
<div v-if="message.images" class="message-images">
|
||||
<el-image
|
||||
v-for="(image, index) in getMessageImages(message.images)"
|
||||
:key="index"
|
||||
:src="(image)"
|
||||
:preview-src-list="getMessageImages(message.images)"
|
||||
:initial-index="index"
|
||||
class="message-image"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="messages.length === 0" class="empty-messages">
|
||||
暂无留言
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送留言 -->
|
||||
<div class="send-message">
|
||||
<el-form :model="messageForm" @submit.prevent="sendMessage">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="messageForm.content"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入留言内容(最多500字)"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="sendingMessage"
|
||||
@click="sendMessage"
|
||||
:disabled="!messageForm.content.trim()"
|
||||
>
|
||||
发送留言
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="messagesVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const orders = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const statusVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const statusFormRef = ref()
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
order_no: '',
|
||||
order_status: '',
|
||||
seller_name: '',
|
||||
buyer_name: ''
|
||||
})
|
||||
|
||||
// 当前查看的订单
|
||||
const currentOrder = ref({})
|
||||
|
||||
// 状态处理表单
|
||||
const statusForm = reactive({
|
||||
order_status: 2,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 状态处理验证规则
|
||||
const statusRules = {
|
||||
order_status: [
|
||||
{ required: true, message: '请选择处理结果', trigger: 'change' }
|
||||
],
|
||||
remark: [
|
||||
{ required: true, message: '请输入处理备注', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 留言相关数据
|
||||
const messagesVisible = ref(false)
|
||||
const messages = ref([])
|
||||
const sendingMessage = ref(false)
|
||||
const messageForm = reactive({
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 获取订单列表
|
||||
const getOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/orders/sales', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
orders.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取卖单列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
order_no: '',
|
||||
order_status: '',
|
||||
seller_name: '',
|
||||
buyer_name: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getOrders()
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = { 0: '待付款', 1: '待发货', 2: '确认完成', 3: '已取消' }
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status) => {
|
||||
const typeMap = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'danger' }
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0.00'
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 获取留言图片列表
|
||||
const getMessageImages = (images) => {
|
||||
if (!images) return []
|
||||
return images.split(',').filter(img => img.trim())
|
||||
}
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imageUrl) => {
|
||||
if (!imageUrl) return ''
|
||||
if (imageUrl.startsWith('http')) return imageUrl
|
||||
return `${axios.defaults.baseURL.replace('/back', '')}/file/${imageUrl}`
|
||||
}
|
||||
|
||||
// 获取商品图片列表
|
||||
const getProductImages = (images) => {
|
||||
if (!images) return []
|
||||
return images.split(',').filter(img => img.trim())
|
||||
}
|
||||
|
||||
// 显示订单详情
|
||||
const showOrderDetail = (order) => {
|
||||
currentOrder.value = order
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 显示状态处理对话框
|
||||
const showStatusDialog = (order) => {
|
||||
currentOrder.value = order
|
||||
statusForm.order_status = 2
|
||||
statusForm.remark = ''
|
||||
statusVisible.value = true
|
||||
}
|
||||
|
||||
// 提交状态处理
|
||||
const submitStatus = async () => {
|
||||
if (!statusFormRef.value) return
|
||||
|
||||
const valid = await statusFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
await axios.put(`/admin/orders/sales/${currentOrder.value.id}/status`, statusForm)
|
||||
ElMessage.success('订单处理成功')
|
||||
statusVisible.value = false
|
||||
getOrders()
|
||||
} catch (error) {
|
||||
console.error('处理订单失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示留言对话框
|
||||
const showMessages = async (order) => {
|
||||
currentOrder.value = order
|
||||
messagesVisible.value = true
|
||||
await getMessages()
|
||||
}
|
||||
|
||||
// 获取订单留言
|
||||
const getMessages = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||
params: {
|
||||
type: 'sales'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
messages.value = response.data.data.list || []
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '获取留言失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取留言失败:', error)
|
||||
ElMessage.error('获取留言失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 发送留言
|
||||
const sendMessage = async () => {
|
||||
if (!messageForm.content.trim()) {
|
||||
ElMessage.warning('请输入留言内容')
|
||||
return
|
||||
}
|
||||
|
||||
sendingMessage.value = true
|
||||
try {
|
||||
const response = await axios.post(`/admin/orders/${currentOrder.value.id}/messages`, {
|
||||
content: messageForm.content.trim()
|
||||
}, {
|
||||
params: {
|
||||
type: 'sales'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('留言发送成功')
|
||||
messageForm.content = ''
|
||||
await getMessages() // 刷新留言列表
|
||||
} else {
|
||||
ElMessage.error(response.data.message || '发送留言失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送留言失败:', error)
|
||||
ElMessage.error('发送留言失败')
|
||||
} finally {
|
||||
sendingMessage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getOrders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sales-orders-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 留言相关样式 */
|
||||
.messages-container {
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
flex: 1;
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.message-item.admin-message {
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.send-message {
|
||||
border-top: 1px solid #e4e7ed;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* 留言图片样式 */
|
||||
.message-images {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div class="categories-page">
|
||||
<div class="page-header">
|
||||
<h2>商品分类管理</h2>
|
||||
<el-button type="primary" @click="showCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分类
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="categories"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column label="分类图标" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.category_icon"
|
||||
:src="getImageUrl(row.category_icon)"
|
||||
fit="cover"
|
||||
style="width: 50px; height: 50px; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else class="text-gray-400">无图标</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="category_name" label="分类名称" min-width="200" />
|
||||
|
||||
<el-table-column prop="sort_order" label="排序" width="100" />
|
||||
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="updateStatus(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="danger-text"
|
||||
@click="deleteCategory(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑分类对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogMode === 'create' ? '添加分类' : '编辑分类'"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="categoryFormRef"
|
||||
:model="categoryForm"
|
||||
:rules="categoryRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="category_name">
|
||||
<el-input
|
||||
v-model="categoryForm.category_name"
|
||||
placeholder="请输入分类名称"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类图标">
|
||||
<ImageUpload
|
||||
v-model="categoryForm.category_icon"
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序序号" prop="sort_order">
|
||||
<el-input-number
|
||||
v-model="categoryForm.sort_order"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="categoryForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitCategory">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const categories = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogMode = ref('create')
|
||||
const submitting = ref(false)
|
||||
const categoryFormRef = ref()
|
||||
|
||||
// 分类表单
|
||||
const categoryForm = reactive({
|
||||
category_name: '',
|
||||
category_icon: '',
|
||||
sort_order: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const categoryRules = {
|
||||
category_name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 50, message: '分类名称不能超过50个字符', trigger: 'blur' }
|
||||
],
|
||||
sort_order: [
|
||||
{ required: true, message: '请输入排序序号', trigger: 'blur' },
|
||||
{ type: 'number', message: '排序序号必须为数字' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取分类列表
|
||||
const getCategories = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/admin/products/categories')
|
||||
|
||||
if (response.data.success) {
|
||||
categories.value = response.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${window.location.origin}${url}`
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogMode.value = 'create'
|
||||
resetCategoryForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (category) => {
|
||||
dialogMode.value = 'edit'
|
||||
Object.assign(categoryForm, {
|
||||
id: category.id,
|
||||
category_name: category.category_name,
|
||||
category_icon: category.category_icon || '',
|
||||
sort_order: category.sort_order,
|
||||
status: category.status
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 重置分类表单
|
||||
const resetCategoryForm = () => {
|
||||
Object.assign(categoryForm, {
|
||||
category_name: '',
|
||||
category_icon: '',
|
||||
sort_order: 0,
|
||||
status: 1
|
||||
})
|
||||
if (categoryFormRef.value) {
|
||||
categoryFormRef.value.resetFields()
|
||||
}
|
||||
// 确保图片上传组件也被重置
|
||||
setTimeout(() => {
|
||||
// 使用 nextTick 确保 DOM 更新完成
|
||||
categoryForm.category_icon = ''
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 提交分类表单
|
||||
const submitCategory = async () => {
|
||||
if (!categoryFormRef.value) return
|
||||
|
||||
const valid = await categoryFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
if (dialogMode.value === 'create') {
|
||||
await axios.post('/admin/products/categories', categoryForm)
|
||||
ElMessage.success('分类创建成功')
|
||||
} else {
|
||||
const { id, ...updateData } = categoryForm
|
||||
await axios.put(`/admin/products/categories/${id}`, updateData)
|
||||
ElMessage.success('分类更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
// 重置表单,确保下次添加时状态正确
|
||||
resetCategoryForm()
|
||||
getCategories()
|
||||
} catch (error) {
|
||||
console.error('提交分类失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = async (category) => {
|
||||
try {
|
||||
await axios.put(`/admin/products/categories/${category.id}`, {
|
||||
status: category.status
|
||||
})
|
||||
ElMessage.success('状态更新成功')
|
||||
} catch (error) {
|
||||
console.error('更新状态失败:', error)
|
||||
// 恢复原状态
|
||||
category.status = category.status === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
const deleteCategory = async (category) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除分类 "${category.category_name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await axios.delete(`/admin/products/categories/${category.id}`)
|
||||
ElMessage.success('分类删除成功')
|
||||
getCategories()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除分类失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getCategories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.categories-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.danger-text:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,747 @@
|
||||
<template>
|
||||
<div class="primary-products-page">
|
||||
<div class="page-header">
|
||||
<h2>一级商品管理</h2>
|
||||
<el-button type="primary" @click="showCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="商品分类">
|
||||
<el-select
|
||||
v-model="searchForm.category_id"
|
||||
placeholder="请选择分类"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:label="category.category_name"
|
||||
:value="category.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商品名称">
|
||||
<el-input
|
||||
v-model="searchForm.product_name"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="上架" value="1" />
|
||||
<el-option label="下架" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getProducts">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="products"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column label="商品图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="getProductImages(row.product_images)[0]"
|
||||
:src="getImageUrl(getProductImages(row.product_images)[0])"
|
||||
:preview-src-list="getProductImages(row.product_images).map(img => getImageUrl(img))"
|
||||
fit="cover"
|
||||
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else class="text-gray-400">无图片</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="分类" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.category?.category_name || '未分类' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="价格" width="150">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="text-red-500 font-semibold">¥{{ formatAmount(row.current_price) }}</div>
|
||||
<div v-if="row.original_price !== row.current_price" class="text-gray-400 text-xs line-through">
|
||||
¥{{ formatAmount(row.original_price) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="stock_quantity" label="库存" width="100" />
|
||||
|
||||
<el-table-column prop="sales_count" label="销量" width="100" />
|
||||
|
||||
<el-table-column prop="view_count" label="浏览量" width="100" />
|
||||
|
||||
<!-- <el-table-column prop="status" label="状态" width="100">-->
|
||||
<!-- <template #default="{ row }">-->
|
||||
<!-- <el-switch-->
|
||||
<!-- v-model="row.status"-->
|
||||
<!-- :active-value="1"-->
|
||||
<!-- :inactive-value="0"-->
|
||||
<!-- @change="updateStatus(row)"-->
|
||||
<!-- />-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<!-- -->
|
||||
<!-- <el-table-column prop="is_hot" label="热门" width="100">-->
|
||||
<!-- <template #default="{ row }">-->
|
||||
<!-- <el-switch-->
|
||||
<!-- v-model="row.is_hot"-->
|
||||
<!-- :active-value="1"-->
|
||||
<!-- :inactive-value="0"-->
|
||||
<!-- @change="updateHotStatus(row)"-->
|
||||
<!-- />-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showProductDetail(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="showEditDialog(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="danger-text"
|
||||
@click="deleteProduct(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getProducts"
|
||||
@current-change="getProducts"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑商品对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogMode === 'create' ? '添加商品' : '编辑商品'"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="productFormRef"
|
||||
:model="productForm"
|
||||
:rules="productRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品名称" prop="product_name">
|
||||
<el-input
|
||||
v-model="productForm.product_name"
|
||||
placeholder="请输入商品名称"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品分类" prop="category_id">
|
||||
<el-select
|
||||
v-model="productForm.category_id"
|
||||
placeholder="请选择分类"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:label="category.category_name"
|
||||
:value="category.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="商品描述">
|
||||
<el-input
|
||||
v-model="productForm.product_description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入商品描述"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商品图片">
|
||||
<ImageUpload
|
||||
v-model="productForm.product_images"
|
||||
:limit="9"
|
||||
:multiple="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="原价" prop="original_price">
|
||||
<el-input-number
|
||||
v-model="productForm.original_price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="现价" prop="current_price">
|
||||
<el-input-number
|
||||
v-model="productForm.current_price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="库存数量" prop="stock_quantity">
|
||||
<el-input-number
|
||||
v-model="productForm.stock_quantity"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序序号">
|
||||
<el-input-number
|
||||
v-model="productForm.sort_order"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="productForm.status">
|
||||
<el-radio :label="1">上架</el-radio>
|
||||
<el-radio :label="0">下架</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="热门商品">
|
||||
<el-switch
|
||||
v-model="productForm.is_hot"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitProduct">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 商品详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="商品详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="商品ID">{{ currentProduct.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品名称">{{ currentProduct.product_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品分类">{{ currentProduct.category?.category_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentProduct.status === 1 ? 'success' : 'danger'">
|
||||
{{ currentProduct.status === 1 ? '上架' : '下架' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="热门商品">
|
||||
<el-tag :type="currentProduct.is_hot === 1 ? 'warning' : 'info'">
|
||||
{{ currentProduct.is_hot === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原价">¥{{ formatAmount(currentProduct.original_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="现价">¥{{ formatAmount(currentProduct.current_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存数量">{{ currentProduct.stock_quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="销量">{{ currentProduct.sales_count }}</el-descriptions-item>
|
||||
<el-descriptions-item label="浏览量">{{ currentProduct.view_count }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排序序号">{{ currentProduct.sort_order }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品描述" :span="2">{{ currentProduct.product_description || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentProduct.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentProduct.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<div v-if="getProductImages(currentProduct.product_images).length > 0" class="mt-4">
|
||||
<h4>商品图片</h4>
|
||||
<div class="image-gallery">
|
||||
<el-image
|
||||
v-for="(image, index) in getProductImages(currentProduct.product_images)"
|
||||
:key="index"
|
||||
:src="getImageUrl(image)"
|
||||
:preview-src-list="getProductImages(currentProduct.product_images).map(img => getImageUrl(img))"
|
||||
fit="cover"
|
||||
class="product-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const products = ref([])
|
||||
const categories = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const dialogMode = ref('create')
|
||||
const submitting = ref(false)
|
||||
const productFormRef = ref()
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
category_id: '',
|
||||
product_name: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 商品表单
|
||||
const productForm = reactive({
|
||||
product_name: '',
|
||||
category_id: '',
|
||||
product_description: '',
|
||||
product_images: '',
|
||||
original_price: 0,
|
||||
current_price: 0,
|
||||
stock_quantity: 0,
|
||||
sort_order: 0,
|
||||
status: 1,
|
||||
is_hot: 0
|
||||
})
|
||||
|
||||
// 当前查看的商品
|
||||
const currentProduct = ref({})
|
||||
|
||||
// 表单验证规则
|
||||
const productRules = {
|
||||
product_name: [
|
||||
{ required: true, message: '请输入商品名称', trigger: 'blur' },
|
||||
{ max: 100, message: '商品名称不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
category_id: [
|
||||
{ required: true, message: '请选择商品分类', trigger: 'change' }
|
||||
],
|
||||
original_price: [
|
||||
{ required: true, message: '请输入原价', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '原价不能小于0', trigger: 'blur' }
|
||||
],
|
||||
current_price: [
|
||||
{ required: true, message: '请输入现价', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '现价不能小于0', trigger: 'blur' }
|
||||
],
|
||||
stock_quantity: [
|
||||
{ required: true, message: '请输入库存数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '库存数量不能小于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取商品分类
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const response = await axios.get('/admin/products/categories')
|
||||
if (response.data.success) {
|
||||
categories.value = response.data.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
const getProducts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/products/primary', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
products.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
category_id: '',
|
||||
product_name: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getProducts()
|
||||
}
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${window.location.origin}${url}`
|
||||
}
|
||||
|
||||
// 获取商品图片列表
|
||||
const getProductImages = (images) => {
|
||||
if (!images) return []
|
||||
return images.split(',').filter(img => img.trim())
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0.00'
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
dialogMode.value = 'create'
|
||||
resetProductForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = (product) => {
|
||||
dialogMode.value = 'edit'
|
||||
Object.assign(productForm, {
|
||||
id: product.id,
|
||||
product_name: product.product_name,
|
||||
category_id: product.category_id,
|
||||
product_description: product.product_description,
|
||||
product_images: product.product_images || '',
|
||||
original_price: product.original_price,
|
||||
current_price: product.current_price,
|
||||
stock_quantity: product.stock_quantity,
|
||||
sort_order: product.sort_order,
|
||||
status: product.status,
|
||||
is_hot: product.is_hot || 0
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 显示商品详情
|
||||
const showProductDetail = (product) => {
|
||||
currentProduct.value = product
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 重置商品表单
|
||||
const resetProductForm = () => {
|
||||
Object.assign(productForm, {
|
||||
product_name: '',
|
||||
category_id: '',
|
||||
product_description: '',
|
||||
product_images: '',
|
||||
original_price: 0,
|
||||
current_price: 0,
|
||||
stock_quantity: 0,
|
||||
sort_order: 0,
|
||||
status: 1,
|
||||
is_hot: 0
|
||||
})
|
||||
if (productFormRef.value) {
|
||||
productFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 提交商品表单
|
||||
const submitProduct = async () => {
|
||||
if (!productFormRef.value) return
|
||||
|
||||
const valid = await productFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
if (dialogMode.value === 'create') {
|
||||
await axios.post('/admin/products/primary', productForm)
|
||||
ElMessage.success('商品创建成功')
|
||||
} else {
|
||||
const { id, ...updateData } = productForm
|
||||
await axios.put(`/admin/products/primary/${id}`, updateData)
|
||||
ElMessage.success('商品更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
getProducts()
|
||||
} catch (error) {
|
||||
console.error('提交商品失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = async (product) => {
|
||||
try {
|
||||
await axios.put(`/admin/products/primary/${product.id}`, {
|
||||
status: product.status
|
||||
})
|
||||
ElMessage.success('状态更新成功')
|
||||
} catch (error) {
|
||||
console.error('更新状态失败:', error)
|
||||
// 恢复原状态
|
||||
product.status = product.status === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 更新热门状态
|
||||
const updateHotStatus = async (product) => {
|
||||
try {
|
||||
await axios.put(`/admin/products/primary/${product.id}`, {
|
||||
is_hot: product.is_hot
|
||||
})
|
||||
ElMessage.success('热门状态更新成功')
|
||||
} catch (error) {
|
||||
console.error('更新热门状态失败:', error)
|
||||
// 恢复原状态
|
||||
product.is_hot = product.is_hot === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 删除商品
|
||||
const deleteProduct = async (product) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除商品 "${product.product_name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await axios.delete(`/admin/products/primary/${product.id}`)
|
||||
ElMessage.success('商品删除成功')
|
||||
getProducts()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除商品失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getCategories()
|
||||
getProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.primary-products-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.danger-text:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div class="secondary-products-page">
|
||||
<div class="page-header">
|
||||
<h2>二级商品管理</h2>
|
||||
<span class="page-desc">用户上架的商品,仅支持查看</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="卖家ID">
|
||||
<el-input
|
||||
v-model="searchForm.seller_id"
|
||||
placeholder="请输入卖家ID"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商品名称">
|
||||
<el-input
|
||||
v-model="searchForm.product_name"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="下架" value="0" />
|
||||
<el-option label="上架" value="1" />
|
||||
<el-option label="已售出" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getProducts">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="products"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
|
||||
<el-table-column label="商品图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="getProductImages(row.product_images)[0]"
|
||||
:src="getImageUrl(getProductImages(row.product_images)[0])"
|
||||
:preview-src-list="getProductImages(row.product_images).map(img => getImageUrl(img))"
|
||||
fit="cover"
|
||||
style="width: 60px; height: 60px; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else class="text-gray-400">无图片</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="卖家信息" width="140">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.seller?.customer_name || row.seller?.phone }}</div>
|
||||
<div class="text-xs text-gray-500">ID: {{ row.seller_id }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="原商品类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.original_product_type === 1 ? 'primary' : 'warning'" size="small">
|
||||
{{ row.original_product_type === 1 ? '一级商品' : '二级商品' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="价格信息" width="150">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="text-green-600 font-semibold">售价: ¥{{ formatAmount(row.selling_price) }}</div>
|
||||
<div class="text-gray-500 text-xs">成本: ¥{{ formatAmount(row.cost_price) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
|
||||
<el-table-column prop="view_count" label="浏览量" width="100" />
|
||||
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)" size="small">
|
||||
{{ getStatusName(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" size="small" @click="showProductDetail(row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<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="getProducts"
|
||||
@current-change="getProducts"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="二级商品详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="商品ID">{{ currentProduct.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品名称">{{ currentProduct.product_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家姓名">{{ currentProduct.seller?.customer_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家手机">{{ currentProduct.seller?.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="原商品ID">{{ currentProduct.original_product_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="原商品类型">
|
||||
<el-tag :type="currentProduct.original_product_type === 1 ? 'primary' : 'warning'">
|
||||
{{ currentProduct.original_product_type === 1 ? '一级商品' : '二级商品' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="成本价">¥{{ formatAmount(currentProduct.cost_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="售价">¥{{ formatAmount(currentProduct.selling_price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="利润">
|
||||
<span :class="getProfitClass(currentProduct.selling_price - currentProduct.cost_price)">
|
||||
¥{{ formatAmount(currentProduct.selling_price - currentProduct.cost_price) }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ currentProduct.quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="浏览量">{{ currentProduct.view_count }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusTagType(currentProduct.status)">
|
||||
{{ getStatusName(currentProduct.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="商品描述" :span="2">{{ currentProduct.product_description || '无' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(currentProduct.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(currentProduct.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 卖家信息 -->
|
||||
<div v-if="currentProduct.seller" class="mt-4">
|
||||
<h4>卖家信息</h4>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="卖家ID">{{ currentProduct.seller.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="卖家身份码">{{ currentProduct.seller.identity_code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实姓名">{{ currentProduct.seller.real_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前积分">{{ currentProduct.seller.current_points }}</el-descriptions-item>
|
||||
<el-descriptions-item label="公司名称" :span="2">{{ currentProduct.seller.company_name || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<div v-if="getProductImages(currentProduct.product_images).length > 0" class="mt-4">
|
||||
<h4>商品图片</h4>
|
||||
<div class="image-gallery">
|
||||
<el-image
|
||||
v-for="(image, index) in getProductImages(currentProduct.product_images)"
|
||||
:key="index"
|
||||
:src="getImageUrl(image)"
|
||||
:preview-src-list="getProductImages(currentProduct.product_images).map(img => getImageUrl(img))"
|
||||
fit="cover"
|
||||
class="product-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const products = ref([])
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
seller_id: '',
|
||||
product_name: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 当前查看的商品
|
||||
const currentProduct = ref({})
|
||||
|
||||
// 获取商品列表
|
||||
const getProducts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...searchForm
|
||||
}
|
||||
|
||||
// 过滤空值
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||||
delete params[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await axios.get('/admin/products/secondary', { params })
|
||||
|
||||
if (response.data.success) {
|
||||
products.value = response.data.data.list || []
|
||||
pagination.total = response.data.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取二级商品列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
seller_id: '',
|
||||
product_name: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getProducts()
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = { 0: '下架', 1: '上架', 2: '已售出' }
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status) => {
|
||||
const typeMap = { 0: 'danger', 1: 'success', 2: 'info' }
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取利润颜色类
|
||||
const getProfitClass = (profit) => {
|
||||
if (profit > 0) {
|
||||
return 'text-green-600 font-semibold'
|
||||
} else if (profit < 0) {
|
||||
return 'text-red-600 font-semibold'
|
||||
}
|
||||
return 'text-gray-600'
|
||||
}
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${window.location.origin}${url}`
|
||||
}
|
||||
|
||||
// 获取商品图片列表
|
||||
const getProductImages = (images) => {
|
||||
if (!images) return []
|
||||
return images.split(',').filter(img => img.trim())
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0.00'
|
||||
return Number(amount).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 显示商品详情
|
||||
const showProductDetail = (product) => {
|
||||
currentProduct.value = product
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 组件挂载后获取数据
|
||||
onMounted(() => {
|
||||
getProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.secondary-products-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: -18px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-form .el-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3B82F6' // 扩展主题色
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: "localhost",
|
||||
cors: true,
|
||||
port: 1111,
|
||||
open: false, //自动打开
|
||||
proxy: {
|
||||
// 这里的ccc可乱写, 是拼接在url后面的地址 如果接口中没有统一的后缀可自定义
|
||||
// 如果有统一后缀, 如api, 直接写api即可, 也不用rewrite了
|
||||
"^/back": {
|
||||
target: "http://localhost:8090", // 真实接口地址, 后端给的基地址
|
||||
changeOrigin: true, // 允许跨域
|
||||
secure: false, //关键参数,不懂的自己去查
|
||||
rewrite: (path) => path.replace(/^\/back/, '/back')
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user