Files
yaoayanui/src/views/oem/OemUsersView.vue
fengchuanhn@gmail.com a2ff5b065e 1
2026-05-22 16:25:19 +08:00

412 lines
11 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 {
listOemUsersApi,
createOemUserApi,
updateOemUserApi,
deleteOemUserApi,
} from "../../api/oemUsers.js";
const auth = useAuthStore();
/** 新建/编辑时可分配的角色(仅普通用户、代理) */
const FORM_ROLE_OPTIONS = [
{ label: "普通用户", value: 0 },
{ label: "代理", value: 2 },
];
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 oemOptions = ref([]);
const agentOptions = ref([]);
const filters = ref({
username: "",
oem_id: null,
agent_id: null,
});
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,
});
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 roleLabel(roleId) {
return ROLE_LABELS[roleId] ?? `角色 ${roleId}`;
}
function roleSeverity(roleId) {
if (roleId === ROLE_ADMIN) return "info";
if (roleId === ROLE_AGENT) return "warn";
if (roleId === ROLE_OEM) return "success";
return "secondary";
}
const ownerNameById = computed(() => {
const map = new Map();
for (const u of oemOptions.value) map.set(u.id, u.username);
for (const u of agentOptions.value) map.set(u.id, u.username);
return map;
});
function ownerLabel(userId) {
if (userId == null) return "—";
return ownerNameById.value.get(userId) ?? `#${userId}`;
}
function showMessage(severity, message) {
feedback.value = { severity, message };
}
async function loadFilterOptions() {
const [ agentRes] = await Promise.all([
listOemUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_AGENT }),
]);
if (agentRes.ok && Array.isArray(agentRes.data?.items)) {
agentOptions.value = agentRes.data.items;
}
}
async function loadUsers() {
loading.value = true;
feedback.value = { severity: "", message: "" };
const res = await listOemUsersApi(auth.token, {
page: page.value,
pageSize: pageSize.value,
username: filters.value.username,
oemId: filters.value.oem_id,
agentId: filters.value.agent_id,
});
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();
}
async function applyFilters() {
page.value = 1;
await loadUsers();
}
async function resetFilters() {
filters.value = { username: "", oem_id: null, agent_id: null };
page.value = 1;
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,
};
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;
}
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 createOemUserApi(auth.token, payload);
} else {
res = await updateOemUserApi(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 deleteOemUserApi(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(async () => {
await loadFilterOptions();
await loadUsers();
});
</script>
<template>
<div class="admin-page admin-users">
<div class="admin-users__header">
<div>
<h1 class="admin-page__title gradient-text">用户管理</h1>
</div>
<Button label="新建用户" @click="openCreateDialog" />
</div>
<div class="admin-users__filters">
<InputText
v-model="filters.username"
placeholder="搜索用户名"
class="admin-users__search"
@keyup.enter="applyFilters"
/>
<Select
v-model="filters.agent_id"
:options="agentOptions"
option-label="username"
option-value="id"
placeholder="全部代理"
show-clear
filter
class="admin-users__filter-select"
/>
<Button label="搜索" @click="applyFilters" />
<Button label="重置" severity="secondary" text @click="resetFilters" />
</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="oem_id" header="OEM" style="width: 8rem">
<template #body="{ data }">
{{ ownerLabel(data.oem_id) }}
</template>
</Column>
<Column field="agent_id" header="代理" style="width: 8rem">
<template #body="{ data }">
{{ ownerLabel(data.agent_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="FORM_ROLE_OPTIONS"
option-label="label"
option-value="value"
class="w-full"
/>
</div>
</div>
<template #footer>
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
<Button label="保存" :loading="saving" @click="saveUser" />
</template>
</Dialog>
</div>
</template>