|
|
@@ -0,0 +1,320 @@
|
|
|
+<!-- 外部客户端管理 -->
|
|
|
+<template>
|
|
|
+ <div class="ele-body">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <external-client-search @search="reload" />
|
|
|
+ <ele-pro-table
|
|
|
+ ref="table"
|
|
|
+ :columns="columns"
|
|
|
+ :datasource="datasource"
|
|
|
+ height="calc(100vh - 385px)"
|
|
|
+ row-key="id"
|
|
|
+ :page-size="pageSize"
|
|
|
+ @columns-change="handleColumnChange"
|
|
|
+ :cache-key="cacheKeyUrl"
|
|
|
+ >
|
|
|
+ <template v-slot:toolbar>
|
|
|
+ <el-button
|
|
|
+ v-if="$hasPermission('main:externalClient:save')"
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ class="ele-btn-icon"
|
|
|
+ @click="openEdit()"
|
|
|
+ >
|
|
|
+ 新建
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ <template v-slot:status="{ row }">
|
|
|
+ <el-tag :type="Number(row.status) === 1 ? 'success' : 'info'">
|
|
|
+ {{ Number(row.status) === 1 ? '启用' : '禁用' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <template v-slot:action="{ row }">
|
|
|
+ <el-link
|
|
|
+ v-if="row.clientId"
|
|
|
+ v-clipboard:copy="row.clientId"
|
|
|
+ v-clipboard:error="onCopyClientIdError"
|
|
|
+ v-clipboard:success="onCopyClientId"
|
|
|
+ type="primary"
|
|
|
+ :underline="false"
|
|
|
+ icon="el-icon-document-copy"
|
|
|
+ class="ele-action"
|
|
|
+ >
|
|
|
+ 复制clientId
|
|
|
+ </el-link>
|
|
|
+ <el-link
|
|
|
+ v-if="$hasPermission('main:externalClient:update')"
|
|
|
+ type="primary"
|
|
|
+ :underline="false"
|
|
|
+ icon="el-icon-edit"
|
|
|
+ class="ele-action"
|
|
|
+ @click="openEdit(row)"
|
|
|
+ >
|
|
|
+ 修改
|
|
|
+ </el-link>
|
|
|
+ <el-popconfirm
|
|
|
+ v-if="$hasPermission('main:externalClient:enableOrDisable')"
|
|
|
+ class="ele-action"
|
|
|
+ :title="
|
|
|
+ Number(row.status) === 1
|
|
|
+ ? '禁用后该客户端已有 Token 将被吊销,确定继续吗?'
|
|
|
+ : '确定启用此客户端吗?'
|
|
|
+ "
|
|
|
+ @confirm="toggleStatus(row)"
|
|
|
+ >
|
|
|
+ <template v-slot:reference>
|
|
|
+ <el-link type="primary" :underline="false">
|
|
|
+ {{ Number(row.status) === 1 ? '禁用' : '启用' }}
|
|
|
+ </el-link>
|
|
|
+ </template>
|
|
|
+ </el-popconfirm>
|
|
|
+ <el-popconfirm
|
|
|
+ v-if="$hasPermission('main:externalClient:rotateSecret')"
|
|
|
+ class="ele-action"
|
|
|
+ title="轮换后旧 Secret 和已有 Token 会立即失效,确定继续吗?"
|
|
|
+ @confirm="rotateSecret(row)"
|
|
|
+ >
|
|
|
+ <template v-slot:reference>
|
|
|
+ <el-link type="warning" :underline="false">重置签名</el-link>
|
|
|
+ </template>
|
|
|
+ </el-popconfirm>
|
|
|
+ <el-popconfirm
|
|
|
+ v-if="$hasPermission('main:externalClient:revokeToken')"
|
|
|
+ class="ele-action"
|
|
|
+ title="确定吊销该客户端当前已有的全部 Token 吗?"
|
|
|
+ @confirm="revokeToken(row)"
|
|
|
+ >
|
|
|
+ <template v-slot:reference>
|
|
|
+ <el-link type="danger" :underline="false">吊销 Token</el-link>
|
|
|
+ </template>
|
|
|
+ </el-popconfirm>
|
|
|
+ </template>
|
|
|
+ </ele-pro-table>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <external-client-edit
|
|
|
+ :visible.sync="showEdit"
|
|
|
+ :data="current"
|
|
|
+ @done="handleEditDone"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ title="客户端 Secret"
|
|
|
+ :visible.sync="secretVisible"
|
|
|
+ width="560px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :show-close="false"
|
|
|
+ >
|
|
|
+ <el-alert
|
|
|
+ title="Secret 只展示这一次,请立即安全保存并交付给外部系统。关闭后无法再次查询明文。"
|
|
|
+ type="warning"
|
|
|
+ :closable="false"
|
|
|
+ show-icon
|
|
|
+ style="margin-bottom: 16px"
|
|
|
+ />
|
|
|
+ <el-input v-model="clientSecret" type="textarea" :rows="3" readonly />
|
|
|
+ <template v-slot:footer>
|
|
|
+ <el-button
|
|
|
+ v-clipboard:copy="clientSecret"
|
|
|
+ v-clipboard:error="onCopyError"
|
|
|
+ v-clipboard:success="onCopy"
|
|
|
+ >
|
|
|
+ 复制 Secret
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" @click="closeSecret">
|
|
|
+ 我已安全保存
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import tabMixins from '@/mixins/tableColumnsMixin';
|
|
|
+ import ExternalClientSearch from './components/external-client-search.vue';
|
|
|
+ import ExternalClientEdit from './components/external-client-edit.vue';
|
|
|
+ import {
|
|
|
+ pageExternalClients,
|
|
|
+ setExternalClientStatus,
|
|
|
+ rotateExternalClientSecret,
|
|
|
+ revokeExternalClientToken
|
|
|
+ } from '@/api/system/externalClient';
|
|
|
+
|
|
|
+ export default {
|
|
|
+ name: 'ExternalClient',
|
|
|
+ mixins: [tabMixins],
|
|
|
+ components: {
|
|
|
+ ExternalClientSearch,
|
|
|
+ ExternalClientEdit
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ columns: [
|
|
|
+ {
|
|
|
+ columnKey: 'index',
|
|
|
+ type: 'index',
|
|
|
+ width: 55,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'left',
|
|
|
+ label: '序号'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'clientId',
|
|
|
+ label: '客户端 ID',
|
|
|
+ minWidth: 230,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'systemCode',
|
|
|
+ label: '系统编码',
|
|
|
+ minWidth: 120,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'systemName',
|
|
|
+ label: '系统名称',
|
|
|
+ minWidth: 140,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'status',
|
|
|
+ label: '状态',
|
|
|
+ width: 80,
|
|
|
+ align: 'center',
|
|
|
+ slot: 'status'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'effectiveTime',
|
|
|
+ label: '生效时间',
|
|
|
+ minWidth: 155,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'expireTime',
|
|
|
+ label: '失效时间',
|
|
|
+ minWidth: 155,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'ipWhitelist',
|
|
|
+ label: '来源 IP 白名单',
|
|
|
+ minWidth: 180,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'lastAccessTime',
|
|
|
+ label: '最近访问时间',
|
|
|
+ minWidth: 155,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ prop: 'createTime',
|
|
|
+ label: '创建时间',
|
|
|
+ minWidth: 155,
|
|
|
+ showOverflowTooltip: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ columnKey: 'action',
|
|
|
+ label: '操作',
|
|
|
+ width: 340,
|
|
|
+ align: 'center',
|
|
|
+ resizable: false,
|
|
|
+ slot: 'action'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ current: null,
|
|
|
+ showEdit: false,
|
|
|
+ pageSize: this.$store.state.tablePageSize,
|
|
|
+ cacheKeyUrl: 'external-client-management',
|
|
|
+ secretVisible: false,
|
|
|
+ clientSecret: ''
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ datasource({ page, limit, where, order }) {
|
|
|
+ return pageExternalClients({
|
|
|
+ ...where,
|
|
|
+ ...order,
|
|
|
+ pageNum: page,
|
|
|
+ size: limit
|
|
|
+ });
|
|
|
+ },
|
|
|
+ reload(where) {
|
|
|
+ this.$refs.table.reload({ page: 1, where });
|
|
|
+ },
|
|
|
+ openEdit(row) {
|
|
|
+ this.current = row || null;
|
|
|
+ this.showEdit = true;
|
|
|
+ },
|
|
|
+ handleEditDone(result) {
|
|
|
+ this.reload();
|
|
|
+ if (result && result.clientSecret) {
|
|
|
+ this.clientSecret = result.clientSecret;
|
|
|
+ this.secretVisible = true;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async toggleStatus(row) {
|
|
|
+ try {
|
|
|
+ const status = Number(row.status) === 1 ? 0 : 1;
|
|
|
+ await setExternalClientStatus(row.id, status);
|
|
|
+ this.$message.success(
|
|
|
+ status === 0
|
|
|
+ ? '客户端已禁用,已有 Token 已同步吊销'
|
|
|
+ : '客户端已启用'
|
|
|
+ );
|
|
|
+ this.reload();
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || '状态更新失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async rotateSecret(row) {
|
|
|
+ try {
|
|
|
+ const result = await rotateExternalClientSecret(row.id);
|
|
|
+ this.$message.success('Secret 轮换成功,旧凭证已失效');
|
|
|
+ this.clientSecret = result && result.clientSecret;
|
|
|
+ if (this.clientSecret) {
|
|
|
+ this.secretVisible = true;
|
|
|
+ }
|
|
|
+ this.reload();
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || 'Secret 轮换失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async revokeToken(row) {
|
|
|
+ try {
|
|
|
+ await revokeExternalClientToken(row.id);
|
|
|
+ this.$message.success('客户端 Token 已全部吊销');
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || 'Token 吊销失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ closeSecret() {
|
|
|
+ this.secretVisible = false;
|
|
|
+ this.clientSecret = '';
|
|
|
+ },
|
|
|
+ onCopy() {
|
|
|
+ this.$message.success('Secret 已复制');
|
|
|
+ },
|
|
|
+ onCopyError() {
|
|
|
+ this.$message.error('Secret 复制失败,请手动复制');
|
|
|
+ },
|
|
|
+ onCopyClientId() {
|
|
|
+ this.$message.success('API Key 已复制');
|
|
|
+ },
|
|
|
+ onCopyClientIdError() {
|
|
|
+ this.$message.error('API Key 复制失败,请手动复制');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+ :deep(.ele-action) {
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ele-action:last-child) {
|
|
|
+ margin-right: 0;
|
|
|
+ }
|
|
|
+</style>
|