fix(frontend): 错误提示单一化/成功提示后置/日志页首次成功拉取
- request.ts blob 失败分支:先解析真实文案再统一提示——此前先弹兜底 「导出失败」再异步补弹真实 message(双提示),401 时跳转提示与 实际原因错位;401 语义文案在导出场景不再失真 - useConfirmAction:收尾(列表刷新)成功后才弹成功提示——此前先弹 「已删除」再刷新,after 失败被吞时成功/失败提示同屏矛盾 - 日志页登录 tab 首次成功加载后不再重复拉取(loginLoadedOk 由 fetcher 成功返回置位,失败切回可重拉,不复活「失败停空表」旧坑) - login_flow.py 密码改从 E2E_ADMIN_PASSWORD 读(与 system_management 同一约定),硬编码默认值在他机上会假绿为「凭据错误」 - e2e 冒烟:登录 8 项 + 系统管理 13 项全过(拦截器全局改动验证) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
c2b462df3b
commit
7f8f88c148
4 files changed
+37
-20
No files matched your search
@@ -23,6 +23,8 @@
|
|||||||
公共工具见 `e2e/common.py`。
|
公共工具见 `e2e/common.py`。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from playwright.sync_api import Page, sync_playwright
|
from playwright.sync_api import Page, sync_playwright
|
||||||
|
|
||||||
from common import (
|
from common import (
|
||||||
@@ -36,7 +38,9 @@ from common import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
USERNAME = "admin"
|
USERNAME = "admin"
|
||||||
PASSWORD = "Admin@2026"
|
# 与 system_management 同一约定:开发库 admin 密码从环境变量传入(各库不同,
|
||||||
|
# 硬编码会在他机上假绿为「凭据错误」),缺省保留历史默认值兜底本机老库
|
||||||
|
PASSWORD = os.environ.get("E2E_ADMIN_PASSWORD", "Admin@2026")
|
||||||
|
|
||||||
|
|
||||||
def run(page: Page) -> None:
|
def run(page: Page) -> None:
|
||||||
|
|||||||
+13
-12
@@ -63,20 +63,21 @@ service.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
(error: AxiosError<ApiResult>) => {
|
async (error: AxiosError<ApiResult>) => {
|
||||||
const status = error.response?.status
|
const status = error.response?.status
|
||||||
let message = error.response?.data?.message ?? '网络异常,请稍后重试'
|
const raw = error.response?.data
|
||||||
// blob 请求失败时 data 是 Blob(如导出接口返回的 JSON 信封错误),需读出真实 message
|
let message = (raw as ApiResult | undefined)?.message ?? '网络异常,请稍后重试'
|
||||||
if (error.response?.data instanceof Blob) {
|
// blob 请求失败时 data 是 Blob(如导出接口返回的 JSON 信封错误):
|
||||||
|
// 先解析出真实文案再统一提示 —— 先弹兜底再异步补弹会造成双提示,
|
||||||
|
// 401 时跳转提示也会与实际原因错位
|
||||||
|
if (raw instanceof Blob) {
|
||||||
message = '导出失败,请稍后重试'
|
message = '导出失败,请稍后重试'
|
||||||
void error.response.data.text().then((text) => {
|
try {
|
||||||
try {
|
const body = JSON.parse(await raw.text()) as { message?: string }
|
||||||
const body = JSON.parse(text) as { message?: string }
|
if (body.message) message = body.message
|
||||||
if (body.message) ElMessage.error(body.message)
|
} catch {
|
||||||
} catch {
|
/* 非 JSON 响应,保持兜底提示 */
|
||||||
/* 非 JSON 响应,保持兜底提示 */
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ export function useConfirmAction() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await options.run()
|
await options.run()
|
||||||
ElMessage.success(options.successText)
|
|
||||||
await options.after?.()
|
await options.after?.()
|
||||||
|
// 收尾(列表刷新)成功后才提示:先提示后收尾会让用户看到「已删除」
|
||||||
|
// 而列表仍留旧行(after 抛错被吞),成功/失败提示同屏矛盾
|
||||||
|
ElMessage.success(options.successText)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handledError(error) // 失败提示由请求拦截器统一完成
|
handledError(error) // 失败提示由请求拦截器统一完成
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
OperationLogItem,
|
OperationLogItem,
|
||||||
OperationLogQueryParams,
|
OperationLogQueryParams,
|
||||||
} from '@/api/logs'
|
} from '@/api/logs'
|
||||||
|
import type { PageData } from '@/api/types'
|
||||||
import StatusTag from '@/components/table/StatusTag.vue'
|
import StatusTag from '@/components/table/StatusTag.vue'
|
||||||
import AppPagination from '@/components/table/AppPagination.vue'
|
import AppPagination from '@/components/table/AppPagination.vue'
|
||||||
import { usePagedList } from '@/composables/usePagedList'
|
import { usePagedList } from '@/composables/usePagedList'
|
||||||
@@ -120,6 +121,15 @@ const loginFilters = reactive({
|
|||||||
})
|
})
|
||||||
const loginTimeRange = ref<[string, string] | null>(null)
|
const loginTimeRange = ref<[string, string] | null>(null)
|
||||||
|
|
||||||
|
// fetcher 成功返回即置位(抛错不会走到置位行):供 watch 判断「首次成功」,
|
||||||
|
// 失败后切回 tab 会重拉,不会停在空表
|
||||||
|
const loginLoadedOk = ref(false)
|
||||||
|
async function loginFetcher(params: LoginLogQueryParams): Promise<PageData<LoginLogItem>> {
|
||||||
|
const data = await logApi.listLoginLogs(params)
|
||||||
|
loginLoadedOk.value = true
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
records: loginRecords,
|
records: loginRecords,
|
||||||
total: loginTotal,
|
total: loginTotal,
|
||||||
@@ -130,7 +140,7 @@ const {
|
|||||||
search: searchLoginLogs,
|
search: searchLoginLogs,
|
||||||
onPageChange: onLoginPageChange,
|
onPageChange: onLoginPageChange,
|
||||||
onSizeChange: onLoginSizeChange,
|
onSizeChange: onLoginSizeChange,
|
||||||
} = usePagedList<LoginLogItem, LoginLogQueryParams>(logApi.listLoginLogs, () => ({
|
} = usePagedList<LoginLogItem, LoginLogQueryParams>(loginFetcher, () => ({
|
||||||
username: loginFilters.username,
|
username: loginFilters.username,
|
||||||
ip: loginFilters.ip,
|
ip: loginFilters.ip,
|
||||||
success: loginFilters.success,
|
success: loginFilters.success,
|
||||||
@@ -150,12 +160,12 @@ function resetLoginFilters(): void {
|
|||||||
|
|
||||||
onMounted(loadOperationLogs)
|
onMounted(loadOperationLogs)
|
||||||
|
|
||||||
// 登录日志 tab 每次激活都刷新(首屏不打多余请求;此前"只加载一次"的
|
// 登录日志 tab 首次成功加载后不再重复拉取(来回切 tab 的重复请求是纯冗余,
|
||||||
// 标志在首次失败后不会重置,切回时停在空表且无从自查)
|
// 筛选/翻页各自触发刷新)。loginLoadedOk 由 fetcher 成功返回时置位:
|
||||||
|
// 此前「每次激活都刷新」与「无条件只拉一次」两个版本各有代价 —— 前者
|
||||||
|
// 冗余请求、后者首次失败后停在空表且无从自查。
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
if (tab === 'login') {
|
if (tab === 'login' && !loginLoadedOk.value) void loadLoginLogs()
|
||||||
void loadLoginLogs()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user