增加商机线索管理修复若干bug

This commit is contained in:
2026-09-11 11:11:43 +08:00
parent b9cdabe045
commit 9c07af2b59
39 changed files with 6219 additions and 42 deletions
+157 -40
View File
@@ -46,9 +46,20 @@
</el-form>
</div>
<div class="letter-bar">
<span
v-for="item in letterBar"
:key="item"
class="letter-item"
:class="{ active: activeLetter === item, empty: item !== '全部' && !letterCounts[item] }"
@click="handleLetterClick(item)"
>
{{ item }}
</span>
</div>
<div class="table-container" v-loading="loading">
<el-table :data="tableData" stripe border>
<el-table-column type="index" label="#" width="56" align="center" />
<el-table :data="pagedContacts" stripe border>
<el-table-column label="姓名" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<div class="name-cell-wrapper">
@@ -78,18 +89,9 @@
<el-table-column label="更新时间" width="160" align="center">
<template #default="{ row }">{{ formatDateTime(row.update_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
<el-button
v-if="Number(row.is_primary) !== 1"
link
type="primary"
size="small"
@click="handleSetPrimary(row)"
>
设为主联系人
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
@@ -102,11 +104,10 @@
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:total="totalCount"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSearch"
@current-change="fetchList"
@size-change="handlePageSizeChange"
/>
</div>
</div>
@@ -165,10 +166,11 @@
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ref, reactive, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
import { listContacts, updateContact, deleteContact } from "@/api/contact";
import { pinyin } from "pinyin-pro";
import { listContacts, deleteContact } from "@/api/contact";
import ContactEdit from "./components/edit.vue";
import {
RELATED_TYPE_OPTIONS,
@@ -178,7 +180,6 @@ import {
} from "../dict";
const loading = ref(false);
const tableData = ref([]);
const editVisible = ref(false);
const currentEditData = ref(null);
@@ -186,6 +187,16 @@ const currentEditData = ref(null);
const detailVisible = ref(false);
const detailData = ref(null);
// 全量联系人(用于本地 A-Z 排序 / 首字母筛选)
const allContacts = ref([]);
// A-Z 索引
const collator = new Intl.Collator("zh-Hans-CN", { sensitivity: "base" });
const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
.split("")
.filter((l) => !["I", "U", "V"].includes(l));
const activeLetter = ref("全部");
const filters = reactive({
keyword: "",
related_type: "",
@@ -195,28 +206,93 @@ const filters = reactive({
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0,
});
/** 取姓名首字母:中文按拼音、英文按字母,数字/符号/无法识别归 # */
function initialOf(name) {
const s = String(name || "").trim();
if (!s) return "#";
const first = s.charAt(0);
if (/[a-zA-Z]/.test(first)) return first.toUpperCase();
if (!/[\u4e00-\u9fa5]/.test(first)) return "#";
try {
const py = pinyin(first, { pattern: "first", toneType: "none" });
const letter = String(py || "").charAt(0).toUpperCase();
return /[A-Z]/.test(letter) ? letter : "#";
} catch (e) {
return "#";
}
}
function rankOf(letter) {
return letter === "#" ? 99 : letter.charCodeAt(0) - 65;
}
/** A-Z 排序(「全部」视图同样按 A-Z) */
const sortedContacts = computed(() => {
const list = [...allContacts.value];
list.sort((a, b) => {
const la = initialOf(a.contact_name);
const lb = initialOf(b.contact_name);
if (la !== lb) return rankOf(la) - rankOf(lb);
return collator.compare(a.contact_name || "", b.contact_name || "");
});
return list;
});
/** 各首字母数量 */
const letterCounts = computed(() => {
const map = {};
for (const c of allContacts.value) {
const l = initialOf(c.contact_name);
map[l] = (map[l] || 0) + 1;
}
return map;
});
/** 顶部索引:全部 + A-Z(存在 # 数据时追加 #) */
const letterBar = computed(() => [
"全部",
...LETTERS,
...(letterCounts.value["#"] ? ["#"] : []),
]);
/** 按首字母筛选(全部 → 不筛选) */
const filteredContacts = computed(() => {
if (activeLetter.value === "全部") return sortedContacts.value;
return sortedContacts.value.filter((c) => initialOf(c.contact_name) === activeLetter.value);
});
/** 本地分页 */
const pagedContacts = computed(() => {
const start = (pagination.page - 1) * pagination.pageSize;
return filteredContacts.value.slice(start, start + pagination.pageSize);
});
const totalCount = computed(() => filteredContacts.value.length);
onMounted(() => {
fetchList();
});
/** 拉取全量联系人(分页循环,最多 50 页),本地做 A-Z 排序与筛选 */
async function fetchList() {
loading.value = true;
try {
const res = await listContacts({
page: pagination.page,
pageSize: pagination.pageSize,
...filters,
});
const data = res?.data || {};
const list = Array.isArray(data) ? data : data.list || [];
tableData.value = list;
pagination.total = data.total || list.length;
const pageSize = 100;
let all = [];
let total = 0;
for (let page = 1; page <= 50; page++) {
const res = await listContacts({ page, pageSize, ...filters });
const data = res?.data || {};
const list = Array.isArray(data) ? data : data.list || [];
all = all.concat(list);
total = data.total || all.length;
if (list.length === 0 || all.length >= total) break;
}
allContacts.value = all;
} catch (e) {
tableData.value = [];
pagination.total = 0;
allContacts.value = [];
ElMessage.error(e.message || "查询失败");
} finally {
loading.value = false;
@@ -232,9 +308,19 @@ function resetFilters() {
filters.keyword = "";
filters.related_type = "";
filters.is_primary = "";
activeLetter.value = "全部";
handleSearch();
}
function handleLetterClick(item) {
activeLetter.value = item;
pagination.page = 1;
}
function handlePageSizeChange() {
pagination.page = 1;
}
function openCreate() {
currentEditData.value = null;
editVisible.value = true;
@@ -250,16 +336,6 @@ function openEdit(row) {
editVisible.value = true;
}
async function handleSetPrimary(row) {
try {
await updateContact({ ...row, is_primary: 1 });
ElMessage.success("已设为主联系人");
fetchList();
} catch (e) {
ElMessage.error(e.message || "操作失败");
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
@@ -318,6 +394,47 @@ async function handleDelete(row) {
border: 1px solid var(--el-border-color-lighter);
}
.letter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
padding: 8px 12px;
margin-bottom: 12px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
.letter-item {
min-width: 28px;
height: 26px;
padding: 0 8px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 13px;
color: var(--el-text-color-regular);
cursor: pointer;
user-select: none;
transition: all 0.15s;
&:hover {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
&.active {
color: #fff;
background: var(--el-color-primary);
}
&.empty {
color: var(--el-text-color-placeholder);
}
}
}
.name-cell-wrapper {
display: flex;
align-items: center;