fix test errors

This commit is contained in:
2026-04-15 11:05:50 +08:00
parent 4809ad03e4
commit 51f0315d68
9 changed files with 402 additions and 45 deletions
+19 -14
View File
@@ -20,7 +20,7 @@ const createModalOpen = ref(false)
const createLoading = ref(false) const createLoading = ref(false)
const createForm = reactive({ const createForm = reactive({
name: '', name: '',
expires_at: null as string | null, expires_at: undefined as string | undefined,
}) })
// Plain key modal // Plain key modal
@@ -50,7 +50,7 @@ const readonlyColumns = columns.filter((c) => c.key !== 'enabled' && c.key !== '
function showCreateModal() { function showCreateModal() {
createForm.name = '' createForm.name = ''
createForm.expires_at = null createForm.expires_at = undefined
createModalOpen.value = true createModalOpen.value = true
} }
@@ -84,9 +84,10 @@ async function handleDelete(record: ApiKeyRecord) {
} }
function handleCopyKey() { function handleCopyKey() {
navigator.clipboard.writeText(plainKey.value).then(() => { navigator.clipboard.writeText(plainKey.value).then(
message.success('已复制') () => message.success('已复制'),
}) () => message.error('复制失败,请手动复制'),
)
} }
function handlePlainKeyModalClose() { function handlePlainKeyModalClose() {
@@ -97,6 +98,10 @@ function handlePlainKeyModalClose() {
onMounted(() => { onMounted(() => {
apiKeyStore.fetchMyKeys() apiKeyStore.fetchMyKeys()
}) })
onBeforeUnmount(() => {
plainKey.value = ''
})
</script> </script>
<template> <template>
@@ -120,17 +125,17 @@ onMounted(() => {
:loading="apiKeyStore.loading" :loading="apiKeyStore.loading"
:pagination="false" :pagination="false"
row-key="id" row-key="id"
:row-class-name="(record: ApiKeyRecord) => isExpired(record) ? 'expired-row' : ''" :row-class-name="(record: ApiKeyRecord) => isExpired(record as ApiKeyRecord) ? 'expired-row' : ''"
size="small" size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'expires_at'"> <template v-if="column.key === 'expires_at'">
<span v-if="!record.expires_at">永不过期</span> <span v-if="!record.expires_at">永不过期</span>
<span v-else> <span v-else>
<span :class="{ 'text-red-500': isExpired(record) }"> <span :class="{ 'text-red-500': isExpired(record as ApiKeyRecord) }">
{{ formatTime(record.expires_at) }} {{ formatTime(record.expires_at) }}
</span> </span>
<a-tag v-if="isExpired(record)" color="red" class="ml-1">已过期</a-tag> <a-tag v-if="isExpired(record as ApiKeyRecord)" color="red" class="ml-1">已过期</a-tag>
</span> </span>
</template> </template>
<template v-else-if="column.dataIndex === 'key_prefix'"> <template v-else-if="column.dataIndex === 'key_prefix'">
@@ -185,7 +190,7 @@ onMounted(() => {
:loading="apiKeyStore.loading" :loading="apiKeyStore.loading"
:pagination="false" :pagination="false"
row-key="id" row-key="id"
:row-class-name="(record: ApiKeyRecord) => isExpired(record) ? 'expired-row' : ''" :row-class-name="(record: ApiKeyRecord) => isExpired(record as ApiKeyRecord) ? 'expired-row' : ''"
size="small" size="small"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
@@ -198,10 +203,10 @@ onMounted(() => {
<template v-else-if="column.key === 'expires_at'"> <template v-else-if="column.key === 'expires_at'">
<span v-if="!record.expires_at">永不过期</span> <span v-if="!record.expires_at">永不过期</span>
<span v-else> <span v-else>
<span :class="{ 'text-red-500': isExpired(record) }"> <span :class="{ 'text-red-500': isExpired(record as ApiKeyRecord) }">
{{ formatTime(record.expires_at) }} {{ formatTime(record.expires_at) }}
</span> </span>
<a-tag v-if="isExpired(record)" color="red" class="ml-1">已过期</a-tag> <a-tag v-if="isExpired(record as ApiKeyRecord)" color="red" class="ml-1">已过期</a-tag>
</span> </span>
</template> </template>
<template v-else-if="column.dataIndex === 'last_used_at'"> <template v-else-if="column.dataIndex === 'last_used_at'">
@@ -210,15 +215,15 @@ onMounted(() => {
<template v-else-if="column.key === 'enabled'"> <template v-else-if="column.key === 'enabled'">
<a-switch <a-switch
:checked="record.enabled" :checked="record.enabled"
:disabled="isExpired(record)" :disabled="isExpired(record as ApiKeyRecord)"
size="small" size="small"
@change="(checked: boolean) => handleToggle(record, checked)" @change="(checked: boolean | string | number) => handleToggle(record as ApiKeyRecord, !!checked)"
/> />
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-popconfirm <a-popconfirm
title="确定要删除此 API Key " title="确定要删除此 API Key "
@confirm="handleDelete(record)" @confirm="handleDelete(record as ApiKeyRecord)"
> >
<a-button type="link" size="small" danger> <a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template> <template #icon><DeleteOutlined /></template>
+12 -7
View File
@@ -186,13 +186,18 @@ function handleModeChange(val: unknown) {
} }
// 树事件 // 树事件
function onCheck(checked: (string | number)[], info: { halfCheckedKeys: (string | number)[] }) { function onCheck(
checkedKeys.value = checked.map(String) checked: (string | number)[] | { checked: (string | number)[]; halfChecked: (string | number)[] },
halfCheckedKeys.value = info.halfCheckedKeys.map(String) info: { halfCheckedKeys?: (string | number)[] },
) {
const keys = Array.isArray(checked) ? checked : checked.checked
checkedKeys.value = keys.map(String)
const half = Array.isArray(checked) ? (info.halfCheckedKeys ?? []) : checked.halfChecked
halfCheckedKeys.value = half.map(String)
} }
function onExpand(keys: string[]) { function onExpand(keys: (string | number)[]) {
expandedKeys.value = keys expandedKeys.value = keys.map(String)
autoExpandParent.value = false autoExpandParent.value = false
} }
@@ -233,8 +238,8 @@ function scopesToCheckedKeys(userScopes: ScopeRecord[]): string[] {
} }
// checkedKeys → scopes(智能判断全选/部分选) // checkedKeys → scopes(智能判断全选/部分选)
function checkedKeysToScopes(): { scope_type: string; scope_id: number }[] { function checkedKeysToScopes(): Omit<ScopeRecord, 'name'>[] {
const scopes: { scope_type: string; scope_id: number }[] = [] const scopes: Omit<ScopeRecord, 'name'>[] = []
const keySet = new Set(checkedKeys.value) const keySet = new Set(checkedKeys.value)
for (const key of checkedKeys.value) { for (const key of checkedKeys.value) {
@@ -0,0 +1,323 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import { createPinia, setActivePinia } from 'pinia'
import { message } from 'ant-design-vue'
import ApiKeyPanel from '@/components/ApiKeyPanel.vue'
import { useUserStore } from '@/stores/user'
import { fakeJwtWithRole } from '@/test/helpers'
import type { ApiKeyRecord } from '@/types/api'
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
})
vi.mock('@/utils/request', () => ({
api: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}))
import { api } from '@/utils/request'
const mockWriteText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: mockWriteText },
writable: true,
configurable: true,
})
const mockKeys: ApiKeyRecord[] = [
{
id: 1,
user_id: 1,
name: '测试Key',
key_prefix: 'a1b2c3d4',
last_used_at: null,
expires_at: null,
enabled: true,
created_at: '2026-04-01T10:00:00',
},
{
id: 2,
user_id: 1,
name: '过期Key',
key_prefix: 'e5f6g7h8',
last_used_at: '2026-04-10T08:00:00',
expires_at: '2025-01-01T00:00:00',
enabled: true,
created_at: '2025-12-01T10:00:00',
},
]
function getSetupState(w: ReturnType<typeof mount>) {
// @ts-expect-error setupState is Vue internal, not in public types
return w.getCurrentComponent().setupState
}
describe('ApiKeyPanel', () => {
let wrapper: ReturnType<typeof mount>
beforeEach(() => {
vi.restoreAllMocks()
mockWriteText.mockReset()
mockWriteText.mockResolvedValue(undefined)
document.body.innerHTML = ''
setActivePinia(createPinia())
})
afterEach(() => {
wrapper?.unmount()
document.body.innerHTML = ''
})
function setupUser(apiKeyEnabled: boolean) {
const store = useUserStore()
store.setToken(fakeJwtWithRole('admin'), 'r.p.s', true)
store.setUser({
id: 1,
username: 'testuser',
email: 'test@example.com',
role: 'admin',
status: 1,
api_key_enabled: apiKeyEnabled,
})
}
function setupApi(keys: ApiKeyRecord[] = []) {
vi.mocked(api.get).mockImplementation((url: string) => {
if (url === '/api/v1/me/api-keys') return Promise.resolve(keys) as never
return Promise.resolve([]) as never
})
vi.mocked(api.post).mockResolvedValue({
plain_key: 'dk_test_plain_key_123',
api_key: { ...mockKeys[0], id: 99, name: '新Key' },
} as never)
vi.mocked(api.patch).mockImplementation(() => Promise.resolve(undefined) as never)
vi.mocked(api.delete).mockImplementation(() => Promise.resolve(undefined) as never)
}
async function mountPanel() {
wrapper = mount(ApiKeyPanel, { attachTo: document.body })
await flushPromises()
await nextTick()
return wrapper
}
function queryBody(selector: string) {
return document.body.querySelectorAll(selector)
}
describe('渲染模式', () => {
it('enabled + 有Key → 显示完整面板(表格+数量tag+生成按钮)', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
const html = document.body.innerHTML
expect(html).toContain('2/10')
expect(queryBody('.ant-table').length).toBeGreaterThan(0)
const btns = Array.from(queryBody('.ant-btn-primary'))
expect(btns.some((btn) => btn.textContent?.includes('生成 API Key'))).toBe(true)
})
it('enabled + 无Key → 显示引导空状态', async () => {
setupUser(true)
setupApi([])
await mountPanel()
expect(queryBody('.ant-empty').length).toBeGreaterThan(0)
const btns = Array.from(queryBody('.ant-btn-primary'))
expect(btns.some((b) => b.textContent?.includes('生成第一个 API Key'))).toBe(true)
})
it('disabled + 无Key → 显示未启用提示', async () => {
setupUser(false)
setupApi([])
await mountPanel()
expect(document.body.innerHTML).toContain('API Key 功能未启用,请联系管理员开启')
})
it('disabled + 有Key → 显示警告Alert + 只读表格', async () => {
setupUser(false)
setupApi(mockKeys)
await mountPanel()
expect(queryBody('.ant-alert-warning').length).toBeGreaterThan(0)
expect(queryBody('.ant-table').length).toBeGreaterThan(0)
const btns = Array.from(queryBody('.ant-btn-primary'))
expect(btns.some((btn) => btn.textContent?.includes('生成 API Key'))).toBe(false)
expect(queryBody('.ant-switch').length).toBe(0)
})
})
describe('交互', () => {
it('点击生成按钮 → 打开创建 Modal', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
const btn = Array.from(queryBody('.ant-btn-primary')).find(
(b) => b.textContent?.includes('生成 API Key'),
) as HTMLElement
btn.click()
await flushPromises()
await nextTick()
const footer = document.body.querySelector('.ant-modal-footer')
expect(footer).toBeTruthy()
})
it('提交创建 → 调用 createKey API → 显示明文 Modal', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
const vm = getSetupState(wrapper)
vm.createForm.name = '新Key'
vm.createModalOpen = true
await nextTick()
await flushPromises()
const okBtn = document.body.querySelector(
'.ant-modal-footer .ant-btn-primary',
) as HTMLElement
okBtn?.click()
await flushPromises()
await nextTick()
await flushPromises()
expect(vi.mocked(api.post)).toHaveBeenCalledWith('/api/v1/me/api-keys', {
name: '新Key',
expires_at: undefined,
})
const html = document.body.innerHTML
expect(html).toContain('dk_test_plain_key_123')
expect(html).toContain('此密钥仅显示一次')
})
it('复制按钮 → 调用 clipboard.writeText', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
const vm = getSetupState(wrapper)
vm.plainKey = 'dk_secret_key_abc'
vm.plainKeyModalOpen = true
await nextTick()
await flushPromises()
const copyBtn = Array.from(queryBody('.ant-btn-primary')).find(
(b) => b.textContent?.includes('复制'),
) as HTMLElement
copyBtn?.click()
await flushPromises()
expect(mockWriteText).toHaveBeenCalledWith('dk_secret_key_abc')
})
it('关闭明文 Modal → plainKey 清空', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
const vm = getSetupState(wrapper)
vm.plainKey = 'dk_secret_key'
vm.plainKeyModalOpen = true
await nextTick()
vm.handlePlainKeyModalClose()
await nextTick()
expect(vm.plainKey).toBe('')
expect(vm.plainKeyModalOpen).toBe(false)
})
it('Toggle switch → 调用 toggleKey API', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
expect(queryBody('.ant-switch').length).toBe(2)
await getSetupState(wrapper).handleToggle(mockKeys[0], false)
await flushPromises()
expect(vi.mocked(api.patch)).toHaveBeenCalledWith(
'/api/v1/me/api-keys/1/toggle',
{ enabled: false },
)
})
it('删除 → 调用 deleteKey API', async () => {
setupUser(true)
setupApi(mockKeys)
const successSpy = vi.spyOn(message, 'success')
await mountPanel()
await getSetupState(wrapper).handleDelete(mockKeys[0])
await flushPromises()
expect(vi.mocked(api.delete)).toHaveBeenCalledWith('/api/v1/me/api-keys/1')
expect(successSpy).toHaveBeenCalledWith('已删除')
})
})
describe('边界测试', () => {
it('达到10个上限 → 生成按钮禁用', async () => {
setupUser(true)
const tenKeys: ApiKeyRecord[] = Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
user_id: 1,
name: `Key ${i + 1}`,
key_prefix: `prefix${String(i).padStart(2, '0')}`,
last_used_at: null,
expires_at: null,
enabled: true,
created_at: '2026-04-01T10:00:00',
}))
setupApi(tenKeys)
await mountPanel()
expect(document.body.innerHTML).toContain('10/10')
const disabledBtn = document.body.querySelector('.ant-btn-primary[disabled]')
expect(disabledBtn).toBeTruthy()
})
it('过期Key → 行变灰 + 已过期标签 + Switch 禁用', async () => {
setupUser(true)
setupApi(mockKeys)
await mountPanel()
expect(queryBody('.expired-row').length).toBeGreaterThanOrEqual(1)
expect(document.body.innerHTML).toContain('已过期')
const switches = Array.from(queryBody('.ant-switch'))
const disabledSwitches = switches.filter((s) =>
s.classList.contains('ant-switch-disabled'),
)
expect(disabledSwitches.length).toBeGreaterThanOrEqual(1)
})
it('onMounted → 调用 fetchMyKeys', async () => {
setupUser(true)
setupApi([])
await mountPanel()
expect(vi.mocked(api.get)).toHaveBeenCalledWith('/api/v1/me/api-keys')
})
})
})
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, vi } from 'vitest' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia' import { setActivePinia, createPinia } from 'pinia'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router' import { createRouter, createMemoryHistory } from 'vue-router'
@@ -29,17 +29,28 @@ function createTestRouter() {
} }
describe('MainLayout', () => { describe('MainLayout', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let wrapper: any
beforeEach(() => { beforeEach(() => {
vi.restoreAllMocks()
document.body.innerHTML = ''
setActivePinia(createPinia()) setActivePinia(createPinia())
localStorage.clear() localStorage.clear()
}) })
afterEach(() => {
wrapper?.unmount()
wrapper = undefined
document.body.innerHTML = ''
})
async function mountLayout(route = '/') { async function mountLayout(route = '/') {
const router = createTestRouter() const router = createTestRouter()
await router.push(route) await router.push(route)
await router.isReady() await router.isReady()
return mount(MainLayout, { const w = mount(MainLayout, {
global: { global: {
plugins: [router], plugins: [router],
stubs: { stubs: {
@@ -65,6 +76,8 @@ describe('MainLayout', () => {
default: '<div class="test-content">Page Content</div>', default: '<div class="test-content">Page Content</div>',
}, },
}) })
wrapper = w
return w
} }
it('renders all four layout areas', async () => { it('renders all four layout areas', async () => {
@@ -26,6 +26,8 @@ vi.mock('@/utils/request', () => ({
get: vi.fn(), get: vi.fn(),
post: vi.fn(), post: vi.fn(),
put: vi.fn(), put: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
}, },
})) }))
@@ -56,7 +58,10 @@ describe('Profile Page', () => {
async function mountPage() { async function mountPage() {
const { api } = await import('@/utils/request') const { api } = await import('@/utils/request')
vi.mocked(api.get).mockResolvedValue(mockUser) vi.mocked(api.get).mockImplementation((url: string) => {
if (url === '/api/v1/me/api-keys') return Promise.resolve([]) as never
return Promise.resolve(mockUser) as never
})
const store = useUserStore() const store = useUserStore()
store.setToken(fakeJwtWithRole('administrator'), 'r.p.s', true) store.setToken(fakeJwtWithRole('administrator'), 'r.p.s', true)
+5
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { ApiError } from '@/types/api' import { ApiError } from '@/types/api'
import ApiKeyPanel from '@/components/ApiKeyPanel.vue'
const userStore = useUserStore() const userStore = useUserStore()
@@ -95,4 +96,8 @@ const handleSubmit = async () => {
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-card> </a-card>
<a-card title="API Key 管理" class="mt-4">
<ApiKeyPanel />
</a-card>
</template> </template>
+12 -11
View File
@@ -328,7 +328,8 @@ watch(
</a-form-item> </a-form-item>
<a-form-item label="状态"> <a-form-item label="状态">
<a-select <a-select
v-model:value="store.filters.enabled" :value="(store.filters.enabled as any)"
@update:value="(v: any) => { store.filters.enabled = v }"
placeholder="全部" placeholder="全部"
allow-clear allow-clear
style="width: 100px" style="width: 100px"
@@ -393,11 +394,11 @@ watch(
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-space> <a-space>
<a-button type="link" size="small" @click="openEdit(record)"> <a-button type="link" size="small" @click="openEdit(record as any)">
<template #icon><EditOutlined /></template> <template #icon><EditOutlined /></template>
编辑 编辑
</a-button> </a-button>
<a-button type="link" danger size="small" @click="handleDelete(record)"> <a-button type="link" danger size="small" @click="handleDelete(record as any)">
<template #icon><DeleteOutlined /></template> <template #icon><DeleteOutlined /></template>
删除 删除
</a-button> </a-button>
@@ -443,8 +444,8 @@ watch(
" "
show-search show-search
:filter-option=" :filter-option="
(input: string, option: { label: string }) => (input: string, option: { label?: string } | undefined) =>
option.label.toLowerCase().includes(input.toLowerCase()) (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
" "
/> />
</a-form-item> </a-form-item>
@@ -462,8 +463,8 @@ watch(
" "
show-search show-search
:filter-option=" :filter-option="
(input: string, option: { label: string }) => (input: string, option: { label?: string } | undefined) =>
option.label.toLowerCase().includes(input.toLowerCase()) (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
" "
/> />
</a-form-item> </a-form-item>
@@ -480,8 +481,8 @@ watch(
allow-clear allow-clear
show-search show-search
:filter-option=" :filter-option="
(input: string, option: { label: string }) => (input: string, option: { label?: string } | undefined) =>
option.label.toLowerCase().includes(input.toLowerCase()) (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
" "
/> />
</a-form-item> </a-form-item>
@@ -500,9 +501,9 @@ watch(
:not-found-content="skuSearching ? '搜索中...' : '无匹配结果'" :not-found-content="skuSearching ? '搜索中...' : '无匹配结果'"
@search="handleSkuSearch" @search="handleSkuSearch"
@change=" @change="
(val: number | undefined) => { (val: unknown) => {
const found = skuSearchOptions.find((s) => s.value === val) const found = skuSearchOptions.find((s) => s.value === val)
form.origin_sku = found ? found.label.split(' - ')[0] : '' form.origin_sku = found ? found.label.split(' - ')[0] ?? '' : ''
} }
" "
/> />
+9 -9
View File
@@ -342,15 +342,15 @@ function handleDelete(record: { id: number; sku: string }) {
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-space> <a-space>
<a-button type="link" size="small" @click="openCreateMapping(record)"> <a-button type="link" size="small" @click="openCreateMapping(record as any)">
<template #icon><LinkOutlined /></template> <template #icon><LinkOutlined /></template>
新建链接 新建链接
</a-button> </a-button>
<a-button type="link" size="small" @click="openEdit(record)"> <a-button type="link" size="small" @click="openEdit(record as any)">
<template #icon><EditOutlined /></template> <template #icon><EditOutlined /></template>
编辑 编辑
</a-button> </a-button>
<a-button type="link" danger size="small" @click="handleDelete(record)"> <a-button type="link" danger size="small" @click="handleDelete(record as any)">
<template #icon><DeleteOutlined /></template> <template #icon><DeleteOutlined /></template>
删除 删除
</a-button> </a-button>
@@ -397,8 +397,8 @@ function handleDelete(record: { id: number; sku: string }) {
label: (c.label && c.label !== 'null') ? c.label : c.name label: (c.label && c.label !== 'null') ? c.label : c.name
}))" }))"
show-search show-search
:filter-option="(input: string, option: { label: string }) => :filter-option="(input: string, option: { label?: string } | undefined) =>
option.label.toLowerCase().includes(input.toLowerCase())" (option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
/> />
</a-form-item> </a-form-item>
<a-form-item label="SKU 编码" name="sku"> <a-form-item label="SKU 编码" name="sku">
@@ -447,8 +447,8 @@ function handleDelete(record: { id: number; sku: string }) {
label: (p.label && p.label !== 'null') ? p.label : (p.name || `平台 #${p.id}`) label: (p.label && p.label !== 'null') ? p.label : (p.name || `平台 #${p.id}`)
}))" }))"
show-search show-search
:filter-option="(input: string, option: { label: string }) => :filter-option="(input: string, option: { label?: string } | undefined) =>
option.label.toLowerCase().includes(input.toLowerCase())" (option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
/> />
</a-form-item> </a-form-item>
<a-form-item label="店铺" name="store_id"> <a-form-item label="店铺" name="store_id">
@@ -458,8 +458,8 @@ function handleDelete(record: { id: number; sku: string }) {
:options="filteredMappingStores" :options="filteredMappingStores"
allow-clear allow-clear
show-search show-search
:filter-option="(input: string, option: { label: string }) => :filter-option="(input: string, option: { label?: string } | undefined) =>
option.label.toLowerCase().includes(input.toLowerCase())" (option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
/> />
</a-form-item> </a-form-item>
<a-form-item label="平台 SKU" name="platform_outer_sku"> <a-form-item label="平台 SKU" name="platform_outer_sku">
+1 -1
View File
@@ -6,7 +6,7 @@ export function decodeJwtPayload(token: string): Record<string, unknown> | null
try { try {
const parts = token.split('.') const parts = token.split('.')
if (parts.length !== 3) return null if (parts.length !== 3) return null
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/') const base64 = parts[1]!.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64 + '='.repeat((4 - base64.length % 4) % 4) const padded = base64 + '='.repeat((4 - base64.length % 4) % 4)
const decoded = atob(padded) const decoded = atob(padded)
return JSON.parse(decoded) return JSON.parse(decoded)