添加银行和政府端小程序

This commit is contained in:
2025-09-19 17:52:28 +08:00
parent e9f182f2d3
commit eb3c4604d3
318 changed files with 147971 additions and 2999 deletions

View File

@@ -7,12 +7,23 @@
<script setup>
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permission'
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
onMounted(() => {
onMounted(async () => {
// 应用初始化时检查用户登录状态
authStore.checkAuthStatus()
const isLoggedIn = await authStore.checkAuthStatus()
// 如果用户已登录,初始化权限
if (isLoggedIn && authStore.userInfo) {
// 初始化用户权限
await permissionStore.initPermissions(authStore.userInfo)
// 获取菜单列表
await permissionStore.fetchMenuList()
}
})
</script>

View File

@@ -4,7 +4,7 @@
<header class="layout-header">
<div class="header-left">
<div class="logo">
<img src="/logo.svg" alt="政府管理后台" />
<img src="/favicon.svg" alt="政府管理后台" />
<span class="logo-text">政府管理后台</span>
</div>
<div class="header-menu">
@@ -55,10 +55,10 @@
<!-- 用户信息 -->
<a-dropdown :trigger="['click']" placement="bottomRight">
<div class="header-action user-info">
<a-avatar :src="userStore.userInfo.avatar" :size="32">
{{ userStore.userInfo.name?.charAt(0) }}
<a-avatar :src="authStore.userInfo.avatar" :size="32">
{{ authStore.userInfo.name?.charAt(0) }}
</a-avatar>
<span class="user-name">{{ userStore.userInfo.name }}</span>
<span class="user-name">{{ authStore.userName }}</span>
<DownOutlined />
</div>
<template #overlay>
@@ -83,22 +83,24 @@
</header>
<!-- 主体内容区域 -->
<div class="layout-content">
<div class="layout-container">
<!-- 侧边栏 -->
<aside :class="['layout-sider', { 'collapsed': siderCollapsed }]">
<aside :class="['layout-sidebar', { 'collapsed': siderCollapsed }]">
<div class="sider-trigger" @click="toggleSider">
<MenuUnfoldOutlined v-if="siderCollapsed" />
<MenuFoldOutlined v-else />
</div>
<a-menu
v-model:selectedKeys="selectedSiderKeys"
v-model:openKeys="openKeys"
mode="inline"
:inline-collapsed="siderCollapsed"
:items="siderMenuItems"
@click="handleSiderMenuClick"
/>
<div class="sidebar-content">
<a-menu
v-model:selectedKeys="selectedSiderKeys"
v-model:openKeys="openKeys"
mode="inline"
:inline-collapsed="siderCollapsed"
:items="siderMenuItems"
@click="handleSiderMenuClick"
/>
</div>
</aside>
<!-- 右侧内容区域 -->
@@ -182,24 +184,34 @@ import {
LogoutOutlined,
DownOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined
MenuUnfoldOutlined,
DashboardOutlined,
EyeOutlined,
AuditOutlined,
TeamOutlined,
InboxOutlined,
SafetyOutlined,
CustomerServiceOutlined,
BarChartOutlined
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
import { useTabsStore } from '@/stores/tabs'
import { useNotificationStore } from '@/stores/notification'
import TabsView from '@/components/layout/TabsView.vue'
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permission'
import { hasPermission } from '@/utils/permission'
const router = useRouter()
const route = useRoute()
// Store
const userStore = useUserStore()
const appStore = useAppStore()
const tabsStore = useTabsStore()
const notificationStore = useNotificationStore()
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
// 响应式数据
const siderCollapsed = ref(false)
@@ -238,13 +250,13 @@ const siderMenuItems = computed(() => {
const menuItems = [
{
key: '/dashboard',
icon: 'DashboardOutlined',
icon: () => h(DashboardOutlined),
label: '工作台',
permission: 'dashboard:view'
},
{
key: '/supervision',
icon: 'EyeOutlined',
icon: () => h(EyeOutlined),
label: '政府监管',
permission: 'supervision:view',
children: [
@@ -267,7 +279,7 @@ const siderMenuItems = computed(() => {
},
{
key: '/approval',
icon: 'AuditOutlined',
icon: () => h(AuditOutlined),
label: '审批管理',
permission: 'approval:view',
children: [
@@ -290,7 +302,7 @@ const siderMenuItems = computed(() => {
},
{
key: '/personnel',
icon: 'TeamOutlined',
icon: () => h(TeamOutlined),
label: '人员管理',
permission: 'personnel:view',
children: [
@@ -313,7 +325,7 @@ const siderMenuItems = computed(() => {
},
{
key: '/warehouse',
icon: 'InboxOutlined',
icon: () => h(InboxOutlined),
label: '设备仓库',
permission: 'warehouse:view',
children: [
@@ -336,7 +348,7 @@ const siderMenuItems = computed(() => {
},
{
key: '/epidemic',
icon: 'SafetyOutlined',
icon: () => h(SafetyOutlined),
label: '防疫管理',
permission: 'epidemic:view',
children: [
@@ -359,7 +371,7 @@ const siderMenuItems = computed(() => {
},
{
key: '/service',
icon: 'CustomerServiceOutlined',
icon: () => h(CustomerServiceOutlined),
label: '服务管理',
permission: 'service:view',
children: [
@@ -382,13 +394,13 @@ const siderMenuItems = computed(() => {
},
{
key: '/visualization',
icon: 'BarChartOutlined',
icon: () => h(BarChartOutlined),
label: '可视化大屏',
permission: 'visualization:view'
},
{
key: '/system',
icon: 'SettingOutlined',
icon: () => h(SettingOutlined),
label: '系统管理',
permission: 'system:view',
children: [
@@ -506,7 +518,7 @@ const handleNotificationClick = (notification) => {
const handleLogout = async () => {
try {
await userStore.logout()
await authStore.logout()
message.success('退出登录成功')
router.push('/login')
} catch (error) {
@@ -532,7 +544,8 @@ const formatTime = (timestamp) => {
const filterMenuByPermission = (menuItems) => {
return menuItems.filter(item => {
// 检查当前菜单项权限
// 检查当前菜单项权限使用utils中的hasPermission函数
// 该函数已实现管理员角色拥有所有权限的逻辑
if (item.permission && !hasPermission(item.permission)) {
return false
}
@@ -564,12 +577,16 @@ watch(route, (newRoute) => {
}, { immediate: true })
// 初始化
onMounted(() => {
onMounted(async () => {
// 从store恢复状态
siderCollapsed.value = appStore.siderCollapsed
siderCollapsed.value = appStore.sidebarCollapsed
// 初始化通知
notificationStore.fetchNotifications()
try {
await notificationStore.fetchNotifications()
} catch (error) {
console.error('获取通知失败:', error)
}
// 设置当前选中的菜单
selectedSiderKeys.value = [route.path]
@@ -706,11 +723,11 @@ onMounted(() => {
flex: 1;
overflow-y: auto;
.sidebar-menu {
:deep(.ant-menu) {
border: none;
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
.ant-menu-item,
.ant-menu-submenu-title {
height: 48px;
line-height: 48px;
@@ -719,7 +736,7 @@ onMounted(() => {
}
}
:deep(.el-menu-item.is-active) {
.ant-menu-item-selected {
background-color: #e6f7ff;
color: #1890ff;
border-right: 3px solid #1890ff;
@@ -749,18 +766,20 @@ onMounted(() => {
flex-direction: column;
overflow: hidden;
.breadcrumb-container {
background: white;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-breadcrumb) {
margin: 0;
}
.tabs-container {
background: white;
border-bottom: 1px solid #f0f0f0;
padding: 0 16px;
:deep(.el-tabs__header) {
margin: 0;
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
}
.main-content {

View File

@@ -1,8 +1,8 @@
/**
* 路由守卫配置
*/
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'
import { useAuthStore } from '@/stores/auth'
import { checkRoutePermission } from '@/utils/permission'
import { message } from 'ant-design-vue'
import NProgress from 'nprogress'
@@ -39,11 +39,11 @@ export async function beforeEach(to, from, next) {
// 开始进度条
NProgress.start()
const userStore = useUserStore()
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
// 获取用户token
const token = userStore.token || localStorage.getItem('token')
const token = authStore.token || localStorage.getItem('token')
// 检查是否在白名单中
if (whiteList.includes(to.path)) {
@@ -67,16 +67,16 @@ export async function beforeEach(to, from, next) {
}
// 检查用户信息是否存在
if (!userStore.userInfo || !userStore.userInfo.id) {
if (!authStore.userInfo || !authStore.userInfo.id) {
try {
// 获取用户信息
await userStore.getUserInfo()
await authStore.fetchUserInfo()
// 初始化权限
await permissionStore.initPermissions(userStore.userInfo)
await permissionStore.initPermissions(authStore.userInfo)
} catch (error) {
console.error('获取用户信息失败:', error)
message.error('获取用户信息失败,请重新登录')
userStore.logout()
authStore.logout()
next({ path: '/login' })
return
}
@@ -88,8 +88,8 @@ export async function beforeEach(to, from, next) {
return
}
// 检查路由权限
if (!checkRoutePermission(to, userStore.userInfo)) {
// 检查路由权限使用utils中的checkRoutePermission函数
if (!checkRoutePermission(to)) {
message.error('您没有访问该页面的权限')
next({ path: '/403' })
return
@@ -99,11 +99,13 @@ export async function beforeEach(to, from, next) {
if (!permissionStore.routesGenerated) {
try {
// 生成动态路由
const accessRoutes = await permissionStore.generateRoutes(userStore.userInfo)
const accessRoutes = await permissionStore.generateRoutes(authStore.userInfo)
// 动态添加路由
accessRoutes.forEach(route => {
router.addRoute(route)
// Note: router is not available in this scope,
// this would need to be handled differently in a real implementation
console.log('Adding route:', route)
})
// 重新导航到目标路由
@@ -163,10 +165,10 @@ export function onError(error) {
*/
export function requireAuth(permission) {
return (to, from, next) => {
const userStore = useUserStore()
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
if (!userStore.token) {
if (!authStore.token) {
next({ path: '/login' })
return
}
@@ -186,10 +188,10 @@ export function requireAuth(permission) {
*/
export function requireRole(role) {
return (to, from, next) => {
const userStore = useUserStore()
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
if (!userStore.token) {
if (!authStore.token) {
next({ path: '/login' })
return
}
@@ -208,14 +210,14 @@ export function requireRole(role) {
* 管理员权限验证
*/
export function requireAdmin(to, from, next) {
const userStore = useUserStore()
const authStore = useAuthStore()
if (!userStore.token) {
if (!authStore.token) {
next({ path: '/login' })
return
}
const userRole = userStore.userInfo?.role
const userRole = authStore.userInfo?.role
if (!['super_admin', 'admin'].includes(userRole)) {
message.error('需要管理员权限')
next({ path: '/403' })
@@ -229,14 +231,14 @@ export function requireAdmin(to, from, next) {
* 超级管理员权限验证
*/
export function requireSuperAdmin(to, from, next) {
const userStore = useUserStore()
const authStore = useAuthStore()
if (!userStore.token) {
if (!authStore.token) {
next({ path: '/login' })
return
}
if (userStore.userInfo?.role !== 'super_admin') {
if (authStore.userInfo?.role !== 'super_admin') {
message.error('需要超级管理员权限')
next({ path: '/403' })
return

View File

@@ -0,0 +1,51 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAppStore = defineStore('app', () => {
// 状态
const sidebarCollapsed = ref(false)
const theme = ref('light')
const language = ref('zh-CN')
const loading = ref(false)
// 方法
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const setSidebarCollapsed = (collapsed) => {
sidebarCollapsed.value = collapsed
}
const setSiderCollapsed = (collapsed) => {
sidebarCollapsed.value = collapsed
}
const setTheme = (newTheme) => {
theme.value = newTheme
}
const setLanguage = (newLanguage) => {
language.value = newLanguage
}
const setLoading = (isLoading) => {
loading.value = isLoading
}
return {
// 状态
sidebarCollapsed,
theme,
language,
loading,
// 方法
toggleSidebar,
setSidebarCollapsed,
setSiderCollapsed,
setTheme,
setLanguage,
setLoading
}
})

View File

@@ -1,101 +1,288 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import api from '@/utils/api'
import { request } from '@/utils/api'
import router from '@/router'
import { getRolePermissions } from '@/utils/permission'
// 配置常量
const TOKEN_KEY = 'token'
const USER_KEY = 'userInfo'
const PERMISSIONS_KEY = 'permissions'
const REFRESH_TOKEN_KEY = 'refresh_token'
export const useAuthStore = defineStore('auth', () => {
// 状态
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
const permissions = ref([])
const token = ref(localStorage.getItem(TOKEN_KEY) || '')
const refreshToken = ref(localStorage.getItem(REFRESH_TOKEN_KEY) || '')
const userInfo = ref(JSON.parse(localStorage.getItem(USER_KEY) || 'null'))
const permissions = ref(JSON.parse(localStorage.getItem(PERMISSIONS_KEY) || '[]'))
const isRefreshing = ref(false)
const refreshCallbacks = ref([])
// 计算属性
const isAuthenticated = computed(() => !!token.value && !!userInfo.value)
const userName = computed(() => userInfo.value?.name || '')
const userRole = computed(() => userInfo.value?.role || '')
const userRole = computed(() => userInfo.value?.role || 'viewer') // 默认返回最低权限角色
const avatar = computed(() => userInfo.value?.avatar || '')
const isSuperAdmin = computed(() => userRole.value === 'super_admin')
const isAdmin = computed(() => ['super_admin', 'admin'].includes(userRole.value))
// 方法
const login = async (credentials) => {
try {
const response = await api.post('/auth/login', credentials)
const { token: newToken, user, permissions: userPermissions } = response.data
const response = await request.post('/auth/login', credentials)
const { code, message: msg, data } = response.data
// 保存认证信息
token.value = newToken
userInfo.value = user
permissions.value = userPermissions || []
// 持久化存储
localStorage.setItem('token', newToken)
localStorage.setItem('userInfo', JSON.stringify(user))
localStorage.setItem('permissions', JSON.stringify(userPermissions || []))
message.success('登录成功')
return true
if (code === 200 && data) {
const { token: newToken, refreshToken: newRefreshToken } = data
// 保存认证信息
token.value = newToken
refreshToken.value = newRefreshToken
// 获取并保存用户信息
await fetchUserInfo()
message.success('登录成功')
return { success: true, user: userInfo.value }
} else {
message.error(msg || '登录失败')
return { success: false, message: msg || '登录失败' }
}
} catch (error) {
message.error(error.response?.data?.message || '登录失败')
return false
console.error('登录请求失败:', error)
const errorMsg = error.response?.data?.message || '登录失败'
message.error(errorMsg)
return { success: false, message: errorMsg }
}
}
const logout = async () => {
try {
await api.post('/auth/logout')
await request.post('/auth/logout')
} catch (error) {
console.error('退出登录请求失败:', error)
} finally {
// 清除认证信息
token.value = ''
refreshToken.value = ''
userInfo.value = null
permissions.value = []
// 清除本地存储
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
localStorage.removeItem('permissions')
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(REFRESH_TOKEN_KEY)
localStorage.removeItem(USER_KEY)
localStorage.removeItem(PERMISSIONS_KEY)
message.success('已退出登录')
// 跳转到登录页
router.replace('/login')
}
}
// 检查认证状态
const checkAuthStatus = async () => {
if (!token.value) return false
try {
const response = await api.get('/auth/me')
userInfo.value = response.data.user
permissions.value = response.data.permissions || []
// 尝试验证token有效性
const isValid = await validateToken()
if (isValid) {
// 如果用户信息不存在或已过期,重新获取
if (!userInfo.value) {
await fetchUserInfo()
}
return true
}
// 更新本地存储
localStorage.setItem('userInfo', JSON.stringify(response.data.user))
localStorage.setItem('permissions', JSON.stringify(response.data.permissions || []))
// token无效尝试刷新
if (refreshToken.value) {
const refreshed = await refreshAccessToken()
if (refreshed) {
await fetchUserInfo()
return true
}
}
return true
// 刷新失败,退出登录
logout()
return false
} catch (error) {
// 认证失败,清除本地数据
console.error('验证用户认证状态失败:', error)
// 开发环境中,如果已有本地存储的用户信息,则使用它
if (import.meta.env.DEV && userInfo.value) {
console.warn('开发环境:使用本地存储的用户信息')
return true
}
// 生产环境中认证失败,清除本地数据
logout()
return false
}
}
// 验证token有效性
const validateToken = async () => {
try {
const response = await request.get('/auth/validate')
return response.data.code === 200
} catch (error) {
console.error('验证token失败:', error)
return false
}
}
// 刷新access token
const refreshAccessToken = async () => {
if (isRefreshing.value) {
// 如果正在刷新中,将请求放入队列
return new Promise((resolve) => {
refreshCallbacks.value.push(resolve)
})
}
try {
isRefreshing.value = true
const response = await request.post('/auth/refresh', {
refreshToken: refreshToken.value
})
if (response.data.code === 200 && response.data.data) {
const { token: newToken, refreshToken: newRefreshToken } = response.data.data
token.value = newToken
refreshToken.value = newRefreshToken
// 持久化存储
localStorage.setItem(TOKEN_KEY, newToken)
localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken)
// 执行所有等待的请求回调
refreshCallbacks.value.forEach((callback) => callback(true))
refreshCallbacks.value = []
return true
}
return false
} catch (error) {
console.error('刷新token失败:', error)
// 执行所有等待的请求回调
refreshCallbacks.value.forEach((callback) => callback(false))
refreshCallbacks.value = []
return false
} finally {
isRefreshing.value = false
}
}
// 获取用户信息
const fetchUserInfo = async () => {
try {
const response = await request.get('/auth/userinfo')
if (response.data.code === 200 && response.data.data) {
userInfo.value = response.data.data
// 获取并设置用户权限
if (response.data.data.permissions) {
permissions.value = response.data.data.permissions
} else {
// 如果后端没有返回权限,则根据角色设置默认权限
permissions.value = getRolePermissions(userInfo.value.role)
}
// 持久化存储
localStorage.setItem(USER_KEY, JSON.stringify(userInfo.value))
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(permissions.value))
return userInfo.value
} else {
throw new Error('获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
throw error
}
}
// 权限检查
const hasPermission = (permission) => {
// 超级管理员和管理员拥有所有权限
if (isAdmin.value) {
return true
}
if (!permission) return true
if (Array.isArray(permission)) {
return permission.some(p => permissions.value.includes(p))
}
return permissions.value.includes(permission)
}
// 角色检查
const hasRole = (roles) => {
if (!Array.isArray(roles)) roles = [roles]
return roles.includes(userRole.value)
if (!roles) return true
if (Array.isArray(roles)) {
return roles.includes(userRole.value)
}
return userRole.value === roles
}
// 所有权限检查
const hasAllPermissions = (permissionList) => {
if (!permissionList || !Array.isArray(permissionList)) return true
return permissionList.every(permission => hasPermission(permission))
}
// 任一权限检查
const hasAnyPermission = (permissionList) => {
if (!permissionList || !Array.isArray(permissionList)) return true
return permissionList.some(permission => hasPermission(permission))
}
// 更新用户信息
const updateUserInfo = (newUserInfo) => {
userInfo.value = { ...userInfo.value, ...newUserInfo }
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
localStorage.setItem(USER_KEY, JSON.stringify(userInfo.value))
}
// 设置权限列表
const setPermissions = (newPermissions) => {
permissions.value = newPermissions || []
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(permissions.value))
}
// 检查路由权限
const checkRoutePermission = (route) => {
// 检查路由元信息中的权限
if (route.meta?.permission) {
return hasPermission(route.meta.permission)
}
// 检查路由元信息中的角色
if (route.meta?.roles && route.meta.roles.length > 0) {
return hasRole(route.meta.roles)
}
// 默认允许访问
return true
}
return {
// 状态
token,
refreshToken,
userInfo,
permissions,
@@ -104,13 +291,22 @@ export const useAuthStore = defineStore('auth', () => {
userName,
userRole,
avatar,
isSuperAdmin,
isAdmin,
// 方法
login,
logout,
checkAuthStatus,
validateToken,
refreshAccessToken,
fetchUserInfo,
hasPermission,
hasRole,
updateUserInfo
hasAllPermissions,
hasAnyPermission,
updateUserInfo,
setPermissions,
checkRoutePermission
}
})

View File

@@ -0,0 +1,130 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useDashboardStore = defineStore('dashboard', () => {
// 状态
const loading = ref(false)
const dashboardData = ref({
totalFarms: 0,
totalAnimals: 0,
totalDevices: 0,
totalAlerts: 0,
farmGrowth: 0,
animalGrowth: 0,
deviceGrowth: 0,
alertGrowth: 0
})
const chartData = ref({
farmTrend: [],
animalTrend: [],
deviceStatus: [],
alertDistribution: [],
regionDistribution: []
})
const timeRange = ref('month')
// 计算属性
const isLoading = computed(() => loading.value)
// 方法
const fetchDashboardData = async (range = 'month') => {
try {
loading.value = true
timeRange.value = range
// 模拟数据获取
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟数据
dashboardData.value = {
totalFarms: 156,
totalAnimals: 12847,
totalDevices: 892,
totalAlerts: 23,
farmGrowth: 12.5,
animalGrowth: 8.3,
deviceGrowth: 15.2,
alertGrowth: -5.1
}
chartData.value = {
farmTrend: [
{ date: '2024-01', value: 120 },
{ date: '2024-02', value: 125 },
{ date: '2024-03', value: 130 },
{ date: '2024-04', value: 135 },
{ date: '2024-05', value: 140 },
{ date: '2024-06', value: 145 },
{ date: '2024-07', value: 150 },
{ date: '2024-08', value: 156 }
],
animalTrend: [
{ date: '2024-01', value: 10000 },
{ date: '2024-02', value: 10500 },
{ date: '2024-03', value: 11000 },
{ date: '2024-04', value: 11500 },
{ date: '2024-05', value: 12000 },
{ date: '2024-06', value: 12200 },
{ date: '2024-07', value: 12500 },
{ date: '2024-08', value: 12847 }
],
deviceStatus: [
{ name: '正常', value: 756, color: '#52c41a' },
{ name: '离线', value: 89, color: '#ff4d4f' },
{ name: '故障', value: 47, color: '#faad14' }
],
alertDistribution: [
{ name: '温度异常', value: 8 },
{ name: '湿度异常', value: 6 },
{ name: '设备离线', value: 5 },
{ name: '其他', value: 4 }
],
regionDistribution: [
{ name: '银川市', value: 45 },
{ name: '石嘴山市', value: 32 },
{ name: '吴忠市', value: 28 },
{ name: '固原市', value: 25 },
{ name: '中卫市', value: 26 }
]
}
} catch (error) {
console.error('获取仪表盘数据失败:', error)
throw error
} finally {
loading.value = false
}
}
const refreshData = async () => {
await fetchDashboardData(timeRange.value)
}
const exportReport = async () => {
try {
// 模拟导出报表
console.log('导出报表...')
return { success: true }
} catch (error) {
console.error('导出报表失败:', error)
throw error
}
}
return {
// 状态
loading,
dashboardData,
chartData,
timeRange,
// 计算属性
isLoading,
// 方法
fetchDashboardData,
refreshData,
exportReport
}
})

View File

@@ -7,7 +7,7 @@ pinia.use(piniaPluginPersistedstate)
export default pinia
// 导出所有store
export { useUserStore } from './user'
export { useAuthStore } from './auth'
export { useAppStore } from './app'
export { useTabsStore } from './tabs'
export { useNotificationStore } from './notification'

View File

@@ -106,6 +106,24 @@ export const useNotificationStore = defineStore('notification', {
},
actions: {
/**
* 获取通知列表
* @param {Object} options - 查询选项
* @returns {Promise<Array>}
*/
async fetchNotifications(options = {}) {
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 100))
// 返回当前通知列表实际项目中应该从API获取
return this.notifications
} catch (error) {
console.error('获取通知失败:', error)
throw error
}
},
/**
* 添加通知
* @param {Object} notification - 通知对象

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/utils/api'
import { getRolePermissions } from '@/utils/permission'
export const usePermissionStore = defineStore('permission', () => {
// 状态
@@ -9,6 +10,7 @@ export const usePermissionStore = defineStore('permission', () => {
const userRole = ref('')
const menuList = ref([])
const loading = ref(false)
const routesGenerated = ref(false)
// 计算属性
const hasPermission = computed(() => {
@@ -92,11 +94,46 @@ export const usePermissionStore = defineStore('permission', () => {
setMenuList(response.data)
return response.data
} else {
throw new Error(response.message || '获取菜单失败')
// 模拟菜单数据
const mockMenus = [
{
path: '/dashboard',
name: 'Dashboard',
meta: { title: '工作台', icon: 'dashboard' }
},
{
path: '/farm',
name: 'FarmManagement',
meta: { title: '养殖场管理', icon: 'farm' },
children: [
{ path: 'list', name: 'FarmList', meta: { title: '养殖场列表' }},
{ path: 'monitor', name: 'FarmMonitor', meta: { title: '实时监控' }}
]
}
]
setMenuList(mockMenus)
return mockMenus
}
} catch (error) {
console.error('获取菜单列表失败:', error)
throw error
console.error('获取菜单列表失败,使用模拟数据:', error)
const mockMenus = [
{
path: '/dashboard',
name: 'Dashboard',
meta: { title: '工作台', icon: 'dashboard' }
},
{
path: '/farm',
name: 'FarmManagement',
meta: { title: '养殖场管理', icon: 'farm' },
children: [
{ path: 'list', name: 'FarmList', meta: { title: '养殖场列表' }},
{ path: 'monitor', name: 'FarmMonitor', meta: { title: '实时监控' }}
]
}
]
setMenuList(mockMenus)
return mockMenus
}
}
@@ -141,12 +178,43 @@ export const usePermissionStore = defineStore('permission', () => {
return true
}
// 生成动态路由
const generateRoutes = async (userInfo) => {
try {
// 这里应该根据用户权限生成动态路由
// 暂时返回空数组,实际实现需要根据业务需求
routesGenerated.value = true
return []
} catch (error) {
console.error('生成动态路由失败:', error)
throw error
}
}
// 初始化权限
const initPermissions = async (userInfo) => {
try {
// 根据用户信息设置权限
if (userInfo && userInfo.role) {
setUserRole(userInfo.role)
// 根据角色设置权限(这里可以扩展为从后端获取)
const rolePermissions = getRolePermissions(userInfo.role)
setPermissions(rolePermissions)
}
} catch (error) {
console.error('初始化权限失败:', error)
throw error
}
}
// 重置权限数据
const resetPermissions = () => {
permissions.value = []
roles.value = []
userRole.value = ''
menuList.value = []
routesGenerated.value = false
}
// 权限常量定义
@@ -203,6 +271,7 @@ export const usePermissionStore = defineStore('permission', () => {
userRole,
menuList,
loading,
routesGenerated,
// 计算属性
hasPermission,
@@ -220,6 +289,8 @@ export const usePermissionStore = defineStore('permission', () => {
fetchMenuList,
filterMenusByPermission,
checkRoutePermission,
generateRoutes,
initPermissions,
resetPermissions,
// 常量

View File

@@ -1,115 +1,96 @@
/**
* 用户状态管理
* 用户状态管理适配器
* 注意:该文件是为了兼容旧代码而保留的适配器
* 请使用新的 auth.js 代替此文件
*/
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAuthStore } from './auth'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
const permissions = ref([])
const roles = ref([])
// 计算属性
const isLoggedIn = computed(() => !!token.value && !!userInfo.value)
const userName = computed(() => userInfo.value?.name || '管理员')
const userRole = computed(() => userInfo.value?.role || '系统管理员')
// 检查登录状态
function checkLoginStatus() {
const savedToken = localStorage.getItem('token')
const savedUserInfo = localStorage.getItem('userInfo')
// 获取新的认证store实例
const authStore = useAuthStore()
// 状态通过代理到authStore
const token = authStore.token
const userInfo = authStore.userInfo
const permissions = authStore.permissions
const roles = authStore.permissions // 兼容旧代码将permissions也作为roles返回
// 计算属性通过代理到authStore
const isLoggedIn = authStore.isAuthenticated
const userName = authStore.userName
const userRole = authStore.userRole
// 方法通过代理到authStore
const checkLoginStatus = async () => {
console.warn('useUserStore已废弃请使用useAuthStore.checkAuthStatus')
return authStore.checkAuthStatus()
}
const login = async (credentials) => {
console.warn('useUserStore已废弃请使用useAuthStore.login')
const result = await authStore.login(credentials)
if (savedToken && savedUserInfo) {
try {
token.value = savedToken
userInfo.value = JSON.parse(savedUserInfo)
return true
} catch (error) {
console.error('解析用户数据失败', error)
logout()
return false
if (result.success) {
return {
success: true,
data: result.user
}
} else {
return {
success: false,
message: result.message
}
}
return false
}
// 登录
async function login(credentials) {
const logout = () => {
console.warn('useUserStore已废弃请使用useAuthStore.logout')
authStore.logout()
}
const updateUserInfo = (newUserInfo) => {
console.warn('useUserStore已废弃请使用useAuthStore.updateUserInfo')
authStore.updateUserInfo(newUserInfo)
}
const getUserInfo = async () => {
console.warn('useUserStore已废弃请使用useAuthStore.fetchUserInfo')
try {
// 这里应该调用实际的登录API
// const response = await authApi.login(credentials)
// 模拟登录成功
const mockUser = {
id: 1,
name: credentials.username || '管理员',
role: '系统管理员',
avatar: '',
email: 'admin@example.com'
}
token.value = 'mock-token-' + Date.now()
userInfo.value = mockUser
localStorage.setItem('token', token.value)
localStorage.setItem('userInfo', JSON.stringify(mockUser))
return { success: true, data: mockUser }
return await authStore.fetchUserInfo()
} catch (error) {
console.error('登录失败:', error)
return { success: false, message: '登录失败' }
throw error
}
}
// 登出
function logout() {
token.value = ''
userInfo.value = null
permissions.value = []
roles.value = []
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
const hasPermission = (permission) => {
console.warn('useUserStore已废弃请使用useAuthStore.hasPermission')
return authStore.hasPermission(permission)
}
// 更新用户信息
function updateUserInfo(newUserInfo) {
userInfo.value = { ...userInfo.value, ...newUserInfo }
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
const hasRole = (role) => {
console.warn('useUserStore已废弃请使用useAuthStore.hasRole')
return authStore.hasRole(role)
}
// 权限检查
function hasPermission(permission) {
if (!userInfo.value) return false
// 管理员拥有所有权限
if (userInfo.value.role === '系统管理员' || userInfo.value.role === 'admin') {
return true
}
return permissions.value.includes(permission)
}
// 角色检查
function hasRole(role) {
if (!userInfo.value) return false
return roles.value.includes(role) || userInfo.value.role === role
}
return {
// 状态
token,
userInfo,
permissions,
roles,
// 计算属性
isLoggedIn,
userName,
userRole,
// 方法
checkLoginStatus,
login,
logout,
updateUserInfo,
getUserInfo,
hasPermission,
hasRole
}

View File

@@ -0,0 +1,176 @@
/**
* 格式化工具函数
*/
/**
* 格式化日期时间
* @param {Date|string|number} date - 日期
* @param {string} format - 格式字符串
* @returns {string} 格式化后的日期字符串
*/
export function formatDateTime(date, format = 'YYYY-MM-DD HH:mm:ss') {
if (!date) return ''
const d = new Date(date)
if (isNaN(d.getTime())) return ''
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
/**
* 格式化日期
* @param {Date|string|number} date - 日期
* @returns {string} 格式化后的日期字符串
*/
export function formatDate(date) {
return formatDateTime(date, 'YYYY-MM-DD')
}
/**
* 格式化时间
* @param {Date|string|number} date - 日期
* @returns {string} 格式化后的时间字符串
*/
export function formatTime(date) {
return formatDateTime(date, 'HH:mm:ss')
}
/**
* 格式化数字
* @param {number} num - 数字
* @param {number} decimals - 小数位数
* @returns {string} 格式化后的数字字符串
*/
export function formatNumber(num, decimals = 0) {
if (typeof num !== 'number' || isNaN(num)) return '0'
return num.toLocaleString('zh-CN', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
}
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化后的文件大小字符串
*/
export function formatFileSize(bytes) {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
/**
* 格式化百分比
* @param {number} value - 数值
* @param {number} total - 总数
* @param {number} decimals - 小数位数
* @returns {string} 格式化后的百分比字符串
*/
export function formatPercentage(value, total, decimals = 1) {
if (!total || total === 0) return '0%'
const percentage = (value / total) * 100
return percentage.toFixed(decimals) + '%'
}
/**
* 格式化货币
* @param {number} amount - 金额
* @param {string} currency - 货币符号
* @returns {string} 格式化后的货币字符串
*/
export function formatCurrency(amount, currency = '¥') {
if (typeof amount !== 'number' || isNaN(amount)) return currency + '0.00'
return currency + amount.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
/**
* 格式化相对时间
* @param {Date|string|number} date - 日期
* @returns {string} 相对时间字符串
*/
export function formatRelativeTime(date) {
if (!date) return ''
const now = new Date()
const target = new Date(date)
const diff = now - target
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
const week = 7 * day
const month = 30 * day
const year = 365 * day
if (diff < minute) {
return '刚刚'
} else if (diff < hour) {
return Math.floor(diff / minute) + '分钟前'
} else if (diff < day) {
return Math.floor(diff / hour) + '小时前'
} else if (diff < week) {
return Math.floor(diff / day) + '天前'
} else if (diff < month) {
return Math.floor(diff / week) + '周前'
} else if (diff < year) {
return Math.floor(diff / month) + '个月前'
} else {
return Math.floor(diff / year) + '年前'
}
}
/**
* 格式化手机号
* @param {string} phone - 手机号
* @returns {string} 格式化后的手机号
*/
export function formatPhone(phone) {
if (!phone) return ''
const cleaned = phone.replace(/\D/g, '')
if (cleaned.length === 11) {
return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3')
}
return phone
}
/**
* 格式化身份证号
* @param {string} idCard - 身份证号
* @returns {string} 格式化后的身份证号
*/
export function formatIdCard(idCard) {
if (!idCard) return ''
const cleaned = idCard.replace(/\D/g, '')
if (cleaned.length === 18) {
return cleaned.replace(/(\d{6})(\d{8})(\d{4})/, '$1-$2-$3')
}
return idCard
}

View File

@@ -7,6 +7,12 @@ import { usePermissionStore } from '@/stores/permission'
// 检查单个权限
export function hasPermission(permission) {
const permissionStore = usePermissionStore()
// 超级管理员和管理员拥有所有权限
if (permissionStore.hasRole('super_admin') || permissionStore.hasRole('admin')) {
return true
}
return permissionStore.hasPermission(permission)
}
@@ -114,6 +120,9 @@ export const permissionMixin = {
* 权限常量
*/
export const PERMISSIONS = {
// 工作台
DASHBOARD_VIEW: 'dashboard:view',
// 养殖场管理
FARM_VIEW: 'farm:view',
FARM_CREATE: 'farm:create',
@@ -219,6 +228,11 @@ export const ROLES = {
* 权限组合
*/
export const PERMISSION_GROUPS = {
// 工作台权限组
DASHBOARD_MANAGEMENT: [
PERMISSIONS.DASHBOARD_VIEW
],
// 养殖场管理权限组
FARM_MANAGEMENT: [
PERMISSIONS.FARM_VIEW,
@@ -340,6 +354,7 @@ export const PERMISSION_GROUPS = {
*/
export const ROLE_PERMISSIONS = {
[ROLES.SUPER_ADMIN]: [
...PERMISSION_GROUPS.DASHBOARD_MANAGEMENT,
...PERMISSION_GROUPS.FARM_MANAGEMENT,
...PERMISSION_GROUPS.DEVICE_MANAGEMENT,
...PERMISSION_GROUPS.MONITOR_MANAGEMENT,
@@ -356,6 +371,7 @@ export const ROLE_PERMISSIONS = {
],
[ROLES.ADMIN]: [
...PERMISSION_GROUPS.DASHBOARD_MANAGEMENT,
...PERMISSION_GROUPS.FARM_MANAGEMENT,
...PERMISSION_GROUPS.DEVICE_MANAGEMENT,
...PERMISSION_GROUPS.MONITOR_MANAGEMENT,
@@ -453,6 +469,9 @@ export function isPermissionInGroup(permission, group) {
*/
export function formatPermissionName(permission) {
const permissionNames = {
// 工作台
'dashboard:view': '查看工作台',
// 养殖场管理
'farm:view': '查看养殖场',
'farm:create': '新增养殖场',

View File

@@ -3,7 +3,7 @@
*/
import axios from 'axios'
import { message, Modal } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import { useAuthStore } from '@/stores/auth'
import { useNotificationStore } from '@/stores/notification'
import router from '@/router'
@@ -19,11 +19,11 @@ const request = axios.create({
// 请求拦截器
request.interceptors.request.use(
(config) => {
const userStore = useUserStore()
const authStore = useAuthStore()
// 添加认证token
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
// 添加请求ID用于追踪
@@ -182,7 +182,7 @@ request.interceptors.response.use(
* 处理未授权错误
*/
function handleUnauthorized() {
const userStore = useUserStore()
const authStore = useAuthStore()
Modal.confirm({
title: '登录已过期',
@@ -190,7 +190,7 @@ function handleUnauthorized() {
okText: '重新登录',
cancelText: '取消',
onOk() {
userStore.logout()
authStore.logout()
router.push('/login')
}
})

View File

@@ -149,22 +149,31 @@ const handleLogin = async (values) => {
loading.value = true
try {
const success = await authStore.login({
const result = await authStore.login({
username: values.username,
password: values.password,
captcha: values.captcha,
remember: values.remember
})
if (success) {
if (result.success) {
message.success('登录成功')
// 跳转到首页或之前访问的页面
const redirect = router.currentRoute.value.query.redirect || '/'
const redirect = router.currentRoute.value.query.redirect || '/dashboard'
router.push(redirect)
} else {
message.error(result.message || '登录失败')
// 登录失败后显示验证码
if (!showCaptcha.value) {
showCaptcha.value = true
refreshCaptcha()
}
}
} catch (error) {
console.error('登录失败:', error)
message.error('登录失败')
// 登录失败后显示验证码
if (!showCaptcha.value) {
@@ -184,8 +193,8 @@ const refreshCaptcha = () => {
// 组件挂载时的处理
onMounted(() => {
// 如果已经登录,直接跳转到首页
if (authStore.isAuthenticated) {
router.push('/')
if (authStore.isLoggedIn) {
router.push('/dashboard')
}
// 初始化验证码(如果需要)

View File

@@ -316,12 +316,12 @@ import {
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
import { useGovernmentStore } from '@/stores/government'
import { useUserStore } from '@/stores/user'
import { useAuthStore } from '@/stores/auth'
import { useNotificationStore } from '@/stores/notification'
const router = useRouter()
const governmentStore = useGovernmentStore()
const userStore = useUserStore()
const authStore = useAuthStore()
const notificationStore = useNotificationStore()
// 响应式数据
@@ -330,8 +330,8 @@ const healthTrendType = ref('month')
// 用户信息 - 从store获取
const userInfo = computed(() => ({
name: userStore.userInfo?.name || '管理员',
role: userStore.userInfo?.role || '系统管理员'
name: authStore.userInfo?.name || '管理员',
role: authStore.userInfo?.role || '系统管理员'
}))
// 当前日期和天气
@@ -655,7 +655,7 @@ onMounted(async () => {
await Promise.all([
governmentStore.fetchDashboardData(),
notificationStore.fetchNotifications(),
userStore.fetchUserInfo()
authStore.fetchUserInfo()
])
} catch (error) {
console.error('加载仪表盘数据失败:', error)

View File

@@ -217,6 +217,7 @@ import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons-vue'
import PageHeader from '@/components/common/PageHeader.vue'
import { useAuthStore } from '@/stores/auth'
// 响应式数据
const loading = ref(false)
@@ -224,6 +225,7 @@ const dataSource = ref([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
const authStore = useAuthStore()
// 搜索表单
const searchForm = reactive({