4.16更新

This commit is contained in:
hrx 2026-04-16 17:10:14 +08:00
parent ca27783bb2
commit f84af8856e
18 changed files with 1149 additions and 12 deletions

10
src/apis/apiKey.ts Normal file
View File

@ -0,0 +1,10 @@
import { get, post } from '@/utlis/request';
// 获取当前账号下可查看的 API Key 应用列表
export const getApiKeyList = <T>(): Promise<T> => get('/dapi/downapps.dspy');
// 申请新的 API Key
export const applyApiKey = <T, D>(data: D): Promise<T> => post('/dapi/apply_apikey.dspy', data);
// 根据应用 id 查看对应的 API Key 详情
export const getApiKeyDetail = <T, D>(id:D ): Promise<T> => get('/dapi/get_apikey.dspy', id);

View File

@ -54,6 +54,12 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe617;</span>
<div class="name">apikey</div>
<div class="code-name">&amp;#xe617;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe654;</span>
<div class="name">消费记录</div>
@ -180,9 +186,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1775638325739') format('woff2'),
url('iconfont.woff?t=1775638325739') format('woff'),
url('iconfont.ttf?t=1775638325739') format('truetype');
src: url('iconfont.woff2?t=1776308634739') format('woff2'),
url('iconfont.woff?t=1776308634739') format('woff'),
url('iconfont.ttf?t=1776308634739') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -208,6 +214,15 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-apikey"></span>
<div class="name">
apikey
</div>
<div class="code-name">.icon-apikey
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-xiaofeijilu"></span>
<div class="name">
@ -397,6 +412,14 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-apikey"></use>
</svg>
<div class="name">apikey</div>
<div class="code-name">#icon-apikey</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-xiaofeijilu"></use>

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 5136943 */
src: url('iconfont.woff2?t=1775638325739') format('woff2'),
url('iconfont.woff?t=1775638325739') format('woff'),
url('iconfont.ttf?t=1775638325739') format('truetype');
src: url('iconfont.woff2?t=1776308634739') format('woff2'),
url('iconfont.woff?t=1776308634739') format('woff'),
url('iconfont.ttf?t=1776308634739') format('truetype');
}
.iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-apikey:before {
content: "\e617";
}
.icon-xiaofeijilu:before {
content: "\e654";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "44608574",
"name": "apikey",
"font_class": "apikey",
"unicode": "e617",
"unicode_decimal": 58903
},
{
"icon_id": "1020316",
"name": "消费记录",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,315 @@
.apikey-page {
height: 100%;
padding-bottom: 40px;
box-sizing: border-box;
}
.apikey-card {
height: 100%;
background: #1e2024;
border: 1px solid #2a2d32;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.apikey-card :deep(.el-card__body) {
padding: 28px 32px;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-header {
display: flex;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.page-header .header-main .title-row {
display: flex;
align-items: center;
gap: 12px;
font-size: 24px;
font-weight: 700;
color: #fff;
}
.page-header .header-main .title-row .iconfont {
font-size: 26px;
color: #00c4b3;
}
.page-header .header-main .header-desc {
margin: 10px 0 0;
color: rgba(255, 255, 255, 0.65);
font-size: 14px;
line-height: 1.6;
}
.page-header .header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .header-actions .refresh-btn {
background: #25282e;
border-color: #3a3e45;
color: rgba(255, 255, 255, 0.88);
}
.guide-card {
margin-bottom: 24px;
padding: 24px 28px;
border-radius: 14px;
background: #25282e;
border: 1px solid #32363d;
}
.guide-section + .guide-section {
margin-top: 24px;
}
.guide-title {
margin-bottom: 14px;
font-size: 18px;
font-weight: 700;
color: #ffffff;
}
.guide-list {
margin: 0;
padding-left: 24px;
color: rgba(255, 255, 255, 0.86);
line-height: 1.9;
font-size: 15px;
}
.guide-list li + li {
margin-top: 4px;
}
.login-require,
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.login-require-content,
.empty-state {
flex-direction: column;
text-align: center;
}
.login-require-content img,
.empty-state img {
width: 220px;
height: 220px;
object-fit: contain;
opacity: 0.55;
}
.login-require-content {
display: flex;
gap: 12px;
}
.login-title,
.empty-title {
font-size: 20px;
color: #fff;
font-weight: 600;
}
.login-desc,
.empty-desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.7;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
padding: 20px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(44, 255, 248, 0.08), rgba(44, 104, 255, 0.08));
border: 1px solid rgba(255, 255, 255, 0.08);
}
.stat-card .stat-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12px;
}
.stat-card .stat-value {
font-size: 28px;
color: #fff;
font-weight: 700;
}
.stat-card .stat-value.success {
color: #3fe0b5;
}
.stat-card .stat-value.warning {
color: #f5b04c;
}
.toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.toolbar .toolbar-left {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.toolbar .toolbar-right {
display: flex;
align-items: center;
}
.toolbar .toolbar-tip {
font-size: 13px;
color: rgba(255, 255, 255, 0.45);
}
.search-input {
width: 320px;
}
.table-wrapper {
flex: 1;
overflow: auto;
border-radius: 12px;
}
.apikey-table {
width: 100%;
background: transparent;
}
.apikey-table :deep(.el-table__header-wrapper) .el-table__header th {
background-color: #1e2024;
color: rgba(255, 255, 255, 0.75);
border-bottom: 1px solid #2a2d32;
font-weight: 600;
}
.apikey-table :deep(.el-table__body-wrapper) .el-table__body tr {
background-color: #1e2024;
transition: background 0.2s;
}
.apikey-table :deep(.el-table__body-wrapper) .el-table__body tr:hover td {
background-color: #25282e !important;
}
.apikey-table :deep(.el-table__body-wrapper) .el-table__body tr td {
border-bottom: 1px solid #2a2d32;
color: rgba(255, 255, 255, 0.88);
}
.apikey-table :deep(.el-table__row--striped) td {
background-color: #1a1c20 !important;
}
.apikey-table .key-mask {
font-family: Consolas, 'Courier New', monospace;
font-size: 13px;
color: #87f5eb;
background: #14161a;
padding: 4px 8px;
border-radius: 6px;
letter-spacing: 0.3px;
}
.view-key-content .view-key-name,
.view-key-content .view-key-value {
margin-bottom: 20px;
}
.view-key-content .view-key-name .label,
.view-key-content .view-key-value .label {
font-weight: 600;
color: rgba(255, 255, 255, 0.75);
display: inline-block;
width: 90px;
}
.view-key-content .view-key-name .value,
.view-key-content .view-key-value .value {
color: #fff;
}
.view-key-content .key-box {
margin-top: 8px;
padding: 14px;
background: #14161a;
border-radius: 12px;
border: 1px solid #2a2d32;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.view-key-content .key-box code {
font-family: Consolas, 'Courier New', monospace;
color: #87f5eb;
font-size: 14px;
word-break: break-all;
flex: 1;
}
.view-key-content .view-key-tip {
margin-top: 16px;
padding: 10px 12px;
background: rgba(245, 176, 76, 0.12);
border-radius: 8px;
color: #f5b04c;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
}
.view-key-content .view-key-tip i {
font-size: 16px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.full-width {
width: 100%;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner),
:deep(.el-select__wrapper) {
background-color: #25282e !important;
border: 1px solid #3a3e45 !important;
box-shadow: none !important;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
color: rgba(255, 255, 255, 0.92) !important;
}
:deep(.el-radio) {
margin-right: 20px;
color: rgba(255, 255, 255, 0.85);
}
:deep(.el-dialog) {
background: #1e2024;
border: 1px solid #2a2d32;
border-radius: 16px;
}
:deep(.el-dialog__title),
:deep(.el-form-item__label) {
color: #fff !important;
}
:deep(.el-button--primary.is-link) {
color: #3fe0b5 !important;
}
:deep(.el-button--danger.is-link) {
color: #f56c6c !important;
}
:deep(.el-table__inner-wrapper) {
border: 0;
}
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.apikey-card :deep(.el-card__body) {
padding: 20px;
}
.stats-grid {
grid-template-columns: 1fr;
}
.toolbar {
flex-direction: column;
}
.search-input {
width: 100%;
}
.table-wrapper {
overflow-x: auto;
}
}

View File

@ -0,0 +1,384 @@
.apikey-page {
height: 100%;
padding-bottom: 40px;
box-sizing: border-box;
}
.apikey-card {
height: 100%;
background: #1e2024;
border: 1px solid #2a2d32;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
:deep(.el-card__body) {
padding: 28px 32px;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
.page-header {
display: flex;
justify-content: space-between;
gap: 24px;
margin-bottom: 24px;
flex-wrap: wrap;
.header-main {
.title-row {
display: flex;
align-items: center;
gap: 12px;
font-size: 24px;
font-weight: 700;
color: #fff;
.iconfont {
font-size: 26px;
color: #00c4b3;
}
}
.header-desc {
margin: 10px 0 0;
color: rgba(255, 255, 255, 0.65);
font-size: 14px;
line-height: 1.6;
}
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
.refresh-btn {
background: #25282e;
border-color: #3a3e45;
color: rgba(255, 255, 255, 0.88);
}
}
}
.guide-card {
margin-bottom: 24px;
padding: 24px 28px;
border-radius: 14px;
background: #25282e;
border: 1px solid #32363d;
}
.guide-section+.guide-section {
margin-top: 24px;
}
.guide-title {
margin-bottom: 14px;
font-size: 18px;
font-weight: 700;
color: #ffffff;
}
.guide-list {
margin: 0;
padding-left: 24px;
color: rgba(255, 255, 255, 0.86);
line-height: 1.9;
font-size: 15px;
li+li {
margin-top: 4px;
}
}
.login-require,
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.login-require-content,
.empty-state {
flex-direction: column;
text-align: center;
img {
width: 220px;
height: 220px;
object-fit: contain;
opacity: 0.55;
}
}
.login-require-content {
display: flex;
gap: 12px;
}
.login-title,
.empty-title {
font-size: 20px;
color: #fff;
font-weight: 600;
}
.login-desc,
.empty-desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
line-height: 1.7;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
padding: 20px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(44, 255, 248, 0.08), rgba(44, 104, 255, 0.08));
border: 1px solid rgba(255, 255, 255, 0.08);
.stat-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12px;
}
.stat-value {
font-size: 28px;
color: #fff;
font-weight: 700;
&.success {
color: #3fe0b5;
}
&.warning {
color: #f5b04c;
}
}
}
.toolbar {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
.toolbar-left {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.toolbar-right {
display: flex;
align-items: center;
}
.toolbar-tip {
font-size: 13px;
color: rgba(255, 255, 255, 0.45);
}
}
.search-input {
width: 320px;
}
.table-wrapper {
flex: 1;
overflow: auto;
border-radius: 12px;
}
.apikey-table {
width: 100%;
background: transparent;
:deep(.el-table__header-wrapper) {
.el-table__header th {
background-color: #1e2024;
color: rgba(255, 255, 255, 0.75);
border-bottom: 1px solid #2a2d32;
font-weight: 600;
}
}
:deep(.el-table__body-wrapper) {
.el-table__body tr {
background-color: #1e2024;
transition: background 0.2s;
&:hover td {
background-color: #25282e !important;
}
td {
border-bottom: 1px solid #2a2d32;
color: rgba(255, 255, 255, 0.88);
}
}
}
:deep(.el-table__row--striped) {
td {
background-color: #1a1c20 !important;
}
}
.key-mask {
font-family: Consolas, 'Courier New', monospace;
font-size: 13px;
color: #87f5eb;
background: #14161a;
padding: 4px 8px;
border-radius: 6px;
letter-spacing: 0.3px;
}
}
.view-key-content {
.view-key-name,
.view-key-value {
margin-bottom: 20px;
.label {
font-weight: 600;
color: rgba(255, 255, 255, 0.75);
display: inline-block;
width: 90px;
}
.value {
color: #fff;
}
}
.key-box {
margin-top: 8px;
padding: 14px;
background: #14161a;
border-radius: 12px;
border: 1px solid #2a2d32;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
code {
font-family: Consolas, 'Courier New', monospace;
color: #87f5eb;
font-size: 14px;
word-break: break-all;
flex: 1;
}
}
.view-key-tip {
margin-top: 16px;
padding: 10px 12px;
background: rgba(245, 176, 76, 0.12);
border-radius: 8px;
color: #f5b04c;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
i {
font-size: 16px;
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.full-width {
width: 100%;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner),
:deep(.el-select__wrapper) {
background-color: #25282e !important;
border: 1px solid #3a3e45 !important;
box-shadow: none !important;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
color: rgba(255, 255, 255, 0.92) !important;
}
:deep(.el-radio) {
margin-right: 20px;
color: rgba(255, 255, 255, 0.85);
}
:deep(.el-dialog) {
background: #1e2024;
border: 1px solid #2a2d32;
border-radius: 16px;
}
:deep(.el-dialog__title),
:deep(.el-form-item__label) {
color: #fff !important;
}
:deep(.el-button--primary.is-link) {
color: #3fe0b5 !important;
}
:deep(.el-button--danger.is-link) {
color: #f56c6c !important;
}
:deep(.el-table__inner-wrapper) {
border: 0;
}
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.apikey-card :deep(.el-card__body) {
padding: 20px;
}
.stats-grid {
grid-template-columns: 1fr;
}
.toolbar {
flex-direction: column;
}
.search-input {
width: 100%;
}
.table-wrapper {
overflow-x: auto;
}
}

View File

@ -75,6 +75,25 @@
</el-menu-item>
</el-menu>
</div>
<!-- 我的分组 -->
<div class="tool-navigation mb-8">
<div class="menu-title">我的</div>
<el-menu
:default-active="activeIndex"
class="custom-menu"
background-color="transparent"
text-color="#fff"
active-text-color="#00FFCC"
@select="handleSelect"
>
<el-menu-item index="apikey">
<div class="menu-item-content">
<i class="iconfont icon-apikey"></i>
<span class="menu-text">令牌管理</span>
</div>
</el-menu-item>
</el-menu>
</div>
<!-- 底部占位空白 -->
<div class="pb-20"></div>
@ -126,6 +145,9 @@ const handleSelect = (index: string) => {
case 'material-square':
router.push('/material-square');
break;
case 'apikey':
router.push('/apikey');
break;
default:
break;
}
@ -152,7 +174,10 @@ const updateActiveIndex = () => {
activeIndex.value = 'digital-human';
} else if (path.includes('/material-square')) {
activeIndex.value = 'material-square';
} else {
} else if (path.includes('/apikey')) {
activeIndex.value = 'apikey';
}
else {
activeIndex.value = 'index';
}
}

View File

@ -83,8 +83,13 @@ const router = createRouter({
path: '/material-square',
name: 'material-square',
component: () => import('@/views/videoGenerate/videoGenerate.vue')
},
// apikey
{
path: '/apikey',
name: 'apikey',
component: () => import('@/views/APiKey/apikey.vue')
}
]
},

32
src/types/apiKey.ts Normal file
View File

@ -0,0 +1,32 @@
export interface ListApikeys {
id: string;
name: null;
apikeyid: string;
};
export interface ListResult {
apikeys: Array< ListApikeys >;
};
export interface AddParams {
appname: string;
description: string;
}
export interface AddResult{
status: string;
}
interface DetailData {
id: string;
name: null;
description: null;
secretkey: string;
allowedips: null;
orgid: string;
apikey: string;
};
export interface DetailResult {
status: string;
data: DetailData;
};

View File

@ -22,11 +22,11 @@ const shouldIncludeCredentials = (url?: string, baseURL?: string) => {
axios.interceptors.request.use(
function (config) {
// console.log('=== 请求拦截器 ===')
console.log('请求 URL:', config)
// console.log('请求 URL:', config)
// console.log('请求方法:', config.method)
// console.log('请求参数 (params):', config.params)
// console.log('请求数据 (data):', config.data)
console.log('请求头:', config.headers)
// console.log('请求头:', config.headers)
config.baseURL = apiBaseURL
config.withCredentials = shouldIncludeCredentials(config.url, config.baseURL)
return config

332
src/views/APiKey/apikey.vue Normal file
View File

@ -0,0 +1,332 @@
<template>
<div class="apikey-page">
<el-card class="apikey-card">
<div class="page-header">
<div class="header-main">
<div class="title-row">
<i class="iconfont icon-apikey"></i>
<span>API Key 管理</span>
</div>
<p class="header-desc">创建和管理访问令牌用于在外部系统中安全调用你的能力接口</p>
</div>
<div class="header-actions">
<el-button class="refresh-btn" :loading="loading" @click="loadKeys">刷新</el-button>
<el-button type="primary" @click="openCreateDialog">创建新令牌</el-button>
</div>
</div>
<div class="guide-card">
<div class="guide-section">
<div class="guide-title">API 密钥安全指南</div>
<ul class="guide-list">
<li>请不要与他人共享您的 API Keys暴露 API Key 可能会破坏您的帐户安全和访问的服务</li>
<li>避免将 API Key 暴露在浏览器或其他客户端代码中</li>
<li>为了保护您的帐户安全一旦 API Keys 被发现泄露平台发现后可能会将其禁用</li>
</ul>
</div>
</div>
<div v-if="!isLoggedIn" class="login-require">
<div class="login-require-content">
<img src="../../assets/images/preparation.png" alt="">
<div class="login-title">请先登录后管理 API Key</div>
<div class="login-desc">登录后可创建复制查看和删除自己的访问令牌</div>
<el-button type="primary" @click="handleLogin">去登录</el-button>
</div>
</div>
<template v-else>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">令牌总数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
<div class="stat-card">
<div class="stat-label">启用中</div>
<div class="stat-value success">{{ stats.active }}</div>
</div>
</div>
<div class="table-wrapper">
<el-table :data="keys" class="apikey-table" stripe v-loading="loading">
<el-table-column prop="name" label="令牌名称" min-width="150" />
<!-- <el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip /> -->
<el-table-column label="id" min-width="220">
<template #default="{ row }">
<code class="key-mask">{{ maskKey(row.key || '') }}</code>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openViewDialog(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-card>
<!-- 创建 API Key 对话框 -->
<el-dialog v-model="createDialogVisible" title="创建 API Key" width="560px" destroy-on-close
class="create-key-dialog">
<el-form ref="formRef" :model="createForm" :rules="formRules" label-position="top">
<el-form-item label="令牌名称" prop="appname">
<el-input v-model="createForm.appname" placeholder="例如:前端开发环境 / 自动化脚本" maxlength="30" show-word-limit />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="createForm.description" type="textarea" :rows="3" maxlength="100" show-word-limit
placeholder="填写使用场景,方便后续区分" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="createLoading" @click="createKey">确认创建</el-button>
</div>
</template>
</el-dialog>
<!-- 查看完整 Key 对话框 -->
<el-dialog v-model="viewDialogVisible" title="API Key 详情" width="560px" class="view-key-dialog" @close="handleViewDialogClose">
<div v-loading="detailLoading" class="dialog-content-wrapper">
<div class="view-key-content">
<div class="view-key-name">
<span class="label">令牌名称</span>
<span class="value">{{ viewingKey?.name || '未命名' }}</span>
</div>
<div class="view-key-value">
<span class="label">完整 Key</span>
<div class="key-box">
<code>{{ viewingKey?.key }}</code>
<el-button link type="primary" size="small" @click="copyViewingKey">
复制
</el-button>
</div>
</div>
<div class="view-key-tip">
<i class="el-icon-info"></i>
请妥善保管该密钥 切勿泄露
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang='ts'>
import { computed, reactive, ref, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useRouter } from 'vue-router';
import { useLoginStore } from '@/store/LoginStore';
import { applyApiKey, getApiKeyList, getApiKeyDetail } from '@/apis/apiKey';
import type { AddParams, AddResult, DetailResult, ListApikeys } from '@/types/apiKey';
const router = useRouter();
const loginStore = useLoginStore();
const isLoggedIn = computed(() => loginStore.isLoggedIn);
//
const keys = ref<ListApikeys[]>([]);
const loading = ref(false);
//
const stats = computed(() => {
const total = keys.value.length;
const active = keys.value.length; //
return { total, active };
});
//
const createDialogVisible = ref(false);
const createLoading = ref(false);
const formRef = ref<FormInstance>();
const createForm = reactive<AddParams>({
appname: '',
description: '',
});
const formRules: FormRules = {
appname: [
{ required: true, message: '请输入令牌名称', trigger: 'blur' },
{ max: 30, message: '名称不能超过30个字符', trigger: 'blur' }
]
};
//
const viewDialogVisible = ref(false);
const viewingKey = ref<any>(null);
const detailLoading = ref(false);
// API Key
const maskKey = (key: string) => {
if (!key || key.length <= 8) return '****';
const start = key.slice(0, 6);
const end = key.slice(-4);
return `${start}****${end}`;
};
const copyToClipboard = async (text: string, successMsg = '复制成功') => {
try {
await navigator.clipboard.writeText(text);
ElMessage.success(successMsg);
} catch (err) {
ElMessage.error('复制失败,请手动复制');
}
};
//
const loadKeys = async () => {
if (!isLoggedIn.value) return;
loading.value = true;
try {
const res = await getApiKeyList<any>();
// { status: 'ok', data: { apikeys: [...] } }
const apikeys = res.data?.apikeys || res.apikeys || [];
keys.value = apikeys.map((item: any) => ({
id: item.id,
name: item.name || '未命名',
note: item.description || '暂无备注', // description
key: item.apikeyid || '', // key
status: 'active',
createdAt: new Date().toISOString()
}));
} catch (error: any) {
ElMessage.error(error?.message || '获取 API Key 列表失败');
keys.value = [];
} finally {
loading.value = false;
}
};
//
const openCreateDialog = () => {
createForm.appname = '';
createForm.description = '';
formRef.value?.clearValidate();
createDialogVisible.value = true;
};
// API Key
const createKey = async () => {
if (!formRef.value) return;
await formRef.value.validate();
createLoading.value = true;
try {
const res = await applyApiKey<AddResult, AddParams>({
appname: createForm.appname,
description: createForm.description,
});
if (res.status === 'ok') {
createDialogVisible.value = false;
await loadKeys();
ElMessage.success('创建成功,点击查看可复制完整 Key');
}
} catch (error: any) {
ElMessage.error(error?.message || '创建失败');
} finally {
createLoading.value = false;
}
};
// Key
const openViewDialog = async (row: ListApikeys) => {
if (!row.id) {
ElMessage.error('令牌 ID 无效');
return;
}
detailLoading.value = true;
viewDialogVisible.value = false; //
try {
const res = await getApiKeyDetail<DetailResult, { id: string }>({ id: row.id });
if (res.status === 'ok' && res.data) {
// viewingKey apikey
viewingKey.value = {
...row,
key: res.data.apikey || '',
name: res.data.name || row.name || '未命名',
note: res.data.description || '暂无备注',
};
viewDialogVisible.value = true;
} else {
ElMessage.error('获取 API Key 详情失败');
}
} catch (error: any) {
console.error('获取详情异常:', error);
ElMessage.error(error?.message || '网络错误,请稍后重试');
} finally {
detailLoading.value = false;
}
};
// Key
const copyViewingKey = () => {
if (viewingKey.value?.key) {
copyToClipboard(viewingKey.value.key, '完整 API Key 已复制');
} else {
ElMessage.warning('无可复制的 Key');
}
};
//
const handleViewDialogClose = () => {
viewDialogVisible.value = false;
viewingKey.value = null;
};
//
const handleLogin = () => {
router.push('/login');
};
//
watch(
() => loginStore.isLoggedIn,
(loggedIn) => {
if (loggedIn) {
loadKeys();
} else {
keys.value = [];
}
},
{ immediate: true }
);
onMounted(() => {
if (loginStore.isLoggedIn) {
loadKeys();
}
});
</script>
<style scoped lang='less'>
@import url('@/assets/less/apiKey/apikey.less');
</style>
<style lang='less'>
.custom-select-popup {
background-color: #2A2D34 !important;
border-radius: 12px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;
border: none !important;
}
.custom-select-popup .el-select-dropdown__item {
color: #ffffff !important;
background-color: transparent !important;
}
.custom-select-popup .el-select-dropdown__item.selected,
.custom-select-popup .el-select-dropdown__item:hover {
background-color: #3a3d46 !important;
}
.el-input__count-inner{
background-color: #25282e !important;
}
.el-input__count{
background-color: #25282e !important;
}
</style>

View File

@ -9,7 +9,7 @@
loop
playsinline
crossorigin="anonymous"
src="../../assets/images/login.mp4"
src="../../assets/images/idfile.mp4"
></video>
</div>