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

This commit is contained in:
hero920103 committed 2026-09-11 11:11:43 +08:00
1 parent b9cdabe045
commit 9c07af2b59
39 files changed
+6219 -42

No files matched your search

@@ -0,0 +1,208 @@
<template>
<el-dialog
:model-value="visible"
:title="isEdit ? '编辑回访' : '新增回访'"
width="560px"
:close-on-click-modal="false"
@update:model-value="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="关联类型" prop="related_type">
<el-radio-group v-model="form.related_type" :disabled="isEdit" @change="handleTypeChange">
<el-radio v-for="i in PIPELINE_RELATED_TYPE_OPTIONS" :key="i.value" :value="i.value">
{{ i.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="relatedLabel" prop="related_id">
<el-select
v-model="form.related_id"
filterable
remote
:disabled="isEdit"
:remote-method="searchRelated"
:loading="relatedLoading"
:placeholder="`搜索${relatedLabel}`"
style="width: 100%"
>
<el-option v-for="o in relatedOptions" :key="o.id" :label="o.name" :value="o.id" />
</el-select>
</el-form-item>
<el-form-item label="回访方式" prop="follow_type">
<el-select v-model="form.follow_type" style="width: 100%">
<el-option v-for="i in FOLLOW_TYPE_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
</el-select>
</el-form-item>
<el-form-item label="回访时间" prop="follow_time">
<el-date-picker
v-model="form.follow_time"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="回访内容" prop="content">
<RichContentEditor
v-model="form.content"
:min-height="140"
placeholder="请输入回访内容,可粘贴或上传图片"
/>
</el-form-item>
<el-form-item label="下次联系" prop="next_contact_time">
<el-date-picker
v-model="form.next_contact_time"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch } from "vue";
import { ElMessage } from "element-plus";
import { addFollow, updateFollow, getClueList, getBusinessList, getProjectList } from "@/api/crmPipeline";
import { PIPELINE_RELATED_TYPE_OPTIONS, FOLLOW_TYPE_OPTIONS } from "../../dict";
import RichContentEditor from "../../components/RichContentEditor.vue";
const props = defineProps({
visible: { type: Boolean, default: false },
editData: { type: Object, default: null },
});
const emit = defineEmits(["update:visible", "success"]);
const formRef = ref();
const submitting = ref(false);
const isEdit = ref(false);
const relatedLoading = ref(false);
const relatedOptions = ref([]);
const defaultForm = () => ({
id: null,
related_type: 1,
related_id: "",
related_name: "",
follow_type: "1",
follow_time: "",
content: "",
next_contact_time: "",
});
const form = reactive(defaultForm());
const relatedLabel = computed(() => {
const t = PIPELINE_RELATED_TYPE_OPTIONS.find((i) => i.value === Number(form.related_type));
return t ? t.label : "关联对象";
});
const validateContent = (rule, value, callback) => {
const text = String(value || "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/g, " ")
.trim();
const hasImg = /<img/i.test(value || "");
if (!text && !hasImg) {
callback(new Error("请输入回访内容"));
} else {
callback();
}
};
const rules = {
related_type: [{ required: true, message: "请选择关联类型", trigger: "change" }],
related_id: [{ required: true, message: "请选择关联对象", trigger: "change" }],
follow_type: [{ required: true, message: "请选择回访方式", trigger: "change" }],
content: [{ validator: validateContent, trigger: "change" }],
};
watch(
() => props.visible,
(val) => {
if (val) {
Object.assign(form, defaultForm());
if (props.editData) {
isEdit.value = true;
Object.assign(form, props.editData);
searchRelated("");
} else {
isEdit.value = false;
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
form.follow_time = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(
now.getHours()
)}:${pad(now.getMinutes())}:00`;
searchRelated("");
}
}
}
);
function handleTypeChange() {
form.related_id = "";
form.related_name = "";
relatedOptions.value = [];
searchRelated("");
}
async function searchRelated(keyword) {
relatedLoading.value = true;
try {
const type = Number(form.related_type);
let res;
if (type === 1) res = await getClueList({ page: 1, pageSize: 20, keyword: keyword || "" });
else if (type === 2) res = await getBusinessList({ page: 1, pageSize: 20, keyword: keyword || "" });
else res = await getProjectList({ page: 1, pageSize: 20, keyword: keyword || "" });
const list = res?.data?.list || [];
relatedOptions.value = list.map((o) => ({
id: o.id,
name: o.clue_name || o.business_name || o.project_name || `#${o.id}`,
}));
} catch (e) {
relatedOptions.value = [];
} finally {
relatedLoading.value = false;
}
}
function handleClose() {
emit("update:visible", false);
}
async function handleSubmit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch (e) {
return;
}
submitting.value = true;
try {
const selected = relatedOptions.value.find((o) => o.id === form.related_id);
const payload = {
...form,
related_name: form.related_name || selected?.name || "",
};
if (form.id) {
await updateFollow(payload);
ElMessage.success("更新成功");
} else {
await addFollow(payload);
ElMessage.success("新增成功");
}
emit("success");
handleClose();
} catch (e) {
ElMessage.error(e.message || "操作失败");
} finally {
submitting.value = false;
}
}
</script>
+175
View File
@@ -0,0 +1,175 @@
<template>
<div class="crm-page">
<div class="page-header">
<div>
<h2>回访记录</h2>
<p>回访贯穿线索、商机、项目全流程,记录每一次客户沟通</p>
</div>
<div class="header-actions">
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新增回访</el-button>
</div>
</div>
<div class="filter-bar">
<el-form :inline="true" :model="filters" @submit.prevent>
<el-form-item>
<el-input
v-model="filters.keyword"
clearable
placeholder="搜索关联对象 / 回访内容"
:prefix-icon="Search"
style="width: 260px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="关联类型">
<el-select v-model="filters.related_type" clearable placeholder="全部" style="width: 120px">
<el-option
v-for="i in PIPELINE_RELATED_TYPE_OPTIONS"
:key="i.value"
:label="i.label"
:value="i.value"
/>
</el-select>
</el-form-item>
<el-form-item label="回访方式">
<el-select v-model="filters.follow_type" clearable placeholder="全部" style="width: 120px">
<el-option v-for="i in FOLLOW_TYPE_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-container" v-loading="loading">
<el-table :data="tableData" stripe border row-key="id">
<el-table-column label="关联类型" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ pipelineRelatedTypeText(row.related_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="related_name" label="关联对象" min-width="180" show-overflow-tooltip />
<el-table-column label="回访方式" width="100" align="center">
<template #default="{ row }">{{ followTypeText(row.follow_type) }}</template>
</el-table-column>
<el-table-column label="回访时间" width="160" align="center">
<template #default="{ row }">{{ formatDateTime(row.follow_time) }}</template>
</el-table-column>
<el-table-column label="回访内容" min-width="240" show-overflow-tooltip>
<template #default="{ row }">{{ stripHtml(row.content) }}</template>
</el-table-column>
<el-table-column label="下次联系时间" width="160" align="center">
<template #default="{ row }">{{ formatDateTime(row.next_contact_time) }}</template>
</el-table-column>
<el-table-column prop="owner_user_name" label="回访人" width="100" />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无回访记录" :image-size="80" /></template>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSearch"
@current-change="fetchList"
/>
</div>
</div>
<FollowEdit v-model:visible="editVisible" :edit-data="currentRow" @success="fetchList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
import { getFollowList, deleteFollow } from "@/api/crmPipeline";
import FollowEdit from "./components/edit.vue";
import {
PIPELINE_RELATED_TYPE_OPTIONS,
FOLLOW_TYPE_OPTIONS,
followTypeText,
pipelineRelatedTypeText,
formatDateTime,
stripHtml,
} from "../dict";
const loading = ref(false);
const tableData = ref([]);
const editVisible = ref(false);
const currentRow = ref(null);
const filters = reactive({ keyword: "", related_type: "", follow_type: "" });
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
onMounted(() => {
fetchList();
});
async function fetchList() {
loading.value = true;
try {
const res = await getFollowList({
page: pagination.page,
pageSize: pagination.pageSize,
...filters,
});
tableData.value = res?.data?.list || [];
pagination.total = res?.data?.total || 0;
} catch (e) {
tableData.value = [];
pagination.total = 0;
ElMessage.error(e.message || "查询失败");
} finally {
loading.value = false;
}
}
function handleSearch() {
pagination.page = 1;
fetchList();
}
function resetFilters() {
filters.keyword = "";
filters.related_type = "";
filters.follow_type = "";
handleSearch();
}
function openCreate() {
currentRow.value = null;
editVisible.value = true;
}
function openEdit(row) {
currentRow.value = { ...row };
editVisible.value = true;
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm("确定删除该回访记录吗?", "删除确认", { type: "warning" });
await deleteFollow({ id: row.id });
ElMessage.success("删除成功");
fetchList();
} catch (e) {
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
}
}
</script>
<style lang="less" scoped src="../styles/crm-page.less"></style>