Sfoglia il codice sorgente

feat:去除多余的管理页面

lipan 1 settimana fa
parent
commit
d179a59de9

+ 4 - 0
src/api/ude.js

@@ -78,6 +78,10 @@ export const createService = data => service.post(`${BASE}/services`, data) // d
 export const updateService = (id, data) => service.put(`${BASE}/services/${id}`, data)
 export const deleteService = id => service.delete(`${BASE}/services/${id}`)
 
+// 启用服务联动挂载 / 停用服务联动卸载(服务启用状态开关联动,后端同步更新 status 和 mount_status)
+export const mountService = id => service.post(`${BASE}/services/${id}/mount`)
+export const unmountService = id => service.post(`${BASE}/services/${id}/unmount`)
+
 // 一键刷库:下发刷库指令,外挂程序启动并同步配置后对保护目录内全量数据做加密处理
 export const brushService = id => service.post(`${BASE}/services/${id}/brush`)
 

+ 170 - 36
src/view/ude/access/access.vue

@@ -75,7 +75,7 @@
         <el-table-column align="left" label="接入标识" prop="iden" min-width="140" />
         <el-table-column align="left" label="密钥标识" prop="keyIden" min-width="140" />
         <el-table-column align="left" label="密钥算法" prop="keyAlgo" width="120" />
-        <el-table-column align="left" label="服务状态" width="110">
+        <el-table-column align="left" label="服务状态" width="130">
           <template #default="scope">
             <el-switch
               v-model="scope.row.status"
@@ -178,8 +178,26 @@
         <el-form-item label="端口" prop="port">
           <el-input v-model="form.port" clearable placeholder="请输入外挂服务端口" maxlength="5" style="width: 140px" />
         </el-form-item>
-        <el-form-item label="接入标识">
-          <el-input v-model="form.iden" disabled placeholder="由系统自动生成" style="width: 400px" />
+        <el-form-item label="接入标识" prop="iden">
+          <!-- 新增/编辑(非挂载成功):下拉选接入应用管理里"启用"状态的应用,显示的是 key(apiKey),
+               实际存进 form.iden 的是密钥值(apiSecret),提交后保存到服务表 iden 字段 -->
+          <el-select
+            v-if="type === 'create' || form.mountStatus !== 'mounted'"
+            v-model="form.iden"
+            clearable
+            filterable
+            placeholder="请选择接入应用"
+            style="width: 400px"
+          >
+            <el-option
+              v-for="a in appOptions"
+              :key="a.label"
+              :label="a.label"
+              :value="a.label"
+            />
+          </el-select>
+          <!-- 编辑且挂载成功:实际挂载正在用该 iden 作 X-TOKEN,改了会造成账实分离,锁定回显 -->
+          <el-input v-else v-model="form.iden" disabled style="width: 400px" />
         </el-form-item>
         <el-form-item label="密钥标识" prop="keyIden">
           <el-select
@@ -191,7 +209,7 @@
             style="width: 320px"
           >
             <el-option
-              v-for="k in keyOptions"
+              v-for="k in (type === 'create' ? keyOptionsForCreate : keyOptions)"
               :key="k.value"
               :label="k.label"
               :value="k.value"
@@ -208,7 +226,9 @@
         <el-form-item label="密钥算法">
           <el-input v-model="form.keyAlgo" disabled placeholder="请输入密钥算法" style="width: 220px" />
         </el-form-item>
-        <el-form-item label="服务状态" prop="status">
+        <!-- 服务状态开关仅新增弹窗显示:新增时选择初始状态;
+             编辑页不需要(状态由列表开关管理,挂载/卸载联动) -->
+        <el-form-item v-if="type === 'create'" label="服务状态" prop="status">
           <el-switch
             v-model="form.status"
             active-value="启用"
@@ -278,9 +298,16 @@
       width="560px"
       :before-close="closeLocalTree"
     >
+      <!-- 刷新:换 key 销毁重建树,从根目录重新懒加载(服务器目录有变更时使用),勾选状态一并重置 -->
+      <div class="tree-toolbar">
+        <el-button v-preReClick icon="refresh" size="small" @click="refreshLocalTree"
+          >刷新整个目录</el-button
+        >
+      </div>
       <!-- v-if: 关闭弹窗即销毁树,重开时按当前表单 IP/port 重新懒加载,避免 lazy 缓存串机器 -->
       <el-tree
         v-if="localTreeVisible"
+        :key="localTreeKey"
         ref="localTreeRef"
         lazy
         show-checkbox
@@ -317,9 +344,12 @@ import {
   createDirectory,
   deleteDirectory,
   brushService,
+  mountService,
+  unmountService,
   getDirChildren
 } from '@/api/ude'
-import { ref, computed, watch } from 'vue'
+import { getSysApplicationList } from '@/api/sys/sysApplication'
+import { ref, computed, watch, nextTick } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { formatDate } from '@/utils/format'
 
@@ -382,9 +412,35 @@ const getKeyData = async () => {
   const keys = await getAllKeys()
   keyOptions.value = keys.map(k => ({
     value: k.iden,
-    label: k.name ? `${k.iden}(${k.name})` : k.iden
+    label: k.name ? `${k.iden}(${k.name})` : k.iden,
+    status: String(k.status) // 密钥状态:'1'=启用,'2'=停用;转字符串保证类型一致
   }))
 }
+// 新增弹窗:密钥标识只能从"启用"状态的密钥里选;编辑弹窗保持全量列表不动
+const keyOptionsForCreate = computed(() =>
+  keyOptions.value.filter(k => k.status === '1')
+)
+
+// ---------- 接入标识下拉(接入应用管理列表,仅启用状态的应用) ----------
+const appOptions = ref([])
+const getAppData = async () => {
+  const all = []
+  let page = 1
+  for (;;) {
+    // POST 分页拉取应用管理列表(gva 侧接口:响应 code=100000 才算成功,
+    // 数据在 data.applicationLists,结构 {name, apiKey, apiSecret, status})
+    const res = await getSysApplicationList({ page, pageSize: 100 })
+    if (res.code !== 100000) break
+    const list = res.data.applicationLists || []
+    all.push(...list)
+    if (all.length >= res.data.total || list.length === 0) break
+    page++
+  }
+  // status=1 启用、2 停用;下拉显示 apiKey(key),选中的 value 是 apiSecret(密钥值)
+  appOptions.value = all
+    .filter(a => a.status === 1)
+    .map(a => ({ label: a.apiKey, value: a.apiSecret }))
+}
 
 // ---------- 数据加载:合并服务 + 保护目录 ----------
 const allServicesCache = ref([]) // 全量服务(用于客户端校验查重)
@@ -406,6 +462,7 @@ const getTableData = async () => {
 
 getTableData()
 getKeyData()
+getAppData()
 
 // ---------- 服务状态开关 ----------
 // 挂载状态:后端字段 mountStatus(mounted/unmounted/failed) → 中文文案
@@ -417,31 +474,71 @@ const mountTagType = s =>
   ({ mounted: 'success', unmounted: 'info', failed: 'danger' }[s] || 'info')
 
 const changeStatus = async row => {
-  // 已挂载的服务禁止停用:挂载中目录仍被 FUSE 占用,此时停用会造成
-  // "库里状态是停用、机器上实际还在挂载"的账实分离,必须先卸载再停用。
-  // 语法:el-switch 的 @change 触发时,v-model 已把 row.status 改成新值;
-  // 拦截时手动改回 '启用',开关就会弹回原位,且不发任何请求。
-  if (row.mountStatus === 'mounted' && row.status === '停用') {
-    row.status = '启用'
-    ElMessage({ type: 'warning', message: '当前服务已被挂载,卸掉挂载才可停用' })
-    return
+  // 服务启用状态开关联动,按开关方向 + 当前挂载状态分流:
+  //   拨向"启用"              → 调挂载接口(外挂把该服务全部保护目录挂上)
+  //   拨向"停用"且已挂载     → 先弹框确认,再调卸载接口(外挂真实卸载+后端同步改库)
+  //   拨向"停用"且未挂载     → 不调卸载接口,只调更新接口改库里这条数据的 status
+  // 语法:el-switch 的 @change 触发时,v-model 已把 row.status 改成新值,按它判断方向。
+  // 启用(会真实挂载)和"已挂载状态停用"(会真实卸载)两个方向都先弹框确认。
+  if (row.status === '启用' || (row.status === '停用' && row.mountStatus === 'mounted')) {
+    try {
+      if (row.status === '启用') {
+        await ElMessageBox.confirm(
+          `确认启用服务「${row.name}」吗?启用后将挂载其全部保护目录。`,
+          '启用服务',
+          { type: 'warning', confirmButtonText: '确认启用', cancelButtonText: '取消' }
+        )
+      } else {
+        await ElMessageBox.confirm(
+          `确认停用服务「${row.name}」吗?停用后将卸载其全部保护目录的挂载。`,
+          '停用服务',
+          { type: 'warning', confirmButtonText: '确认停用', cancelButtonText: '取消' }
+        )
+      }
+    } catch (e) {
+      // 用户点了取消,开关弹回原位,不发任何请求
+      row.status = row.status === '启用' ? '停用' : '启用'
+      return
+    }
   }
   try {
-    await updateService(row.id, {
-      name: row.name,
-      ip: row.ip,
-      port: row.port,
-      iden: row.iden,
-      keyIden: row.keyIden,
-      keyAlgo: row.keyAlgo,
-      status: row.status,
-      // 状态开关是"全字段覆盖式"更新,必须带上 brush,否则会把一键刷库配置覆盖成 0
-      brush: row.brush || 0
-    })
-    ElMessage({ type: 'success', message: '状态更新成功' })
+    if (row.status === '停用' && row.mountStatus !== 'mounted') {
+      // 未挂载停用:全字段覆盖式更新(必须带 brush,否则零值会覆盖一键刷库配置);
+      // 不传 directories → 后端不做挂载联动,mount_status 保持原值(不会把 failed 冲掉)
+      await updateService(row.id, {
+        name: row.name,
+        ip: row.ip,
+        port: row.port,
+        iden: row.iden,
+        keyIden: row.keyIden,
+        keyAlgo: row.keyAlgo,
+        status: '停用',
+        brush: row.brush || 0
+      })
+      ElMessage({ type: 'success', message: '服务已停用' })
+    } else {
+      const res = row.status === '启用'
+        ? await mountService(row.id)
+        : await unmountService(row.id)
+      // 启用方向:挂载失败(整体 failed)→ 后端已把状态落库为停用,
+      // 开关同步弹回"停用"位,启用状态与挂载成败挂钩
+      if (row.status === '启用' && res?.mountStatus === 'failed') {
+        row.status = '停用'
+        ElMessage({ type: 'warning', message: res.mountWarning || '挂载失败,服务保持停用', duration: 6000 })
+      } else if (res?.mountWarning) {
+        // 挂/卸载部分目录失败:后端仍返回 200 + mountWarning(黄色警告),
+        // status/mount_status 已按实际结果落库,刷新表格即可看到最新状态
+        ElMessage({ type: 'warning', message: res.mountWarning, duration: 6000 })
+      } else {
+        ElMessage({
+          type: 'success',
+          message: row.status === '启用' ? '服务已启用,保护目录挂载完成' : '服务已停用,挂载已全部卸载'
+        })
+      }
+    }
     getTableData()
   } catch (e) {
-    // 失败回滚开关
+    // 请求级失败(网络/参数错误):回滚开关到原状态,错误提示已由 ude.js 拦截器弹出
     row.status = row.status === '启用' ? '停用' : '启用'
   }
 }
@@ -482,6 +579,12 @@ const ipv4Regex =
 // ---------- 服务器本地目录懒加载树(从根目录 / 开始,只列目录) ----------
 const localTreeVisible = ref(false)
 const localTreeRef = ref(null)
+// 树刷新 key:懒加载树把「已加载」标记记在内部 node 上,直接清数据数组不会重新触发 load。
+// 每次刷新 key+1 强制 Vue 销毁重建整个 el-tree,从根目录重新懒加载,勾选状态一并重置
+const localTreeKey = ref(0)
+const refreshLocalTree = () => {
+  localTreeKey.value++
+}
 
 const openLocalTree = async () => {
   // 目录树由 udeServer 转发到外挂程序(/path/lists)获取,依赖表单里填的外挂服务地址:
@@ -511,15 +614,39 @@ const loadLocalDirNode = (node, resolve) => {
     .catch(() => resolve([])) // 失败降级为空,不卡树
 }
 
-// 确认勾选:只取「完全勾选」的节点(不含半选父节点,避免父子重复回填),与已有行合并去重
+// 确认勾选:取所有「完全勾选」的节点,父子同时勾选时只保留最上层(被父级
+// 级联带勾的后代丢弃,父目录已覆盖子目录),与已有行合并去重。
+// 注意不能用旧写法 getCheckedNodes(true)(只取叶子):懒加载树里勾选一个还没
+// 展开的非叶目录(如 /home)时它不算叶子,会回填为空或只回填它已加载的子孙。
 const confirmLocalTree = () => {
   const tree = localTreeRef.value
   if (!tree) return
-  const nodes = tree.getCheckedNodes(true, false)
-  if (!nodes.length) {
+  // getCheckedNodes(false, false):返回所有完全勾选的节点(含父节点,不含半选)
+  const checked = tree.getCheckedNodes(false, false)
+  if (!checked.length) {
     ElMessage({ type: 'warning', message: '请至少勾选一个目录' })
     return
   }
+  // 1. normalize(解决尾斜杠/多斜杠)
+  const norm = p => p.replace(/\/+/g, '/').replace(/\/$/, '') || '/'
+  // 父路径推导:'/a/b' → '/a','/a' → '/'(根)
+  const seen = new Set()
+  const list = []
+  for (const n of checked) {
+    if (!n || typeof n.path !== 'string') continue
+    const p = norm(n.path)
+    if (seen.has(p)) continue
+    seen.add(p)
+    list.push({ ...n, path: p })
+  }
+  // 3. 叶子过滤
+  const nodes = list.filter(n =>
+    !list.some(m =>
+      m.path !== n.path &&
+      m.path.startsWith(n.path === '/' ? '/' : n.path + '/')
+    )
+  )
+
   const existing = new Set(
     form.value.directories.map(d => (d.directory || '').trim()).filter(Boolean)
   )
@@ -631,12 +758,10 @@ const rules = ref({
     { required: true, message: '请输入外挂服务端口', trigger: 'blur' },
     { validator: validatePort, trigger: 'blur' }
   ],
-  keyIden: [{ required: true, message: '请选择密钥标识', trigger: 'change' }]
+  keyIden: [{ required: true, message: '请选择密钥标识', trigger: 'change' }],
+  iden: [{ required: true, message: '请选择接入标识', trigger: 'change' }]
 })
 
-// 接入标识自动生成
-const genIden = () => `SVC${Date.now()}${Math.floor(Math.random() * 10000)}`
-
 const addDirectory = () => {
   form.value.directories.push({ directory: '' })
 }
@@ -651,7 +776,7 @@ const openDialog = () => {
     name: '',
     ip: '',
     port: '',
-    iden: genIden(),
+    iden: '', // 接入标识改为下拉选择(保存的是所选应用的密钥值 apiSecret)
     keyIden: '',
     keyAlgo: 'SM4-CTR',
     status: '启用',
@@ -659,6 +784,8 @@ const openDialog = () => {
     directories: [{ directory: '' }]
   }
   dialogFormVisible.value = true
+  // el-dialog 内容关闭后不销毁,上一次弹窗的校验红框会残留到本次打开,主动清一次
+  nextTick(() => dialogForm.value?.clearValidate())
 }
 
 const updateServiceInfo = row => {
@@ -682,6 +809,8 @@ const updateServiceInfo = row => {
         : [{ directory: '' }]
   }
   dialogFormVisible.value = true
+  // 同 openDialog:清掉上一次弹窗残留的校验红框(编辑已挂载服务时 iden 是禁用回显,更不该带红框)
+  nextTick(() => dialogForm.value?.clearValidate())
 }
 
 const closeDialog = () => {
@@ -834,6 +963,11 @@ const brushServiceInfo = async row => {
 </script>
 
 <style scoped>
+/* 目录树弹窗顶部工具条(刷新按钮所在行) */
+.tree-toolbar {
+  margin-bottom: 8px;
+}
+
 .directory-text {
   display: inline-block;
   max-width: 180px;

+ 0 - 18
src/view/ude/system/log.vue

@@ -1,18 +0,0 @@
-<template>
-  <div>
-    <el-card shadow="never">
-      <template #header>
-        <div class="card-header">
-          <span>日志管理</span>
-        </div>
-      </template>
-      <el-empty description="日志管理页面(待实现)" />
-    </el-card>
-  </div>
-</template>
-
-<script setup>
-defineOptions({
-  name: 'MaskLog'
-})
-</script>

+ 0 - 18
src/view/ude/system/role.vue

@@ -1,18 +0,0 @@
-<template>
-  <div>
-    <el-card shadow="never">
-      <template #header>
-        <div class="card-header">
-          <span>角色管理</span>
-        </div>
-      </template>
-      <el-empty description="角色管理页面(待实现)" />
-    </el-card>
-  </div>
-</template>
-
-<script setup>
-defineOptions({
-  name: 'MaskRole'
-})
-</script>

+ 0 - 18
src/view/ude/system/user.vue

@@ -1,18 +0,0 @@
-<template>
-  <div>
-    <el-card shadow="never">
-      <template #header>
-        <div class="card-header">
-          <span>用户管理</span>
-        </div>
-      </template>
-      <el-empty description="用户管理页面(待实现)" />
-    </el-card>
-  </div>
-</template>
-
-<script setup>
-defineOptions({
-  name: 'MaskUser'
-})
-</script>