add sku mapping
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
<script setup lang="ts">
|
||||
import { useApiKeyStore, MAX_KEYS_PER_USER } from '@/stores/api-key'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import {
|
||||
PlusOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { ApiKeyRecord } from '@/types/api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const apiKeyStore = useApiKeyStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const apiKeyEnabled = computed(() => userStore.user?.api_key_enabled ?? false)
|
||||
const hasKeys = computed(() => apiKeyStore.keys.length > 0)
|
||||
|
||||
// Create modal
|
||||
const createModalOpen = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({
|
||||
name: '',
|
||||
expires_at: null as string | null,
|
||||
})
|
||||
|
||||
// Plain key modal
|
||||
const plainKeyModalOpen = ref(false)
|
||||
const plainKey = ref('')
|
||||
|
||||
function isExpired(record: ApiKeyRecord): boolean {
|
||||
return !!record.expires_at && dayjs(record.expires_at).isBefore(dayjs())
|
||||
}
|
||||
|
||||
function formatTime(time: string | null): string {
|
||||
if (!time) return '-'
|
||||
return time.replace('T', ' ').substring(0, 19)
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'name', width: 150 },
|
||||
{ title: '前缀', dataIndex: 'key_prefix', width: 120 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 170 },
|
||||
{ title: '过期时间', key: 'expires_at', width: 170 },
|
||||
{ title: '最后使用', dataIndex: 'last_used_at', width: 170 },
|
||||
{ title: '状态', key: 'enabled', width: 100 },
|
||||
{ title: '操作', key: 'action', width: 80 },
|
||||
]
|
||||
|
||||
const readonlyColumns = columns.filter((c) => c.key !== 'enabled' && c.key !== 'action')
|
||||
|
||||
function showCreateModal() {
|
||||
createForm.name = ''
|
||||
createForm.expires_at = null
|
||||
createModalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!createForm.name.trim()) {
|
||||
message.warning('请输入 API Key 名称')
|
||||
return
|
||||
}
|
||||
createLoading.value = true
|
||||
try {
|
||||
const result = await apiKeyStore.createKey({
|
||||
name: createForm.name.trim(),
|
||||
expires_at: createForm.expires_at || undefined,
|
||||
})
|
||||
if (result) {
|
||||
createModalOpen.value = false
|
||||
plainKey.value = result.plain_key
|
||||
plainKeyModalOpen.value = true
|
||||
}
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(record: ApiKeyRecord, checked: boolean) {
|
||||
await apiKeyStore.toggleKey(record.id, checked)
|
||||
}
|
||||
|
||||
async function handleDelete(record: ApiKeyRecord) {
|
||||
await apiKeyStore.deleteKey(record.id)
|
||||
}
|
||||
|
||||
function handleCopyKey() {
|
||||
navigator.clipboard.writeText(plainKey.value).then(() => {
|
||||
message.success('已复制')
|
||||
})
|
||||
}
|
||||
|
||||
function handlePlainKeyModalClose() {
|
||||
plainKey.value = ''
|
||||
plainKeyModalOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
apiKeyStore.fetchMyKeys()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mode 3: api_key_enabled=false 且无 Key -->
|
||||
<a-empty
|
||||
v-if="!apiKeyEnabled && !hasKeys"
|
||||
description="API Key 功能未启用,请联系管理员开启"
|
||||
/>
|
||||
|
||||
<!-- Mode 4: api_key_enabled=false 且有 Key — 警告 + 只读表格 -->
|
||||
<div v-else-if="!apiKeyEnabled && hasKeys">
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
message="API Key 功能已被管理员关闭。您的所有 API Key 当前无法用于认证。已有 Key 不会被删除,功能重新开启后将自动恢复。如需开启,请联系管理员。"
|
||||
/>
|
||||
<a-table
|
||||
:columns="readonlyColumns"
|
||||
:data-source="apiKeyStore.keys"
|
||||
:loading="apiKeyStore.loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:row-class-name="(record: ApiKeyRecord) => isExpired(record) ? 'expired-row' : ''"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'expires_at'">
|
||||
<span v-if="!record.expires_at">永不过期</span>
|
||||
<span v-else>
|
||||
<span :class="{ 'text-red-500': isExpired(record) }">
|
||||
{{ formatTime(record.expires_at) }}
|
||||
</span>
|
||||
<a-tag v-if="isExpired(record)" color="red" class="ml-1">已过期</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'key_prefix'">
|
||||
{{ record.key_prefix }}****
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'created_at'">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'last_used_at'">
|
||||
{{ record.last_used_at ? formatTime(record.last_used_at) : '从未使用' }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- Mode 2: api_key_enabled=true 且无 Key — 引导空状态 -->
|
||||
<div v-else-if="apiKeyEnabled && !hasKeys && !apiKeyStore.loading">
|
||||
<a-empty description="还没有 API Key">
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
生成第一个 API Key
|
||||
</a-button>
|
||||
</a-empty>
|
||||
</div>
|
||||
|
||||
<!-- Mode 1: api_key_enabled=true 且有 Key — 完整面板 -->
|
||||
<div v-else-if="apiKeyEnabled">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<a-tag>{{ apiKeyStore.keyCount }}/{{ MAX_KEYS_PER_USER }}</a-tag>
|
||||
<a-tooltip
|
||||
v-if="!apiKeyStore.canCreate"
|
||||
title="已达上限,请删除不需要的 Key 后再生成"
|
||||
>
|
||||
<a-button type="primary" disabled>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
生成 API Key
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-button
|
||||
v-else
|
||||
type="primary"
|
||||
@click="showCreateModal"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
生成 API Key
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="apiKeyStore.keys"
|
||||
:loading="apiKeyStore.loading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:row-class-name="(record: ApiKeyRecord) => isExpired(record) ? 'expired-row' : ''"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'key_prefix'">
|
||||
{{ record.key_prefix }}****
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'created_at'">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expires_at'">
|
||||
<span v-if="!record.expires_at">永不过期</span>
|
||||
<span v-else>
|
||||
<span :class="{ 'text-red-500': isExpired(record) }">
|
||||
{{ formatTime(record.expires_at) }}
|
||||
</span>
|
||||
<a-tag v-if="isExpired(record)" color="red" class="ml-1">已过期</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'last_used_at'">
|
||||
{{ record.last_used_at ? formatTime(record.last_used_at) : '从未使用' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<a-switch
|
||||
:checked="record.enabled"
|
||||
:disabled="isExpired(record)"
|
||||
size="small"
|
||||
@change="(checked: boolean) => handleToggle(record, checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-popconfirm
|
||||
title="确定要删除此 API Key 吗?"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<a-spin v-if="apiKeyStore.loading && !hasKeys && apiKeyEnabled" class="mt-4" />
|
||||
|
||||
<!-- Create Modal -->
|
||||
<a-modal
|
||||
v-model:open="createModalOpen"
|
||||
title="生成 API Key"
|
||||
:confirm-loading="createLoading"
|
||||
@ok="handleCreate"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="名称" required>
|
||||
<a-input
|
||||
v-model:value="createForm.name"
|
||||
placeholder="例如:数据同步脚本、监控系统"
|
||||
:maxlength="100"
|
||||
@press-enter="handleCreate"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="过期时间">
|
||||
<a-date-picker
|
||||
v-model:value="createForm.expires_at"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
placeholder="留空表示永不过期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- Plain Key Modal -->
|
||||
<a-modal
|
||||
:open="plainKeyModalOpen"
|
||||
title="API Key 已生成"
|
||||
:footer="null"
|
||||
:mask-closable="false"
|
||||
@cancel="handlePlainKeyModalClose"
|
||||
>
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
message="此密钥仅显示一次,关闭后无法再次查看。请妥善保存。"
|
||||
/>
|
||||
<div class="bg-gray-100 p-3 rounded font-mono text-sm break-all select-all mb-3">
|
||||
{{ plainKey }}
|
||||
</div>
|
||||
<a-button type="primary" block @click="handleCopyKey">
|
||||
<template #icon><CopyOutlined /></template>
|
||||
复制 API Key
|
||||
</a-button>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.expired-row) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user