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