初始化询价智能体新仓:同步本地工程并排除密钥与 .env。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 员工管理:岗位 Tab、筛选、新建/编辑(Mock)。
|
||||
* 企微 userid 绑定展示对齐原型;不向企微换 ID。
|
||||
* 顶栏:同步企业微信 / 新建员工(Teleport)。
|
||||
*/
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { mockStaff, type StaffRow } from "/@/views/inquiry/mock/staff";
|
||||
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");
|
||||
const filters = reactive({
|
||||
keyword: "",
|
||||
role: "",
|
||||
route: "",
|
||||
status: "" as "" | "active" | "disabled",
|
||||
});
|
||||
|
||||
const modalOpen = ref(false);
|
||||
const editing = ref<StaffRow | null>(null);
|
||||
const form = reactive({
|
||||
name: "",
|
||||
wecomId: "",
|
||||
phone: "",
|
||||
dept: "",
|
||||
role: "sales" as StaffRow["role"],
|
||||
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.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;
|
||||
}),
|
||||
);
|
||||
|
||||
const roleLabelMap: Record<StaffRow["role"], string> = {
|
||||
sales: "销售",
|
||||
air: "航线·空运",
|
||||
sea: "产品·海运",
|
||||
land: "产品·陆运",
|
||||
};
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, {
|
||||
name: "",
|
||||
wecomId: "",
|
||||
phone: "",
|
||||
dept: "",
|
||||
role: "sales",
|
||||
routes: "",
|
||||
notifyEventException: false,
|
||||
status: "active",
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: StaffRow) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
wecomId: row.wecomId,
|
||||
phone: row.phoneMasked,
|
||||
dept: row.dept,
|
||||
role: row.role,
|
||||
routes: row.routes === "-" ? "" : row.routes,
|
||||
notifyEventException: Boolean(row.notifyEventException),
|
||||
status: row.status,
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
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("员工已创建");
|
||||
}
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
function removeStaff(row: StaffRow) {
|
||||
Modal.confirm({
|
||||
title: `删除员工 ${row.name}?`,
|
||||
content: "删除后不可恢复,确认继续?",
|
||||
okType: "danger",
|
||||
onOk() {
|
||||
rows.value = rows.value.filter((r) => r.id !== row.id);
|
||||
message.success("已删除");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function syncWecom() {
|
||||
message.success("已同步企业微信");
|
||||
}
|
||||
|
||||
function routeTags(routes: string) {
|
||||
if (!routes || routes === "-") return [];
|
||||
return routes.split(/[/·,、]/).map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<div class="staff-topbar-actions">
|
||||
<button type="button" class="btn btn-secondary" @click="syncWecom">
|
||||
<ProtoIcon name="refresh" :size="16" />同步企业微信
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="openCreate">
|
||||
<ProtoIcon name="userPlus" :size="16" />新建员工
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="tabs-line-bar">
|
||||
<div class="tabs-line">
|
||||
<button type="button" :class="{ 'is-active': tab === 'all' }" @click="tab = 'all'">
|
||||
全部 <span class="tab-count is-blue">{{ counts.all }}</span>
|
||||
</button>
|
||||
<button type="button" :class="{ 'is-active': tab === 'sales' }" @click="tab = 'sales'">
|
||||
销售 <span class="tab-count is-rose">{{ counts.sales }}</span>
|
||||
</button>
|
||||
<button type="button" :class="{ 'is-active': tab === 'air' }" @click="tab = 'air'">
|
||||
航线 · 空运 <span class="tab-count is-violet">{{ counts.air }}</span>
|
||||
</button>
|
||||
<button type="button" :class="{ 'is-active': tab === 'sea' }" @click="tab = 'sea'">
|
||||
产品 · 海运 <span class="tab-count is-teal">{{ counts.sea }}</span>
|
||||
</button>
|
||||
<button type="button" :class="{ 'is-active': tab === 'land' }" @click="tab = 'land'">
|
||||
产品 · 陆运 <span class="tab-count is-amber">{{ counts.land }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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.role">
|
||||
<option value="">所属岗位</option>
|
||||
<option value="sales">销售</option>
|
||||
<option value="air">航线·空运</option>
|
||||
<option value="sea">产品·海运</option>
|
||||
<option value="land">产品·陆运</option>
|
||||
</select>
|
||||
<select v-model="filters.route">
|
||||
<option value="">负责线路</option>
|
||||
<option value="CGO-KUL">CGO-KUL</option>
|
||||
<option value="HKG-KUL">HKG-KUL</option>
|
||||
<option value="全线">全线</option>
|
||||
<option value="外采-空运">外采-空运</option>
|
||||
<option value="海运全线">海运全线</option>
|
||||
<option value="中港/中亚/中欧">中港/中亚/中欧</option>
|
||||
<option value="国内长途/零担">国内长途/零担</option>
|
||||
<option value="国内城配/拖车/打包">国内城配/拖车/打包</option>
|
||||
<option value="东南亚">东南亚</option>
|
||||
</select>
|
||||
<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>企业微信 ID</th>
|
||||
<th>岗位</th>
|
||||
<th>负责线路</th>
|
||||
<th>创建时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filtered" :key="row.id">
|
||||
<td>
|
||||
<div class="avatar-chip">
|
||||
<div class="av" :style="{ background: row.avatarColor }">{{ row.name.slice(-1) }}</div>
|
||||
<div>
|
||||
<div style="font-weight: 800">{{ row.name }}</div>
|
||||
<div style="font-size: 10px; color: #64748b; font-weight: 700">
|
||||
{{ row.dept }} · {{ row.phoneMasked }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: ui-monospace, monospace; font-size: 10px">{{ row.wecomId }}</td>
|
||||
<td>{{ row.roleLabel }}</td>
|
||||
<td>
|
||||
<div v-if="routeTags(row.routes).length" class="role-perm-summary">
|
||||
<span v-for="tag in routeTags(row.routes)" :key="tag" class="tag tag-violet">{{ tag }}</span>
|
||||
</div>
|
||||
<span v-else style="color: #94a3b8">-</span>
|
||||
</td>
|
||||
<td>{{ row.createdAt }}</td>
|
||||
<td>
|
||||
<span class="tag" :class="row.status === 'active' ? 'tag-green' : 'tag-rose'">
|
||||
{{ row.status === "active" ? "启用" : "禁用" }}
|
||||
</span>
|
||||
</td>
|
||||
<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>
|
||||
</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="editing ? '编辑员工' : '新建员工'"
|
||||
size="md"
|
||||
ok-text="保存"
|
||||
@ok="saveStaff"
|
||||
>
|
||||
<div class="form-row">
|
||||
<label class="lbl">姓名 <span class="req">*</span></label>
|
||||
<input v-model="form.name" class="field" type="text" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">企微 ID <span class="req">*</span></label>
|
||||
<input v-model="form.wecomId" class="field field-mono" type="text" placeholder="如 zhangmin@yt" />
|
||||
</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">部门</label>
|
||||
<input v-model="form.dept" class="field" type="text" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">岗位</label>
|
||||
<select v-model="form.role" class="field">
|
||||
<option value="sales">销售</option>
|
||||
<option value="air">航线·空运</option>
|
||||
<option value="sea">产品·海运</option>
|
||||
<option value="land">产品·陆运</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">负责线路</label>
|
||||
<input v-model="form.routes" class="field" type="text" placeholder="销售可留空" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">消息提醒</label>
|
||||
<label class="check-line">
|
||||
<input v-model="form.notifyEventException" type="checkbox" />
|
||||
<span>接收事件异常消息提醒</span>
|
||||
</label>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.staff-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.check-line {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #344054;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.check-line input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
accent-color: #155eef;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user