完成系统管理部分

This commit is contained in:
super
2026-01-17 19:32:14 +08:00
parent 183b295e40
commit 76632cc306
18 changed files with 472 additions and 1301 deletions

5
.env
View File

@@ -2,7 +2,4 @@
VITE_APP_TITLE=楠溪屿后台管理系统
# API基础URL
VITE_API_BASE_URL=/api
# 1Panel API 配置
VITE_1PANEL_API_KEY=KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI
VITE_API_BASE_URL=https://api.superwax.cn:4433/api

View File

@@ -2,5 +2,5 @@
VITE_APP_TITLE=楠溪屿后台管理系统
# API基础URL - 生产环境
VITE_API_BASE_URL=/api
VITE_API_BASE_URL=https://api.superwax.cn:4433/api

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,3 @@
/**
* API 模块导出
*/
// 1Panel API
export * from './1panel'

View File

@@ -32,6 +32,10 @@ export interface ProjectInfo {
lastDeployMessage?: string
createdTime?: string
updatedTime?: string
// 新增字段
systemType?: 'admin' | 'portal' // 系统类型
integrateToFramework?: boolean // 是否集成到框架
menuCount?: number // 菜单数量
}
export interface DeployRequest {
@@ -86,6 +90,15 @@ export async function getAllProjects() {
return res.data.data
}
/**
* 获取集成到框架的业务项目
* 返回 systemType=admin 且 integrateToFramework=true 的项目
*/
export async function getIntegratedProjects(): Promise<ProjectInfo[]> {
const res = await request.get<ProjectInfo[]>('/platform/project/integrated')
return res.data.data
}
/**
* 获取项目详情
*/
@@ -200,14 +213,12 @@ export async function checkUploadFiles(projectId: number, paths: string[]): Prom
}
/**
* 准备上传(清理旧文件
* 准备上传(已废弃
* @deprecated upload 接口支持 overwrite 参数,不再需要此函数
*/
export async function prepareUploadFile(projectId: number, path: string): Promise<boolean> {
const res = await request.post<boolean>('/platform/project/upload/prepare', {
projectId,
path
})
return res.data.data
// 保留函数签名以向后兼容,但不再调用后端
return true
}
/**

View File

@@ -1,8 +1,8 @@
import { request } from '@/utils/request'
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
export function getMenuList() {
return request.get<MenuRecord[]>('/system/menu/list')
export function getMenuList(params?: { projectId?: number }) {
return request.get<MenuRecord[]>('/system/menu/list', { params })
}
export function createMenu(data: MenuFormData) {

View File

@@ -112,7 +112,7 @@ import {
} from '@ant-design/icons-vue'
import type { UploadFile, DuplicateFile } from '@/types'
import DuplicateFileModal from './DuplicateFileModal.vue'
import { checkUploadFiles, prepareUploadFile, uploadFileChunk } from '@/api/project'
import { checkUploadFiles, uploadFileChunk } from '@/api/project'
const props = withDefaults(defineProps<{
autoUpload?: boolean
@@ -375,21 +375,33 @@ async function handleDrop(event: DragEvent) {
const files: File[] = []
console.log('[Upload] 拖入项目数量:', items.length)
// 先收集所有的 entry避免在异步操作中 items 被清空
const entries: { entry: FileSystemEntry | null, file: File | null }[] = []
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.kind === 'file') {
const entry = item.webkitGetAsEntry?.()
if (entry) {
await traverseFileTree(entry, '', files)
} else {
const file = item.getAsFile()
if (file) {
files.push(file)
}
}
const file = item.getAsFile()
entries.push({ entry, file })
console.log(`[Upload] 项目 ${i}: entry=${entry?.name || 'null'}, isFile=${entry?.isFile}, isDir=${entry?.isDirectory}`)
}
}
// 处理收集到的 entries
for (const { entry, file } of entries) {
if (entry) {
await traverseFileTree(entry, '', files)
} else if (file) {
// 如果没有 entry直接使用 file
files.push(file)
}
}
console.log('[Upload] 解析后文件数量:', files.length)
if (files.length > 0) {
addFiles(files)
}
@@ -405,23 +417,36 @@ async function traverseFileTree(
): Promise<void> {
if (entry.isFile) {
const fileEntry = entry as FileSystemFileEntry
const file = await new Promise<File>((resolve, reject) => {
fileEntry.file(resolve, reject)
})
Object.defineProperty(file, 'relativePath', {
value: path + entry.name,
writable: false
})
files.push(file)
try {
const file = await new Promise<File>((resolve, reject) => {
fileEntry.file(resolve, reject)
})
// 为文件设置相对路径
const relativePath = path + entry.name
Object.defineProperty(file, 'relativePath', {
value: relativePath,
writable: false
})
files.push(file)
} catch (e) {
console.error('读取文件失败:', entry.name, e)
}
} else if (entry.isDirectory) {
const dirEntry = entry as FileSystemDirectoryEntry
const reader = dirEntry.createReader()
const entries = await new Promise<FileSystemEntry[]>((resolve, reject) => {
reader.readEntries(resolve, reject)
})
// readEntries 可能不会一次返回所有条目,需要循环调用直到返回空数组
let allEntries: FileSystemEntry[] = []
let readBatch: FileSystemEntry[] = []
for (const childEntry of entries) {
do {
readBatch = await new Promise<FileSystemEntry[]>((resolve, reject) => {
reader.readEntries(resolve, reject)
})
allEntries = allEntries.concat(readBatch)
} while (readBatch.length > 0)
for (const childEntry of allEntries) {
await traverseFileTree(childEntry, path + entry.name + '/', files)
}
}
@@ -480,10 +505,7 @@ async function uploadFile(file: UploadFile): Promise<void> {
const filePath = file.path.startsWith('/') ? file.path.substring(1) : file.path
const fullPath = `${basePath}/${filePath}`
// 1. 准备上传(清理可能存在的旧文件)
await prepareUploadFile(projectIdNum, fullPath)
// 2. 计算目标目录(不包含文件名)
// 计算目标目录(不包含文件
// 1Panel 接口需要的是上传到的目录路径
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/') + 1)

View File

@@ -88,6 +88,8 @@ const loading = ref(false)
const formRef = ref<FormInstance>()
const formData = reactive<MenuFormData>({
id: undefined,
projectId: undefined, // Add this
parentId: undefined,
name: '',
code: '',
@@ -121,7 +123,8 @@ watch(
// Reset to defaults
Object.assign(formData, {
id: undefined,
parentId: props.record?.parentId || 0, // Use record parentId if exists, else 0
projectId: props.record?.projectId, // Copy projectId
parentId: props.record?.parentId || 0,
name: '',
code: '',
type: 'menu',

View File

@@ -180,14 +180,30 @@
<!-- 内容区域 -->
<a-layout-content class="content">
<router-view />
<!-- 子项目模式使用 iframe 嵌入 -->
<template v-if="projectStore.isInSubProject && subProjectUrl">
<!-- 加载指示器 -->
<div v-if="iframeLoading" class="iframe-loading">
<a-spin size="large" tip="正在加载..." />
</div>
<iframe
ref="subIframeRef"
:src="subProjectUrl"
:class="['sub-project-iframe', { 'iframe-hidden': iframeLoading }]"
frameborder="0"
allowfullscreen
@load="onIframeLoad"
/>
</template>
<!-- 框架模式正常路由 -->
<router-view v-else />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, computed, watch, h, type Component } from 'vue'
import { ref, computed, watch, h, type Component, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Modal } from 'ant-design-vue'
import * as Icons from '@ant-design/icons-vue'
@@ -211,9 +227,55 @@ const projectStore = useProjectStore()
// 项目配置
const projectConfig = getCurrentProject()
onMounted(async () => {
// 加载集成项目
await projectStore.loadIntegratedProjects()
// 【重要】检查 URL 是否是子项目路由,如 /app/{projectId}/xxx
const path = route.path
const match = path.match(/^\/app\/([^/]+)(.*)$/)
if (match) {
const projectId = match[1]
const subPath = match[2] || '/dashboard'
// 切换到对应项目
projectStore.switchProject(projectId)
// 获取项目信息并加载 iframe
const project = projectStore.getProjectById(projectId)
if (project?.baseUrl) {
iframeLoading.value = true
subProjectUrl.value = `${project.baseUrl}${subPath}?__embedded=true`
// 【修复】恢复菜单选中状态
// subPath 如 /community/tags需要找到对应的菜单 key
const menuKey = subPath.startsWith('/') ? subPath : `/${subPath}`
selectedKeys.value = [menuKey]
// 【修复】展开父级菜单
// 例如 /community/tags 的父级是 /community
const pathParts = menuKey.split('/').filter(Boolean)
if (pathParts.length > 1) {
// 构造父级路径,如 ['community', 'tags'] -> '/community'
const parentPath = `/${pathParts[0]}`
// 子项目菜单使用 sub-{key} 格式
openKeys.value = [`sub-${parentPath}`]
}
}
}
})
const collapsed = ref(false)
const selectedKeys = ref<string[]>([])
const openKeys = ref<string[]>([])
const subProjectUrl = ref<string>('') // 子项目 iframe URL
const iframeLoading = ref(false) // iframe 加载状态
const subIframeRef = ref<HTMLIFrameElement | null>(null)
// iframe 加载完成回调
function onIframeLoad() {
iframeLoading.value = false
}
// 图标名称到组件的映射
// 获取图标组件
@@ -269,6 +331,7 @@ function handleMenuClick({ key }: { key: string | number }) {
// 返回框架
if (menuKey === '__back_to_framework') {
projectStore.exitSubProject()
subProjectUrl.value = '' // 清空 iframe
router.push('/')
return
}
@@ -277,16 +340,38 @@ function handleMenuClick({ key }: { key: string | number }) {
if (menuKey.startsWith('project-')) {
const projectId = menuKey.replace('project-', '')
projectStore.switchProject(projectId)
// 跳转到子项目的默认页面
router.push(`/app/${projectId}/dashboard`)
// 获取项目的 baseUrl构建 iframe URL
const project = projectStore.getProjectById(projectId)
if (project?.baseUrl) {
// 使用 baseUrl 作为 iframe 地址,添加嵌入模式标识
iframeLoading.value = true // 开始加载
subProjectUrl.value = `${project.baseUrl}?__embedded=true`
// 【重要】同步更新地址栏,使用 /app/{projectId}/dashboard 格式
router.push(`/app/${projectId}/dashboard`)
} else {
console.warn(`项目 ${projectId} 没有配置 baseUrl`)
subProjectUrl.value = ''
}
return
}
// 子项目模式下的菜单点击
if (projectStore.isInSubProject) {
const subMenuPath = projectStore.getMenuRoutePath(menuKey)
if (subMenuPath) {
router.push(subMenuPath)
const project = projectStore.currentProject
if (project?.baseUrl) {
// 从菜单 key 中提取路径
// 注意:子项目菜单的 key 格式可能是 "item-/dashboard" 或直接 "/dashboard"
let menuPath = menuKey
if (menuPath.startsWith('item-')) {
menuPath = menuPath.replace('item-', '')
}
menuPath = menuPath.startsWith('/') ? menuPath : `/${menuPath}`
iframeLoading.value = true // 开始加载
subProjectUrl.value = `${project.baseUrl}${menuPath}?__embedded=true`
// 【重要】同步更新地址栏
router.push(`/app/${project.id}${menuPath}`)
}
return
}
@@ -539,4 +624,26 @@ watch(() => route.path, updateMenuState, { immediate: true })
color: #fff;
background: rgba(24, 144, 255, 0.2) !important;
}
/* 子项目 iframe 样式 */
.sub-project-iframe {
width: 100%;
height: 100%;
border: none;
transition: opacity 0.3s ease;
}
.iframe-hidden {
opacity: 0;
position: absolute;
}
.iframe-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
background: #f5f5f5;
}
</style>

View File

@@ -138,81 +138,8 @@ const codePortMenus: ProjectMenuItem[] = [
]
// Mock 项目数据 - 包含 codePort 项目
export const mockProjects: PlatformProject[] = [
{
id: 'codePort',
name: 'CodePort 码头',
shortName: 'CodePort',
logo: '码',
color: '#1890ff',
description: '人才外包平台管理后台',
enabled: true,
menuCount: 25,
createdAt: '2024-01-01T00:00:00Z',
baseUrl: 'http://localhost:5174',
menus: codePortMenus,
currentVersion: '1.0.0',
versions: [
{
id: 'v1.2.0',
version: '1.2.0',
description: '新增了客服系统和会话管理模块',
createdAt: '2024-03-15T10:00:00Z',
createdBy: '管理员',
isCurrent: true,
files: [
{ name: 'dist.zip', size: 5242880, path: '/opt/1panel/www/sites/codeport/index' }
],
snapshot: {
name: 'CodePort 码头',
shortName: 'CodePort',
logo: '码',
color: '#1890ff',
description: '人才外包平台管理后台',
baseUrl: 'http://localhost:5174',
menus: codePortMenus
}
},
{
id: 'v1.1.0',
version: '1.1.0',
description: '优化了用户管理和项目列表',
createdAt: '2024-02-01T10:00:00Z',
createdBy: '管理员',
isCurrent: false,
files: [
{ name: 'dist.zip', size: 4718592, path: '/opt/1panel/www/sites/codeport/index' }
],
snapshot: {
name: 'CodePort 码头',
shortName: 'CodePort',
logo: '码',
color: '#1890ff',
description: '人才外包平台管理后台',
baseUrl: 'http://localhost:5174',
menus: codePortMenus
}
},
{
id: 'v1.0.0',
version: '1.0.0',
description: '初始版本',
createdAt: '2024-01-01T00:00:00Z',
createdBy: '管理员',
isCurrent: false,
snapshot: {
name: 'CodePort 码头',
shortName: 'CodePort',
logo: '码',
color: '#1890ff',
description: '人才外包平台管理后台',
baseUrl: 'http://localhost:5174',
menus: codePortMenus
}
}
]
}
]
// Mock 项目数据 - 暂时为空,由后端接口加载
export const mockProjects: PlatformProject[] = []
/**
* 获取所有启用的项目

View File

@@ -19,6 +19,16 @@ const constantRoutes: RouteRecordRaw[] = [
requiresAuth: false
}
},
// 子项目路由(通配符捕获,由 MainLayout 处理 iframe 加载)
{
path: '/app/:projectId/:pathMatch(.*)*',
name: 'SubProject',
component: () => import('@/layouts/MainLayout.vue'),
meta: {
title: '子项目',
requiresAuth: true
}
},
// 注意:动态路由添加前不要有通配符路由,否则会覆盖动态路由
// 404 路由将在动态路由生成后添加到最后
]

View File

@@ -12,6 +12,7 @@ import {
type ProjectMenuItem,
type ProjectVersion
} from '@/mock/projects'
import { getIntegratedProjects, type ProjectInfo } from '@/api/project'
export const useProjectStore = defineStore('project', () => {
// 所有项目列表(响应式)
@@ -293,6 +294,134 @@ export const useProjectStore = defineStore('project', () => {
return true
}
/**
* 同步项目菜单
* 从全局菜单列表中提取各项目的菜单
*/
async function syncProjectMenus(allMenus: any[]) {
if (!allMenus || allMenus.length === 0) return
// 动态导入以避免循环依赖
const { buildMenuTree, transformToAntdMenu } = await import('@/utils/route')
// 1. 收集所有菜单中涉及的 projectId排除 null 和框架本身 9
const projectIdsInMenus = new Set<string>()
allMenus.forEach(m => {
if (m.projectId && String(m.projectId) !== '9') {
projectIdsInMenus.add(String(m.projectId))
}
})
// 2. 确保 projects 中包含这些项目(如果不存在则创建临时占位)
projectIdsInMenus.forEach(pid => {
const exists = projects.value.some(p => String(p.id) === pid)
if (!exists) {
// 创建一个临时项目对象
projects.value.push({
id: pid,
name: `项目 ${pid}`,
shortName: `P${pid}`,
logo: '代',
color: '#1890ff',
description: '',
baseUrl: '',
enabled: true,
menus: [],
menuCount: 0,
createdAt: new Date().toISOString()
} as any)
}
})
// 3. 同步菜单到各项目
for (const project of projects.value) {
// 过滤出属于该项目的菜单
const projectMenus = allMenus.filter(m =>
m.projectId && String(m.projectId) === String(project.id)
)
if (projectMenus.length > 0) {
// 构建菜单树
const tree = buildMenuTree(projectMenus)
// 转换为 Ant Design 菜单格式 (MainLayout 需要 label 字段)
const antdMenus = transformToAntdMenu(tree)
project.menus = JSON.parse(JSON.stringify(antdMenus))
// 重新计算菜单数量
const count = projectMenus.filter(m => ['directory', 'menu'].includes(m.type)).length
project.menuCount = count
}
}
}
/**
* 加载集成到框架的项目
*/
async function loadIntegratedProjects() {
try {
const list = await getIntegratedProjects()
// 将后端返回的项目信息合并到 projects 中
// 注意:这里我们假设后端返回的 id 是 number而 store 中是 string需要转换
// 且我们需要保留 mock 数据中的菜单配置(如果 id 匹配的话),或者为新项目提供默认菜单
const integratedProjects: PlatformProject[] = list.map(item => {
// 查找是否已存在(匹配已有数据,可能是 syncProjectMenus 创建的临时对象)
const existing = projects.value.find(p => p.id === String(item.id))
if (existing) {
// 更新基本信息,【重要】保留已同步的菜单
return {
...existing,
name: item.name,
shortName: item.shortName || item.name,
logo: item.logo || existing.logo,
color: item.color || existing.color,
baseUrl: item.url || existing.baseUrl,
enabled: true,
// 保留已同步的菜单(如果有的话)
menus: existing.menus || [],
menuCount: existing.menuCount || 0
}
} else {
// 新项目,创建默认结构
return {
id: String(item.id),
name: item.name,
shortName: item.shortName || item.name,
logo: item.logo || item.name.charAt(0),
color: item.color || '#1890ff',
description: item.description,
baseUrl: item.url || '',
enabled: true,
menus: [],
menuCount: 0,
createdAt: item.createdTime || new Date().toISOString()
}
}
})
// 更新 projects 列表
// 策略:保留非 integrated 的项目(可能是纯 mock 的),替换/添加 integrated 的项目
// 但为了简单,我们先把 integratedProjects 合并进去
for (const p of integratedProjects) {
const index = projects.value.findIndex(existing => existing.id === p.id)
if (index > -1) {
projects.value[index] = p
} else {
projects.value.push(p)
}
}
// 此外,我们可能需要一个专门的列表来存储“仅后端返回的集成项目”,
// 但目前 MainLayout 使用 enabledProjects (即 projects.filter(enabled))
// 只要我们设置 enabled: true它们就会显示。
} catch (error) {
console.error('加载集成项目失败:', error)
}
}
return {
// 状态
projects,
@@ -318,6 +447,9 @@ export const useProjectStore = defineStore('project', () => {
addFilesToVersion,
// 菜单路由方法
getMenuRoutePath,
getMenuRouteMap
getMenuRouteMap,
// 新增
loadIntegratedProjects,
syncProjectMenus
}
})

View File

@@ -112,16 +112,32 @@ export const useUserStore = defineStore('user', () => {
// 动态导入 router 以避免循环依赖
const { default: router } = await import('@/router')
// 构建菜单树
const menuTree = buildMenuTree(menus)
// 0. 深拷贝菜单数据,防止 buildMenuTree 修改原始对象导致污染
const menuList = JSON.parse(JSON.stringify(menus))
// 生成动态路由
const routes = generateRoutes(menuTree)
// 1. 同步菜单到各个项目
try {
const { useProjectStore } = await import('@/stores/project')
await useProjectStore().syncProjectMenus(JSON.parse(JSON.stringify(menus)))
} catch (e) {
console.warn('Sync project menus failed', e)
}
// 2. 筛选框架菜单projectId 为空或 9 (NanxiAdmin)
const frameworkMenus = menuList.filter((m: any) => !m.projectId || String(m.projectId) === '9')
const frameworkMenuTree = buildMenuTree(frameworkMenus)
// 生成 Ant Design 菜单 (仅包含框架菜单)
antdMenus.value = transformToAntdMenu(frameworkMenuTree)
// 3. 生成动态路由 (仅为框架菜单生成,子项目菜单不在框架中生成路由)
// 子项目的组件文件不在框架项目中,因此不能直接生成路由
// 子项目将通过 iframe 嵌入或外部链接的方式访问
// 只使用框架菜单生成路由 (projectId 为空或等于 9)
const routes = generateRoutes(frameworkMenuTree)
dynamicRoutes.value = routes
// 生成 Ant Design 菜单
antdMenus.value = transformToAntdMenu(menuTree)
// 动态添加路由到 router 实例
routes.forEach(route => {
router.addRoute(route)

View File

@@ -4,6 +4,7 @@
export interface SysMenu {
id: number
projectId?: number
parentId: number
name: string
code: string

View File

@@ -6,6 +6,8 @@ import { message } from 'ant-design-vue'
import type { ApiResponse } from '@/types/api/response'
// 创建axios实例
// 创建axios实例
console.log('API Base URL:', import.meta.env.VITE_API_BASE_URL)
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 30000,

View File

@@ -50,7 +50,12 @@
</template>
<template v-if="column.key === 'menuCount'">
<a-badge :count="countMenuItems(record.menus)" :overflow-count="99" show-zero>
<a-badge
:count="record.menuCount || 0"
:overflow-count="99"
show-zero
:number-style="{ backgroundColor: record.color || '#1890ff' }"
>
<a-button type="link" size="small" @click="goToMenus(record as any)">
查看菜单
</a-button>
@@ -134,6 +139,25 @@
</a-select>
</a-form-item>
<a-form-item label="系统类型" name="systemType">
<a-radio-group v-model:value="formData.systemType">
<a-radio value="admin">管理端</a-radio>
<a-radio value="portal">门户类</a-radio>
</a-radio-group>
<div class="form-item-help">管理端后台管理系统门户类用户端网站</div>
</a-form-item>
<a-form-item
v-if="formData.systemType === 'admin'"
label="集成到框架"
name="integrateToFramework"
>
<a-switch v-model:checked="formData.integrateToFramework" />
<span style="margin-left: 8px; color: #666;">
{{ formData.integrateToFramework ? '将在左侧菜单的"业务项目"中显示' : '不在框架中显示' }}
</span>
</a-form-item>
<a-form-item label="项目标识" name="code">
<a-input
v-model:value="formData.code"
@@ -687,7 +711,10 @@ const formData = reactive({
serverId: undefined as number | undefined,
deployPath: '',
remark: '',
fileList: [] as any[]
fileList: [] as any[],
// 新增字段
systemType: 'admin' as 'admin' | 'portal', // 系统类型
integrateToFramework: false // 是否集成到框架
})
const formRules = {
@@ -842,7 +869,10 @@ function handleAdd() {
serverId: undefined,
deployPath: '',
remark: '',
fileList: []
fileList: [],
// 新增字段默认值
systemType: 'admin',
integrateToFramework: false
})
formVisible.value = true
}
@@ -864,7 +894,10 @@ function handleEdit(record: any) {
serverId: record.serverId,
deployPath: record.deployPath || '',
remark: record.remark || '',
fileList: []
fileList: [],
// 新增字段
systemType: record.systemType || 'admin',
integrateToFramework: record.integrateToFramework || false
})
// 触发联动逻辑(如果需要加载域名列表),并保留现有域名设置
@@ -897,7 +930,10 @@ async function handleFormSubmit() {
projectGroup: formData.group,
serverId: formData.serverId,
deployPath: formData.deployPath,
remark: formData.remark
remark: formData.remark,
// 新增字段
systemType: formData.systemType,
integrateToFramework: formData.systemType === 'admin' ? formData.integrateToFramework : false
}
if (currentProject.value?.id) {
@@ -947,7 +983,21 @@ function handleDelete(record: any) {
}
function goToMenus(record: PlatformProject) {
router.push({ path: '/platform/menus', query: { projectId: record.id } })
// 尝试匹配路由,防止死循环
// 先尝试 /system/menus (对应文件夹名)
let targetPath = '/system/menus'
const route1 = router.resolve(targetPath)
if (route1.matched.length === 0) {
// 如果没匹配到,尝试 /system/menu (对应常见命名)
targetPath = '/system/menu'
const route2 = router.resolve(targetPath)
if (route2.matched.length === 0) {
message.error('未找到菜单管理页面路由')
return
}
}
router.push({ path: targetPath, query: { projectId: record.id } })
}
const uploadProjectDeployPath = ref<string>('')

View File

@@ -3,9 +3,22 @@
<!-- 操作栏 -->
<a-card class="table-card" :bordered="false">
<template #title>
<a-button type="primary" @click="handleAdd()">
<PlusOutlined /> 新增菜单
</a-button>
<a-space>
<a-select
v-model:value="queryProjectId"
placeholder="所属项目"
style="width: 200px"
allowClear
@change="loadData"
>
<a-select-option v-for="p in projectList" :key="p.id" :value="p.id">
{{ p.shortName }}
</a-select-option>
</a-select>
<a-button type="primary" @click="handleAdd()">
<PlusOutlined /> 新增菜单
</a-button>
</a-space>
</template>
<!-- 表格树形 -->
@@ -64,13 +77,26 @@ import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
import { getMenuList, deleteMenu } from '@/api/system/menu'
import { getAllProjects, type ProjectInfo } from '@/api/project'
import MenuFormModal from '@/components/system/menu/MenuFormModal.vue'
import { buildMenuTree } from '@/utils/route'
// Note: buildMenuTree expects SysMenu, MenuRecord extends SysMenu so it's fine.
// Using 'any' cast if strict checking fails due to optional properties.
import { useRoute } from 'vue-router'
const route = useRoute()
const loading = ref(false)
const treeData = ref<MenuRecord[]>([])
const queryProjectId = ref<number | undefined>(undefined)
const projectList = ref<ProjectInfo[]>([])
// 加载项目列表
async function loadProjects() {
try {
const res = await getAllProjects()
projectList.value = res || []
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
// 表格列定义
const columns = [
@@ -111,7 +137,7 @@ function getTypeName(type: string): string {
async function loadData() {
loading.value = true
try {
const res = await getMenuList()
const res = await getMenuList({ projectId: queryProjectId.value })
// res.data.data is the array
const list = res.data.data || []
// Cast to any because buildMenuTree typings might be strict about SysMenu properties
@@ -125,7 +151,8 @@ async function loadData() {
function handleAdd(parentId?: number) {
currentRecord.value = {
parentId: parentId || 0
parentId: parentId || 0,
projectId: queryProjectId.value // 传入当前选中的项目ID如果未选中则是undefined/null即归属主框架
} as MenuFormData
modalVisible.value = true
}
@@ -145,7 +172,29 @@ async function handleDelete(id: number) {
}
}
onMounted(() => {
onMounted(async () => {
await loadProjects()
// 从路由获取参数,如果有则优先使用
if (route.query.projectId) {
const pid = Number(route.query.projectId)
if (!isNaN(pid)) {
queryProjectId.value = pid
}
}
// 如果没有选中项目,默认选中“楠溪框架”
if (!queryProjectId.value && projectList.value.length > 0) {
const defaultProject = projectList.value.find(p =>
p.code === 'NanxiAdmin' ||
p.shortName === 'NanxiAdmin' ||
p.name === '楠溪框架'
)
if (defaultProject) {
queryProjectId.value = defaultProject.id
}
}
loadData()
})
</script>

View File

@@ -9,6 +9,8 @@ import path from 'path'
// https://vite.dev/config/
export default defineConfig({
// 使用绝对路径,确保在任何子路由下都能正确加载资源
base: '/',
plugins: [
vue(),
// Vue函数自动导入
@@ -67,39 +69,6 @@ export default defineConfig({
console.log(`[Backend API Proxy] ${req.method} ${req.url}`)
})
}
},
// 主服务器代理
'/1panel-api/server1': {
target: 'http://47.109.57.58:42588',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/1panel-api\/server1/, '/api/v2'),
configure: (proxy) => {
proxy.on('proxyReq', (_proxyReq, req) => {
console.log(`[1Panel Proxy - 主服务器] ${req.method} ${req.url}`)
})
}
},
// 测试服务器代理
'/1panel-api/server2': {
target: 'http://192.168.1.100:42588',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/1panel-api\/server2/, '/api/v2'),
configure: (proxy) => {
proxy.on('proxyReq', (_proxyReq, req) => {
console.log(`[1Panel Proxy - 测试服务器] ${req.method} ${req.url}`)
})
}
},
// 备份服务器代理
'/1panel-api/server3': {
target: 'http://10.0.0.1:42588',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/1panel-api\/server3/, '/api/v2'),
configure: (proxy) => {
proxy.on('proxyReq', (_proxyReq, req) => {
console.log(`[1Panel Proxy - 备份服务器] ${req.method} ${req.url}`)
})
}
}
}
}