Commit c890936c by Sweet Zhang

新单跟进功能对接

parent 5bb6124d
{
"name": "ruoyi",
"name": "csf",
"version": "3.9.0",
"description": "CSF-CORE",
"author": "若依",
"author": "csf",
"license": "MIT",
"type": "module",
"scripts": {
......@@ -15,17 +15,17 @@
},
"repository": {
"type": "git",
"url": "https://gitee.com/y_project/RuoYi-Vue.git"
"url": ""
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@element-plus/icons-vue": "^2.3.1",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
"clipboard": "2.0.11",
"dayjs": "^1.11.18",
"echarts": "5.6.0",
"element-plus": "2.9.9",
"element-plus": "^2.9.9",
"file-saver": "2.0.5",
"fuse.js": "6.6.2",
"js-beautify": "1.14.11",
......@@ -37,7 +37,8 @@
"vue": "3.5.16",
"vue-cropper": "1.1.1",
"vue-router": "4.5.1",
"vuedraggable": "4.1.0"
"vuedraggable": "4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.2.4",
......
import request from '@/utils/request'
// 分页获取新单跟进列表
export function getPolicyFollowList(data) {
return request({
url: '/csf/api/policy_follow/list/page/vo',
method: 'post',
data: data
})
}
\ No newline at end of file
......@@ -3,9 +3,9 @@ import request from '@/utils/request'
// 查询字典类型列表
export function listType(query) {
return request({
url: '/system/dict/type/list',
method: 'get',
params: query
url: '/user/api/sysDict/type/list',
method: 'post',
data: query
})
}
......
<template>
<div class="data-management-page">
<!-- 页面标题 -->
<el-page-header
@back="handleBack"
content="新单跟进"
class="page-header"
/>
<!-- 查询区域 -->
<el-card class="search-card">
<!-- 第一行筛选条件 -->
<el-row :gutter="20" class="search-row">
<el-col :span="8">
<div class="form-item">
<label class="form-label">新单编号</label>
<el-input
v-model="searchForm.newOrderNo"
placeholder="请输入"
clearable
size="medium"
@keyup.enter="handleSearch"
/>
</div>
</el-col>
<el-col :span="8">
<div class="form-item">
<label class="form-label">保单编号</label>
<el-input
v-model="searchForm.policyNo"
placeholder="请输入"
clearable
size="medium"
@keyup.enter="handleSearch"
/>
</div>
</el-col>
<el-col :span="8">
<div class="form-item">
<label class="form-label">客户姓名</label>
<el-input
v-model="searchForm.customerName"
placeholder="请输入"
clearable
size="medium"
@keyup.enter="handleSearch"
/>
</div>
</el-col>
</el-row>
<!-- 第二行筛选条件 -->
<el-row :gutter="20" class="search-row">
<el-col :span="8">
<div class="form-item">
<label class="form-label">客户编号</label>
<el-input
v-model="searchForm.customerNo"
placeholder="请输入"
clearable
size="medium"
@keyup.enter="handleSearch"
/>
</div>
</el-col>
<el-col :span="8">
<div class="form-item">
<label class="form-label">签单时间</label>
<el-date-picker
v-model="searchForm.signDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="medium"
/>
</div>
</el-col>
<el-col :span="8">
<div class="form-item">
<label class="form-label">新单状态</label>
<el-select
v-model="searchForm.newOrderStatus"
placeholder="请选择"
clearable
size="medium"
>
<!-- 增加全部,默认传空字符串 -->
<el-option label="全部" value=" " />
<el-option v-for="item in policyFollowStatusList" :key="item.itemValue" :label="item.itemLabel" :value="item.itemValue" />
</el-select>
</div>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-buttons">
<el-button
type="primary"
@click="handleSearch"
size="medium"
:icon="Search"
class="search-btn"
>
查询
</el-button>
<el-button
@click="resetForm"
size="medium"
:icon="RefreshLeft"
>
重置
</el-button>
<el-button
type="info"
@click="toggleAdvancedSearch"
size="medium"
:icon="expandSearch ? ArrowUp : ArrowDown"
>
{{ expandSearch ? '收起筛选' : '更多筛选' }}
</el-button>
</el-col>
</el-row>
<!-- 可展开的高级筛选区域 -->
<el-collapse v-model="activeNames" v-if="expandSearch" class="advanced-search">
<el-collapse-item name="1" title="高级筛选">
<el-row :gutter="20">
<el-col :span="8">
<div class="form-item">
<label class="form-label">产品类型</label>
<el-select
v-model="searchForm.productType"
placeholder="请选择"
clearable
size="small"
>
<el-option label="全部" value="" />
<el-option label="寿险" value="life" />
<el-option label="健康险" value="health" />
<el-option label="意外险" value="accident" />
<el-option label="年金险" value="annuity" />
</el-select>
</div>
</el-col>
<el-col :span="8">
<div class="form-item">
<label class="form-label">业务员</label>
<el-select
v-model="searchForm.salesman"
placeholder="请选择"
clearable
size="small"
>
<el-option label="全部" value="" />
<el-option label="张三" value="zhangsan" />
<el-option label="李四" value="lisi" />
<el-option label="王五" value="wangwu" />
</el-select>
</div>
</el-col>
<el-col :span="8">
<div class="form-item">
<label class="form-label">保费金额</label>
<el-input
v-model="searchForm.premium"
placeholder="请输入"
clearable
size="small"
/>
</div>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
</el-card>
<!-- Excel导入区域 -->
<div class="import-area">
<el-card class="import-card">
<div class="import-content">
<div class="import-actions">
<el-upload
class="upload-excel"
:auto-upload="false"
:on-change="handleFileChange"
:show-file-list="false"
accept=".xlsx, .xls"
>
<el-button
type="success"
:icon="UploadFilled"
size="medium"
>
上传Excel文件
</el-button>
</el-upload>
<el-button
text
@click="downloadTemplate"
size="small"
class="download-template-btn"
>
下载模板
</el-button>
</div>
<!-- 文件信息显示 -->
<div v-if="selectedFile" class="file-info">
<div class="file-info-content">
<el-icon><document /></el-icon>
<span class="file-name">{{ selectedFile.name }}</span>
<span class="file-size">({{ formatFileSize(selectedFile.size) }})</span>
</div>
<el-button
type="primary"
@click="handleImport"
size="medium"
:loading="importLoading"
class="confirm-import-btn"
>
确认导入
</el-button>
</div>
</div>
</el-card>
</div>
<!-- 列表区域 -->
<el-card class="table-card">
<!-- <div class="table-actions">
<el-button
type="danger"
:icon="Delete"
size="small"
@click="handleDeleteSelected"
:disabled="selectedRows.length === 0"
>
批量删除
</el-button>
<el-button
type="primary"
:icon="Plus"
size="small"
@click="handleAdd"
>
新增
</el-button>
</div> -->
<el-table
v-loading="tableLoading"
:data="tableData"
border
style="width: 100%"
height="350"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
:row-class-name="tableRowClassName"
>
<el-table-column type="selection" width="55" align="center" />
<!-- 新单状态需要通过policyFollowStatusList和value匹配,显示label -->
<el-table-column prop="status" label="新单状态" min-width="100" align="center" sortable>
<template #default="scope">
<span>{{ policyFollowStatusList.value?.find(item => item.itemValue == scope.row.status)?.itemLabel || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="policyNo" label="保单号" min-width="100" align="center" sortable/>
<el-table-column prop="customerName" label="客户名称" min-width="100" align="center" sortable/>
<el-table-column prop="signDate" label="签单日期" min-width="100" align="center" sortable/>
<el-table-column prop="signer" label="签单人" min-width="100" align="center" sortable/>
<el-table-column prop="paymentTerm" label="供款年期" min-width="100" align="center" sortable/>
<el-table-column prop="paymentPremium" label="期交保费" min-width="100" align="center" sortable/>
<el-table-column prop="productName" label="产品名称" min-width="100" align="center" sortable/>
<el-table-column prop="insurer " label="保险公司" min-width="100" align="center" sortable/>
<el-table-column prop="reconciliationCompany" label="对账公司" min-width="100" align="center" sortable/>
<el-table-column prop="policyHolder" label="保单持有人" min-width="100" align="center" sortable/>
<el-table-column prop="insured" label="受保人" min-width="100" align="center" sortable/>
<el-table-column prop="currency" label="币种" min-width="100" align="center" sortable/>
<el-table-column prop="initialPremium" label="首期保费" min-width="100" align="center" sortable/>
<!-- 转介人信息列 -->
<el-table-column label="转介人信息" min-width="150" align="center" sortable>
<template #default="scope">
<div v-if="scope.row.brokerList && scope.row.brokerList.length > 0">
<el-popover
placement="top-start"
title="转介人详情"
:width="300"
trigger="hover"
>
<template #reference>
<el-button text size="small" type="primary">
查看转介人({{ scope.row.brokerList.length }})
</el-button>
</template>
<div class="broker-details">
<div
v-for="(broker, index) in scope.row.brokerList"
:key="index"
class="broker-item"
>
<p><strong>姓名:</strong> {{ broker.brokerName || '未填写' }}</p>
<p><strong>团队:</strong> {{ broker.team || '未填写' }}</p>
<p><strong>备注:</strong> {{ broker.remark || '无' }}</p>
<el-divider v-if="index < scope.row.brokerList.length - 1" />
</div>
</div>
</el-popover>
</div>
<span v-else class="no-broker">无转介人</span>
</template>
</el-table-column>
<!-- <el-table-column
label="操作"
min-width="180"
align="center"
>
<template #default="scope">
<el-button
text
size="small"
@click="handleView(scope.row)"
>
查看
</el-button>
<el-button
text
size="small"
type="primary"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
text
size="small"
type="danger"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column> -->
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { Search, RefreshLeft, UploadFilled, Delete, Plus, Document } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import * as XLSX from 'xlsx'
import axios from 'axios'
import { getPolicyFollowList } from '@/api/sign/underwritingMain'
import { getToken } from "@/utils/auth"
import { listType } from '@/api/system/dict/type'
// 通过dictType=csf_policy_follow_status获取新单状态字典值,获取对象中的dictItemList
const policyFollowStatusList = ref([]);
const getLists = ()=>{
listType({typeList: ['csf_policy_follow_status']}).then(res => {
if (res.code === 200 && res.data) {
const statusData = res.data.find(item => item.dictType === 'csf_policy_follow_status');
policyFollowStatusList.value = statusData?.dictItemList || [];
} else {
policyFollowStatusList.value = [];
}
}).catch(error => {
console.error('获取状态列表失败:', error);
policyFollowStatusList.value = [];
})
}
// const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/csf/api/policy_follow/upload/excel')
const uploadUrl = ref('http://139.224.145.34:9002/csf/api/policy_follow/upload/excel')
// 搜索表单数据
const searchForm = reactive({
policyBizId: '', // 新单编号(对应接口的policyBizId)
policyNo: '', // 保单编号
customerName: '', // 客户姓名
customerBizId: '', // 客户编号(对应接口的customerBizId)
signDateRange: [], // 签单时间范围
status: '', // 新单状态(对应接口的status)
// 高级筛选字段
insurer : '', // 保险公司
productCode: '' // 产品代码(对应接口的productCode)
})
// 控制高级筛选的展开/收起
const expandSearch = ref(false)
const activeNames = ref(['1'])
// 切换高级筛选显示状态
const toggleAdvancedSearch = () => {
expandSearch.value = !expandSearch.value
}
// 表格数据
const tableData = ref([])
const tableLoading = ref(false)
const selectedRows = ref([])
// 分页数据
const pagination = reactive({
currentPage: 1, // 当前页码(对应接口的pageNo)
pageSize: 10, // 每页条数(对应接口的pageSize)
total: 0, // 总条数
sortField: '', // 排序字段
sortOrder: '' // 排序方向
})
// Excel导入相关
const selectedFile = ref(null)
const importLoading = ref(false)
// 页面加载时获取数据
onMounted(() => {
fetchTableData();
getLists();
})
// 获取列表数据
const fetchTableData = async () => {
tableLoading.value = true
try {
// 构造接口请求参数
const params = {
pageNo: pagination.currentPage, // 注意:如果后端是从0开始的页码,需要减1
pageSize: pagination.pageSize,
sortField: pagination.sortField,
sortOrder: pagination.sortOrder,
status: searchForm.status,
policyBizId: searchForm.policyBizId,
policyNo: searchForm.policyNo,
customerName: searchForm.customerName,
customerBizId: searchForm.customerBizId,
insurer : searchForm.insurer,
productCode: searchForm.productCode,
// 签单时间范围需要根据后端要求的参数名进行调整
// 例如:如果后端需要startSignDate和endSignDate
...(searchForm.signDateRange && searchForm.signDateRange.length === 2 && {
startSignDate: searchForm.signDateRange[0],
endSignDate: searchForm.signDateRange[1]
})
}
// 调用后台接口
const response = await getPolicyFollowList(params)
// 处理接口响应
if (response.code === 200) {
const result = response.data
// 将接口返回的数据映射到表格
tableData.value = result.records.map(record => ({
...record
}))
console.log('tableData',tableData.value)
// 更新分页信息
pagination.total = result.total
pagination.currentPage = result.current || pagination.currentPage
pagination.pageSize = result.size || pagination.pageSize
} else {
// 接口返回错误信息
ElMessage.error(`获取数据失败: ${response.data.msg || '未知错误'}`)
tableData.value = []
pagination.total = 0
}
} catch (error) {
// 捕获网络或其他异常
console.error('请求失败:', error)
ElMessage.error('网络异常,请稍后重试')
tableData.value = []
pagination.total = 0
} finally {
tableLoading.value = false
}
}
// 增加通过表格排序,排序字段sortField,升降序sortOrder
const handleSortChange = (column) => {
pagination.sortField = column.prop
pagination.sortOrder = column.order === 'ascending' ? 'ascend' : 'descend'
fetchTableData()
}
// 处理查询
const handleSearch = () => {
pagination.currentPage = 1
fetchTableData()
ElMessage.success('查询成功')
}
// 重置表单
const resetForm = () => {
searchForm.name = ''
searchForm.status = ''
searchForm.dateRange = []
}
// 处理分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
fetchTableData()
}
// 处理分页页码变化
const handleCurrentChange = (val) => {
pagination.currentPage = val
fetchTableData()
}
// 处理表格选择变化
const handleSelectionChange = (rows) => {
selectedRows.value = rows
}
// 表格行样式
const tableRowClassName = ({ row }) => {
return row.status === 'inactive' ? 'row-inactive' : ''
}
// 处理文件选择
const handleFileChange = (file) => {
selectedFile.value = file.raw
ElMessage.success(`已选择文件: ${file.name}`)
}
// 处理导入 - 调整为multipart/form-data格式
const handleImport = async () => {
if (!selectedFile.value) {
ElMessage.warning('请先选择文件')
return
}
// 二次确认
ElMessageBox.confirm(
`确定要导入"${selectedFile.value.name}"吗?`,
'导入确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
).then(async () => {
importLoading.value = true
try {
// 创建FormData对象,用于multipart/form-data格式
const formData = new FormData()
// 将文件添加到FormData,注意参数名要与后端保持一致
formData.append('file', selectedFile.value)
// 可以添加其他参数(如果后端需要)
// formData.append('otherParam', 'value')
// 调用上传接口
const response = await axios.post(
'http://139.224.145.34:9002/csf/api/policy_follow/upload/excel',
formData,
{
headers: {
'Content-Type': 'multipart/form-data', // 指定内容类型,
'Authorization': 'Bearer ' + getToken()
}
}
)
// 处理接口响应
if (response.data.code === 200) {
ElMessage.success(`导入成功,共 ${response.data?.successCount || 0} 条数据`)
selectedFile.value = null
// 重新获取表格数据
fetchTableData()
} else {
ElMessage.error(`导入失败: ${response.data.msg || '未知错误'}`)
// 如果有错误详情,可以展示
if (response.data.data?.errorMsg) {
console.error('导入错误详情:', response.data.errorMsg)
}
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('文件上传失败,请稍后重试')
} finally {
importLoading.value = false
}
}).catch(() => {
// 用户取消导入
ElMessage.info('已取消导入')
})
}
// 下载模板
const downloadTemplate = () => {
// 模拟模板数据
const templateData = [
{ name: '示例1', category: '类别A', status: 'active' },
{ name: '示例2', category: '类别B', status: 'inactive' }
]
const worksheet = XLSX.utils.json_to_sheet(templateData)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, '数据模板')
// 生成并下载文件
XLSX.writeFile(workbook, '数据导入模板.xlsx')
ElMessage.success('模板下载成功')
}
// // 处理批量删除
// const handleDeleteSelected = () => {
// ElMessageBox.confirm(
// `确定要删除选中的 ${selectedRows.value.length} 条数据吗?`,
// '删除确认',
// {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// }
// ).then(() => {
// // 模拟删除操作
// setTimeout(() => {
// ElMessage.success('删除成功')
// fetchTableData()
// }, 500)
// })
// }
// // 处理单个删除
// const handleDelete = (row) => {
// ElMessageBox.confirm(
// `确定要删除 ${row.name} 吗?`,
// '删除确认',
// {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// }
// ).then(() => {
// // 模拟删除操作
// setTimeout(() => {
// ElMessage.success('删除成功')
// fetchTableData()
// }, 500)
// })
// }
// 处理新增
// const handleAdd = () => {
// ElMessage.info('打开新增对话框')
// // 实际项目中这里会打开新增数据的对话框
// }
// 处理编辑
// const handleEdit = (row) => {
// ElMessage.info(`编辑 ID: ${row.id} 的数据`)
// // 实际项目中这里会打开编辑数据的对话框
// }
// 处理查看
// const handleView = (row) => {
// ElMessage.info(`查看 ID: ${row.id} 的数据详情`)
// // 实际项目中这里会打开查看数据详情的对话框
// }
// 处理返回
const handleBack = () => {
ElMessage.info('返回上一页')
// 实际项目中这里会进行路由跳转
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
</script>
<style scoped>
.data-management-page {
padding: 20px;
max-width: 1600px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
padding: 15px 20px;
}
.search-buttons {
display: flex;
gap: 10px;
}
.import-area {
margin-bottom: 20px;
}
.import-card {
padding: 10px;
background-color: #f6ffed;
border: 1px solid #b7eb8f;
}
.import-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.import-actions {
display: flex;
align-items: center;
gap: 10px;
}
.upload-excel {
display: inline-block;
}
/* 文件信息显示样式 */
.file-info {
margin-top: 15px;
padding: 12px 16px;
background-color: #f5f7fa;
border-radius: 4px;
border: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
}
.file-info-content {
display: flex;
align-items: center;
gap: 8px;
}
.file-info-content .el-icon {
color: #409eff;
font-size: 16px;
}
.file-name {
font-weight: 500;
color: #303133;
}
.file-size {
color: #909399;
font-size: 12px;
}
.confirm-import-btn {
margin-left: 12px;
}
/* 下载模板按钮样式 */
.download-template-btn {
color: #409eff;
font-size: 12px;
padding: 8px 12px;
}
.download-template-btn:hover {
background-color: #ecf5ff;
}
.table-card {
padding: 15px 20px;
}
.table-actions {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination {
margin-top: 15px;
text-align: right;
}
/* 禁用状态行样式 */
::v-deep .row-inactive {
background-color: #f5f5f5;
color: #9e9e9e;
}
.form-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 14px;
color: #4e5969;
font-weight: 500;
line-height: 1;
padding-left: 2px;
}
.search-row {
margin-bottom: 18px;
}
.search-row:last-child {
margin-bottom: 0;
}
.search-buttons {
display: flex;
gap: 10px;
justify-content: flex-start;
align-items: flex-end;
padding-bottom: 2px;
}
.advanced-search {
margin-top: 18px;
border-top: 1px dashed #e5e7eb;
padding-top: 18px;
}
.el-collapse-item__content {
padding-top: 15px !important;
}
/* 转介人详情样式 */
.broker-details {
max-height: 200px;
overflow-y: auto;
}
.broker-item {
padding: 8px 0;
}
.broker-item p {
margin: 4px 0;
font-size: 12px;
line-height: 1.4;
}
.broker-item strong {
color: #606266;
}
.no-broker {
color: #909399;
font-size: 12px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.import-actions {
flex-wrap: wrap;
}
.download-template-btn {
margin-top: 10px;
}
.file-info {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.confirm-import-btn {
margin-left: 0;
align-self: flex-end;
}
}
@media (max-width: 992px) {
.search-card .el-row {
row-gap: 15px;
}
.search-card .el-col {
flex: 0 0 50%;
max-width: 50%;
}
.search-buttons {
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.search-card .el-col {
flex: 0 0 100%;
max-width: 100%;
}
.table-actions {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.import-actions {
flex-direction: column;
align-items: flex-start;
}
.download-template-btn {
margin-top: 10px;
}
}
/* 防止表头换行 */
::v-deep .el-table .el-table__header-wrapper .el-table__cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 表头单元格内容不换行 */
::v-deep .el-table .el-table__header-wrapper .el-table__cell .cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment