docs: 更新项目文档,完善需求和技术细节

This commit is contained in:
ylweng
2025-09-01 02:35:41 +08:00
parent e1647902e2
commit ad20888cd4
57 changed files with 961 additions and 156 deletions

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

File diff suppressed because it is too large Load Diff

34
admin-system/package.json Normal file
View 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
View 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>

View 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
View 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')

View 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

View 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
}
})

View 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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

View 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";
`
}
}
}
})

View File

@@ -0,0 +1,26 @@
# 爱鉴花后台管理系统功能模块
## 1. 用户管理
- 用户列表查看
- 用户信息编辑
- 用户权限管理
## 2. 商品管理
- 商品列表查看
- 商品信息添加/编辑/删除
- 商品分类管理
## 3. 订单管理
- 订单列表查看
- 订单状态更新
- 订单详情查看
## 4. 数据统计
- 用户数据统计
- 销售数据统计
- 识别数据统计
## 5. 系统设置
- 系统参数配置
- 权限管理
- 日志查看

View File

@@ -0,0 +1,32 @@
# 爱鉴花后台管理系统开发计划
## 第一阶段基础框架搭建1-2周
- 项目初始化
- 页面结构设计
- 基础组件开发
- 路由配置
## 第二阶段核心功能开发3-6周
- 用户管理模块
- 商品管理模块
- 订单管理模块
## 第三阶段数据统计功能开发7-10周
- 数据统计模块
- 图表展示
- 报表导出
## 第四阶段系统设置功能开发11-12周
- 系统参数配置
- 权限管理
- 日志查看
## 第五阶段测试和优化13-14周
- 功能测试
- 性能优化
- 用户体验优化
## 第六阶段部署和维护15-16周
- 部署上线
- 系统监控
- 持续优化