fix test errors
This commit is contained in:
@@ -20,7 +20,7 @@ const createModalOpen = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({
|
||||
name: '',
|
||||
expires_at: null as string | null,
|
||||
expires_at: undefined as string | undefined,
|
||||
})
|
||||
|
||||
// Plain key modal
|
||||
@@ -50,7 +50,7 @@ const readonlyColumns = columns.filter((c) => c.key !== 'enabled' && c.key !== '
|
||||
|
||||
function showCreateModal() {
|
||||
createForm.name = ''
|
||||
createForm.expires_at = null
|
||||
createForm.expires_at = undefined
|
||||
createModalOpen.value = true
|
||||
}
|
||||
|
||||
@@ -84,9 +84,10 @@ async function handleDelete(record: ApiKeyRecord) {
|
||||
}
|
||||
|
||||
function handleCopyKey() {
|
||||
navigator.clipboard.writeText(plainKey.value).then(() => {
|
||||
message.success('已复制')
|
||||
})
|
||||
navigator.clipboard.writeText(plainKey.value).then(
|
||||
() => message.success('已复制'),
|
||||
() => message.error('复制失败,请手动复制'),
|
||||
)
|
||||
}
|
||||
|
||||
function handlePlainKeyModalClose() {
|
||||
@@ -97,6 +98,10 @@ function handlePlainKeyModalClose() {
|
||||
onMounted(() => {
|
||||
apiKeyStore.fetchMyKeys()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
plainKey.value = ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -120,17 +125,17 @@ onMounted(() => {
|
||||
:loading="apiKeyStore.loading"
|
||||
:pagination="false"
|
||||
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"
|
||||
>
|
||||
<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) }">
|
||||
<span :class="{ 'text-red-500': isExpired(record as ApiKeyRecord) }">
|
||||
{{ formatTime(record.expires_at) }}
|
||||
</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>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'key_prefix'">
|
||||
@@ -185,7 +190,7 @@ onMounted(() => {
|
||||
:loading="apiKeyStore.loading"
|
||||
:pagination="false"
|
||||
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"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -198,10 +203,10 @@ onMounted(() => {
|
||||
<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) }">
|
||||
<span :class="{ 'text-red-500': isExpired(record as ApiKeyRecord) }">
|
||||
{{ formatTime(record.expires_at) }}
|
||||
</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>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'last_used_at'">
|
||||
@@ -210,15 +215,15 @@ onMounted(() => {
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<a-switch
|
||||
:checked="record.enabled"
|
||||
:disabled="isExpired(record)"
|
||||
:disabled="isExpired(record as ApiKeyRecord)"
|
||||
size="small"
|
||||
@change="(checked: boolean) => handleToggle(record, checked)"
|
||||
@change="(checked: boolean | string | number) => handleToggle(record as ApiKeyRecord, !!checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-popconfirm
|
||||
title="确定要删除此 API Key 吗?"
|
||||
@confirm="handleDelete(record)"
|
||||
@confirm="handleDelete(record as ApiKeyRecord)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
|
||||
@@ -186,13 +186,18 @@ function handleModeChange(val: unknown) {
|
||||
}
|
||||
|
||||
// 树事件
|
||||
function onCheck(checked: (string | number)[], info: { halfCheckedKeys: (string | number)[] }) {
|
||||
checkedKeys.value = checked.map(String)
|
||||
halfCheckedKeys.value = info.halfCheckedKeys.map(String)
|
||||
function onCheck(
|
||||
checked: (string | number)[] | { checked: (string | number)[]; halfChecked: (string | number)[] },
|
||||
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[]) {
|
||||
expandedKeys.value = keys
|
||||
function onExpand(keys: (string | number)[]) {
|
||||
expandedKeys.value = keys.map(String)
|
||||
autoExpandParent.value = false
|
||||
}
|
||||
|
||||
@@ -233,8 +238,8 @@ function scopesToCheckedKeys(userScopes: ScopeRecord[]): string[] {
|
||||
}
|
||||
|
||||
// checkedKeys → scopes(智能判断全选/部分选)
|
||||
function checkedKeysToScopes(): { scope_type: string; scope_id: number }[] {
|
||||
const scopes: { scope_type: string; scope_id: number }[] = []
|
||||
function checkedKeysToScopes(): Omit<ScopeRecord, 'name'>[] {
|
||||
const scopes: Omit<ScopeRecord, 'name'>[] = []
|
||||
const keySet = new Set(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 { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
@@ -29,17 +29,28 @@ function createTestRouter() {
|
||||
}
|
||||
|
||||
describe('MainLayout', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
document.body.innerHTML = ''
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
wrapper = undefined
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
async function mountLayout(route = '/') {
|
||||
const router = createTestRouter()
|
||||
await router.push(route)
|
||||
await router.isReady()
|
||||
|
||||
return mount(MainLayout, {
|
||||
const w = mount(MainLayout, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
@@ -65,6 +76,8 @@ describe('MainLayout', () => {
|
||||
default: '<div class="test-content">Page Content</div>',
|
||||
},
|
||||
})
|
||||
wrapper = w
|
||||
return w
|
||||
}
|
||||
|
||||
it('renders all four layout areas', async () => {
|
||||
|
||||
@@ -26,6 +26,8 @@ vi.mock('@/utils/request', () => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -56,7 +58,10 @@ describe('Profile Page', () => {
|
||||
|
||||
async function mountPage() {
|
||||
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()
|
||||
store.setToken(fakeJwtWithRole('administrator'), 'r.p.s', true)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ApiError } from '@/types/api'
|
||||
import ApiKeyPanel from '@/components/ApiKeyPanel.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -95,4 +96,8 @@ const handleSubmit = async () => {
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card title="API Key 管理" class="mt-4">
|
||||
<ApiKeyPanel />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -328,7 +328,8 @@ watch(
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="store.filters.enabled"
|
||||
:value="(store.filters.enabled as any)"
|
||||
@update:value="(v: any) => { store.filters.enabled = v }"
|
||||
placeholder="全部"
|
||||
allow-clear
|
||||
style="width: 100px"
|
||||
@@ -393,11 +394,11 @@ watch(
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<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>
|
||||
编辑
|
||||
</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>
|
||||
删除
|
||||
</a-button>
|
||||
@@ -443,8 +444,8 @@ watch(
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
(input: string, option: { label: string }) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
(input: string, option: { label?: string } | undefined) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -462,8 +463,8 @@ watch(
|
||||
"
|
||||
show-search
|
||||
:filter-option="
|
||||
(input: string, option: { label: string }) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
(input: string, option: { label?: string } | undefined) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -480,8 +481,8 @@ watch(
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="
|
||||
(input: string, option: { label: string }) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())
|
||||
(input: string, option: { label?: string } | undefined) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -500,9 +501,9 @@ watch(
|
||||
:not-found-content="skuSearching ? '搜索中...' : '无匹配结果'"
|
||||
@search="handleSkuSearch"
|
||||
@change="
|
||||
(val: number | undefined) => {
|
||||
(val: unknown) => {
|
||||
const found = skuSearchOptions.find((s) => s.value === val)
|
||||
form.origin_sku = found ? found.label.split(' - ')[0] : ''
|
||||
form.origin_sku = found ? found.label.split(' - ')[0] ?? '' : ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
@@ -342,15 +342,15 @@ function handleDelete(record: { id: number; sku: string }) {
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<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>
|
||||
新建链接
|
||||
</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>
|
||||
编辑
|
||||
</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>
|
||||
删除
|
||||
</a-button>
|
||||
@@ -397,8 +397,8 @@ function handleDelete(record: { id: number; sku: string }) {
|
||||
label: (c.label && c.label !== 'null') ? c.label : c.name
|
||||
}))"
|
||||
show-search
|
||||
:filter-option="(input: string, option: { label: string }) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())"
|
||||
:filter-option="(input: string, option: { label?: string } | undefined) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
|
||||
/>
|
||||
</a-form-item>
|
||||
<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}`)
|
||||
}))"
|
||||
show-search
|
||||
:filter-option="(input: string, option: { label: string }) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())"
|
||||
:filter-option="(input: string, option: { label?: string } | undefined) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="店铺" name="store_id">
|
||||
@@ -458,8 +458,8 @@ function handleDelete(record: { id: number; sku: string }) {
|
||||
:options="filteredMappingStores"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="(input: string, option: { label: string }) =>
|
||||
option.label.toLowerCase().includes(input.toLowerCase())"
|
||||
:filter-option="(input: string, option: { label?: string } | undefined) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="平台 SKU" name="platform_outer_sku">
|
||||
|
||||
@@ -6,7 +6,7 @@ export function decodeJwtPayload(token: string): Record<string, unknown> | null
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
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 decoded = atob(padded)
|
||||
return JSON.parse(decoded)
|
||||
|
||||
Reference in New Issue
Block a user