增加商机线索管理修复若干bug
This commit is contained in:
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(/ /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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user