修改:完善api管理界面
This commit is contained in:
parent
e1456f14d6
commit
f93332d259
@ -3,7 +3,7 @@
|
|||||||
* doc.json: /AA/getApiAppList、/AA/createApiApp 等
|
* doc.json: /AA/getApiAppList、/AA/createApiApp 等
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get, post } from './request'
|
import { get, httpDelete, post } from './request'
|
||||||
import { buildQuery } from './request'
|
import { buildQuery } from './request'
|
||||||
import type { PageResult } from './types'
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
@ -37,13 +37,13 @@ export interface ApiAppListResponse {
|
|||||||
* GET /AA/getApiAppList
|
* GET /AA/getApiAppList
|
||||||
* 分页获取 API 应用列表,需鉴权(x-token、x-user-id)
|
* 分页获取 API 应用列表,需鉴权(x-token、x-user-id)
|
||||||
*/
|
*/
|
||||||
export async function getApiAppList(
|
export async function getMyApiAppList(
|
||||||
params: GetApiAppListParams = {},
|
params: GetApiAppListParams = {},
|
||||||
config?: { headers?: Record<string, string> },
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<ApiAppListResponse> {
|
): Promise<ApiAppListResponse> {
|
||||||
const { page = 1, pageSize = 10, keyword, createdAtRange } = params
|
const { page = 1, pageSize = 10, keyword, createdAtRange } = params
|
||||||
const query = buildQuery({ page, pageSize, keyword, createdAtRange })
|
const query = buildQuery({ page, pageSize, keyword, createdAtRange })
|
||||||
return get<ApiAppListResponse>('/AA/getApiAppList', query, config)
|
return get<ApiAppListResponse>('/AA/getMyApiAppList', query, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建 API 应用请求体(appKey/appSecret 由后端生成时可传空) */
|
/** 创建 API 应用请求体(appKey/appSecret 由后端生成时可传空) */
|
||||||
@ -65,9 +65,20 @@ export interface CreateApiAppResponse {
|
|||||||
* POST /AA/createApiApp
|
* POST /AA/createApiApp
|
||||||
* 创建 API 应用,需鉴权。成功后返回新创建的 ApiApp(含 appKey、appSecret)
|
* 创建 API 应用,需鉴权。成功后返回新创建的 ApiApp(含 appKey、appSecret)
|
||||||
*/
|
*/
|
||||||
export async function createApiApp(
|
export async function createMyApiApp(
|
||||||
body: CreateApiAppBody,
|
body: CreateApiAppBody,
|
||||||
config?: { headers?: Record<string, string> },
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<CreateApiAppResponse> {
|
): Promise<CreateApiAppResponse> {
|
||||||
return post<CreateApiAppResponse>('/AA/createApiApp', body, config)
|
return post<CreateApiAppResponse>('/AA/createMyApiApp', body, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /AA/deleteMyApiApp
|
||||||
|
* 删除 API 应用,需鉴权
|
||||||
|
*/
|
||||||
|
export async function deleteMyApiApp(
|
||||||
|
ID: number,
|
||||||
|
config?: { headers?: Record<string, string> },
|
||||||
|
): Promise<CreateApiAppResponse> {
|
||||||
|
return httpDelete<CreateApiAppResponse>('/AA/deleteMyApiApp?ID='+ ID, null, config)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,3 +159,28 @@ export async function put<T = unknown>(
|
|||||||
}
|
}
|
||||||
return res.json() as Promise<T>
|
return res.json() as Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带 x-token 等自定义头的 POST 请求
|
||||||
|
*/
|
||||||
|
export async function httpDelete<T = unknown>(
|
||||||
|
path: string,
|
||||||
|
body?: unknown,
|
||||||
|
config?: RequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
|
const url = new URL(path, BASE_URL || window.location.origin)
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept-Language': i18n.global.locale.value as string,
|
||||||
|
...config?.headers,
|
||||||
|
}
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
||||||
|
}
|
||||||
|
return res.json() as Promise<T>
|
||||||
|
}
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"noData": "No data",
|
"noData": "No data",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"chance": "chance"
|
"chance": "chance",
|
||||||
|
"cancel": "Cancel"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"yesnoTimeSeries": "YES/NO Time Series",
|
"yesnoTimeSeries": "YES/NO Time Series",
|
||||||
@ -148,7 +149,18 @@
|
|||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"keyDefault": "Key #{n}",
|
"keyDefault": "Key #{n}",
|
||||||
"defaultDesc": "API Key"
|
"defaultDesc": "API Key",
|
||||||
|
"newKeyTitle": "Created Successfully",
|
||||||
|
"newKeyHint": "Save now — App Secret is only shown once",
|
||||||
|
"appKeyLabel": "App Key",
|
||||||
|
"appSecretLabel": "App Secret",
|
||||||
|
"copyAppKey": "Copy App Key",
|
||||||
|
"copyAppSecret": "Copy App Secret",
|
||||||
|
"copied": "Copied",
|
||||||
|
"closeBtn": "I've saved it, Close",
|
||||||
|
"deleteConfirm": "Are you sure you want to delete this API Key? This action cannot be undone.",
|
||||||
|
"deleteConfirmTitle": "Confirm Delete",
|
||||||
|
"deleteSuccess": "Deleted successfully"
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "Comments",
|
"comments": "Comments",
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"noData": "データがありません",
|
"noData": "データがありません",
|
||||||
"more": "その他",
|
"more": "その他",
|
||||||
"user": "ユーザー",
|
"user": "ユーザー",
|
||||||
"chance": "確率"
|
"chance": "確率",
|
||||||
|
"cancel": "キャンセル"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"yesnoTimeSeries": "YES/NO 時系列",
|
"yesnoTimeSeries": "YES/NO 時系列",
|
||||||
@ -148,7 +149,18 @@
|
|||||||
"copy": "コピー",
|
"copy": "コピー",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"keyDefault": "Key #{n}",
|
"keyDefault": "Key #{n}",
|
||||||
"defaultDesc": "API Key"
|
"defaultDesc": "API Key",
|
||||||
|
"newKeyTitle": "作成完了",
|
||||||
|
"newKeyHint": "今すぐ保存してください。App Secret はここにのみ表示されます",
|
||||||
|
"appKeyLabel": "App Key",
|
||||||
|
"appSecretLabel": "App Secret",
|
||||||
|
"copyAppKey": "App Key をコピー",
|
||||||
|
"copyAppSecret": "App Secret をコピー",
|
||||||
|
"copied": "コピーしました",
|
||||||
|
"closeBtn": "保存しました、閉じる",
|
||||||
|
"deleteConfirm": "この API Key を削除してもよろしいですか?この操作は元に戻せません。",
|
||||||
|
"deleteConfirmTitle": "削除の確認",
|
||||||
|
"deleteSuccess": "削除しました"
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "コメント",
|
"comments": "コメント",
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"noData": "데이터 없음",
|
"noData": "데이터 없음",
|
||||||
"more": "더보기",
|
"more": "더보기",
|
||||||
"user": "사용자",
|
"user": "사용자",
|
||||||
"chance": "확률"
|
"chance": "확률",
|
||||||
|
"cancel": "취소"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"yesnoTimeSeries": "YES/NO 시계열",
|
"yesnoTimeSeries": "YES/NO 시계열",
|
||||||
@ -148,7 +149,18 @@
|
|||||||
"copy": "복사",
|
"copy": "복사",
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"keyDefault": "Key #{n}",
|
"keyDefault": "Key #{n}",
|
||||||
"defaultDesc": "API Key"
|
"defaultDesc": "API Key",
|
||||||
|
"newKeyTitle": "생성 완료",
|
||||||
|
"newKeyHint": "지금 저장하세요. App Secret은 여기서만 표시됩니다",
|
||||||
|
"appKeyLabel": "App Key",
|
||||||
|
"appSecretLabel": "App Secret",
|
||||||
|
"copyAppKey": "App Key 복사",
|
||||||
|
"copyAppSecret": "App Secret 복사",
|
||||||
|
"copied": "복사됨",
|
||||||
|
"closeBtn": "저장 완료, 닫기",
|
||||||
|
"deleteConfirm": "이 API Key를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||||
|
"deleteConfirmTitle": "삭제 확인",
|
||||||
|
"deleteSuccess": "삭제되었습니다"
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "댓글",
|
"comments": "댓글",
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"noData": "暂无数据",
|
"noData": "暂无数据",
|
||||||
"more": "更多操作",
|
"more": "更多操作",
|
||||||
"user": "用户",
|
"user": "用户",
|
||||||
"chance": "概率"
|
"chance": "概率",
|
||||||
|
"cancel": "取消"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"yesnoTimeSeries": "YES/NO 分时",
|
"yesnoTimeSeries": "YES/NO 分时",
|
||||||
@ -148,7 +149,18 @@
|
|||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"keyDefault": "Key #{n}",
|
"keyDefault": "Key #{n}",
|
||||||
"defaultDesc": "API Key"
|
"defaultDesc": "API Key",
|
||||||
|
"newKeyTitle": "创建成功",
|
||||||
|
"newKeyHint": "请立即保存,AppSecret 仅在此处显示一次",
|
||||||
|
"appKeyLabel": "App Key",
|
||||||
|
"appSecretLabel": "App Secret",
|
||||||
|
"copyAppKey": "复制 App Key",
|
||||||
|
"copyAppSecret": "复制 App Secret",
|
||||||
|
"copied": "已复制",
|
||||||
|
"closeBtn": "我已保存,关闭",
|
||||||
|
"deleteConfirm": "确定要删除该 API Key 吗?此操作不可撤销。",
|
||||||
|
"deleteConfirmTitle": "删除确认",
|
||||||
|
"deleteSuccess": "删除成功"
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "评论",
|
"comments": "评论",
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"noData": "暫無數據",
|
"noData": "暫無數據",
|
||||||
"more": "更多操作",
|
"more": "更多操作",
|
||||||
"user": "用戶",
|
"user": "用戶",
|
||||||
"chance": "機率"
|
"chance": "機率",
|
||||||
|
"cancel": "取消"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"yesnoTimeSeries": "YES/NO 分時",
|
"yesnoTimeSeries": "YES/NO 分時",
|
||||||
@ -148,7 +149,18 @@
|
|||||||
"copy": "複製",
|
"copy": "複製",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"keyDefault": "Key #{n}",
|
"keyDefault": "Key #{n}",
|
||||||
"defaultDesc": "API Key"
|
"defaultDesc": "API Key",
|
||||||
|
"newKeyTitle": "創建成功",
|
||||||
|
"newKeyHint": "請立即保存,AppSecret 僅在此處顯示一次",
|
||||||
|
"appKeyLabel": "App Key",
|
||||||
|
"appSecretLabel": "App Secret",
|
||||||
|
"copyAppKey": "複製 App Key",
|
||||||
|
"copyAppSecret": "複製 App Secret",
|
||||||
|
"copied": "已複製",
|
||||||
|
"closeBtn": "我已保存,關閉",
|
||||||
|
"deleteConfirm": "確定要刪除該 API Key 嗎?此操作不可撤銷。",
|
||||||
|
"deleteConfirmTitle": "刪除確認",
|
||||||
|
"deleteSuccess": "刪除成功"
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "評論",
|
"comments": "評論",
|
||||||
|
|||||||
@ -34,7 +34,14 @@
|
|||||||
>
|
>
|
||||||
{{ t('apiKey.copy') }}
|
{{ t('apiKey.copy') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn action-delete" type="button">{{ t('apiKey.delete') }}</button>
|
<button
|
||||||
|
class="action-btn action-delete"
|
||||||
|
type="button"
|
||||||
|
:disabled="deletingId === item.ID"
|
||||||
|
@click="handleDelete(item)"
|
||||||
|
>
|
||||||
|
{{ deletingId === item.ID ? t('common.loading') : t('apiKey.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<div ref="loadMoreSentinelRef" class="load-more-sentinel" aria-hidden="true"></div>
|
<div ref="loadMoreSentinelRef" class="load-more-sentinel" aria-hidden="true"></div>
|
||||||
@ -46,12 +53,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除确认弹窗 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="confirmTarget != null" class="new-key-overlay" role="dialog" aria-modal="true">
|
||||||
|
<div class="new-key-dialog confirm-dialog">
|
||||||
|
<h2 class="new-key-title confirm-title">{{ t('apiKey.deleteConfirmTitle') }}</h2>
|
||||||
|
<p class="confirm-msg">{{ t('apiKey.deleteConfirm') }}</p>
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<button class="confirm-cancel-btn" type="button" @click="cancelDelete">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="confirm-ok-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="deletingId != null"
|
||||||
|
@click="confirmDelete"
|
||||||
|
>
|
||||||
|
{{ deletingId != null ? t('common.loading') : t('apiKey.delete') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- 创建成功弹窗 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="newKeyVisible" class="new-key-overlay" role="dialog" aria-modal="true">
|
||||||
|
<div class="new-key-dialog">
|
||||||
|
<h2 class="new-key-title">{{ t('apiKey.newKeyTitle') }}</h2>
|
||||||
|
<p class="new-key-hint">{{ t('apiKey.newKeyHint') }}</p>
|
||||||
|
|
||||||
|
<div class="new-key-field">
|
||||||
|
<span class="new-key-label">{{ t('apiKey.appKeyLabel') }}</span>
|
||||||
|
<div class="new-key-value-row">
|
||||||
|
<span class="new-key-value">{{ newKeyData.appKey }}</span>
|
||||||
|
<button
|
||||||
|
class="new-key-copy-btn"
|
||||||
|
:class="{ copied: copiedField === 'appKey' }"
|
||||||
|
type="button"
|
||||||
|
@click="copyNewKey('appKey')"
|
||||||
|
>
|
||||||
|
{{ copiedField === 'appKey' ? t('apiKey.copied') : t('apiKey.copyAppKey') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="new-key-field">
|
||||||
|
<span class="new-key-label">{{ t('apiKey.appSecretLabel') }}</span>
|
||||||
|
<div class="new-key-value-row">
|
||||||
|
<span class="new-key-value new-key-secret">{{ newKeyData.appSecret }}</span>
|
||||||
|
<button
|
||||||
|
class="new-key-copy-btn"
|
||||||
|
:class="{ copied: copiedField === 'appSecret' }"
|
||||||
|
type="button"
|
||||||
|
@click="copyNewKey('appSecret')"
|
||||||
|
>
|
||||||
|
{{ copiedField === 'appSecret' ? t('apiKey.copied') : t('apiKey.copyAppSecret') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="new-key-close-btn" type="button" @click="closeNewKeyDialog">
|
||||||
|
{{ t('apiKey.closeBtn') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { getApiAppList, createApiApp, type ApiApp } from '../api/apiApp'
|
import { getMyApiAppList, createMyApiApp, deleteMyApiApp, type ApiApp } from '../api/apiApp'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
|
|
||||||
@ -63,11 +137,40 @@ const apiKeys = ref<ApiApp[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const creating = ref(false)
|
const creating = ref(false)
|
||||||
|
const deletingId = ref<number | null>(null)
|
||||||
|
const confirmTarget = ref<ApiApp | null>(null)
|
||||||
const loadError = ref('')
|
const loadError = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const loadMoreSentinelRef = ref<HTMLElement | null>(null)
|
const loadMoreSentinelRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 创建成功弹窗状态
|
||||||
|
const newKeyVisible = ref(false)
|
||||||
|
const newKeyData = ref<{ appKey: string; appSecret: string }>({ appKey: '', appSecret: '' })
|
||||||
|
const copiedField = ref<'appKey' | 'appSecret' | null>(null)
|
||||||
|
|
||||||
|
function copyNewKey(field: 'appKey' | 'appSecret') {
|
||||||
|
const val = newKeyData.value[field]
|
||||||
|
if (!val) return
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(val)
|
||||||
|
.then(() => {
|
||||||
|
copiedField.value = field
|
||||||
|
setTimeout(() => {
|
||||||
|
if (copiedField.value === field) copiedField.value = null
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toastStore.show(t('profile.copyFailed'), 'error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNewKeyDialog() {
|
||||||
|
newKeyVisible.value = false
|
||||||
|
newKeyData.value = { appKey: '', appSecret: '' }
|
||||||
|
copiedField.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 12
|
const PAGE_SIZE = 12
|
||||||
|
|
||||||
const noMore = computed(
|
const noMore = computed(
|
||||||
@ -89,7 +192,7 @@ async function fetchList(append: boolean) {
|
|||||||
loadError.value = ''
|
loadError.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getApiAppList(
|
const res = await getMyApiAppList(
|
||||||
{ page: nextPage, pageSize: PAGE_SIZE },
|
{ page: nextPage, pageSize: PAGE_SIZE },
|
||||||
{ headers },
|
{ headers },
|
||||||
)
|
)
|
||||||
@ -129,7 +232,7 @@ async function handleCreate() {
|
|||||||
|
|
||||||
creating.value = true
|
creating.value = true
|
||||||
try {
|
try {
|
||||||
const res = await createApiApp(
|
const res = await createMyApiApp(
|
||||||
{
|
{
|
||||||
appKey: '',
|
appKey: '',
|
||||||
appSecret: '',
|
appSecret: '',
|
||||||
@ -143,7 +246,13 @@ async function handleCreate() {
|
|||||||
toastStore.show(res.msg || t('error.loadFailed'), 'error')
|
toastStore.show(res.msg || t('error.loadFailed'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toastStore.show(t('toast.createKeySuccess'))
|
if (res.data) {
|
||||||
|
newKeyData.value = {
|
||||||
|
appKey: res.data.appKey ?? '',
|
||||||
|
appSecret: res.data.appSecret ?? '',
|
||||||
|
}
|
||||||
|
newKeyVisible.value = true
|
||||||
|
}
|
||||||
await fetchList(false)
|
await fetchList(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toastStore.show(
|
toastStore.show(
|
||||||
@ -155,6 +264,45 @@ async function handleCreate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDelete(item: ApiApp) {
|
||||||
|
if (item.ID == null) return
|
||||||
|
confirmTarget.value = item
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDelete() {
|
||||||
|
confirmTarget.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
const item = confirmTarget.value
|
||||||
|
if (!item || item.ID == null) return
|
||||||
|
|
||||||
|
const headers = userStore.getAuthHeaders()
|
||||||
|
if (!headers) {
|
||||||
|
toastStore.show(t('error.pleaseLogin'), 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deletingId.value = item.ID
|
||||||
|
try {
|
||||||
|
const res = await deleteMyApiApp(item.ID, { headers })
|
||||||
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
|
toastStore.show(res.msg || t('error.loadFailed'), 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toastStore.show(t('apiKey.deleteSuccess'))
|
||||||
|
confirmTarget.value = null
|
||||||
|
await fetchList(false)
|
||||||
|
} catch (e) {
|
||||||
|
toastStore.show(
|
||||||
|
e instanceof Error ? e.message : t('error.loadFailed'),
|
||||||
|
'error',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
deletingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function copyKey(key: string) {
|
function copyKey(key: string) {
|
||||||
if (!key) return
|
if (!key) return
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@ -359,3 +507,178 @@ onUnmounted(removeLoadMoreObserver)
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ── 创建成功弹窗(非 scoped,Teleport 挂载到 body) ── */
|
||||||
|
.new-key-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-dialog {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 24px 20px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-value {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111827;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-secret {
|
||||||
|
color: #5b5bd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-copy-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-copy-btn.copied {
|
||||||
|
background: #dcfce7;
|
||||||
|
border-color: #86efac;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-close-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #5b5bd6;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-key-close-btn:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 删除确认弹窗 ── */
|
||||||
|
.confirm-dialog {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-msg {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #374151;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-cancel-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-ok-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0;
|
||||||
|
background: #dc2626;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-ok-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user