195 lines
6.5 KiB
Vue
195 lines
6.5 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, reactive, ref } from "vue";
|
||
import type { FormInst, FormRules } from "naive-ui";
|
||
import {
|
||
NAlert,
|
||
NButton,
|
||
NDescriptions,
|
||
NDescriptionsItem,
|
||
NForm,
|
||
NFormItem,
|
||
NInput,
|
||
NSpace,
|
||
NSpin,
|
||
NTabPane,
|
||
NTabs,
|
||
NText,
|
||
} from "naive-ui";
|
||
import PageHeader from "@/components/PageHeader.vue";
|
||
import HelpTip from "@/components/HelpTip.vue";
|
||
import LoadFailed from "@/components/LoadFailed.vue";
|
||
import { changePassword, getSettings, type RuntimeSettings } from "@/api/admin";
|
||
import { ApiError } from "@/api/http";
|
||
import { message } from "@/utils/notify";
|
||
import { limitsLabel } from "@/utils/labels";
|
||
|
||
const loading = ref(true);
|
||
const loadError = ref("");
|
||
const settings = ref<RuntimeSettings | null>(null);
|
||
const pwd = reactive({ old_password: "", new_password: "", confirm: "" });
|
||
const pwdLoading = ref(false);
|
||
const pwdError = ref("");
|
||
const formRef = ref<FormInst | null>(null);
|
||
|
||
const rules: FormRules = {
|
||
new_password: [
|
||
{
|
||
validator(_, value: string) {
|
||
if (Array.from(value || "").length < 12) {
|
||
return new Error("新密码至少 12 个字符");
|
||
}
|
||
return true;
|
||
},
|
||
trigger: ["blur", "input"],
|
||
},
|
||
],
|
||
confirm: [
|
||
{
|
||
validator() {
|
||
if (pwd.new_password !== pwd.confirm) {
|
||
return new Error("两次输入的新密码不一致");
|
||
}
|
||
return true;
|
||
},
|
||
trigger: ["blur", "input"],
|
||
},
|
||
],
|
||
};
|
||
|
||
async function load() {
|
||
loading.value = true;
|
||
loadError.value = "";
|
||
try {
|
||
settings.value = await getSettings();
|
||
} catch (e) {
|
||
loadError.value = e instanceof Error ? e.message : "加载失败";
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
void load();
|
||
});
|
||
|
||
async function submitPassword() {
|
||
pwdError.value = "";
|
||
try {
|
||
await formRef.value?.validate();
|
||
} catch {
|
||
return;
|
||
}
|
||
pwdLoading.value = true;
|
||
try {
|
||
await changePassword(pwd.old_password, pwd.new_password);
|
||
message.success("管理员密码已修改");
|
||
pwd.old_password = "";
|
||
pwd.new_password = "";
|
||
pwd.confirm = "";
|
||
} catch (e) {
|
||
pwdError.value = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "修改失败";
|
||
} finally {
|
||
pwdLoading.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page">
|
||
<PageHeader title="系统" help="管理员密码在此修改;运行参数只读,修改请改配置文件后重启。" />
|
||
<div class="page-body">
|
||
<n-tabs type="line" animated>
|
||
<n-tab-pane name="password" tab="管理员密码">
|
||
<n-alert v-if="pwdError" type="error" style="margin-bottom: 12px; max-width: 480px" :title="pwdError" />
|
||
<n-form
|
||
ref="formRef"
|
||
:model="pwd"
|
||
:rules="rules"
|
||
label-placement="left"
|
||
label-width="100"
|
||
style="max-width: 480px"
|
||
>
|
||
<n-form-item label="原密码" path="old_password">
|
||
<n-input v-model:value="pwd.old_password" type="password" show-password-on="click" />
|
||
</n-form-item>
|
||
<n-form-item path="new_password">
|
||
<template #label>
|
||
新密码
|
||
<HelpTip>按字符数计至少 12 位。后端不检查 nst_ 前缀(那是端登录密码规则)。</HelpTip>
|
||
</template>
|
||
<n-input v-model:value="pwd.new_password" type="password" show-password-on="click" />
|
||
</n-form-item>
|
||
<n-form-item label="确认新密码" path="confirm">
|
||
<n-input v-model:value="pwd.confirm" type="password" show-password-on="click" />
|
||
</n-form-item>
|
||
<n-space>
|
||
<n-button type="primary" :loading="pwdLoading" @click="submitPassword">修改密码</n-button>
|
||
</n-space>
|
||
</n-form>
|
||
</n-tab-pane>
|
||
<n-tab-pane name="settings" tab="运行参数(只读)">
|
||
<n-spin :show="loading">
|
||
<LoadFailed v-if="loadError && !settings" :description="loadError" @retry="load" />
|
||
<template v-else-if="settings">
|
||
<n-descriptions label-placement="left" :column="2" bordered size="small" style="margin-bottom: 16px">
|
||
<n-descriptions-item>
|
||
<template #label>端监听地址 <HelpTip>listen</HelpTip></template>
|
||
{{ settings.listen }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item>
|
||
<template #label>后台监听地址 <HelpTip>admin_listen</HelpTip></template>
|
||
{{ settings.admin_listen || "(空,与端共用)" }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item>
|
||
<template #label>会话闲置天数 <HelpTip>session_idle_days</HelpTip></template>
|
||
{{ settings.session_idle_days }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item>
|
||
<template #label>记录保留天数 <HelpTip>record_retention_days</HelpTip></template>
|
||
{{ settings.record_retention_days }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item>
|
||
<template #label>防重小时数 <HelpTip>idempotency_hours</HelpTip></template>
|
||
{{ settings.idempotency_hours }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item>
|
||
<template #label>回执保留天数 <HelpTip>receipt_retention_days</HelpTip></template>
|
||
{{ settings.receipt_retention_days }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item>
|
||
<template #label>SQLite 同步模式 <HelpTip>sqlite_synchronous</HelpTip></template>
|
||
{{ settings.sqlite_synchronous }}
|
||
</n-descriptions-item>
|
||
</n-descriptions>
|
||
<n-text strong style="display: block; margin-bottom: 8px">限额</n-text>
|
||
<n-descriptions label-placement="left" :column="2" bordered size="small">
|
||
<n-descriptions-item v-for="(v, k) in settings.limits" :key="k">
|
||
<template #label>
|
||
{{ limitsLabel[String(k)] || k }}
|
||
<HelpTip>{{ k }}</HelpTip>
|
||
</template>
|
||
{{ v }}
|
||
</n-descriptions-item>
|
||
</n-descriptions>
|
||
</template>
|
||
</n-spin>
|
||
</n-tab-pane>
|
||
</n-tabs>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
.page-body {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 16px;
|
||
}
|
||
</style>
|