CRM里面的客户、线索管理适配手机端

This commit is contained in:
2026-03-12 17:31:23 +08:00
parent 0432e2430a
commit 41d7bd6c86
13 changed files with 1802 additions and 983 deletions

View File

@@ -1,21 +1,28 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-row>
<el-col :span="12">
<el-drawer
v-model="dialogVisible"
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
>
<!-- 基本信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
<el-form-item label="线索名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入线索名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择客户来源" class="w-1/1">
<el-select v-model="formData.source" placeholder="请选择客户来源" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
@@ -24,20 +31,11 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="ownerUserId">
<el-select
v-model="formData.ownerUserId"
:disabled="formType !== 'create'"
class="w-1/1"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
@@ -47,36 +45,33 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 联系方式 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">联系方式</div>
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="微信" prop="wechat">
<el-input v-model="formData.wechat" placeholder="请输入微信" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="QQ" prop="qq">
<el-input v-model="formData.qq" placeholder="请输入 QQ" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 客户信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">客户信息</div>
<el-form-item label="客户行业" prop="industryId">
<el-select v-model="formData.industryId" placeholder="请选择客户行业" class="w-1/1">
<el-select v-model="formData.industryId" placeholder="请选择客户行业" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
@@ -85,10 +80,8 @@
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择客户级别" class="w-1/1">
<el-select v-model="formData.level" placeholder="请选择客户级别" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
@@ -97,52 +90,52 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 地址信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">地址信息</div>
<el-form-item label="地址" prop="areaId">
<el-cascader
v-model="formData.areaId"
:options="areaList"
:props="defaultProps"
class="w-1/1"
style="width: 100%"
clearable
filterable
placeholder="请选择城市"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 其他信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">其他信息</div>
<el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker
v-model="formData.contactNextTime"
placeholder="选择下次联系时间"
type="datetime"
value-format="x"
class="!w-1/1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -258,3 +251,44 @@ const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,34 +1,36 @@
<template>
<div v-loading="loading">
<div class="flex items-start justify-between">
<div>
<!-- 左上线索基本信息 -->
<el-col>
<el-row>
<span class="text-xl font-bold">{{ clue.name }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上按钮 -->
<div class="mobile-detail-header" v-loading="loading">
<!-- 标题和操作按钮 -->
<div class="mobile-detail-header__top">
<div class="mobile-detail-header__title">{{ clue.name }}</div>
<div class="mobile-detail-header__actions">
<slot></slot>
</div>
</div>
<!-- 关键信息卡片 -->
<div class="mobile-detail-header__card">
<div class="mobile-info-list">
<div class="mobile-info-row">
<span class="mobile-info-row__label">线索来源</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">手机</span>
<span class="mobile-info-row__value">{{ clue.mobile || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">负责人</span>
<span class="mobile-info-row__value">{{ clue.ownerUserName || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">创建时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.createTime) || '-' }}</span>
</div>
</div>
</div>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="vertical">
<el-descriptions-item label="线索来源">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</el-descriptions-item>
<el-descriptions-item label="手机"> {{ clue.mobile }} </el-descriptions-item>
<el-descriptions-item label="负责人">
{{ clue.ownerUserName }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(clue.createTime) }}
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
@@ -41,3 +43,57 @@ defineProps<{
loading: boolean // 加载中
}>()
</script>
<style lang="scss" scoped>
.mobile-detail-header {
padding: 12px;
background: #f5f7fa;
}
.mobile-detail-header__top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
gap: 12px;
}
.mobile-detail-header__title {
font-size: 18px;
font-weight: 600;
color: #303133;
flex: 1;
word-break: break-all;
}
.mobile-detail-header__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex-shrink: 0;
}
.mobile-detail-header__card {
background: #fff;
border-radius: 10px;
padding: 14px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-info-list {
font-size: 13px;
}
.mobile-info-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&__label {
color: #909399;
flex-shrink: 0;
margin-right: 12px;
}
&__value {
color: #303133;
text-align: right;
}
}
</style>

View File

@@ -1,61 +1,97 @@
<template>
<ContentWrap>
<el-collapse v-model="activeNames" class="">
<el-collapse-item name="basicInfo">
<template #title>
<span class="text-base font-bold">基本信息</span>
</template>
<el-descriptions :column="4">
<el-descriptions-item label="线索名称">
{{ clue.name }}
</el-descriptions-item>
<el-descriptions-item label="客户来源">
<div class="mobile-detail-info">
<!-- 基本信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
<div class="mobile-info-list">
<div class="mobile-info-row">
<span class="mobile-info-row__label">线索名称</span>
<span class="mobile-info-row__value">{{ clue.name || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">客户来源</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
</el-descriptions-item>
<el-descriptions-item label="手机">{{ clue.mobile }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ clue.telephone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ clue.email }}</el-descriptions-item>
<el-descriptions-item label="地址">
{{ clue.areaName }} {{ clue.detailAddress }}
</el-descriptions-item>
<el-descriptions-item label="QQ">{{ clue.qq }}</el-descriptions-item>
<el-descriptions-item label="微信">{{ clue.wechat }}</el-descriptions-item>
<el-descriptions-item label="客户行业">
</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">手机</span>
<span class="mobile-info-row__value">{{ clue.mobile || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">电话</span>
<span class="mobile-info-row__value">{{ clue.telephone || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">邮箱</span>
<span class="mobile-info-row__value">{{ clue.email || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">地址</span>
<span class="mobile-info-row__value">{{ clue.areaName || '' }} {{ clue.detailAddress || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">QQ</span>
<span class="mobile-info-row__value">{{ clue.qq || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">微信</span>
<span class="mobile-info-row__value">{{ clue.wechat || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">客户行业</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="clue.industryId" />
</el-descriptions-item>
<el-descriptions-item label="客户级别">
</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">客户级别</span>
<span class="mobile-info-row__value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="clue.level" />
</el-descriptions-item>
<el-descriptions-item label="下次联系时间">
{{ formatDate(clue.contactNextTime) }}
</el-descriptions-item>
<el-descriptions-item label="备注">{{ clue.remark }}</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
<el-collapse-item name="systemInfo">
<template #title>
<span class="text-base font-bold">系统信息</span>
</template>
<el-descriptions :column="4">
<el-descriptions-item label="负责人">{{ clue.ownerUserName }}</el-descriptions-item>
<el-descriptions-item label="最后跟进记录">
{{ clue.contactLastContent }}
</el-descriptions-item>
<el-descriptions-item label="最后跟进时间">
{{ formatDate(clue.contactLastTime) }}
</el-descriptions-item>
<el-descriptions-item label="">&nbsp;</el-descriptions-item>
<el-descriptions-item label="创建人">{{ clue.creatorName }}</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(clue.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(clue.updateTime) }}
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</ContentWrap>
</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">下次联系时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.contactNextTime) || '-' }}</span>
</div>
<div class="mobile-info-row" v-if="clue.remark">
<span class="mobile-info-row__label">备注</span>
<span class="mobile-info-row__value">{{ clue.remark }}</span>
</div>
</div>
</div>
<!-- 系统信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">系统信息</div>
<div class="mobile-info-list">
<div class="mobile-info-row">
<span class="mobile-info-row__label">负责人</span>
<span class="mobile-info-row__value">{{ clue.ownerUserName || '-' }}</span>
</div>
<div class="mobile-info-row" v-if="clue.contactLastContent">
<span class="mobile-info-row__label">最后跟进记录</span>
<span class="mobile-info-row__value">{{ clue.contactLastContent }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">最后跟进时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.contactLastTime) || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">创建人</span>
<span class="mobile-info-row__value">{{ clue.creatorName || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">创建时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.createTime) || '-' }}</span>
</div>
<div class="mobile-info-row">
<span class="mobile-info-row__label">更新时间</span>
<span class="mobile-info-row__value">{{ formatDate(clue.updateTime) || '-' }}</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import * as ClueApi from '@/api/crm/clue'
@@ -66,7 +102,48 @@ defineOptions({ name: 'CrmClueDetailsInfo' })
const { clue } = defineProps<{
clue: ClueApi.ClueVO // 线索明细
}>()
const activeNames = ref(['basicInfo', 'systemInfo']) // 展示的折叠面板
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.mobile-detail-info {
padding: 12px;
background: #f5f7fa;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-info-list {
font-size: 13px;
}
.mobile-info-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&__label {
color: #909399;
flex-shrink: 0;
margin-right: 12px;
}
&__value {
color: #303133;
text-align: right;
word-break: break-all;
}
}
</style>

View File

@@ -1,52 +1,18 @@
<template>
<doc-alert title="【线索】线索管理" url="https://doc.iocoder.cn/crm/clue/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="线索名称" prop="name">
<div class="mobile-page">
<!-- 搜索头部 -->
<div class="mobile-page__header">
<div class="mobile-page__search">
<el-input
v-model="queryParams.name"
placeholder="请输入线索名称"
placeholder="搜索线索名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
:prefix-icon="Search"
/>
</el-form-item>
<el-form-item label="转化状态" prop="transformStatus">
<el-select v-model="queryParams.transformStatus" class="!w-240px">
<el-option :value="false" label="未转化" />
<el-option :value="true" label="已转化" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input
v-model="queryParams.telephone"
placeholder="请输入电话"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" :icon="Filter" @click="filterDrawerVisible = true" />
</div>
<div class="mobile-page__actions">
<el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:clue:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
@@ -57,108 +23,129 @@
:loading="exportLoading"
v-hasPermi="['crm:clue:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</div>
</div>
<!-- 列表 -->
<ContentWrap>
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="线索名称" align="center" prop="name" fixed="left" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column label="线索来源" align="center" prop="source" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column label="手机" align="center" prop="mobile" width="120" />
<el-table-column label="电话" align="center" prop="telephone" width="130" />
<el-table-column label="邮箱" align="center" prop="email" width="180" />
<el-table-column label="地址" align="center" prop="detailAddress" width="180" />
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column align="center" label="客户级别" prop="level" width="135">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
<el-table-column
label="最后跟进时间"
align="center"
prop="contactLastTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" />
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100" />
<el-table-column
label="更新时间"
align="center"
prop="updateTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:clue:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:clue:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- Tab 切换 -->
<div class="mobile-page__tabs">
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
</div>
<!-- 线索列表 -->
<div class="mobile-page__content" v-loading="loading">
<div class="mobile-item-list">
<div
v-for="item in list"
:key="item.id"
class="mobile-item-card mobile-item-card--clickable"
@click="openDetail(item.id)"
>
<div class="mobile-item-card__header">
<span class="mobile-item-card__name">{{ item.name }}</span>
<el-tag v-if="item.transformStatus" type="success" size="small">已转化</el-tag>
<el-tag v-else type="info" size="small">未转化</el-tag>
</div>
<div class="mobile-item-card__body">
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">线索来源</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="item.source" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">手机</span>
<span class="mobile-item-card__info-value">{{ item.mobile || '-' }}</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">负责人</span>
<span class="mobile-item-card__info-value">{{ item.ownerUserName || '-' }}</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">客户级别</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">最后跟进</span>
<span class="mobile-item-card__info-value">{{ item.contactLastTime ? dateFormatter(null, null, item.contactLastTime) : '-' }}</span>
</div>
</div>
<div class="mobile-item-card__footer" @click.stop>
<el-button
size="small"
type="primary"
@click="openForm('update', item.id)"
v-hasPermi="['crm:clue:update']"
>编辑</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(item.id)"
v-hasPermi="['crm:clue:delete']"
>删除</el-button>
</div>
</div>
<div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无线索数据</div>
</div>
<!-- 分页 -->
<div class="mobile-pagination" v-if="total > 0">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20]"
layout="total, prev, pager, next"
:pager-count="5"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
</div>
<!-- 筛选抽屉 -->
<el-drawer
v-model="filterDrawerVisible"
title="筛选条件"
direction="rtl"
size="100%"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form">
<div class="mobile-form__section">
<div class="mobile-form__section-title">筛选条件</div>
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="线索名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入线索名称" clearable />
</el-form-item>
<el-form-item label="转化状态" prop="transformStatus">
<el-select v-model="queryParams.transformStatus" style="width: 100%">
<el-option :value="false" label="未转化" />
<el-option :value="true" label="已转化" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="queryParams.mobile" placeholder="请输入手机号" clearable />
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input v-model="queryParams.telephone" placeholder="请输入电话" clearable />
</el-form-item>
</el-form>
</div>
<div class="mobile-form__footer">
<el-button @click="resetQuery" style="flex: 1">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm" style="flex: 1">确认</el-button>
</div>
</div>
</el-drawer>
<!-- 表单弹窗添加/修改 -->
<ClueForm ref="formRef" @success="getList" />
@@ -171,6 +158,7 @@ import download from '@/utils/download'
import * as ClueApi from '@/api/crm/clue'
import ClueForm from './ClueForm.vue'
import { TabsPaneContext } from 'element-plus'
import { Search, Filter } from '@element-plus/icons-vue'
defineOptions({ name: 'CrmClue' })
@@ -180,6 +168,7 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const filterDrawerVisible = ref(false) // 筛选抽屉
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@@ -263,8 +252,149 @@ const handleExport = async () => {
}
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterDrawerVisible.value = false
handleQuery()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__header {
margin-bottom: 12px;
}
.mobile-page__search {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.mobile-page__actions {
display: flex;
gap: 8px;
}
.mobile-page__tabs {
background: #fff;
border-radius: 10px;
padding: 0 12px;
margin-bottom: 12px;
}
.mobile-page__content {
min-height: 200px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&--clickable {
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f5f5f5;
}
}
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
}
</style>

View File

@@ -1,21 +1,28 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-row>
<el-col :span="12">
<el-drawer
v-model="dialogVisible"
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
>
<!-- 基本信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
<el-form-item label="客户名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入客户名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择客户来源" class="w-1/1">
<el-select v-model="formData.source" placeholder="请选择客户来源" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
@@ -24,20 +31,11 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="ownerUserId">
<el-select
v-model="formData.ownerUserId"
:disabled="formType !== 'create'"
class="w-1/1"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
@@ -47,36 +45,33 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 联系方式 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">联系方式</div>
<el-form-item label="手机" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机" />
</el-form-item>
<el-form-item label="电话" prop="telephone">
<el-input v-model="formData.telephone" placeholder="请输入电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="微信" prop="wechat">
<el-input v-model="formData.wechat" placeholder="请输入微信" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="QQ" prop="qq">
<el-input v-model="formData.qq" placeholder="请输入 QQ" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 客户信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">客户信息</div>
<el-form-item label="客户行业" prop="industryId">
<el-select v-model="formData.industryId" placeholder="请选择客户行业" class="w-1/1">
<el-select v-model="formData.industryId" placeholder="请选择客户行业" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
@@ -85,10 +80,8 @@
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择客户级别" class="w-1/1">
<el-select v-model="formData.level" placeholder="请选择客户级别" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
@@ -97,52 +90,52 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 地址信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">地址信息</div>
<el-form-item label="地址" prop="areaId">
<el-cascader
v-model="formData.areaId"
:options="areaList"
:props="defaultProps"
class="w-1/1"
style="width: 100%"
clearable
filterable
placeholder="请选择城市"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
</div>
<!-- 其他信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">其他信息</div>
<el-form-item label="下次联系时间" prop="contactNextTime">
<el-date-picker
v-model="formData.contactNextTime"
placeholder="选择下次联系时间"
type="datetime"
value-format="x"
class="!w-1/1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" />
<el-input type="textarea" v-model="formData.remark" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -258,3 +251,44 @@ const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,53 +1,73 @@
<!-- 客户导入窗口 -->
<template>
<Dialog v-model="dialogVisible" title="客户导入" width="400">
<div class="flex items-center my-10px">
<span class="mr-10px">负责人</span>
<el-select v-model="ownerUserId" class="!w-240px" clearable>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
<el-drawer
v-model="dialogVisible"
title="客户导入"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<div class="mobile-form__section">
<div class="mobile-form__section-title">导入设置</div>
<el-form label-position="top">
<el-form-item label="负责人">
<el-select v-model="ownerUserId" style="width: 100%" clearable>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mobile-form__section">
<div class="mobile-form__section-title">上传文件</div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:disabled="formLoading"
:limit="1"
:on-exceed="handleExceed"
accept=".xlsx, .xls"
action="none"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的客户数据客户名称重复
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</div>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</div>
</div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:disabled="formLoading"
:limit="1"
:on-exceed="handleExceed"
accept=".xlsx, .xls"
action="none"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的客户数据客户名称重复
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</el-drawer>
</template>
<script lang="ts" setup>
import * as CustomerApi from '@/api/crm/customer'
@@ -156,3 +176,44 @@ const importTemplate = async () => {
download.excel(res, '客户导入模版.xls')
}
</script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,219 +1,175 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="客户名称" prop="name">
<div class="mobile-page">
<!-- 搜索头部 -->
<div class="mobile-page__header">
<div class="mobile-page__search">
<el-input
v-model="queryParams.name"
class="!w-240px"
placeholder="搜索客户名称"
clearable
placeholder="请输入客户名称"
@keyup.enter="handleQuery"
:prefix-icon="Search"
/>
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input
v-model="queryParams.mobile"
class="!w-240px"
clearable
placeholder="请输入手机"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select
v-model="queryParams.industryId"
class="!w-240px"
clearable
placeholder="请选择所属行业"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select
v-model="queryParams.level"
class="!w-240px"
clearable
placeholder="请选择客户级别"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select
v-model="queryParams.source"
class="!w-240px"
clearable
placeholder="请选择客户来源"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
<el-button type="primary" :icon="Filter" @click="filterDrawerVisible = true" />
</div>
<div class="mobile-page__actions">
<el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:customer:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button v-hasPermi="['crm:customer:import']" plain type="warning" @click="handleImport">
<Icon icon="ep:upload" />
<el-button type="warning" plain @click="handleImport" v-hasPermi="['crm:customer:import']">
导入
</el-button>
<el-button
v-hasPermi="['crm:customer:export']"
:loading="exportLoading"
plain
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:customer:export']"
>
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</div>
</div>
<!-- 列表 -->
<ContentWrap>
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" fixed="left" label="客户名称" prop="name" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column align="center" label="客户来源" prop="source" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column align="center" label="手机" prop="mobile" width="120" />
<el-table-column align="center" label="电话" prop="telephone" width="130" />
<el-table-column align="center" label="邮箱" prop="email" width="180" />
<el-table-column align="center" label="客户级别" prop="level" width="135">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
<el-table-column align="center" label="锁定状态" prop="lockStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
</template>
</el-table-column>
<el-table-column align="center" label="成交状态" prop="dealStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="最后跟进时间"
prop="contactLastTime"
width="180px"
/>
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" />
<el-table-column align="center" label="地址" prop="detailAddress" width="180" />
<el-table-column align="center" label="距离进入公海天数" prop="poolDay" width="140">
<template #default="scope"> {{ scope.row.poolDay }} </template>
</el-table-column>
<el-table-column align="center" label="负责人" prop="ownerUserName" width="100px" />
<el-table-column align="center" label="所属部门" prop="ownerUserDeptName" width="100px" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="updateTime"
width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
<el-table-column align="center" fixed="right" label="操作" min-width="150">
<template #default="scope">
<el-button
v-hasPermi="['crm:customer:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['crm:customer:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- Tab 切换 -->
<div class="mobile-page__tabs">
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane label="我负责的" name="1" />
<el-tab-pane label="我参与的" name="2" />
<el-tab-pane label="下属负责的" name="3" />
</el-tabs>
</div>
<!-- 客户列表 -->
<div class="mobile-page__content" v-loading="loading">
<div class="mobile-item-list">
<div
v-for="item in list"
:key="item.id"
class="mobile-item-card mobile-item-card--clickable"
@click="openDetail(item.id)"
>
<div class="mobile-item-card__header">
<span class="mobile-item-card__name">{{ item.name }}</span>
<el-tag v-if="item.dealStatus" type="success" size="small">已成交</el-tag>
<el-tag v-else type="info" size="small">未成交</el-tag>
</div>
<div class="mobile-item-card__body">
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">客户来源</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="item.source" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">手机</span>
<span class="mobile-item-card__info-value">{{ item.mobile || '-' }}</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">负责人</span>
<span class="mobile-item-card__info-value">{{ item.ownerUserName || '-' }}</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">客户级别</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">最后跟进</span>
<span class="mobile-item-card__info-value">{{ item.contactLastTime ? dateFormatter(null, null, item.contactLastTime) : '-' }}</span>
</div>
</div>
<div class="mobile-item-card__footer" @click.stop>
<el-button
size="small"
type="primary"
@click="openForm('update', item.id)"
v-hasPermi="['crm:customer:update']"
>编辑</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(item.id)"
v-hasPermi="['crm:customer:delete']"
>删除</el-button>
</div>
</div>
<div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无客户数据</div>
</div>
<!-- 分页 -->
<div class="mobile-pagination" v-if="total > 0">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20]"
layout="total, prev, pager, next"
:pager-count="5"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
</div>
<!-- 筛选抽屉 -->
<el-drawer
v-model="filterDrawerVisible"
title="筛选条件"
direction="rtl"
size="100%"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form">
<div class="mobile-form__section">
<div class="mobile-form__section-title">筛选条件</div>
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="客户名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入客户名称" clearable />
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input v-model="queryParams.mobile" placeholder="请输入手机" clearable />
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select v-model="queryParams.industryId" placeholder="请选择所属行业" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select v-model="queryParams.level" placeholder="请选择客户级别" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select v-model="queryParams.source" placeholder="请选择客户来源" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mobile-form__footer">
<el-button @click="resetQuery" style="flex: 1">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm" style="flex: 1">确认</el-button>
</div>
</div>
</el-drawer>
<!-- 表单弹窗添加/修改 -->
<CustomerForm ref="formRef" @success="getList" />
@@ -228,6 +184,7 @@ import * as CustomerApi from '@/api/crm/customer'
import CustomerForm from './CustomerForm.vue'
import CustomerImportForm from './CustomerImportForm.vue'
import { TabsPaneContext } from 'element-plus'
import { Search, Filter } from '@element-plus/icons-vue'
defineOptions({ name: 'CrmCustomer' })
@@ -237,6 +194,7 @@ const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const filterDrawerVisible = ref(false) // 筛选抽屉
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@@ -328,6 +286,12 @@ const handleExport = async () => {
}
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterDrawerVisible.value = false
handleQuery()
}
/** 监听路由变化更新列表 */
watch(
() => currentRoute.value,
@@ -341,3 +305,138 @@ onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__header {
margin-bottom: 12px;
}
.mobile-page__search {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.mobile-page__actions {
display: flex;
gap: 8px;
}
.mobile-page__tabs {
background: #fff;
border-radius: 10px;
padding: 0 12px;
margin-bottom: 12px;
}
.mobile-page__content {
min-height: 200px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&--clickable {
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f5f5f5;
}
}
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
}
</style>

View File

@@ -1,57 +1,72 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="200px"
v-loading="formLoading"
>
<el-form-item label="规则适用人群" prop="userIds">
<el-select multiple filterable v-model="formData.userIds">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="规则适用部门" prop="deptIds">
<el-tree-select
v-model="formData.deptIds"
:data="deptTree"
:props="defaultProps"
multiple
filterable
check-strictly
node-key="id"
placeholder="请选择规则适用部门"
/>
</el-form-item>
<el-form-item
:label="
formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT
? '拥有客户数上限'
: '锁定客户数上限'
"
prop="maxCount"
<el-drawer
v-model="dialogVisible"
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
>
<el-input-number v-model="formData.maxCount" placeholder="请输入数量上限" />
</el-form-item>
<el-form-item
label="成交客户是否占用拥有客户数"
v-if="formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
prop="dealCountEnabled"
>
<el-switch v-model="formData.dealCountEnabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<div class="mobile-form__section">
<div class="mobile-form__section-title">规则设置</div>
<el-form-item label="规则适用人群" prop="userIds">
<el-select multiple filterable v-model="formData.userIds" style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="规则适用部门" prop="deptIds">
<el-tree-select
v-model="formData.deptIds"
:data="deptTree"
:props="defaultProps"
multiple
filterable
check-strictly
node-key="id"
placeholder="请选择规则适用部门"
style="width: 100%"
/>
</el-form-item>
<el-form-item
:label="
formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT
? '拥有客户数上限'
: '锁定客户数上限'
"
prop="maxCount"
>
<el-input-number v-model="formData.maxCount" placeholder="请输入数量上限" style="width: 100%" />
</el-form-item>
<el-form-item
label="成交客户是否占用拥有客户数"
v-if="formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
prop="dealCountEnabled"
>
<el-switch v-model="formData.dealCountEnabled" />
</el-form-item>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
@@ -148,3 +163,44 @@ const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,84 +1,83 @@
<template>
<el-button plain @click="handleQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 刷新 </el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['crm:customer-limit-config:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
class="mt-4"
>
<el-table-column label="编号" align="center" prop="id" />
<el-table-column
label="规则适用人群"
align="center"
:formatter="(row) => row.users?.map((user: any) => user.nickname).join('')"
/>
<el-table-column
label="规则适用部门"
align="center"
:formatter="(row) => row.depts?.map((dept: any) => dept.name).join('')"
/>
<el-table-column
:label="
confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT ? '拥有客户数上限' : '锁定客户数上限'
"
align="center"
prop="maxCount"
/>
<el-table-column
v-if="confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
label="成交客户是否占用拥有客户数"
align="center"
prop="dealCountEnabled"
min-width="100"
>
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealCountEnabled" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['crm:customer-limit-config:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['crm:customer-limit-config:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<div class="mobile-list-page">
<!-- 操作按钮 -->
<div class="mobile-list-page__actions">
<el-button plain size="small" @click="handleQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 刷新
</el-button>
<el-button
type="primary"
size="small"
@click="openForm('create')"
v-hasPermi="['crm:customer-limit-config:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</div>
<!-- 列表 -->
<div class="mobile-item-list" v-loading="loading">
<div
v-for="item in list"
:key="item.id"
class="mobile-item-card"
>
<div class="mobile-item-card__header">
<span class="mobile-item-card__name">规则 #{{ item.id }}</span>
<el-tag type="primary" size="small">上限 {{ item.maxCount }}</el-tag>
</div>
<div class="mobile-item-card__body">
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">适用人群</span>
<span class="mobile-item-card__info-value">{{ item.users?.map((user: any) => user.nickname).join('') || '-' }}</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">适用部门</span>
<span class="mobile-item-card__info-value">{{ item.depts?.map((dept: any) => dept.name).join('') || '-' }}</span>
</div>
<div class="mobile-item-card__info-row" v-if="confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT">
<span class="mobile-item-card__info-label">成交客户占用</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.dealCountEnabled" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">创建时间</span>
<span class="mobile-item-card__info-value">{{ dateFormatter(null, null, item.createTime) }}</span>
</div>
</div>
<div class="mobile-item-card__footer">
<el-button
size="small"
type="primary"
@click="openForm('update', item.id)"
v-hasPermi="['crm:customer-limit-config:update']"
>编辑</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(item.id)"
v-hasPermi="['crm:customer-limit-config:delete']"
>删除</el-button>
</div>
</div>
<div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无配置数据</div>
</div>
<!-- 分页 -->
<div class="mobile-pagination" v-if="total > 0">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20]"
layout="total, prev, pager, next"
:pager-count="5"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
<!-- 表单弹窗添加/修改 -->
<CustomerLimitConfigForm ref="formRef" @success="getList" />
@@ -148,3 +147,79 @@ onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.mobile-list-page {
padding: 12px 0;
}
.mobile-list-page__actions {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
word-break: break-all;
}
&__footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
</style>

View File

@@ -1,22 +1,36 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<!-- 列表 -->
<ContentWrap>
<el-tabs>
<el-tab-pane label="拥有客户数限制">
<CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" />
</el-tab-pane>
<el-tab-pane label="锁定客户数限制">
<CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
<div class="mobile-page">
<!-- Tab 切换 -->
<div class="mobile-page__tabs">
<el-tabs v-model="activeTab">
<el-tab-pane label="拥有客户数限制" name="quantity">
<CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" />
</el-tab-pane>
<el-tab-pane label="锁定客户数限制" name="lock">
<CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import CustomerLimitConfigList from './CustomerLimitConfigList.vue'
import { LimitConfType } from '@/api/crm/customer/limitConfig'
defineOptions({ name: 'CrmCustomerLimitConfig' })
const activeTab = ref('quantity')
</script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__tabs {
background: #fff;
border-radius: 10px;
padding: 0 12px;
}
</style>

View File

@@ -1,28 +1,42 @@
<template>
<Dialog v-model="dialogVisible" title="分配客户">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="负责人" prop="ownerUserId">
<el-select v-model="formData.ownerUserId" class="w-1/1">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<el-drawer
v-model="dialogVisible"
title="分配客户"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
>
<div class="mobile-form__section">
<div class="mobile-form__section-title">分配设置</div>
<el-form-item label="负责人" prop="ownerUserId">
<el-select v-model="formData.ownerUserId" style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
</el-form>
<!-- 底部操作按钮 -->
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</div>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import * as CustomerApi from '@/api/crm/customer'
@@ -83,3 +97,44 @@ const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
.el-button {
flex: 1;
height: 40px;
font-size: 15px;
}
}
</style>

View File

@@ -1,175 +1,142 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="客户名称" prop="name">
<div class="mobile-page">
<!-- 搜索头部 -->
<div class="mobile-page__header">
<div class="mobile-page__search">
<el-input
v-model="queryParams.name"
class="!w-240px"
placeholder="搜索客户名称"
clearable
placeholder="请输入客户名称"
@keyup.enter="handleQuery"
:prefix-icon="Search"
/>
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input
v-model="queryParams.mobile"
class="!w-240px"
clearable
placeholder="请输入手机"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select
v-model="queryParams.industryId"
class="!w-240px"
clearable
placeholder="请选择所属行业"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select
v-model="queryParams.level"
class="!w-240px"
clearable
placeholder="请选择客户级别"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select
v-model="queryParams.source"
class="!w-240px"
clearable
placeholder="请选择客户来源"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery(undefined)">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button type="primary" :icon="Filter" @click="filterDrawerVisible = true" />
</div>
<div class="mobile-page__actions">
<el-button
v-hasPermi="['crm:customer:export']"
:loading="exportLoading"
plain
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['crm:customer:export']"
>
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</div>
</div>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="客户名称" fixed="left" prop="name" width="160">
<template #default="scope">
<el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
{{ scope.row.name }}
</el-link>
</template>
</el-table-column>
<el-table-column align="center" label="客户来源" prop="source" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
</template>
</el-table-column>
<el-table-column label="手机" align="center" prop="mobile" width="120" />
<el-table-column label="电话" align="center" prop="telephone" width="130" />
<el-table-column label="邮箱" align="center" prop="email" width="180" />
<el-table-column align="center" label="客户级别" prop="level" width="135">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column align="center" label="客户行业" prop="industryId" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="下次联系时间"
prop="contactNextTime"
width="180px"
/>
<el-table-column align="center" label="备注" prop="remark" width="200" />
<el-table-column align="center" label="成交状态" prop="dealStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="最后跟进时间"
prop="contactLastTime"
width="180px"
/>
<el-table-column align="center" label="最后跟进记录" prop="contactLastContent" width="200" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="updateTime"
width="180px"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180px"
/>
<el-table-column align="center" label="创建人" prop="creatorName" width="100px" />
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 公海客户列表 -->
<div class="mobile-page__content" v-loading="loading">
<div class="mobile-item-list">
<div
v-for="item in list"
:key="item.id"
class="mobile-item-card mobile-item-card--clickable"
@click="openDetail(item.id)"
>
<div class="mobile-item-card__header">
<span class="mobile-item-card__name">{{ item.name }}</span>
<el-tag v-if="item.dealStatus" type="success" size="small">已成交</el-tag>
<el-tag v-else type="info" size="small">未成交</el-tag>
</div>
<div class="mobile-item-card__body">
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">客户来源</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="item.source" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">手机</span>
<span class="mobile-item-card__info-value">{{ item.mobile || '-' }}</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">客户级别</span>
<span class="mobile-item-card__info-value">
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="item.level" />
</span>
</div>
<div class="mobile-item-card__info-row">
<span class="mobile-item-card__info-label">最后跟进</span>
<span class="mobile-item-card__info-value">{{ item.contactLastTime ? dateFormatter(null, null, item.contactLastTime) : '-' }}</span>
</div>
</div>
</div>
<div v-if="list.length === 0 && !loading" class="mobile-empty-tip">暂无公海客户数据</div>
</div>
<!-- 分页 -->
<div class="mobile-pagination" v-if="total > 0">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20]"
layout="total, prev, pager, next"
:pager-count="5"
@size-change="getList"
@current-change="getList"
/>
</div>
</div>
</div>
<!-- 筛选抽屉 -->
<el-drawer
v-model="filterDrawerVisible"
title="筛选条件"
direction="rtl"
size="100%"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form">
<div class="mobile-form__section">
<div class="mobile-form__section-title">筛选条件</div>
<el-form :model="queryParams" ref="queryFormRef" label-position="top">
<el-form-item label="客户名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入客户名称" clearable />
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input v-model="queryParams.mobile" placeholder="请输入手机" clearable />
</el-form-item>
<el-form-item label="所属行业" prop="industryId">
<el-select v-model="queryParams.industryId" placeholder="请选择所属行业" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户级别" prop="level">
<el-select v-model="queryParams.level" placeholder="请选择客户级别" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户来源" prop="source">
<el-select v-model="queryParams.source" placeholder="请选择客户来源" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mobile-form__footer">
<el-button @click="resetQuery" style="flex: 1">重置</el-button>
<el-button type="primary" @click="handleFilterConfirm" style="flex: 1">确认</el-button>
</div>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
@@ -177,6 +144,7 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as CustomerApi from '@/api/crm/customer'
import { Search, Filter } from '@element-plus/icons-vue'
defineOptions({ name: 'CrmCustomerPool' })
@@ -185,6 +153,7 @@ const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const filterDrawerVisible = ref(false) // 筛选抽屉
const queryParams = ref({
pageNo: 1,
pageSize: 10,
@@ -255,6 +224,12 @@ const handleExport = async () => {
}
}
/** 筛选确认 */
const handleFilterConfirm = () => {
filterDrawerVisible.value = false
handleQuery()
}
/** 监听路由变化更新列表 */
watch(
() => currentRoute.value,
@@ -268,3 +243,124 @@ onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-page__header {
margin-bottom: 12px;
}
.mobile-page__search {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.mobile-page__actions {
display: flex;
gap: 8px;
}
.mobile-page__content {
min-height: 200px;
}
.mobile-item-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-item-card {
background: #fff;
border-radius: 10px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&--clickable {
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f5f5f5;
}
}
&__header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
&__name {
flex: 1;
font-size: 15px;
font-weight: 600;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__body {
font-size: 13px;
}
&__info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
}
&__info-label {
color: #909399;
flex-shrink: 0;
}
&__info-value {
color: #606266;
text-align: right;
}
}
.mobile-empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.mobile-pagination {
margin-top: 12px;
display: flex;
justify-content: center;
:deep(.el-pagination) {
flex-wrap: wrap;
justify-content: center;
}
}
.mobile-form {
padding: 0 4px;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 12px 16px;
padding-bottom: calc(12px + constant(safe-area-inset-bottom));
padding-bottom: calc(12px + env(safe-area-inset-bottom));
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
z-index: 10;
margin: 0 -4px;
}
</style>

View File

@@ -1,62 +1,55 @@
<template>
<doc-alert title="【客户】客户管理、公海客户" url="https://doc.iocoder.cn/crm/customer/" />
<doc-alert title="【通用】数据权限" url="https://doc.iocoder.cn/crm/permission/" />
<ContentWrap>
<div class="mobile-page" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="160px"
v-loading="formLoading"
label-position="top"
>
<el-card shadow="never">
<!-- 操作 -->
<template #header>
<div class="flex items-center justify-between">
<CardTitle title="客户公海规则设置" />
<el-button
type="primary"
@click="onSubmit"
v-hasPermi="['crm:customer-pool-config:update']"
>
保存
</el-button>
</div>
</template>
<!-- 表单 -->
<el-form-item label="客户公海规则设置" prop="enabled">
<el-radio-group v-model="formData.enabled" @change="changeEnable" class="ml-4">
<el-radio :value="false" size="large">不启用</el-radio>
<el-radio :value="true" size="large">启用</el-radio>
<!-- 公海规则设置 -->
<div class="mobile-form__section">
<div class="mobile-form__section-header">
<span class="mobile-form__section-title">客户公海规则设置</span>
<el-button
type="primary"
size="small"
@click="onSubmit"
v-hasPermi="['crm:customer-pool-config:update']"
>
保存
</el-button>
</div>
<el-form-item label="客户公海规则" prop="enabled">
<el-radio-group v-model="formData.enabled" @change="changeEnable">
<el-radio :value="false">不启用</el-radio>
<el-radio :value="true">启用</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.enabled">
<el-form-item>
<el-input-number class="mr-2" v-model="formData.contactExpireDays" />
天不跟进或
<el-input-number class="mx-2" v-model="formData.dealExpireDays" />
天未成交
</el-form-item>
<el-form-item label="提前提醒设置" prop="notifyEnabled">
<el-radio-group
v-model="formData.notifyEnabled"
@change="changeNotifyEnable"
class="ml-4"
>
<el-radio :value="false" size="large">不提醒</el-radio>
<el-radio :value="true" size="large">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="formData.notifyEnabled">
<el-form-item>
提前 <el-input-number class="mx-2" v-model="formData.notifyDays" /> 天提醒
</el-form-item>
</div>
</div>
<!-- 规则详情 -->
<div class="mobile-form__section" v-if="formData.enabled">
<div class="mobile-form__section-title">规则详情</div>
<div class="mobile-form__inline-group">
<el-input-number v-model="formData.contactExpireDays" :min="1" style="width: 80px" />
<span class="mobile-form__inline-text">天不跟进或</span>
<el-input-number v-model="formData.dealExpireDays" :min="1" style="width: 80px" />
<span class="mobile-form__inline-text">天未成交</span>
</div>
</el-card>
<el-form-item label="提前提醒设置" prop="notifyEnabled" style="margin-top: 16px">
<el-radio-group v-model="formData.notifyEnabled" @change="changeNotifyEnable">
<el-radio :value="false">不提醒</el-radio>
<el-radio :value="true">提醒</el-radio>
</el-radio-group>
</el-form-item>
<div class="mobile-form__inline-group" v-if="formData.notifyEnabled">
<span class="mobile-form__inline-text">提前</span>
<el-input-number v-model="formData.notifyDays" :min="1" style="width: 80px" />
<span class="mobile-form__inline-text">天提醒</span>
</div>
</div>
</el-form>
</ContentWrap>
</div>
</template>
<script setup lang="ts">
import * as CustomerPoolConfigApi from '@/api/crm/customer/poolConfig'
@@ -134,3 +127,42 @@ onMounted(() => {
getConfig()
})
</script>
<style lang="scss" scoped>
.mobile-page {
padding: 12px;
background: #f5f7fa;
min-height: 100vh;
}
.mobile-form__section {
background: #fff;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.mobile-form__section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.mobile-form__inline-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.mobile-form__inline-text {
color: #606266;
font-size: 14px;
}
</style>