落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-14 14:09:45 +08:00
co-authored by Cursor
parent 8bdf4990a2
commit a70bce3c8f
138 changed files with 7239 additions and 1085 deletions
@@ -0,0 +1,61 @@
/**
* 账号管理 API:走 Jeecg 系统用户,不另起 inquiry 账号表。
* 企微 ID 暂写入 workNo(工号),与现网 Jeecg SysUser 字段对齐;无独立 wecom 列时避免改系统实体。
*/
import {
changePassword,
deleteUser,
getAllRolesListNoByTenant,
getUserRoles,
list as listUsers,
saveOrUpdateUser,
} from '/@/views/system/user/user.api';
export {
changePassword,
deleteUser,
getAllRolesListNoByTenant,
getUserRoles,
listUsers,
saveOrUpdateUser,
};
export type AccountRow = {
id: string;
username: string;
name: string;
wecomUserId: string;
roleId: string;
roleLabel: string;
roleTag: string;
phoneMasked: string;
lastLogin: string;
status: 'active' | 'disabled';
};
const ROLE_TAG_CYCLE = ['tag-violet', 'tag-blue', 'tag-teal', 'tag-amber'];
export function roleTagFor(index: number) {
return ROLE_TAG_CYCLE[index % ROLE_TAG_CYCLE.length];
}
/** Jeecg status:1 正常 → active;其余视为禁用。 */
export function mapUserToAccount(
u: any,
roleId = '',
roleLabel = '未绑定角色',
roleTag = 'tag-blue',
): AccountRow {
return {
id: String(u.id || ''),
username: u.username || '',
name: u.realname || u.username || '',
wecomUserId: u.workNo || u.wecomId || u.thirdId || '',
roleId,
roleLabel,
roleTag,
phoneMasked: u.phone || '未填写',
lastLogin: u.lastLoginTime || u.createTime || '—',
status: Number(u.status) === 1 ? 'active' : 'disabled',
};
}
@@ -1,15 +1,27 @@
<script setup lang="ts">
/**
* 账号管理:列表、新建/编辑、重置密码(Mock)。
* 弹窗对齐原型 admin-accounts.html(原生 overlay,紧凑表单)。
* 账号管理:对接 Jeecg /sys/user(主账登录账号)。
* 与员工岗位页分离——本页管后台登录,员工页管企微业务角色。
*/
import { computed, reactive, ref } from "vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { message, Modal } from "ant-design-vue";
import { mockAccounts, type AccountRow } from "/@/views/inquiry/mock/accounts";
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[]>([...mockAccounts]);
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);
@@ -17,19 +29,12 @@ const form = reactive({
username: "",
name: "",
wecomUserId: "",
roleId: "ops",
roleId: "",
phone: "",
status: "active" as AccountRow["status"],
password: "",
});
const roleOptions = [
{ value: "super", label: "超级管理员", tag: "tag-violet" },
{ value: "ops", label: "运营管理员", tag: "tag-blue" },
{ value: "config", label: "业务配置员", tag: "tag-teal" },
{ value: "audit", label: "只读审计", tag: "tag-amber" },
];
const filtered = computed(() =>
rows.value.filter((r) => {
const kw = filters.keyword.trim().toLowerCase();
@@ -45,13 +50,61 @@ const modalSub = computed(() =>
);
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: "ops",
roleId: roleOptions.value[0]?.value || "",
phone: "",
status: "active",
password: "",
@@ -65,15 +118,15 @@ function openEdit(row: AccountRow) {
username: row.username,
name: row.name,
wecomUserId: row.wecomUserId || "",
roleId: row.roleId,
phone: row.phoneMasked,
roleId: row.roleId || roleOptions.value[0]?.value || "",
phone: row.phoneMasked === "未填写" ? "" : row.phoneMasked,
status: row.status,
password: "",
});
modalOpen.value = true;
}
function save() {
async function save() {
if (!form.username.trim() || !form.name.trim()) {
message.warning("请填写账号与姓名");
return;
@@ -82,51 +135,37 @@ function save() {
message.warning("请填写企业微信 ID");
return;
}
if (!/^[A-Za-z0-9._@-]+$/.test(form.wecomUserId.trim())) {
message.warning("企业微信 ID 格式不正确");
return;
}
if (!editing.value && !form.password.trim()) {
message.warning("请填写初始密码");
return;
}
const role = roleOptions.find((r) => r.value === form.roleId) || roleOptions[1];
if (editing.value) {
Object.assign(editing.value, {
username: form.username,
name: form.name,
wecomUserId: form.wecomUserId.trim(),
roleId: form.roleId,
roleLabel: role.label,
roleTag: role.tag,
phoneMasked: form.phone,
status: form.status,
});
message.success("账号已更新");
} else {
rows.value.push({
id: `a${Date.now()}`,
username: form.username,
name: form.name,
wecomUserId: form.wecomUserId.trim(),
roleId: form.roleId,
roleLabel: role.label,
roleTag: role.tag,
phoneMasked: form.phone || "未填写",
lastLogin: "—",
status: form.status,
});
message.success("账号已创建");
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 || "保存失败");
}
modalOpen.value = false;
}
const resetOpen = ref(false);
const resetTarget = ref<AccountRow | null>(null);
const resetForm = reactive({
password: "",
confirm: "",
});
const resetForm = reactive({ password: "", confirm: "" });
function resetPassword(row: AccountRow) {
resetTarget.value = row;
@@ -140,7 +179,7 @@ function closeReset() {
resetTarget.value = null;
}
function confirmResetPassword() {
async function confirmResetPassword() {
if (!resetForm.password.trim()) {
message.warning("请输入新密码");
return;
@@ -153,8 +192,18 @@ function confirmResetPassword() {
message.warning("两次输入的密码不一致");
return;
}
message.success(`已重置 ${resetTarget.value?.username || ""} 的密码`);
closeReset();
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) {
@@ -165,18 +214,30 @@ function removeAccount(row: AccountRow) {
Modal.confirm({
title: `删除账号 ${row.username}?`,
okType: "danger",
onOk() {
rows.value = rows.value.filter((r) => r.id !== row.id);
message.success("已删除");
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">
<button type="button" class="btn btn-secondary" disabled title="导出待后续开放">
<ProtoIcon name="download" :size="14" />导出
</button>
<button type="button" class="btn btn-primary" @click="openCreate">
@@ -212,6 +273,12 @@ function removeAccount(row: AccountRow) {
</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>
@@ -293,7 +360,7 @@ function removeAccount(row: AccountRow) {
v-model="form.wecomUserId"
class="field field-mono"
type="text"
placeholder="请输入企微通讯录账号"
placeholder="写入工号字段,用于推送关联"
/>
</div>
<div class="form-row">
@@ -309,11 +376,7 @@ function removeAccount(row: AccountRow) {
<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 type="button" :class="{ 'is-selected': form.status === 'active' }" @click="form.status = 'active'">
启用
</button>
<button