添加 IntelliJ IDEA 项目配置文件
This commit is contained in:
38
admin-system/src/App.vue
Normal file
38
admin-system/src/App.vue
Normal 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>
|
||||
53
admin-system/src/api/order.ts
Normal file
53
admin-system/src/api/order.ts
Normal 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 })
|
||||
}
|
||||
53
admin-system/src/api/user.ts
Normal file
53
admin-system/src/api/user.ts
Normal 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 })
|
||||
}
|
||||
310
admin-system/src/layouts/index.vue
Normal file
310
admin-system/src/layouts/index.vue
Normal 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
25
admin-system/src/main.ts
Normal 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')
|
||||
132
admin-system/src/router/index.ts
Normal file
132
admin-system/src/router/index.ts
Normal 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
|
||||
119
admin-system/src/stores/user.ts
Normal file
119
admin-system/src/stores/user.ts
Normal 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
|
||||
}
|
||||
})
|
||||
191
admin-system/src/style/index.scss
Normal file
191
admin-system/src/style/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
184
admin-system/src/style/mixins.scss
Normal file
184
admin-system/src/style/mixins.scss
Normal 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);
|
||||
}
|
||||
65
admin-system/src/style/variables.scss
Normal file
65
admin-system/src/style/variables.scss
Normal 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;
|
||||
75
admin-system/src/types/order.ts
Normal file
75
admin-system/src/types/order.ts
Normal 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
|
||||
}
|
||||
52
admin-system/src/types/user.ts
Normal file
52
admin-system/src/types/user.ts
Normal 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
|
||||
}
|
||||
97
admin-system/src/utils/request.ts
Normal file
97
admin-system/src/utils/request.ts
Normal 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
|
||||
}
|
||||
416
admin-system/src/views/dashboard/index.vue
Normal file
416
admin-system/src/views/dashboard/index.vue
Normal 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>
|
||||
34
admin-system/src/views/finance/index.vue
Normal file
34
admin-system/src/views/finance/index.vue
Normal 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>
|
||||
237
admin-system/src/views/login/index.vue
Normal file
237
admin-system/src/views/login/index.vue
Normal 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>
|
||||
34
admin-system/src/views/order/index.vue
Normal file
34
admin-system/src/views/order/index.vue
Normal 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>
|
||||
34
admin-system/src/views/quality/index.vue
Normal file
34
admin-system/src/views/quality/index.vue
Normal 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>
|
||||
34
admin-system/src/views/settings/index.vue
Normal file
34
admin-system/src/views/settings/index.vue
Normal 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>
|
||||
34
admin-system/src/views/supplier/index.vue
Normal file
34
admin-system/src/views/supplier/index.vue
Normal 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>
|
||||
34
admin-system/src/views/transport/index.vue
Normal file
34
admin-system/src/views/transport/index.vue
Normal 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>
|
||||
517
admin-system/src/views/user/index.vue
Normal file
517
admin-system/src/views/user/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user