Files
yaoayanui/src/views/admin/AdminUsersView.vue
fengchuanhn@gmail.com 3b0a8d777c 11
2026-05-21 00:19:08 +08:00

367 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onMounted, ref } from "vue";
import { useAuthStore, ROLE_ADMIN, ROLE_AGENT,ROLE_OEM } from "../../stores/auth.js";
import {
listAdminUsersApi,
createAdminUserApi,
updateAdminUserApi,
deleteAdminUserApi,
} from "../../api/adminUsers.js";
const auth = useAuthStore();
const ROLE_OPTIONS = [
{ label: "普通用户", value: 0 },
{ label: "管理员", value: 1 },
{ label: "代理", value: 2 },
{ label: "OEM", value: 3 },
];
const ROLE_LABELS = {
0: "普通用户",
[ROLE_ADMIN]: "管理员",
[ROLE_AGENT]: "代理",
[ROLE_OEM]: "OEM",
};
const users = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const loading = ref(false);
const saving = ref(false);
const feedback = ref({ severity: "", message: "" });
const dialogVisible = ref(false);
const dialogMode = ref("create");
const editingId = ref(null);
const emptyForm = () => ({
username: "",
password: "",
phone: "",
role_id: 0,
vip_end_time: "",
clear_vip_end_time: false,
});
const form = ref(emptyForm());
const dialogTitle = computed(() =>
dialogMode.value === "create" ? "新建用户" : "编辑用户",
);
function formatDateTime(value) {
if (!value) return "—";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "—";
return d.toLocaleString("zh-CN", { hour12: false });
}
function toDatetimeLocalValue(iso) {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function roleLabel(roleId) {
return ROLE_LABELS[roleId] ?? `角色 ${roleId}`;
}
function roleSeverity(roleId) {
if (roleId === ROLE_ADMIN) return "info";
if (roleId === ROLE_AGENT) return "warn";
return "secondary";
}
function showMessage(severity, message) {
feedback.value = { severity, message };
}
async function loadUsers() {
loading.value = true;
feedback.value = { severity: "", message: "" };
const res = await listAdminUsersApi(auth.token, {
page: page.value,
pageSize: pageSize.value,
});
loading.value = false;
if (!res.ok) {
showMessage("error", res.message || "加载用户列表失败");
return;
}
const data = res.data;
users.value = Array.isArray(data?.items) ? data.items : [];
total.value = typeof data?.total === "number" ? data.total : 0;
if (typeof data?.page === "number") page.value = data.page;
if (typeof data?.page_size === "number") pageSize.value = data.page_size;
}
async function onPage(event) {
page.value = event.page + 1;
pageSize.value = event.rows;
await loadUsers();
}
function openCreateDialog() {
dialogMode.value = "create";
editingId.value = null;
form.value = emptyForm();
dialogVisible.value = true;
}
function openEditDialog(row) {
dialogMode.value = "edit";
editingId.value = row.id;
form.value = {
username: row.username,
password: "",
phone: row.phone || "",
role_id: row.role_id,
vip_end_time: toDatetimeLocalValue(row.vip_end_time),
clear_vip_end_time: false,
};
dialogVisible.value = true;
}
function buildPayload() {
const payload = {
username: form.value.username.trim(),
phone: form.value.phone.trim() || null,
role_id: form.value.role_id,
};
if (form.value.password.trim()) {
payload.password = form.value.password;
}
if (form.value.clear_vip_end_time) {
payload.clear_vip_end_time = true;
} else if (form.value.vip_end_time) {
payload.vip_end_time = new Date(form.value.vip_end_time).toISOString();
}
return payload;
}
async function saveUser() {
if (!form.value.username.trim()) {
showMessage("warn", "用户名不能为空");
return;
}
if (dialogMode.value === "create" && form.value.password.length < 6) {
showMessage("warn", "密码至少 6 位");
return;
}
saving.value = true;
feedback.value = { severity: "", message: "" };
const payload = buildPayload();
let res;
if (dialogMode.value === "create") {
if (!payload.password) {
saving.value = false;
showMessage("warn", "请设置初始密码");
return;
}
res = await createAdminUserApi(auth.token, payload);
} else {
res = await updateAdminUserApi(auth.token, editingId.value, payload);
}
saving.value = false;
if (!res.ok) {
showMessage("error", res.message || "保存失败");
return;
}
dialogVisible.value = false;
showMessage("success", res.message || "保存成功");
await loadUsers();
}
async function removeUser(row) {
if (row.id === auth.currentUser?.id) {
showMessage("warn", "不能删除当前登录账号");
return;
}
const ok = window.confirm(`确定删除用户「${row.username}」?此操作不可恢复。`);
if (!ok) return;
const res = await deleteAdminUserApi(auth.token, row.id);
if (!res.ok) {
showMessage("error", res.message || "删除失败");
return;
}
showMessage("success", res.message || "已删除");
if (users.value.length === 1 && page.value > 1) {
page.value -= 1;
}
await loadUsers();
}
onMounted(() => {
loadUsers();
});
</script>
<template>
<div class="admin-page admin-users">
<div class="admin-users__header">
<div>
<h1 class="admin-page__title gradient-text">用户管理</h1>
<p class="text-sm text-text-muted">创建编辑与删除系统用户分配角色与 VIP 到期时间</p>
</div>
<Button label="新建用户" @click="openCreateDialog" />
</div>
<Message
v-if="feedback.message"
:severity="feedback.severity"
:closable="true"
class="admin-users__message"
@close="feedback.message = ''"
>
{{ feedback.message }}
</Message>
<DataTable
:value="users"
:loading="loading"
lazy
paginator
:rows="pageSize"
:total-records="total"
:first="(page - 1) * pageSize"
:rows-per-page-options="[10, 20, 50]"
paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
current-page-report-template=" {totalRecords} "
striped-rows
size="small"
class="admin-users__table"
data-key="id"
@page="onPage"
>
<Column field="id" header="ID" style="width: 4rem" />
<Column field="username" header="用户名" />
<Column field="phone" header="手机号">
<template #body="{ data }">
{{ data.phone || "—" }}
</template>
</Column>
<Column field="role_id" header="角色" style="width: 7rem">
<template #body="{ data }">
<Tag :value="roleLabel(data.role_id)" :severity="roleSeverity(data.role_id)" />
</template>
</Column>
<Column field="vip_end_time" header="VIP 到期">
<template #body="{ data }">
{{ formatDateTime(data.vip_end_time) }}
</template>
</Column>
<Column field="created_at" header="注册时间">
<template #body="{ data }">
{{ formatDateTime(data.created_at) }}
</template>
</Column>
<Column header="操作" style="width: 9rem">
<template #body="{ data }">
<div class="admin-users__actions">
<Button
label="编辑"
size="small"
severity="secondary"
text
@click="openEditDialog(data)"
/>
<Button
label="删除"
size="small"
severity="danger"
text
:disabled="data.id === auth.currentUser?.id"
@click="removeUser(data)"
/>
</div>
</template>
</Column>
<template #empty>
<p class="py-6 text-center text-sm text-text-muted">暂无用户</p>
</template>
</DataTable>
<Dialog
v-model:visible="dialogVisible"
:header="dialogTitle"
modal
class="admin-users-dialog"
:style="{ width: '28rem' }"
>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">用户名</label>
<InputText v-model="form.username" class="w-full" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">
{{ dialogMode === "create" ? "密码" : "新密码(留空不修改)" }}
</label>
<Password
v-model="form.password"
:feedback="false"
toggle-mask
fluid
input-class="w-full"
:placeholder="dialogMode === 'create' ? '至少 6 位' : '留空则不修改'"
/>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">手机号可选</label>
<InputText v-model="form.phone" class="w-full" autocomplete="off" />
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">角色</label>
<Select
v-model="form.role_id"
:options="ROLE_OPTIONS"
option-label="label"
option-value="value"
class="w-full"
/>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">VIP 到期时间</label>
<InputText
v-model="form.vip_end_time"
type="datetime-local"
class="w-full"
:disabled="form.clear_vip_end_time"
/>
<label v-if="dialogMode === 'edit'" class="flex items-center gap-2 text-sm text-text-muted">
<Checkbox v-model="form.clear_vip_end_time" :binary="true" input-id="clear-vip" />
<span>清除 VIP 到期时间</span>
</label>
</div>
</div>
<template #footer>
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
<Button label="保存" :loading="saving" @click="saveUser" />
</template>
</Dialog>
</div>
</template>