增加商机线索管理修复若干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
@@ -0,0 +1,239 @@
<template>
<el-dialog
:model-value="visible"
:title="isEdit ? '编辑项目' : '新增项目'"
width="760px"
:close-on-click-modal="false"
@update:model-value="handleClose"
@opened="handleOpened"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" label-position="right">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="project_name">
<el-input v-model="form.project_name" placeholder="请输入项目名称" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号" prop="project_no">
<el-input v-model="form.project_no" placeholder="请输入项目编号" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户" prop="customer_id">
<el-select
v-model="form.customer_id"
filterable
remote
clearable
:remote-method="searchCustomers"
:loading="customerLoading"
placeholder="输入客户名称搜索"
style="width: 100%"
>
<el-option v-for="c in customerOptions" :key="c.id" :label="c.customer_name" :value="c.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户行业" prop="industry">
<el-input v-model="form.industry" placeholder="如:互联网、制造业" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目金额" prop="amount">
<el-input v-model="form.amount" placeholder="请输入项目金额">
<template #append>元</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目状态" prop="status">
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
<el-option v-for="i in PROJECT_STATUS_OPTIONS" :key="i.value" :label="i.label" :value="Number(i.value)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="owner_user_id">
<el-select v-model="form.owner_user_id" filterable placeholder="请选择负责人" style="width: 100%">
<el-option v-for="u in userOptions" :key="u.id" :label="u.name" :value="String(u.id)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开始日期" prop="start_date">
<el-date-picker v-model="form.start_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="end_date">
<el-date-picker v-model="form.end_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对接人" prop="contact_person">
<el-input v-model="form.contact_person" placeholder="请输入对接人" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对接人职位" prop="contact_position">
<el-input v-model="form.contact_position" placeholder="如:采购经理、技术总监" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="对接人手机" prop="contact_phone">
<el-input v-model="form.contact_phone" placeholder="请输入手机号" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="项目地址" prop="address">
<el-input v-model="form.address" placeholder="请输入项目地址" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注(选填)" />
</el-form-item>
</el-col>
</el-row>
</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, watch } from "vue";
import { ElMessage } from "element-plus";
import { createProject, updateProject } from "@/api/crmPipeline";
import { getCrmCustomerList } from "@/api/crm";
import { getAllUsers } from "@/api/user";
import { useAuthStore } from "@/stores/auth";
import { PROJECT_STATUS_OPTIONS } from "../../dict";
const props = defineProps({
visible: { type: Boolean, default: false },
editData: { type: Object, default: null },
});
const emit = defineEmits(["update:visible", "success"]);
const authStore = useAuthStore();
const formRef = ref();
const submitting = ref(false);
const isEdit = ref(false);
const internalId = ref(null);
const userOptions = ref([]);
const customerOptions = ref([]);
const customerLoading = ref(false);
const defaultForm = () => ({
project_name: "",
project_no: "",
customer_id: "",
customer_name: "",
owner_user_id: "",
owner_user_name: "",
industry: "",
amount: "",
status: 1,
start_date: "",
end_date: "",
contact_person: "",
contact_position: "",
contact_phone: "",
address: "",
remark: "",
});
const form = reactive(defaultForm());
const rules = {
project_name: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
};
watch(
() => props.visible,
(val) => {
if (val) {
if (props.editData) {
isEdit.value = true;
internalId.value = props.editData.id;
Object.assign(form, defaultForm(), props.editData);
form.status = Number(props.editData.status) || 1;
} else {
isEdit.value = false;
internalId.value = null;
Object.assign(form, defaultForm());
form.owner_user_id = authStore.user?.id ? String(authStore.user.id) : "";
form.owner_user_name = authStore.user?.name || "";
}
loadUsers();
searchCustomers(form.customer_name || "");
}
}
);
async function loadUsers() {
if (userOptions.value.length) return;
try {
const res = await getAllUsers();
const data = res?.data || {};
const list = Array.isArray(data) ? data : data.list || [];
userOptions.value = list.map((u) => ({ id: u.uid || u.id, name: u.name || u.account || `用户${u.uid || u.id}` }));
} catch (e) {
userOptions.value = [];
}
}
async function searchCustomers(keyword) {
customerLoading.value = true;
try {
const res = await getCrmCustomerList({ page: 1, pageSize: 20, keyword: keyword || "" });
const data = res?.data || {};
customerOptions.value = (data.list || []).map((c) => ({ id: c.id, customer_name: c.customer_name }));
} catch (e) {
customerOptions.value = [];
} finally {
customerLoading.value = false;
}
}
function handleClose() {
emit("update:visible", false);
}
function handleOpened() {
formRef.value?.clearValidate();
}
async function handleSubmit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch (e) {
return;
}
const owner = userOptions.value.find((u) => String(u.id) === String(form.owner_user_id));
if (owner) form.owner_user_name = owner.name;
submitting.value = true;
try {
const payload = { ...form, amount: Number(form.amount) || 0 };
if (internalId.value) {
await updateProject(internalId.value, payload);
ElMessage.success("更新成功");
} else {
await createProject(payload);
ElMessage.success("创建成功");
}
emit("success");
handleClose();
} catch (e) {
ElMessage.error(e.message || "操作失败");
} finally {
submitting.value = false;
}
}
</script>
@@ -0,0 +1,234 @@
<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.status" clearable placeholder="全部" style="width: 130px">
<el-option v-for="i in PROJECT_STATUS_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="项目名称" min-width="180" show-overflow-tooltip fixed>
<template #default="{ row }">
<span class="name-link" @click="openDetail(row)">{{ row.project_name }}</span>
</template>
</el-table-column>
<el-table-column prop="project_no" label="项目编号" width="140" show-overflow-tooltip />
<el-table-column prop="customer_name" label="客户名称" min-width="160" show-overflow-tooltip />
<el-table-column label="项目金额" width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="projectStatusTag(row.status)" size="small">{{ projectStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="开始日期" width="120" align="center">
<template #default="{ row }">{{ formatDateOnly(row.start_date) }}</template>
</el-table-column>
<el-table-column label="结束日期" width="120" align="center">
<template #default="{ row }">{{ formatDateOnly(row.end_date) }}</template>
</el-table-column>
<el-table-column label="负责人" width="100">
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
</el-table-column>
<el-table-column label="对接人" width="100">
<template #default="{ row }">{{ row.contact_person || "-" }}</template>
</el-table-column>
<el-table-column label="操作" width="210" align="center" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
size="small"
:disabled="!row.customer_id"
@click="openContactBook(row)"
>
通讯录
</el-button>
<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>
<ProjectEdit v-model:visible="editVisible" :edit-data="currentRow" @success="fetchList" />
<!-- 项目联系人:直接读写正式公司联系人库(与 ERP/CRM 客户通讯录共用同一份数据) -->
<ContactBook
v-model:visible="contactBookVisible"
company-type="customer"
:company-id="contactBookCompanyId"
:company-name="contactBookCompanyName"
/>
<el-dialog v-model="detailVisible" :title="currentRow?.project_name || '项目详情'" width="720px">
<el-descriptions v-if="currentRow" :column="2" border size="small">
<el-descriptions-item label="项目名称" :span="2">{{ currentRow.project_name }}</el-descriptions-item>
<el-descriptions-item label="项目编号">{{ currentRow.project_no || "-" }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ currentRow.customer_name || "-" }}</el-descriptions-item>
<el-descriptions-item label="项目金额">{{ formatMoney(currentRow.amount) }}</el-descriptions-item>
<el-descriptions-item label="项目状态">
<el-tag :type="projectStatusTag(currentRow.status)" size="small">
{{ projectStatusText(currentRow.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="开始日期">{{ formatDateOnly(currentRow.start_date) }}</el-descriptions-item>
<el-descriptions-item label="结束日期">{{ formatDateOnly(currentRow.end_date) }}</el-descriptions-item>
<el-descriptions-item label="负责人">{{ currentRow.owner_user_name || "-" }}</el-descriptions-item>
<el-descriptions-item label="对接人">{{ currentRow.contact_person || "-" }}</el-descriptions-item>
<el-descriptions-item label="对接人职位">{{ currentRow.contact_position || "-" }}</el-descriptions-item>
<el-descriptions-item label="对接人手机">{{ currentRow.contact_phone || "-" }}</el-descriptions-item>
<el-descriptions-item label="项目地址" :span="2">{{ currentRow.address || "-" }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ currentRow.remark || "-" }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</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 { getProjectList, deleteProject } from "@/api/crmPipeline";
import ProjectEdit from "./components/edit.vue";
import ContactBook from "../../erp/components/contactBook.vue";
import {
PROJECT_STATUS_OPTIONS,
projectStatusText,
projectStatusTag,
formatMoney,
formatDateOnly,
} from "../dict";
const loading = ref(false);
const tableData = ref([]);
const editVisible = ref(false);
const detailVisible = ref(false);
const currentRow = ref(null);
// 项目联系人(正式库)
const contactBookVisible = ref(false);
const contactBookCompanyId = ref(null);
const contactBookCompanyName = ref("");
const filters = reactive({ keyword: "", status: "" });
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
onMounted(() => {
fetchList();
});
async function fetchList() {
loading.value = true;
try {
const res = await getProjectList({
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.status = "";
handleSearch();
}
function openCreate() {
currentRow.value = null;
editVisible.value = true;
}
function openEdit(row) {
currentRow.value = { ...row };
editVisible.value = true;
}
function openDetail(row) {
currentRow.value = { ...row };
detailVisible.value = true;
}
function openContactBook(row) {
if (!row.customer_id) {
ElMessage.warning("该项目未关联正式客户,暂无正式联系人");
return;
}
contactBookCompanyId.value = row.customer_id;
contactBookCompanyName.value = row.customer_name || "";
contactBookVisible.value = true;
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`确定删除项目「${row.project_name}」吗?删除后不可恢复。`, "删除确认", {
type: "warning",
});
await deleteProject(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>