添加 IntelliJ IDEA 项目配置文件

This commit is contained in:
ylweng
2025-09-02 21:59:27 +08:00
parent 59cfe620fe
commit 501c218a83
56 changed files with 11886 additions and 126 deletions

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

@@ -0,0 +1,38 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from './stores/user'
const userStore = useUserStore()
onMounted(() => {
// 应用初始化时检查登录状态
userStore.checkLoginStatus()
})
</script>
<style lang="scss">
#app {
height: 100vh;
overflow: hidden;
}
// 全局样式重置
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
font-size: 14px;
color: #333;
background-color: #f0f2f5;
}
</style>

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
import type { ApiResponse, PaginatedResponse } from '@/utils/request'
import type { Order, OrderListParams, OrderCreateForm, OrderUpdateForm } from '@/types/order'
// 获取订单列表
export const getOrderList = (params: OrderListParams): Promise<ApiResponse<PaginatedResponse<Order>>> => {
return request.get('/orders', { params })
}
// 获取订单详情
export const getOrderDetail = (id: number): Promise<ApiResponse<Order>> => {
return request.get(`/orders/${id}`)
}
// 创建订单
export const createOrder = (data: OrderCreateForm): Promise<ApiResponse<Order>> => {
return request.post('/orders', data)
}
// 更新订单
export const updateOrder = (id: number, data: OrderUpdateForm): Promise<ApiResponse<Order>> => {
return request.put(`/orders/${id}`, data)
}
// 删除订单
export const deleteOrder = (id: number): Promise<ApiResponse> => {
return request.delete(`/orders/${id}`)
}
// 取消订单
export const cancelOrder = (id: number, reason?: string): Promise<ApiResponse> => {
return request.put(`/orders/${id}/cancel`, { reason })
}
// 确认订单
export const confirmOrder = (id: number): Promise<ApiResponse> => {
return request.put(`/orders/${id}/confirm`)
}
// 订单验收
export const acceptOrder = (id: number, data: { actualWeight: number; notes?: string }): Promise<ApiResponse> => {
return request.put(`/orders/${id}/accept`, data)
}
// 完成订单
export const completeOrder = (id: number): Promise<ApiResponse> => {
return request.put(`/orders/${id}/complete`)
}
// 获取订单统计数据
export const getOrderStatistics = (params?: { startDate?: string; endDate?: string }): Promise<ApiResponse> => {
return request.get('/orders/statistics', { params })
}

View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
import type { ApiResponse } from '@/utils/request'
import type { User, LoginForm, LoginResponse, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
// 用户登录
export const login = (data: LoginForm): Promise<ApiResponse<LoginResponse>> => {
return request.post('/auth/login', data)
}
// 获取用户信息
export const getUserInfo = (): Promise<ApiResponse<{ user: User; permissions: string[] }>> => {
return request.get('/auth/me')
}
// 用户登出
export const logout = (): Promise<ApiResponse> => {
return request.post('/auth/logout')
}
// 获取用户列表
export const getUserList = (params: UserListParams): Promise<ApiResponse> => {
return request.get('/users', { params })
}
// 创建用户
export const createUser = (data: UserCreateForm): Promise<ApiResponse<User>> => {
return request.post('/users', data)
}
// 更新用户
export const updateUser = (id: number, data: UserUpdateForm): Promise<ApiResponse<User>> => {
return request.put(`/users/${id}`, data)
}
// 删除用户
export const deleteUser = (id: number): Promise<ApiResponse> => {
return request.delete(`/users/${id}`)
}
// 批量删除用户
export const batchDeleteUsers = (ids: number[]): Promise<ApiResponse> => {
return request.delete('/users/batch', { data: { ids } })
}
// 重置用户密码
export const resetUserPassword = (id: number, newPassword: string): Promise<ApiResponse> => {
return request.put(`/users/${id}/password`, { password: newPassword })
}
// 启用/禁用用户
export const toggleUserStatus = (id: number, status: 'active' | 'inactive' | 'banned'): Promise<ApiResponse> => {
return request.put(`/users/${id}/status`, { status })
}

View File

@@ -0,0 +1,310 @@
<template>
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '240px'" class="layout-aside">
<div class="logo-container">
<img v-if="!isCollapse" src="/logo.png" alt="Logo" class="logo" />
<span v-if="!isCollapse" class="logo-text">NiuMall</span>
<img v-else src="/logo.png" alt="Logo" class="logo-mini" />
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:unique-opened="true"
class="layout-menu"
router
>
<template v-for="route in menuRoutes" :key="route.path">
<el-menu-item :index="route.path" v-if="!route.children">
<el-icon><component :is="route.meta?.icon" /></el-icon>
<template #title>{{ route.meta?.title }}</template>
</el-menu-item>
<el-sub-menu :index="route.path" v-else>
<template #title>
<el-icon><component :is="route.meta?.icon" /></el-icon>
<span>{{ route.meta?.title }}</span>
</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="child.path"
>
{{ child.meta?.title }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
<!-- 主要内容区域 -->
<el-container class="layout-main">
<!-- 头部 -->
<el-header class="layout-header">
<div class="header-left">
<el-button
type="text"
:icon="isCollapse ? 'Expand' : 'Fold'"
@click="toggleCollapse"
/>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="item in breadcrumbs"
:key="item.path"
:to="item.path"
>
{{ item.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<div class="user-info">
<el-avatar :src="userStore.avatar" :size="32" />
<span class="username">{{ userStore.username }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item command="settings">
<el-icon><Setting /></el-icon>
系统设置
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 内容区域 -->
<el-main class="layout-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// 菜单折叠状态
const isCollapse = ref(false)
// 当前激活的菜单
const activeMenu = computed(() => route.path)
// 菜单路由配置
const menuRoutes = computed(() => {
return router.getRoutes()
.find(r => r.path === '/')
?.children?.filter(child => child.meta?.title) || []
})
// 面包屑导航
const breadcrumbs = computed(() => {
const matched = route.matched.filter(item => item.meta?.title)
return matched.map(item => ({
path: item.path,
title: item.meta?.title
}))
})
// 切换菜单折叠状态
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
// 处理用户下拉菜单命令
const handleCommand = async (command: string) => {
switch (command) {
case 'profile':
router.push('/profile')
break
case 'settings':
router.push('/settings')
break
case 'logout':
try {
await ElMessageBox.confirm(
'确定要退出登录吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await userStore.logoutAction()
router.push('/login')
} catch (error) {
// 用户取消操作
}
break
}
}
// 监听路由变化,在移动端自动收起菜单
watch(
() => route.path,
() => {
if (window.innerWidth <= 768) {
isCollapse.value = true
}
}
)
</script>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
}
.layout-aside {
background: #304156;
transition: width 0.3s ease;
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
background: #263445;
.logo {
width: 32px;
height: 32px;
margin-right: 10px;
}
.logo-mini {
width: 32px;
height: 32px;
}
.logo-text {
color: white;
font-size: 18px;
font-weight: 600;
}
}
}
.layout-menu {
border: none;
background: #304156;
:deep(.el-menu-item) {
color: #bfcbd9;
&:hover {
background: #48576a;
color: #ffffff;
}
&.is-active {
background: #4CAF50;
color: #ffffff;
}
}
:deep(.el-sub-menu__title) {
color: #bfcbd9;
&:hover {
background: #48576a;
color: #ffffff;
}
}
}
.layout-main {
display: flex;
flex-direction: column;
}
.layout-header {
background: white;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.header-right {
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: background-color 0.3s ease;
&:hover {
background-color: #f5f5f5;
}
.username {
font-size: 14px;
color: #333;
}
}
}
}
.layout-content {
background: #f0f2f5;
padding: 20px;
overflow-y: auto;
}
@media (max-width: 768px) {
.layout-aside {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
height: 100vh;
}
.layout-main {
margin-left: 0;
}
.layout-header {
padding: 0 15px;
.header-left {
gap: 15px;
}
}
.layout-content {
padding: 15px;
}
}
</style>"

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

@@ -0,0 +1,25 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import './style/index.scss'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')

View File

@@ -0,0 +1,132 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 布局组件
import Layout from '@/layouts/index.vue'
// 页面组件
import Login from '@/views/login/index.vue'
import Dashboard from '@/views/dashboard/index.vue'
import UserManagement from '@/views/user/index.vue'
import OrderManagement from '@/views/order/index.vue'
import SupplierManagement from '@/views/supplier/index.vue'
import TransportManagement from '@/views/transport/index.vue'
import FinanceManagement from '@/views/finance/index.vue'
import QualityManagement from '@/views/quality/index.vue'
import Settings from '@/views/settings/index.vue'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: Login,
meta: {
title: '登录',
requiresAuth: false
}
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
title: '数据驾驶舱',
icon: 'DataAnalysis'
}
},
{
path: 'user',
name: 'UserManagement',
component: UserManagement,
meta: {
title: '用户管理',
icon: 'User'
}
},
{
path: 'order',
name: 'OrderManagement',
component: OrderManagement,
meta: {
title: '订单管理',
icon: 'ShoppingCart'
}
},
{
path: 'supplier',
name: 'SupplierManagement',
component: SupplierManagement,
meta: {
title: '供应商管理',
icon: 'OfficeBuilding'
}
},
{
path: 'transport',
name: 'TransportManagement',
component: TransportManagement,
meta: {
title: '运输管理',
icon: 'Truck'
}
},
{
path: 'quality',
name: 'QualityManagement',
component: QualityManagement,
meta: {
title: '质量管理',
icon: 'Medal'
}
},
{
path: 'finance',
name: 'FinanceManagement',
component: FinanceManagement,
meta: {
title: '财务管理',
icon: 'Money'
}
},
{
path: 'settings',
name: 'Settings',
component: Settings,
meta: {
title: '系统设置',
icon: 'Setting'
}
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 设置页面标题
document.title = to.meta?.title ? `${to.meta.title} - 活牛采购智能数字化系统` : '活牛采购智能数字化系统'
// 检查是否需要登录
if (to.path !== '/login' && !userStore.isLoggedIn) {
next('/login')
} else if (to.path === '/login' && userStore.isLoggedIn) {
next('/')
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,119 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, LoginForm } from '@/types/user'
import { login, getUserInfo, logout } from '@/api/user'
import { ElMessage } from 'element-plus'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref<string>(localStorage.getItem('token') || '')
const userInfo = ref<User | null>(null)
const permissions = ref<string[]>([])
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const avatar = computed(() => userInfo.value?.avatar || '/default-avatar.png')
const username = computed(() => userInfo.value?.username || '')
const role = computed(() => userInfo.value?.role || '')
// 登录
const loginAction = async (loginForm: LoginForm) => {
try {
const response = await login(loginForm)
const { access_token, user } = response.data
token.value = access_token
userInfo.value = user
// 保存到本地存储
localStorage.setItem('token', access_token)
localStorage.setItem('userInfo', JSON.stringify(user))
ElMessage.success('登录成功')
return Promise.resolve()
} catch (error: any) {
ElMessage.error(error.message || '登录失败')
return Promise.reject(error)
}
}
// 获取用户信息
const getUserInfoAction = async () => {
try {
const response = await getUserInfo()
userInfo.value = response.data.user
permissions.value = response.data.permissions || []
localStorage.setItem('userInfo', JSON.stringify(response.data.user))
localStorage.setItem('permissions', JSON.stringify(response.data.permissions))
} catch (error) {
// 如果获取用户信息失败,清除登录状态
logoutAction()
throw error
}
}
// 登出
const logoutAction = async () => {
try {
await logout()
} catch (error) {
console.error('登出接口调用失败:', error)
} finally {
// 清除状态和本地存储
token.value = ''
userInfo.value = null
permissions.value = []
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
localStorage.removeItem('permissions')
ElMessage.success('已退出登录')
}
}
// 检查登录状态
const checkLoginStatus = () => {
const storedToken = localStorage.getItem('token')
const storedUserInfo = localStorage.getItem('userInfo')
const storedPermissions = localStorage.getItem('permissions')
if (storedToken && storedUserInfo) {
token.value = storedToken
userInfo.value = JSON.parse(storedUserInfo)
permissions.value = storedPermissions ? JSON.parse(storedPermissions) : []
}
}
// 检查权限
const hasPermission = (permission: string) => {
return permissions.value.includes(permission) || permissions.value.includes('*')
}
// 检查角色
const hasRole = (roleName: string) => {
return userInfo.value?.role === roleName
}
return {
// 状态
token,
userInfo,
permissions,
// 计算属性
isLoggedIn,
avatar,
username,
role,
// 方法
loginAction,
getUserInfoAction,
logoutAction,
checkLoginStatus,
hasPermission,
hasRole
}
})

View File

@@ -0,0 +1,191 @@
// 全局样式文件
@import './variables.scss';
@import './mixins.scss';
// 全局重置样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
font-size: 14px;
color: #333;
background-color: #f0f2f5;
}
// 清除默认样式
ul, ol {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
// 通用工具类
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
.w-full { width: 100%; }
.h-full { height: 100%; }
// 间距工具类
@for $i from 0 through 40 {
.mt-#{$i} { margin-top: #{$i}px; }
.mb-#{$i} { margin-bottom: #{$i}px; }
.ml-#{$i} { margin-left: #{$i}px; }
.mr-#{$i} { margin-right: #{$i}px; }
.pt-#{$i} { padding-top: #{$i}px; }
.pb-#{$i} { padding-bottom: #{$i}px; }
.pl-#{$i} { padding-left: #{$i}px; }
.pr-#{$i} { padding-right: #{$i}px; }
}
// Element Plus 自定义样式
.el-card {
border-radius: 8px;
border: none;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-button {
border-radius: 6px;
}
.el-input__wrapper {
border-radius: 6px;
}
.el-select .el-input__wrapper {
border-radius: 6px;
}
.el-table {
border-radius: 8px;
overflow: hidden;
.el-table__header-wrapper {
th {
background-color: #fafafa;
color: #333;
font-weight: 600;
}
}
.el-table__row {
&:hover > td {
background-color: #f5f7fa;
}
}
}
.el-pagination {
margin-top: 20px;
justify-content: center;
}
.el-dialog {
border-radius: 12px;
.el-dialog__header {
padding: 20px 20px 10px;
border-bottom: 1px solid #e6e6e6;
}
.el-dialog__body {
padding: 20px;
}
}
.el-form {
.el-form-item__label {
color: #333;
font-weight: 500;
}
}
// 自定义滚动条
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
// 动画效果
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
// 响应式设计
@media (max-width: 768px) {
.mobile-hidden {
display: none !important;
}
.el-table {
font-size: 12px;
}
.el-dialog {
width: 90% !important;
margin: 5vh auto !important;
}
}

View File

@@ -0,0 +1,184 @@
// SCSS Mixins
// 清除浮动
@mixin clearfix {
&::after {
content: "";
display: table;
clear: both;
}
}
// 文本省略
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 多行文本省略
@mixin ellipsis-multiline($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
// 绝对居中
@mixin absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
// Flex 居中
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// 响应式断点
@mixin mobile {
@media (max-width: 767px) {
@content;
}
}
@mixin tablet {
@media (min-width: 768px) and (max-width: 1023px) {
@content;
}
}
@mixin desktop {
@media (min-width: 1024px) {
@content;
}
}
// 卡片样式
@mixin card-style {
background: white;
border-radius: $border-radius-large;
box-shadow: $box-shadow-light;
padding: $spacing-lg;
}
// 按钮基础样式
@mixin button-base {
display: inline-flex;
align-items: center;
justify-content: center;
padding: $spacing-sm $spacing-md;
border: none;
border-radius: $border-radius-base;
font-size: $font-size-small;
font-weight: 500;
cursor: pointer;
transition: $transition-base;
text-decoration: none;
outline: none;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
// 主要按钮样式
@mixin button-primary {
@include button-base;
background-color: $primary-color;
color: white;
&:hover:not(:disabled) {
background-color: $primary-light;
}
&:active {
background-color: $primary-dark;
}
}
// 次要按钮样式
@mixin button-secondary {
@include button-base;
background-color: transparent;
color: $primary-color;
border: 1px solid $primary-color;
&:hover:not(:disabled) {
background-color: $primary-color;
color: white;
}
}
// 表格样式
@mixin table-style {
width: 100%;
border-collapse: collapse;
border-radius: $border-radius-large;
overflow: hidden;
box-shadow: $box-shadow-light;
th, td {
padding: $spacing-md;
text-align: left;
border-bottom: 1px solid $border-lighter;
}
th {
background-color: $background-base;
font-weight: 600;
color: $text-primary;
}
tr:hover {
background-color: $background-light;
}
}
// 输入框样式
@mixin input-style {
width: 100%;
padding: $spacing-sm $spacing-md;
border: 1px solid $border-base;
border-radius: $border-radius-base;
font-size: $font-size-small;
color: $text-primary;
background-color: white;
transition: $transition-border;
&:focus {
outline: none;
border-color: $primary-color;
box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
}
&::placeholder {
color: $text-placeholder;
}
&:disabled {
background-color: $background-base;
cursor: not-allowed;
}
}
// 加载动画
@mixin loading-spin {
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
animation: spin 1s linear infinite;
}
// 渐变背景
@mixin gradient-background($start-color, $end-color, $direction: to right) {
background: linear-gradient($direction, $start-color, $end-color);
}

View File

@@ -0,0 +1,65 @@
// SCSS 变量定义
// 颜色变量
$primary-color: #4CAF50;
$primary-light: #81C784;
$primary-dark: #388E3C;
$success-color: #67C23A;
$warning-color: #E6A23C;
$danger-color: #F56C6C;
$info-color: #409EFF;
$text-primary: #303133;
$text-regular: #606266;
$text-secondary: #909399;
$text-placeholder: #C0C4CC;
$border-base: #DCDFE6;
$border-light: #E4E7ED;
$border-lighter: #EBEEF5;
$border-extra-light: #F2F6FC;
$background-base: #F5F7FA;
$background-light: #FAFCFF;
// 尺寸变量
$header-height: 60px;
$sidebar-width: 240px;
$sidebar-collapsed-width: 64px;
// 字体大小
$font-size-extra-small: 12px;
$font-size-small: 14px;
$font-size-base: 16px;
$font-size-medium: 18px;
$font-size-large: 20px;
$font-size-extra-large: 24px;
// 间距
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
// 圆角
$border-radius-small: 4px;
$border-radius-base: 6px;
$border-radius-large: 8px;
$border-radius-round: 20px;
// 阴影
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
$box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
// 过渡动画
$transition-base: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
$transition-fade: opacity 0.3s cubic-bezier(0.55, 0, 0.1, 1);
$transition-border: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
// Z-index 层级
$z-index-normal: 1;
$z-index-top: 1000;
$z-index-popper: 2000;

View File

@@ -0,0 +1,75 @@
// 订单相关类型定义
export interface Order {
id: number
orderNo: string
buyerId: number
buyerName: string
supplierId: number
supplierName: string
traderId?: number
traderName?: string
cattleBreed: string
cattleCount: number
expectedWeight: number
actualWeight?: number
unitPrice: number
totalAmount: number
paidAmount: number
remainingAmount: number
status: OrderStatus
deliveryAddress: string
expectedDeliveryDate: string
actualDeliveryDate?: string
notes?: string
createdAt: string
updatedAt: string
}
export type OrderStatus =
| 'pending' // 待确认
| 'confirmed' // 已确认
| 'preparing' // 准备中
| 'shipping' // 运输中
| 'delivered' // 已送达
| 'accepted' // 已验收
| 'completed' // 已完成
| 'cancelled' // 已取消
| 'refunded' // 已退款
export interface OrderListParams {
page?: number
pageSize?: number
orderNo?: string
buyerId?: number
supplierId?: number
status?: OrderStatus
startDate?: string
endDate?: string
}
export interface OrderCreateForm {
buyerId: number
supplierId: number
traderId?: number
cattleBreed: string
cattleCount: number
expectedWeight: number
unitPrice: number
deliveryAddress: string
expectedDeliveryDate: string
notes?: string
}
export interface OrderUpdateForm {
cattleBreed?: string
cattleCount?: number
expectedWeight?: number
actualWeight?: number
unitPrice?: number
deliveryAddress?: string
expectedDeliveryDate?: string
actualDeliveryDate?: string
notes?: string
status?: OrderStatus
}

View File

@@ -0,0 +1,52 @@
// 用户相关类型定义
export interface User {
id: number
username: string
email: string
phone?: string
avatar?: string
role: string
status: 'active' | 'inactive' | 'banned'
createdAt: string
updatedAt: string
}
export interface LoginForm {
username: string
password: string
captcha?: string
}
export interface LoginResponse {
access_token: string
token_type: string
expires_in: number
user: User
}
export interface UserListParams {
page?: number
pageSize?: number
keyword?: string
role?: string
status?: string
}
export interface UserCreateForm {
username: string
email: string
phone?: string
password: string
role: string
status: 'active' | 'inactive'
}
export interface UserUpdateForm {
username?: string
email?: string
phone?: string
role?: string
status?: 'active' | 'inactive' | 'banned'
avatar?: string
}

View File

@@ -0,0 +1,97 @@
import axios from 'axios'
import type { AxiosResponse, AxiosError } from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 添加认证token
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error: AxiosError) => {
ElMessage.error('请求配置错误')
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
// 检查业务状态码
if (data.success === false) {
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
return data
},
(error: AxiosError) => {
// 处理HTTP错误状态码
const { response } = error
if (response) {
switch (response.status) {
case 401:
ElMessage.error('未授权,请重新登录')
// 清除登录状态并跳转到登录页
const userStore = useUserStore()
userStore.logoutAction()
window.location.href = '/login'
break
case 403:
ElMessage.error('访问被拒绝,权限不足')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error(`请求失败: ${response.status}`)
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请稍后重试')
} else {
ElMessage.error('网络错误,请检查网络连接')
}
return Promise.reject(error)
}
)
export default request
// 通用API响应类型
export interface ApiResponse<T = any> {
success: boolean
data: T
message: string
timestamp: string
}
// 分页响应类型
export interface PaginatedResponse<T = any> {
items: T[]
total: number
page: number
pageSize: number
totalPages: number
}

View File

@@ -0,0 +1,416 @@
<template>
<div class="dashboard">
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-cards">
<el-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.key">
<el-card class="stat-card" :body-style="{ padding: '20px' }">
<div class="stat-content">
<div class="stat-icon" :style="{ backgroundColor: stat.color }">
<el-icon :size="24"><component :is="stat.icon" /></el-icon>
</div>
<div class="stat-info">
<div class="stat-number">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
<el-icon><component :is="stat.trend > 0 ? 'TrendCharts' : 'Bottom'" /></el-icon>
{{ Math.abs(stat.trend) }}%
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" class="charts-section">
<el-col :lg="16">
<el-card title="订单趋势" class="chart-card">
<div ref="orderTrendChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :lg="8">
<el-card title="订单状态分布" class="chart-card">
<div ref="orderStatusChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 数据表格区域 -->
<el-row :gutter="20" class="tables-section">
<el-col :lg="12">
<el-card title="最近订单" class="table-card">
<el-table :data="recentOrders" size="small">
<el-table-column prop="orderNo" label="订单号" width="120" />
<el-table-column prop="supplierName" label="供应商" />
<el-table-column prop="cattleCount" label="数量" width="80" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :lg="12">
<el-card title="供应商排行" class="table-card">
<el-table :data="topSuppliers" size="small">
<el-table-column type="index" label="排名" width="60" />
<el-table-column prop="name" label="供应商名称" />
<el-table-column prop="orderCount" label="订单数" width="80" />
<el-table-column prop="totalAmount" label="总金额" width="120">
<template #default="{ row }">
¥{{ row.totalAmount.toLocaleString() }}
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
// 统计数据
const stats = ref([
{
key: 'totalOrders',
label: '总订单数',
value: '1,234',
icon: 'ShoppingCart',
color: '#409EFF',
trend: 12.5
},
{
key: 'completedOrders',
label: '已完成订单',
value: '856',
icon: 'CircleCheck',
color: '#67C23A',
trend: 8.3
},
{
key: 'totalAmount',
label: '总交易额',
value: '¥2.34M',
icon: 'Money',
color: '#E6A23C',
trend: 15.7
},
{
key: 'activeSuppliers',
label: '活跃供应商',
value: '168',
icon: 'OfficeBuilding',
color: '#F56C6C',
trend: -2.1
}
])
// 最近订单
const recentOrders = ref([
{
orderNo: 'ORD20240101001',
supplierName: '山东畜牧合作社',
cattleCount: 50,
status: 'shipping'
},
{
orderNo: 'ORD20240101002',
supplierName: '河北养殖基地',
cattleCount: 30,
status: 'completed'
},
{
orderNo: 'ORD20240101003',
supplierName: '内蒙古牧场',
cattleCount: 80,
status: 'pending'
}
])
// 供应商排行
const topSuppliers = ref([
{
name: '山东畜牧合作社',
orderCount: 45,
totalAmount: 1250000
},
{
name: '河北养殖基地',
orderCount: 38,
totalAmount: 980000
},
{
name: '内蒙古牧场',
orderCount: 32,
totalAmount: 850000
}
])
// 图表元素引用
const orderTrendChart = ref<HTMLElement>()
const orderStatusChart = ref<HTMLElement>()
// 获取状态类型
const getStatusType = (status: string) => {
const statusMap: Record<string, string> = {
pending: 'warning',
confirmed: 'info',
shipping: 'primary',
completed: 'success',
cancelled: 'danger'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
pending: '待确认',
confirmed: '已确认',
shipping: '运输中',
completed: '已完成',
cancelled: '已取消'
}
return statusMap[status] || status
}
// 初始化订单趋势图表
const initOrderTrendChart = () => {
if (!orderTrendChart.value) return
const chart = echarts.init(orderTrendChart.value)
const option = {
title: {
text: '近30天订单趋势',
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['订单数量', '完成订单']
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
},
yAxis: {
type: 'value'
},
series: [
{
name: '订单数量',
type: 'line',
data: [120, 132, 101, 134, 90, 230, 210, 180, 160, 190, 200, 220],
smooth: true,
itemStyle: {
color: '#409EFF'
}
},
{
name: '完成订单',
type: 'line',
data: [100, 120, 90, 120, 80, 200, 190, 160, 140, 170, 180, 200],
smooth: true,
itemStyle: {
color: '#67C23A'
}
}
]
}
chart.setOption(option)
// 响应式调整
window.addEventListener('resize', () => {
chart.resize()
})
}
// 初始化订单状态图表
const initOrderStatusChart = () => {
if (!orderStatusChart.value) return
const chart = echarts.init(orderStatusChart.value)
const option = {
title: {
text: '订单状态分布',
textStyle: {
fontSize: 14,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
series: [
{
name: '订单状态',
type: 'pie',
radius: ['50%', '70%'],
data: [
{ value: 335, name: '已完成', itemStyle: { color: '#67C23A' } },
{ value: 210, name: '运输中', itemStyle: { color: '#409EFF' } },
{ value: 180, name: '已确认', itemStyle: { color: '#E6A23C' } },
{ value: 130, name: '待确认', itemStyle: { color: '#F56C6C' } },
{ value: 45, name: '已取消', itemStyle: { color: '#909399' } }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
chart.setOption(option)
// 响应式调整
window.addEventListener('resize', () => {
chart.resize()
})
}
onMounted(() => {
nextTick(() => {
initOrderTrendChart()
initOrderStatusChart()
})
})
</script>
<style lang="scss" scoped>
.dashboard {
.stats-cards {
margin-bottom: 20px;
}
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-right: 15px;
}
.stat-info {
flex: 1;
.stat-number {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.stat-trend {
font-size: 12px;
display: flex;
align-items: center;
gap: 2px;
&.up {
color: #67C23A;
}
&.down {
color: #F56C6C;
}
}
}
}
}
.charts-section {
margin-bottom: 20px;
}
.chart-card {
border-radius: 8px;
.chart-container {
height: 300px;
}
}
.tables-section {
.table-card {
border-radius: 8px;
:deep(.el-card__header) {
border-bottom: 1px solid #f0f0f0;
padding: 15px 20px;
.card-header {
font-size: 16px;
font-weight: 500;
color: #333;
}
}
}
}
}
@media (max-width: 768px) {
.dashboard {
.stats-cards {
.stat-card {
margin-bottom: 15px;
.stat-content {
.stat-icon {
width: 50px;
height: 50px;
margin-right: 10px;
}
.stat-info {
.stat-number {
font-size: 20px;
}
}
}
}
}
.chart-container {
height: 250px !important;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="finance-management">
<el-card>
<div class="page-header">
<h2>财务管理</h2>
<p>订单结算支付管理和财务报表</p>
</div>
<el-empty description="财务管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 财务管理页面
</script>
<style lang="scss" scoped>
.finance-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<h1 class="title">活牛采购智能数字化系统</h1>
</div>
<p class="subtitle">管理后台</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="rules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
size="large"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-btn"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<p class="demo-account">
<strong>演示账号</strong>
</p>
<div class="demo-list">
<el-tag @click="setDemoAccount('admin', 'admin123')">管理员: admin / admin123</el-tag>
<el-tag type="success" @click="setDemoAccount('buyer', 'buyer123')">采购人: buyer / buyer123</el-tag>
<el-tag type="warning" @click="setDemoAccount('trader', 'trader123')">贸易商: trader / trader123</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import type { LoginForm } from '@/types/user'
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref<FormInstance>()
const loading = ref(false)
// 表单数据
const loginForm = reactive<LoginForm>({
username: '',
password: ''
})
// 表单验证规则
const rules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
// 登录处理
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
loading.value = true
await userStore.loginAction(loginForm)
ElMessage.success('登录成功!')
router.push('/')
} catch (error) {
console.error('登录失败:', error)
} finally {
loading.value = false
}
}
// 设置演示账号
const setDemoAccount = (username: string, password: string) => {
loginForm.username = username
loginForm.password = password
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-box {
width: 100%;
max-width: 400px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.login-header {
text-align: center;
padding: 40px 40px 30px;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
.logo-img {
width: 40px;
height: 40px;
margin-right: 10px;
}
.title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
}
.subtitle {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
}
.login-form {
padding: 40px;
.login-btn {
width: 100%;
height: 44px;
font-size: 16px;
border-radius: 6px;
}
}
.login-footer {
padding: 0 40px 40px;
text-align: center;
.demo-account {
font-size: 14px;
color: #666;
margin-bottom: 15px;
}
.demo-list {
display: flex;
flex-direction: column;
gap: 10px;
.el-tag {
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
}
}
@media (max-width: 480px) {
.login-container {
padding: 10px;
}
.login-header {
padding: 30px 30px 20px;
.logo .title {
font-size: 16px;
}
}
.login-form {
padding: 30px;
}
.login-footer {
padding: 0 30px 30px;
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="order-management">
<el-card>
<div class="page-header">
<h2>订单管理</h2>
<p>管理活牛采购订单的全生命周期流程</p>
</div>
<el-empty description="订单管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 订单管理页面
</script>
<style lang="scss" scoped>
.order-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="quality-management">
<el-card>
<div class="page-header">
<h2>质量管理</h2>
<p>牛只质量检验检疫证明管理和质量追溯</p>
</div>
<el-empty description="质量管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 质量管理页面
</script>
<style lang="scss" scoped>
.quality-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="settings">
<el-card>
<div class="page-header">
<h2>系统设置</h2>
<p>系统配置权限管理和参数设置</p>
</div>
<el-empty description="系统设置功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 系统设置页面
</script>
<style lang="scss" scoped>
.settings {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="supplier-management">
<el-card>
<div class="page-header">
<h2>供应商管理</h2>
<p>管理供应商信息资质认证和绩效评估</p>
</div>
<el-empty description="供应商管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 供应商管理页面
</script>
<style lang="scss" scoped>
.supplier-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="transport-management">
<el-card>
<div class="page-header">
<h2>运输管理</h2>
<p>实时跟踪运输过程监控车辆位置和牛只状态</p>
</div>
<el-empty description="运输管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
// 运输管理页面
</script>
<style lang="scss" scoped>
.transport-management {
.page-header {
text-align: center;
margin-bottom: 30px;
h2 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
}
</style>

View File

@@ -0,0 +1,517 @@
<template>
<div class="user-management">
<el-card class="search-card">
<div class="search-form">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="用户名">
<el-input v-model="searchForm.keyword" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
<el-option label="管理员" value="admin" />
<el-option label="采购人" value="buyer" />
<el-option label="贸易商" value="trader" />
<el-option label="供应商" value="supplier" />
<el-option label="司机" value="driver" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="正常" value="active" />
<el-option label="禁用" value="inactive" />
<el-option label="封禁" value="banned" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>用户列表</span>
<el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
</div>
</template>
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="role" label="角色">
<template #default="{ row }">
<el-tag :type="getRoleType(row.role)">
{{ getRoleText(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="warning" size="small" @click="handleResetPassword(row)">重置密码</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<!-- 用户表单弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="80px"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色">
<el-option label="管理员" value="admin" />
<el-option label="采购人" value="buyer" />
<el-option label="贸易商" value="trader" />
<el-option label="供应商" value="supplier" />
<el-option label="司机" value="driver" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="正常" value="active" />
<el-option label="禁用" value="inactive" />
</el-select>
</el-form-item>
<el-form-item v-if="!form.id" label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { User, UserListParams, UserCreateForm, UserUpdateForm } from '@/types/user'
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 搜索表单
const searchForm = reactive<UserListParams>({
keyword: '',
role: '',
status: ''
})
// 分页配置
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<User[]>([])
const selectedUsers = ref<User[]>([])
// 表单数据
const form = reactive<UserCreateForm & { id?: number }>({
username: '',
email: '',
phone: '',
password: '',
role: '',
status: 'active'
})
// 对话框标题
const dialogTitle = ref('新增用户')
// 表单验证规则
const rules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
// 模拟数据
const mockUsers: User[] = [
{
id: 1,
username: 'admin',
email: 'admin@example.com',
phone: '13800138000',
role: 'admin',
status: 'active',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
},
{
id: 2,
username: 'buyer01',
email: 'buyer01@example.com',
phone: '13800138001',
role: 'buyer',
status: 'active',
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z'
},
{
id: 3,
username: 'supplier01',
email: 'supplier01@example.com',
phone: '13800138002',
role: 'supplier',
status: 'inactive',
createdAt: '2024-01-03T00:00:00Z',
updatedAt: '2024-01-03T00:00:00Z'
}
]
// 获取用户列表
const getUserList = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500))
// 简单的过滤逻辑
let filteredUsers = [...mockUsers]
if (searchForm.keyword) {
filteredUsers = filteredUsers.filter(user =>
user.username.includes(searchForm.keyword!) ||
user.email.includes(searchForm.keyword!)
)
}
if (searchForm.role) {
filteredUsers = filteredUsers.filter(user => user.role === searchForm.role)
}
if (searchForm.status) {
filteredUsers = filteredUsers.filter(user => user.status === searchForm.status)
}
// 分页处理
const start = (pagination.page - 1) * pagination.pageSize
const end = start + pagination.pageSize
tableData.value = filteredUsers.slice(start, end)
pagination.total = filteredUsers.length
} catch (error) {
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getUserList()
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
role: '',
status: ''
})
handleSearch()
}
// 新增用户
const handleAdd = () => {
dialogTitle.value = '新增用户'
resetForm()
dialogVisible.value = true
}
// 编辑用户
const handleEdit = (row: User) => {
dialogTitle.value = '编辑用户'
Object.assign(form, {
id: row.id,
username: row.username,
email: row.email,
phone: row.phone,
role: row.role,
status: row.status
})
dialogVisible.value = true
}
// 删除用户
const handleDelete = async (row: User) => {
try {
await ElMessageBox.confirm(
`确定要删除用户 "${row.username}" 吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 模拟删除
ElMessage.success('删除成功')
getUserList()
} catch (error) {
// 用户取消操作
}
}
// 重置密码
const handleResetPassword = async (row: User) => {
try {
await ElMessageBox.confirm(
`确定要重置用户 "${row.username}" 的密码吗?`,
'重置密码确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 模拟重置密码
ElMessage.success('密码重置成功新密码为123456')
} catch (error) {
// 用户取消操作
}
}
// 表格选择变化
const handleSelectionChange = (selection: User[]) => {
selectedUsers.value = selection
}
// 分页大小变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
pagination.page = 1
getUserList()
}
// 当前页变化
const handleCurrentChange = (page: number) => {
pagination.page = page
getUserList()
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
submitLoading.value = true
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
ElMessage.success(form.id ? '更新成功' : '创建成功')
dialogVisible.value = false
getUserList()
} catch (error) {
console.error('表单验证失败:', error)
} finally {
submitLoading.value = false
}
}
// 关闭对话框
const handleDialogClose = () => {
formRef.value?.resetFields()
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: undefined,
username: '',
email: '',
phone: '',
password: '',
role: '',
status: 'active'
})
}
// 获取角色类型
const getRoleType = (role: string) => {
const roleMap: Record<string, string> = {
admin: 'danger',
buyer: 'primary',
trader: 'success',
supplier: 'warning',
driver: 'info'
}
return roleMap[role] || 'info'
}
// 获取角色文本
const getRoleText = (role: string) => {
const roleMap: Record<string, string> = {
admin: '管理员',
buyer: '采购人',
trader: '贸易商',
supplier: '供应商',
driver: '司机'
}
return roleMap[role] || role
}
// 获取状态类型
const getStatusType = (status: string) => {
const statusMap: Record<string, string> = {
active: 'success',
inactive: 'warning',
banned: 'danger'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
active: '正常',
inactive: '禁用',
banned: '封禁'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
onMounted(() => {
getUserList()
})
</script>
<style lang="scss" scoped>
.user-management {
.search-card {
margin-bottom: 20px;
.search-form {
.demo-form-inline {
.el-form-item {
margin-right: 20px;
margin-bottom: 0;
}
}
}
}
.table-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
@media (max-width: 768px) {
.user-management {
.search-form {
.demo-form-inline {
.el-form-item {
margin-right: 0;
margin-bottom: 15px;
width: 100%;
}
}
}
.el-table {
.el-table-column {
&:nth-child(n+4) {
display: none;
}
}
}
}
}
</style>