优化线索、商机、项目相关开发

This commit is contained in:
hero920103 committed 2026-09-11 17:00:23 +08:00
1 parent 9c07af2b59
commit 840673bda5
55 files changed
+4866 -625

No files matched your search

@@ -28,6 +28,19 @@
<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="stage">
<el-select v-model="form.stage" style="width: 100%">
<el-option
v-for="i in PIPELINE_RELATED_TYPE_OPTIONS"
:key="i.value"
:label="i.label"
:value="i.value"
/>
</el-select>
<div style="font-size: 12px; color: var(--el-text-color-secondary); line-height: 1.4; margin-top: 2px">
按该次沟通实际发生的阶段记录,默认跟随关联类型
</div>
</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" />
@@ -90,6 +103,8 @@ const defaultForm = () => ({
related_type: 1,
related_id: "",
related_name: "",
// 所属阶段:1线索/2商机/3项目,默认跟随关联类型,可手动调整
stage: 1,
follow_type: "1",
follow_time: "",
content: "",
@@ -131,6 +146,8 @@ watch(
if (props.editData) {
isEdit.value = true;
Object.assign(form, props.editData);
// 编辑:默认保留记录原本的阶段(缺失时按关联类型回退),可手动修正
form.stage = Number(props.editData.stage || props.editData.related_type || 1);
searchRelated("");
} else {
isEdit.value = false;
@@ -148,6 +165,8 @@ watch(
function handleTypeChange() {
form.related_id = "";
form.related_name = "";
// 切换关联类型时,阶段默认跟随新类型(仍可手动调整)
form.stage = Number(form.related_type);
relatedOptions.value = [];
searchRelated("");
}
@@ -189,6 +208,8 @@ async function handleSubmit() {
const payload = {
...form,
related_name: form.related_name || selected?.name || "",
// 回访阶段:允许与关联类型不同(如补录早期阶段的沟通),默认跟随关联类型
stage: Number(form.stage) || Number(form.related_type),
};
if (form.id) {
await updateFollow(payload);
+117 -4
View File
@@ -49,7 +49,16 @@
<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>
<el-tag size="small" effect="plain" :type="pipelineStageTag(row.related_type)">
{{ pipelineRelatedTypeText(row.related_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="阶段" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" effect="plain" :type="pipelineStageTag(row.stage || row.related_type)">
{{ pipelineStageText(row.stage || row.related_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="related_name" label="关联对象" min-width="180" show-overflow-tooltip />
@@ -59,8 +68,12 @@
<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 label="回访内容" min-width="240">
<template #default="{ row }">
<div class="content-cell" :title="stripHtml(row.content)" @click="openDetail(row)">
{{ stripHtml(row.content) }}
</div>
</template>
</el-table-column>
<el-table-column label="下次联系时间" width="160" align="center">
<template #default="{ row }">{{ formatDateTime(row.next_contact_time) }}</template>
@@ -89,12 +102,46 @@
</div>
<FollowEdit v-model:visible="editVisible" :edit-data="currentRow" @success="fetchList" />
<!-- 回访详情 -->
<el-dialog v-model="detailVisible" :title="detailData?.related_name || '回访详情'" width="680px">
<el-descriptions v-if="detailData" :column="2" border size="small">
<el-descriptions-item label="关联类型">
<el-tag size="small" effect="plain" :type="pipelineStageTag(detailData.related_type)">
{{ pipelineRelatedTypeText(detailData.related_type) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="关联对象">{{ detailData.related_name || "-" }}</el-descriptions-item>
<el-descriptions-item label="回访方式">{{ followTypeText(detailData.follow_type) }}</el-descriptions-item>
<el-descriptions-item label="回访时间">{{ formatDateTime(detailData.follow_time) }}</el-descriptions-item>
<el-descriptions-item label="回访人">{{ detailData.owner_user_name || "-" }}</el-descriptions-item>
<el-descriptions-item label="下次联系时间">
{{ formatDateTime(detailData.next_contact_time) }}
</el-descriptions-item>
<el-descriptions-item label="回访内容" :span="2">
<div class="detail-content" v-html="detailData.content" @click="handleContentClick"></div>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button type="primary" @click="openEditFromDetail">编辑</el-button>
</template>
</el-dialog>
<!-- 回访内容图片放大预览 -->
<el-image-viewer
v-if="previewVisible"
:url-list="previewUrls"
:initial-index="previewIndex"
:z-index="5000"
@close="previewVisible = false"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { ElMessage, ElMessageBox, ElImageViewer } from "element-plus";
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
import { getFollowList, deleteFollow } from "@/api/crmPipeline";
import FollowEdit from "./components/edit.vue";
@@ -103,6 +150,8 @@ import {
FOLLOW_TYPE_OPTIONS,
followTypeText,
pipelineRelatedTypeText,
pipelineStageText,
pipelineStageTag,
formatDateTime,
stripHtml,
} from "../dict";
@@ -112,6 +161,15 @@ const tableData = ref([]);
const editVisible = ref(false);
const currentRow = ref(null);
// 详情弹窗
const detailVisible = ref(false);
const detailData = ref(null);
// 图片预览
const previewVisible = ref(false);
const previewUrls = ref([]);
const previewIndex = ref(0);
const filters = reactive({ keyword: "", related_type: "", follow_type: "" });
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
@@ -160,6 +218,33 @@ function openEdit(row) {
editVisible.value = true;
}
function openDetail(row) {
detailData.value = { ...row };
detailVisible.value = true;
}
function openEditFromDetail() {
if (!detailData.value) return;
currentRow.value = { ...detailData.value };
detailVisible.value = false;
editVisible.value = true;
}
/** 点击详情里的图片 → 放大预览(支持同一条记录内多图切换) */
function handleContentClick(e) {
const target = e.target;
if (!target || String(target.tagName || "").toUpperCase() !== "IMG") return;
const container = e.currentTarget;
if (!container) return;
const imgs = Array.from(container.querySelectorAll("img"));
const urls = imgs.map((img) => img.getAttribute("src")).filter(Boolean);
if (!urls.length) return;
const index = imgs.indexOf(target);
previewUrls.value = urls;
previewIndex.value = index < 0 ? 0 : index;
previewVisible.value = true;
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm("确定删除该回访记录吗?", "删除确认", { type: "warning" });
@@ -173,3 +258,31 @@ async function handleDelete(row) {
</script>
<style lang="less" scoped src="../styles/crm-page.less"></style>
<style lang="less" scoped>
.content-cell {
cursor: pointer;
color: var(--el-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
}
.detail-content {
word-break: break-word;
:deep(img) {
max-width: 220px;
max-height: 160px;
height: auto;
border-radius: 4px;
cursor: zoom-in;
vertical-align: middle;
margin: 2px 4px 2px 0;
}
}
</style>