落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,20 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 员工管理:岗位 Tab、筛选、新建/编辑(Mock)。
|
||||
* 企微 userid 绑定展示对齐原型;不向企微换 ID。
|
||||
* 顶栏:同步企业微信 / 新建员工(Teleport)。
|
||||
* 员工管理:对接主账 /inquiry/staffRole。
|
||||
* 企微 userid 由运营手工绑定,不向企微换号;停用走 disable 接口。
|
||||
*/
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { mockStaff, type StaffRow } from "/@/views/inquiry/mock/staff";
|
||||
import {
|
||||
disableStaff,
|
||||
getStaffCounts,
|
||||
getStaffList,
|
||||
saveStaff as saveStaffApi,
|
||||
} from "/@/views/inquiry/staffRole/staffRole.api";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
|
||||
const rows = ref<StaffRow[]>([...mockStaff]);
|
||||
const tab = ref<"all" | "sales" | "air" | "sea" | "land">("all");
|
||||
type StaffRole = "sales" | "air" | "sea" | "land";
|
||||
|
||||
type StaffRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
phoneMasked: string;
|
||||
dept: string;
|
||||
wecomId: string;
|
||||
role: StaffRole;
|
||||
roleLabel: string;
|
||||
routes: string;
|
||||
notifyEventException: boolean;
|
||||
createdAt: string;
|
||||
status: "active" | "disabled";
|
||||
avatarColor: string;
|
||||
};
|
||||
|
||||
const AVATAR_COLORS = ["#e11d48", "#f43f5e", "#7c3aed", "#0e7490", "#b54708", "#155eef", "#64748b"];
|
||||
|
||||
const roleLabelMap: Record<StaffRole, string> = {
|
||||
sales: "销售",
|
||||
air: "航线·空运",
|
||||
sea: "产品·海运",
|
||||
land: "产品·陆运",
|
||||
};
|
||||
|
||||
const rows = ref<StaffRow[]>([]);
|
||||
const loading = ref(false);
|
||||
const counts = ref({ all: 0, sales: 0, air: 0, sea: 0, land: 0 });
|
||||
const tab = ref<"all" | StaffRole>("all");
|
||||
const filters = reactive({
|
||||
keyword: "",
|
||||
role: "",
|
||||
role: "" as "" | StaffRole,
|
||||
route: "",
|
||||
status: "" as "" | "active" | "disabled",
|
||||
});
|
||||
@@ -26,42 +58,86 @@ const form = reactive({
|
||||
wecomId: "",
|
||||
phone: "",
|
||||
dept: "",
|
||||
role: "sales" as StaffRow["role"],
|
||||
role: "sales" as StaffRole,
|
||||
routes: "",
|
||||
/** 是否接收事件异常消息提醒;对齐旧仓 notifyEventException */
|
||||
notifyEventException: false,
|
||||
status: "active" as StaffRow["status"],
|
||||
});
|
||||
|
||||
const counts = computed(() => ({
|
||||
all: rows.value.length,
|
||||
sales: rows.value.filter((r) => r.role === "sales").length,
|
||||
air: rows.value.filter((r) => r.role === "air").length,
|
||||
sea: rows.value.filter((r) => r.role === "sea").length,
|
||||
land: rows.value.filter((r) => r.role === "land").length,
|
||||
}));
|
||||
const filtered = computed(() => rows.value);
|
||||
|
||||
const filtered = computed(() =>
|
||||
rows.value.filter((r) => {
|
||||
if (tab.value !== "all" && r.role !== tab.value) return false;
|
||||
const kw = filters.keyword.trim().toLowerCase();
|
||||
if (kw) {
|
||||
const hay = `${r.name}${r.wecomId}${r.phoneMasked}`.toLowerCase();
|
||||
if (!hay.includes(kw)) return false;
|
||||
}
|
||||
if (filters.role && r.role !== filters.role) return false;
|
||||
if (filters.route && !r.routes.includes(filters.route)) return false;
|
||||
if (filters.status && r.status !== filters.status) return false;
|
||||
return true;
|
||||
}),
|
||||
function colorFor(name: string) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < name.length; i++) h = (h + name.charCodeAt(i) * (i + 1)) % AVATAR_COLORS.length;
|
||||
return AVATAR_COLORS[h];
|
||||
}
|
||||
|
||||
function mapRow(r: any): StaffRow {
|
||||
const role = (r.roleCode || "sales") as StaffRole;
|
||||
return {
|
||||
id: String(r.id || ""),
|
||||
name: r.name || "",
|
||||
phoneMasked: r.mobile || "未填写",
|
||||
dept: r.departName || "未分配",
|
||||
wecomId: r.wecomId || "",
|
||||
role: ["sales", "air", "sea", "land"].includes(role) ? role : "sales",
|
||||
roleLabel: r.roleLabel || roleLabelMap[role] || role,
|
||||
routes: r.routes || "-",
|
||||
notifyEventException: r.notifyEventException === 1 || r.notifyEventException === true,
|
||||
createdAt: (r.createTime || "").toString().slice(0, 10) || "—",
|
||||
status: r.status === "disabled" ? "disabled" : "active",
|
||||
avatarColor: colorFor(r.name || "员"),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCounts() {
|
||||
try {
|
||||
const res: any = await getStaffCounts();
|
||||
const data = res?.result || res || {};
|
||||
counts.value = {
|
||||
all: Number(data.all || 0),
|
||||
sales: Number(data.sales || 0),
|
||||
air: Number(data.air || 0),
|
||||
sea: Number(data.sea || 0),
|
||||
land: Number(data.land || 0),
|
||||
};
|
||||
} catch {
|
||||
/* 列表仍可用 */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const roleCode = tab.value !== "all" ? tab.value : filters.role || undefined;
|
||||
const res: any = await getStaffList({
|
||||
keyword: filters.keyword.trim() || undefined,
|
||||
roleCode,
|
||||
routes: filters.route || undefined,
|
||||
status: filters.status || undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
const records = res?.records || res?.result?.records || [];
|
||||
rows.value = (records as any[]).map(mapRow);
|
||||
await loadCounts();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "加载员工失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(
|
||||
() => [tab.value, filters.keyword, filters.role, filters.route, filters.status],
|
||||
() => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => loadList(), 200);
|
||||
},
|
||||
);
|
||||
|
||||
const roleLabelMap: Record<StaffRow["role"], string> = {
|
||||
sales: "销售",
|
||||
air: "航线·空运",
|
||||
sea: "产品·海运",
|
||||
land: "产品·陆运",
|
||||
};
|
||||
onMounted(() => loadList());
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
@@ -83,8 +159,8 @@ function openEdit(row: StaffRow) {
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
wecomId: row.wecomId,
|
||||
phone: row.phoneMasked,
|
||||
dept: row.dept,
|
||||
phone: row.phoneMasked === "未填写" ? "" : row.phoneMasked,
|
||||
dept: row.dept === "未分配" ? "" : row.dept,
|
||||
role: row.role,
|
||||
routes: row.routes === "-" ? "" : row.routes,
|
||||
notifyEventException: Boolean(row.notifyEventException),
|
||||
@@ -93,63 +169,60 @@ function openEdit(row: StaffRow) {
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function saveStaff() {
|
||||
async function saveStaff() {
|
||||
if (!form.name.trim() || !form.wecomId.trim()) {
|
||||
message.warning("请填写姓名与企业微信 ID");
|
||||
return;
|
||||
}
|
||||
if (editing.value) {
|
||||
Object.assign(editing.value, {
|
||||
name: form.name,
|
||||
wecomId: form.wecomId,
|
||||
phoneMasked: form.phone,
|
||||
dept: form.dept,
|
||||
role: form.role,
|
||||
roleLabel: roleLabelMap[form.role],
|
||||
routes: form.routes || "-",
|
||||
notifyEventException: form.notifyEventException,
|
||||
status: form.status,
|
||||
});
|
||||
message.success("员工已更新");
|
||||
} else {
|
||||
rows.value.unshift({
|
||||
id: `s${Date.now()}`,
|
||||
name: form.name,
|
||||
phoneMasked: form.phone || "未填写",
|
||||
dept: form.dept || "未分配",
|
||||
wecomId: form.wecomId,
|
||||
role: form.role,
|
||||
roleLabel: roleLabelMap[form.role],
|
||||
routes: form.routes || "-",
|
||||
notifyEventException: form.notifyEventException,
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
status: form.status,
|
||||
avatarColor: "#155eef",
|
||||
});
|
||||
message.success("员工已创建");
|
||||
const body = {
|
||||
id: editing.value?.id,
|
||||
name: form.name.trim(),
|
||||
wecomId: form.wecomId.trim(),
|
||||
mobile: form.phone.trim() || undefined,
|
||||
departName: form.dept.trim() || undefined,
|
||||
roleCode: form.role,
|
||||
roleLabel: roleLabelMap[form.role],
|
||||
routes: form.routes.trim() || undefined,
|
||||
notifyEventException: form.notifyEventException ? 1 : 0,
|
||||
status: form.status,
|
||||
};
|
||||
try {
|
||||
await saveStaffApi(body, Boolean(editing.value));
|
||||
message.success(editing.value ? "员工已更新" : "员工已创建");
|
||||
modalOpen.value = false;
|
||||
await loadList();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "保存失败");
|
||||
}
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
function removeStaff(row: StaffRow) {
|
||||
Modal.confirm({
|
||||
title: `删除员工 ${row.name}?`,
|
||||
content: "删除后不可恢复,确认继续?",
|
||||
title: `停用员工 ${row.name}?`,
|
||||
content: "主账无物理删除接口,确认后将状态设为禁用。",
|
||||
okType: "danger",
|
||||
onOk() {
|
||||
rows.value = rows.value.filter((r) => r.id !== row.id);
|
||||
message.success("已删除");
|
||||
async onOk() {
|
||||
try {
|
||||
await disableStaff(row.id);
|
||||
message.success("已停用");
|
||||
await loadList();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "停用失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function syncWecom() {
|
||||
message.success("已同步企业微信");
|
||||
message.info("企微通讯录同步尚未开放,请手工维护企微 ID");
|
||||
}
|
||||
|
||||
function routeTags(routes: string) {
|
||||
if (!routes || routes === "-") return [];
|
||||
return routes.split(/[/·,、]/).map((s) => s.trim()).filter(Boolean);
|
||||
return routes
|
||||
.split(/[/·,、]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -231,6 +304,12 @@ function routeTags(routes: string) {
|
||||
</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>
|
||||
<div class="avatar-chip">
|
||||
@@ -260,8 +339,13 @@ function routeTags(routes: string) {
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<button class="link-action" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button class="link-action link-danger" type="button" @click="removeStaff(row)">
|
||||
删除
|
||||
<button
|
||||
v-if="row.status === 'active'"
|
||||
class="link-action link-danger"
|
||||
type="button"
|
||||
@click="removeStaff(row)"
|
||||
>
|
||||
停用
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -323,11 +407,7 @@ function routeTags(routes: string) {
|
||||
<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
|
||||
|
||||
Reference in New Issue
Block a user