Generating commit message...

This commit is contained in:
2025-08-30 14:33:49 +08:00
parent 4d469e95f0
commit 7f9bfbb381
99 changed files with 69225 additions and 35 deletions

11
admin-system/.env Normal file
View File

@@ -0,0 +1,11 @@
# 环境配置
VITE_APP_NAME=结伴客后台管理系统
VITE_APP_VERSION=1.0.0
# API配置
VITE_API_BASE_URL=/api
VITE_API_TIMEOUT=10000
# 功能开关
VITE_FEATURE_ANALYTICS=false
VITE_FEATURE_DEBUG=false

View File

@@ -0,0 +1,13 @@
# 开发环境配置
NODE_ENV=development
# API配置
VITE_API_BASE_URL=http://localhost:3000/api/v1
VITE_API_TIMEOUT=30000
# 功能开关
VITE_FEATURE_ANALYTICS=true
VITE_FEATURE_DEBUG=true
# 开发工具
VITE_DEV_TOOLS=true

View File

@@ -0,0 +1,13 @@
# 生产环境配置
NODE_ENV=production
# API配置
VITE_API_BASE_URL=https://api.jiebanke.com/api/v1
VITE_API_TIMEOUT=15000
# 功能开关
VITE_FEATURE_ANALYTICS=true
VITE_FEATURE_DEBUG=false
# 性能优化
VITE_COMPRESSION=true

245
admin-system/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,245 @@
# 结伴客后台管理系统部署指南
## 部署方式
### 1. 本地开发环境部署
#### 使用 npm 脚本
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
# 预览生产版本
npm run preview
```
#### 使用部署脚本
**Linux/Mac:**
```bash
chmod +x deploy.sh
./deploy.sh
```
**Windows PowerShell:**
```powershell
.\deploy.ps1
```
### 2. Docker 容器化部署
#### 开发环境
```bash
# 启动开发环境(包含前端、后端、数据库等所有服务)
docker-compose up frontend-dev backend mysql redis rabbitmq
# 或启动所有服务
docker-compose up
```
#### 生产环境
```bash
# 构建并启动生产环境
docker-compose up -d frontend backend mysql redis rabbitmq
# 查看服务状态
docker-compose ps
# 查看日志
docker-compose logs frontend
```
### 3. 单独服务部署
#### 前端服务
```bash
# 开发模式
docker-compose up frontend-dev
# 生产模式
docker-compose up frontend
```
#### 后端服务
```bash
docker-compose up backend
```
#### 数据库服务
```bash
docker-compose up mysql redis rabbitmq
```
## 环境配置
### 环境变量文件
创建以下环境变量文件:
**.env.development** (开发环境):
```
VITE_APP_TITLE=结伴客后台管理系统(开发)
VITE_API_BASE_URL=http://localhost:3000
VITE_APP_VERSION=dev
```
**.env.production** (生产环境):
```
VITE_APP_TITLE=结伴客后台管理系统
VITE_API_BASE_URL=https://api.jiebanke.com
VITE_APP_VERSION=v1.0.0
```
### Docker 环境变量
`docker-compose.yml` 中配置:
```yaml
environment:
- NODE_ENV=production
- DB_HOST=mysql
- DB_PORT=3306
- DB_NAME=jiebanke
- DB_USER=root
- DB_PASSWORD=password
- REDIS_HOST=redis
- REDIS_PORT=6379
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_PORT=5672
```
## 端口映射
| 服务 | 容器端口 | 主机端口 | 说明 |
|------|----------|----------|------|
| 前端 | 80 | 80 | 生产环境前端 |
| 前端开发 | 5173 | 5173 | 开发环境前端 |
| 后端API | 3000 | 3000 | Node.js后端服务 |
| MySQL | 3306 | 3306 | 数据库服务 |
| Redis | 6379 | 6379 | 缓存服务 |
| RabbitMQ | 5672 | 5672 | 消息队列服务 |
| RabbitMQ管理 | 15672 | 15672 | 管理界面 |
## 健康检查
系统提供健康检查端点:
- 前端: `http://localhost:80/health`
- 后端: `http://localhost:3000/health`
## 监控和日志
### 查看日志
```bash
# 查看所有服务日志
docker-compose logs
# 查看特定服务日志
docker-compose logs frontend
docker-compose logs backend
```
### 服务状态监控
```bash
# 查看服务状态
docker-compose ps
# 查看资源使用情况
docker stats
```
## 故障排除
### 常见问题
1. **端口冲突**
- 检查端口是否被占用
- 修改 `docker-compose.yml` 中的端口映射
2. **依赖安装失败**
- 清除缓存: `npm cache clean --force`
- 删除 node_modules 重新安装
3. **数据库连接失败**
- 检查数据库服务是否启动
- 验证环境变量配置
4. **构建失败**
- 检查 Node.js 版本 (要求 >= 16)
- 检查系统依赖
### 调试模式
启动服务时添加调试标志:
```bash
# 前端开发调试
npm run dev -- --debug
# Docker 调试模式
docker-compose up --build --force-recreate
```
## 备份和恢复
### 数据库备份
```bash
# 备份MySQL数据
docker exec -i mysql mysqldump -uroot -ppassword jiebanke > backup.sql
# 恢复MySQL数据
docker exec -i mysql mysql -uroot -ppassword jiebanke < backup.sql
```
### 卷备份
```bash
# 备份数据库卷
docker run --rm -v mysql_data:/source -v $(pwd):/backup alpine tar czf /backup/mysql_backup.tar.gz -C /source .
```
## 安全建议
1. **修改默认密码**
- MySQL root 密码
- RabbitMQ 默认用户密码
- Redis 密码(如果需要)
2. **启用HTTPS**
- 配置SSL证书
- 使用反向代理
3. **防火墙配置**
- 只开放必要的端口
- 限制外部访问
4. **定期更新**
- 更新Docker镜像
- 更新依赖包
## 性能优化
1. **启用缓存**
- 配置Redis缓存
- 使用CDN加速静态资源
2. **负载均衡**
- 使用nginx负载均衡
- 配置多实例部署
3. **数据库优化**
- 添加索引
- 查询优化
- 连接池配置
## 支持
如遇部署问题,请检查:
1. Docker 和 Docker Compose 版本
2. 系统资源(内存、磁盘空间)
3. 网络连接
4. 查看详细错误日志

53
admin-system/Dockerfile Normal file
View File

@@ -0,0 +1,53 @@
# 结伴客后台管理系统 Dockerfile
FROM node:18-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产环境
FROM nginx:alpine AS production
# 复制构建好的文件到nginx目录
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# 开发环境
FROM node:18-alpine AS development
# 设置工作目录
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖(包括开发依赖)
RUN npm ci
# 复制源代码
COPY . .
# 暴露端口
EXPOSE 5173
# 启动开发服务器
CMD ["npm", "run", "dev", "--", "--host"]

286
admin-system/README.md Normal file
View File

@@ -0,0 +1,286 @@
# 结伴客后台管理系统
基于 Vue 3 + TypeScript + Ant Design Vue 构建的后台管理系统,用于管理结伴客平台的用户、商家、旅行计划、动物认领等业务。
## 🚀 特性
- **现代化技术栈**: Vue 3 + TypeScript + Vite
- **UI框架**: Ant Design Vue 4.x
- **状态管理**: Pinia
- **路由管理**: Vue Router 4
- **HTTP客户端**: Axios
- **构建工具**: Vite
- **代码规范**: ESLint + Prettier
## 📦 安装依赖
```bash
cd admin-system
npm install
```
## 🛠️ 开发
```bash
# 启动开发服务器
npm run dev
# 类型检查
npm run type-check
# 代码格式化
npm run lint
# 构建生产版本
npm run build
# 预览生产版本
npm run preview
```
## 🌐 环境配置
系统支持多环境配置:
### 开发环境 (.env.development)
```env
NODE_ENV=development
VITE_API_BASE_URL=http://localhost:3000/api/v1
VITE_FEATURE_DEBUG=true
```
### 生产环境 (.env.production)
```env
NODE_ENV=production
VITE_API_BASE_URL=https://api.jiebanke.com/api/v1
VITE_FEATURE_DEBUG=false
```
## 📁 项目结构
```
admin-system/
├── src/
│ ├── api/ # API接口
│ ├── assets/ # 静态资源
│ ├── components/ # 组件
│ │ ├── common/ # 通用组件
│ │ ├── layout/ # 布局组件
│ │ ├── user/ # 用户相关组件
│ │ ├── merchant/ # 商家相关组件
│ │ ├── travel/ # 旅行相关组件
│ │ ├── animal/ # 动物相关组件
│ │ ├── order/ # 订单相关组件
│ │ ├── promotion/ # 推广相关组件
│ │ └── dashboard/ # 仪表板组件
│ ├── composables/ # 组合式函数
│ ├── layouts/ # 布局文件
│ ├── pages/ # 页面组件
│ │ ├── dashboard/ # 仪表板
│ │ ├── user/ # 用户管理
│ │ ├── merchant/ # 商家管理
│ │ ├── travel/ # 旅行管理
│ │ ├── animal/ # 动物管理
│ │ ├── order/ # 订单管理
│ │ ├── promotion/ # 推广管理
│ │ ├── system/ # 系统设置
│ │ └── Login.vue # 登录页面
│ ├── router/ # 路由配置
│ ├── stores/ # 状态管理
│ ├── styles/ # 样式文件
│ ├── types/ # TypeScript类型定义
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── public/ # 公共资源
├── tests/ # 测试文件
├── index.html # HTML模板
├── vite.config.ts # Vite配置
├── tsconfig.json # TypeScript配置
├── package.json # 项目配置
└── README.md # 项目说明
```
## 🔐 权限管理
系统采用基于角色的权限控制RBAC
### 用户角色
- **超级管理员**: 所有权限
- **运营管理员**: 用户管理、内容审核
- **财务管理员**: 订单管理、财务管理
- **客服管理员**: 用户支持、投诉处理
### 权限标识
- `user:read` - 查看用户
- `user:write` - 管理用户
- `merchant:read` - 查看商家
- `merchant:write` - 管理商家
- `travel:read` - 查看旅行计划
- `travel:write` - 管理旅行计划
- `animal:read` - 查看动物信息
- `animal:write` - 管理动物信息
- `order:read` - 查看订单
- `order:write` - 管理订单
- `system:read` - 查看系统设置
- `system:write` - 管理系统设置
## 📊 功能模块
### 1. 仪表板
- 系统概览统计
- 实时数据监控
- 快捷操作入口
### 2. 用户管理
- 用户列表查看
- 用户信息编辑
- 用户状态管理
- 用户行为分析
### 3. 商家管理
- 商家入驻审核
- 商家信息管理
- 商家商品管理
- 商家数据统计
### 4. 旅行管理
- 旅行计划审核
- 旅行计划管理
- 旅行匹配监控
- 旅行数据统计
### 5. 动物管理
- 动物信息管理
- 认领申请审核
- 动物状态跟踪
- 认领数据分析
### 6. 订单管理
- 订单列表查看
- 订单状态管理
- 订单数据分析
- 财务报表生成
### 7. 推广管理
- 推广数据统计
- 提现申请审核
- 推广效果分析
- 奖励发放管理
### 8. 系统设置
- 系统参数配置
- 操作日志查看
- 缓存管理
- 系统监控
## 🎨 主题定制
系统支持主题定制:
```less
// 主要主题变量
--primary-color: #1890ff; // 主题色
--success-color: #52c41a; // 成功色
--warning-color: #faad14; // 警告色
--error-color: #ff4d4f; // 错误色
```
## 📱 响应式设计
系统支持多种屏幕尺寸:
- **桌面端** (≥1200px): 完整功能展示
- **平板端** (768px-1199px): 适配布局
- **移动端** (<768px): 简化界面
## 🔧 开发规范
### 代码风格
- 使用TypeScript严格模式
- 遵循Vue 3组合式API规范
- 使用ESLint + Prettier统一代码风格
- 组件使用PascalCase命名
- 文件使用kebab-case命名
### 组件开发
```vue
<template>
<!-- 模板代码 -->
</template>
<script setup lang="ts">
// 组合式API
</script>
<style scoped>
/* 组件样式 */
</style>
```
### API调用
```typescript
// 使用封装的request方法
import { request } from '@/api'
const getUsers = async () => {
try {
const response = await request.get('/users')
return response.data
} catch (error) {
console.error('获取用户失败:', error)
}
}
```
## 🚀 部署指南
### 开发环境部署
1. 安装依赖: `npm install`
2. 启动开发服务器: `npm run dev`
3. 访问: http://localhost:3001
### 生产环境部署
1. 构建项目: `npm run build`
2. 部署到静态文件服务器
3. 配置Nginx反向代理
### Docker部署
```dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
## 📞 技术支持
- 文档: [查看API文档](./docs/api-documentation.md)
- 问题反馈: [创建Issue](https://github.com/jiebanke/admin-system/issues)
- 邮箱: support@jiebanke.com
## 📄 许可证
MIT License - 详见 [LICENSE](LICENSE) 文件
## 🎯 版本历史
### v1.0.0 (2025-01-01)
- 初始版本发布
- 基础框架搭建
- 核心功能模块
- 权限管理系统
- 响应式设计
---
**最后更新**: 2025-01-01
**当前版本**: v1.0.0

46
admin-system/deploy.ps1 Normal file
View File

@@ -0,0 +1,46 @@
# 结伴客后台管理系统部署脚本 (PowerShell)
Write-Host "开始部署结伴客后台管理系统..." -ForegroundColor Green
# 检查Node.js是否安装
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-Host "错误: Node.js 未安装,请先安装 Node.js" -ForegroundColor Red
exit 1
}
# 检查npm是否安装
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
Write-Host "错误: npm 未安装,请先安装 npm" -ForegroundColor Red
exit 1
}
# 安装依赖
Write-Host "安装项目依赖..." -ForegroundColor Yellow
npm install
if ($LASTEXITCODE -ne 0) {
Write-Host "依赖安装失败,请检查错误信息" -ForegroundColor Red
exit 1
}
# 构建项目
Write-Host "构建项目..." -ForegroundColor Yellow
npm run build
if ($LASTEXITCODE -ne 0) {
Write-Host "构建失败,请检查错误信息" -ForegroundColor Red
exit 1
}
Write-Host "构建成功!" -ForegroundColor Green
# 创建生产环境配置文件
Write-Host "创建生产环境配置..." -ForegroundColor Yellow
@"
VITE_APP_TITLE=
VITE_API_BASE_URL=https://api.jiebanke.com
VITE_APP_VERSION=v1.0.0
"@ | Out-File -FilePath .env.production -Encoding UTF8
Write-Host "部署完成!" -ForegroundColor Green
Write-Host "运行 'npm run preview' 预览生产版本" -ForegroundColor Cyan
Write-Host "或运行 'npm run dev' 启动开发服务器" -ForegroundColor Cyan

44
admin-system/deploy.sh Normal file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# 结伴客后台管理系统部署脚本
echo "开始部署结伴客后台管理系统..."
# 检查Node.js是否安装
if ! command -v node &> /dev/null; then
echo "错误: Node.js 未安装,请先安装 Node.js"
exit 1
fi
# 检查npm是否安装
if ! command -v npm &> /dev/null; then
echo "错误: npm 未安装,请先安装 npm"
exit 1
fi
# 安装依赖
echo "安装项目依赖..."
npm install
# 构建项目
echo "构建项目..."
npm run build
# 检查构建是否成功
if [ $? -eq 0 ]; then
echo "构建成功!"
else
echo "构建失败,请检查错误信息"
exit 1
fi
# 创建生产环境配置文件
echo "创建生产环境配置..."
cat > .env.production << EOF
VITE_APP_TITLE=结伴客后台管理系统
VITE_API_BASE_URL=https://api.jiebanke.com
VITE_APP_VERSION=v1.0.0
EOF
echo "部署完成!"
echo "运行 'npm run preview' 预览生产版本"
echo "或运行 'npm run dev' 启动开发服务器"

View File

@@ -0,0 +1,109 @@
version: '3.8'
services:
# 前端应用
frontend:
build:
context: .
target: production
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: unless-stopped
networks:
- jiebanke-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
# 开发环境前端
frontend-dev:
build:
context: .
target: development
ports:
- "5173:5173"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
restart: unless-stopped
networks:
- jiebanke-network
command: npm run dev -- --host
# 后端API服务
backend:
build:
context: ../backend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=mysql
- DB_PORT=3306
- DB_NAME=jiebanke
- DB_USER=root
- DB_PASSWORD=password
- REDIS_HOST=redis
- REDIS_PORT=6379
- RABBITMQ_HOST=rabbitmq
- RABBITMQ_PORT=5672
depends_on:
- mysql
- redis
- rabbitmq
restart: unless-stopped
networks:
- jiebanke-network
# MySQL数据库
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=jiebanke
- MYSQL_USER=jiebanke
- MYSQL_PASSWORD=password
volumes:
- mysql_data:/var/lib/mysql
- ../backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
networks:
- jiebanke-network
# Redis缓存
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stopped
networks:
- jiebanke-network
# RabbitMQ消息队列
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=password
restart: unless-stopped
networks:
- jiebanke-network
volumes:
mysql_data:
networks:
jiebanke-network:
driver: bridge

86
admin-system/env.d.ts vendored Normal file
View File

@@ -0,0 +1,86 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string
readonly VITE_APP_VERSION: string
readonly VITE_API_BASE_URL: string
readonly VITE_API_TIMEOUT: string
readonly VITE_FEATURE_ANALYTICS: string
readonly VITE_FEATURE_DEBUG: string
readonly VITE_DEV_TOOLS?: string
readonly VITE_COMPRESSION?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
// Vue文件模块声明
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// 图片资源声明
declare module '*.png' {
const src: string
export default src
}
declare module '*.jpg' {
const src: string
export default src
}
declare module '*.jpeg' {
const src: string
export default src
}
declare module '*.gif' {
const src: string
export default src
}
declare module '*.svg' {
const src: string
export default src
}
declare module '*.ico' {
const src: string
export default src
}
declare module '*.webp' {
const src: string
export default src
}
// 样式文件声明
declare module '*.css' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.scss' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.less' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.styl' {
const classes: { readonly [key: string]: string }
export default classes
}
// JSON文件声明
declare module '*.json' {
const value: any
export default value
}

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

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- VITE_APP_NAME %></title>
<meta name="description" content="结伴客后台管理系统 - 专业的旅行结伴与动物认领平台管理后台" />
<meta name="keywords" content="结伴客,后台管理,旅行,动物认领,商家管理" />
<!-- 预加载关键资源 -->
<link rel="preconnect" href="<%- VITE_API_BASE_URL %>" crossorigin>
<!-- 主题颜色 -->
<meta name="theme-color" content="#1890ff">
<!-- 移动端优化 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="<%- VITE_APP_NAME %>">
<!-- 避免被搜索引擎索引 -->
<meta name="robots" content="noindex, nofollow">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<!-- 环境变量提示 -->
<script>
console.log('环境:', '<%- NODE_ENV %>');
console.log('版本:', '<%- VITE_APP_VERSION %>');
console.log('API地址:', '<%- VITE_API_BASE_URL %>');
</script>
</body>
</html>

38
admin-system/nginx.conf Normal file
View File

@@ -0,0 +1,38 @@
server {
listen 80;
server_name localhost;
# 静态文件服务
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# API代理配置
location /api/ {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
}
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

3257
admin-system/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,41 @@
{
"name": "jiebanke-admin",
"version": "1.0.0",
"description": "结伴客后台管理系统",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"ant-design-vue": "^4.0.0",
"axios": "^1.4.0",
"@ant-design/icons-vue": "^6.1.0",
"dayjs": "^1.11.0",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@types/lodash-es": "^4.17.0",
"@types/node": "^18.0.0",
"@vitejs/plugin-vue": "^4.2.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.4.0",
"typescript": "~5.0.0",
"vite": "^4.3.0",
"vue-tsc": "^1.4.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^2.8.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

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

@@ -0,0 +1,30 @@
<template>
<a-config-provider :locale="zhCN">
<router-view />
</a-config-provider>
</template>
<script setup lang="ts">
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { onMounted } from 'vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
onMounted(() => {
// 初始化应用
appStore.initializeApp()
// 开发环境调试信息
if (import.meta.env.DEV) {
console.log('🎯 应用组件挂载完成')
}
})
</script>
<style scoped>
#app {
min-height: 100vh;
background-color: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,339 @@
import axios from 'axios'
import { message } from 'ant-design-vue'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
// API基础配置
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
const timeout = parseInt(import.meta.env.VITE_API_TIMEOUT || '10000')
// 创建axios实例
const api: AxiosInstance = axios.create({
baseURL,
timeout,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
api.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 添加认证token
const token = localStorage.getItem('admin_token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
// 记录请求日志(开发环境)
if (import.meta.env.DEV) {
console.log('🚀 API请求:', config.method?.toUpperCase(), config.url)
}
return config
},
(error) => {
console.error('❌ 请求拦截器错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response: AxiosResponse) => {
// 处理成功响应
const { data } = response
if (import.meta.env.DEV) {
console.log('✅ API响应:', response.config.url, data)
}
// 检查业务逻辑成功
if (data && data.success) {
return data
} else {
// 业务逻辑错误
const errorMsg = data?.message || '请求失败'
message.error(errorMsg)
return Promise.reject(new Error(errorMsg))
}
},
(error) => {
// 处理错误响应
console.error('❌ API错误:', error)
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
// 未授权,跳转到登录页
message.error('登录已过期,请重新登录')
localStorage.removeItem('admin_token')
window.location.href = '/login'
break
case 403:
message.error('权限不足,无法访问')
break
case 404:
message.error('请求的资源不存在')
break
case 429:
message.error('请求过于频繁,请稍后再试')
break
case 500:
message.error('服务器内部错误')
break
default:
message.error(data?.message || '请求失败')
}
} else if (error.request) {
// 网络错误
message.error('网络连接失败,请检查网络设置')
} else {
// 其他错误
message.error('请求配置错误')
}
return Promise.reject(error)
}
)
// 通用请求方法
export const request = {
get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
api.get(url, config).then(res => res.data),
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.post(url, data, config).then(res => res.data),
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.put(url, data, config).then(res => res.data),
delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<T> =>
api.delete(url, config).then(res => res.data),
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> =>
api.patch(url, data, config).then(res => res.data)
}
// 认证相关API
export const authAPI = {
// 管理员登录
login: (credentials: { username: string; password: string }) =>
request.post<{
success: boolean
data: {
token: string
user: any
}
}>('/auth/admin/login', credentials),
// 获取当前用户信息
getCurrentUser: () =>
request.get<{
success: boolean
data: {
user: any
}
}>('/auth/me'),
// 刷新token
refreshToken: () =>
request.post<{
success: boolean
data: {
token: string
}
}>('/auth/refresh'),
// 退出登录
logout: () =>
request.post('/auth/logout')
}
// 用户管理API
export const userAPI = {
// 获取用户列表
getUsers: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
}) => request.get('/users', { params }),
// 获取用户详情
getUser: (id: number) => request.get(`/users/${id}`),
// 创建用户
createUser: (data: any) => request.post('/users', data),
// 更新用户
updateUser: (id: number, data: any) => request.put(`/users/${id}`, data),
// 删除用户
deleteUser: (id: number) => request.delete(`/users/${id}`),
// 批量操作
batchUsers: (ids: number[], action: string) =>
request.post('/users/batch', { ids, action })
}
// 商家管理API
export const merchantAPI = {
// 获取商家列表
getMerchants: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
type?: string
}) => request.get('/merchants', { params }),
// 获取商家详情
getMerchant: (id: number) => request.get(`/merchants/${id}`),
// 审核商家
approveMerchant: (id: number, data: any) => request.post(`/merchants/${id}/approve`, data),
// 拒绝商家
rejectMerchant: (id: number, reason: string) => request.post(`/merchants/${id}/reject`, { reason }),
// 禁用商家
disableMerchant: (id: number) => request.post(`/merchants/${id}/disable`),
// 启用商家
enableMerchant: (id: number) => request.post(`/merchants/${id}/enable`)
}
// 旅行管理API
export const travelAPI = {
// 获取旅行计划列表
getTravelPlans: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
destination?: string
}) => request.get('/travel/plans', { params }),
// 获取旅行计划详情
getTravelPlan: (id: number) => request.get(`/travel/plans/${id}`),
// 审核旅行计划
approveTravelPlan: (id: number) => request.post(`/travel/plans/${id}/approve`),
// 拒绝旅行计划
rejectTravelPlan: (id: number, reason: string) => request.post(`/travel/plans/${id}/reject`, { reason }),
// 关闭旅行计划
closeTravelPlan: (id: number) => request.post(`/travel/plans/${id}/close`)
}
// 动物管理API
export const animalAPI = {
// 获取动物列表
getAnimals: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
type?: string
}) => request.get('/animals', { params }),
// 获取动物详情
getAnimal: (id: number) => request.get(`/animals/${id}`),
// 创建动物
createAnimal: (data: any) => request.post('/animals', data),
// 更新动物
updateAnimal: (id: number, data: any) => request.put(`/animals/${id}`, data),
// 删除动物
deleteAnimal: (id: number) => request.delete(`/animals/${id}`),
// 审核动物认领
approveAnimalClaim: (id: number) => request.post(`/animals/claims/${id}/approve`),
// 拒绝动物认领
rejectAnimalClaim: (id: number, reason: string) => request.post(`/animals/claims/${id}/reject`, { reason })
}
// 订单管理API
export const orderAPI = {
// 获取订单列表
getOrders: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
type?: string
startDate?: string
endDate?: string
}) => request.get('/orders', { params }),
// 获取订单详情
getOrder: (id: number) => request.get(`/orders/${id}`),
// 更新订单状态
updateOrderStatus: (id: number, status: string) => request.put(`/orders/${id}/status`, { status }),
// 导出订单
exportOrders: (params: any) => request.get('/orders/export', {
params,
responseType: 'blob'
})
}
// 推广管理API
export const promotionAPI = {
// 获取推广数据
getPromotionStats: () => request.get('/promotion/stats'),
// 获取推广记录
getPromotionRecords: (params?: {
page?: number
pageSize?: number
search?: string
status?: string
}) => request.get('/promotion/records', { params }),
// 审核提现申请
approveWithdrawal: (id: number) => request.post(`/promotion/withdrawals/${id}/approve`),
// 拒绝提现申请
rejectWithdrawal: (id: number, reason: string) => request.post(`/promotion/withdrawals/${id}/reject`, { reason }),
// 导出推广数据
exportPromotionData: (params: any) => request.get('/promotion/export', {
params,
responseType: 'blob'
})
}
// 系统管理API
export const systemAPI = {
// 获取系统配置
getConfig: () => request.get('/system/config'),
// 更新系统配置
updateConfig: (data: any) => request.put('/system/config', data),
// 获取操作日志
getOperationLogs: (params?: {
page?: number
pageSize?: number
search?: string
action?: string
startDate?: string
endDate?: string
}) => request.get('/system/logs', { params }),
// 清理缓存
clearCache: () => request.post('/system/cache/clear'),
// 系统健康检查
healthCheck: () => request.get('/system/health')
}
export default api

View File

@@ -0,0 +1,350 @@
<template>
<a-layout class="main-layout">
<!-- 侧边栏 -->
<a-layout-sider
v-model:collapsed="collapsed"
:trigger="null"
collapsible
:width="240"
class="layout-sider"
>
<div class="logo">
<img src="@/assets/logo.png" alt="Logo" v-if="!collapsed" />
<h1 v-if="!collapsed">结伴客管理</h1>
<span v-else>JK</span>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
:inline-collapsed="collapsed"
>
<a-menu-item key="dashboard">
<template #icon>
<DashboardOutlined />
</template>
<span>仪表板</span>
<router-link to="/dashboard" />
</a-menu-item>
<a-menu-item key="users">
<template #icon>
<UserOutlined />
</template>
<span>用户管理</span>
<router-link to="/users" />
</a-menu-item>
<a-menu-item key="merchants">
<template #icon>
<ShopOutlined />
</template>
<span>商家管理</span>
<router-link to="/merchants" />
</a-menu-item>
<a-menu-item key="travel">
<template #icon>
<CompassOutlined />
</template>
<span>旅行管理</span>
<router-link to="/travel" />
</a-menu-item>
<a-menu-item key="animals">
<template #icon>
<HeartOutlined />
</template>
<span>动物管理</span>
<router-link to="/animals" />
</a-menu-item>
<a-menu-item key="orders">
<template #icon>
<ShoppingCartOutlined />
</template>
<span>订单管理</span>
<router-link to="/orders" />
</a-menu-item>
<a-menu-item key="promotion">
<template #icon>
<GiftOutlined />
</template>
<span>推广管理</span>
<router-link to="/promotion" />
</a-menu-item>
<a-menu-item key="system">
<template #icon>
<SettingOutlined />
</template>
<span>系统设置</span>
<router-link to="/system" />
</a-menu-item>
</a-menu>
</a-layout-sider>
<!-- 主内容区 -->
<a-layout>
<!-- 顶部导航 -->
<a-layout-header class="layout-header">
<div class="header-left">
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<menu-fold-outlined
v-else
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item>首页</a-breadcrumb-item>
<a-breadcrumb-item>{{ currentRouteMeta.title }}</a-breadcrumb-item>
</a-breadcrumb>
</div>
<div class="header-right">
<a-space :size="16">
<a-tooltip title="消息">
<a-badge :count="5" dot>
<bell-outlined class="header-icon" />
</a-badge>
</a-tooltip>
<a-tooltip title="帮助文档">
<question-circle-outlined class="header-icon" />
</a-tooltip>
<a-dropdown :trigger="['click']">
<a-avatar
size="small"
:src="userAvatar"
class="avatar"
/>
<template #overlay>
<a-menu>
<a-menu-item>
<user-outlined />
<span>个人中心</span>
</a-menu-item>
<a-menu-item>
<setting-outlined />
<span>账户设置</span>
</a-menu-item>
<a-menu-divider />
<a-menu-item @click="handleLogout">
<logout-outlined />
<span>退出登录</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<span class="username">{{ userName }}</span>
</a-space>
</div>
</a-layout-header>
<!-- 页面内容 -->
<a-layout-content class="layout-content">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAppStore } from '@/stores/app'
import { message, Modal } from 'ant-design-vue'
import {
DashboardOutlined,
UserOutlined,
ShopOutlined,
CompassOutlined,
HeartOutlined,
ShoppingCartOutlined,
GiftOutlined,
SettingOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
BellOutlined,
QuestionCircleOutlined,
LogoutOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
const appStore = useAppStore()
const collapsed = ref(false)
const selectedKeys = ref<string[]>(['dashboard'])
// 计算属性
const currentRouteMeta = computed(() => route.meta || {})
const userName = computed(() => appStore.state.user?.nickname || '管理员')
const userAvatar = computed(() => appStore.state.user?.avatar || 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin')
// 监听路由变化
router.afterEach((to) => {
selectedKeys.value = [to.name as string]
})
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: '确认退出',
content: '您确定要退出登录吗?',
okText: '确定',
cancelText: '取消',
onOk() {
appStore.logout()
message.success('退出成功')
router.push('/login')
}
})
}
</script>
<style scoped lang="less">
.main-layout {
min-height: 100vh;
}
.layout-sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
z-index: 10;
.logo {
height: 64px;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
margin: 16px;
border-radius: 6px;
img {
height: 32px;
margin-right: 8px;
}
h1 {
color: white;
font-size: 16px;
font-weight: 600;
margin: 0;
}
span {
color: white;
font-size: 18px;
font-weight: bold;
}
}
:deep(.ant-menu-dark) {
background: transparent;
}
:deep(.ant-menu-item) {
margin: 4px 0;
border-radius: 6px;
&.ant-menu-item-selected {
background: #1890ff;
}
}
}
.layout-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: white;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.header-left {
display: flex;
align-items: center;
.trigger {
font-size: 18px;
cursor: pointer;
margin-right: 16px;
color: #666;
&:hover {
color: #1890ff;
}
}
.breadcrumb {
margin-bottom: 0;
}
}
.header-right {
.header-icon {
font-size: 16px;
color: #666;
cursor: pointer;
&:hover {
color: #1890ff;
}
}
.avatar {
cursor: pointer;
background: #1890ff;
}
.username {
font-weight: 500;
color: #333;
}
}
}
.layout-content {
padding: 24px;
background: #f5f5f5;
min-height: calc(100vh - 64px);
overflow: auto;
}
// 响应式设计
@media (max-width: 768px) {
.layout-header {
padding: 0 16px;
.header-left {
.breadcrumb {
display: none;
}
}
.header-right {
.username {
display: none;
}
}
}
.layout-content {
padding: 16px;
}
}
</style>

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

@@ -0,0 +1,35 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import router from './router'
import App from './App.vue'
// 引入样式文件
import 'ant-design-vue/dist/reset.css'
import './styles/index.less'
// 创建应用实例
const app = createApp(App)
// 配置Pinia状态管理
const pinia = createPinia()
// 注册路由
app.use(router)
// 注册状态管理
app.use(pinia)
// 注册Ant Design Vue
app.use(Antd)
// 挂载应用
app.mount('#app')
// 开发环境调试信息
if (import.meta.env.DEV) {
console.log('🚀 结伴客后台管理系统启动成功')
console.log('📦 版本:', import.meta.env.VITE_APP_VERSION)
console.log('🌐 环境:', import.meta.env.MODE)
console.log('🔗 API地址:', import.meta.env.VITE_API_BASE_URL)
}

View File

@@ -0,0 +1,193 @@
<template>
<div class="login-container">
<div class="login-form">
<div class="login-header">
<h1>{{ appName }}</h1>
<p>欢迎回来请登录您的账号</p>
</div>
<a-form
:model="formState"
name="login"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
name="username"
:rules="[{ required: true, message: '请输入用户名!' }]"
>
<a-input
v-model:value="formState.username"
size="large"
placeholder="用户名"
>
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password
v-model:value="formState.password"
size="large"
placeholder="密码"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
size="large"
:loading="loading"
block
>
登录
</a-button>
</a-form-item>
</a-form>
<div class="login-footer">
<p>© 2025 结伴客系统 - 后台管理系统 v{{ appVersion }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useAppStore } from '@/stores/app'
interface FormState {
username: string
password: string
}
const router = useRouter()
const appStore = useAppStore()
const loading = ref(false)
const formState = reactive<FormState>({
username: '',
password: ''
})
const appName = import.meta.env.VITE_APP_NAME || '结伴客后台管理系统'
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
const onFinish = async (values: FormState) => {
loading.value = true
try {
// 模拟登录过程
console.log('登录信息:', values)
// TODO: 调用真实登录接口
// const response = await authAPI.login(values)
// 模拟登录成功
await new Promise(resolve => setTimeout(resolve, 1000))
// 保存token
localStorage.setItem('admin_token', 'mock_token_123456')
// 更新用户状态
appStore.setUser({
id: 1,
username: values.username,
nickname: '管理员',
role: 'admin'
})
message.success('登录成功!')
// 跳转到首页或重定向页面
const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/dashboard')
} catch (error) {
console.error('登录失败:', error)
message.error('登录失败,请检查用户名和密码')
} finally {
loading.value = false
}
}
const onFinishFailed = (errorInfo: any) => {
console.log('表单验证失败:', errorInfo)
message.warning('请填写完整的登录信息')
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-form {
width: 100%;
max-width: 400px;
padding: 40px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #1890ff;
margin-bottom: 8px;
font-size: 28px;
font-weight: 600;
}
.login-header p {
color: #666;
margin: 0;
}
.login-footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.login-footer p {
color: #999;
font-size: 12px;
margin: 0;
}
:deep(.ant-input-affix-wrapper) {
border-radius: 6px;
}
:deep(.ant-btn) {
border-radius: 6px;
height: 44px;
font-size: 16px;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,688 @@
<template>
<div class="animal-management">
<a-page-header
title="动物管理"
sub-title="管理动物信息和认领记录"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button type="primary" @click="showCreateModal">
<template #icon>
<PlusOutlined />
</template>
新增动物
</a-button>
</a-space>
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="animals" tab="动物列表">
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keyword"
placeholder="动物名称/编号"
allow-clear
/>
</a-form-item>
<a-form-item label="类型">
<a-select
v-model:value="searchForm.type"
placeholder="全部类型"
style="width: 120px"
allow-clear
>
<a-select-option value="alpaca">羊驼</a-select-option>
<a-select-option value="dog">狗狗</a-select-option>
<a-select-option value="cat">猫咪</a-select-option>
<a-select-option value="rabbit">兔子</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="available">可认领</a-select-option>
<a-select-option value="claimed">已认领</a-select-option>
<a-select-option value="reserved">预留中</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<!-- 动物表格 -->
<a-table
:columns="animalColumns"
:data-source="animalList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image
:width="60"
:height="60"
:src="record.image_url"
:fallback="fallbackImage"
style="border-radius: 6px;"
/>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'price'">
¥{{ record.price }}
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleViewAnimal(record)">
<EyeOutlined />
查看
</a-button>
<a-button size="small" @click="handleEditAnimal(record)">
<EditOutlined />
编辑
</a-button>
<a-button size="small" danger @click="handleDeleteAnimal(record)">
<DeleteOutlined />
删除
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="claims" tab="认领记录">
<a-card>
<!-- 认领记录搜索 -->
<div class="search-container">
<a-form layout="inline" :model="claimSearchForm">
<a-form-item label="关键词">
<a-input
v-model:value="claimSearchForm.keyword"
placeholder="用户/动物名称"
allow-clear
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="claimSearchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleClaimSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleClaimReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<!-- 认领记录表格 -->
<a-table
:columns="claimColumns"
:data-source="claimList"
:loading="claimLoading"
:pagination="claimPagination"
:row-key="record => record.id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'animal_image'">
<a-image
:width="40"
:height="40"
:src="record.animal_image"
:fallback="fallbackImage"
style="border-radius: 4px;"
/>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getClaimStatusColor(record.status)">
{{ getClaimStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<template v-if="record.status === 'pending'">
<a-button size="small" type="primary" @click="handleApproveClaim(record)">
<CheckOutlined />
通过
</a-button>
<a-button size="small" danger @click="handleRejectClaim(record)">
<CloseOutlined />
拒绝
</a-button>
</template>
<a-button size="small" @click="handleViewClaim(record)">
<EyeOutlined />
详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
PlusOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
CheckOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
interface Animal {
id: number
name: string
type: string
breed: string
age: number
price: number
status: string
image_url: string
description: string
created_at: string
}
interface AnimalClaim {
id: number
animal_id: number
animal_name: string
animal_image: string
user_name: string
user_phone: string
status: string
applied_at: string
processed_at: string
}
interface SearchForm {
keyword: string
type: string
status: string
}
interface ClaimSearchForm {
keyword: string
status: string
}
const activeTab = ref('animals')
const loading = ref(false)
const claimLoading = ref(false)
const searchForm = reactive<SearchForm>({
keyword: '',
type: '',
status: ''
})
const claimSearchForm = reactive<ClaimSearchForm>({
keyword: '',
status: ''
})
// 模拟数据
const animalList = ref<Animal[]>([
{
id: 1,
name: '小白',
type: 'alpaca',
breed: '苏利羊驼',
age: 2,
price: 1000,
status: 'available',
image_url: 'https://api.dicebear.com/7.x/bottts/svg?seed=alpaca',
description: '温顺可爱的羊驼',
created_at: '2024-01-10'
},
{
id: 2,
name: '旺财',
type: 'dog',
breed: '金毛寻回犬',
age: 1,
price: 800,
status: 'claimed',
image_url: 'https://api.dicebear.com/7.x/bottts/svg?seed=dog',
description: '活泼聪明的金毛',
created_at: '2024-02-15'
}
])
const claimList = ref<AnimalClaim[]>([
{
id: 1,
animal_id: 1,
animal_name: '小白',
animal_image: 'https://api.dicebear.com/7.x/bottts/svg?seed=alpaca',
user_name: '张先生',
user_phone: '13800138000',
status: 'pending',
applied_at: '2024-03-01',
processed_at: ''
}
])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const claimPagination = reactive({
current: 1,
pageSize: 20,
total: 30,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const animalColumns = [
{
title: '图片',
key: 'image',
width: 80,
align: 'center'
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 100
},
{
title: '类型',
key: 'type',
width: 100,
align: 'center'
},
{
title: '品种',
dataIndex: 'breed',
key: 'breed',
width: 120
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
width: 80,
align: 'center',
customRender: ({ text }: { text: number }) => `${text}岁`
},
{
title: '价格',
key: 'price',
width: 100,
align: 'center'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 150,
align: 'center'
}
]
const claimColumns = [
{
title: '动物',
key: 'animal_image',
width: 60,
align: 'center'
},
{
title: '动物名称',
dataIndex: 'animal_name',
key: 'animal_name',
width: 100
},
{
title: '用户',
dataIndex: 'user_name',
key: 'user_name',
width: 100
},
{
title: '联系电话',
dataIndex: 'user_phone',
key: 'user_phone',
width: 120
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '申请时间',
dataIndex: 'applied_at',
key: 'applied_at',
width: 120
},
{
title: '处理时间',
dataIndex: 'processed_at',
key: 'processed_at',
width: 120
},
{
title: '操作',
极速版 key: 'actions',
width: 150,
align: 'center'
}
]
const fallbackImage = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRkZGIi8+CjxwYXRoIGQ9Ik0zMCAxNUMzMS42NTY5IDE1IDMzIDE2LjM0MzEgMzMgMThDMzMgMTkuNjU2OSAzMS42NTY5IDIxIDMwIDIxQzI4LjM0MzEgMjEgMjcgMTkuNjU2OSAyNyAxOEMyNyAxNi4zNDMxIDI4LjM0MzEgMTUgMzAgMTVaIiBmaWxsPSIjQ0NDQ0NDIi8+CjxwYXRoIGQ9Ik0yMi41IDI1QzIyLjUgMjUuODI4NCAyMS44Mjg0IDI2LjUgMjEgMjYuNUgxOUMxOC4xNzE2IDI2LjUgMTcuNSAyNS44Mjg0IDE3LjUgMjVDMTcuNSAyNC4xNzE2IDE4LjE3MTYgMjMuNSAxOSAyMy41SDIxQzIxLjgyODQgMjMuNSAyMi41IDI0LjE3MTYgMjIuNSAyNVoiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTQyLjUgMjVDNDIuNSAyNS44Mjg0IDQxLjgyODQgMjYuNSA0MSAyNi41SDM5QzM4LjE3MTYgMjYuNSAzNy41IDI1LjgyODQgMzcuNSAyNUMzNy41IDI0LjE3MTYgMzguMTcxNiAyMy41IDM5IDIzLjVMNDEgMjMuNUM0MS44Mjg0IDIzLjUgNDIuNSAyNC4xNzE2IDQyLjUgMjVaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjwvc3ZnPgo='
// 类型映射
const getTypeColor = (type: string) => {
const colors = {
alpaca: 'pink',
dog: 'orange',
cat: 'blue',
rabbit: 'green'
}
return colors[type as keyof typeof colors] || 'default'
}
const getTypeText = (type: string) => {
const texts = {
alpaca: '羊驼',
dog: '狗狗',
cat: '猫咪',
rabbit: '兔子'
}
return texts[type as keyof typeof texts] || '未知'
}
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
available: 'green',
claimed: 'blue',
reserved: 'orange'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string) => {
const texts = {
available: '可认领',
claimed: '已认领',
reserved: '预留中'
}
return texts[status as keyof typeof texts] || '未知'
}
const getClaimStatusColor = (status: string) => {
const colors = {
pending: 'orange',
approved: 'green',
rejected: 'red',
completed: 'blue'
}
return colors[status as keyof typeof colors] || 'default'
}
const getClaimStatusText = (status: string) => {
const texts = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
completed: '已完成'
}
return texts[status as keyof typeof texts] || '未知'
}
// 生命周期
onMounted(() => {
loadAnimals()
loadClaims()
})
// 方法
const loadAnimals = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载动物列表失败')
} finally {
loading.value = false
}
}
const loadClaims = async () => {
claimLoading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载认领记录失败')
} finally {
claimLoading.value = false
}
}
const handleTabChange = (key: string) => {
if (key === 'animals') {
loadAnimals()
} else if (key === 'claims') {
loadClaims()
}
}
const handleSearch = () => {
pagination.current = 1
loadAnimals()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
type: '',
status: ''
})
pagination.current = 1
loadAnimals()
}
const handleClaimSearch = () => {
claimPagination.current = 1
loadClaims()
}
const handleClaimReset = () => {
Object.assign(claimSearchForm, {
keyword: '',
status: ''
})
claimPagination.current = 1
loadClaims()
}
const handleRefresh = () => {
if (activeTab.value === 'animals') {
loadAnimals()
} else {
loadClaims()
}
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadAnimals()
}
const handleViewAnimal = (record: Animal) => {
message.info(`查看动物: ${record.name}`)
}
const handleEditAnimal = (record: Animal) => {
message.info(`编辑动物: ${record.name}`)
}
const handleDeleteAnimal = (record: Animal) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除动物 "${record.name}" 吗?`,
okText: '确定',
okType: 'danger',
onOk: async () => {
try {
message.success('动物已删除')
loadAnimals()
} catch (error) {
message.error('删除失败')
}
}
})
}
const handleApproveClaim = (record: AnimalClaim) => {
Modal.confirm({
title: '确认通过',
content: `确定要通过用户 "${record.user_name}" 的认领申请吗?`,
onOk: async () => {
try {
message.success('认领申请已通过')
loadClaims()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleRejectClaim = (record: AnimalClaim) => {
Modal.confirm({
title: '确认拒绝',
content: `确定要拒绝用户 "${record.user_name}" 的认领申请吗?`,
onOk: async () => {
try {
message.success('认领申请已拒绝')
loadClaims()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleViewClaim = (record: AnimalClaim) => {
message.info(`查看认领详情: ${record.animal_name}`)
}
const showCreateModal = () => {
message.info('新增动物功能开发中')
}
</script>
<style scoped lang="less">
.animal-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<div class="dashboard-container">
<a-page-header
title="仪表板"
sub-title="系统概览和统计数据"
>
<template #extra>
<a-space>
<a-button>刷新</a-button>
<a-button type="primary">导出数据</a-button>
</a-space>
</template>
</a-page-header>
<a-divider />
<!-- 统计卡片 -->
<a-row :gutter="16" class="stats-row">
<a-col :span="6">
<a-card>
<a-statistic
title="总用户数"
:value="11284"
:precision="0"
suffix="人"
>
<template #prefix>
<UserOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="商家数量"
:value="356"
:precision="0"
suffix="家"
>
<template #prefix>
<ShopOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="旅行计划"
:value="1287"
:precision="0"
suffix="个"
>
<template #prefix>
<CompassOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="动物认领"
:value="542"
:precision="0"
suffix="只"
>
<template #prefix>
<HeartOutlined />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" class="chart-row">
<a-col :span="12">
<a-card title="用户增长趋势" class="chart-card">
<div class="chart-placeholder">
<BarChartOutlined />
<p>用户增长图表</p>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="订单统计" class="chart-card">
<div class="chart-placeholder">
<PieChartOutlined />
<p>订单分布图表</p>
</div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" class="activity-row">
<a-col :span="16">
<a-card title="最近活动" class="activity-card">
<a-list item-layout="horizontal" :data-source="recentActivities">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta
:description="item.description"
>
<template #title>
<a>{{ item.title }}</a>
</template>
<template #avatar>
<a-avatar :src="item.avatar" />
</template>
</a-list-item-meta>
<div>{{ item.time }}</div>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="系统信息" class="info-card">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="系统版本">
{{ appVersion }}
</a-descriptions-item>
<a-descriptions-item label="运行环境">
{{ environment }}
</a-descriptions-item>
<a-descriptions-item label="启动时间">
{{ startupTime }}
</a-descriptions-item>
<a-descriptions-item label="内存使用">
256MB / 2GB
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import {
UserOutlined,
ShopOutlined,
CompassOutlined,
HeartOutlined,
BarChartOutlined,
PieChartOutlined
} from '@ant-design/icons-vue'
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
const environment = import.meta.env.MODE || 'development'
const startupTime = new Date().toLocaleString()
const recentActivities = [
{
title: '新用户注册',
description: '用户"旅行爱好者"完成了注册',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=1',
time: '2分钟前'
},
{
title: '旅行计划创建',
description: '用户"探险家"发布了西藏旅行计划',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=2',
time: '5分钟前'
},
{
title: '动物认领',
description: '用户"动物之友"认领了一只羊驼',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=3',
time: '10分钟前'
},
{
title: '订单完成',
description: '花店"鲜花坊"完成了一笔鲜花订单',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=4',
time: '15分钟前'
}
]
</script>
<style scoped>
.dashboard-container {
padding: 24px;
background: #f5f5f5;
min-height: calc(100vh - 64px);
}
.stats-row {
margin-bottom: 24px;
}
.chart-row {
margin-bottom: 24px;
}
.activity-row {
margin-bottom: 24px;
}
.chart-card,
.activity-card,
.info-card {
border-radius: 8px;
}
.chart-placeholder {
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
font-size: 16px;
}
.chart-placeholder .anticon {
font-size: 48px;
margin-bottom: 16px;
color: #1890ff;
}
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-card-body) {
padding: 24px;
}
:deep(.ant-statistic) {
text-align: center;
}
:deep(.ant-statistic-title) {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
:deep(.ant-statistic-content) {
font-size: 24px;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,420 @@
<template>
<div class="merchant-management">
<a-page-header
title="商家管理"
sub-title="管理入驻商家信息"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button type="primary" @click="showCreateModal">
<template #icon>
<ShopOutlined />
</template>
新增商家
</a-button>
</a-space>
</template>
</a-page-header>
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keyword"
placeholder="商家名称/联系人/手机号"
allow-clear
/>
</a-form-item>
<a-form-item label="类型">
<a-select
v-model:value="searchForm.type"
placeholder="全部类型"
style="width: 120px"
allow-clear
>
<a-select-option value="flower_shop">花店</a-select-option>
<a-select-option value="activity_organizer">活动组织</a-select-option>
<a-select-option value="farm_owner">农场主</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
<a-select-option value="disabled">已禁用</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<!-- 商家表格 -->
<a-table
:columns="columns"
:data-source="merchantList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleView(record)">
<EyeOutlined />
查看
</a-button>
<template v-if="record.status === 'pending'">
<a-button size="small" type="primary" @click="handleApprove(record)">
<CheckOutlined />
通过
</a-button>
<a-button size="small" danger @click="handleReject(record)">
<CloseOutlined />
拒绝
</a-button>
</template>
<template v-else-if="record.status === 'approved'">
<a-button size="small" danger @click="handleDisable(record)">
<StopOutlined />
禁用
</a-button>
</template>
<template v-else-if="record.status === 'disabled'">
<a-button size="small" type="primary" @click="handleEnable(record)">
<PlayCircleOutlined />
启用
</a-button>
</template>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ShopOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
CheckOutlined,
CloseOutlined,
StopOutlined,
PlayCircleOutlined
} from '@ant-design/icons-vue'
interface Merchant {
id: number
business_name: string
merchant_type: string
contact_person: string
contact_phone: string
status: string
created_at: string
}
interface SearchForm {
keyword: string
type: string
status: string
}
const loading = ref(false)
const searchForm = reactive<SearchForm>({
keyword: '',
type: '',
status: ''
})
// 模拟商家数据
const merchantList = ref<Merchant[]>([
{
id: 1,
business_name: '鲜花坊',
merchant_type: 'flower_shop',
contact_person: '张经理',
contact_phone: '13800138000',
status: 'approved',
created_at: '2024-01-15'
},
{
id: 2,
business_name: '阳光农场',
merchant_type: 'farm_owner',
contact_person: '李场主',
contact_phone: '13800138001',
status: 'pending',
created_at: '2024-02-20'
}
])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const columns = [
{
title: '商家名称',
dataIndex: 'business_name',
key: 'business_name',
width: 150
},
{
title: '类型',
key: 'type',
width: 100,
align: 'center'
},
{
title: '联系人',
dataIndex: 'contact_person',
key: 'contact_person',
width: 100
},
{
title: '联系电话',
dataIndex: 'contact_phone',
key: 'contact_phone',
width: 120
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '入驻时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center'
}
]
// 类型映射
const getTypeColor = (type: string) => {
const colors = {
flower_shop: 'pink',
activity_organizer: 'green',
farm_owner: 'orange'
}
return colors[type as keyof typeof colors] || 'default'
}
const getTypeText = (type: string) => {
const texts = {
flower_shop: '花店',
activity_organizer: '活动组织',
farm_owner: '农场'
}
return texts[type as keyof typeof texts] || '未知'
}
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
approved: 'green',
rejected: 'red',
disabled: 'default'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string) => {
const texts = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
disabled: '已禁用'
}
return texts[status as keyof typeof texts] || '未知'
}
// 生命周期
onMounted(() => {
loadMerchants()
})
// 方法
const loadMerchants = async () => {
loading.value = true
try {
// TODO: 调用真实API
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载商家列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadMerchants()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
type: '',
status: ''
})
pagination.current = 1
loadMerchants()
}
const handleRefresh = () => {
loadMerchants()
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadMerchants()
}
const handleView = (record: Merchant) => {
message.info(`查看商家: ${record.business_name}`)
}
const handleApprove = async (record: Merchant) => {
Modal.confirm({
title: '确认通过',
content: `确定要通过商家 "${record.business_name}" 的入驻申请吗?`,
onOk: async () => {
try {
message.success('商家入驻申请已通过')
loadMerchants()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleReject = async (record: Merchant) => {
Modal.confirm({
title: '确认拒绝',
content: `确定要拒绝商家 "${record.business_name}" 的入驻申请吗?`,
onOk: async () => {
try {
message.success('商家入驻申请已拒绝')
loadMerchants()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleDisable = async (record: Merchant) => {
Modal.confirm({
title: '确认禁用',
content: `确定要禁用商家 "${record.business_name}" 吗?`,
onOk: async () => {
try {
message.success('商家已禁用')
loadMerchants()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleEnable = async (record: Merchant) => {
Modal.confirm({
title: '确认启用',
content: `确定要启用商家 "${record.business_name}" 吗?`,
onOk: async () => {
try {
message.success('商家已启用')
loadMerchants()
} catch (error) {
message.error('操作失败')
}
}
})
}
const showCreateModal = () => {
message.info('新增商家功能开发中')
}
</script>
<style scoped lang="less">
.merchant-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,437 @@
<template>
<div class="order-management">
<a-page-header title="订单管理" sub-title="管理花束订单和交易记录">
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<a-button type="primary" @click="showStats">
<template #icon><BarChartOutlined /></template>
销售统计
</a-button>
</a-space>
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<a-tab-pane key="orders" tab="订单列表">
<a-card>
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="订单号">
<a-input v-model:value="searchForm.order_no" placeholder="输入订单号" allow-clear />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
<a-select-option value="pending">待支付</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<a-select-option value="shipped">已发货</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
<a-select-option value极速版="refunded">已退款</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="下单时间">
<a-range-picker v-model:value="searchForm.orderTime" :placeholder="['开始时间', '结束时间']" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">重置</a-button>
</a-form-item>
</a-form>
</div>
<a-table
:columns="orderColumns"
:data-source="orderList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'order_no'">
<a-typography-text copyable>{{ record.order_no }}</a-typography-text>
</template>
<template v-else-if="column.key === 'amount'">¥{{ record.amount }}</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
</template>
<template v-else-if="column.key === 'payment_method'">
<span>{{ getPaymentMethodText(record.payment_method) }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleViewOrder(record)">
<EyeOutlined />详情
</a-button>
<template v-if="record.status === 'paid'">
<a-button size="small" type="primary" @click="handleShip(record)">
<CarOutlined />发货
</a-button>
</template>
<template v-if="record.status === 'shipped'">
<a-button size="small" type="primary" @click="handleComplete(record)">
<CheckCircleOutlined />完成
</a-button>
</template>
<template v-if="['pending', 'paid'].includes(record.status)">
<a-button size="small" danger @click="handleCancel(record)">
<极速版CloseOutlined />取消
</a-button>
</template>
<template v-if="record.status === '极速版paid'">
<a-button size="small" danger @click="handleRefund(record)">
<RollbackOutlined />退款
</a-button>
</template>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="statistics" tab="销售统计">
<a-card title="销售数据概览">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic title="今日订单" :value="statistics.today_orders" :precision="0" :value-style="{ color: '#3f8600' }">
<template #prefix><ShoppingOutlined /></template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="今日销售额" :value="statistics.today_sales" :precision="2" prefix="¥" :value-style="{ color: '#cf1322' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月订单" :value="statistics.month_orders" :precision="0" :value-style="{ color: '#1890极速版ff' }" />
</a-col>
<a-col :span="6">
<a-statistic title="本月销售额" :value="statistics.month_sales" :precision="2" prefix="¥" :极速版value-style="{ color: '#722ed1' }" />
</a-col>
</a-row>
</a-card>
<a-card title="销售趋势" style="margin-top: 16px;">
<div style="height: 300px;">
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">
<BarChartOutlined style="font-size: 48px; margin-right: 12px;" />
<span>销售趋势图表开发中</span>
</div>
</div>
</a-card>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
BarChartOutlined,
EyeOutlined,
CarOutlined,
CheckCircleOutlined,
CloseOutlined,
RollbackOutlined,
ShoppingOutlined
} from '@ant-design/icons-vue'
interface Order {
id: number
order_no: string
user_id: number
user_name: string
user_phone: string
amount: number
status: string
payment_method: string
created_at: string
paid_at: string
shipped_at: string
completed_at: string
}
interface SearchForm {
order_no: string
status: string
orderTime: any[]
}
interface Statistics {
today_orders: number
today_sales: number
month_orders: number
month_sales: number
}
const activeTab = ref('orders')
const loading = ref(false)
const searchForm = reactive<SearchForm>({
order_no: '',
status: '',
orderTime: []
})
const statistics = reactive<Statistics>({
today_orders: 0,
today_sales: 0,
month_orders: 0,
month_sales: 0
})
const orderList = ref<Order[]>([
{
id: 1,
order_no: 'ORD202403150001',
user_id: 1001,
user_name: '张先生',
user_phone: '13800138000',
amount: 299.99,
status: 'paid',
payment_method: 'wechat',
created_at: '2024-03-15 10:30:00',
paid_at: '2024-03-15 10:35:00',
shipped_at: '',
completed_at: ''
},
{
id: 2,
order_no: 'ORD202403140002',
user_id: 1002,
user_name: '李女士',
user_phone: '13800138001',
amount: 极速版199.99,
status: 'shipped',
payment_method: 'alipay',
created_at: '2024-03-14 14:20:00',
paid_at: '2024-03-14 14:25:00',
shipped_at: '2024-03-15 09:00:00',
completed_at: ''
}
])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const orderColumns = [
{ title: '订单号', key: 'order_no', width: 160 },
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
{ title: '联系电话', dataIndex: 'user极速版_phone', key: 'user_phone', width: 120 },
{ title: '金额', key: 'amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '支付方式', key: 'payment_method', width: 100, align: 'center' },
{ title: '下单时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
{ title: '操作', key: 'actions', width: 200, align: 'center' }
]
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
paid: 'blue',
shipped: 'green',
completed: 'purple',
cancelled: 'red',
refunded: 'default'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string) => {
const texts = {
pending: '待支付',
paid: '已支付',
shipped: '已发货',
completed: '已完成',
cancelled: '已取消',
refunded: '已退款'
}
return texts[status as keyof typeof texts] || '未知'
}
const getPaymentMethodText = (method: string) => {
const texts = {
wechat: '微信支付',
alipay: '支付宝',
bank: '银行卡',
balance: '余额支付'
}
return texts[method as keyof typeof texts] || '未知'
}
onMounted(() => {
loadOrders()
loadStatistics()
})
const loadOrders = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载订单列表失败')
} finally {
loading.value = false
}
}
const loadStatistics = async () => {
try {
statistics.today_orders = 15
statistics.today_sales = 4500.50
statistics.month_orders = 120
statistics.month_sales = 35600.80
} catch (error) {
message.error('加载统计数据失败')
}
}
const handleTabChange = (key: string) => {
if (key === 'orders') {
loadOrders()
} else if (key === 'statistics') {
loadStatistics()
}
}
const handleSearch = () => {
pagination.current = 1
loadOrders()
}
const handleReset = () => {
Object.assign(searchForm, {
order_no: '',
status: '',
orderTime: []
})
pagination.current = 1
loadOrders()
}
const handleRefresh = () => {
if (activeTab.value === 'orders') {
loadOrders()
} else {
loadStatistics()
}
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange极速版'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadOrders()
}
const handleViewOrder = (record: Order) => {
message.info(`查看订单: ${record.order_no}`)
}
const handleShip = async (record: Order) => {
Modal.confirm({
title: '确认发货',
content: `确定要发货订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
message.success('订单已发货')
loadOrders()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleComplete = async (record: Order) => {
Modal.confirm({
title: '确认完成',
content: `确定要完成订单 "${record.order_no}" 吗?`,
onOk: async () => {
try {
message.success('订单已完成')
loadOrders()
} catch (error) {
message.error('操作失败')
}
极速版 }
})
}
const handleCancel = async (record: Order) => {
Modal.confirm({
title: '确认取消',
content: `确定要取消订单 "${record.order极速版_no}" 吗?`,
on极速版Ok: async () => {
try {
message.success('订单已取消')
loadOrders()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleRefund = async (record: Order) => {
Modal.confirm({
title: '确认退款',
content: `确定要退款订单 "${record.order_no}" 吗?退款金额: ¥${record.amount}`,
onOk: async () => {
try {
message.success('退款申请已提交')
loadOrders()
} catch (error) {
message.error('操作失败')
}
}
})
}
const showStats = () => {
activeTab.value = 'statistics'
loadStatistics()
}
</script>
<style scoped lang="less">
.order-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,573 @@
<template>
<div class="promotion-management">
<a-page-header title="推广管理" sub-title="管理推广活动和奖励">
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<a-button type="primary" @click="showCreateModal">
<template #icon><PlusOutlined /></template>
新建活动
</a-button>
</a-space>
</template>
</a-page-header>
<a-tabs v-model:activeKey="activeTab" @change="handle极速版TabChange">
<a-tab-pane key="activities" tab="推广活动">
<a-card>
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="活动名称">
<a-input v-model:value="searchForm.name" placeholder="输入活动名称" allow-clear />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
<a-select-option value="active">进行中</a-select-option>
<a-select-option value="upcoming">未开始</a-select-极速版option>
<a-select-option value="ended">已结束</a-select-option>
<a-select-option value="paused极速版">已暂停</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="活动时间">
<a-range-picker v-model:value="searchForm.activityTime" :placeholder="['开始极速版时间', '结束时间']" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">重置</a-button>
</a-form-item>
</a-form>
</div>
<a-table
:columns="activityColumns"
:data-source="activityList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
</template>
<template v-else-if="column.key === 'reward_type'">
<span>{{ getRewardTypeText(record.reward_type) }}</span>
</template>
<template v-else-if="column.key === 'reward_amount'">
<span v-if="record.reward_type === 'cash'">¥{{ record.reward_amount }}</span>
<span v-else-if="record.reward_type === 'points'">{{ record.reward_amount }}积分</span>
<span v-else-if="record.reward_type === 'coupon'">{{ record.re极速版ward_amount }}元券</span>
</template>
<template v-else-if="column.key === 'participants'">
<a-progress
:percent="record.current_participants / record.max_participants * 100"
size="small"
:show-info="false"
/>
<div style="font-size: 12px; text-align: center;">
{{ record.current_participants }}/{{ record.max_participants }}
</div>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleViewActivity(record)">
<EyeOutlined />详情
</a-button>
<a-button size="small" @click="handleEditActivity(record)">
<EditOutlined />编辑
</极速版a-button>
<template v-if="record.status === 'active'">
极速版 <a-button size="small" danger @click="handlePauseActivity(record)">
<PauseOutlined />暂停
</a-button>
</template>
<template v-if="record.status === 'paused'">
<a-button size="small" type="primary" @click="handleResumeActivity(record)">
<PlayCircleOutlined />继续
</a-button>
</template>
<a-button size="small" danger @click="handleDeleteActivity(record)">
<DeleteOutlined />删除
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="rewards" tab="奖励记录">
<a-card>
<div class="search-container">
<a-form layout="inline" :model="rewardSearchForm">
<a-form-item label="用户">
<a-input v-model:value="rewardSearchForm.user" placeholder="用户名/手机号" allow-clear />
</a-form-item>
<a-form-item label="奖励类型">
<a-select v-model:value="rewardSearchForm.reward_type" placeholder="全部类型" style="width: 120px" allow-clear>
<a-select-option value="cash">现金</a-select-option>
<a-select-option value="points">积分</a-select-option>
<a-select-option value="coupon">优惠券</a-select-option>
</a-select>
</极速版a-form-item>
<a-form-item label="状态">
<a-select v-model:value="rewardSearchForm.status" placeholder="全部状态" style="width: 120px" allow-clear>
<a-select-option value="pending">待发放</a-select-option>
<a-select-option value="issued">已发放</a-select-option>
<a-select-option value="failed">发放失败</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleRewardSearch">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleRewardReset">重置</a-button>
</a-form-item>
</a-form>
</div>
<a-table
:columns="rewardColumns"
:data-source="rewardList"
:loading="rewardLoading"
:pagination="rewardPagination"
:极速版row-key="record => record.id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'reward_type'">
<span>{{ getRewardTypeText(record.reward_type) }}</span>
</template>
<template v-else-if="column.key === 'reward_amount'">
<span v-if="record.reward_type === 'cash'">¥{{ record.reward_amount }}</span>
<span v-else-if="record.reward_type === 'points'">{{ record.reward_amount }}积分</span>
<span v-else-if="record.reward_type === 'coupon'">{{ record.reward_amount }}元券</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getRewardStatusColor(record.status)">{{ getRewardStatusText(record.status) }}</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<template v-if="record.status === 'pending'">
<a-button size="small" type="primary" @click="handleIssueReward(record)">
<CheckOutlined />发放
</a-button>
</template>
<a-button size="small" @click="handleViewRew极速版ard(record)">
<EyeOutlined />详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a极速版-tab-pane>
</a-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
PlusOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
PauseOutlined,
PlayCircleOutlined,
CheckOutlined
} from '@ant-design/icons-vue'
interface PromotionActivity {
id: number
name: string
description: string
reward_type: string极速版
reward_amount: number
status: string
start_time: string
end_time: string
max_participants:极速版 number
current_participants: number
created_at: string
}
interface RewardRecord {
id: number
user_id: number
user_name: string
user_phone: string
activity_id: number
activity_name: string
reward_type: string
reward_amount: number
status: string
issued_at: string
created_at: string
}
interface SearchForm {
name: string
status: string
activityTime: any[]
}
interface RewardSearchForm {
user: string
reward_type: string
status: string
}
const activeTab = ref('activities')
const loading = ref(false)
const rewardLoading = ref(false)
const searchForm = reactive<SearchForm>({
name: '',
status: '',
activityTime: []
})
const rewardSearchForm = reactive<RewardSearchForm>({
user: '',
reward_type: '',
status: ''
})
const activityList = ref<PromotionActivity[]>([
{
id: 1,
name: '邀请好友得现金',
description: '邀请好友注册即可获得现金奖励',
reward_type: 'cash',
reward_amount: 10,
status: 'active',
start_time: '极速版2024-03-01',
end_time: '2024-03-31',
max_participants: 1000极速版,
current_participants: 356,
created_at: '2024-02-20'
},
{
id: 2,
name: '分享活动得积分',
description: '分享活动页面即可获得积分奖励',
reward_type: 'points',
reward_amount: 100,
status: 'upcoming',
start_time: '2024-04-01',
end_time: '202极速版4-04-30',
max_participants: 500,
current_p极速版articipants: 0,
created_at: '2024-03-10'
}
])
const rewardList = ref<RewardRecord[]>([
{
id: 1,
user_id: 1001,
user_name: '张先生',
user_phone: '13800138000',
activity_id: 1,
activity_name: '邀请好友极速版得现金',
reward_type: 'cash',
reward_amount: 10,
status: 'issued',
issued_at: '2024-03-05 14:30:00',
created_at: '2024-03-05 14:25:00'
}
])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const rewardPagination = reactive({
current: 1,
pageSize: 20,
total: 30,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const activityColumns = [
{ title: '活动名称', dataIndex: 'name', key: '极速版name', width: 150 },
{ title极速版: '奖励类型', key: 'reward_type', width: 100, align: 'center' },
{ title: '奖励金额', key: 'reward_amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '活动时间', key: 'time', width: 200,
customRender: ({ record }: { record: PromotionActivity }) =>
`${record.start_time} 至 ${record.end_time}`
},
{ title: '参与人数', key: 'participants', width: 120, align: 'center' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 120 },
{ title: '操作', key: 'actions', width: 200, align: 'center' }
]
const rewardColumns = [
{ title: '用户', dataIndex: 'user_name', key: 'user_name', width: 100 },
{ title: '联系电话', dataIndex: 'user_phone', key: 'user_phone', width: 120 },
{ title: '活动名称', dataIndex: 'activity_name', key: 'activity_name', width: 150 },
{ title: '奖励类型', key: 'reward_type', width: 100, align: 'center极速版' },
{ title: '奖励金额', key: 'reward_amount', width: 100, align: 'center' },
{ title: '状态', key: 'status', width: 100, align: 'center' },
{ title: '发放时间', dataIndex: 'issued_at极速版', key: 'issued_at', width: 极速版150 },
{ title: '申请时间', dataIndex: 'created_at', key: 'created_at', width: 150 },
{ title: '操作', key: 'actions', width: 120, align: 'center' }
]
const getStatusColor = (status: string) => {
const colors = {
active: 'green',
upcoming: 'blue',
ended: 'default',
paused: 'orange'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string)极速版 {
const texts = {
active: '进行中',
upcoming: '未开始',
ended: '已结束',
paused: '已暂停'
}
return texts[status as keyof typeof texts] || '未知'
}
const getRewardTypeText = (type: string) => {
const texts = {
cash: '现金',
points: '积分',
coupon: '优惠券'
}
return texts[type as keyof typeof texts] || '未知'
}
const getRewardStatusColor = (status: string) => {
const colors = {
pending极速版: 'orange',
issued: 'green',
failed: 'red'
}
return colors[status as keyof typeof colors] || 'default'
}
const getRewardStatusText = (status: string) => {
const texts = {
pending: '待发放',
issued: '已发放',
failed: '发放失败'
}
return texts[status as keyof typeof texts] || '未知'
}
onMounted(() => {
loadActivities()
loadRewards()
})
const loadActivities = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载活动列表失败')
} finally {
loading.value = false
}
}
const loadRewards = async () => {
rewardLoading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载奖励记录失败')
} finally {
rewardLoading.value = false
}
}
const handleTabChange = (key: string) => {
if (key === 'activities') {
loadActivities()
} else if (key === 'rewards') {
loadRewards()
极速版 }
}
const handleSearch = () => {
pagination.current = 1
loadActivities()
}
const handleReset = () => {
Object.assign(searchForm, {
name: '',
status: '',
activityTime: []
})
pagination.current = 1极速版
loadActivities()
}
const handleRewardSearch = () => {
rewardPagination.current = 1
loadRewards()
}
const handleRewardReset = () => {
Object.assign(rewardSearchForm, {
user: '',
reward_type: '',
status: ''
})
rewardPagination.current = 1
loadRewards()
}
const handleRefresh = () => {
if (activeTab.value === 'activities') {
loadActivities()
} else {
loadRewards()
}
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadActivities()
}
const handleViewActivity = (record: PromotionActivity) => {
message.info(`查看活动: ${record.name}`)
}
const handleEditActivity = (record: PromotionActivity) => {
message.info(`编辑活动: ${record.name极速版}`)
}
const handlePauseActivity = async (record: PromotionActivity) => {
Modal.confirm({
title: '确认暂停',
content: `确定要暂停活动 "${record.name}" 极速版吗?`,
onOk: async () => {
try {
message.success('活动已暂停')
loadActivities()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleResumeActivity = async (record: PromotionActivity) => {
Modal.confirm({
title: '确认继续',
content: `确定要继续活动 "${record.name}" 吗?`,
onOk: async () => {
try {
message.success('活动已继续')
loadActivities()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleDeleteActivity = async (record: PromotionActivity) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除活动 "${record.name}" 吗?`,
okText: '确定',
okType: 'danger',
onOk: async () => {
try {
message.success('活动已删除')
loadActivities()
} catch (error) {
message.error('删除失败')
}
}
})
}
const handleIssueReward = async (record: RewardRecord) => {
Modal.confirm({
title: '确认发放',
content: `确定要发放奖励给用户 "${record.user_name}" 吗?`,
onOk: async ()极速版 => {
try {
message.success('奖励已发放')
loadRewards()
极速版 } catch (error) {
message.error('操作失败')
}
}
})
}
const handleViewReward = (record: RewardRecord) => {
message.info(`查看奖励记录: ${record.user_name}`)
}
const showCreateModal = () => {
message.info('新建活动功能开发中')
}
</script>
<style scoped lang="less">
.promotion-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,289 @@
<template>
<div class="system-management">
<a-page-header title="系统管理" sub-title="管理系统设置和配置">
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<ReloadOutlined />
刷新
</a-button>
</a-space>
</template>
</a-page-header>
<a-row :gutter="16">
<a-col :span="8">
<a-card title="系统信息" size="small">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="系统版本">v1.0.0</a-descriptions-item>
<a-descriptions-item label="运行环境">Production</a-descriptions-item>
<极速版a-descriptions-item label="启动时间">2024-03-15 10:00:00</a-descriptions-item>
<a-descriptions-item label="运行时长">12天3小时</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="数据库状态" size="small">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="连接状态">
<a-tag color="green">正常</a-tag>
</a-descriptions-item>
<a-descriptions-item label极速版="数据库类型">MySQL</a-descriptions-item>
<a-descriptions-item label="连接数">15极速版/100</a-descriptions-item>
<a-descriptions-item label="查询次数">1,234/分钟</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="缓存状态" size="small">
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="Redis状态">
<a-tag color="green">正常</a-tag>
</a-descriptions-item>
<a-descriptions-item label="内存使用">65%</a-descriptions-item>
<a-descriptions-item label="命中率">92%</a-descriptions-item>
<a-descriptions-item label="键数量">1,234</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px;">
<a-col :span="12">
<a-card title="服务监控" size="small">
<a-list item-layout="horizontal" :data-source="services">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :description="item.description">
<template #title>
<a-space>
<a-tag :color="item.status === 'running' ? 'green' : 'red'">
{{ item.status === 'running' ? '运行中' : '停止' }}
</a-tag>
{{ item.name }}
</a-space>
</template>
<template #avatar>
<a-avatar :style="{ backgroundColor: item.status === 'running' ? '#52c41a' : '#ff4d4f' }">
<DatabaseOutlined v-if="item.type === 'database'" />
<CloudServerOutlined v-if="item.type === 'cache'" />
<MessageOutlined v-if="item.type === 'mq'" />
</a-avatar>
</template>
</a-list-item-meta>
<template #actions>
<a-button size="small" v-if="item.status === 'running'" danger @click="handleStopService(item)">
停止
</a-button>
<a-button size="small" v-if="item.status === 'stopped'" type="primary" @click="handleStartService(item)">
启动
</a-button>
</template>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="系统日志" size="small">
<a-timeline>
<a-t极速版imeline-item color="green">
<p>用户登录成功 - admin (2024-03-15 14:30:22)</p>
</a-timeline-item>
<a-timeline-item color="blue">
<p>数据库备份完成 - 备份文件: backup_20240315.sql (2024-03-15 14:00:00)</p>
</a-timeline-item>
<a-timeline-item color="orange">
<p>系统警告 - 内存使用率超过80% (2024-03-15 13:极速版45:18)</极速版p>
</a-timeline-item>
<a-timeline-item color="green">
<p>定时任务执行 - 清理过期日志 (2024-03-15 13:30:00)</p>
</a-timeline-item>
</a-timeline>
<div style="text-align: center; margin-top: 16px;">
<a-button type="link" @click="viewLogs">查看完整日志</a-button>
</div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px;">
<a-col :span="24">
<a-card title="系统极速版设置" size="small">
<a-form :model="systemSettings" layout="vertical">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="系统名称">
<a-input v-model:value="systemSettings.systemName" placeholder="请输入系统名称" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="系统版本">
<a-input v-model:value="systemSettings.systemVersion" placeholder="请输入系统版本极速版" />
</a-form-item>
</极速版a-col>
<a-col :span="8">
<a-form-item label="维护模式">
<a-switch v-model:checked="systemSettings.maintenanceMode" />
</极速版a-form-item>
</a-col>
</a-row>
<a-row :gutter="极速版16">
<a-col :span="8">
<a-form-item label="会话超时(分钟)">
<a-input-number v-model:value="systemSettings.sessionTimeout" :min="5" :max="480" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="每页显示数量">
<a-input-number v-model:value="systemSettings.pageSize" :min="10" :max="100" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="启用API文档">
<a-switch v-model:checked="systemSettings.enableSwagger" />
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-button type="primary" @click="saveSettings">保存设置</a-button>
<a-button style="margin-left: 8px" @click="resetSettings">重置</a-button>
</a-form-item>
</a-form>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
ReloadOutlined,
DatabaseOutlined,
CloudServerOutlined,
MessageOutlined
} from '@ant-design/icons-vue'
interface Service {
id: number
name: string
type: string
description: string
status: string
}
interface SystemSettings {
systemName: string
systemVersion: string
maintenanceMode: boolean
sessionTimeout: number
pageSize: number
enableSwagger: boolean
}
const services = ref<Service[]>([
{
id: 1,
name: 'MySQL数据库',
type: 'database',
description: '主数据库服务',
status: 'running'
},
{
id: 2,
name: 'Redis缓存',
type: 'cache',
description: '缓存服务',
status: 'running'
},
{
id: 3,
name: 'RabbitMQ',
type: 'mq',
description: '消息队列服务',
status: 'stopped'
}
])
const systemSettings = reactive<SystemSettings>({
systemName: '结伴客管理系统',
systemVersion: 'v1.0.0',
maintenanceMode: false,
sessionTimeout: 30,
pageSize: 20,
enableSwagger: true
})
const handleRefresh = () => {
message.success('系统状态已刷新')
}
const handleStopService = (service: Service极速版) => {
Modal.confirm({
title: '确认停止',
content: `确定要停止服务 "${service.name}" 吗?`,
onOk: async () => {
try {
service.status = 'stopped'
message.success('服务已停止')
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleStartService = (service: Service) => {
Modal.confirm({
title: '确认启动',
content: `确定要启动服务 "${service.name}" 吗?`,
onOk: async () => {
try {
service.status = 'running'
message.success('服务已启动')
} catch (error) {
message.error('操作失败')
}
}
})
}
const viewLogs = () => {
message.info('查看系统日志功能开发中')
}
const saveSettings = () => {
message.success('系统设置已保存')
}
const resetSettings = () => {
Object.assign(systemSettings, {
systemName: '结伴客管理系统',
systemVersion: 'v1.0.极速版0',
maintenanceMode: false,
sessionTimeout: 30,
pageSize: 极速版20,
enableSwagger: true
})
message.success('设置已重置')
}
</script>
<style scoped lang="less">
.system-management {
.ant-card {
margin-bottom: 16px;
}
.ant-descriptions-item-label {
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,382 @@
<template>
<div class="travel-management">
<a-page-header
title="旅行管理"
sub-title="管理旅行计划和匹配"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button type="primary" @click="showStats">
<template #icon>
<BarChartOutlined />
</template>
数据统计
</a-button>
</a-space>
</template>
</a-page-header>
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="目的地">
<a-input
v-model:value="searchForm.destination"
placeholder="输入目的地"
allow-clear
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="recruiting">招募中</a-select-option>
<a-select-option value="full">已满员</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="旅行时间">
<a-range-picker
v-model:value="searchForm.travelTime"
:placeholder="['开始时间', '结束时间']"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<!-- 旅行计划表格 -->
<a-table
:columns="columns"
:data-source="travelList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'destination'">
<strong>{{ record.destination }}</strong>
<div style="font-size: 12px; color: #666;">
{{ record.start_date }} 至 {{ record.end_date }}
</div>
</template>
<template v-else-if="column.key === 'budget'">
¥{{ record.budget }}
</template>
<template v-else-if="column.key === 'members'">
<a-progress
:percent="(record.current_members / record.max_members) * 100"
size="small"
:show-info="false"
/>
<div style="font-size: 12px; text-align: center;">
{{ record.current_members }}/{{ record.max_members }}
</div>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleView(record)">
<EyeOutlined />
详情
</a-button>
<a-button size="small" @click="handleMembers(record)">
<TeamOutlined />
成员
</a-button>
<template v-if="record.status === 'recruiting'">
<a-button size="small" type="primary" @click="handlePromote(record)">
<RocketOutlined />
推广
</a-button>
<a-button size="small" danger @click="handleClose(record)">
<CloseOutlined />
关闭
</a-button>
</template>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import type { TableProps } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
BarChartOutlined,
EyeOutlined,
TeamOutlined,
RocketOutlined,
CloseOutlined
} from '@ant-design/icons-vue'
interface TravelPlan {
id: number
destination: string
start_date: string
end_date: string
budget: number
max_members: number
current_members: number
status: string
creator: string
created_at: string
}
interface SearchForm {
destination: string
status: string
travelTime: any[]
}
const loading = ref(false)
const searchForm = reactive<SearchForm>({
destination: '',
status: '',
travelTime: []
})
// 模拟旅行数据
const travelList = ref<TravelPlan[]>([
{
id: 1,
destination: '西藏',
start_date: '2024-07-01',
end_date: '2024-07-15',
budget: 5000,
max_members: 6,
current_members: 3,
status: 'recruiting',
creator: '旅行爱好者',
created_at: '2024-06-01'
},
{
id: 2,
destination: '云南',
start_date: '2024-08-10',
end_date: '2024-08-20',
budget: 3000,
max_members: 4,
current_members: 4,
status: 'full',
creator: '探险家',
created_at: '2024-07-15'
}
])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const columns = [
{
title: '旅行信息',
key: 'destination',
width: 200
},
{
title: '预算',
key: 'budget',
width: 100,
align: 'center'
},
{
title: '成员',
key: 'members',
width: 120,
align: 'center'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '创建者',
dataIndex: 'creator',
key: 'creator',
width: 100
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center'
}
]
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
recruiting: 'blue',
full: 'green',
completed: 'purple',
cancelled: 'red'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string) => {
const texts = {
recruiting: '招募中',
full: '已满员',
completed: '已完成',
cancelled: '已取消'
}
return texts[status as keyof typeof texts] || '未知'
}
// 生命周期
onMounted(() => {
loadTravelPlans()
})
// 方法
const loadTravelPlans = async () => {
loading.value = true
try {
// TODO: 调用真实API
await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) {
message.error('加载旅行计划失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 极速版1
loadTravelPlans()
}
const handleReset = () => {
Object.assign(searchForm, {
destination: '',
status: '',
travelTime: []
})
pagination.current = 1
loadTravelPlans()
}
const handleRefresh = () => {
loadTravelPlans()
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadTravelPlans()
}
const handleView = (record: TravelPlan) => {
message.info(`查看旅行计划: ${record.destination}`)
}
const handleMembers = (record: TravelPlan) => {
message.info(`查看成员: ${record.destination}`)
}
const handlePromote = (record: TravelPlan) => {
Modal.confirm({
title: '确认推广',
content: `确定要推广旅行计划 "${record.destination}" 吗?`,
onOk: async () => {
try {
message.success('旅行计划已推广')
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleClose = (record: TravelPlan) => {
Modal.confirm({
title: '确认关闭',
content: `确定要关闭旅行计划 "${record.destination}" 吗?`,
onOk: async () => {
try {
message.success('旅行计划已关闭')
loadTravelPlans()
} catch (error) {
message.error('操作失败')
}
}
})
}
const showStats = () => {
message.info('数据统计功能开发中')
}
</script>
<style scoped lang="less">
.travel-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,650 @@
<template>
<div class="user-management">
<a-page-header
title="用户管理"
sub-title="管理系统用户信息"
>
<template #extra>
<a-space>
<a-button @click="handleRefresh">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
<a-button type="primary" @click="showCreateModal">
<template #icon>
<UserAddOutlined />
</template>
新增用户
</a-button>
</a-space>
</template>
</a-page-header>
<a-card>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm">
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keyword"
placeholder="用户名/昵称/手机号"
allow-clear
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
style="width: 120px"
allow-clear
>
<a-select-option value="active">正常</a-select-option>
<a-select-option value="inactive">禁用</a-select-option>
<a-select-option value="banned">封禁</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="注册时间">
<a-range-picker
v-model:value="searchForm.registerTime"
:placeholder="['开始时间', '结束时间']"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-form>
</div>
<!-- 用户表格 -->
<a-table
:columns="columns"
:data-source="userList"
:loading="loading"
:pagination="pagination"
:row-key="record => record.id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'avatar'">
<a-avatar :src="record.avatar" :size="32">
{{ record.nickname?.charAt(0) }}
</a-avatar>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'level'">
<a-tag color="blue">Lv.{{ record.level }}</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-space :size="8">
<a-button size="small" @click="handleView(record)">
<EyeOutlined />
查看
</a-button>
<a-button size="small" @click="handleEdit(record)">
<EditOutlined />
编辑
</a-button>
<a-dropdown>
<a-button size="small">
更多
<DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item
v-if="record.status === 'active'"
@click="handleDisable(record)"
>
<StopOutlined />
禁用
</a-menu-item>
<a-menu-item
v-else
@click="handleEnable(record)"
>
<PlayCircleOutlined />
启用
</a-menu-item>
<a-menu-item
v-if="record.status !== 'banned'"
@click="handleBan(record)"
>
<WarningOutlined />
封禁
</a-menu-item>
<a-menu-divider />
<a-menu-item
danger
@click="handleDelete(record)"
>
<DeleteOutlined />
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 创建/编辑用户模态框 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
width="600px"
:confirm-loading="modalLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
ref="formRef"
:model="formState"
:rules="formRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户名" name="username">
<a-input
v-model:value="formState.username"
placeholder="请输入用户名"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="昵称" name="nickname">
<a-input
v-model:value="formState.nickname"
placeholder="请输入昵称"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="密码" name="password" v-if="isCreate">
<a-input-password
v-model:value="formState.password"
placeholder="请输入密码"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="手机号" name="phone">
<a-input
v-model:value="formState.phone"
placeholder="请输入手机号"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="邮箱" name="email">
<a-input
v-model:value="formState.email"
placeholder="请输入邮箱"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status" placeholder="请选择状态">
<a-select-option value="active">正常</a-select-option>
<a-select-option value="inactive">禁用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="备注" name="remark">
<a-textarea
v-model:value="formState.remark"
placeholder="请输入备注信息"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { message, Modal, type FormInstance } from 'ant-design-vue'
import {
UserAddOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
StopOutlined,
PlayCircleOutlined,
WarningOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import type { TableProps } from 'ant-design-vue'
interface User {
id: number
username: string
nickname: string
avatar: string
email: string
phone: string
status: string
level: number
points: number
created_at: string
updated_at: string
}
interface SearchForm {
keyword: string
status: string
registerTime: any[]
}
interface FormState {
username: string
nickname: string
password: string
email: string
phone: string
status: string
remark: string
}
const loading = ref(false)
const modalVisible = ref(false)
const modalLoading = ref(false)
const formRef = ref<FormInstance>()
const editingUser = ref<User | null>(null)
const searchForm = reactive<SearchForm>({
keyword: '',
status: '',
registerTime: []
})
const formState = reactive<FormState>({
username: '',
nickname: '',
password: '',
email: '',
phone: '',
status: 'active',
remark: ''
})
const formRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 4, max: 20, message: '用户名长度为4-20个字符', trigger: 'blur' }
],
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6个字符', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
]
}
// 模拟用户数据
const userList = ref<User[]>([
{
id: 1,
username: 'admin',
nickname: '系统管理员',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=admin',
email: 'admin@jiebanke.com',
phone: '13800138000',
status: 'active',
level: 10,
points: 10000,
created_at: '2024-01-01',
updated_at: '2024-01-01'
},
{
id: 2,
username: 'user001',
nickname: '旅行爱好者',
avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=user1',
email: 'user001@example.com',
phone: '13800138001',
status: 'active',
level: 3,
points: 1500,
created_at: '2024-02-15',
updated_at: '2024-02-15'
}
])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 50,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条记录`
})
const columns = [
{
title: '用户',
key: 'avatar',
width: 60,
align: 'center'
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 120
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
width: 120
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
width: 150
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120
},
{
title: '等级',
key: 'level',
width: 80,
align: 'center'
},
{
title: '积分',
dataIndex: 'points',
key: 'points',
width: 80,
align: 'center'
},
{
title: '状态',
key: 'status',
width: 80,
align: 'center'
},
{
title: '注册时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center'
}
]
const modalTitle = computed(() => editingUser.value ? '编辑用户' : '新增用户')
const isCreate = computed(() => !editingUser.value)
// 状态映射
const getStatusColor = (status: string) => {
const colors = {
active: 'green',
inactive: 'orange',
banned: 'red'
}
return colors[status as keyof typeof colors] || 'default'
}
const getStatusText = (status: string) => {
const texts = {
active: '正常',
inactive: '禁用',
banned: '封禁'
}
return texts[status as keyof typeof texts] || '未知'
}
// 生命周期
onMounted(() => {
loadUsers()
})
// 方法
const loadUsers = async () => {
loading.value = true
try {
// TODO: 调用真实API
// const response = await userAPI.getUsers({
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm
// })
await new Promise(resolve => setTimeout(resolve, 500))
// userList.value = response.data.users
// pagination.total = response.data.pagination.total
} catch (error) {
message.error('加载用户列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadUsers()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
status: '',
registerTime: []
})
pagination.current = 1
loadUsers()
}
const handleRefresh = () => {
loadUsers()
message.success('数据已刷新')
}
const handleTableChange: TableProps['onChange'] = (pag) => {
pagination.current = pag.current!
pagination.pageSize = pag.pageSize!
loadUsers()
}
const showCreateModal = () => {
editingUser.value = null
Object.assign(formState, {
username: '',
nickname: '',
password: '',
email: '',
phone: '',
status: 'active',
remark: ''
})
modalVisible.value = true
}
const handleView = (record: User) => {
// TODO: 跳转到用户详情页
message.info(`查看用户: ${record.nickname}`)
}
const handleEdit = (record: User) => {
editingUser.value = record
Object.assign(formState, {
username: record.username,
nickname: record.nickname,
password: '',
email: record.email,
phone: record.phone,
status: record.status,
remark: ''
})
modalVisible.value = true
}
const handleDisable = async (record: User) => {
Modal.confirm({
title: '确认禁用',
content: `确定要禁用用户 "${record.nickname}" 吗?`,
onOk: async () => {
try {
// await userAPI.updateUser(record.id, { status: 'inactive' })
message.success('用户已禁用')
loadUsers()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleEnable = async (record: User) => {
Modal.confirm({
title: '确认启用',
content: `确定要启用用户 "${record.nickname}" 吗?`,
onOk: async () => {
try {
// await userAPI.updateUser(record.id, { status: 'active' })
message.success('用户已启用')
loadUsers()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleBan = async (record: User) => {
Modal.confirm({
title: '确认封禁',
content: `确定要封禁用户 "${record.nickname}" 吗?`,
onOk: async () => {
try {
// await userAPI.updateUser(record.id, { status: 'banned' })
message.success('用户已封禁')
loadUsers()
} catch (error) {
message.error('操作失败')
}
}
})
}
const handleDelete = async (record: User) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除用户 "${record.nickname}" 吗?此操作不可恢复。`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
// await userAPI.deleteUser(record.id)
message.success('用户已删除')
loadUsers()
} catch (error) {
message.error('删除失败')
}
}
})
}
const handleModalOk = async () => {
try {
await formRef.value?.validate()
modalLoading.value = true
if (editingUser.value) {
// 编辑用户
// await userAPI.updateUser(editingUser.value.id, formState)
message.success('用户信息更新成功')
} else {
// 创建用户
// await userAPI.createUser(formState)
message.success('用户创建成功')
}
modalVisible.value = false
loadUsers()
} catch (error) {
console.error('表单验证失败:', error)
} finally {
modalLoading.value = false
}
}
const handleModalCancel = () => {
modalVisible.value = false
}
</script>
<style scoped lang="less">
.user-management {
.search-container {
margin-bottom: 16px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
:deep(.ant-form-item) {
margin-bottom: 16px;
}
}
}
:deep(.ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,142 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
// 基础路由
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/dashboard',
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('@/pages/Login.vue'),
meta: { requiresAuth: false, layout: 'auth' }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/pages/dashboard/index.vue'),
meta: {
requiresAuth: true,
title: '仪表板',
icon: 'DashboardOutlined'
}
},
{
path: '/users',
name: 'Users',
component: () => import('@/pages/user/index.vue'),
meta: {
requiresAuth: true,
title: '用户管理',
icon: 'UserOutlined',
permissions: ['user:read']
}
},
{
path: '/merchants',
name: 'Merchants',
component: () => import('@/pages/merchant/index.vue'),
meta: {
requiresAuth: true,
title: '商家管理',
icon: 'ShopOutlined',
permissions: ['merchant:read']
}
},
{
path: '/travel',
name: 'Travel',
component: () => import('@/pages/travel/index.vue'),
meta: {
requiresAuth: true,
title: '旅行管理',
icon: 'CompassOutlined',
permissions: ['travel:read']
}
},
{
path: '/animals',
name: 'Animals',
component: () => import('@/pages/animal/index.vue'),
meta: {
requiresAuth: true,
title: '动物管理',
icon: 'HeartOutlined',
permissions: ['animal:read']
}
},
{
path: '/orders',
name: 'Orders',
component: () => import('@/pages/order/index.vue'),
meta: {
requiresAuth: true,
title: '订单管理',
icon: 'ShoppingCartOutlined',
permissions: ['order:read']
}
},
{
path: '/promotion',
name: 'Promotion',
component: () => import('@/pages/promotion/index.vue'),
meta: {
requiresAuth: true,
title: '推广管理',
icon: 'GiftOutlined',
permissions: ['promotion:read']
}
},
{
path: '/system',
name: 'System',
component: () => import('@/pages/system/index.vue'),
meta: {
requiresAuth: true,
title: '系统设置',
icon: 'SettingOutlined',
permissions: ['system:read']
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/pages/NotFound.vue'),
meta: { requiresAuth: false }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
const { meta } = to
const isAuthenticated = localStorage.getItem('admin_token') !== null
// 检查是否需要认证
if (meta.requiresAuth && !isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
// 如果已认证且访问登录页,重定向到首页
if (to.name === 'Login' && isAuthenticated) {
next({ name: 'Dashboard' })
return
}
next()
})
// 错误处理
router.onError((error) => {
console.error('🚨 路由错误:', error)
})
export default router

View File

@@ -0,0 +1,127 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface AppState {
// 应用配置
name: string
version: string
// 用户信息
user: any
// 系统状态
loading: boolean
error: string | null
// 功能开关
features: {
analytics: boolean
debug: boolean
}
}
export const useAppStore = defineStore('app', () => {
// 状态
const state = ref<AppState>({
name: import.meta.env.VITE_APP_NAME || '结伴客后台管理系统',
version: import.meta.env.VITE_APP_VERSION || '1.0.0',
user: null,
loading: false,
error: null,
features: {
analytics: import.meta.env.VITE_FEATURE_ANALYTICS === 'true',
debug: import.meta.env.VITE_FEATURE_DEBUG === 'true'
}
})
// Getters
const isAuthenticated = () => !!state.value.user
const isLoading = () => state.value.loading
const hasError = () => state.value.error !== null
// Actions
const initializeApp = async () => {
state.value.loading = true
try {
// 初始化应用配置
console.log('🚀 初始化应用:', state.value.name, state.value.version)
// 检查用户登录状态
await checkAuthStatus()
// 初始化分析工具(如果启用)
if (state.value.features.analytics) {
initAnalytics()
}
state.value.error = null
} catch (error) {
state.value.error = error instanceof Error ? error.message : '初始化失败'
console.error('❌ 应用初始化失败:', error)
} finally {
state.value.loading = false
}
}
const checkAuthStatus = async () => {
try {
// 检查本地存储的token
const token = localStorage.getItem('admin_token')
if (token) {
// TODO: 验证token有效性
// const user = await authAPI.verifyToken(token)
// state.value.user = user
console.log('✅ 用户已登录')
} else {
console.log(' 用户未登录')
}
} catch (error) {
console.warn('⚠️ 认证状态检查失败:', error)
// 清除无效的token
localStorage.removeItem('admin_token')
}
}
const setUser = (user: any) => {
state.value.user = user
}
const setLoading = (loading: boolean) => {
state.value.loading = loading
}
const setError = (error: string | null) => {
state.value.error = error
}
const clearError = () => {
state.value.error = null
}
const logout = () => {
state.value.user = null
localStorage.removeItem('admin_token')
// TODO: 调用退出接口
}
// 私有方法
const initAnalytics = () => {
if (state.value.features.analytics) {
console.log('📊 初始化分析工具')
// TODO: 集成分析工具
}
}
return {
// 状态
state,
// Getters
isAuthenticated,
isLoading,
hasError,
// Actions
initializeApp,
setUser,
setLoading,
setError,
clearError,
logout
}
})

View File

@@ -0,0 +1,8 @@
export { useAppStore } from './app'
// 导出其他store模块
// export { useUserStore } from './user'
// export { useTravelStore } from './travel'
// export { useAnimalStore } from './animal'
// export { useMerchantStore } from './merchant'
// export { useOrderStore } from './order'
// export { usePromotionStore } from './promotion'

View File

@@ -0,0 +1,322 @@
/* 全局样式重置 */
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
font-size: 14px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.85);
background-color: #f5f5f5;
}
#app {
min-height: 100vh;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 通用工具类 */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.mb-0 {
margin-bottom: 0;
}
.mb-8 {
margin-bottom: 8px;
}
.mb-16 {
margin-bottom: 16px;
}
.mb-24 {
margin-bottom: 24px;
}
.mt-0 {
margin-top: 0;
}
.mt-8 {
margin-top: 8px;
}
.mt-16 {
margin-top: 16px;
}
.mt-24 {
margin-top: 24px;
}
.p-0 {
padding: 0;
}
.p-8 {
padding: 8px;
}
.p-16 {
padding: 16px;
}
.p-24 {
padding: 24px;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease-out;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(20px);
}
/* 响应式断点 */
@media (max-width: 576px) {
.hidden-xs {
display: none !important;
}
}
@media (max-width: 768px) {
.hidden-sm {
display: none !important;
}
}
@media (max-width: 992px) {
.hidden-md {
display: none !important;
}
}
@media (max-width: 1200px) {
.hidden-lg {
display: none !important;
}
}
/* 自定义Ant Design样式覆盖 */
.ant-layout {
background: #f5f5f5;
}
.ant-layout-header {
padding: 0 24px;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
z-index: 1;
}
.ant-layout-sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
}
.ant-layout-content {
padding: 24px;
min-height: calc(100vh - 64px);
}
.ant-card {
border-radius: 8px;
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12),
0 5px 12px 4px rgba(0, 0, 0, 0.09);
}
.ant-card-head {
border-bottom: 1px solid #f0f0f0;
min-height: 48px;
}
.ant-card-head-title {
font-weight: 600;
}
.ant-table {
border-radius: 8px;
overflow: hidden;
}
.ant-table-thead > tr > th {
background: #fafafa;
font-weight: 600;
}
.ant-btn {
border-radius: 6px;
font-weight: 400;
}
.ant-btn-lg {
height: 40px;
padding: 0 16px;
font-size: 16px;
}
.ant-input,
.ant-input-affix-wrapper {
border-radius: 6px;
}
.ant-select-selector {
border-radius: 6px !important;
}
.ant-modal {
border-radius: 8px;
overflow: hidden;
}
.ant-modal-header {
border-bottom: 1px solid #f0f0f0;
border-radius: 8px 8px 0 0;
}
.ant-modal-footer {
border-top: 1px solid #f0f0f0;
border-radius: 0 0 8px 8px;
}
/* 自定义主题色 */
:root {
--primary-color: #1890ff;
--success-color: #52c41a;
--warning-color: #faad14;
--error-color: #ff4d4f;
--info-color: #1890ff;
}
/* 暗色主题支持 */
[data-theme='dark'] {
--primary-color: #177ddc;
--success-color: #49aa19;
--warning-color: #d89614;
--error-color: #a61d24;
--info-color: #177ddc;
}
/* 打印样式 */
@media print {
.no-print {
display: none !important;
}
.print-break {
page-break-before: always;
}
}
/* 高亮文本 */
.highlight {
background: #fffbe6;
padding: 2px 4px;
border-radius: 2px;
color: #faad14;
font-weight: 500;
}
/* 状态标签 */
.status-tag {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-success {
background: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.status-warning {
background: #fffbe6;
border: 1px solid #ffe58f;
color: #faad14;
}
.status-error {
background: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.status-processing {
background: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
}

View File

@@ -0,0 +1,26 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}

View File

@@ -0,0 +1,37 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3001,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api/v1')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'antd-vendor': ['ant-design-vue', '@ant-design/icons-vue'],
'utils-vendor': ['axios', 'dayjs', 'lodash-es']
}
}
}
}
})