feat(web): 审计日志表与账号设置改密

This commit is contained in:
2026-10-08 23:02:16 +08:00
parent 1b57b75be0
commit 66a9a93914
2 changed files with 227 additions and 8 deletions
+98 -4
View File
@@ -1,13 +1,107 @@
<script setup lang="ts"> <script setup lang="ts">
/**
* 审计日志(交互设计-审计日志):只读时间线 + 域筛选 + 分页
*/
import { computed, ref, watch } from "vue";
import PageHeader from "@/components/common/page-header.vue"; import PageHeader from "@/components/common/page-header.vue";
import { useAuditLogs } from "@/queries/admin.ts";
import { formatDateTime } from "@/utils/time.ts";
const domain = ref<string | null>(null);
const page = ref(1);
const pageSize = ref(50);
const params = computed(() => ({
page: page.value,
page_size: pageSize.value,
domain: domain.value ?? undefined,
}));
const auditQuery = useAuditLogs(() => params.value);
const rows = computed(() => auditQuery.data.value?.items ?? []);
const total = computed(() => auditQuery.data.value?.total ?? 0);
const domainOptions = [
{ value: null, label: "全部域" },
{ value: "key", label: "Key" },
{ value: "payment", label: "支付" },
{ value: "member", label: "成员" },
{ value: "account", label: "账号" },
];
const columns = [
{ title: "时间", key: "created_at", width: 170 },
{ title: "操作者", key: "operator", width: 120 },
{ title: "域", key: "domain", width: 90 },
{ title: "操作", key: "action", width: 160 },
{ title: "摘要", key: "summary" },
];
watch(domain, () => {
page.value = 1;
});
</script> </script>
<template> <template>
<section> <section>
<PageHeader <PageHeader title="审计日志" subtitle="管理操作留痕,只读">
title="审计日志" <template #actions>
subtitle="管理操作留痕(信息结构见交互设计-审计日志)" <a-select
v-model:value="domain"
:options="domainOptions"
style="width: 140px"
placeholder="域"
allow-clear
/> />
<a-skeleton active :paragraph="{ rows: 4 }" /> </template>
</PageHeader>
<a-table
:columns="columns"
:data-source="rows"
:row-key="(r: { id: number }) => r.id"
:loading="auditQuery.isPending.value || auditQuery.isFetching.value"
:pagination="{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['50', '100'],
showTotal: (t: number) => `共 ${t} 条`,
onChange: (p: number, ps: number) => {
page = p;
pageSize = ps;
},
}"
size="middle"
:scroll="{ x: 900 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'created_at'">
{{ formatDateTime(record.created_at) }}
</template>
<template v-else-if="column.key === 'operator'">
{{ record.operator || "系统" }}
</template>
<template v-else-if="column.key === 'domain'">
<a-tag>{{ record.domain }}</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<span class="audit-action">{{ record.action }}</span>
</template>
<template v-else-if="column.key === 'summary'">
{{ record.summary }}
</template>
</template>
</a-table>
</section> </section>
</template> </template>
<style scoped>
.audit-action {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
</style>
+128 -3
View File
@@ -1,10 +1,135 @@
<script setup lang="ts"> <script setup lang="ts">
/**
* 账号设置——改密(交互设计-登录与会话 §3)
*/
import { computed, reactive, ref } from "vue";
import { message } from "antdv-next";
import type { ApiError } from "@/api/http.ts";
import PageHeader from "@/components/common/page-header.vue"; import PageHeader from "@/components/common/page-header.vue";
import { useChangePassword } from "@/queries/session.ts";
const changePassword = useChangePassword();
const form = reactive({
old_password: "",
new_password: "",
confirm_password: "",
});
const fieldError = ref<{ old_password?: string; new_password?: string }>({});
const formError = ref("");
const successMsg = ref("");
const canSubmit = computed(
() =>
form.old_password.length > 0 &&
form.new_password.length >= 8 &&
form.confirm_password.length > 0,
);
async function onSubmit() {
if (!canSubmit.value || changePassword.isPending.value) return;
fieldError.value = {};
formError.value = "";
successMsg.value = "";
if (form.new_password.length < 8) {
fieldError.value.new_password = "新密码至少 8 位";
return;
}
if (form.new_password !== form.confirm_password) {
formError.value = "两次输入的新密码不一致";
return;
}
try {
await changePassword.mutateAsync({
old_password: form.old_password,
new_password: form.new_password,
});
form.old_password = "";
form.new_password = "";
form.confirm_password = "";
successMsg.value = "密码已更新;其他设备的登录已失效";
message.success(successMsg.value);
} catch (e) {
const err = e as ApiError;
if (err.fields?.old_password) {
fieldError.value.old_password = err.fields.old_password;
} else if (err.fields?.new_password) {
fieldError.value.new_password = err.fields.new_password;
} else {
formError.value = err.message || "修改失败";
}
}
}
</script> </script>
<template> <template>
<section> <section class="settings">
<PageHeader title="账号设置" subtitle="修改密码" /> <PageHeader title="账号设置" subtitle="修改管理面登录密码" />
<a-skeleton active :paragraph="{ rows: 3 }" />
<a-form layout="vertical" class="settings__form">
<a-form-item
label="旧密码"
required
:validate-status="fieldError.old_password ? 'error' : undefined"
:help="fieldError.old_password"
>
<a-input-password
v-model:value="form.old_password"
autocomplete="current-password"
@press-enter="onSubmit"
/>
</a-form-item>
<a-form-item
label="新密码"
required
:validate-status="fieldError.new_password ? 'error' : undefined"
:help="fieldError.new_password || '至少 8 位'"
>
<a-input-password
v-model:value="form.new_password"
autocomplete="new-password"
@press-enter="onSubmit"
/>
</a-form-item>
<a-form-item label="确认新密码" required>
<a-input-password
v-model:value="form.confirm_password"
autocomplete="new-password"
@press-enter="onSubmit"
/>
</a-form-item>
<a-alert
v-if="formError"
type="error"
show-icon
:message="formError"
style="margin-bottom: 16px"
/>
<a-alert
v-else-if="successMsg"
type="success"
show-icon
:message="successMsg"
style="margin-bottom: 16px"
/>
<a-button
type="primary"
:loading="changePassword.isPending.value"
:disabled="!canSubmit"
@click="onSubmit"
>
修改密码
</a-button>
</a-form>
</section> </section>
</template> </template>
<style scoped>
.settings__form {
max-width: 420px;
}
</style>