Files
yunzerwebsiteallinone/backend/src/views/apps/crm/business/index.vue
T
2026-10-21 17:45:50 +08:00

296 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.stage" clearable placeholder="全部" style="width: 130px">
<el-option v-for="i in BUSINESS_STAGE_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
</el-select>
</el-form-item>
<el-form-item label="级别">
<el-select v-model="filters.level" clearable placeholder="全部" style="width: 110px">
<el-option v-for="i in PIPELINE_LEVEL_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 130px">
<el-option v-for="i in BUSINESS_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="状态" width="110" align="center" fixed>
<template #default="{ row }">
<el-tag :type="businessStatusTag(row.status)" size="small">{{ businessStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="商机名称" min-width="170" show-overflow-tooltip fixed>
<template #default="{ row }">
<span class="name-link" @click="openDetail(row)">{{ row.business_name }}</span>
</template>
</el-table-column>
<el-table-column prop="customer_name" label="客户名称" min-width="160" show-overflow-tooltip />
<el-table-column label="商机阶段" width="110" align="center">
<template #default="{ row }">
<el-tag :type="businessStageTag(row.stage)" size="small">{{ businessStageText(row.stage) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="级别" width="90" align="center">
<template #default="{ row }">
<el-tag :type="pipelineLevelTag(row.level)" size="small" effect="plain">
{{ pipelineLevelText(row.level) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="预计金额" width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="预计成交日期" width="130" align="center">
<template #default="{ row }">{{ formatDateOnly(row.expect_deal_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="160" align="center">
<template #default="{ row }">{{ formatDateTime(row.next_contact_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" :disabled="row.locked === 1" @click="openEdit(row)">
编辑
</el-button>
<el-button
link
type="warning"
size="small"
:disabled="row.locked === 1 || row.status === 2 || row.status === 3"
@click="openConvert(row)"
>
转项目
</el-button>
<el-button
v-if="row.status === 3"
link
type="success"
size="small"
:disabled="row.locked === 1"
@click="handleRestore(row)"
>
恢复
</el-button>
<el-button
v-else
link
type="info"
size="small"
:disabled="row.locked === 1 || row.status === 2"
@click="handleDiscard(row)"
>
废弃
</el-button>
<el-button
link
type="danger"
size="small"
:disabled="row.locked === 1 || row.status === 2"
@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>
<BusinessEdit v-model:visible="editVisible" :edit-data="currentRow" @success="fetchList" />
<BusinessConvert v-model:visible="convertVisible" :business="currentRow" @success="handleConvertSuccess" />
<BusinessDetail
v-model:visible="detailVisible"
:business="currentRow"
@refresh="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 { getBusinessList, deleteBusiness, changeBusinessStatus } from "@/api/crmPipeline";
import BusinessEdit from "./components/edit.vue";
import BusinessConvert from "./components/convert.vue";
import BusinessDetail from "./components/detail.vue";
import {
BUSINESS_STAGE_OPTIONS,
BUSINESS_STATUS_OPTIONS,
PIPELINE_LEVEL_OPTIONS,
businessStageText,
businessStageTag,
businessStatusText,
businessStatusTag,
pipelineLevelText,
pipelineLevelTag,
formatMoney,
formatDateOnly,
formatDateTime,
} from "../dict";
const loading = ref(false);
const tableData = ref([]);
const editVisible = ref(false);
const convertVisible = ref(false);
const detailVisible = ref(false);
const currentRow = ref(null);
const filters = reactive({ keyword: "", stage: "", level: "", status: "" });
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
onMounted(() => {
fetchList();
});
async function fetchList() {
loading.value = true;
try {
const res = await getBusinessList({
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.stage = "";
filters.level = "";
filters.status = "";
handleSearch();
}
function openCreate() {
currentRow.value = null;
editVisible.value = true;
}
function openEdit(row) {
currentRow.value = { ...row };
editVisible.value = true;
}
function openConvert(row) {
currentRow.value = { ...row };
convertVisible.value = true;
}
function openDetail(row) {
currentRow.value = { ...row };
detailVisible.value = true;
}
function handleConvertSuccess() {
fetchList();
ElMessage.success("商机已转化为项目,可在项目管理中继续跟进");
}
/** 废弃商机:保留全部过程数据,仅把状态置为已废弃,可随时恢复 */
async function handleDiscard(row) {
try {
await ElMessageBox.confirm(
`确定废弃商机「${row.business_name}」吗?废弃后可随时恢复为「跟进中」。`,
"废弃确认",
{ type: "warning" }
);
await changeBusinessStatus(row.id, 3);
ElMessage.success("商机已废弃");
fetchList();
} catch (e) {
if (e !== "cancel") ElMessage.error(e.message || "废弃失败");
}
}
/** 恢复商机:把已废弃的商机恢复为跟进中 */
async function handleRestore(row) {
try {
await ElMessageBox.confirm(`确定将商机「${row.business_name}」恢复为「跟进中」吗?`, "恢复确认", {
type: "info",
});
await changeBusinessStatus(row.id, 1);
ElMessage.success("商机已恢复为跟进中");
fetchList();
} catch (e) {
if (e !== "cancel") ElMessage.error(e.message || "恢复失败");
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`确定删除商机「${row.business_name}」吗?删除后不可恢复。`, "删除确认", {
type: "warning",
});
await deleteBusiness(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>