lipan преди 2 седмици
родител
ревизия
7342b35160
променени са 2 файла, в които са добавени 540 реда и са изтрити 8 реда
  1. 3 1
      .env.development
  2. 537 7
      src/view/ude/access/access.vue

+ 3 - 1
.env.development

@@ -7,7 +7,9 @@ VITE_BASE_PATH = http://10.10.10.30
 VITE_POSITION = close
 VITE_EDITOR = vscode
 # UDE 后端地址,本地开发连 10.10.10.66
-VITE_UDE_SERVER = http://localhost:9990
+VITE_UDE_SERVER = http://localhost:9993
+# 数据库透明加密后端 udeServer(接入管理/密钥管理,go-zero,端口 9993)
+VITE_UDE_DB_SERVER = http://localhost:9993
 // 业务端服务
 //VITE_SERVICE_PATH = http://127.0.0.1
 //VITE_SERVICE_PORT = 9200

+ 537 - 7
src/view/ude/access/access.vue

@@ -1,18 +1,548 @@
 <template>
   <div>
-    <el-card shadow="never">
-      <template #header>
-        <div class="card-header">
-          <span>接入管理</span>
+    <div class="gva-search-box">
+      <el-form ref="searchForm" :inline="true" :model="searchInfo">
+        <el-form-item label="服务名称">
+          <el-input
+            v-model="searchInfo.name"
+            clearable
+            placeholder="请输入服务名称(模糊)"
+          />
+        </el-form-item>
+        <el-form-item label="外挂服务IP">
+          <el-input
+            v-model="searchInfo.ip"
+            clearable
+            placeholder="请输入外挂服务IP"
+          />
+        </el-form-item>
+        <el-form-item label="接入标识">
+          <el-input
+            v-model="searchInfo.iden"
+            clearable
+            placeholder="请输入接入标识"
+          />
+        </el-form-item>
+        <el-form-item label="服务状态">
+          <el-select
+            v-model="searchInfo.status"
+            clearable
+            placeholder="请选择"
+            style="width: 120px"
+          >
+            <el-option label="启用" value="启用" />
+            <el-option label="停用" value="停用" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button v-preReClick icon="search" type="primary" @click="onSubmit"
+            >查询
+          </el-button>
+          <el-button v-preReClick icon="refresh" @click="onReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="gva-table-box">
+      <div class="gva-btn-list">
+        <el-button v-preReClick icon="plus" type="primary" @click="openDialog"
+          >新增
+        </el-button>
+      </div>
+
+      <el-table
+        ref="multipleTable"
+        :data="tableData"
+        row-key="id"
+        style="width: 100%"
+        tooltip-effect="dark"
+      >
+        <el-table-column align="left" label="服务名称" prop="name" min-width="140" />
+        <el-table-column align="left" label="外挂服务IP" prop="ip" width="150" />
+        <el-table-column align="left" label="端口" prop="port" width="90" />
+        <el-table-column align="left" label="保护目录" min-width="200">
+          <template #default="scope">
+            <el-tooltip
+              v-if="scope.row.directoryText"
+              :content="scope.row.directoryText"
+              placement="top"
+            >
+              <span class="directory-text">{{ scope.row.directoryText }}</span>
+            </el-tooltip>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <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">
+          <template #default="scope">
+            <el-switch
+              v-model="scope.row.status"
+              active-value="启用"
+              inactive-value="停用"
+              inline-prompt
+              active-text="启用"
+              inactive-text="停用"
+              @change="changeStatus(scope.row)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column align="left" label="创建时间" width="180">
+          <template #default="scope">
+            <span>{{ formatDate(scope.row.createAt) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column align="left" label="操作" fixed="right" width="200">
+          <template #default="scope">
+            <el-button
+              v-preReClick
+              icon="view"
+              link
+              type="primary"
+              @click="viewService(scope.row)"
+              >详情
+            </el-button>
+            <el-button
+              v-preReClick
+              icon="edit"
+              link
+              type="primary"
+              @click="updateServiceInfo(scope.row)"
+              >编辑
+            </el-button>
+            <el-popover v-model="scope.row.visible" placement="top" width="160">
+              <p>确定要删除吗?</p>
+              <div style="text-align: right; margin-top: 8px">
+                <el-button
+                  v-preReClick
+                  link
+                  type="primary"
+                  @click="scope.row.visible = false"
+                  >取消
+                </el-button>
+                <el-button
+                  v-preReClick
+                  type="primary"
+                  @click="deleteServiceInfo(scope.row)"
+                  >确定
+                </el-button>
+              </div>
+              <template #reference>
+                <el-button
+                  v-preReClick
+                  icon="delete"
+                  link
+                  type="primary"
+                  @click="scope.row.visible = true"
+                  >删除
+                </el-button>
+              </template>
+            </el-popover>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="gva-pagination">
+        <el-pagination
+          :current-page="page"
+          :page-size="pageSize"
+          :page-sizes="[10, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
+    </div>
+
+    <!-- 详情弹窗 -->
+    <el-dialog v-model="detailVisible" title="服务详情" :before-close="closeDetail">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="服务名称">{{ detail.name }}</el-descriptions-item>
+        <el-descriptions-item label="外挂服务IP">{{ detail.ip }}</el-descriptions-item>
+        <el-descriptions-item label="端口">{{ detail.port }}</el-descriptions-item>
+        <el-descriptions-item label="接入标识">{{ detail.iden }}</el-descriptions-item>
+        <el-descriptions-item label="密钥标识">{{ detail.keyIden }}</el-descriptions-item>
+        <el-descriptions-item label="密钥算法">{{ detail.keyAlgo }}</el-descriptions-item>
+        <el-descriptions-item label="服务状态">{{ detail.status }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ formatDate(detail.createAt) }}</el-descriptions-item>
+        <el-descriptions-item label="保护目录" :span="2">{{ detail.directoryText || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog
+      v-model="dialogFormVisible"
+      :before-close="closeDialog"
+      :title="type === 'create' ? '新增服务' : '编辑服务'"
+    >
+      <el-form ref="dialogForm" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="服务名称" prop="name">
+          <el-input v-model="form.name" clearable placeholder="请输入服务名称" />
+        </el-form-item>
+        <el-form-item label="外挂服务IP" prop="ip">
+          <el-input v-model="form.ip" clearable placeholder="请输入外挂服务IP" />
+        </el-form-item>
+        <el-form-item label="端口" prop="port">
+          <el-input v-model="form.port" clearable placeholder="请输入外挂服务端口" />
+        </el-form-item>
+        <el-form-item label="接入标识">
+          <el-input v-model="form.iden" disabled placeholder="由系统自动生成" />
+        </el-form-item>
+        <el-form-item label="密钥标识" prop="keyIden">
+          <el-select
+            v-model="form.keyIden"
+            clearable
+            filterable
+            placeholder="请选择密钥标识"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="k in keyOptions"
+              :key="k.value"
+              :label="k.label"
+              :value="k.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="密钥算法" prop="keyAlgo">
+          <el-input v-model="form.keyAlgo" clearable placeholder="请输入密钥算法" />
+        </el-form-item>
+        <el-form-item label="服务状态" prop="status">
+          <el-switch
+            v-model="form.status"
+            active-value="启用"
+            inactive-value="停用"
+            inline-prompt
+            active-text="启用"
+            inactive-text="停用"
+          />
+        </el-form-item>
+        <el-form-item label="保护目录">
+          <div style="width: 100%">
+            <div
+              v-for="(item, index) in formDirectories"
+              :key="index"
+              style="display: flex; align-items: center; margin-bottom: 8px"
+            >
+              <el-input
+                v-model="item.directory"
+                clearable
+                placeholder="请输入保护目录路径"
+                style="margin-right: 8px"
+              />
+              <el-button
+                v-preReClick
+                icon="delete"
+                link
+                type="danger"
+                @click="removeDirectory(index)"
+                >删除
+              </el-button>
+            </div>
+            <el-button v-preReClick icon="plus" link type="primary" @click="addDirectory"
+              >添加目录
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button v-preReClick @click="closeDialog">取 消</el-button>
+          <el-button v-preReClick type="primary" @click="enterDialog">确 定</el-button>
         </div>
       </template>
-      <el-empty description="接入管理页面(待实现)" />
-    </el-card>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
+import {
+  createService,
+  deleteService,
+  getServiceList,
+  updateService,
+  getKeyList,
+  getDirectoryList,
+  createDirectory,
+  deleteDirectory
+} from '@/api/service'
+import { ref, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { formatDate } from '@/utils/format'
+
 defineOptions({
   name: 'MaskAccess'
 })
-</script>
+
+// ---------- 查询条件 ----------
+const searchInfo = ref({
+  name: '',
+  ip: '',
+  iden: '',
+  status: ''
+})
+
+// ---------- 列表(后端列表无分页/过滤,前端做客户端过滤 + 分页) ----------
+const allData = ref([])
+const page = ref(1)
+const pageSize = ref(10)
+
+const filterTableData = computed(() => {
+  const name = searchInfo.value.name.trim()
+  const ip = searchInfo.value.ip.trim()
+  const iden = searchInfo.value.iden.trim()
+  const status = searchInfo.value.status
+  return allData.value.filter(item => {
+    if (name && !String(item.name || '').includes(name)) return false
+    if (ip && !String(item.ip || '').includes(ip)) return false
+    if (iden && !String(item.iden || '').includes(iden)) return false
+    if (status && item.status !== status) return false
+    return true
+  })
+})
+
+const tableData = computed(() => {
+  const start = (page.value - 1) * pageSize.value
+  return filterTableData.value.slice(start, start + pageSize.value)
+})
+
+const total = computed(() => filterTableData.value.length)
+
+const handleSizeChange = val => {
+  pageSize.value = val
+  page.value = 1
+}
+const handleCurrentChange = val => {
+  page.value = val
+}
+const onSubmit = () => {
+  page.value = 1
+}
+const onReset = () => {
+  searchInfo.value = { name: '', ip: '', iden: '', status: '' }
+  page.value = 1
+}
+
+// ---------- 密钥下拉(GET /keys) ----------
+const keyOptions = ref([])
+const getKeyData = async () => {
+  const res = await getKeyList()
+  keyOptions.value = (res.list || []).map(k => ({
+    value: k.iden,
+    label: k.name ? `${k.iden}(${k.name})` : k.iden
+  }))
+}
+
+// ---------- 数据加载:合并服务 + 保护目录 ----------
+const getTableData = async () => {
+  const [svcRes, dirRes] = await Promise.all([getServiceList(), getDirectoryList()])
+  const dirs = dirRes.list || []
+  const dirMap = {}
+  dirs.forEach(d => {
+    if (!dirMap[d.serviceId]) dirMap[d.serviceId] = []
+    dirMap[d.serviceId].push({ id: d.id, directory: d.directory })
+  })
+  allData.value = (svcRes.list || []).map(s => {
+    const list = dirMap[s.id] || []
+    return { ...s, directories: list, directoryText: list.map(x => x.directory).join('、') }
+  })
+}
+
+getTableData()
+getKeyData()
+
+// ---------- 服务状态开关 ----------
+const changeStatus = async row => {
+  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
+    })
+    ElMessage({ type: 'success', message: '状态更新成功' })
+    getTableData()
+  } catch (e) {
+    // 失败回滚开关
+    row.status = row.status === '启用' ? '停用' : '启用'
+  }
+}
+
+// ---------- 详情 ----------
+const detailVisible = ref(false)
+const detail = ref({})
+const viewService = row => {
+  detail.value = row
+  detailVisible.value = true
+}
+const closeDetail = () => {
+  detailVisible.value = false
+  detail.value = {}
+}
+
+// ---------- 新增/编辑 ----------
+const dialogFormVisible = ref(false)
+const type = ref('')
+const dialogForm = ref(null)
+const form = ref({
+  id: 0,
+  name: '',
+  ip: '',
+  port: '',
+  iden: '',
+  keyIden: '',
+  keyAlgo: 'SM4-CTR',
+  status: '启用'
+})
+const formDirectories = ref([])
+
+const rules = ref({
+  name: [{ required: true, message: '请输入服务名称', trigger: 'blur' }],
+  ip: [{ required: true, message: '请输入外挂服务IP', trigger: 'blur' }],
+  port: [{ required: true, message: '请输入外挂服务端口', trigger: 'blur' }],
+  keyIden: [{ required: true, message: '请选择密钥标识', trigger: 'change' }],
+  keyAlgo: [{ required: true, message: '请输入密钥算法', trigger: 'blur' }]
+})
+
+// 后端主键无自增,前端生成唯一 int64 作为 id / 目录 id
+const genId = () => Number(`${Date.now()}${Math.floor(Math.random() * 90) + 10}`)
+// 接入标识自动生成
+const genIden = () => `SVC${Date.now()}${Math.floor(Math.random() * 10000)}`
+
+const addDirectory = () => {
+  formDirectories.value.push({ directory: '' })
+}
+const removeDirectory = index => {
+  formDirectories.value.splice(index, 1)
+}
+
+const openDialog = () => {
+  type.value = 'create'
+  form.value = {
+    id: 0,
+    name: '',
+    ip: '',
+    port: '',
+    iden: genIden(),
+    keyIden: '',
+    keyAlgo: 'SM4-CTR',
+    status: '启用'
+  }
+  formDirectories.value = [{ directory: '' }]
+  dialogFormVisible.value = true
+}
+
+const updateServiceInfo = row => {
+  type.value = 'update'
+  form.value = {
+    id: row.id,
+    name: row.name,
+    ip: row.ip,
+    port: row.port,
+    iden: row.iden,
+    keyIden: row.keyIden,
+    keyAlgo: row.keyAlgo,
+    status: row.status
+  }
+  formDirectories.value =
+    row.directories && row.directories.length
+      ? row.directories.map(d => ({ id: d.id, directory: d.directory }))
+      : [{ directory: '' }]
+  dialogFormVisible.value = true
+}
+
+const closeDialog = () => {
+  dialogFormVisible.value = false
+  form.value = {
+    id: 0,
+    name: '',
+    ip: '',
+    port: '',
+    iden: '',
+    keyIden: '',
+    keyAlgo: 'SM4-CTR',
+    status: '启用'
+  }
+  formDirectories.value = []
+}
+
+const enterDialog = async () => {
+  dialogForm.value.validate(async valid => {
+    if (!valid) return
+    let serviceId = form.value.id
+    try {
+      if (type.value === 'create') {
+        serviceId = genId()
+        await createService({
+          id: serviceId,
+          name: form.value.name,
+          ip: form.value.ip,
+          port: form.value.port,
+          iden: form.value.iden,
+          keyIden: form.value.keyIden,
+          keyAlgo: form.value.keyAlgo,
+          status: form.value.status
+        })
+      } else {
+        await updateService(serviceId, {
+          name: form.value.name,
+          ip: form.value.ip,
+          port: form.value.port,
+          iden: form.value.iden,
+          keyIden: form.value.keyIden,
+          keyAlgo: form.value.keyAlgo,
+          status: form.value.status
+        })
+        // 保护目录按「删旧 + 重建」同步
+        const oldDirs = formDirectories.value.filter(d => d.id).map(d => d.id)
+        await Promise.all(oldDirs.map(id => deleteDirectory(id)))
+      }
+
+      // 保存保护目录(过滤空值)
+      const dirs = formDirectories.value.map(d => d.directory.trim()).filter(Boolean)
+      for (const dir of dirs) {
+        await createDirectory({ id: genId(), serviceId, directory: dir })
+      }
+
+      ElMessage({ type: 'success', message: '操作成功' })
+      closeDialog()
+      getTableData()
+    } catch (e) {
+      // 错误提示已由 service.js 拦截器弹出
+    }
+  })
+}
+
+// ---------- 删除(含关联保护目录) ----------
+const deleteServiceInfo = async row => {
+  row.visible = false
+  try {
+    const dirIds = (row.directories || []).map(d => d.id).filter(Boolean)
+    await Promise.all(dirIds.map(id => deleteDirectory(id)))
+    await deleteService(row.id)
+    ElMessage({ type: 'success', message: '删除成功' })
+    if (tableData.value.length === 1 && page.value > 1) {
+      page.value--
+    }
+    getTableData()
+  } catch (e) {
+    // 错误提示已由 service.js 拦截器弹出
+  }
+}
+</script>
+
+<style scoped>
+.directory-text {
+  display: inline-block;
+  max-width: 180px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+</style>