add sku mapping

This commit is contained in:
2026-04-14 13:45:28 +08:00
parent b647680576
commit b1cd4ea0eb
16 changed files with 3176 additions and 0 deletions
+291
View File
@@ -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>