feat(frontend): improve admin usability and light theme

This commit is contained in:
2026-08-04 21:50:20 +08:00
parent 402f050b17
commit e72750235d
15 changed files with 256 additions and 60 deletions

View File

@@ -14,6 +14,15 @@ body {
-webkit-font-smoothing: antialiased;
}
body:not([arco-theme='dark']) {
--color-text-1: #172033;
--color-text-2: #344054;
--color-text-3: #566176;
--color-text-4: #737e91;
color: var(--color-text-1);
}
.echarts-tooltip-diy {
background: linear-gradient(
304.17deg,

View File

@@ -12,6 +12,14 @@ export interface HttpResponse<T = unknown> {
}
let initialized = false;
let sessionExpiredDialogVisible = false;
function networkErrorMessage(error: unknown) {
if (!axios.isAxiosError<HttpResponse>(error)) return '网络请求失败,请稍后重试';
if (!error.response) return '无法连接服务器,请检查网络后重试';
if (error.response.status === 403) return '当前账号无权执行此操作';
return error.response.data?.msg || `请求失败(${error.response.status}`;
}
export function setupHttp() {
if (initialized) {
@@ -47,27 +55,35 @@ export function setupHttp() {
[50008, 50012, 50014].includes(res.code) &&
response.config.url !== '/api/user/info'
) {
Modal.error({
if (!sessionExpiredDialogVisible) {
sessionExpiredDialogVisible = true;
Modal.error({
title: '登录已失效',
content: '当前登录状态已失效,请重新登录。',
content: '当前登录状态已失效重新登录后将返回当前页面。',
okText: '重新登录',
async onOk() {
const userStore = useUserStore();
sessionStorage.setItem('auth-redirect', `${window.location.pathname}${window.location.search}`);
await userStore.logout();
window.location.reload();
window.location.assign('/login');
},
});
onCancel() {
sessionExpiredDialogVisible = false;
},
});
}
}
return Promise.reject(new Error(res.msg || '请求失败'));
}
return res as unknown as AxiosResponse<HttpResponse>;
},
(error) => {
const content = networkErrorMessage(error);
Message.error({
content: error.msg || '网络请求失败',
content,
duration: 5 * 1000,
});
return Promise.reject(error);
return Promise.reject(new Error(content));
},
);
}

View File

@@ -1,6 +1,11 @@
<template>
<div class="dashboard">
<a-spin :loading="loading" class="dashboard-spin">
<a-alert v-if="errorMessage" type="error" show-icon class="dashboard-alert">
{{ errorMessage }}
<template #action><a-button size="small" @click="loadOverview">重新加载</a-button></template>
</a-alert>
<div class="dashboard-meta">统计口径以服务端为准 · 更新时间{{ updatedAt || '尚未加载' }}</div>
<a-grid :cols="{ xs: 1, sm: 2, lg: 4 }" :col-gap="16" :row-gap="16">
<a-grid-item v-for="item in cards" :key="item.key">
<a-card :bordered="false" class="metric-card">
@@ -58,6 +63,8 @@ const emptyOverview = (): DashboardOverview => ({
order_statuses: [], product_statuses: [], payment_channels: [], recent_orders: [],
});
const loading = ref(false);
const errorMessage = ref('');
const updatedAt = ref('');
const overview = ref<DashboardOverview>(emptyOverview());
const router = useRouter();
const userStore = useUserStore();
@@ -110,8 +117,9 @@ const paymentChannelOption = computed<EChartsOption>(() => ({
series: [{ type: 'bar', data: safeArray(overview.value.payment_channels).map((item) => centsToYuan(item.value)) }],
}));
onMounted(async () => {
async function loadOverview() {
loading.value = true;
errorMessage.value = '';
try {
const data = await platformApi.overview();
overview.value = {
@@ -124,17 +132,23 @@ onMounted(async () => {
payment_channels: safeArray(data.payment_channels),
recent_orders: safeArray(data.recent_orders),
};
updatedAt.value = new Date().toLocaleString('zh-CN', { hour12: false });
} catch (error) {
Message.error((error as Error).message);
errorMessage.value = (error as Error).message;
Message.error(errorMessage.value);
} finally {
loading.value = false;
}
});
}
onMounted(loadOverview);
</script>
<style scoped lang="less">
.dashboard { padding: 20px; }
.dashboard-spin { width: 100%; }
.dashboard-alert { margin-bottom: 16px; }
.dashboard-meta { margin-bottom: 12px; color: var(--color-text-3); font-size: 12px; text-align: right; }
.metric-card { min-height: 120px; }
.metric-hint { margin-top: 8px; color: var(--color-text-3); font-size: 12px; }
.section-card { margin-top: 16px; }

View File

@@ -11,9 +11,9 @@
>
<a-form-item
field="username"
label="用户名"
:rules="[{ required: true, message: '请输入用户名' }]"
:validate-trigger="['change', 'blur']"
hide-label
>
<a-input
v-model="userInfo.username"
@@ -26,9 +26,9 @@
</a-form-item>
<a-form-item
field="password"
label="密码"
:rules="[{ required: true, message: '请输入密码' }]"
:validate-trigger="['change', 'blur']"
hide-label
>
<a-input-password
v-model="userInfo.password"
@@ -42,7 +42,6 @@
</a-form-item>
<div class="login-form-password-actions">
<a-checkbox
checked="rememberPassword"
:model-value="loginConfig.rememberPassword"
@change="setRememberPassword as any"
>
@@ -104,11 +103,12 @@ const handleSubmit = async ({
try {
await userStore.login(values as LoginData);
const { redirect, ...othersQuery } = router.currentRoute.value.query;
const storedRedirect = sessionStorage.getItem('auth-redirect');
sessionStorage.removeItem('auth-redirect');
router.push({
name: (redirect as string) || 'dashboard-overview',
query: {
...othersQuery,
},
...(storedRedirect?.startsWith('/')
? { path: storedRedirect }
: { name: (redirect as string) || 'dashboard-overview', query: othersQuery }),
});
Message.success('登录成功');
const { rememberPassword } = loginConfig.value;
@@ -197,6 +197,12 @@ const setRememberPassword = (value: boolean) => {
margin-bottom: 16px;
}
:deep(.arco-form-item-label-col) {
margin-bottom: 6px;
color: #526078;
font-weight: 500;
}
:deep(.arco-input-wrapper) {
height: 48px;
padding: 0 14px;

View File

@@ -13,6 +13,15 @@
</a-button>
</a-space>
</template>
<a-alert
v-if="definition.detailActions?.length"
class="workflow-alert"
type="warning"
show-icon
title="此资源包含受控业务状态"
>
请从详情中的专用操作推进流程操作结果以服务端状态设备回执或审核记录为准并会保留审计记录
</a-alert>
<a-form :model="filters" layout="inline" class="filters" @submit="search">
<a-form-item label="关键字">
<a-input v-model="filters.keyword" allow-clear placeholder="服务端筛选" />
@@ -223,6 +232,9 @@
</a-drawer>
<a-modal :visible="actionVisible" :title="activeAction?.name" :ok-loading="actionSubmitting" :ok-button-props="{ status: activeAction?.danger ? 'danger' : 'normal' }" @cancel="actionVisible = false" @ok="submitDetailAction">
<a-alert v-if="activeAction?.danger" class="action-alert" type="error" show-icon>
这是高风险操作可能改变业务状态且无法直接撤销请确认目标记录和填写内容准确
</a-alert>
<a-form :model="actionForm" layout="vertical">
<a-form-item v-for="field in activeAction?.fields ?? []" :key="field.key" :label="field.label" :required="field.required">
<a-input-number v-if="field.type === 'number' || field.type === 'money'" v-model="actionForm[field.key]" :precision="field.type === 'money' ? 2 : 0" />
@@ -1122,6 +1134,10 @@ function optionLabel(option: Row) {
.filters {
margin-bottom: 16px;
}
.workflow-alert,
.action-alert {
margin-bottom: 16px;
}
.pagination {
display: flex;
justify-content: flex-end;