初始化询价智能体新仓:同步本地工程并排除密钥与 .env。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-13 14:30:44 +08:00
co-authored by Cursor
commit 2a14def882
2621 changed files with 393165 additions and 0 deletions
@@ -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>