264 lines
7.4 KiB
Vue
264 lines
7.4 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 数据库管理(Database)
|
||
*
|
||
* 迁移自 TempLate/GERP/pages/system/database.html(166 行)
|
||
*
|
||
* 原生实现要点:
|
||
* - 仅管理员可访问(GERP.CheckAdmin),非管理员显示无权访问
|
||
* - 主实例(默认服务器):GET /API/FormAPI/Tables 返回全部表,按 type=static/dynamic 分两组展示
|
||
* - 子服务器:GET /api/gerp/msync/servers/{server}/form/?Action=tables 返回该服务器表列表
|
||
* - 表项字段:name / title / type(static|dynamic) / count(记录数)
|
||
* - 点击"查看"跳转 dbtable.html?server&table&title
|
||
*
|
||
* Vue 版改动:
|
||
* - server / admin_verify 由 request.ts 拦截器统一注入(主实例 FormAPI 路径自动注入)
|
||
* - 权限校验改用 userStore.isAdmin 前端预拦截;非管理员显示无权访问占位
|
||
* - 子服务器表列表接口路径已含 serverName,admin_verify 仍由拦截器按 URL query 注入
|
||
* - 跳转改用 vue-router:/system/dbtable?table=xxx&title=xxx(子服务器追加 server)
|
||
* - 返回服务器列表改用 router.push(替代 GERP.Navigate)
|
||
*/
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { get } from '@/api/request'
|
||
import { setPageTitle, getServer, isDefaultServer } from '@/utils/serverScope'
|
||
import { useUserStore } from '@/stores/user'
|
||
import AdminBadge from '@/components/AdminBadge.vue'
|
||
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
const isAdmin = computed(() => userStore.isAdmin)
|
||
const denied = ref(false)
|
||
|
||
/** 服务器标识(主实例为 gvsds-master) */
|
||
const serverName = getServer()
|
||
/** 是否主实例 */
|
||
const isMain = isDefaultServer()
|
||
|
||
/** 表项(对齐后端字段) */
|
||
interface TableItem {
|
||
name: string
|
||
title?: string
|
||
type?: 'static' | 'dynamic'
|
||
count?: number
|
||
}
|
||
|
||
const loading = ref(false)
|
||
/** 静态表列表(仅主实例有分组) */
|
||
const staticTables = ref<TableItem[]>([])
|
||
/** 系统表 / 子服务器数据表列表 */
|
||
const dynamicTables = ref<TableItem[]>([])
|
||
|
||
/** 页面标题 */
|
||
const pageTitle = computed(() => (isMain ? '数据库管理' : `${serverName} — 数据库管理`))
|
||
|
||
setPageTitle(pageTitle.value)
|
||
|
||
/** 加载主实例表列表:GET /API/FormAPI/Tables */
|
||
async function loadMainTables(): Promise<void> {
|
||
loading.value = true
|
||
try {
|
||
const data = await get<TableItem[]>('/API/FormAPI/Tables')
|
||
const tables = Array.isArray(data) ? data : []
|
||
staticTables.value = tables.filter((t) => t.type === 'static')
|
||
dynamicTables.value = tables.filter((t) => t.type === 'dynamic')
|
||
} catch {
|
||
/* 拦截器已弹错误 */
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 加载子服务器表列表:GET /api/gerp/msync/servers/{server}/form/?Action=tables */
|
||
async function loadSubTables(): Promise<void> {
|
||
loading.value = true
|
||
try {
|
||
const data = await get<TableItem[]>(`/api/gerp/msync/servers/${encodeURIComponent(serverName)}/form/`, {
|
||
Action: 'tables',
|
||
})
|
||
dynamicTables.value = Array.isArray(data) ? data : []
|
||
} catch {
|
||
dynamicTables.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 查看表:跳转 DbTable 页面 */
|
||
function viewTable(table: TableItem): void {
|
||
const query: Record<string, string> = {
|
||
table: table.name,
|
||
title: table.title || table.name,
|
||
}
|
||
if (!isMain) query.server = serverName
|
||
router.push({ path: '/system/dbtable', query })
|
||
}
|
||
|
||
/** 返回服务器列表 */
|
||
function goBack(): void {
|
||
router.push({ path: '/system/msync-servers' })
|
||
}
|
||
|
||
/** 表项展示文本:标题与名称 */
|
||
function tableLabel(table: TableItem): string {
|
||
return table.title && table.title !== table.name ? `${table.title}(${table.name})` : table.name
|
||
}
|
||
|
||
/** 表项记录数 / 类型描述 */
|
||
function tableMeta(table: TableItem): string {
|
||
if (typeof table.count === 'number' && table.count >= 0) return `${table.count} 条记录`
|
||
return table.type === 'dynamic' ? '系统表' : '自定义表'
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (!isAdmin.value) {
|
||
denied.value = true
|
||
return
|
||
}
|
||
if (isMain) {
|
||
loadMainTables()
|
||
} else {
|
||
loadSubTables()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="db-page">
|
||
<AdminBadge />
|
||
|
||
<el-empty
|
||
v-if="denied"
|
||
description="无权访问:该页面仅管理员可查看,请切换至管理员角色或联系管理员"
|
||
>
|
||
<template #image>
|
||
<i class="fas fa-lock" style="font-size: 48px; color: var(--el-color-danger)" />
|
||
</template>
|
||
</el-empty>
|
||
|
||
<template v-else>
|
||
<!-- 子服务器:返回服务器列表 -->
|
||
<div v-if="!isMain" class="db-back" @click="goBack">
|
||
<i class="fas fa-arrow-left" /> <span>返回服务器列表</span>
|
||
</div>
|
||
|
||
<!-- 页头 -->
|
||
<div class="db-header">
|
||
<h1 class="page-title">
|
||
<i class="fas fa-database icon-primary" />
|
||
<span>{{ pageTitle }}</span>
|
||
</h1>
|
||
</div>
|
||
|
||
<!-- 静态表分组(仅主实例) -->
|
||
<div v-if="isMain" class="db-section">
|
||
<h3>静态表</h3>
|
||
<div v-loading="loading" class="db-list">
|
||
<div v-for="t in staticTables" :key="t.name" class="db-table-item">
|
||
<span class="db-table-name">{{ tableLabel(t) }}</span>
|
||
<span class="db-table-meta">{{ tableMeta(t) }}</span>
|
||
<el-button size="small" type="primary" @click="viewTable(t)">
|
||
<i class="fas fa-table" /> 查看
|
||
</el-button>
|
||
</div>
|
||
<el-empty v-if="!loading && staticTables.length === 0" description="暂无数据" :image-size="60" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统表 / 子服务器数据表分组 -->
|
||
<div class="db-section">
|
||
<h3>{{ isMain ? '系统表' : '数据表' }}</h3>
|
||
<div v-loading="loading" class="db-list">
|
||
<div v-for="t in dynamicTables" :key="t.name" class="db-table-item">
|
||
<span class="db-table-name">{{ tableLabel(t) }}</span>
|
||
<span class="db-table-meta">{{ tableMeta(t) }}</span>
|
||
<el-button size="small" type="primary" @click="viewTable(t)">
|
||
<i class="fas fa-table" /> 查看
|
||
</el-button>
|
||
</div>
|
||
<el-empty v-if="!loading && dynamicTables.length === 0" description="暂无数据表" :image-size="60" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.db-page {
|
||
padding: 20px;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.db-back {
|
||
cursor: pointer;
|
||
color: var(--el-color-primary);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 14px;
|
||
margin-bottom: 16px;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.db-header {
|
||
margin-bottom: 20px;
|
||
|
||
.page-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.icon-primary {
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.db-section {
|
||
margin-bottom: 30px;
|
||
|
||
h3 {
|
||
margin-bottom: 10px;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.db-list {
|
||
min-height: 60px;
|
||
}
|
||
|
||
.db-table-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
background: var(--el-bg-color);
|
||
border: 1px solid var(--el-border-color);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
border-color: var(--el-color-primary);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.db-table-name {
|
||
flex: 1;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.db-table-meta {
|
||
margin-right: 16px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
</style>
|