docs: 更新项目文档,完善需求和技术细节
This commit is contained in:
18
admin-system/.env.development
Normal file
18
admin-system/.env.development
Normal file
@@ -0,0 +1,18 @@
|
||||
# 爱鉴花后台管理系统 - 开发环境配置
|
||||
|
||||
# 应用配置
|
||||
VUE_APP_TITLE=爱鉴花后台管理系统
|
||||
VUE_APP_VERSION=1.0.0
|
||||
VUE_APP_PORT=3250
|
||||
|
||||
# API配置
|
||||
VUE_APP_API_BASE_URL=http://localhost:3200/api/v1
|
||||
VUE_APP_API_TIMEOUT=30000
|
||||
|
||||
# 功能开关
|
||||
VUE_APP_FEATURE_ANALYTICS=true
|
||||
VUE_APP_FEATURE_WEBSOCKET=false
|
||||
|
||||
# 开发工具配置
|
||||
VUE_APP_DEBUG=true
|
||||
VUE_APP_DEVTOOLS=true
|
||||
22
admin-system/README.md
Normal file
22
admin-system/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 爱鉴花后台管理系统
|
||||
|
||||
## 项目介绍
|
||||
这是爱鉴花项目的后台管理系统,基于Vue3开发。
|
||||
|
||||
## 技术栈
|
||||
- Vue3
|
||||
- Vue Router
|
||||
- Vuex
|
||||
- Element Plus
|
||||
|
||||
## 文件结构
|
||||
- src: 源代码
|
||||
- components: 组件
|
||||
- views: 页面
|
||||
- router: 路由配置
|
||||
- store: 状态管理
|
||||
- utils: 工具函数
|
||||
- public: 静态资源
|
||||
|
||||
## 开发说明
|
||||
请确保安装了Node.js环境,并使用npm管理依赖。
|
||||
11814
admin-system/package-lock.json
generated
Normal file
11814
admin-system/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
admin-system/package.json
Normal file
34
admin-system/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "aijianhua-admin",
|
||||
"version": "1.0.0",
|
||||
"description": "爱鉴花后台管理系统",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --port 3250",
|
||||
"build": "vue-cli-service build",
|
||||
"dev": "vue-cli-service serve --port 3250",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.3.12",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/eslint-config-standard": "^8.0.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"sass": "^1.64.1",
|
||||
"sass-loader": "^13.3.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
31
admin-system/src/App.vue
Normal file
31
admin-system/src/App.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
229
admin-system/src/layouts/MainLayout.vue
Normal file
229
admin-system/src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
:icon="sidebarCollapsed ? 'Expand' : 'Fold'"
|
||||
@click="toggleSidebar"
|
||||
class="sidebar-toggle"
|
||||
/>
|
||||
<span class="logo">
|
||||
<i class="el-icon-flower" style="color: #4CAF50; margin-right: 8px;"></i>
|
||||
爱鉴花后台管理系统
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<el-dropdown>
|
||||
<span class="user-info">
|
||||
<el-avatar :size="32" :src="userInfo.avatar_url || ''" />
|
||||
<span class="username">{{ userInfo.username || '管理员' }}</span>
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="goToProfile">
|
||||
<el-icon><user /></el-icon>
|
||||
个人信息
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout" divided>
|
||||
<el-icon><switch-button /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<div class="main-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="sidebarCollapsed ? '64px' : '200px'" class="sidebar">
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
:collapse="sidebarCollapsed"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
class="sidebar-menu"
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><odometer /></el-icon>
|
||||
<template #title>仪表盘</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu index="user-management">
|
||||
<template #title>
|
||||
<el-icon><user /></el-icon>
|
||||
<span>用户管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/users">用户列表</el-menu-item>
|
||||
<el-menu-item index="/users/add">添加用户</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="product-management">
|
||||
<template #title>
|
||||
<el-icon><goods /></el-icon>
|
||||
<span>商品管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/products">商品列表</el-menu-item>
|
||||
<el-menu-item index="/products/add">添加商品</el-menu-item>
|
||||
<el-menu-item index="/categories">分类管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="order-management">
|
||||
<template #title>
|
||||
<el-icon><document /></el-icon>
|
||||
<span>订单管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/orders">订单列表</el-menu-item>
|
||||
<el-menu-item index="/orders/pending">待处理订单</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="statistics">
|
||||
<template #title>
|
||||
<el-icon><data-analysis /></el-icon>
|
||||
<span>数据统计</span>
|
||||
</template>
|
||||
<el-menu-item index="/statistics">数据概览</el-menu-item>
|
||||
<el-menu-item index="/statistics/reports">报表分析</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="settings">
|
||||
<template #title>
|
||||
<el-icon><setting /></el-icon>
|
||||
<span>系统设置</span>
|
||||
</template>
|
||||
<el-menu-item index="/settings">系统参数</el-menu-item>
|
||||
<el-menu-item index="/settings/roles">权限管理</el-menu-item>
|
||||
<el-menu-item index="/settings/logs">操作日志</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import {
|
||||
Odometer,
|
||||
User,
|
||||
Goods,
|
||||
Document,
|
||||
DataAnalysis,
|
||||
Setting,
|
||||
ArrowDown,
|
||||
SwitchButton
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'MainLayout',
|
||||
components: {
|
||||
Odometer,
|
||||
User,
|
||||
Goods,
|
||||
Document,
|
||||
DataAnalysis,
|
||||
Setting,
|
||||
ArrowDown,
|
||||
SwitchButton
|
||||
},
|
||||
computed: {
|
||||
...mapState(['sidebarCollapsed', 'userInfo'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleSidebar', 'logout']),
|
||||
goToProfile() {
|
||||
this.$message.info('跳转到个人信息页面')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
margin-right: 16px;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #fff;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
20
admin-system/src/main.js
Normal file
20
admin-system/src/main.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册Element Plus图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
74
admin-system/src/router/index.js
Normal file
74
admin-system/src/router/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '../views/Login.vue'
|
||||
import Layout from '../layouts/MainLayout.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
meta: { title: '仪表盘' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'Users',
|
||||
component: () => import('../views/Users.vue'),
|
||||
meta: { title: '用户管理' }
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
name: 'Products',
|
||||
component: () => import('../views/Products.vue'),
|
||||
meta: { title: '商品管理' }
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
name: 'Orders',
|
||||
component: () => import('../views/Orders.vue'),
|
||||
meta: { title: '订单管理' }
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'Statistics',
|
||||
component: () => import('../views/Statistics.vue'),
|
||||
meta: { title: '数据统计' }
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
component: () => import('../views/Settings.vue'),
|
||||
meta: { title: '系统设置' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫 - 登录验证
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isLoggedIn = localStorage.getItem('token')
|
||||
|
||||
if (to.path !== '/login' && !isLoggedIn) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && isLoggedIn) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
45
admin-system/src/store/index.js
Normal file
45
admin-system/src/store/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
token: localStorage.getItem('token') || '',
|
||||
userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'),
|
||||
sidebarCollapsed: false
|
||||
},
|
||||
mutations: {
|
||||
SET_TOKEN(state, token) {
|
||||
state.token = token
|
||||
localStorage.setItem('token', token)
|
||||
},
|
||||
SET_USER_INFO(state, userInfo) {
|
||||
state.userInfo = userInfo
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
},
|
||||
CLEAR_AUTH(state) {
|
||||
state.token = ''
|
||||
state.userInfo = {}
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
},
|
||||
TOGGLE_SIDEBAR(state) {
|
||||
state.sidebarCollapsed = !state.sidebarCollapsed
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
login({ commit }, { token, userInfo }) {
|
||||
commit('SET_TOKEN', token)
|
||||
commit('SET_USER_INFO', userInfo)
|
||||
},
|
||||
logout({ commit }) {
|
||||
commit('CLEAR_AUTH')
|
||||
},
|
||||
toggleSidebar({ commit }) {
|
||||
commit('TOGGLE_SIDEBAR')
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isLoggedIn: state => !!state.token,
|
||||
userInfo: state => state.userInfo,
|
||||
sidebarCollapsed: state => state.sidebarCollapsed
|
||||
}
|
||||
})
|
||||
124
admin-system/src/utils/api.js
Normal file
124
admin-system/src/utils/api.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:3200/api/v1',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
// 从localStorage获取token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
// 直接返回数据部分
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,清除本地存储并跳转到登录页
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
console.error('权限不足')
|
||||
break
|
||||
case 500:
|
||||
console.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
console.error('请求失败', error.response.data)
|
||||
}
|
||||
} else {
|
||||
console.error('网络错误', error.message)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 认证相关API
|
||||
export const authAPI = {
|
||||
// 用户登录
|
||||
login: (data) => api.post('/auth/login', data),
|
||||
|
||||
// 用户注册
|
||||
register: (data) => api.post('/auth/register', data),
|
||||
|
||||
// 获取当前用户信息
|
||||
getCurrentUser: () => api.get('/users/me')
|
||||
}
|
||||
|
||||
// 用户管理API
|
||||
export const userAPI = {
|
||||
// 获取用户列表
|
||||
getUsers: (params) => api.get('/users', { params }),
|
||||
|
||||
// 获取用户详情
|
||||
getUser: (id) => api.get(`/users/${id}`),
|
||||
|
||||
// 更新用户信息
|
||||
updateUser: (id, data) => api.put(`/users/${id}`, data),
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id) => api.delete(`/users/${id}`)
|
||||
}
|
||||
|
||||
// 商品管理API
|
||||
export const productAPI = {
|
||||
// 获取商品列表
|
||||
getProducts: (params) => api.get('/products', { params }),
|
||||
|
||||
// 获取商品详情
|
||||
getProduct: (id) => api.get(`/products/${id}`),
|
||||
|
||||
// 创建商品
|
||||
createProduct: (data) => api.post('/products', data),
|
||||
|
||||
// 更新商品
|
||||
updateProduct: (id, data) => api.put(`/products/${id}`, data),
|
||||
|
||||
// 删除商品
|
||||
deleteProduct: (id) => api.delete(`/products/${id}`)
|
||||
}
|
||||
|
||||
// 订单管理API
|
||||
export const orderAPI = {
|
||||
// 获取订单列表
|
||||
getOrders: (params) => api.get('/orders', { params }),
|
||||
|
||||
// 获取订单详情
|
||||
getOrder: (id) => api.get(`/orders/${id}`),
|
||||
|
||||
// 更新订单状态
|
||||
updateOrderStatus: (id, data) => api.put(`/orders/${id}/status`, data)
|
||||
}
|
||||
|
||||
// 数据统计API
|
||||
export const statisticsAPI = {
|
||||
// 获取用户统计数据
|
||||
getUserStats: () => api.get('/admin/stats/users'),
|
||||
|
||||
// 获取订单统计数据
|
||||
getOrderStats: () => api.get('/admin/stats/orders'),
|
||||
|
||||
// 获取商品统计数据
|
||||
getProductStats: () => api.get('/admin/stats/products')
|
||||
}
|
||||
|
||||
export default api
|
||||
416
admin-system/src/views/Dashboard.vue
Normal file
416
admin-system/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>仪表盘</h1>
|
||||
<p>欢迎回来,{{ userInfo.username }}!这里是系统概览。</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据概览卡片 -->
|
||||
<el-row :gutter="20" class="stats-cards">
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon users">
|
||||
<el-icon><user /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.userCount || 0 }}</div>
|
||||
<div class="stat-label">总用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon products">
|
||||
<el-icon><goods /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.productCount || 0 }}</div>
|
||||
<div class="stat-label">商品总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon orders">
|
||||
<el-icon><document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.todayOrderCount || 0 }}</div>
|
||||
<div class="stat-label">今日订单</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon revenue">
|
||||
<el-icon><money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">¥{{ statsData.todayRevenue || 0 }}</div>
|
||||
<div class="stat-label">今日收入</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<el-col :xs="24" :lg="16">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<h3>用户增长趋势</h3>
|
||||
<el-select v-model="chartRange" size="small" style="width: 120px;" @change="fetchChartData">
|
||||
<el-option label="近7天" value="7" />
|
||||
<el-option label="近30天" value="30" />
|
||||
<el-option label="近90天" value="90" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="userChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :lg="8">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<h3>商品分类占比</h3>
|
||||
</template>
|
||||
<div ref="categoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<el-card class="recent-activity">
|
||||
<template #header>
|
||||
<h3>最近活动</h3>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(activity, index) in recentActivities"
|
||||
:key="index"
|
||||
:timestamp="activity.time"
|
||||
:type="activity.type"
|
||||
>
|
||||
{{ activity.content }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import * as echarts from 'echarts'
|
||||
import { statisticsAPI } from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
setup() {
|
||||
const store = useStore()
|
||||
const chartRange = ref('7')
|
||||
const userChart = ref(null)
|
||||
const categoryChart = ref(null)
|
||||
const userChartInstance = ref(null)
|
||||
const categoryChartInstance = ref(null)
|
||||
const statsData = ref({})
|
||||
const chartData = ref({})
|
||||
|
||||
const userInfo = computed(() => store.state.userInfo)
|
||||
|
||||
const recentActivities = [
|
||||
{
|
||||
time: '2024-01-15 14:30',
|
||||
type: 'primary',
|
||||
content: '新用户注册:用户「张三」注册了账号'
|
||||
},
|
||||
{
|
||||
time: '2024-01-15 13:45',
|
||||
type: 'success',
|
||||
content: '订单创建:订单 #10086 已创建,金额 ¥199'
|
||||
},
|
||||
{
|
||||
time: '2024-01-15 12:20',
|
||||
type: 'warning',
|
||||
content: '系统警告:数据库连接数接近上限'
|
||||
},
|
||||
{
|
||||
time: '2024-01-15 10:15',
|
||||
type: 'info',
|
||||
content: '商品更新:商品「玫瑰花束」信息已更新'
|
||||
}
|
||||
]
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatsData = async () => {
|
||||
try {
|
||||
// 这里应该调用实际的统计数据API
|
||||
// 暂时使用模拟数据
|
||||
statsData.value = {
|
||||
userCount: 1234,
|
||||
productCount: 567,
|
||||
todayOrderCount: 89,
|
||||
todayRevenue: 12345
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图表数据
|
||||
const fetchChartData = async () => {
|
||||
try {
|
||||
// 这里应该调用实际的图表数据API
|
||||
// 暂时使用模拟数据
|
||||
chartData.value = {
|
||||
userGrowth: {
|
||||
dates: ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日'],
|
||||
counts: [10, 25, 15, 30, 20, 35, 40]
|
||||
},
|
||||
categoryDistribution: [
|
||||
{ name: '鲜花', value: 45 },
|
||||
{ name: '盆栽', value: 30 },
|
||||
{ name: '种子', value: 15 },
|
||||
{ name: '工具', value: 10 }
|
||||
]
|
||||
}
|
||||
renderCharts()
|
||||
} catch (error) {
|
||||
console.error('获取图表数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderCharts = () => {
|
||||
// 用户增长趋势图
|
||||
if (userChart.value) {
|
||||
if (!userChartInstance.value) {
|
||||
userChartInstance.value = echarts.init(userChart.value)
|
||||
}
|
||||
|
||||
const userOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.value.userGrowth.dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: chartData.value.userGrowth.counts,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {}
|
||||
}]
|
||||
}
|
||||
|
||||
userChartInstance.value.setOption(userOption)
|
||||
}
|
||||
|
||||
// 商品分类占比图
|
||||
if (categoryChart.value) {
|
||||
if (!categoryChartInstance.value) {
|
||||
categoryChartInstance.value = echarts.init(categoryChart.value)
|
||||
}
|
||||
|
||||
const categoryOption = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [{
|
||||
name: '商品分类',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData.value.categoryDistribution
|
||||
}]
|
||||
}
|
||||
|
||||
categoryChartInstance.value.setOption(categoryOption)
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const handleResize = () => {
|
||||
if (userChartInstance.value) {
|
||||
userChartInstance.value.resize()
|
||||
}
|
||||
if (categoryChartInstance.value) {
|
||||
categoryChartInstance.value.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatsData()
|
||||
fetchChartData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
chartRange,
|
||||
recentActivities,
|
||||
statsData,
|
||||
userChart,
|
||||
categoryChart
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #606266;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-icon.users {
|
||||
background: #e8f5e8;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-icon.products {
|
||||
background: #e3f2fd;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.stat-icon.orders {
|
||||
background: #fff3e0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.stat-icon.revenue {
|
||||
background: #fce4ec;
|
||||
color: #E91E63;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-header h3 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 240px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recent-activity {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.recent-activity h3 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
245
admin-system/src/views/Login.vue
Normal file
245
admin-system/src/views/Login.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-form">
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<i class="el-icon-flower" style="color: #4CAF50; font-size: 48px;"></i>
|
||||
<h1>爱鉴花后台管理系统</h1>
|
||||
</div>
|
||||
<p>请输入您的账号和密码登录系统</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
ref="loginFormRef"
|
||||
class="login-form-content"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
prefix-icon="user"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
prefix-icon="lock"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
class="login-btn"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© 2024 爱鉴花 - AI花卉识别平台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-background">
|
||||
<div class="background-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { authAPI } from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const loginFormRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = ref({
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
const valid = await loginFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用登录API
|
||||
const response = await authAPI.login({
|
||||
login: loginForm.value.username,
|
||||
password: loginForm.value.password
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 存储token和用户信息
|
||||
const userInfo = {
|
||||
id: response.data.user_id,
|
||||
username: response.data.username,
|
||||
phone: response.data.phone,
|
||||
email: response.data.email,
|
||||
user_type: response.data.user_type,
|
||||
avatar_url: response.data.avatar_url
|
||||
}
|
||||
|
||||
store.dispatch('login', {
|
||||
token: response.data.token,
|
||||
userInfo
|
||||
})
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
ElMessage.error(response.message || '登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('登录失败:' + (error.message || '网络错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loginForm,
|
||||
loginRules,
|
||||
loginFormRef,
|
||||
loading,
|
||||
handleLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 400px;
|
||||
background: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 2;
|
||||
margin: auto;
|
||||
margin-right: 10%;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
color: #4CAF50;
|
||||
font-size: 24px;
|
||||
margin-top: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form-content {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
background: #4CAF50;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="flowers" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="8" fill="%234CAF50" opacity="0.1"/></pattern></defs><rect x="0" y="0" width="100" height="100" fill="url(%23flowers)"/></svg>') repeat;
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.login-form {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
348
admin-system/src/views/Orders.vue
Normal file
348
admin-system/src/views/Orders.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div class="orders-container">
|
||||
<el-card class="orders-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>订单管理</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="订单状态">
|
||||
<el-select v-model="searchForm.status" clearable placeholder="请选择">
|
||||
<el-option label="待支付" value="pending" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
<el-option label="已发货" value="shipped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<el-table :data="orderList" border style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="order_number" label="订单号" width="180" />
|
||||
<el-table-column prop="username" label="用户" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" />
|
||||
<el-table-column prop="total_amount" label="订单金额" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.total_amount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.payment_status === 'pending'">待支付</el-tag>
|
||||
<el-tag v-else-if="scope.row.payment_status === 'paid'" type="success">已支付</el-tag>
|
||||
<el-tag v-else-if="scope.row.payment_status === 'cancelled'" type="danger">已取消</el-tag>
|
||||
<el-tag v-else>{{ scope.row.payment_status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发货状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.shipping_status === 'pending'">待发货</el-tag>
|
||||
<el-tag v-else-if="scope.row.shipping_status === 'shipped'" type="success">已发货</el-tag>
|
||||
<el-tag v-else-if="scope.row.shipping_status === 'completed'" type="primary">已完成</el-tag>
|
||||
<el-tag v-else>{{ scope.row.shipping_status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="下单时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleUpdateStatus(scope.row)"
|
||||
:disabled="scope.row.payment_status === 'cancelled'"
|
||||
>
|
||||
状态
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="pagination"
|
||||
/>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="订单详情" width="700px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="订单号">{{ currentOrder.order_number }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户">{{ currentOrder.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentOrder.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">¥{{ currentOrder.total_amount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付状态">
|
||||
<el-tag v-if="currentOrder.payment_status === 'pending'">待支付</el-tag>
|
||||
<el-tag v-else-if="currentOrder.payment_status === 'paid'" type="success">已支付</el-tag>
|
||||
<el-tag v-else-if="currentOrder.payment_status === 'cancelled'" type="danger">已取消</el-tag>
|
||||
<el-tag v-else>{{ currentOrder.payment_status }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发货状态">
|
||||
<el-tag v-if="currentOrder.shipping_status === 'pending'">待发货</el-tag>
|
||||
<el-tag v-else-if="currentOrder.shipping_status === 'shipped'" type="success">已发货</el-tag>
|
||||
<el-tag v-else-if="currentOrder.shipping_status === 'completed'" type="primary">已完成</el-tag>
|
||||
<el-tag v-else>{{ currentOrder.shipping_status }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址">{{ currentOrder.shipping_address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下单时间">{{ formatDate(currentOrder.created_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-table :data="currentOrder.items" style="margin-top: 20px;" border>
|
||||
<el-table-column prop="product_name" label="商品名称" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column prop="unit_price" label="单价" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.unit_price }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ (scope.row.unit_price * scope.row.quantity).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 更新订单状态对话框 -->
|
||||
<el-dialog v-model="statusDialogVisible" title="更新订单状态" width="500px">
|
||||
<el-form :model="statusForm" label-width="100px">
|
||||
<el-form-item label="支付状态">
|
||||
<el-select v-model="statusForm.payment_status" placeholder="请选择">
|
||||
<el-option label="待支付" value="pending" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发货状态">
|
||||
<el-select v-model="statusForm.shipping_status" placeholder="请选择">
|
||||
<el-option label="待发货" value="pending" />
|
||||
<el-option label="已发货" value="shipped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitStatusUpdate">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { orderAPI } from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Orders',
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusDialogVisible = ref(false)
|
||||
|
||||
const searchForm = reactive({
|
||||
status: ''
|
||||
})
|
||||
|
||||
const dateRange = ref([])
|
||||
|
||||
const statusForm = reactive({
|
||||
id: null,
|
||||
payment_status: '',
|
||||
shipping_status: ''
|
||||
})
|
||||
|
||||
const orderList = ref([])
|
||||
const currentOrder = ref({})
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
const fetchOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
status: searchForm.status,
|
||||
start_date: dateRange.value && dateRange.value[0] ? dateRange.value[0] : undefined,
|
||||
end_date: dateRange.value && dateRange.value[1] ? dateRange.value[1] : undefined
|
||||
}
|
||||
|
||||
const response = await orderAPI.getOrders(params)
|
||||
if (response.code === 200) {
|
||||
orderList.value = response.data.orders
|
||||
pagination.total = response.data.pagination.total
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取订单列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取订单列表失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
pagination.page = 1
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
const handleView = (row) => {
|
||||
currentOrder.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
const handleUpdateStatus = (row) => {
|
||||
statusForm.id = row.id
|
||||
statusForm.payment_status = row.payment_status
|
||||
statusForm.shipping_status = row.shipping_status
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交状态更新
|
||||
const submitStatusUpdate = async () => {
|
||||
try {
|
||||
const response = await orderAPI.updateOrderStatus(statusForm.id, {
|
||||
payment_status: statusForm.payment_status,
|
||||
shipping_status: statusForm.shipping_status
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('状态更新成功')
|
||||
statusDialogVisible.value = false
|
||||
fetchOrders()
|
||||
} else {
|
||||
ElMessage.error(response.message || '状态更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('状态更新失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页相关
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
pagination.page = 1
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
detailDialogVisible,
|
||||
statusDialogVisible,
|
||||
searchForm,
|
||||
dateRange,
|
||||
statusForm,
|
||||
orderList,
|
||||
currentOrder,
|
||||
pagination,
|
||||
formatDate,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleView,
|
||||
handleUpdateStatus,
|
||||
submitStatusUpdate,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.orders-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.orders-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
450
admin-system/src/views/Products.vue
Normal file
450
admin-system/src/views/Products.vue
Normal file
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<div class="products-container">
|
||||
<el-card class="products-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>商品管理</span>
|
||||
<el-button type="primary" @click="handleAddProduct">新增商品</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="商品名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="searchForm.category_id" clearable placeholder="请选择">
|
||||
<el-option label="全部分类" :value="0" />
|
||||
<el-option label="鲜花" :value="1" />
|
||||
<el-option label="盆栽" :value="2" />
|
||||
<el-option label="种子" :value="3" />
|
||||
<el-option label="工具" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<el-table :data="productList" border style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="商品名称" />
|
||||
<el-table-column prop="category_name" label="分类" width="100" />
|
||||
<el-table-column prop="price" label="价格" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.price }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock" label="库存" width="100" />
|
||||
<el-table-column label="图片" width="120">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
:src="scope.row.image"
|
||||
class="product-image"
|
||||
:preview-src-list="[scope.row.image]"
|
||||
fit="cover"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 1" type="success">上架</el-tag>
|
||||
<el-tag v-else type="danger">下架</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[12, 24, 48, 96]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="pagination"
|
||||
/>
|
||||
|
||||
<!-- 商品编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="80px">
|
||||
<el-form-item label="商品名称" prop="name">
|
||||
<el-input v-model="editForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="category_id">
|
||||
<el-select v-model="editForm.category_id" placeholder="请选择">
|
||||
<el-option label="鲜花" :value="1" />
|
||||
<el-option label="盆栽" :value="2" />
|
||||
<el-option label="种子" :value="3" />
|
||||
<el-option label="工具" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="价格" prop="price">
|
||||
<el-input-number v-model="editForm.price" :min="0" :step="0.1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="库存" prop="stock">
|
||||
<el-input-number v-model="editForm.stock" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图片" prop="image">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
action="/api/v1/upload"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:before-upload="beforeImageUpload"
|
||||
>
|
||||
<img v-if="editForm.image" :src="editForm.image" class="image-preview" />
|
||||
<el-icon v-else class="image-uploader-icon"><plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="editForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="editForm.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="上架"
|
||||
inactive-text="下架"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { productAPI } from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Products',
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const editFormRef = ref()
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
category_id: null
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
category_id: 1,
|
||||
price: 0,
|
||||
stock: 0,
|
||||
image: '',
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
const editRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入商品名称', trigger: 'blur' }
|
||||
],
|
||||
category_id: [
|
||||
{ required: true, message: '请选择分类', trigger: 'change' }
|
||||
],
|
||||
price: [
|
||||
{ required: true, message: '请输入价格', trigger: 'blur' }
|
||||
],
|
||||
stock: [
|
||||
{ required: true, message: '请输入库存', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const productList = ref([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 12,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
const fetchProducts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
category_id: searchForm.category_id
|
||||
}
|
||||
|
||||
const response = await productAPI.getProducts(params)
|
||||
if (response.code === 200) {
|
||||
productList.value = response.data.products
|
||||
pagination.total = response.data.pagination.total
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取商品列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取商品列表失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchProducts()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.category_id = null
|
||||
pagination.page = 1
|
||||
fetchProducts()
|
||||
}
|
||||
|
||||
// 新增商品
|
||||
const handleAddProduct = () => {
|
||||
dialogTitle.value = '新增商品'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 重置表单
|
||||
Object.assign(editForm, {
|
||||
id: null,
|
||||
name: '',
|
||||
category_id: 1,
|
||||
price: 0,
|
||||
stock: 0,
|
||||
image: '',
|
||||
description: '',
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑商品
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑商品'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 填充表单数据
|
||||
Object.assign(editForm, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
category_id: row.category_id,
|
||||
price: row.price,
|
||||
stock: row.stock,
|
||||
image: row.image,
|
||||
description: row.description,
|
||||
status: row.status
|
||||
})
|
||||
}
|
||||
|
||||
// 删除商品
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除商品 "${row.name}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const response = await productAPI.deleteProduct(row.id)
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchProducts()
|
||||
} else {
|
||||
ElMessage.error(response.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败: ' + error.message)
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
if (!editFormRef.value) return
|
||||
|
||||
const valid = await editFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
let response
|
||||
if (editForm.id) {
|
||||
// 更新商品
|
||||
response = await productAPI.updateProduct(editForm.id, editForm)
|
||||
} else {
|
||||
// 创建商品
|
||||
response = await productAPI.createProduct(editForm)
|
||||
}
|
||||
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
ElMessage.success(editForm.id ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchProducts()
|
||||
} else {
|
||||
ElMessage.error(response.message || (editForm.id ? '更新失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error((editForm.id ? '更新失败' : '创建失败') + ': ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传相关
|
||||
const handleImageSuccess = (response, file) => {
|
||||
// 实际项目中需要根据接口返回格式处理
|
||||
editForm.image = response.data.url || URL.createObjectURL(file.raw)
|
||||
}
|
||||
|
||||
const beforeImageUpload = (file) => {
|
||||
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isJPG) {
|
||||
ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('上传头像图片大小不能超过 2MB!')
|
||||
}
|
||||
|
||||
return isJPG && isLt2M
|
||||
}
|
||||
|
||||
// 分页相关
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
pagination.page = 1
|
||||
fetchProducts()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchProducts()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProducts()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
dialogVisible,
|
||||
dialogTitle,
|
||||
editFormRef,
|
||||
searchForm,
|
||||
editForm,
|
||||
editRules,
|
||||
productList,
|
||||
pagination,
|
||||
formatDate,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleAddProduct,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
submitForm,
|
||||
handleImageSuccess,
|
||||
beforeImageUpload,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.products-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.products-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-uploader .image-preview {
|
||||
width: 100%;
|
||||
height: 178px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-uploader .el-upload {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
}
|
||||
|
||||
.image-uploader .el-upload:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
text-align: center;
|
||||
line-height: 178px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
1042
admin-system/src/views/Settings.vue
Normal file
1042
admin-system/src/views/Settings.vue
Normal file
File diff suppressed because it is too large
Load Diff
457
admin-system/src/views/Statistics.vue
Normal file
457
admin-system/src/views/Statistics.vue
Normal file
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<div class="statistics-container">
|
||||
<el-card class="statistics-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据统计</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<el-row :gutter="20" class="stats-overview">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon users">
|
||||
<el-icon><user /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.userCount || 0 }}</div>
|
||||
<div class="stat-label">总用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon products">
|
||||
<el-icon><goods /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.productCount || 0 }}</div>
|
||||
<div class="stat-label">商品总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon orders">
|
||||
<el-icon><document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.orderCount || 0 }}</div>
|
||||
<div class="stat-label">订单总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon revenue">
|
||||
<el-icon><money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">¥{{ statsData.totalRevenue || 0 }}</div>
|
||||
<div class="stat-label">总销售额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<el-col :span="24">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<h3>用户增长趋势</h3>
|
||||
<el-select v-model="userChartRange" size="small" style="width: 120px;" @change="fetchUserChartData">
|
||||
<el-option label="近7天" value="7" />
|
||||
<el-option label="近30天" value="30" />
|
||||
<el-option label="近90天" value="90" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="userChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<h3>商品分类分布</h3>
|
||||
</template>
|
||||
<div ref="categoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<h3>订单状态分布</h3>
|
||||
</template>
|
||||
<div ref="orderStatusChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { statisticsAPI } from '../utils/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'Statistics',
|
||||
setup() {
|
||||
const userChart = ref(null)
|
||||
const categoryChart = ref(null)
|
||||
const orderStatusChart = ref(null)
|
||||
|
||||
const userChartInstance = ref(null)
|
||||
const categoryChartInstance = ref(null)
|
||||
const orderStatusChartInstance = ref(null)
|
||||
|
||||
const userChartRange = ref('30')
|
||||
|
||||
const statsData = reactive({
|
||||
userCount: 0,
|
||||
productCount: 0,
|
||||
orderCount: 0,
|
||||
totalRevenue: 0
|
||||
})
|
||||
|
||||
const chartData = reactive({
|
||||
userGrowth: [],
|
||||
categoryDistribution: [],
|
||||
orderStatusDistribution: []
|
||||
})
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatsData = async () => {
|
||||
try {
|
||||
const response = await statisticsAPI.getUserStats()
|
||||
if (response.code === 200) {
|
||||
statsData.userCount = response.data.user_count
|
||||
statsData.productCount = response.data.product_count
|
||||
statsData.orderCount = response.data.order_count
|
||||
statsData.totalRevenue = response.data.total_revenue
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取统计数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取统计数据失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户图表数据
|
||||
const fetchUserChartData = async () => {
|
||||
try {
|
||||
const response = await statisticsAPI.getUserStats()
|
||||
if (response.code === 200) {
|
||||
chartData.userGrowth = response.data.user_growth
|
||||
renderUserChart()
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取用户图表数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类图表数据
|
||||
const fetchCategoryChartData = async () => {
|
||||
try {
|
||||
const response = await statisticsAPI.getProductStats()
|
||||
if (response.code === 200) {
|
||||
chartData.categoryDistribution = response.data.category_distribution
|
||||
renderCategoryChart()
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取分类图表数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取分类图表数据失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单状态图表数据
|
||||
const fetchOrderStatusChartData = async () => {
|
||||
try {
|
||||
const response = await statisticsAPI.getOrderStats()
|
||||
if (response.code === 200) {
|
||||
chartData.orderStatusDistribution = response.data.status_distribution
|
||||
renderOrderStatusChart()
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取订单状态图表数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取订单状态图表数据失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染用户增长趋势图
|
||||
const renderUserChart = () => {
|
||||
if (userChart.value) {
|
||||
if (!userChartInstance.value) {
|
||||
userChartInstance.value = echarts.init(userChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.userGrowth.dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: chartData.userGrowth.counts,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {}
|
||||
}]
|
||||
}
|
||||
|
||||
userChartInstance.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染商品分类分布图
|
||||
const renderCategoryChart = () => {
|
||||
if (categoryChart.value) {
|
||||
if (!categoryChartInstance.value) {
|
||||
categoryChartInstance.value = echarts.init(categoryChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [{
|
||||
name: '商品分类',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData.categoryDistribution
|
||||
}]
|
||||
}
|
||||
|
||||
categoryChartInstance.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染订单状态分布图
|
||||
const renderOrderStatusChart = () => {
|
||||
if (orderStatusChart.value) {
|
||||
if (!orderStatusChartInstance.value) {
|
||||
orderStatusChartInstance.value = echarts.init(orderStatusChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [{
|
||||
name: '订单状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData.orderStatusDistribution
|
||||
}]
|
||||
}
|
||||
|
||||
orderStatusChartInstance.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const handleResize = () => {
|
||||
if (userChartInstance.value) {
|
||||
userChartInstance.value.resize()
|
||||
}
|
||||
if (categoryChartInstance.value) {
|
||||
categoryChartInstance.value.resize()
|
||||
}
|
||||
if (orderStatusChartInstance.value) {
|
||||
orderStatusChartInstance.value.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatsData()
|
||||
fetchUserChartData()
|
||||
fetchCategoryChartData()
|
||||
fetchOrderStatusChartData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
userChart,
|
||||
categoryChart,
|
||||
orderStatusChart,
|
||||
userChartRange,
|
||||
statsData,
|
||||
fetchUserChartData
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.statistics-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.statistics-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-icon.users {
|
||||
background: #e8f5e8;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-icon.products {
|
||||
background: #e3f2fd;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.stat-icon.orders {
|
||||
background: #fff3e0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.stat-icon.revenue {
|
||||
background: #fce4ec;
|
||||
color: #E91E63;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-header h3 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
365
admin-system/src/views/Users.vue
Normal file
365
admin-system/src/views/Users.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="users-container">
|
||||
<el-card class="users-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="handleAddUser">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="用户名/手机号/邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型">
|
||||
<el-select v-model="searchForm.user_type" clearable placeholder="请选择">
|
||||
<el-option label="农户" value="farmer" />
|
||||
<el-option label="买家" value="buyer" />
|
||||
<el-option label="管理员" value="admin" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-table :data="userList" border style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="user_type" label="用户类型">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.user_type === 'farmer'">农户</el-tag>
|
||||
<el-tag v-else-if="scope.row.user_type === 'buyer'" type="success">买家</el-tag>
|
||||
<el-tag v-else-if="scope.row.user_type === 'admin'" type="danger">管理员</el-tag>
|
||||
<el-tag v-else>{{ scope.row.user_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="注册时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_login" label="最后登录" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.last_login ? formatDate(scope.row.last_login) : '从未登录' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)" :disabled="scope.row.user_type === 'admin'">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="pagination"
|
||||
/>
|
||||
|
||||
<!-- 用户编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="80px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="editForm.username" :disabled="!!editForm.id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="editForm.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="editForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型" prop="user_type">
|
||||
<el-select v-model="editForm.user_type" placeholder="请选择">
|
||||
<el-option label="农户" value="farmer" />
|
||||
<el-option label="买家" value="buyer" />
|
||||
<el-option label="管理员" value="admin" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!editForm.id" label="密码" prop="password">
|
||||
<el-input v-model="editForm.password" type="password" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { userAPI } from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const editFormRef = ref()
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
user_type: ''
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
id: null,
|
||||
username: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
user_type: 'farmer',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const editRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
user_type: [
|
||||
{ required: true, message: '请选择用户类型', trigger: 'change' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const userList = ref([])
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
user_type: searchForm.user_type
|
||||
}
|
||||
|
||||
const response = await userAPI.getUsers(params)
|
||||
if (response.code === 200) {
|
||||
userList.value = response.data.users
|
||||
pagination.total = response.data.pagination.total
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取用户列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户列表失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.user_type = ''
|
||||
pagination.page = 1
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
const handleAddUser = () => {
|
||||
dialogTitle.value = '新增用户'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 重置表单
|
||||
Object.assign(editForm, {
|
||||
id: null,
|
||||
username: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
user_type: 'farmer',
|
||||
password: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑用户'
|
||||
dialogVisible.value = true
|
||||
|
||||
// 填充表单数据
|
||||
Object.assign(editForm, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
phone: row.phone,
|
||||
email: row.email,
|
||||
user_type: row.user_type,
|
||||
password: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除用户 "${row.username}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const response = await userAPI.deleteUser(row.id)
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchUsers()
|
||||
} else {
|
||||
ElMessage.error(response.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败: ' + error.message)
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
if (!editFormRef.value) return
|
||||
|
||||
const valid = await editFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
let response
|
||||
if (editForm.id) {
|
||||
// 更新用户
|
||||
response = await userAPI.updateUser(editForm.id, {
|
||||
email: editForm.email,
|
||||
user_type: editForm.user_type
|
||||
})
|
||||
} else {
|
||||
// 创建用户(这里简化处理,实际应该调用注册接口)
|
||||
ElMessage.warning('演示版本,暂不支持创建用户')
|
||||
dialogVisible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
ElMessage.success(editForm.id ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchUsers()
|
||||
} else {
|
||||
ElMessage.error(response.message || (editForm.id ? '更新失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error((editForm.id ? '更新失败' : '创建失败') + ': ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页相关
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
pagination.page = 1
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchUsers()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
dialogVisible,
|
||||
dialogTitle,
|
||||
editFormRef,
|
||||
searchForm,
|
||||
editForm,
|
||||
editRules,
|
||||
userList,
|
||||
pagination,
|
||||
formatDate,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleAddUser,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
submitForm,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.users-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
51
admin-system/vue.config.js
Normal file
51
admin-system/vue.config.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
|
||||
// 开发服务器配置
|
||||
devServer: {
|
||||
port: 3250,
|
||||
host: 'localhost',
|
||||
open: true,
|
||||
hot: true,
|
||||
compress: true,
|
||||
|
||||
// 代理配置,解决跨域问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3200',
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/api': '/api/v1'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 构建配置
|
||||
configureWebpack: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': require('path').resolve(__dirname, 'src')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 生产环境配置
|
||||
productionSourceMap: false,
|
||||
|
||||
// CSS配置
|
||||
css: {
|
||||
extract: process.env.NODE_ENV === 'production',
|
||||
sourceMap: false,
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
additionalData: `
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/mixins.scss";
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
26
admin-system/功能模块.md
Normal file
26
admin-system/功能模块.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 爱鉴花后台管理系统功能模块
|
||||
|
||||
## 1. 用户管理
|
||||
- 用户列表查看
|
||||
- 用户信息编辑
|
||||
- 用户权限管理
|
||||
|
||||
## 2. 商品管理
|
||||
- 商品列表查看
|
||||
- 商品信息添加/编辑/删除
|
||||
- 商品分类管理
|
||||
|
||||
## 3. 订单管理
|
||||
- 订单列表查看
|
||||
- 订单状态更新
|
||||
- 订单详情查看
|
||||
|
||||
## 4. 数据统计
|
||||
- 用户数据统计
|
||||
- 销售数据统计
|
||||
- 识别数据统计
|
||||
|
||||
## 5. 系统设置
|
||||
- 系统参数配置
|
||||
- 权限管理
|
||||
- 日志查看
|
||||
32
admin-system/开发计划.md
Normal file
32
admin-system/开发计划.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 爱鉴花后台管理系统开发计划
|
||||
|
||||
## 第一阶段:基础框架搭建(1-2周)
|
||||
- 项目初始化
|
||||
- 页面结构设计
|
||||
- 基础组件开发
|
||||
- 路由配置
|
||||
|
||||
## 第二阶段:核心功能开发(3-6周)
|
||||
- 用户管理模块
|
||||
- 商品管理模块
|
||||
- 订单管理模块
|
||||
|
||||
## 第三阶段:数据统计功能开发(7-10周)
|
||||
- 数据统计模块
|
||||
- 图表展示
|
||||
- 报表导出
|
||||
|
||||
## 第四阶段:系统设置功能开发(11-12周)
|
||||
- 系统参数配置
|
||||
- 权限管理
|
||||
- 日志查看
|
||||
|
||||
## 第五阶段:测试和优化(13-14周)
|
||||
- 功能测试
|
||||
- 性能优化
|
||||
- 用户体验优化
|
||||
|
||||
## 第六阶段:部署和维护(15-16周)
|
||||
- 部署上线
|
||||
- 系统监控
|
||||
- 持续优化
|
||||
Reference in New Issue
Block a user