完成系统管理部分
This commit is contained in:
5
.env
5
.env
@@ -2,7 +2,4 @@
|
|||||||
VITE_APP_TITLE=楠溪屿后台管理系统
|
VITE_APP_TITLE=楠溪屿后台管理系统
|
||||||
|
|
||||||
# API基础URL
|
# API基础URL
|
||||||
VITE_API_BASE_URL=/api
|
VITE_API_BASE_URL=https://api.superwax.cn:4433/api
|
||||||
|
|
||||||
# 1Panel API 配置
|
|
||||||
VITE_1PANEL_API_KEY=KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
VITE_APP_TITLE=楠溪屿后台管理系统
|
VITE_APP_TITLE=楠溪屿后台管理系统
|
||||||
|
|
||||||
# API基础URL - 生产环境
|
# API基础URL - 生产环境
|
||||||
VITE_API_BASE_URL=/api
|
VITE_API_BASE_URL=https://api.superwax.cn:4433/api
|
||||||
|
|
||||||
|
|||||||
1122
src/api/1panel.ts
1122
src/api/1panel.ts
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
|||||||
/**
|
/**
|
||||||
* API 模块导出
|
* API 模块导出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 1Panel API
|
|
||||||
export * from './1panel'
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export interface ProjectInfo {
|
|||||||
lastDeployMessage?: string
|
lastDeployMessage?: string
|
||||||
createdTime?: string
|
createdTime?: string
|
||||||
updatedTime?: string
|
updatedTime?: string
|
||||||
|
// 新增字段
|
||||||
|
systemType?: 'admin' | 'portal' // 系统类型
|
||||||
|
integrateToFramework?: boolean // 是否集成到框架
|
||||||
|
menuCount?: number // 菜单数量
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeployRequest {
|
export interface DeployRequest {
|
||||||
@@ -86,6 +90,15 @@ export async function getAllProjects() {
|
|||||||
return res.data.data
|
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> {
|
export async function prepareUploadFile(projectId: number, path: string): Promise<boolean> {
|
||||||
const res = await request.post<boolean>('/platform/project/upload/prepare', {
|
// 保留函数签名以向后兼容,但不再调用后端
|
||||||
projectId,
|
return true
|
||||||
path
|
|
||||||
})
|
|
||||||
return res.data.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
||||||
|
|
||||||
export function getMenuList() {
|
export function getMenuList(params?: { projectId?: number }) {
|
||||||
return request.get<MenuRecord[]>('/system/menu/list')
|
return request.get<MenuRecord[]>('/system/menu/list', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMenu(data: MenuFormData) {
|
export function createMenu(data: MenuFormData) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import type { UploadFile, DuplicateFile } from '@/types'
|
import type { UploadFile, DuplicateFile } from '@/types'
|
||||||
import DuplicateFileModal from './DuplicateFileModal.vue'
|
import DuplicateFileModal from './DuplicateFileModal.vue'
|
||||||
import { checkUploadFiles, prepareUploadFile, uploadFileChunk } from '@/api/project'
|
import { checkUploadFiles, uploadFileChunk } from '@/api/project'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
autoUpload?: boolean
|
autoUpload?: boolean
|
||||||
@@ -375,21 +375,33 @@ async function handleDrop(event: DragEvent) {
|
|||||||
|
|
||||||
const files: File[] = []
|
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++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
const item = items[i]
|
const item = items[i]
|
||||||
if (item.kind === 'file') {
|
if (item.kind === 'file') {
|
||||||
const entry = item.webkitGetAsEntry?.()
|
const entry = item.webkitGetAsEntry?.()
|
||||||
if (entry) {
|
const file = item.getAsFile()
|
||||||
await traverseFileTree(entry, '', files)
|
entries.push({ entry, file })
|
||||||
} else {
|
console.log(`[Upload] 项目 ${i}: entry=${entry?.name || 'null'}, isFile=${entry?.isFile}, isDir=${entry?.isDirectory}`)
|
||||||
const file = item.getAsFile()
|
|
||||||
if (file) {
|
|
||||||
files.push(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理收集到的 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) {
|
if (files.length > 0) {
|
||||||
addFiles(files)
|
addFiles(files)
|
||||||
}
|
}
|
||||||
@@ -405,23 +417,36 @@ async function traverseFileTree(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (entry.isFile) {
|
if (entry.isFile) {
|
||||||
const fileEntry = entry as FileSystemFileEntry
|
const fileEntry = entry as FileSystemFileEntry
|
||||||
const file = await new Promise<File>((resolve, reject) => {
|
try {
|
||||||
fileEntry.file(resolve, reject)
|
const file = await new Promise<File>((resolve, reject) => {
|
||||||
})
|
fileEntry.file(resolve, reject)
|
||||||
Object.defineProperty(file, 'relativePath', {
|
})
|
||||||
value: path + entry.name,
|
// 为文件设置相对路径
|
||||||
writable: false
|
const relativePath = path + entry.name
|
||||||
})
|
Object.defineProperty(file, 'relativePath', {
|
||||||
files.push(file)
|
value: relativePath,
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
files.push(file)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取文件失败:', entry.name, e)
|
||||||
|
}
|
||||||
} else if (entry.isDirectory) {
|
} else if (entry.isDirectory) {
|
||||||
const dirEntry = entry as FileSystemDirectoryEntry
|
const dirEntry = entry as FileSystemDirectoryEntry
|
||||||
const reader = dirEntry.createReader()
|
const reader = dirEntry.createReader()
|
||||||
|
|
||||||
const entries = await new Promise<FileSystemEntry[]>((resolve, reject) => {
|
// readEntries 可能不会一次返回所有条目,需要循环调用直到返回空数组
|
||||||
reader.readEntries(resolve, reject)
|
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)
|
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 filePath = file.path.startsWith('/') ? file.path.substring(1) : file.path
|
||||||
const fullPath = `${basePath}/${filePath}`
|
const fullPath = `${basePath}/${filePath}`
|
||||||
|
|
||||||
// 1. 准备上传(清理可能存在的旧文件)
|
// 计算目标目录(不包含文件名)
|
||||||
await prepareUploadFile(projectIdNum, fullPath)
|
|
||||||
|
|
||||||
// 2. 计算目标目录(不包含文件名)
|
|
||||||
// 1Panel 接口需要的是上传到的目录路径
|
// 1Panel 接口需要的是上传到的目录路径
|
||||||
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/') + 1)
|
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/') + 1)
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ const loading = ref(false)
|
|||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
const formData = reactive<MenuFormData>({
|
const formData = reactive<MenuFormData>({
|
||||||
|
id: undefined,
|
||||||
|
projectId: undefined, // Add this
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
@@ -121,7 +123,8 @@ watch(
|
|||||||
// Reset to defaults
|
// Reset to defaults
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
id: undefined,
|
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: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
type: 'menu',
|
type: 'menu',
|
||||||
|
|||||||
@@ -180,14 +180,30 @@
|
|||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<a-layout-content class="content">
|
<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-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRouter, useRoute } from 'vue-router'
|
||||||
import { Modal } from 'ant-design-vue'
|
import { Modal } from 'ant-design-vue'
|
||||||
import * as Icons from '@ant-design/icons-vue'
|
import * as Icons from '@ant-design/icons-vue'
|
||||||
@@ -211,9 +227,55 @@ const projectStore = useProjectStore()
|
|||||||
// 项目配置
|
// 项目配置
|
||||||
const projectConfig = getCurrentProject()
|
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 collapsed = ref(false)
|
||||||
const selectedKeys = ref<string[]>([])
|
const selectedKeys = ref<string[]>([])
|
||||||
const openKeys = 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') {
|
if (menuKey === '__back_to_framework') {
|
||||||
projectStore.exitSubProject()
|
projectStore.exitSubProject()
|
||||||
|
subProjectUrl.value = '' // 清空 iframe
|
||||||
router.push('/')
|
router.push('/')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -277,16 +340,38 @@ function handleMenuClick({ key }: { key: string | number }) {
|
|||||||
if (menuKey.startsWith('project-')) {
|
if (menuKey.startsWith('project-')) {
|
||||||
const projectId = menuKey.replace('project-', '')
|
const projectId = menuKey.replace('project-', '')
|
||||||
projectStore.switchProject(projectId)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 子项目模式下的菜单点击
|
// 子项目模式下的菜单点击
|
||||||
if (projectStore.isInSubProject) {
|
if (projectStore.isInSubProject) {
|
||||||
const subMenuPath = projectStore.getMenuRoutePath(menuKey)
|
const project = projectStore.currentProject
|
||||||
if (subMenuPath) {
|
if (project?.baseUrl) {
|
||||||
router.push(subMenuPath)
|
// 从菜单 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
|
return
|
||||||
}
|
}
|
||||||
@@ -539,4 +624,26 @@ watch(() => route.path, updateMenuState, { immediate: true })
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
background: rgba(24, 144, 255, 0.2) !important;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -138,81 +138,8 @@ const codePortMenus: ProjectMenuItem[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// Mock 项目数据 - 包含 codePort 项目
|
// Mock 项目数据 - 包含 codePort 项目
|
||||||
export const mockProjects: PlatformProject[] = [
|
// Mock 项目数据 - 暂时为空,由后端接口加载
|
||||||
{
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有启用的项目
|
* 获取所有启用的项目
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ const constantRoutes: RouteRecordRaw[] = [
|
|||||||
requiresAuth: false
|
requiresAuth: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 子项目路由(通配符捕获,由 MainLayout 处理 iframe 加载)
|
||||||
|
{
|
||||||
|
path: '/app/:projectId/:pathMatch(.*)*',
|
||||||
|
name: 'SubProject',
|
||||||
|
component: () => import('@/layouts/MainLayout.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '子项目',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
},
|
||||||
// 注意:动态路由添加前不要有通配符路由,否则会覆盖动态路由
|
// 注意:动态路由添加前不要有通配符路由,否则会覆盖动态路由
|
||||||
// 404 路由将在动态路由生成后添加到最后
|
// 404 路由将在动态路由生成后添加到最后
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
type ProjectMenuItem,
|
type ProjectMenuItem,
|
||||||
type ProjectVersion
|
type ProjectVersion
|
||||||
} from '@/mock/projects'
|
} from '@/mock/projects'
|
||||||
|
import { getIntegratedProjects, type ProjectInfo } from '@/api/project'
|
||||||
|
|
||||||
export const useProjectStore = defineStore('project', () => {
|
export const useProjectStore = defineStore('project', () => {
|
||||||
// 所有项目列表(响应式)
|
// 所有项目列表(响应式)
|
||||||
@@ -293,6 +294,134 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
return true
|
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 {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
projects,
|
projects,
|
||||||
@@ -318,6 +447,9 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
addFilesToVersion,
|
addFilesToVersion,
|
||||||
// 菜单路由方法
|
// 菜单路由方法
|
||||||
getMenuRoutePath,
|
getMenuRoutePath,
|
||||||
getMenuRouteMap
|
getMenuRouteMap,
|
||||||
|
// 新增
|
||||||
|
loadIntegratedProjects,
|
||||||
|
syncProjectMenus
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -112,16 +112,32 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
// 动态导入 router 以避免循环依赖
|
// 动态导入 router 以避免循环依赖
|
||||||
const { default: router } = await import('@/router')
|
const { default: router } = await import('@/router')
|
||||||
|
|
||||||
// 构建菜单树
|
// 0. 深拷贝菜单数据,防止 buildMenuTree 修改原始对象导致污染
|
||||||
const menuTree = buildMenuTree(menus)
|
const menuList = JSON.parse(JSON.stringify(menus))
|
||||||
|
|
||||||
|
// 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 菜单 (仅包含框架菜单)
|
||||||
const routes = generateRoutes(menuTree)
|
antdMenus.value = transformToAntdMenu(frameworkMenuTree)
|
||||||
|
|
||||||
|
// 3. 生成动态路由 (仅为框架菜单生成,子项目菜单不在框架中生成路由)
|
||||||
|
// 子项目的组件文件不在框架项目中,因此不能直接生成路由
|
||||||
|
// 子项目将通过 iframe 嵌入或外部链接的方式访问
|
||||||
|
|
||||||
|
// 只使用框架菜单生成路由 (projectId 为空或等于 9)
|
||||||
|
const routes = generateRoutes(frameworkMenuTree)
|
||||||
dynamicRoutes.value = routes
|
dynamicRoutes.value = routes
|
||||||
|
|
||||||
// 生成 Ant Design 菜单
|
|
||||||
antdMenus.value = transformToAntdMenu(menuTree)
|
|
||||||
|
|
||||||
// 动态添加路由到 router 实例
|
// 动态添加路由到 router 实例
|
||||||
routes.forEach(route => {
|
routes.forEach(route => {
|
||||||
router.addRoute(route)
|
router.addRoute(route)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
export interface SysMenu {
|
export interface SysMenu {
|
||||||
id: number
|
id: number
|
||||||
|
projectId?: number
|
||||||
parentId: number
|
parentId: number
|
||||||
name: string
|
name: string
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import type { ApiResponse } from '@/types/api/response'
|
import type { ApiResponse } from '@/types/api/response'
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
|
// 创建axios实例
|
||||||
|
console.log('API Base URL:', import.meta.env.VITE_API_BASE_URL)
|
||||||
const service: AxiosInstance = axios.create({
|
const service: AxiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
|||||||
@@ -50,7 +50,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="column.key === 'menuCount'">
|
<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 type="link" size="small" @click="goToMenus(record as any)">
|
||||||
查看菜单
|
查看菜单
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -134,6 +139,25 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</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-form-item label="项目标识" name="code">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="formData.code"
|
v-model:value="formData.code"
|
||||||
@@ -687,7 +711,10 @@ const formData = reactive({
|
|||||||
serverId: undefined as number | undefined,
|
serverId: undefined as number | undefined,
|
||||||
deployPath: '',
|
deployPath: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
fileList: [] as any[]
|
fileList: [] as any[],
|
||||||
|
// 新增字段
|
||||||
|
systemType: 'admin' as 'admin' | 'portal', // 系统类型
|
||||||
|
integrateToFramework: false // 是否集成到框架
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
@@ -842,7 +869,10 @@ function handleAdd() {
|
|||||||
serverId: undefined,
|
serverId: undefined,
|
||||||
deployPath: '',
|
deployPath: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
fileList: []
|
fileList: [],
|
||||||
|
// 新增字段默认值
|
||||||
|
systemType: 'admin',
|
||||||
|
integrateToFramework: false
|
||||||
})
|
})
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -864,7 +894,10 @@ function handleEdit(record: any) {
|
|||||||
serverId: record.serverId,
|
serverId: record.serverId,
|
||||||
deployPath: record.deployPath || '',
|
deployPath: record.deployPath || '',
|
||||||
remark: record.remark || '',
|
remark: record.remark || '',
|
||||||
fileList: []
|
fileList: [],
|
||||||
|
// 新增字段
|
||||||
|
systemType: record.systemType || 'admin',
|
||||||
|
integrateToFramework: record.integrateToFramework || false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 触发联动逻辑(如果需要加载域名列表),并保留现有域名设置
|
// 触发联动逻辑(如果需要加载域名列表),并保留现有域名设置
|
||||||
@@ -897,7 +930,10 @@ async function handleFormSubmit() {
|
|||||||
projectGroup: formData.group,
|
projectGroup: formData.group,
|
||||||
serverId: formData.serverId,
|
serverId: formData.serverId,
|
||||||
deployPath: formData.deployPath,
|
deployPath: formData.deployPath,
|
||||||
remark: formData.remark
|
remark: formData.remark,
|
||||||
|
// 新增字段
|
||||||
|
systemType: formData.systemType,
|
||||||
|
integrateToFramework: formData.systemType === 'admin' ? formData.integrateToFramework : false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentProject.value?.id) {
|
if (currentProject.value?.id) {
|
||||||
@@ -947,7 +983,21 @@ function handleDelete(record: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToMenus(record: PlatformProject) {
|
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>('')
|
const uploadProjectDeployPath = ref<string>('')
|
||||||
|
|||||||
@@ -3,9 +3,22 @@
|
|||||||
<!-- 操作栏 -->
|
<!-- 操作栏 -->
|
||||||
<a-card class="table-card" :bordered="false">
|
<a-card class="table-card" :bordered="false">
|
||||||
<template #title>
|
<template #title>
|
||||||
<a-button type="primary" @click="handleAdd()">
|
<a-space>
|
||||||
<PlusOutlined /> 新增菜单
|
<a-select
|
||||||
</a-button>
|
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>
|
</template>
|
||||||
|
|
||||||
<!-- 表格(树形) -->
|
<!-- 表格(树形) -->
|
||||||
@@ -64,13 +77,26 @@ import { message } from 'ant-design-vue'
|
|||||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||||
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
import type { MenuRecord, MenuFormData } from '@/types/system/menu'
|
||||||
import { getMenuList, deleteMenu } from '@/api/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 MenuFormModal from '@/components/system/menu/MenuFormModal.vue'
|
||||||
import { buildMenuTree } from '@/utils/route'
|
import { buildMenuTree } from '@/utils/route'
|
||||||
// Note: buildMenuTree expects SysMenu, MenuRecord extends SysMenu so it's fine.
|
import { useRoute } from 'vue-router'
|
||||||
// Using 'any' cast if strict checking fails due to optional properties.
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const treeData = ref<MenuRecord[]>([])
|
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 = [
|
const columns = [
|
||||||
@@ -111,7 +137,7 @@ function getTypeName(type: string): string {
|
|||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getMenuList()
|
const res = await getMenuList({ projectId: queryProjectId.value })
|
||||||
// res.data.data is the array
|
// res.data.data is the array
|
||||||
const list = res.data.data || []
|
const list = res.data.data || []
|
||||||
// Cast to any because buildMenuTree typings might be strict about SysMenu properties
|
// Cast to any because buildMenuTree typings might be strict about SysMenu properties
|
||||||
@@ -125,7 +151,8 @@ async function loadData() {
|
|||||||
|
|
||||||
function handleAdd(parentId?: number) {
|
function handleAdd(parentId?: number) {
|
||||||
currentRecord.value = {
|
currentRecord.value = {
|
||||||
parentId: parentId || 0
|
parentId: parentId || 0,
|
||||||
|
projectId: queryProjectId.value // 传入当前选中的项目ID(如果未选中则是undefined/null,即归属主框架)
|
||||||
} as MenuFormData
|
} as MenuFormData
|
||||||
modalVisible.value = true
|
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()
|
loadData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import path from 'path'
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
// 使用绝对路径,确保在任何子路由下都能正确加载资源
|
||||||
|
base: '/',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
// Vue函数自动导入
|
// Vue函数自动导入
|
||||||
@@ -67,39 +69,6 @@ export default defineConfig({
|
|||||||
console.log(`[Backend API Proxy] ${req.method} ${req.url}`)
|
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}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user