修复token验证和左侧表单问题

This commit is contained in:
2025-08-19 17:48:53 +08:00
parent 1ceaeb9214
commit 2d44b8764d
39 changed files with 1461 additions and 554 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
VITE_APP_ENV=development
VITE_APP_DEBUG_MODE=true
VITE_APP_TITLE=项目管理系统
VITE_APP_API_BASE_URL=https://www.yunzer.cn/api
VITE_APP_API_BASE_URL=https://www.yunzer.cn/api
+7
View File
@@ -14,6 +14,7 @@ declare module 'vue' {
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -26,8 +27,11 @@ declare module 'vue' {
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
@@ -37,4 +41,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
-4
View File
@@ -2,19 +2,15 @@
import { useDark } from '@vueuse/core'
import { onMounted } from 'vue'
import useColorStore from '@/store/color'
import useUserStore from '@/store/modules/user'
import ENV_CONFIG from '@/config/env'
useDark()
const colorStore = useColorStore()
const userStore = useUserStore()
onMounted(() => {
// 设置页面标题
document.title = ENV_CONFIG.APP_TITLE
// 初始化用户状态
userStore.initUserState()
// 初始化主题色
colorStore.primaryChange(colorStore.primary)
})
+5 -37
View File
@@ -4,48 +4,16 @@ import api from './user'
export interface MenuItem {
smid: number
label: string
icon_class: string
type: number
src: string
sort: number
status: number
parent_id: number
icon_class: string
sort: string
status: number
children?: MenuItem[]
}
export interface RoleItem {
group_id: number
group_name: string
status: number
create_time: string
}
// 获取所有菜单列表
export const getMenuList = () => {
return api.get('/menu/list')
}
// 根据用户角色获取菜单
// 获取管理员菜单接口
export const getUserMenus = () => {
return api.get('/menu/userMenus')
}
// 获取角色列表
export const getRoleList = () => {
return api.get('/menu/roles')
}
// 获取菜单详情
export const getMenuDetail = (id: number) => {
return api.get('/menu/detail', { params: { id } })
}
// 临时菜单接口(使用用户控制器)
export const getTempUserMenus = () => {
return api.get('/user/menus')
}
// 或者直接使用完整路径(如果baseURL有问题)
export const getTempUserMenusDirect = () => {
return api.get('https://www.yunzer.cn/api/user/menus')
return api.get('/admin/menus')
}
+5 -5
View File
@@ -54,19 +54,19 @@ export const login = (data: { account: string; password: string }) => {
return api.post('/admin/login', data)
}
// 获取用户信息接口
// 获取管理员信息接口
export const getUserInfo = () => {
return api.get('/user/info')
return api.get('/admin/info')
}
// 用户登出接口
// 管理员登出接口
export const logout = () => {
return api.post('/user/logout')
return api.post('/admin/logout')
}
// 修改密码接口
export const changePassword = (data: { oldPassword: string; newPassword: string }) => {
return api.post('/user/change-password', data)
return api.post('/admin/change-password', data)
}
export default api
+2 -2
View File
@@ -9,8 +9,8 @@ const ENV_CONFIG = {
APP_TITLE: import.meta.env.VITE_APP_TITLE || '项目管理系统',
// 本地存储key
TOKEN_KEY: 'token',
USER_INFO_KEY: 'userInfo',
TOKEN_KEY: 'admin_token',
USER_INFO_KEY: 'admin_user_info',
// 是否为开发环境
IS_DEV: import.meta.env.DEV,
-5
View File
@@ -8,7 +8,6 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import useUserStore from '@/store/modules/user'
const app = createApp(App)
@@ -21,8 +20,4 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.use(pinia).use(ElementPlus).use(router)
// 初始化用户状态
const userStore = useUserStore()
userStore.initUserState()
app.mount('#app')
+112 -10
View File
@@ -1,8 +1,8 @@
import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router'
import * as MenuUtil from '@/util/menu'
import useFastnavStore from '@/store/fastnav'
import useUserStore from '@/store/modules/user'
import useMenuStore from '@/store/modules/menu'
// 只保留非菜单相关的静态路由
const routes: RouteRecordRaw[] = [
@@ -15,7 +15,16 @@ const routes: RouteRecordRaw[] = [
path: '/',
name: 'Main',
component: () => import('@/views/main/index.vue'),
children: [], // 预定义空的子路由数组
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '工作台'
}
}
],
},
{
path: '/:catchAll(.*)',
@@ -31,8 +40,93 @@ const router = createRouter({
let isAddRoute = false
let isUserStateInitialized = false
// 使用相对路径,vite 的 import.meta.glob 会返回 /src/views/... 格式的key
const views = import.meta.glob('@/views/**/*.vue')
// 创建一个映射,将 @/views/... 格式的路径映射到实际的组件
const viewsMap = new Map()
for (const [key, component] of Object.entries(views)) {
// 将 /src/views/... 转换为 @/views/... 格式作为key
const mappedKey = key.replace('/src/views/', '@/views/')
viewsMap.set(mappedKey, component)
// 同时保留原始key
viewsMap.set(key, component)
}
// 智能查找组件文件
function findComponent(componentName: string, routePath: string): any {
// 处理 routePath,去除开头的斜杠
let cleanPath = routePath.startsWith('/') ? routePath.slice(1) : routePath
// 只保留第一个斜杠后的路径(去掉多余的斜杠)
cleanPath = cleanPath.replace(/\/+/g, '/')
// 可能的路径组合
const possiblePaths = [
// 1. 直接匹配:/src/views/路径.vue
`/src/views/${cleanPath}.vue`,
// 2. index.vue 结尾:/src/views/路径/index.vue
`/src/views/${cleanPath}/index.vue`,
// 3. 小写路径匹配
`/src/views/${cleanPath.toLowerCase()}.vue`,
`/src/views/${cleanPath.toLowerCase()}/index.vue`,
].filter(Boolean)
// 精确匹配 - 使用映射查找
for (const path of possiblePaths) {
if (viewsMap.get(path)) {
return viewsMap.get(path)
}
}
// 模糊匹配 - 改进逻辑
const searchTerms = [componentName.toLowerCase()]
// 优先匹配更精确的路径
for (const [path, component] of Object.entries(views)) {
// 检查路径是否包含组件名(不区分大小写)
if (path.toLowerCase().includes(componentName.toLowerCase())) {
return component
}
}
return null
}
// 从菜单数据生成路由
function generateRoutesFromMenus(menus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
for (const menu of menus) {
// 只处理有src字段的菜单项(type为1表示页面)
if (menu.type === 1 && menu.src) {
const route: RouteRecordRaw = {
path: `/${menu.src}`,
name: menu.label,
component: findComponent(menu.label, menu.src),
meta: {
title: menu.label
}
}
if (route.component) {
routes.push(route)
}
}
// 递归处理子菜单
if (menu.children && menu.children.length > 0) {
const childRoutes = generateRoutesFromMenus(menu.children)
routes.push(...childRoutes)
}
}
return routes
}
router.beforeEach(async (to, _from, next) => {
const userStore = useUserStore()
const menuStore = useMenuStore()
// 等待用户状态初始化完成
if (!isUserStateInitialized) {
@@ -59,9 +153,9 @@ router.beforeEach(async (to, _from, next) => {
// 已经添加过动态路由
if (isAddRoute) {
if (to.meta.desc) {
if (to.meta.title) {
const fastnavStore = useFastnavStore()
fastnavStore.addData(to.meta.desc as string, to.path)
fastnavStore.addData(to.meta.title as string, to.path)
}
next()
return
@@ -70,16 +164,24 @@ router.beforeEach(async (to, _from, next) => {
// 添加主页子路由
isAddRoute = true
// 获取菜单数据
await menuStore.fetchUserMenus()
// 找到主路由
const mainRoute = router.options.routes.find((v) => v.path == '/')!
// 根据用户类型生成菜单和路由(默认使用管理员类型)
const [_genMenus, genRoutes] = MenuUtil.gen(1)
// 根据菜单数据生成路由
const genRoutes = generateRoutesFromMenus(menuStore.getMenus)
// 设置主路由的重定向和子路由
if (genRoutes.length > 0) {
mainRoute.redirect = genRoutes[0].path
mainRoute.children = genRoutes
// 保留默认的Home页面路由,添加动态路由
const existingChildren = mainRoute.children || []
const homeRoute = existingChildren.find(route => route.name === 'Home')
mainRoute.children = homeRoute ? [homeRoute, ...genRoutes] : genRoutes
// 始终重定向到工作台
mainRoute.redirect = '/'
// 添加主路由(包含子路由)
router.addRoute(mainRoute)
@@ -87,8 +189,8 @@ router.beforeEach(async (to, _from, next) => {
// 重新导航到目标路由
next({ ...to, replace: true })
} else {
// 如果没有生成路由,至少重定向到登录页
next({ path: '/login' })
// 如果没有生成路由,重定向到工作台
next({ path: '/' })
}
})
+7 -34
View File
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { getUserMenus, getMenuList, getTempUserMenus, type MenuItem } from '@/api/menu'
import { getUserMenus, type MenuItem } from '@/api/menu'
import ENV_CONFIG from '@/config/env'
interface MenuState {
@@ -8,12 +8,6 @@ interface MenuState {
error: string | null
}
interface ApiResponse<T> {
code: number
message: string
data: T
}
const useMenuStore = defineStore('menu', {
state: (): MenuState => ({
menus: [],
@@ -33,43 +27,22 @@ const useMenuStore = defineStore('menu', {
},
actions: {
// 获取用户菜单(使用临时接口)
// 获取管理员菜单
async fetchUserMenus() {
this.loading = true
this.error = null
try {
const response = await getTempUserMenus() as unknown as ApiResponse<MenuItem[]>
const response = await getUserMenus() as unknown as MenuItem[]
if (response.code === 200 && response.data) {
this.menus = response.data
if (response && Array.isArray(response)) {
this.menus = response
} else {
throw new Error(response.message || '获取菜单失败')
throw new Error('获取菜单失败')
}
} catch (error: any) {
this.error = error.message || '获取菜单失败'
console.error('获取用户菜单失败:', error)
} finally {
this.loading = false
}
},
// 获取所有菜单(管理员用)
async fetchAllMenus() {
this.loading = true
this.error = null
try {
const response = await getMenuList() as unknown as ApiResponse<MenuItem[]>
if (response.code === 200 && response.data) {
this.menus = response.data
} else {
throw new Error(response.message || '获取菜单失败')
}
} catch (error: any) {
this.error = error.message || '获取菜单失败'
console.error('获取所有菜单失败:', error)
console.error('获取管理员菜单失败:', error)
} finally {
this.loading = false
}
+13 -1
View File
@@ -103,6 +103,7 @@ const useUserStore = defineStore('user', {
try {
// 验证token是否有效
const response = await getUserInfoApi() as unknown as UserInfo
if (response && response.id) {
this.token = token
this.userInfo = response
@@ -113,10 +114,21 @@ const useUserStore = defineStore('user', {
}
} catch (error: any) {
console.error('获取用户信息失败:', error)
// 网络错误时,不清除本地状态,保持用户登录状态
// 如果有本地token和用户信息,先保持登录状态
// 只有在明确知道token无效时才清除
if (error.response?.status === 401) {
this.clearUserState()
} else {
// 网络错误或其他错误,使用本地存储的数据
try {
const userInfo = JSON.parse(userInfoStr)
this.token = token
this.userInfo = userInfo
this.isLogin = true
} catch (parseError) {
// 本地数据解析失败,清除状态
this.clearUserState()
}
}
}
}
+234
View File
@@ -0,0 +1,234 @@
<template>
<div class="article-list">
<el-card>
<div class="header">
<el-input
v-model="search"
placeholder="搜索文章标题"
clearable
class="search-input"
@keyup.enter="fetchArticles"
/>
<el-select
v-model="selectedCategory"
placeholder="筛选分类"
clearable
class="category-select"
style="width: 150px"
@change="fetchArticles"
>
<el-option
v-for="item in categoryOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-button type="primary" @click="fetchArticles">搜索</el-button>
<el-button type="success" @click="handlePublish">发布文章</el-button>
</div>
<el-table
:data="articles"
style="width: 100%; margin-top: 20px"
v-loading="loading"
border
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="author" label="作者" width="120" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column label="操作" width="220">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
<el-button size="small" type="primary" @click="handlePublishSingle(scope.row)">发布</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
layout="prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="page"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 发布文章对话框 -->
<el-dialog v-model="publishDialogVisible" title="发布文章" width="500px">
<el-form :model="publishForm" label-width="80px">
<el-form-item label="标题">
<el-input v-model="publishForm.title" />
</el-form-item>
<el-form-item label="作者">
<el-input v-model="publishForm.author" />
</el-form-item>
<el-form-item label="分类">
<el-select v-model="publishForm.category" placeholder="请选择分类">
<el-option
v-for="item in categoryOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="内容">
<el-input
v-model="publishForm.content"
type="textarea"
:rows="4"
placeholder="请输入文章内容"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="publishDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPublish">发布</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 这里假设有文章API
// import { getArticleList } from '@/api/article'
const articles = ref<any[]>([])
const loading = ref(false)
const search = ref('')
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const selectedCategory = ref('')
const categoryOptions = ref<string[]>([
'前端', '后端', '数据库', '架构', '安全', '运维', '测试'
])
// 发布文章相关
const publishDialogVisible = ref(false)
const publishForm = ref({
title: '',
author: '',
category: '',
content: ''
})
function fetchArticles() {
loading.value = true
// 这里用模拟数据,实际请替换为API请求
setTimeout(() => {
// 假数据
const all = [
{ id: 1, title: 'Vue3 入门', author: '张三', category: '前端', create_time: '2024-06-01 10:00' },
{ id: 2, title: 'TypeScript 实践', author: '李四', category: '前端', create_time: '2024-06-02 11:00' },
{ id: 3, title: 'PHP 高级技巧', author: '王五', category: '后端', create_time: '2024-06-03 12:00' },
{ id: 4, title: '数据库优化', author: '赵六', category: '数据库', create_time: '2024-06-04 13:00' },
{ id: 5, title: '云原生架构', author: '钱七', category: '架构', create_time: '2024-06-05 14:00' },
{ id: 6, title: '安全最佳实践', author: '孙八', category: '安全', create_time: '2024-06-06 15:00' },
{ id: 7, title: '性能调优', author: '周九', category: '运维', create_time: '2024-06-07 16:00' },
{ id: 8, title: '微服务设计', author: '吴十', category: '架构', create_time: '2024-06-08 17:00' },
{ id: 9, title: '前端工程化', author: '郑十一', category: '前端', create_time: '2024-06-09 18:00' },
{ id: 10, title: '测试驱动开发', author: '冯十二', category: '测试', create_time: '2024-06-10 19:00' },
{ id: 11, title: '持续集成', author: '褚十三', category: '运维', create_time: '2024-06-11 20:00' }
]
let filtered = all
if (search.value) {
filtered = filtered.filter(a => a.title.includes(search.value))
}
if (selectedCategory.value) {
filtered = filtered.filter(a => a.category === selectedCategory.value)
}
total.value = filtered.length
const start = (page.value - 1) * pageSize.value
articles.value = filtered.slice(start, start + pageSize.value)
loading.value = false
}, 500)
}
function handleEdit(row: any) {
// 编辑文章逻辑
alert('编辑文章: ' + row.title)
}
function handleDelete(row: any) {
// 删除文章逻辑
if (confirm('确定要删除文章 "' + row.title + '" 吗?')) {
// 实际应调用API
articles.value = articles.value.filter(a => a.id !== row.id)
total.value--
}
}
function handlePageChange(val: number) {
page.value = val
fetchArticles()
}
// 点击“发布文章”按钮
function handlePublish() {
publishForm.value = {
title: '',
author: '',
category: '',
content: ''
}
publishDialogVisible.value = true
}
// 单条发布(模拟,实际应调用API)
function handlePublishSingle(row: any) {
alert('发布文章: ' + row.title)
}
// 提交发布
function submitPublish() {
if (!publishForm.value.title || !publishForm.value.author || !publishForm.value.category) {
alert('请填写完整信息')
return
}
// 实际应调用API,这里直接添加到表格
const newId = Math.max(...articles.value.map(a => a.id), 0) + 1
articles.value.unshift({
id: newId,
title: publishForm.value.title,
author: publishForm.value.author,
category: publishForm.value.category,
create_time: new Date().toISOString().slice(0, 16).replace('T', ' ')
})
total.value++
publishDialogVisible.value = false
// 可选:重置分页到第一页
page.value = 1
fetchArticles()
}
onMounted(() => {
fetchArticles()
})
</script>
<style scoped>
.article-list {
padding: 20px;
}
.header {
display: flex;
align-items: center;
gap: 10px;
}
.search-input {
width: 300px;
}
.category-select {
min-width: 120px;
}
.pagination {
margin-top: 20px;
text-align: right;
}
</style>
+65 -17
View File
@@ -59,29 +59,29 @@
<el-aside :width="menuStore.width">
<el-menu
router
:default-active="route.path"
:default-active="route.path === '/' ? '/' : route.path"
:collapse="menuStore.collapse"
style="height: 100%;"
>
<template v-for="menu in menus" :key="menu.smid">
<el-sub-menu
v-if="menu.children && menu.children.length > 0"
:index="menu.smid.toString()"
:index="menu.src || menu.smid.toString()"
>
<template #title>
<el-icon>
<component :is="menu.icon_class"></component>
<Setting />
</el-icon>
<span>{{ menu.label }}</span>
</template>
<el-menu-item
v-for="child in menu.children"
:key="child.smid"
:index="child.smid.toString()"
:index="child.src || child.smid.toString()"
@click="handleMenuClick(child)"
>
<el-icon>
<component :is="child.icon_class"></component>
<Setting />
</el-icon>
<span>{{ child.label }}</span>
</el-menu-item>
@@ -90,11 +90,12 @@
<el-menu-item
v-else
:key="menu.smid"
:index="menu.smid.toString()"
:index="menu.smid === 0 ? '/' : (menu.src || menu.smid.toString())"
@click="handleMenuClick(menu)"
>
<el-icon>
<component :is="menu.icon_class"></component>
<Monitor v-if="menu.smid === 0" />
<Setting v-else />
</el-icon>
<template #title>{{ menu.label }}</template>
</el-menu-item>
@@ -165,9 +166,19 @@ import useMenuStore from '@/store/modules/menu'
import useFastnavStore from '@/store/fastnav'
import useMenuCollapseStore from '@/store/menu'
import useColorStore from '@/store/color'
import { onMounted, ref } from 'vue'
import { onMounted, ref, computed } from 'vue'
import { useRoute, useRouter, RouterView } from 'vue-router'
import { useFullscreen, useDark } from '@vueuse/core'
import {
Setting,
User,
Document,
Folder,
Monitor,
Grid,
Menu,
House
} from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
@@ -183,6 +194,25 @@ const colorStore = useColorStore()
const menus = ref<any[]>([])
const collapse = ref(true)
// 图标映射
const iconMap: Record<string, any> = {
'layui-icon-senior': Setting,
'layui-icon-user': User,
'layui-icon-file': Document,
'layui-icon-folder': Folder,
'layui-icon-monitor': Monitor,
'layui-icon-grid': Grid,
'layui-icon-menu': Menu,
'layui-icon-home': House,
// 默认图标
'': Setting
}
// 获取图标组件
const getIconComponent = (iconClass: string) => {
return iconMap[iconClass] || Setting
}
// 新增:控制右键菜单各项的禁用状态
const tagMenuReloadDisabled = ref(false)
const tagMenuCloseDisabled = ref(false)
@@ -191,9 +221,26 @@ const tagMenuLeftDisabled = ref(false)
const tagMenuRightDisabled = ref(false)
onMounted(async () => {
// 获取动态菜单数据
await menuDataStore.fetchUserMenus()
menus.value = menuDataStore.getMenus
// 从store获取菜单数据,如果还没有则等待
if (menuDataStore.getMenus.length === 0) {
await menuDataStore.fetchUserMenus()
}
// 添加固定的工作台菜单
const workbenchMenu = {
smid: 0,
label: '工作台',
type: 1,
src: '',
parent_id: 0,
icon_class: '',
sort: '999999',
status: 1,
children: []
}
// 将工作台菜单放在最前面
menus.value = [workbenchMenu, ...menuDataStore.getMenus]
})
// 折叠侧边栏
@@ -263,12 +310,13 @@ const handleTagCommand = (command: string) => {
// 菜单点击处理
const handleMenuClick = (menu: any) => {
if (menu.type === 1 && menu.src) {
// 内部跳转
const title = menu.label
const path = '/' + menu.src.replace('/', '_')
// 这里需要根据实际情况处理页面跳转
console.log('内部跳转:', title, path)
if (menu.smid === 0) {
// 工作台菜单
router.push('/')
} else if (menu.type === 1 && menu.src) {
// 内部跳转 - 使用src作为路径
const path = '/' + menu.src
router.push(path)
} else if (menu.type === 2 && menu.src) {
// 外部链接
window.open(menu.src, '_blank')
+6 -6
View File
@@ -53,9 +53,9 @@ export default defineConfig({
},
// 服务器配置
// server: {
// port: 3000,
// open: true,
// cors: true
// }
})
server: {
port: 5173, // 保持你当前使用的端口
open: true,
cors: true
}
})