优化
This commit is contained in:
@@ -38,7 +38,7 @@ describe('API error diagnostics', () => {
|
||||
unwrapApiResponse({ code: 'A0403', message: '无权限', data: null, requestId: 'request-1', traceId: 'trace-1' })
|
||||
} catch (error) {
|
||||
expect(getApiErrorDetails(error)).toEqual({
|
||||
message: '无权限',
|
||||
message: '当前账号无权访问该数据,请联系管理员确认权限',
|
||||
code: 'A0403',
|
||||
requestId: 'request-1',
|
||||
traceId: 'trace-1',
|
||||
@@ -56,13 +56,30 @@ describe('API error diagnostics', () => {
|
||||
})
|
||||
|
||||
expect(getApiErrorDetails(error)).toEqual({
|
||||
message: '服务不可用',
|
||||
message: '服务暂时不可用,请稍后重试',
|
||||
code: 'S0503',
|
||||
requestId: 'request-2',
|
||||
traceId: 'trace-2',
|
||||
})
|
||||
})
|
||||
|
||||
it('uses safe actionable messages for HTTP authentication and permission failures', () => {
|
||||
const createError = (status: number, data: unknown) => new axios.AxiosError('Request failed', 'ERR_BAD_RESPONSE', undefined, undefined, {
|
||||
status,
|
||||
statusText: 'Request failed',
|
||||
headers: {},
|
||||
config: { headers: new AxiosHeaders() },
|
||||
data,
|
||||
})
|
||||
|
||||
expect(getApiErrorDetails(createError(401, { message: 'token parse stack' })).message)
|
||||
.toBe('登录状态已失效,请重新登录')
|
||||
expect(getApiErrorDetails(createError(403, { message: 'policy engine details' })).message)
|
||||
.toBe('当前账号无权访问该数据,请联系管理员确认权限')
|
||||
expect(getApiErrorDetails(createError(502, '<html>upstream failed</html>')).message)
|
||||
.toBe('服务暂时不可用,请稍后重试')
|
||||
})
|
||||
|
||||
it('explains a proxy 413 response without a JSON error envelope', () => {
|
||||
const error = new axios.AxiosError('Request failed', 'ERR_BAD_REQUEST', undefined, undefined, {
|
||||
status: 413,
|
||||
|
||||
+18
-5
@@ -113,7 +113,14 @@ export function unwrapApiResponse<T>(response: ApiEnvelope<T>): T {
|
||||
|
||||
export function getApiErrorDetails(error: unknown, fallback = '接口请求失败'): ApiErrorDetails {
|
||||
if (error instanceof ApiResponseError) {
|
||||
return { message: error.message, code: error.code, requestId: error.requestId, traceId: error.traceId }
|
||||
const message = error.code === 'A0401'
|
||||
? '登录状态已失效,请重新登录'
|
||||
: error.code === 'A0403'
|
||||
? '当前账号无权访问该数据,请联系管理员确认权限'
|
||||
: error.code.startsWith('S05')
|
||||
? '服务暂时不可用,请稍后重试'
|
||||
: error.message
|
||||
return { message, code: error.code, requestId: error.requestId, traceId: error.traceId }
|
||||
}
|
||||
if (axios.isAxiosError(error)) {
|
||||
const response = error.response?.data as {
|
||||
@@ -124,11 +131,17 @@ export function getApiErrorDetails(error: unknown, fallback = '接口请求失
|
||||
traceId?: string | null
|
||||
} | undefined
|
||||
const status = error.response?.status
|
||||
const statusMessage = status === 413
|
||||
? '上传文件的请求体过大,服务器已拒绝;请压缩后重试,如文件未超过 5MB,请联系管理员检查网关上传限制'
|
||||
: undefined
|
||||
const statusMessage = status === 401
|
||||
? '登录状态已失效,请重新登录'
|
||||
: status === 403
|
||||
? '当前账号无权访问该数据,请联系管理员确认权限'
|
||||
: status === 413
|
||||
? '上传文件的请求体过大,服务器已拒绝;请压缩后重试,如文件未超过 5MB,请联系管理员检查网关上传限制'
|
||||
: status && status >= 500
|
||||
? '服务暂时不可用,请稍后重试'
|
||||
: undefined
|
||||
return {
|
||||
message: response?.message || response?.error || statusMessage || fallback,
|
||||
message: statusMessage || response?.message || response?.error || fallback,
|
||||
code: response?.code || (error.response?.status ? `HTTP_${error.response.status}` : undefined),
|
||||
requestId: response?.requestId || undefined,
|
||||
traceId: response?.traceId || undefined,
|
||||
|
||||
@@ -88,6 +88,29 @@ describe('DataTable column preferences', () => {
|
||||
expect(wrapper.findAll('.column-stub').map((column) => column.attributes('data-label'))).toEqual(['操作', '名称', '状态'])
|
||||
})
|
||||
|
||||
it('keeps a newly introduced default-hidden column hidden for an existing preference', () => {
|
||||
localStorage.setItem(preferenceStorageKey('table', 'user-1', 'orders-table'), JSON.stringify({
|
||||
version: 1,
|
||||
order: ['actions', 'name', 'status'],
|
||||
hidden: [],
|
||||
}))
|
||||
const wrapper = shallowMount(DataTable, {
|
||||
props: {
|
||||
data: [],
|
||||
columns: [...configurableColumns, { key: 'internalCode', label: '内部编码', defaultVisible: false }],
|
||||
preferenceKey: 'orders-table',
|
||||
columnSettingsMode: 'show',
|
||||
},
|
||||
global: {
|
||||
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
|
||||
stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub },
|
||||
directives: { loading: {} },
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.column-stub').map((column) => column.attributes('data-label'))).toEqual(['操作', '名称', '状态'])
|
||||
})
|
||||
|
||||
it('keeps business actions, column settings, and trailing controls in one toolbar', () => {
|
||||
const wrapper = shallowMount(DataTable, {
|
||||
props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table', columnSettingsMode: 'show' },
|
||||
|
||||
@@ -21,6 +21,39 @@ describe('UI layout preferences', () => {
|
||||
.toEqual({ version: 1, order: ['actions', 'name'], hidden: ['name'] })
|
||||
})
|
||||
|
||||
it('hides newly introduced default-hidden fields without overriding existing visibility choices', () => {
|
||||
writeLayoutPreference('table', 'user-1', 'roles', {
|
||||
version: 1,
|
||||
order: ['actions', 'role', 'status'],
|
||||
hidden: ['status'],
|
||||
})
|
||||
|
||||
expect(readLayoutPreference(
|
||||
'table',
|
||||
'user-1',
|
||||
'roles',
|
||||
['actions', 'role', 'roleCode', 'status'],
|
||||
['roleCode', 'status'],
|
||||
)).toEqual({
|
||||
version: 1,
|
||||
order: ['actions', 'role', 'status', 'roleCode'],
|
||||
hidden: ['status', 'roleCode'],
|
||||
})
|
||||
|
||||
writeLayoutPreference('table', 'user-1', 'roles', {
|
||||
version: 1,
|
||||
order: ['actions', 'role', 'roleCode', 'status'],
|
||||
hidden: [],
|
||||
})
|
||||
expect(readLayoutPreference(
|
||||
'table',
|
||||
'user-1',
|
||||
'roles',
|
||||
['actions', 'role', 'roleCode', 'status'],
|
||||
['roleCode'],
|
||||
).hidden).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps preferences isolated by user and removes them on reset', () => {
|
||||
writeLayoutPreference('query', 'user-1', 'orders', { version: 1, order: ['status', 'keyword'], hidden: [] })
|
||||
expect(readLayoutPreference('query', 'user-1', 'orders', ['keyword', 'status']).order).toEqual(['status', 'keyword'])
|
||||
@@ -30,4 +63,3 @@ describe('UI layout preferences', () => {
|
||||
expect(readLayoutPreference('query', 'user-1', 'orders', ['keyword', 'status']).order).toEqual(['keyword', 'status'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -47,10 +47,12 @@ export function readLayoutPreference(
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(preferenceStorageKey(kind, scope, preferenceKey)) || 'null') as Partial<LayoutPreferenceSnapshot> | null
|
||||
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.order) || !Array.isArray(parsed.hidden)) return fallback
|
||||
const storedKeys = new Set(uniqueKeys(parsed.order))
|
||||
const newlyDefaultHidden = uniqueKeys(defaultHidden).filter((key) => availableKeys.includes(key) && !storedKeys.has(key))
|
||||
return {
|
||||
version: 1,
|
||||
order: reconcileOrder(availableKeys, parsed.order),
|
||||
hidden: uniqueKeys(parsed.hidden).filter((key) => availableKeys.includes(key)),
|
||||
hidden: uniqueKeys([...parsed.hidden, ...newlyDefaultHidden]).filter((key) => availableKeys.includes(key)),
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
@@ -79,4 +81,3 @@ export function removeLayoutPreference(kind: LayoutPreferenceKind, scope: string
|
||||
// Ignore unavailable browser storage and keep the in-memory default layout.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user