Files
inquiry_robot/inquiry-backend/src/views/inquiry/account/index.vue
T

443 lines
13 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 lang="ts">
/**
* 账号管理:对接 Jeecg /sys/user(主账登录账号)。
* 与员工岗位页分离——本页管后台登录,员工页管企微业务角色。
*/
import { computed, onMounted, reactive, ref, watch } from "vue";
import { message, Modal } from "ant-design-vue";
import {
changePassword,
deleteUser,
getAllRolesListNoByTenant,
getUserRoles,
listUsers,
mapUserToAccount,
roleTagFor,
saveOrUpdateUser,
type AccountRow,
} from "/@/views/inquiry/account/account.api";
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
const rows = ref<AccountRow[]>([]);
const loading = ref(false);
const roleOptions = ref<{ value: string; label: string; tag: string }[]>([]);
const filters = reactive({ keyword: "", status: "" as "" | "active" | "disabled" });
const modalOpen = ref(false);
const editing = ref<AccountRow | null>(null);
const form = reactive({
username: "",
name: "",
wecomUserId: "",
roleId: "",
phone: "",
status: "active" as AccountRow["status"],
password: "",
});
const filtered = computed(() =>
rows.value.filter((r) => {
const kw = filters.keyword.trim().toLowerCase();
if (kw && !`${r.username}${r.name}${r.wecomUserId}${r.phoneMasked}`.toLowerCase().includes(kw)) return false;
if (filters.status && r.status !== filters.status) return false;
return true;
}),
);
const modalTitle = computed(() => (editing.value ? "编辑账号" : "新建账号"));
const modalSub = computed(() =>
editing.value ? `正在编辑:${editing.value.username}` : "创建账号并绑定角色",
);
const modalOkText = computed(() => (editing.value ? "保存修改" : "创建账号"));
async function loadRoles() {
const res: any = await getAllRolesListNoByTenant({});
const list = Array.isArray(res) ? res : res?.records || res?.result || [];
roleOptions.value = (list as any[]).map((r, i) => ({
value: String(r.id),
label: r.roleName || r.roleCode || r.id,
tag: roleTagFor(i),
}));
if (!form.roleId && roleOptions.value.length) {
form.roleId = roleOptions.value.find((r) => r.label.includes("运营"))?.value || roleOptions.value[0].value;
}
}
async function loadList() {
loading.value = true;
try {
if (!roleOptions.value.length) await loadRoles();
const res: any = await listUsers({ pageNo: 1, pageSize: 200 });
const records = res?.records || res?.result?.records || [];
const mapped: AccountRow[] = [];
for (const u of records as any[]) {
let roleId = "";
let roleLabel = "未绑定角色";
let roleTag = "tag-blue";
try {
const roles: any = await getUserRoles({ userid: u.id });
const ids = Array.isArray(roles) ? roles : roles?.result || [];
if (ids.length) {
roleId = String(ids[0]);
const hit = roleOptions.value.find((r) => r.value === roleId);
roleLabel = hit?.label || roleLabel;
roleTag = hit?.tag || roleTag;
}
} catch {
/* 无角色仍展示账号 */
}
mapped.push(mapUserToAccount(u, roleId, roleLabel, roleTag));
}
rows.value = mapped;
} catch (e: any) {
message.error(e?.message || "加载账号失败");
} finally {
loading.value = false;
}
}
onMounted(() => loadList());
function openCreate() {
editing.value = null;
Object.assign(form, {
username: "",
name: "",
wecomUserId: "",
roleId: roleOptions.value[0]?.value || "",
phone: "",
status: "active",
password: "",
});
modalOpen.value = true;
}
function openEdit(row: AccountRow) {
editing.value = row;
Object.assign(form, {
username: row.username,
name: row.name,
wecomUserId: row.wecomUserId || "",
roleId: row.roleId || roleOptions.value[0]?.value || "",
phone: row.phoneMasked === "未填写" ? "" : row.phoneMasked,
status: row.status,
password: "",
});
modalOpen.value = true;
}
async function save() {
if (!form.username.trim() || !form.name.trim()) {
message.warning("请填写账号与姓名");
return;
}
if (!form.wecomUserId.trim()) {
message.warning("请填写企业微信 ID");
return;
}
if (!editing.value && !form.password.trim()) {
message.warning("请填写初始密码");
return;
}
if (!form.roleId) {
message.warning("请选择角色");
return;
}
const payload: Record<string, any> = {
id: editing.value?.id,
username: form.username.trim(),
realname: form.name.trim(),
workNo: form.wecomUserId.trim(),
phone: form.phone.trim() || undefined,
status: form.status === "active" ? 1 : 2,
selectedroles: form.roleId,
};
if (!editing.value) payload.password = form.password;
try {
await saveOrUpdateUser(payload, Boolean(editing.value));
message.success(editing.value ? "账号已更新" : "账号已创建");
modalOpen.value = false;
await loadList();
} catch (e: any) {
message.error(e?.message || "保存失败");
}
}
const resetOpen = ref(false);
const resetTarget = ref<AccountRow | null>(null);
const resetForm = reactive({ password: "", confirm: "" });
function resetPassword(row: AccountRow) {
resetTarget.value = row;
resetForm.password = "";
resetForm.confirm = "";
resetOpen.value = true;
}
function closeReset() {
resetOpen.value = false;
resetTarget.value = null;
}
async function confirmResetPassword() {
if (!resetForm.password.trim()) {
message.warning("请输入新密码");
return;
}
if (resetForm.password.length < 8) {
message.warning("密码至少 8 位");
return;
}
if (resetForm.password !== resetForm.confirm) {
message.warning("两次输入的密码不一致");
return;
}
if (!resetTarget.value) return;
try {
await changePassword({
username: resetTarget.value.username,
password: resetForm.password,
confirmpassword: resetForm.confirm,
});
message.success(`已重置 ${resetTarget.value.username} 的密码`);
closeReset();
} catch (e: any) {
message.error(e?.message || "重置密码失败");
}
}
function removeAccount(row: AccountRow) {
if (row.username === "admin") {
message.warning("系统管理员不可删除");
return;
}
Modal.confirm({
title: `删除账号 ${row.username}?`,
okType: "danger",
async onOk() {
try {
await deleteUser({ id: row.id }, () => undefined);
message.success("已删除");
await loadList();
} catch (e: any) {
message.error(e?.message || "删除失败");
}
},
});
}
watch(
() => filters.keyword,
() => {
/* 本地过滤即可,账号量通常很小 */
},
);
</script>
<template>
<Teleport defer to="#page-topbar-actions">
<div class="account-topbar-actions">
<button type="button" class="btn btn-secondary" disabled title="导出待后续开放">
<ProtoIcon name="download" :size="14" />导出
</button>
<button type="button" class="btn btn-primary" @click="openCreate">
<ProtoIcon name="plus" :size="14" />新建账号
</button>
</div>
</Teleport>
<div class="section-card">
<div class="toolbar">
<div class="search">
<ProtoIcon class="search-icon" name="search" :size="16" />
<input v-model="filters.keyword" placeholder="搜索账号、姓名、企微ID、手机号..." />
</div>
<select v-model="filters.status">
<option value="">状态</option>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</div>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>账号</th>
<th>姓名</th>
<th>绑定角色</th>
<th>手机号</th>
<th>最后登录</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="7" style="text-align: center; color: #94a3b8">加载中…</td>
</tr>
<tr v-else-if="!filtered.length">
<td colspan="7" style="text-align: center; color: #94a3b8">暂无账号</td>
</tr>
<tr v-for="row in filtered" :key="row.id">
<td class="account-username">{{ row.username }}</td>
<td>{{ row.name }}</td>
<td>
<span class="tag" :class="row.roleTag">{{ row.roleLabel }}</span>
</td>
<td>{{ row.phoneMasked }}</td>
<td>{{ row.lastLogin }}</td>
<td>
<span class="tag" :class="row.status === 'active' ? 'tag-green' : 'tag-rose'">
{{ row.status === "active" ? "启用" : "禁用" }}
</span>
</td>
<td>
<div class="account-row-actions">
<button class="link-action" type="button" @click="openEdit(row)">编辑</button>
<button class="link-action link-muted" type="button" @click="resetPassword(row)">
重置密码
</button>
<button
v-if="row.username !== 'admin'"
class="link-action link-danger"
type="button"
@click="removeAccount(row)"
>
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span>
共 <b style="color: #0f172a">{{ filtered.length }}</b> 个账号
</span>
<div class="pages">
<button class="is-active" type="button">1</button>
</div>
</div>
</div>
<AdminModal
v-model:open="modalOpen"
:title="modalTitle"
:subtitle="modalSub"
size="md"
:ok-text="modalOkText"
@ok="save"
>
<div class="form-row">
<label class="lbl">账号 <span class="req">*</span></label>
<input
v-model="form.username"
class="field field-mono"
type="text"
placeholder="英文字母、数字、下划线"
:readonly="Boolean(editing)"
:class="{ 'is-readonly': Boolean(editing) }"
/>
</div>
<div v-if="!editing" class="form-row">
<label class="lbl">密码 <span class="req">*</span></label>
<input
v-model="form.password"
class="field"
type="password"
placeholder="至少 8 位,含字母与数字"
/>
</div>
<div class="form-row">
<label class="lbl">姓名 <span class="req">*</span></label>
<input v-model="form.name" class="field" type="text" placeholder="账号持有人姓名" />
</div>
<div class="form-row">
<label class="lbl">企业微信 ID <span class="req">*</span></label>
<input
v-model="form.wecomUserId"
class="field field-mono"
type="text"
placeholder="写入工号字段,用于推送关联"
/>
</div>
<div class="form-row">
<label class="lbl">手机号</label>
<input v-model="form.phone" class="field" type="tel" />
</div>
<div class="form-row">
<label class="lbl">绑定角色 <span class="req">*</span></label>
<select v-model="form.roleId" class="field">
<option v-for="r in roleOptions" :key="r.value" :value="r.value">{{ r.label }}</option>
</select>
</div>
<div class="form-row">
<label class="lbl">状态</label>
<div class="status-toggle">
<button type="button" :class="{ 'is-selected': form.status === 'active' }" @click="form.status = 'active'">
启用
</button>
<button
type="button"
:class="{ 'is-selected': form.status === 'disabled' }"
@click="form.status = 'disabled'"
>
禁用
</button>
</div>
</div>
</AdminModal>
<AdminModal
v-model:open="resetOpen"
title="重置密码"
:subtitle="resetTarget ? `账号:${resetTarget.username}(${resetTarget.name})` : ''"
size="sm"
ok-text="确认重置"
@ok="confirmResetPassword"
>
<div class="form-row">
<label class="lbl">新密码 <span class="req">*</span></label>
<input
v-model="resetForm.password"
class="field"
type="password"
placeholder="至少 8 位,含字母与数字"
autocomplete="new-password"
/>
</div>
<div class="form-row">
<label class="lbl">确认密码 <span class="req">*</span></label>
<input
v-model="resetForm.confirm"
class="field"
type="password"
placeholder="再次输入新密码"
autocomplete="new-password"
/>
</div>
</AdminModal>
</template>
<style scoped>
.account-topbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.account-username {
font-family: ui-monospace, monospace;
font-weight: 800;
font-size: 11px;
}
.account-row-actions {
display: flex;
align-items: center;
gap: 10px;
}
</style>