2026-05-23 16:35:46 +08:00

694 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>