694 lines
16 KiB
Vue
694 lines
16 KiB
Vue
<template>
|
||
<div class="api-key-page">
|
||
<div class="api-key-shell">
|
||
<div class="page-header">
|
||
<div class="title-block">
|
||
<div class="title-line">
|
||
<span class="key-icon">
|
||
<i class="el-icon-key"></i>
|
||
</span>
|
||
<h2>API Key 管理</h2>
|
||
</div>
|
||
<p>创建和管理访问令牌,用于在外部系统中安全调用你的能力接口。</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button
|
||
size="small"
|
||
class="ghost-btn"
|
||
icon="el-icon-refresh"
|
||
:loading="tableLoading"
|
||
@click="fetchApiKeyList"
|
||
>
|
||
刷新
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
class="create-btn"
|
||
icon="el-icon-plus"
|
||
:loading="createLoading"
|
||
@click="handleCreateApiKey"
|
||
>
|
||
创建新令牌
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="safe-card">
|
||
<div class="safe-title">
|
||
<i class="el-icon-warning-outline"></i>
|
||
<h3>API 密钥安全指南</h3>
|
||
</div>
|
||
<div class="safe-list">
|
||
<div v-for="item in safeTips" :key="item" class="safe-item">
|
||
<span></span>
|
||
<p>{{ item }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<div class="stat-card" v-for="item in statCards" :key="item.label" :class="item.type">
|
||
<div class="stat-icon">
|
||
<i :class="item.icon"></i>
|
||
</div>
|
||
<div>
|
||
<span>{{ item.label }}</span>
|
||
<strong>{{ item.value }}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-card">
|
||
<div class="table-header">
|
||
<div>
|
||
<h3>令牌列表</h3>
|
||
<p>查看已创建的访问令牌,后续可接入创建、查看和禁用接口。</p>
|
||
</div>
|
||
<span>共 {{ tokenList.length }} 条</span>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<el-table
|
||
v-loading="tableLoading"
|
||
:data="tokenList"
|
||
class="api-key-table"
|
||
style="width: 100%"
|
||
>
|
||
<el-table-column prop="name" label="令牌名称" min-width="220">
|
||
<template slot-scope="scope">
|
||
<span class="token-name">{{ scope.row.name || '默认令牌' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="apikey" label="API Key" min-width="260">
|
||
<template slot-scope="scope">
|
||
<span class="masked-key">{{ maskApiKey(scope.row.opc_apikey ) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
|
||
<el-table-column label="操作" width="120" align="center">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" size="small" class="view-btn" @click="copyApiKey(scope.row)">复制</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog
|
||
title="添加令牌"
|
||
:visible.sync="createDialogVisible"
|
||
width="420px"
|
||
custom-class="create-token-dialog"
|
||
append-to-body
|
||
@close="resetCreateForm"
|
||
>
|
||
<el-form
|
||
ref="createFormRef"
|
||
class="create-token-form"
|
||
:model="createForm"
|
||
:rules="createRules"
|
||
label-width="90px"
|
||
@submit.native.prevent
|
||
>
|
||
<el-form-item label="令牌名称" prop="appname">
|
||
<el-input
|
||
v-model.trim="createForm.appname"
|
||
maxlength="64"
|
||
clearable
|
||
placeholder="请输入令牌名称"
|
||
/>
|
||
<p class="create-token-tip">用于标识当前令牌的业务应用,建议填写便于识别的令牌名称</p>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button class="dialog-cancel-btn" @click="createDialogVisible = false">取消</el-button>
|
||
<el-button class="dialog-submit-btn" type="primary" :loading="createLoading" @click="submitCreateApiKey">确定</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { reqApikeyList, reqCreateApikey } from '@/api/model/model'
|
||
|
||
export default {
|
||
name: 'TokenManagement',
|
||
data() {
|
||
return {
|
||
tableLoading: false,
|
||
createLoading: false,
|
||
createDialogVisible: false,
|
||
createForm: {
|
||
appname: ''
|
||
},
|
||
createRules: {
|
||
appname: [
|
||
{ required: true, message: '请输入令牌名称', trigger: ['blur', 'change'] }
|
||
]
|
||
},
|
||
safeTips: [
|
||
'请不要与他人共享您的 API Keys,暴露 API Key 可能会破坏您的帐户安全和访问的服务。',
|
||
'避免将 API Key 暴露在浏览器或其他客户端代码中。',
|
||
'为了保护您的帐户安全,一旦 API Keys 被发现泄露,平台发现后可能会将其禁用。'
|
||
],
|
||
tokenList: []
|
||
}
|
||
},
|
||
computed: {
|
||
statCards() {
|
||
return [
|
||
{ label: '令牌总数', value: this.tokenList.length, icon: 'el-icon-key', type: 'primary' },
|
||
{ label: '启用中', value: this.enabledCount, icon: 'el-icon-success', type: 'success' }
|
||
]
|
||
},
|
||
enabledCount() {
|
||
return this.tokenList.filter(item => this.isEnabled(item)).length
|
||
}
|
||
},
|
||
created() {
|
||
this.fetchApiKeyList()
|
||
},
|
||
methods: {
|
||
getUserId() {
|
||
return sessionStorage.getItem('userId') || localStorage.getItem('userId') || ''
|
||
},
|
||
getUserParams() {
|
||
const userid = this.getUserId()
|
||
if (!userid || userid === 'null') {
|
||
this.$message.warning('请先登录后再管理 API Key')
|
||
return null
|
||
}
|
||
return { userid }
|
||
},
|
||
async fetchApiKeyList() {
|
||
const params = this.getUserParams()
|
||
if (!params) return
|
||
|
||
this.tableLoading = true
|
||
try {
|
||
const res = await reqApikeyList(params)
|
||
if (res && res.status === false) {
|
||
throw new Error(res.msg || '获取 API Key 列表失败')
|
||
}
|
||
this.tokenList = this.normalizeApiKeyList(res)
|
||
} catch (error) {
|
||
this.tokenList = []
|
||
this.$message.error(error && error.message ? error.message : '获取 API Key 列表失败')
|
||
} finally {
|
||
this.tableLoading = false
|
||
}
|
||
},
|
||
handleCreateApiKey() {
|
||
this.createDialogVisible = true
|
||
this.$nextTick(() => {
|
||
if (this.$refs.createFormRef) {
|
||
this.$refs.createFormRef.clearValidate()
|
||
}
|
||
})
|
||
},
|
||
async submitCreateApiKey() {
|
||
const isValid = await new Promise(resolve => {
|
||
if (!this.$refs.createFormRef) {
|
||
resolve(false)
|
||
return
|
||
}
|
||
this.$refs.createFormRef.validate(valid => resolve(valid))
|
||
})
|
||
if (!isValid) return
|
||
const appname = this.createForm.appname
|
||
|
||
const params = this.getUserParams()
|
||
if (!params) return
|
||
|
||
this.createLoading = true
|
||
try {
|
||
const res = await reqCreateApikey({ ...params, appname })
|
||
if (res && res.status === false) {
|
||
throw new Error(res.msg || '创建 API Key 失败')
|
||
}
|
||
this.$message.success('API Key 创建成功')
|
||
this.createDialogVisible = false
|
||
this.resetCreateForm()
|
||
await this.fetchApiKeyList()
|
||
} catch (error) {
|
||
this.$message.error(error && error.message ? error.message : '创建 API Key 失败')
|
||
} finally {
|
||
this.createLoading = false
|
||
}
|
||
},
|
||
resetCreateForm() {
|
||
if (this.$refs.createFormRef) {
|
||
this.$refs.createFormRef.resetFields()
|
||
} else {
|
||
this.createForm.appname = ''
|
||
}
|
||
},
|
||
normalizeApiKeyList(res) {
|
||
const data = res && res.data !== undefined ? res.data : res
|
||
const list = Array.isArray(data)
|
||
? data
|
||
: Array.isArray(data && data.apikeys)
|
||
? data.apikeys
|
||
: Array.isArray(data && data.list)
|
||
? data.list
|
||
: Array.isArray(data && data.apikey_list)
|
||
? data.apikey_list
|
||
: Array.isArray(data && data.data)
|
||
? data.data
|
||
: data && typeof data === 'object'
|
||
? [data]
|
||
: []
|
||
|
||
return list.map((item, index) => this.normalizeApiKeyItem(item, index))
|
||
},
|
||
normalizeApiKeyItem(item, index) {
|
||
if (typeof item === 'string') {
|
||
return {
|
||
id: index + 1,
|
||
name: `API Key ${index + 1}`,
|
||
apikey: item,
|
||
status: 1
|
||
}
|
||
}
|
||
|
||
return {
|
||
...item,
|
||
id: item.id || item.apikeyid || item.apikey_id || index + 1,
|
||
name: item.name || item.api_key_name || item.apikey_name || `API Key ${index + 1}`,
|
||
apikey: item.apikey || item.apikeyid || item.api_key || item.key || item.token || item.id || '-',
|
||
created_at: item.created_at || item.create_time || item.updated_at || '',
|
||
status: item.status !== undefined ? item.status : 1
|
||
}
|
||
},
|
||
isEnabled(row) {
|
||
return Number(row.status) !== 0 && row.status !== 'disabled'
|
||
},
|
||
getStatusText(row) {
|
||
return this.isEnabled(row) ? '启用' : '禁用'
|
||
},
|
||
getStatusType(row) {
|
||
return this.isEnabled(row) ? 'success' : 'info'
|
||
},
|
||
maskApiKey(value) {
|
||
const key = String(value || '').trim()
|
||
if (!key) return '-'
|
||
if (key.length <= 8) return key
|
||
const prefix = key.slice(0, 6)
|
||
const suffix = key.slice(-4)
|
||
return `${prefix}****${suffix}`
|
||
},
|
||
copyApiKey(row) {
|
||
const value = row && (row.opc_apikey || row.apikey)
|
||
if (!value || value === '-') {
|
||
this.$message.warning('暂无可复制的 API Key')
|
||
return
|
||
}
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(value).then(() => {
|
||
this.$message.success('API Key 已复制')
|
||
}).catch(() => {
|
||
this.copyByInput(value)
|
||
})
|
||
return
|
||
}
|
||
|
||
this.copyByInput(value)
|
||
},
|
||
copyByInput(value) {
|
||
const input = document.createElement('input')
|
||
input.value = value
|
||
document.body.appendChild(input)
|
||
input.select()
|
||
document.execCommand('copy')
|
||
document.body.removeChild(input)
|
||
this.$message.success('API Key 已复制')
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.api-key-page {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
background: linear-gradient(180deg, #f3f7ff 0%, #f7f9fc 44%, #ffffff 100%);
|
||
}
|
||
|
||
.api-key-shell {
|
||
position: relative;
|
||
min-height: calc(100vh - 48px);
|
||
padding: 24px;
|
||
color: #1f2d3d;
|
||
background: #ffffff;
|
||
border: 1px solid #edf1f7;
|
||
border-radius: 18px;
|
||
box-shadow: 0 12px 30px rgba(31, 45, 61, 0.06);
|
||
overflow: hidden;
|
||
|
||
&::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: -80px;
|
||
right: -80px;
|
||
width: 220px;
|
||
height: 220px;
|
||
background: radial-gradient(circle, rgba(64, 158, 255, 0.12) 0%, rgba(64, 158, 255, 0) 70%);
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.page-header {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.title-line {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
|
||
h2 {
|
||
margin: 0;
|
||
color: #1f2d3d;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
.key-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 34px;
|
||
height: 34px;
|
||
color: #409eff;
|
||
font-size: 18px;
|
||
background: #eef5ff;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.title-block p {
|
||
margin: 0;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.ghost-btn {
|
||
color: #606266;
|
||
background: #f5f7fa;
|
||
border-color: #e4e7ed;
|
||
border-radius: 10px;
|
||
|
||
&:hover,
|
||
&:focus {
|
||
color: #409eff;
|
||
background: #ecf5ff;
|
||
border-color: #b3d8ff;
|
||
}
|
||
}
|
||
|
||
.create-btn {
|
||
background: #409eff;
|
||
border-color: #409eff;
|
||
border-radius: 10px;
|
||
box-shadow: 0 8px 18px rgba(64, 158, 255, 0.24);
|
||
}
|
||
|
||
.safe-card {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 22px 24px;
|
||
margin-bottom: 20px;
|
||
background: linear-gradient(135deg, #f8fbff 0%, #f2f7ff 100%);
|
||
border: 1px solid #edf1f7;
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.safe-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
|
||
i {
|
||
color: #409eff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
h3 {
|
||
margin: 0;
|
||
color: #1f2d3d;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.safe-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.safe-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
|
||
span {
|
||
width: 5px;
|
||
height: 5px;
|
||
margin-top: 8px;
|
||
background: #409eff;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
p {
|
||
margin: 0;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
}
|
||
|
||
.stat-row {
|
||
width: 100%;
|
||
// display: grid;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
// grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 14px;
|
||
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
width: 48%;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 20px 22px;
|
||
background: #ffffff;
|
||
border: 1px solid #edf1f7;
|
||
border-radius: 14px;
|
||
box-shadow: 0 10px 28px rgba(31, 45, 61, 0.06);
|
||
|
||
.stat-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
color: #409eff;
|
||
font-size: 20px;
|
||
background: #eef5ff;
|
||
border-radius: 14px;
|
||
}
|
||
|
||
&.success .stat-icon {
|
||
color: #67c23a;
|
||
background: #f0f9eb;
|
||
}
|
||
|
||
span {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
|
||
strong {
|
||
color: #303133;
|
||
font-size: 28px;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
.table-card {
|
||
padding: 20px;
|
||
background: #ffffff;
|
||
border: 1px solid #edf1f7;
|
||
border-radius: 16px;
|
||
box-shadow: 0 10px 28px rgba(31, 45, 61, 0.06);
|
||
}
|
||
|
||
.table-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 14px;
|
||
|
||
h3 {
|
||
margin: 0 0 6px;
|
||
color: #1f2d3d;
|
||
font-size: 18px;
|
||
}
|
||
|
||
p {
|
||
margin: 0;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
|
||
span {
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
.table-wrap {
|
||
max-height: 360px;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: #f1f2f4;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: #c0c4cc;
|
||
border-radius: 999px;
|
||
}
|
||
}
|
||
|
||
.api-key-table {
|
||
color: #606266;
|
||
background: transparent;
|
||
|
||
/deep/ .el-table__header th {
|
||
color: #475467;
|
||
background: #f8fbff;
|
||
border-bottom: 1px solid #edf1f7;
|
||
}
|
||
|
||
/deep/ .el-table__row {
|
||
background: #ffffff;
|
||
}
|
||
|
||
/deep/ tr:hover > td {
|
||
background: #f8fbff !important;
|
||
}
|
||
|
||
/deep/ td {
|
||
color: #606266;
|
||
background: #ffffff;
|
||
border-bottom: 1px solid #edf1f7;
|
||
}
|
||
|
||
/deep/ .el-table__empty-block {
|
||
background: #ffffff;
|
||
}
|
||
|
||
/deep/ &::before {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.token-name {
|
||
color: #1f2d3d;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.masked-key,
|
||
.view-btn {
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/deep/ .create-token-dialog {
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.18);
|
||
|
||
.el-dialog__header {
|
||
padding: 18px 22px 12px;
|
||
border-bottom: 1px solid #eef2f7;
|
||
}
|
||
|
||
.el-dialog__title {
|
||
color: #1f2d3d;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.el-dialog__body {
|
||
padding: 18px 22px 8px;
|
||
}
|
||
|
||
.el-dialog__footer {
|
||
padding: 14px 22px 18px;
|
||
}
|
||
}
|
||
|
||
.create-token-form {
|
||
/deep/ .el-form-item__label {
|
||
color: #4b5565;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/deep/ .el-input__inner {
|
||
height: 38px;
|
||
border-radius: 10px;
|
||
border-color: #d8e3f0;
|
||
}
|
||
}
|
||
|
||
.create-token-tip {
|
||
margin: 8px 0 0;
|
||
color: #8a94a6;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.dialog-cancel-btn {
|
||
min-width: 74px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.dialog-submit-btn {
|
||
min-width: 90px;
|
||
border-radius: 8px;
|
||
}
|
||
</style>
|