Commit 1deb1dae by zhangxingmin

Merge remote-tracking branch 'origin/test' into prod

parents cc04ed71 b9c161b7
......@@ -17,14 +17,31 @@
margin: 0px;
padding: 0px;
}
body{
font-family: 'Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif' !important;
font-size: 14px !important;
letter-spacing: 0.3px;
color: #383838 !important;
}
/* element-plus表单的label统一样式 */
.el-form-item__label {
color: #383838 !important;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
color: #383838;
padding: 0.2em 0;
}
.el-dialog__header.dialog-header.show-close {
padding-right: 0 ;
}
.statistics-container {
padding: 10px;
background-color: rgba(0,82,217,0.03);
border-radius: 4px;
margin-bottom: 5px;
}
#loader-wrapper {
position: fixed;
top: 0;
......
......@@ -42,13 +42,16 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/node": "^25.0.3",
"@vitejs/plugin-vue": "5.2.4",
"sass-embedded": "1.89.1",
"typescript": "^5.9.3",
"unplugin-auto-import": "0.18.6",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "6.3.5",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1"
"vite-plugin-svg-icons": "2.0.1",
"vue-tsc": "^3.1.8"
},
"overrides": {
"quill": "2.0.2"
......
<template>
<el-watermark :font="font" :content="content">
<router-view />
</el-watermark>
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
import { reactive } from 'vue'
const font = reactive({
color: 'rgba(0, 0, 0, .15)'
})
const content = ref('')
onMounted(() => {
nextTick(() => {
......
......@@ -95,3 +95,121 @@ export function getClientUser(data) {
data: data
})
}
// 银行列表
export function getBankList(data) {
return request({
url: '/base/api/bank/page',
method: 'post',
data: data
})
}
// 转介人列表
export function getUserSaleExpandList(data) {
return request({
url: '/insurance/base/api/userSaleExpand/page',
method: 'post',
data: data
})
}
// 第二个附加险列表
export function secondAdditonalList(data) {
return request({
url: '/product/api/relProjectProductLaunch/parameter/page',
method: 'post',
data: data
})
}
// 获取签单员列表
export function getAllSignList(data) {
return request({
url: '/insurance/base/api/userSignExpand/page',
method: 'post',
data: data
})
}
// 获取团队列表
export function getAllTeam(data) {
return request({
url: '/csf/api/team/page',
method: 'post',
data: data
})
}
// 获取保险公司列表
export function getInsuranceCompany(data) {
return request({
url: '/insurance/base/api/insuranceCompany/page',
data: data,
method: 'post'
})
}
// 获取保险险种列表
export function getInsuranceCategory(data) {
return request({
url: '/insurance/base/api/insuranceCategory/page',
data: data,
method: 'post'
})
}
// 通用excel导入
export function importExcel(data) {
return request({
url: '/oss/api/excel/import',
method: 'post',
data: data
})
}
// 上传获取材料列表
export function uploadMaterialList(data) {
return request({
url: '/oss/api/material/list',
data: data,
method: 'post'
})
}
// 删除上传文件
export function delUploadFile(fileBizId) {
return request({
url: `/oss/api/ossFile/del?fileBizId=${fileBizId}`,
method: 'delete'
})
}
// 上传获取材料列表2
export function uploadRelObjectMaterialList(data) {
return request({
url: '/oss/api/relObjectMaterial/page',
data: data,
method: 'post'
})
}
// 删除材料
export function delMaterial(relObjectMaterialBizId) {
return request({
url: `/oss/api/relObjectMaterial/del?relObjectMaterialBizId=${relObjectMaterialBizId}`,
method: 'delete'
})
}
// 获取文件详情
export function uploadOssFileList(data) {
return request({
url: '/oss/api/ossFile/list',
data: data,
method: 'post'
})
}
// 提交附件
export function uploadOssFileSubmit(data) {
return request({
url: '/oss/api/relObjectMaterial/upload/submit',
data: data,
method: 'post'
})
}
// 下载材料包
export function downloadCompressedFile(data) {
return request({
url: '/oss/api/material/download/compressed/file',
data: data,
method: 'post'
})
}
......@@ -9,14 +9,6 @@ export function getPolicyCommissionList(data) {
})
}
// 更新保单来佣信息
export function updatePolicyCommission(data) {
return request({
url: '/csf/api/commission/update',
method: 'post',
data: data
})
}
// 生成可出账记录
export function generateCommissionRecord(data) {
......@@ -64,7 +56,6 @@ export function downloadPolicyFortuneAccount(data) {
url: '/csf/api/fortune/download/account',
method: 'post',
data: data,
responseType: 'blob'
})
}
......@@ -107,7 +98,6 @@ export function completePolicyFortune(data) {
})
}
// 获取对账公司
// /csf/api/reconciliation_company/list/page
export function getReconciliationCompanyList(data) {
......@@ -148,7 +138,6 @@ export function addPolicyFortuneAccount(data) {
})
}
// 删除发佣
// /csf/api/fortune/delete/fortuneAccount
export function deletePolicyFortuneAccount(data) {
......@@ -168,11 +157,283 @@ export function addPolicyCommission(data) {
data: data
})
}
// 入账管理的统计数据
export function incomeStatistics(data) {
return request({
url: '/csf/api/commission/statistics',
method: 'post',
data: data
})
}
// 出账管理的统计数据
export function billStatistics(data) {
return request({
url: '/csf/api/fortune/statistics',
method: 'post',
data: data
})
}
// 出账管理的统计数据
export function salaryStatistics(data) {
return request({
url: '/csf/api/fortune/fortuneAccount/statistics',
method: 'post',
data: data
})
}
// 薪资管理修订记录
export function salaryEditRecords(data) {
return request({
url: '/csf/api/fortune/editRecords/fortuneAccount',
method: 'post',
data: data
})
}
// 入账管理操作记录
export function incomeEditRecords(data) {
return request({
url: '/csf/api/commission/edit/records',
method: 'post',
data: data
})
}
// 入账管理比对记录列表
export function incomeCompareRecords(data) {
return request({
url: '/csf/api/commission/compare/records',
method: 'post',
data: data
})
}
// 保单号列表
export function policyData(data) {
return request({
url: '/csf/api/CommissionExpected/list/page',
method: 'post',
data: data
})
}
// 应付款管理列表
export function expectedFortuneList(data) {
return request({
url: '/csf/api/expectedFortune/list',
method: 'post',
data: data
})
}
// 应收款管理列表
export function receivedFortuneList(data) {
return request({
url: '/csf/api/CommissionExpected/queryCommissionExpectedByPage',
method: 'post',
data: data
})
}
// 统计应收数据总金额、总入账金额、待入账金额、已入账比例(已入账金额-总金额)、总保单数
export function commissionExpectedStatistics(data) {
return request({
url: '/csf/api/CommissionExpected/statistics',
method: 'post',
data: data
})
}
// 统计计算统计数据 预计发佣金额 HKD、已出账金额 HKD、待出账金额 HKD、总保单数、总保费 HKD
export function expectedFortuneStatistics(data) {
return request({
url: '/csf/api/expectedFortune/statistics',
method: 'post',
data: data
})
}
// 入账比对记录查询
export function commissionEntryRecord(data) {
return request({
url: '/csf/api/commission/compare/records',
method: 'post',
data: data
})
}
// 入账操作记录查询
export function commissionEntryEditRecords(data) {
return request({
url: '/csf/api/commission/edit/records',
method: 'post',
data: data
})
}
// 获取保单发佣列表
export function policyNoCommissionPayRecord(data) {
return request({
url: '/csf/api/fortune/list/page/vo',
method: 'post',
data: data
})
}
// 应收款导出
export function exportReceivedFortune(data) {
return request({
url: '/csf/api/CommissionExpected/export',
method: 'post',
data: data,
responseType: 'blob'
})
}
// 入账记录查询
export function commissionExpectedRecord(data) {
return request({
url: '/csf/api/commission/pageByCommissionexpectedBizId',
method: 'post',
data: data
})
}
// 出账记录查询
export function payRecordList(data) {
return request({
url: '/csf/api/fortune/pageByExpectedFortuneBizId',
method: 'post',
data: data
})
}
// 更新入账信息
export function updateCommissionExpected(data) {
return request({
url: '/csf/api/CommissionExpected/update',
method: 'post',
data: data
})
}
// 修改出账状态
export function updataPayrollStatus(data){
return request({
url: '/csf/api/fortune/update/status',
method: 'post',
data: data
})
}
// 批量新增检核记录
export function addPayrollCheckRecord(data){
return request({
url: '/csf/api/commission/addBatch',
method: 'post',
data: data
})
}
// 新增应收款
export function addReceivedFortune(data){
return request({
url: '/csf/api/CommissionExpected/add',
method: 'post',
data: data
})
}
// 新增出账记录
export function addPayRecord(data){
return request({
url: '/csf/api/expectedFortune/add',
method: 'post',
data: data
})
}
// 获取销售员详情
export function userSaleExpandDetail(data){
return request({
url: '/insurance/base/api/userSaleExpand/detail?userSaleBizId=' + data,
method: 'get',
})
}
// 更新比对状态
export function updateCompareStatus(data){
return request({
url: '/csf/api/commission/updateCompareStatus',
method: 'post',
data: data
})
}
// 更新数据
export function updateCommissionRecord(data){
return request({
url: '/csf/api/commission/update',
method: 'post',
data: data
})
}
// 新增出账检核记录
export function addCheckRecordaddBatch(data){
return request({
url: '/csf/api/fortune/addBatch',
method: 'post',
data: data
})
}
// 设置本期出账金额
export function updatePayoutAmount(data){
return request({
url: '/csf/api/fortune/update',
method: 'post',
data: data
})
}
// 同步预计来佣
export function syncExpectedCommission(data){
return request({
url: '/csf/api/commission/addToExpected',
method: 'post',
data: data
})
}
// 更新出账记录
export function updatePayRecord(data){
return request({
url: '/csf/api/expectedFortune/update',
method: 'post',
data: data
})
}
// 应付款导出
export function exportPayRecord(data) {
return request({
url: '/csf/api/expectedFortune/export',
method: 'post',
data: data,
responseType: 'blob'
})
}
// 入账检核重新比对
export function compareCommissionEntry(data){
return request({
url: '/csf/api/commission/compare?commissionBizId=' + data,
method: 'get',
})
}
// 应付款查询,按照保单期数维度
export function payableReport(data) {
return request({
url: '/csf/api/expectedFortune/payable_report',
method: 'post',
data: data,
})
}
// 应收款报表查询,按照保单期数维度
export function receivableReport(data) {
return request({
url: '/csf/api/CommissionExpected/receivable_report',
method: 'post',
data: data
})
}
\ No newline at end of file
......@@ -222,3 +222,35 @@ export function delSigleAppointment(appointmentBizId) {
method: 'delete'
})
}
// 签约历史记录列表
export function getAppointmentlog(data) {
return request({
url: '/csf/api/appointment/log/page',
method: 'post',
data: data
})
}
// 查看签约历史记录详情
export function getAppointmentlogDetail(appointmentLogBizId) {
return request({
url: '/csf/api/appointment/log/detail?appointmentLogBizId=' + appointmentLogBizId,
method: 'get'
})
}
// 编辑状态预约暂存
export function appointmentEditStorage(data) {
return request({
url: '/csf/api/appointment/edit/storage',
method: 'put',
data: data
})
}
// 编辑状态预约取消申请
export function appointmentEditStatus(data) {
return request({
url: '/csf/api/appointment/edit/status',
method: 'put',
data: data
})
}
......@@ -161,3 +161,11 @@ export function searchScopeList(data) {
data: data
})
}
// 得到计算值
export function calculateFieldValue(data) {
return request({
url: '/csf/api/common/calculate/fieldValue',
method: 'post',
data: data
})
}
import request from '@/utils/request'
// 分页获取保单业务回执列表
export function getPolicyReceiptList(data) {
return request({
url: '/csf/api/policyReceipt/page',
method: 'post',
data: data
})
}
// 添加保单业务回执
export function addPolicyReceipt(data) {
return request({
url: '/csf/api/policyReceipt/add',
method: 'post',
data: data
})
}
//编辑保单业务回执
export function EditPolicyReceipt(data) {
return request({
url: '/csf/api/policyReceipt/edit',
method: 'post',
data: data
})
}
//获取保单业务回执详情
export function getPolicyReceipt(policyReceiptBizId) {
return request({
url: '/csf/api/policyReceipt/detail?policyReceiptBizId=' + policyReceiptBizId,
method: 'get'
})
}
//保费对账列表
export function premiumReconciliationList(data) {
return request({
url: '/csf/api/premiumReconciliation/page',
method: 'post',
data: data
})
}
//获取保费对账详情
export function getPremiumReconciliationInfo(premiumReconciliationBizId) {
return request({
url: `/csf/api/premiumReconciliation/detail?premiumReconciliationBizId=${premiumReconciliationBizId}`,
method: 'get'
})
}
//新增保费对账
export function addPremiumReconciliation(data) {
return request({
url: '/csf/api/premiumReconciliation/add',
method: 'post',
data: data
})
}
//编辑提交保费对账
export function editPremiumReconciliation(data) {
return request({
url: '/csf/api/premiumReconciliation/edit',
method: 'put',
data: data
})
}
//保费对账编辑单个汇款记录
export function editPremiumRemittance(data) {
return request({
url: '/csf/api/premiumRemittance/edit',
method: 'put',
data: data
})
}
//保费对账删除一条汇款记录
export function deletePremiumRemittance(premiumRemittanceBizId) {
return request({
url: `/csf/api/premiumRemittance/del?premiumRemittanceBizId=${premiumRemittanceBizId}`,
method: 'delete'
})
}
//保费对账删除汇款记录中得其他资料附件
export function deletePremiumRemittanceFile(premiumRemittanceFileBizId) {
return request({
url: `/csf/api/premiumRemittanceFile/del?premiumRemittanceFileBizId=${premiumRemittanceFileBizId}`,
method: 'delete'
})
}
//保费对账其他资料附件列表
export function getPremiumRemittanceFileList(data) {
return request({
url: '/csf/api/premiumRemittanceFile/page',
method: 'post',
data: data
})
}
//编辑单个保费对账汇款记录
export function editSiglePremiumRemittance(data) {
return request({
url: '/csf/api/premiumRemittance/edit',
method: 'put',
data: data
})
}
//保费对账汇款记录列表
export function getPremiumRemittanceListApi(data) {
return request({
url: '/csf/api/premiumRemittance/page',
method: 'post',
data: data
})
}
//检查保单号是否已经提交过保费对账
export function getCheckPolicyNoApi(policyNo) {
return request({
url: `/csf/api/premiumReconciliation/check/reconciliation/complete?policyNo=${policyNo}`,
method: 'get'
})
}
// 保费对账认定结果计算待付金额
export function getRemainingUnpaidAmount(data) {
return request({
url: '/csf/api/premiumReconciliation/calculate/remainingUnpaidAmount',
method: 'post',
data: data
})
}
//提交保费对账认定结果
export function submitResult(data) {
return request({
url: '/csf/api/premiumReconciliation/edit/result',
method: 'put',
data: data
})
}
//单个新增保费对账汇款记录
export function addSinglePremiumRemittance(data) {
return request({
url: '/csf/api/premiumRemittance/add',
method: 'post',
data: data
})
}
......@@ -27,7 +27,7 @@ export function updateToPolicyLib(data) {
data: data
})
}
// 更新新单跟进
// 更新新单跟进-基本信息
export function updatePolicyfollow(data) {
return request({
url: '/csf/api/policy_follow/update',
......@@ -56,6 +56,37 @@ export function getPolicyStatus(policyBizId) {
method: 'get'
})
}
// 获取预计来佣生成状态
export function expectedFortuneIsGenerate(policyNo) {
return request({
url: `/csf/api/expectedFortune/isGenerate?policyNo=${policyNo}`,
method: 'get'
})
}
// 生成签约单
export function policyFollowReport(policyBizId) {
return request({
url: `/csf/api/policy_follow/report/download?policyBizId=${policyBizId}`,
method: 'get',
responseType: 'blob' // 关键设置
})
}
// 生成预计发佣
export function expectedFortuneGenerate(data) {
return request({
url: '/csf/api/expectedFortune/generate',
method: 'post',
data: data
})
}
//发佣日志信息
export function getExpectedFortuneLog(data) {
return request({
url: '/csf/api/expectedFortuneLog/page',
method: 'post',
data: data
})
}
// 新单跟进附件列表
export function getAttachmentList(data) {
return request({
......@@ -142,6 +173,14 @@ export function getCommissionList(data) {
data: data
})
}
// 新单跟进的预计发佣列表
export function getExpectedFortuneList(data) {
return request({
url: '/csf/api/expectedFortune/page',
method: 'post',
data: data
})
}
// 新单跟进的预计来佣单个提交
export function editSigalCommission(data) {
return request({
......@@ -173,3 +212,47 @@ export function changePolicyStatus(data) {
data: data
})
}
// 签单人姓名列表
export function signName(data) {
return request({
url: '/csf/api/CommissionExpected/list/page',
method: 'post',
data: data
})
}
// 保存首期缴费
export function saveInitialPayment(data) {
return request({
url: '/csf/api/policy_follow/save_initial_payment',
method: 'post',
data: data
})
}
// 保存邮寄信息
export function saveMailingInfo(data) {
return request({
url: '/csf/api/policy_follow/save_mailing_info',
method: 'post',
data: data
})
}
// 批量保存介绍人
export function batchSaveBrokers(data) {
return request({
url: '/csf/api/policy_follow/batch_save_brokers',
method: 'post',
data: data
})
}
// 通过保险公司、险种查询产品列表及参数
export function getProductList(data) {
return request({
url: '/product/api/relProjectProductLaunch/parameter/page',
method: 'post',
data: data
})
}
......@@ -11,7 +11,8 @@ body {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial,
sans-serif;
}
label {
......@@ -91,7 +92,7 @@ div:focus {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
content: ' ';
clear: both;
height: 0;
}
......@@ -105,7 +106,8 @@ aside {
display: block;
line-height: 32px;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
color: #2c3e50;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
......@@ -122,7 +124,8 @@ aside {
//main-container全局样式
.app-container {
padding: 20px;
padding: 10px;
box-sizing: border-box;
}
.components-container {
......@@ -131,7 +134,7 @@ aside {
}
.text-center {
text-align: center
text-align: center;
}
.sub-navbar {
......@@ -142,7 +145,13 @@ aside {
text-align: right;
padding-right: 20px;
transition: 600ms ease position;
background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
background: linear-gradient(
90deg,
rgba(32, 182, 249, 1) 0%,
rgba(32, 182, 249, 1) 0%,
rgba(33, 120, 241, 1) 100%,
rgba(33, 120, 241, 1) 100%
);
.subtitle {
font-size: 20px;
......
......@@ -3,11 +3,15 @@
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<span
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
v-if="
(item.elTagType == 'default' || item.elTagType == '') &&
(item.elTagClass == '' || item.elTagClass == null)
"
:key="item.value"
:index="index"
:class="item.elTagClass"
>{{ item.label + " " }}</span>
>{{ item.label + ' ' }}</span
>
<el-tag
v-else
:disable-transitions="true"
......@@ -15,7 +19,8 @@
:index="index"
:type="item.elTagType"
:class="item.elTagClass"
>{{ item.label + " " }}</el-tag>
>{{ item.label + ' ' }}</el-tag
>
</template>
</template>
<template v-if="unmatch && showValue">
......@@ -32,30 +37,39 @@ const props = defineProps({
// 数据
options: {
type: Array,
default: null,
default: null
},
// 当前的值
value: [Number, String, Array],
// 当未找到匹配的数据时,显示value
showValue: {
type: Boolean,
default: true,
default: true
},
separator: {
type: String,
default: ",",
default: ','
}
})
const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
return Array.isArray(props.value)
? props.value.map(item => '' + item)
: String(props.value).split(props.separator)
})
const unmatch = computed(() => {
unmatchArray.value = []
// 没有value不显示
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
if (
props.value === null ||
typeof props.value === 'undefined' ||
props.value === '' ||
!Array.isArray(props.options) ||
props.options.length === 0
)
return false
// 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
values.value.forEach(item => {
......@@ -68,9 +82,9 @@ const unmatch = computed(() => {
})
function handleArray(array) {
if (array.length === 0) return ""
if (array.length === 0) return ''
return array.reduce((pre, cur) => {
return pre + " " + cur
return pre + ' ' + cur
})
}
</script>
......
......@@ -14,6 +14,8 @@
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
:drag="drag"
:name="name"
v-if="!disabled"
>
<!-- 上传按钮 -->
......@@ -48,6 +50,11 @@ import Sortable from 'sortablejs'
export default {
name: "FileUpload",
props: {
// 上传文件名称
name: {
type: String,
default: "file"
},
// 上传按钮文字
uploadBtnText: {
type: String,
......
<template>
<div>
<el-affix :offset="affixOffset">
<el-anchor
:direction="direction"
:type="type"
:offset="anchorOffset"
@click="handleAnchorClick"
>
<el-anchor-link
v-for="item in anchorList"
:href="'#' + item.title"
:title="item.name"
@click="e => handleLinkClick(e, item.title)"
/>
</el-anchor>
</el-affix>
</div>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
const props = defineProps({
direction: {
type: String,
default: 'vertical'
},
type: {
type: String,
default: 'default'
},
anchorOffset: {
type: Number,
default: 30
},
affixOffset: {
type: Number,
default: 60
},
anchorList: {
type: Array,
default: () => {
return []
}
},
// 新增:滚动容器的选择器
scrollContainerSelector: {
type: String,
default: ''
},
// 新增:滚动到元素时额外的偏移量
scrollOffset: {
type: Number,
default: 0
},
domIndex: {
type: Number,
default: 1
}
})
const emit = defineEmits(['anchor-click'])
// 处理锚点点击
const handleAnchorClick = e => {
// 阻止默认的锚点跳转
e.preventDefault()
}
// 处理链接点击
const handleLinkClick = (e, anchorId) => {
e.preventDefault()
e.stopPropagation()
emit('anchor-click', anchorId)
// 延迟执行滚动,确保 DOM 已经更新
setTimeout(() => {
scrollToAnchor(anchorId)
}, 50)
}
// 滚动到锚点
const scrollToAnchor = anchorId => {
const targetElement = document.getElementById(anchorId)
if (!targetElement) return
let scrollContainer
if (props.scrollContainerSelector) {
scrollContainer = document.querySelectorAll(props.scrollContainerSelector)
}
if (scrollContainer.length > 0) {
// 计算在容器内的相对位置
const containerRect = scrollContainer[props.domIndex].getBoundingClientRect()
const targetRect = targetElement.getBoundingClientRect()
// 计算滚动位置
const scrollTop =
targetRect.top -
containerRect.top +
scrollContainer[props.domIndex].scrollTop -
props.scrollOffset
// 平滑滚动
scrollContainer[props.domIndex].scrollTo({
top: scrollTop,
behavior: 'smooth'
})
} else {
// 如果没有指定容器,使用默认滚动
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
}
// 暴露方法给父组件
defineExpose({
scrollToAnchor
})
</script>
<template>
<div class="commonDialog-container">
<el-dialog
style="padding: 0 !important"
v-model="showDialog"
:width="dialogWidth"
append-to-body
:close-on-click-modal="closeOnClickModal"
:before-close="handleClose"
center
:show-close="showClose"
header-class="dialog-header"
>
<template #header>
<div class="titleBox">{{ dialogTitle }}</div>
</template>
<div class="content">
<slot></slot>
</div>
<template #footer>
<div class="dialog-footer" v-if="showAction">
<!-- 取消按钮 -->
<el-button type="info" plain v-if="showCancle" @click="close">{{ cancleText }}</el-button>
<!-- 确认按钮 -->
<el-button :loading="confirmLoading" type="primary" v-if="showConfirm" @click="confirm">{{
confirmText
}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted, defineExpose } from 'vue'
const props = defineProps({
dialogTitle: {
type: String,
default: ''
},
cancleText: {
type: String,
default: '取消'
},
showCancle: {
type: Boolean,
default: true
},
confirmText: {
type: String,
default: '确认'
},
showAction: {
type: Boolean,
default: true
},
showClose: {
type: Boolean,
default: false
},
showConfirm: {
type: Boolean,
default: true
},
dialogWidth: {
type: [String, Number],
default: '500px'
},
// 是否点击遮罩层关闭弹窗
closeOnClickModal: {
type: Boolean,
default: true
},
// 打开弹窗
openDialog: {
type: Boolean,
default: true
},
// 打开弹窗
center: {
type: Boolean,
default: true
},
// 打开弹窗
confirmLoading: {
type: Boolean,
default: false
}
})
const showDialog = ref(props.openDialog)
const emit = defineEmits(['confirm', 'close'])
const close = () => {
emit('close')
}
const confirm = () => {
emit('confirm')
}
const handleClose = done => {
close()
done()
}
watch(
() => props.openDialog,
val => {
showDialog.value = val
}
)
</script>
<style scoped lang="scss">
.commonDialog-container {
width: 100%;
box-sizing: border-box;
}
.titleBox {
width: 100%;
background: rgba(0, 82, 217, 0.05);
display: flex;
justify-content: center;
align-items: center;
padding: 15px 0;
color: rgba(0, 0, 0, 0.9);
}
.content {
padding: 0 15px;
box-sizing: border-box;
}
.dialog-footer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 20px 0;
}
</style>
<template>
<div class="editable-table">
<el-form ref="formRef" :model="{}" label-width="120px" size="small">
<el-button type="primary" size="default" style="margin: 12px 0" @click="addRow" :disabled="disabled">
添加一行
</el-button>
<el-table :data="internalData" border style="width: 100%" :row-style="{ height: '48px' }"
:cell-style="{ padding: '6px 0' }">
<el-table-column v-for="field in rowConfig" :key="field.prop" :label="field.label" :width="field.width"
:min-width="field.minWidth">
<template #default="{ row }">
<component :is="getFieldComponent(field.type)" v-bind="getFieldProps(field, row.data)"
@update:model-value="val => handleFieldChange(val, field, row)"
@option-change="option => handleOptionChange(option, field, row)"
@focus="() => loadRemoteOptions(field)"
@filter-change="keyword => handleFilterChange(keyword, field)"
:disabled="!!field.disabled || disabled" />
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ $index }">
<el-button type="danger" size="small" link @click="removeRow($index)" :disabled="disabled">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- <el-button
type="success"
size="small"
style="margin-left: 12px"
@click="batchSave"
:disabled="disabled"
>
批量保存
</el-button> -->
</el-form>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
import SelectField from '@/components/csf-form/fields/SelectField.vue'
import InputField from '@/components/csf-form/fields/InputField.vue'
import UploadField from '@/components/csf-form/fields/UploadField.vue'
import { deepEqual } from '@/utils/csf-deepEqual'
import request from '@/utils/request'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
rowConfig: {
type: Array,
required: true
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'batch-save'])
const formRef = ref()
const internalData = ref([])
// 远程加载状态 & 缓存
const remoteLoading = ref({})
const optionsCache = ref(new Map()) // prop -> options
// 初始化 internalData
watch(
() => props.modelValue,
(newVal) => {
if (!Array.isArray(newVal)) {
console.warn('[EditableTable] modelValue is not an array, reset to empty array')
internalData.value = []
return
}
const currentPlain = internalData.value.map(row => {
const { id, ...rest } = row.data
return { ...rest, id: row.id }
})
if (!deepEqual(newVal, currentPlain)) {
internalData.value = newVal.map((item, index) => ({
id: item.id || Symbol(`row-${index}`),
data: { ...(item || {}) }
}))
}
},
{ immediate: true }
)
// 同步 internalData → modelValue
let isEmitting = false
watch(
internalData,
(newVal) => {
if (isEmitting) return
const plainData = newVal.map(row => {
const { id, ...rest } = row.data
return { ...rest, id: row.id }
})
if (!deepEqual(plainData, props.modelValue)) {
isEmitting = true
emit('update:modelValue', plainData)
nextTick(() => {
isEmitting = false
})
}
},
{ deep: true }
)
// 字段组件映射
function getFieldComponent(type) {
switch (type) {
case 'select':
return SelectField
case 'input':
return InputField
case 'upload':
return UploadField
default:
return InputField
}
}
// 获取字段属性
function getFieldProps(field, rowData) {
const base = {
modelValue: rowData[field.prop],
'onUpdate:modelValue': (val) => {
rowData[field.prop] = val
}
}
switch (field.type) {
case 'select':
return {
...base,
multiple: !!field.multiple,
clearable: true,
filterable: true,
loading: remoteLoading.value[field.prop] || false,
options: optionsCache.value.get(field.prop) || field.options || [],
placeholder: field.placeholder || `请选择${field.label}`
}
case 'input':
return {
...base,
placeholder: field.placeholder || `请输入${field.label}`
}
case 'upload':
return {
...base,
uploadUrl: field.uploadUrl,
maxCount: field.maxCount || 1,
showFileList: field.showFileList !== false
}
default:
return base
}
}
// 处理字段变更(基础)
function handleFieldChange(val, field, row) {
row.data[field.prop] = val
}
// 处理 select 选中(带完整 option)
function handleOptionChange(option, field, row) {
if (!option || !field.onChangeExtraFields) return
// 兼容对象和数组格式
let extras = []
if (Array.isArray(field.onChangeExtraFields)) {
extras = field.onChangeExtraFields
} else if (typeof field.onChangeExtraFields === 'object') {
extras = Object.entries(field.onChangeExtraFields).map(([targetProp, sourceKey]) => ({
targetProp,
sourceKey
}))
}
for (const { targetProp, sourceKey } of extras) {
if (!targetProp || sourceKey == null) continue
const getValue = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj)
const extraValue = getValue(option, sourceKey)
row.data[targetProp] = extraValue
}
}
// 加载远程选项(初始加载)
async function loadRemoteOptions(field) {
if (!field.api || optionsCache.value.has(field.prop)) return
const key = field.prop
remoteLoading.value[key] = true
try {
const params = {
...(field.requestParams || {}),
[field.keywordField || 'keyword']: ''
}
const res = await request(field.api, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(params)
}).then(r => r)
const list = field.transform ? field.transform(res) : (res?.data?.records || [])
optionsCache.value.set(key, list)
} catch (err) {
console.error(`Failed to load options for ${field.prop}:`, err)
} finally {
remoteLoading.value[key] = false
}
}
// 搜索过滤(暂不实现,可扩展)
function handleFilterChange(keyword, field) {
// 可在此实现远程搜索
}
// 行操作
function addRow() {
internalData.value.push({
data: {}
})
}
function removeRow(index) {
internalData.value.splice(index, 1)
}
function batchSave() {
emit('batch-save', internalData.value.map(row => ({ ...row.data, id: row.id })))
}
</script>
<style scoped>
.editable-table :deep(.el-input__wrapper),
.editable-table :deep(.el-select__wrapper) {
box-shadow: none !important;
}
</style>
\ No newline at end of file
<template>
<el-form
ref="formRef"
:model="localModel"
:rules="formRules"
label-width="auto"
v-bind="$attrs"
:validate-on-rule-change="false"
>
<el-row :gutter="20">
<el-col
v-for="item in visibleConfig"
:key="item.prop"
:span="item.span || 6"
:class="{ 'search-form-item': isSearch }"
>
<el-form-item
:label="item.label"
:prop="item.prop"
:label-position="item.labelPosition || 'top'"
>
<component
:is="getFieldComponent(item.type)"
v-bind="getFieldProps(item)"
@update:model-value="val => handleModelChange(val, item)"
@focus="() => loadRemoteOptions(item)"
@filter-change="keyword => handleFilterChange(keyword, item)"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import { onMounted } from 'vue'
import { useSearchFormLogic } from '@/composables/useSearchFormLogic'
// 字段组件映射
import InputField from './fields/InputField.vue'
import SelectField from './fields/SelectField.vue'
import DateField from './fields/DateField.vue'
import MonthField from './fields/MonthField.vue'
import DateRangeField from './fields/DateRangeField.vue'
import CheckboxGroupField from './fields/CheckboxGroupField.vue'
import TextareaField from './fields/TextareaField.vue'
import UploadField from './fields/UploadField.vue'
const fieldMap = {
input: InputField,
select: SelectField,
date: DateField,
month: MonthField,
daterange: DateRangeField,
'checkbox-group': CheckboxGroupField,
textarea: TextareaField,
upload: UploadField
}
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
config: { type: Array, default: () => [] },
isSearch: { type: Boolean, default: false }
})
const emit = defineEmits([
'update:modelValue',
'selectChange',
'uploadSuccess'
])
const {
formRef,
localModel,
visibleConfig,
formRules,
handleModelChange,
getSelectOptions,
getDisabledDateFn,
loadRemoteOptions,
handleFilterChange,
remoteLoading,
init,
getFormData,
validate,
resetForm
} = useSearchFormLogic(props, emit)
// 暴露方法给父组件
defineExpose({ getFormData, validate, resetForm })
// 初始化
onMounted(() => {
init()
})
// 获取字段组件
function getFieldComponent(type) {
return fieldMap[type] || 'span'
}
// 构建字段 props
function getFieldProps(item) {
const base = {
modelValue: localModel.value[item.prop],
disabled: item.disabled,
placeholder: item.placeholder
}
switch (item.type) {
case 'input':
return {
...base,
inputType: item.inputType,
decimalDigits: item.decimalDigits
}
case 'select':
return {
...base,
multiple: !!item.multiple,
clearable: true,
filterable: true,
loading: remoteLoading.value[item.prop] || false,
options: getSelectOptions(item),
placeholder: item.placeholder || `请选择${item.label}`
}
case 'date':
return {
...base,
valueFormat: item.valueFormat || 'YYYY-MM-DD',
disabledDate: getDisabledDateFn(item),
placeholder: `选择${item.label}`
}
case 'month':
return {
...base,
valueFormat: item.valueFormat || 'YYYY-MM',
disabledDate: getDisabledDateFn(item),
placeholder: `选择${item.label}`
}
case 'daterange':
return {
...base,
valueFormat: item.valueFormat || 'YYYY-MM-DD',
disabledDate: getDisabledDateFn(item)
}
case 'checkbox-group':
return {
...base,
options: getSelectOptions(item)
}
case 'textarea':
return {
...base,
clearable: true
}
case 'upload':
return {
...base,
action: item.action,
headers: item.headers,
multiple: !!item.multiple,
limit: item.limit || (item.multiple ? 999 : 1),
accept: item.accept,
listType: item.listType || 'text',
showFileList: item.showFileList,
uploadType: item.uploadType,
link: item.link,
maxSize: item.maxSize
}
default:
return base
}
}
</script>
<style scoped>
.search-form-item {
margin-bottom: 20px;
}
</style>
\ No newline at end of file
<template>
<el-checkbox-group :model-value="modelValue" :disabled="disabled" @update:model-value="handleChange">
<el-checkbox v-for="opt in options" :key="opt.value" :label="opt.value">
{{ opt.label }}
</el-checkbox>
</el-checkbox-group>
</template>
<script setup>
const props = defineProps({
modelValue: String,
disabled: Boolean,
options: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue'])
function handleChange(value) {
emit('update:modelValue', value)
}
</script>
\ No newline at end of file
<!-- src/components/search-form/fields/DateField.vue -->
<template>
<el-date-picker
:model-value="modelValue"
type="date"
:placeholder="placeholder"
:disabled="disabled"
:value-format="valueFormat"
:disabled-date="disabledDate"
style="width: 100%"
@update:model-value="handleChange"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String, // 注意:不能是 required,因为可能为 null
placeholder: String,
disabled: Boolean,
valueFormat: String,
disabledDate: Function
})
const emit = defineEmits(['update:modelValue'])
function handleChange(value) {
emit('update:modelValue', value)
}
</script>
\ No newline at end of file
<template>
<el-date-picker
:model-value="modelValue"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:value-format="valueFormat"
:disabled="disabled"
:disabled-date="disabledDate"
style="width: 100%"
@update:model-value="handleChange"
/>
</template>
<script setup>
const props = defineProps({
modelValue: Array,
valueFormat: String,
disabled: Boolean,
disabledDate: Function
})
const emit = defineEmits(['update:modelValue'])
function handleChange(value) {
emit('update:modelValue', value)
}
</script>
\ No newline at end of file
<template>
<el-input
:model-value="innerValue"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
@input="handleInput"
@update:model-value="handleChange"
/>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: String,
placeholder: String,
clearable: Boolean,
disabled: Boolean,
inputType: { type: String, default: 'text' },
decimalDigits: { type: Number, default: 2 }
})
const emit = defineEmits(['update:modelValue'])
const innerValue = ref(props.modelValue)
watch(() => props.modelValue, val => {
innerValue.value = val
})
function handleInput(value) {
let result = String(value ?? '').trim()
if (props.inputType === 'integer') {
result = result.replace(/[^\d]/g, '')
} else if (props.inputType === 'decimal') {
result = result.replace(/[^\d.]/g, '')
if (result.startsWith('.')) result = '0.' + result.slice(1)
const parts = result.split('.')
if (parts.length > 2) result = parts[0] + '.' + parts.slice(1).join('')
if (result.includes('.')) {
const [int, dec] = result.split('.')
if (dec.length > props.decimalDigits) {
result = int + '.' + dec.slice(0, props.decimalDigits)
}
}
}
innerValue.value = result
emit('update:modelValue', result)
}
function handleChange(value) {
emit('update:modelValue', value)
}
</script>
\ No newline at end of file
<template>
<el-date-picker
:model-value="modelValue"
type="month"
:placeholder="placeholder"
:disabled="disabled"
:value-format="valueFormat"
:disabled-date="disabledDate"
style="width: 100%"
@update:model-value="handleChange"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String,
placeholder: String,
disabled: Boolean,
valueFormat: String,
disabledDate: Function
})
const emit = defineEmits(['update:modelValue'])
function handleChange(value) {
emit('update:modelValue', value)
}
</script>
\ No newline at end of file
<template>
<el-select
:model-value="modelValue"
:multiple="multiple"
:placeholder="placeholder"
:clearable="clearable"
:filterable="filterable"
:disabled="disabled"
:loading="loading"
@update:model-value="handleChange"
@change="handleChangeWithOption"
@focus="$emit('focus')"
@filter-change="$emit('filter-change', $event)"
>
<el-option
v-for="opt in options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</template>
<script setup>
const props = defineProps({
modelValue: [String, Number, Array, null],
multiple: Boolean,
placeholder: String,
clearable: Boolean,
filterable: Boolean,
disabled: Boolean,
loading: Boolean,
options: {
type: Array,
default: () => []
}
})
const emit = defineEmits([
'update:modelValue',
'focus',
'filter-change',
'option-change' // 新增:emit 完整 option 对象
])
function handleChange(value) {
emit('update:modelValue', value)
}
function handleChangeWithOption(value) {
// 单选:value 是 primitive;多选:value 是 array
if (props.multiple) {
// 多选暂不支持 extra fields(可按需扩展)
return
}
const option = props.options.find(opt => opt.value === value)
emit('option-change', option)
}
</script>
\ No newline at end of file
<template>
<el-input
:model-value="modelValue"
type="textarea"
:autosize="{ minRows: 2 }"
:disabled="disabled"
:clearable="clearable"
placeholder="请输入"
style="width: 240px"
@update:model-value="handleChange"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String,
disabled: Boolean,
clearable: Boolean
})
const emit = defineEmits(['update:modelValue'])
function handleChange(value) {
emit('update:modelValue', value)
}
</script>
\ No newline at end of file
<!-- src/components/search-form/fields/UploadField.vue -->
<template>
<el-upload
:file-list="innerFileList"
:action="action"
:headers="headers"
:multiple="multiple"
:limit="limit"
:accept="accept"
:list-type="listType"
:disabled="disabled"
:auto-upload="true"
:show-file-list="showFileList"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:on-remove="handleRemove"
>
<el-icon v-if="uploadType === 'image'" class="iconStyle" :size="20">
<Upload />
</el-icon>
<el-button v-else size="small" type="primary" :link="link" :disabled="disabled">
点击上传文件
</el-button>
<template #tip v-if="maxSize || accept">
<div class="el-upload__tip">
<span v-if="maxSize">大小不超过 {{ formatFileSize(maxSize) }}</span>
<span v-if="accept">支持格式:{{ accept }}</span>
</div>
</template>
</el-upload>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
import { deepEqual } from '@/utils/csf-deepEqual'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
action: String,
headers: Object,
multiple: Boolean,
limit: Number,
accept: String,
listType: { type: String, default: 'text' },
disabled: Boolean,
showFileList: Boolean,
uploadType: String,
link: Boolean,
maxSize: Number
})
const emit = defineEmits(['update:modelValue'])
// 内部状态:不能直接改 props.modelValue
const innerFileList = ref([...props.modelValue])
// 监听外部 modelValue 变化(如重置表单)
watch(
() => props.modelValue,
(newVal, oldVal) => {
if (!deepEqual(newVal, oldVal)) {
innerFileList.value = [...(newVal || [])]
}
},
{ deep: true }
)
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 文件上传前校验
function beforeUpload(file) {
if (props.maxSize && file.size > props.maxSize) {
ElMessage.error(`文件 ${file.name} 超出大小限制(最大 ${formatFileSize(props.maxSize)})`)
return false
}
if (props.accept) {
const allowed = props.accept.split(',').map(ext => ext.trim().toLowerCase())
const fileExt = '.' + file.name.split('.').pop().toLowerCase()
if (!allowed.includes(fileExt)) {
ElMessage.error(`文件类型不支持,仅支持:${props.accept}`)
return false
}
}
return true
}
// 上传成功
function handleSuccess(response, file) {
const data = response.data || response
const url = data.url || data.fileUrl || data.path
const name = data.name || data.fileName || file.name
if (!url) {
ElMessage.error('上传成功但未返回文件地址')
return
}
// 找到当前文件并更新 url 和 name
const target = innerFileList.value.find(f => f.uid === file.uid)
if (target) {
target.url = url
target.name = name
}
// 同步回父组件
emit('update:modelValue', [...innerFileList.value])
ElMessage.success(`文件 ${file.name} 上传成功`)
}
// 超出数量限制
function handleExceed() {
ElMessage.warning('超出文件数量限制')
}
// 上传失败
function handleError(err, file) {
ElMessage.error(`文件 ${file.name} 上传失败`)
console.error('Upload error:', err)
}
// 删除文件
function handleRemove(file) {
emit('update:modelValue', [...innerFileList.value])
}
// 暴露内部 fileList(可选,用于高级控制)
defineExpose({
fileList: innerFileList
})
</script>
<style scoped>
.iconStyle {
color: #409eff;
}
</style>
\ No newline at end of file
<!-- ExcelUploadPreview.vue -->
<template>
<div class="excel-upload-preview">
<!-- 文件上传 -->
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv"
:before-upload="handleBeforeUpload" @change="handleFileChange">
<el-button type="primary">选择 Excel 文件</el-button>
<span v-if="fileName" class="ml-2">已选择:{{ fileName }}</span>
</el-upload>
<!-- 解析消息 -->
<el-alert v-if="parseMessage" :title="parseMessage" :type="parsedValid ? 'success' : 'warning'"
style="margin-top: 12px" />
<!-- 错误信息 -->
<el-alert v-if="errorMessages.length" :closable="false" type="error" style="margin-top: 12px">
<template #default>
<div v-for="(msg, idx) in errorMessages" :key="idx">{{ msg }}</div>
</template>
</el-alert>
<!-- 表格 -->
<el-table v-if="editableData.length" :data="editableData" style="width: 100%; margin-top: 16px" border
max-height="400" size="small">
<!-- 动态列 -->
<el-table-column v-for="header in headers" :key="header" :prop="header" :label="getHeaderLabel(header)"
min-width="120">
<template #default="{ row, $index }">
<el-input v-if="editRowIndex === $index && editColumn === header" v-model="row[header]"
@blur="saveEdit" @keyup.enter="saveEdit" ref="currentInput" />
<span v-else @click="startEdit($index, header)">
{{ row[header] ?? '' }}
</span>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ $index }">
<el-button v-if="editRowIndex === $index" type="text" size="small" @click="cancelEdit">
取消
</el-button>
<el-button v-else type="text" size="small" @click="startEdit($index, headers[0])">
编辑
</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="deleteRow($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 提交按钮 -->
<el-button v-if="showConfirm && editableData.length" type="success" style="margin-top: 16px"
:loading="submitting" @click="handleSubmit">
确认提交(共 {{ editableData.length }} 条)
</el-button>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
const props = defineProps({
showConfirm: { type: Boolean, default: true },
beforeUpload: { type: Function, default: null },
headerRow: { type: Number, default: 0 },
checkStartRow: { type: Number, default: 3},
requiredFields: { type: String, default: '' },
useChineseHeader: { type: Boolean, default: true },
transformSubmitData: {
type: Function,
default: (rows) => rows
}
})
const emit = defineEmits(['submit', 'parsed'])
// refs
const uploadRef = ref(null)
const currentInput = ref(null)
const fileName = ref('')
const headers = ref([])
const chineseHeadersMap = ref({})
const editableData = ref([]) // ✅ 所有操作在此数组上进行
const editRowIndex = ref(-1)
const editColumn = ref(null)
const parseMessage = ref('')
const parsedValid = ref(true)
const errorMessages = ref([])
const submitting = ref(false)
// 获取表头显示文本
const getHeaderLabel = (header) => {
return props.useChineseHeader ? (chineseHeadersMap.value[header] || header) : header
}
// 上传前校验
const handleBeforeUpload = (file) => {
if (props.beforeUpload && !props.beforeUpload(file)) return false
if (!/\.(xlsx|xls|csv)$/i.test(file.name)) {
ElMessage.error('仅支持 Excel 或 CSV 文件')
return false
}
return true
}
// 解析文件
const handleFileChange = async (uploadFile) => {
const file = uploadFile.raw
if (!file) return
fileName.value = file.name
parseMessage.value = ''
parsedValid.value = true
errorMessages.value = []
chineseHeadersMap.value = {}
const formData = new FormData()
formData.append('file', file)
formData.append('headerRow', props.headerRow.toString())
formData.append('checkStartRow', props.checkStartRow.toString())
if (props.requiredFields.length > 0) {
formData.append('requiredFields', props.requiredFields)
}
try {
const res = await request.post('/oss/api/excel/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
if (res.code !== 200 || !res.data?.success) {
throw new Error(res.msg || '解析失败')
}
const parsed = res.data
parseMessage.value = parsed.message || ''
parsedValid.value = parsed.valid !== false
errorMessages.value = parsed.errorMessages || []
const rawData = parsed.data || []
headers.value = parsed.headers || []
if (rawData.length === 0) {
editableData.value = []
} else {
if (props.useChineseHeader) {
const firstRow = rawData[0]
const map = {}
headers.value.forEach(key => {
map[key] = firstRow[key] || key
})
chineseHeadersMap.value = map
editableData.value = rawData.slice(1).map(row => ({ ...row }))
} else {
editableData.value = rawData.map(row => ({ ...row }))
}
}
emit('parsed', editableData.value)
uploadRef.value.clearFiles()
} catch (err) {
console.error('Parse error:', err)
ElMessage.error('文件解析失败:' + (err.message || ''))
}
}
// 开始编辑
const startEdit = (index, column) => {
editRowIndex.value = index
editColumn.value = column
nextTick(() => {
currentInput.value?.focus()
})
}
// 保存编辑
const saveEdit = () => {
editRowIndex.value = -1
editColumn.value = null
}
// 取消编辑
const cancelEdit = () => {
// 无需回滚,因为我们直接操作 editableData(无原始快照)
// 如果需要“撤销到初始状态”,可保留 originalData
saveEdit()
}
// 删除行
const deleteRow = (index) => {
ElMessageBox.confirm('确定删除此行?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
editableData.value.splice(index, 1)
// 重置编辑状态(避免索引错乱)
if (editRowIndex.value === index) {
editRowIndex.value = -1
editColumn.value = null
} else if (editRowIndex.value > index) {
editRowIndex.value-- // 索引前移
}
}).catch(() => { })
}
// 提交最终数据
const handleSubmit = () => {
if (editableData.value.length === 0) {
ElMessage.warning('没有可提交的数据')
return
}
submitting.value = true
try {
// 调用 transformSubmitData 进行格式转换(如类型转换)
const finalData = props.transformSubmitData([...editableData.value])
emit('submit', finalData)
} catch (err) {
ElMessage.error('提交失败:' + (err.message || ''))
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.ml-2 {
margin-left: 8px;
}
</style>
\ No newline at end of file
<template>
<div class="cardOneContainer">
<el-card class="cardStyle">
<template #header v-if="showTitle">
<div class="cardOneheader">
<div class="cardOneLeft">
<div class="mainTitle">
{{ title }}
<slot name="mainTitCustom"></slot>
</div>
<slot name="headerRight"></slot>
</div>
<div class="subTitle" v-if="desTitle">{{ desTitle }}</div>
</div>
</template>
<slot name="content"></slot>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
showTitle: {
type: Boolean,
default: true
},
desTitle: {
type: String,
default: ''
}
})
</script>
<style lang="scss" scoped>
.cardOneContainer {
width: 100%;
box-sizing: border-box;
}
.cardStyle {
width: 100%;
margin-bottom: 10px;
border: none !important;
/* :deep (.el-card__header) {
border: none !important;
} */
}
.headerStyle {
border: none !important;
}
.cardOneheader {
width: 100%;
}
.cardOneLeft {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.mainTitle {
font-size: 18px;
/* border-left: 4px solid #0052d9; */
padding-left: 5px;
display: flex;
align-items: center;
margin-right: 20px;
/* border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; */
}
.mainTitle::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: #0052d9;
margin-right: 5px;
border-radius: 4px;
}
.subTitle {
background: rgba(0, 119, 238, 0.05);
width: 100%;
padding: 10px 10px;
font-size: 14px;
color: rgba(56, 56, 56, 1);
display: flex;
align-items: center;
margin-top: 5px;
border-radius: 3px;
}
</style>
......@@ -178,7 +178,7 @@ const applicant = [
label: '通讯地址',
commonKey: true, //是否是公共字段
key: 'txAddress',
// customerKey: 'mailingAddress',
customerKey: 'residenceAddress',
domType: 'arrowRight',
required: false,
disabled: false,
......
......@@ -8,6 +8,20 @@ const insured = [
showMoudle: true, //模块是否展示
data: [
{
label: '与投保人关系',
key: 'policyholderRel',
domType: 'Select',
required: true,
disabled: false,
placeholder: '请选择',
dictType: 'csf_ap_rel',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '客户类型',
key: 'customerType',
customerKey: 'customerType',
......@@ -37,25 +51,10 @@ const insured = [
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '与投保人关系',
key: 'policyholderRel',
// customerKey: 'customerType',
domType: 'Select',
required: true,
disabled: false,
placeholder: '请选择',
dictType: 'csf_ap_rel',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
}
]
},
// 基础信息
{
fatherTitle: '基础信息',
type: 'object',
......@@ -193,7 +192,7 @@ const insured = [
label: '通讯地址',
commonKey: true, //是否是公共字段
key: 'txAddress',
// customerKey: 'mailingAddress',
customerKey: 'residenceAddress',
domType: 'arrowRight',
required: false,
disabled: false,
......@@ -482,36 +481,7 @@ const insured = [
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
// {
// label: '平均每月收入',
// key: 'monthIncome',
// domType: 'Input',
// inputType: 'number',
// required: false,
// maxLength: 300,
// disabled: false,
// placeholder: '请输入平均每月收入',
// show: true,
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 12, //栅格布局份数
// lg: 8 //栅格布局份数
// },
// {
// label: '平均每月支出',
// key: 'monthExpenditure',
// domType: 'Input',
// inputType: 'number',
// required: false,
// maxLength: 300,
// disabled: false,
// placeholder: '请输入平均每月支出',
// show: true,
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 12, //栅格布局份数
// lg: 8 //栅格布局份数
// },
{
label: '受雇于现职年期',
key: 'currentTenure',
......@@ -527,21 +497,7 @@ const insured = [
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
// {
// label: '总流动资产',
// key: 'totalCurrentAssets',
// domType: 'Input',
// inputType: 'number',
// required: false,
// maxLength: 300,
// disabled: false,
// placeholder: '请输入平均每月支出',
// show: true,
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 12, //栅格布局份数
// lg: 8 //栅格布局份数
// },
{
label: '总负债额',
key: 'totalDebt',
......
......@@ -7,38 +7,6 @@ const policyInfo = [
child: 'no', //没有子级dom,直接展示
fatherrequired: false, //父级必填,代表个人资料这个模块有必填项
data: [
// {
// label: '客户姓名',
// key: 'customerName',
// domType: 'Input',
// inputType: 'text',
// required: false,
// maxLength: 30,
// disabled: false,
// placeholder: '请输入',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
// {
// label: '客户编号',
// key: 'customerBizId',
// domType: 'Input',
// inputType: 'text',
// required: false,
// maxLength: 30,
// disabled: false,
// placeholder: '请输入',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '140px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
{
label: '保单号',
key: 'policyNo',
......@@ -69,22 +37,22 @@ const policyInfo = [
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '签单人',
key: 'signer',
domType: 'Input',
inputType: 'text',
required: false,
maxLength: 30,
disabled: false,
placeholder: '请输入',
show: true,
value: '',
labelPosition: 'top', //标签的位置
labelWidth: '130px', //标签宽度
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
// {
// label: '签单人',
// key: 'signer',
// domType: 'Input',
// inputType: 'text',
// required: false,
// maxLength: 30,
// disabled: false,
// placeholder: '请输入',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '130px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
{
label: '生效日期',
key: 'effectiveDate',
......@@ -113,53 +81,7 @@ const policyInfo = [
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
// {
// label: '产品名称',
// key: 'productName',
// domType: 'SearchSelect',
// required: false,
// maxLength: 30,
// disabled: false,
// placeholder: '请输入',
// show: true,
// value: '',
// labelWidth: '120px', //标签宽度
// sm: 8, //栅格布局份数
// labelPosition: 'top', //标签的位置
// lg: 8 //栅格布局份数
// },
// {
// label: '产品类别',
// key: 'productCate',
// domType: 'Input',
// inputType: 'text',
// required: false,
// maxLength: 30,
// disabled: false,
// placeholder: '请输入',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '130px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
// {
// label: '供款年期',
// key: 'paymentTerm',
// domType: 'Select',
// required: false,
// disabled: false,
// placeholder: '请选择',
// dictType: 'paymentTerm',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
{
label: '期交保费',
key: 'paymentPremium',
......@@ -254,37 +176,32 @@ const policyInfo = [
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
// {
// label: '受保人',
// key: 'insured',
// domType: 'Input',
// inputType: 'text',
// required: false,
// maxLength: 30,
// disabled: false,
// placeholder: '请输入',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
// {
// label: '币种',
// key: 'currency',
// domType: 'Select',
// required: false,
// disabled: false,
// placeholder: '请选择',
// dictType: 'bx_currency_type',
// show: true,
// value: '',
// labelPosition: 'top', //标签的位置
// labelWidth: '120px', //标签宽度
// sm: 8, //栅格布局份数
// lg: 8 //栅格布局份数
// },
{
label: '备注',
key: 'remark',
domType: 'Input',
inputType: 'textarea',
required: false,
maxLength: 30,
disabled: false,
placeholder: '请输入',
show: true,
value: '',
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 24, //栅格布局份数
lg: 24 //栅格布局份数
}
]
},
{
fatherTitle: '缴费信息',
keyType: 'Object', //用于表单收集值时,判断是数组还是对象
key: 'policyFollowUpdateDto',
child: 'no', //没有子级dom,直接展示
fatherrequired: false, //父级必填,代表个人资料这个模块有必填项
data: [
{
label: '首期保费',
key: 'initialPremium',
......@@ -302,10 +219,10 @@ const policyInfo = [
lg: 8 //栅格布局份数
},
{
label: '备注',
key: 'remark',
label: '保单征费',
key: 'policyLevy',
domType: 'Input',
inputType: 'textarea',
inputType: 'number',
required: false,
maxLength: 30,
disabled: false,
......@@ -314,10 +231,80 @@ const policyInfo = [
value: '',
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 24, //栅格布局份数
lg: 24 //栅格布局份数
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '首期已交保费',
key: 'initialPremiumPaid',
domType: 'Input',
inputType: 'number',
required: false,
maxLength: 30,
disabled: false,
placeholder: '请输入',
show: true,
value: '',
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '首期待交保费',
key: 'initialPremiumDue',
domType: 'Input',
inputType: 'number',
required: false,
maxLength: 30,
disabled: false,
placeholder: '请输入',
show: true,
value: '',
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '最晚缴费日期',
key: 'latestPaymentDate',
domType: 'DatePicker',
required: false,
disabled: false,
placeholder: '请选择',
show: true,
value: '',
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 8, //栅格布局份数
lg: 8 //栅格布局份数
}
]
},
//签单人信息
{
keyType: 'Array', //用于表单收集值时,判断是数组还是对象
key: 'signerList',
showTable: true,
subTitle: '签单人信息',
addChildren: true, //是否可以新增子级dom
addChildrenTxt: '签单人', //新增按钮得文本
isOpen: false, //dom是否展开
fatherRequired: false, //父级必填,代表个人资料这个模块有必填项
data: [
// {
// span: 24, //栅格布局份数
// signer: '',
// phone: '',
// email: '',
// idType: '',
// idNo: '',
// dictType: 'csf_id_type',
// registrationNumber: ''
// }
]
}
]
export default policyInfo
const secondHolder = [
// 基础信息
{
fatherTitle: '',
type: 'object',
key: 'person',
labelPosition: 'top', //标签的位置
showTitle: false,
keyType: 'Object', //用于表单收集值时,判断是数组还是对象
key: 'isSecond',
anchorKey: 'isSecond',
showMoudle: true, //模块是否展示
// description: '证件信息至少填写一项',
showTable: false, //是否展示表格
fatherRequired: true, //父级必填,代表个人资料这个模块有必填项
data: [
{
label: '是否有第二持有人',
key: 'isSecond',
domType: 'Select',
required: true,
disabled: false,
placeholder: '请选择',
dictType: 'sys_no_yes',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
}
]
},
//受益人
{
showMoudle: false, //模块是否展示
fatherTitle: '受益人',
keyType: 'Object', //用于表单收集值时,判断是数组还是对象
key: 'apiSecondHolderInfoDto',
anchorKey: 'apiSecondHolderInfoDto',
fatherRequired: true, //父级必填,代表个人资料这个模块有必填项
addChildren: true,
addChildrenTxt: '新增受益人',
showTable: false, //是否展示表格
data: [
{
label: '与受保人关系',
......@@ -23,30 +53,30 @@ const secondHolder = [
lg: 8 //栅格布局份数
},
{
label: '名字',
key: 'name',
customerKey: 'name',
label: '名字(中文)',
key: 'nameCn',
domType: 'Input',
unit: '历史客户',
unitColor: 'rgba(0, 82, 217, 1)',
inputType: 'text',
required: false,
maxLength: 20,
maxLength: 15,
disabled: false,
placeholder: '请输入',
placeholder: '请输入2~6位汉字',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '名字-英文',
key: 'nameEn',
customerKey: 'firstNamePinyin',
showEn: true, //是否填写英文
label: '姓名(拼音/英文)',
key: 'namePyEn',
domType: 'Input',
inputType: 'text',
required: false,
maxLength: 20,
maxLength: 30,
disabled: false,
placeholder: '请输入',
show: true,
......@@ -56,14 +86,13 @@ const secondHolder = [
lg: 8 //栅格布局份数
},
{
label: '性别',
key: 'gender',
customerKey: 'gender',
label: '证件类型',
key: 'documentType',
domType: 'Select',
required: false,
disabled: false,
placeholder: '请选择',
dictType: 'sys_gender',
dictType: 'csf_id_type',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
......@@ -71,29 +100,29 @@ const secondHolder = [
lg: 8 //栅格布局份数
},
{
label: '出生日期',
key: 'birthTime',
customerKey: 'birthdate',
domType: 'DatePicker',
label: '证件号码',
key: 'idNumber',
domType: 'Input',
inputType: 'text',
required: false,
maxLength: 20,
disabled: false,
placeholder: '请选择',
placeholder: '请输入',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '年龄',
key: 'age',
customerKey: 'age',
domType: 'Input',
inputType: 'number',
label: '性别',
key: 'gender',
domType: 'Select',
required: false,
maxLength: 20,
disabled: true,
placeholder: '请输入',
disabled: false,
placeholder: '请选择',
dictType: 'sys_gender',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
......@@ -101,30 +130,28 @@ const secondHolder = [
lg: 8 //栅格布局份数
},
{
label: '证件类型',
key: 'documentType',
customerKey: 'idType',
domType: 'Select',
label: '生日',
key: 'birthday',
domType: 'DatePicker',
required: false,
disabled: false,
placeholder: '请选择',
dictType: 'csf_id_type',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
sm: 12, //栅格布局份数
lg: 8 //栅格布局份数
},
{
label: '证件号码',
key: 'idNumber',
customerKey: 'idCard',
label: '受益比例',
key: 'benefitRatio',
domType: 'Input',
inputType: 'number',
required: false,
maxLength: 20,
maxLength: 300,
disabled: false,
placeholder: '请输入',
placeholder: '请输入受益比例',
show: true,
labelPosition: 'top', //标签的位置
labelWidth: '120px', //标签宽度
......
......@@ -55,7 +55,7 @@
<!-- <router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link> -->
<el-dropdown-item divided command="logout">
<el-dropdown-item command="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
......
import { signName } from '../../api/sign/underwritingMain'
const useDictStore = defineStore('dict', {
state: () => ({
dict: new Array(),
......@@ -6,7 +8,14 @@ const useDictStore = defineStore('dict', {
additionalProductList: [], //附加险产品数据
insureCompanyList: [], //保险公司数据
clientUserList: [], //用户数据,转介人
dictTypeLists: [] //字典列表,根据请求得不同会变化,所以使用之前需要使用useDictLists请求数据
dictTypeLists: [], //字典列表,根据请求得不同会变化,所以使用之前需要使用useDictLists请求数据
signNameList: [],
bankList: [], //银行列表
userSaleExpandList: [], //最新转介人列表
productAdditionalList: [], //附加险产品列表对应的是产品计划
signPeopleList: [], //所有签单员
allTeaList: [], //所有团队
allInsuranceCompanyList: [] //最新的保险公司列表
}),
actions: {
// 获取字典
......@@ -77,6 +86,34 @@ const useDictStore = defineStore('dict', {
// 设置字典列表
setDictTypeLists(typeList) {
this.dictTypeLists = typeList
},
// 设置签单人姓名列表
setSignNameList(nameList) {
this.signNameList = nameList
},
//设置银行列表
setBankList(list) {
this.bankList = list
},
//设置最新转介人列表
setUserSaleExpandList(list) {
this.userSaleExpandList = list
},
//设置最新的附加险产品列表
setProductAdditionalList(list) {
this.productAdditionalList = list
},
//设置签单员列表
setSignPeopleList(list) {
this.signPeopleList = list
},
//设置团队列表
setAllTeaList(list) {
this.allTeaList = list
},
//设置最新的保险公司列表
setAllInsuranceCompanyList(list) {
this.allInsuranceCompanyList = list
}
}
})
......
// src/types/search-form.ts
export type FormFieldType =
| 'input' // 输入框
| 'textarea' // 文本域
| 'select' // 普通下拉框
| 'multi-select' // 多选下拉框
| 'remote-select' // 远程搜索下拉框(单选)
| 'remote-multi-select' // 远程搜索下拉框(多选)
| 'date' // 日期选择器
| 'daterange' // 日期范围选择器
| 'datetime' // 日期时间选择器
| 'datetimerange' // 日期时间范围选择器
| 'cascader' // 级联选择器
| 'radio' // 单选框
| 'checkbox' // 复选框
| 'switch' // 开关
| 'number' // 数字输入框
| 'password' // 密码输入框
| 'custom' // 自定义组件
export interface FormOption {
label: string
value: string | number
disabled?: boolean
children?: FormOption[]
[key: string]: any
}
export interface RemoteSearchConfig {
type: string
apiMethod: (params: any) => Promise<any>
formatResult?: (data: any[]) => FormOption[]
cacheKey?: string
debounceDelay?: number
defaultOptions?: FormOption[]
params?: Record<string, any>
}
export interface ValidationRule {
required?: boolean
message?: string
trigger?: 'blur' | 'change' | ['blur', 'change']
validator?: (rule: any, value: any, callback: (error?: Error) => void) => void
pattern?: RegExp
min?: number
max?: number
len?: number
type?: 'string' | 'number' | 'boolean' | 'method' | 'regexp' | 'integer' | 'float' | 'array' | 'object' | 'enum' | 'date' | 'url' | 'hex' | 'email'
enum?: Array<string | number>
transform?: (value: any) => any
}
export interface FormField {
// 基础配置
type: FormFieldType
field: string
label: string
defaultValue?: any
// 显示配置
placeholder?: string
width?: string | number
colSpan?: number // 栅格占据的列数 (1-24)
hidden?: boolean
disabled?: boolean
readonly?: boolean
clearable?: boolean
// 选项配置(用于select/radio/checkbox等)
options?: FormOption[]
optionLabel?: string
optionValue?: string
// 远程搜索配置
remoteConfig?: RemoteSearchConfig
// 特殊类型配置
dateFormat?: string
rangeSeparator?: string
startPlaceholder?: string
endPlaceholder?: string
showPassword?: boolean
minRows?: number
maxRows?: number
showWordLimit?: boolean
maxlength?: number
step?: number
precision?: number
// 校验规则
rules?: ValidationRule[]
// 自定义组件
component?: any
props?: Record<string, any>
slots?: Record<string, any>
// 事件
events?: Record<string, Function>
// 样式
class?: string
style?: Record<string, string | number>
}
export interface SearchFormProps {
modelValue: Record<string, any>
fields: FormField[]
labelWidth?: string | number
labelPosition?: 'left' | 'right' | 'top'
inline?: boolean
size?: 'large' | 'default' | 'small'
disabled?: boolean
showReset?: boolean
showSearch?: boolean
resetText?: string
searchText?: string
gutter?: number
rowClass?: string
formClass?: string
// 自定义插槽
prefixSlot?: boolean
suffixSlot?: boolean
extraSlot?: boolean
// 搜索和重置前的钩子
beforeSearch?: (formData: Record<string, any>) => boolean | Promise<boolean>
beforeReset?: () => boolean | Promise<boolean>
}
export interface SearchFormEmits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', formData: Record<string, any>): void
(e: 'reset', formData: Record<string, any>): void
(e: 'change', field: string, value: any): void
(e: 'field-validate', field: string, isValid: boolean, message?: string): void
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -13,3 +13,65 @@ export default function deepClone(obj) {
return clonedObj
}
}
// 处理用户名重名时返回姓名+手机号
export function processSameUserName(users) {
const nameCountMap = new Map()
// 统计 realName 重复情况
users.forEach(user => {
if (!nameCountMap.has(user.realName)) {
nameCountMap.set(user.realName, new Set())
}
nameCountMap.get(user.realName).add(user.userBizId)
})
// 处理 showName
return users.map(user => {
const userBizIds = nameCountMap.get(user.realName)
const needDistinguish = userBizIds && userBizIds.size > 1
if (needDistinguish) {
const mobile = user.mobile
const prefix = mobile.substring(0, 3)
const suffix = mobile.substring(mobile.length - 4)
return {
...user,
showName: `${user.realName} ${prefix}****${suffix}`
}
} else {
return {
...user,
showName: user.realName
}
}
})
}
// 处理用户名返回姓名+手机号(手机号固定11位)
export function processUserName(users) {
return users.map(user => {
processUserName
const mobile = user.mobile || ''
if (mobile.length === 11) {
const prefix = mobile.substring(0, 3)
const suffix = mobile.substring(7)
return {
...user,
showName: `${user.realName || ''} ${prefix}****${suffix}`
}
} else if (mobile) {
// 非11位手机号,显示部分信息
const prefix = mobile.substring(0, Math.min(3, mobile.length))
const suffix = mobile.substring(Math.max(mobile.length - 2, 0))
return {
...user,
showName: `${user.realName || ''} ${prefix}***${suffix}`
}
} else {
return {
...user,
showName: user.realName || ''
}
}
})
}
// utils/deepEqual.js
export function deepEqual(a, b) {
if (a === b) return true
if (a == null || b == null) return false
if (typeof a !== typeof b) return false
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false
}
return true
}
if (typeof a === 'object' && typeof b === 'object') {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!deepEqual(a[key], b[key])) return false
}
return true
}
return false
}
\ No newline at end of file
......@@ -32,6 +32,7 @@ export function useDict(...args) {
*/
export function useDictLists(typeLists) {
let params = { typeList: typeLists }
console.log(params)
let dictArray = []
return (() => {
getMoreDicts(params).then(resp => {
......@@ -52,24 +53,3 @@ export function useDictLists(typeLists) {
})
})()
}
\ No newline at end of file
// /**
// * 获取字典数据
// */
// export function useDict(...args) {
// const res = ref({})
// return (() => {
// args.forEach((dictType, index) => {
// res.value[dictType] = []
// const dicts = useDictStore().getDict(dictType)
// if (dicts) {
// res.value[dictType] = dicts
// } else {
// getDicts(dictType).then(resp => {
// res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
// useDictStore().setDict(dictType, res.value[dictType])
// })
// }
// })
// return toRefs(res.value)
// })()
// }
......@@ -210,46 +210,79 @@ export function getTime(type) {
}
/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
* 修复后的防抖函数(支持立即执行、手动取消、返回值)
* @param {Function} func 要防抖的函数
* @param {number} [wait=300] 延迟时间(毫秒)
* @param {boolean} [immediate=false] 是否立即执行
* @returns {Function} 包装后的防抖函数(包含cancel方法)
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
export function debounce(func, wait = 300, immediate = false) {
let timeout = null; // 定时器标识
let args = null; // 缓存函数参数
let context = null; // 缓存函数上下文
let timestamp = 0; // 最后一次触发的时间戳
let result = undefined; // 函数执行结果
// 延迟执行的核心逻辑
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 计算最后一次触发与现在的时间差
const last = Date.now() - timestamp;
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
// 如果时间差小于wait且大于0,说明还在防抖窗口期,重新设置定时器
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
timeout = setTimeout(later, wait - last);
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
// 窗口期结束,清空定时器
timeout = null;
// 非立即执行的情况,在此处执行原函数
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
result = func.apply(context, args);
// 执行后清空上下文和参数,避免内存泄漏
if (!timeout) context = args = null;
}
}
};
// 包装后的防抖函数
const debounced = function(...params) {
// 实时捕获当前的上下文和参数
context = this;
args = params;
// 记录最后一次触发的时间戳
timestamp = Date.now();
// 判断是否需要立即执行
const callNow = immediate && !timeout;
// 清除旧的定时器,避免多次执行
if (timeout) clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(later, wait);
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
// 立即执行的情况,直接调用原函数
if (callNow) {
result = func.apply(context, args)
context = args = null
result = func.apply(context, args);
// 清空上下文和参数
context = args = null;
}
return result
}
// 返回函数执行结果
return result;
};
// 手动取消防抖的方法
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
context = args = null;
timestamp = 0;
};
return debounced;
}
/**
* This is just a simple version of deep copy
* Has a lot of edge cases bug
......@@ -390,6 +423,6 @@ export function isNumberStr(str) {
// 数字千分位格式化,保留2位小数
export function numberWithCommas(x, fixed = 2) {
if (!x) return '0.00'
return x.toFixed(fixed).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 格式化金额为货币格式
export function formatCurrency(value,currency='') {
if (value === undefined || value === null) return currency + '0.00'
return currency + value.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
}
export default {
formatCurrency
}
\ No newline at end of file
......@@ -149,6 +149,7 @@ service.interceptors.response.use(
// 通用下载方法
export function download(url, params, filename, config) {
console.log(url, params, filename, config)
downloadLoadingInstance = ElLoading.service({
text: '正在下载数据,请稍候',
background: 'rgba(0, 0, 0, 0.7)'
......@@ -160,7 +161,7 @@ export function download(url, params, filename, config) {
return tansParams(params)
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/json' },
responseType: 'blob',
...config
})
......
// utils/safeDownload.js
import { ElMessage } from 'element-plus'
/**
* 安全下载函数:适用于后端返回 Blob 文件流 或 JSON 错误 的场景
* @param {Blob} blobData - 接口返回的响应数据(必须是 responseType: 'blob')
* @param {string} defaultFilename - 默认文件名(如 'data.xlsx')
* @param {string} [mimeType] - MIME 类型(用于兜底创建 Blob)
*/
export async function safeDownload(blobData, defaultFilename, mimeType = 'application/octet-stream') {
if (!(blobData instanceof Blob)) {
ElMessage.error('无效的下载数据')
return
}
try {
// 👇 关键:先 peek 前 100 字节,判断是否是 JSON 错误
const firstChunk = await blobData.slice(0, 100).text()
const trimmed = firstChunk.trim()
// 如果看起来像 JSON(以 { 或 [ 开头),尝试解析
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
const fullText = await blobData.text()
let parsed
try {
parsed = JSON.parse(fullText)
} catch (e) {
// 解析失败,当作正常文件(比如内容就是纯文本)
parsed = null
}
// 如果解析成功,且包含错误字段(根据你后端约定)
if (parsed && (parsed.code !== undefined || parsed.msg || parsed.message)) {
const errorMsg = parsed.msg || parsed.message || '导出失败'
ElMessage.error(errorMsg)
return
}
}
// ✅ 是合法文件流,执行下载(使用你已验证的逻辑)
const url = window.URL.createObjectURL(blobData)
const link = document.createElement('a')
link.href = url
link.download = defaultFilename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('safeDownload error:', error)
ElMessage.error('下载过程中发生错误')
}
}
\ No newline at end of file
// dictUtils.js
import request from '@/utils/request'
// 全局缓存:key 为 dictType,value 为 { label: value } 映射对象
const dictCache = new Map()
// 批量加载字典(支持多个 type)
export async function loadDicts(typeList) {
// 过滤已缓存的类型,避免重复请求
const needLoadTypes = typeList.filter(type => !dictCache.has(type))
if (needLoadTypes.length === 0) return
try {
const res = await request({
url: '/user/api/sysDict/type/list', // 替换为你的实际接口地址
method: 'POST',
data: {
typeList: needLoadTypes
}
})
// 替换 loadDicts 中的缓存部分
if (res.code === 200 && Array.isArray(res.data)) {
for (const dict of res.data) {
const { dictType, dictItemList = [] } = dict
if (!dictType) continue
// 缓存完整列表(并可选排序 + 过滤)
const validItems = dictItemList
.filter(item => item.status === 1) // 只取启用的
.sort((a, b) => a.orderNum - b.orderNum) // 按序号排序
dictCache.set(dictType, validItems) // 👈 缓存完整 item 列表
}
}
} catch (error) {
console.error('字典加载失败:', error)
throw error
}
}
// 可选:提供清除缓存方法(用于权限切换等场景)
export function clearDictCache() {
dictCache.clear()
}
export function getDictOptions(dictType) {
const items = dictCache.get(dictType) || []
// console.log('getDictOptions',items)
return items.map(item => ({
value: item.itemValue,
label: item.itemLabel
// 如果需要,还可以加其他字段:disabled, key 等
}))
}
// 同时更新 getDictLabel
export function getDictLabel(dictType, value) {
const items = dictCache.get(dictType) || []
const item = items.find(i => i.itemValue === value)
return item ? item.itemLabel : value
}
\ No newline at end of file
......@@ -152,22 +152,22 @@ watch(drawer, val => {
})
</script>
<style lang="scss" scoped>
::v-deep .el-drawer__header {
:deep(.el-drawer__header ) {
margin-bottom: 0 !important;
}
::v-deep .el-drawer__footer {
:deep(.el-drawer__footer ) {
padding: 0;
}
.addressInput ::v-deep .el-input__wrapper {
.addressInput :deep(.el-input__wrapper ) {
box-shadow: none !important;
}
.addressInput ::v-deep .el-input__inner::placeholder {
.addressInput :deep(.el-input__inner::placeholder ) {
text-align: right;
}
.addressInput ::v-deep .el-input__inner {
.addressInput :deep(.el-input__inner ) {
text-align: right;
}
.addressInput ::v-deep .el-input__wrapper {
.addressInput :deep(.el-input__wrapper ) {
padding: 0 !important;
}
.drawerContent {
......
<template>
<div class="formContainer">
<div class="formLeft" v-if="showAnchor && scrollContainerSelector">
<el-affix :offset="affixOffset">
<el-anchor
:direction="direction"
:type="type"
:offset="anchorOffset"
@click="handleAnchorClick"
>
<!-- -->
<el-anchor-link
v-for="item in anchorList"
:key="item.title"
:href="'#' + item.title"
:title="item.name"
@click="e => handleLinkClick(e, item.title)"
/>
</el-anchor>
</el-affix>
</div>
<div class="formRight" :class="showAnchor ? 'formRightSelf' : ''">
<slot name="form-right"></slot>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
const props = defineProps({
containerRef: {
type: Object,
default: () => {
return {}
}
},
direction: {
type: String,
default: 'vertical'
},
type: {
type: String,
default: 'default'
},
anchorOffset: {
type: Number,
default: 30
},
affixOffset: {
type: Number,
default: 60
},
anchorList: {
type: Array,
default: () => {
return []
}
},
// 新增:滚动容器的选择器
scrollContainerSelector: {
type: String,
default: ''
},
// 新增:滚动到元素时额外的偏移量
scrollOffset: {
type: Number,
default: 0
},
domIndex: {
type: Number,
default: 0
},
showAnchor: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['anchor-click'])
// 处理锚点点击
const handleAnchorClick = e => {
// 阻止默认的锚点跳转
e.preventDefault()
}
// 处理链接点击
const handleLinkClick = (e, anchorId) => {
e.preventDefault()
e.stopPropagation()
emit('anchor-click', anchorId)
// 延迟执行滚动,确保 DOM 已经更新
setTimeout(() => {
scrollToAnchor(anchorId)
}, 50)
}
// 滚动到锚点
const scrollToAnchor = anchorId => {
const targetElement = document.getElementById(anchorId)
if (!targetElement) return
let scrollContainer
if (props.scrollContainerSelector) {
scrollContainer = document.querySelectorAll(props.scrollContainerSelector)
}
if (scrollContainer.length > 0) {
// 计算在容器内的相对位置
const containerRect = scrollContainer[props.domIndex].getBoundingClientRect()
const targetRect = targetElement.getBoundingClientRect()
// 计算滚动位置
const scrollTop =
targetRect.top -
containerRect.top +
scrollContainer[props.domIndex].scrollTop -
props.scrollOffset
// 平滑滚动
scrollContainer[props.domIndex].scrollTo({
top: scrollTop,
behavior: 'smooth'
})
} else {
// 如果没有指定容器,使用默认滚动
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
}
// 预留备用
// const scrollToAnchor = async (anchorId: string) => {
// // 确保 DOM 更新完成(尤其在 tab 切换后)
// await nextTick()
// const target = document.getElementById(anchorId)
// if (!target) {
// console.warn(`[Anchor Scroll] Target element not found: #${anchorId}`)
// return
// }
// // 关键:从目标元素向上查找最近的 .appointmentTabPaneBox
// let scrollContainer: HTMLElement | null = target
// while (scrollContainer && !scrollContainer.classList.contains('appointmentTabPaneBox')) {
// scrollContainer = scrollContainer.parentElement
// }
// if (!scrollContainer) {
// // 如果没找到,回退到全局滚动
// target.scrollIntoView({ behavior: 'smooth', block: 'start' })
// return
// }
// // 计算相对滚动位置
// const containerRect = scrollContainer.getBoundingClientRect()
// const targetRect = target.getBoundingClientRect()
// const scrollTop =
// scrollContainer.scrollTop +
// (targetRect.top - containerRect.top) -
// props.scrollOffset
// scrollContainer.scrollTo({
// top: scrollTop,
// behavior: 'smooth'
// })
// }
// 暴露方法给父组件
defineExpose({
scrollToAnchor
})
</script>
<style lang="scss" scoped>
.formContainer {
width: 100%;
box-sizing: border-box;
display: flex;
}
.formLeft {
width: 15%;
box-sizing: border-box;
height: 100vh;
}
.formRight {
flex: 1;
padding-left: 10px;
box-sizing: border-box;
}
.formRightSelf {
border-left: 3px solid rgb(247 247 247);
}
</style>
......@@ -250,17 +250,17 @@ watch(countryDrawer, val => {
})
</script>
<style lang="scss" scoped>
::v-deep .el-drawer__header {
:deep(.el-drawer__header ) {
margin-bottom: 0 !important;
}
.searchInput ::v-deep .el-input__wrapper {
.searchInput :deep(.el-input__wrapper ) {
box-shadow: none !important;
background-color: #fafbfd !important;
}
.searchInput ::v-deep .el-input__inner::placeholder {
.searchInput :deep(.el-input__inner::placeholder ) {
text-align: left !important;
}
.searchInput ::v-deep .el-input__inner {
.searchInput :deep(.el-input__inner ) {
text-align: left !important;
}
.countryContent {
......@@ -339,7 +339,7 @@ watch(countryDrawer, val => {
box-sizing: border-box;
margin-left: 20px;
border-left: 2px solid #d8ebff;
::v-deep(.el-anchor) {
:deep(.el-anchor) {
.el-anchor__link {
padding: 4px 0;
......@@ -354,7 +354,7 @@ watch(countryDrawer, val => {
}
}
}
::v-deep(.el-anchor__marker) {
:deep(.el-anchor__marker) {
height: 10px !important;
left: -6px !important;
border-radius: 50% !important;
......
......@@ -174,22 +174,22 @@ watch(
)
</script>
<style lang="scss" scoped>
::v-deep .el-drawer__header {
:deep(.el-drawer__header ) {
margin-bottom: 0 !important;
}
::v-deep .el-drawer__footer {
:deep(.el-drawer__footer ) {
padding: 0;
}
.phoneInput ::v-deep .el-input__wrapper {
.phoneInput :deep(.el-input__wrapper ) {
box-shadow: none !important;
}
.phoneInput ::v-deep .el-input__inner::placeholder {
.phoneInput :deep(.el-input__inner::placeholder ) {
text-align: right;
}
.phoneInput ::v-deep .el-input__inner {
.phoneInput :deep(.el-input__inner ) {
text-align: right;
}
.phoneInput ::v-deep .el-input__wrapper {
.phoneInput :deep(.el-input__wrapper ) {
padding: 0 !important;
}
.drawerContent {
......
......@@ -55,7 +55,6 @@
:label-position="child.labelPosition"
:rules="getRules(child)"
>
<!-- @input="handleInputChange(father, child)" -->
<el-input
v-if="child.domType === 'Input'"
:type="child.inputType"
......@@ -223,6 +222,7 @@ const props = defineProps({
apiInsurantInfoDto: { type: Object, default: () => ({}) }, //父组件传递过来的预约信息的详情
appointmentStatus: { type: Number }, //父组件传递过来的预约的状态
customerInfo: { type: Object, default: () => ({}) }, //客户详情回显表单用
currentPolicyholderInfo: { type: Object, default: () => ({}) }, //当前保单的投保人信息
showSubmitBtn: { type: Boolean, default: false }, //父组件状态,新增、修改
pageSource: { type: String, default: '' } //页面来源
})
......@@ -349,16 +349,7 @@ const getRules = child => {
return rules
}
const handleInputChange = (father, child) => {
switch (child.key) {
case 'nameEn':
validateEnglish(form.value[child.key], child.key)
break
default:
break
}
}
const handleDateChange = child => {
let age = null
if (child.key == 'birthday') {
......@@ -433,16 +424,6 @@ const fetchDictData = dictType => {
return []
}
}
// // 添加英文校验函数
// const validateEnglish = (rule, value, callback) => {
// if (value && !/^[A-Za-z]*$/.test(value)) {
// // 清空非英文字符
// form.value.firstNamePinyin = ''
// callback(new Error('只能输入英文字母'))
// } else {
// callback()
// }
// }
const mergeObjects = (obj1, obj2) => {
const result = Object.assign({}, obj1)
......@@ -461,14 +442,20 @@ const mergeObjects = (obj1, obj2) => {
* exportValue:是否从导入联系人列表中导入客户信息到当前表单
*/
const processFormData = async obj => {
form.value = {}
saveKey.value = {}
let tempPhoneList = []
let tempAddressList = []
form.value = {}
saveKey.value = {}
addressQuickList.value = []
phoneQuickList.value = []
// if (!obj.exportValue) {
// form.value = {}
// saveKey.value = {}
// addressQuickList.value = []
// phoneQuickList.value = []
// }
// 深拷贝原始数据
// const processedData = JSON.parse(JSON.stringify(policyDomData))
const processedData = JSON.parse(JSON.stringify(obj.domdata))
for (const section of processedData) {
......@@ -576,7 +563,7 @@ const processFormData = async obj => {
for (const key1 in field) {
if (obj.customerInfo.addressList && obj.customerInfo.addressList.length > 0) {
obj.customerInfo.addressList.forEach(item => {
if (key1 == item.type) {
if (field.customerKey == item.type) {
addressObj = item
}
})
......@@ -619,6 +606,7 @@ const processFormData = async obj => {
}
}
}
//当tab切换走,在切换回来的时候,需要把之前保存的值赋值回来
if (
JSON.stringify(tempPolicyForm.value) !== '{}' &&
......@@ -649,7 +637,11 @@ const processFormData = async obj => {
}
}
if (props.showSubmitBtn) {
if (obj.source && obj.source == 'policyholderRel' && field.key !== 'age') {
field.disabled = false
} else {
field.disabled = true
}
} else {
if (field.key == 'age') {
field.disabled = true
......@@ -660,7 +652,6 @@ const processFormData = async obj => {
}
}
}
addressQuickList.value = tempAddressList
tempPhoneList.forEach(item => {
for (const key in saveKey.value) {
......@@ -696,11 +687,20 @@ const processFormData = async obj => {
if (form.value.smokingAllowed) {
form.value.smokingAllowed = Number(form.value.smokingAllowed)
}
editStatus.value = true
} else {
editStatus.value = false
}
// // 投保人为本人时,信息从投保人带入
// if (obj.source && obj.source == 'policyholderRel') {
// form.value = {
// ...obj.customerInfo,
// policyholderRel: form.value.policyholderRel
// }
// if (form.value.smokingAllowed) {
// form.value.smokingAllowed = Number(form.value.smokingAllowed)
// }
// }
if (form.value['birthday']) {
let age = calculateExactAge(proxy.formatToDate(form.value.birthday))
if (age >= 0) {
......@@ -715,8 +715,6 @@ const processFormData = async obj => {
//弹出右侧抽屉
const handleFoucs = child => {
if (child.disabled) return
console.log('saveKey.value', saveKey.value)
console.log('child', child)
drawerInfo.value = JSON.parse(JSON.stringify(child))
switch (child.drawerType) {
......@@ -810,7 +808,25 @@ const confirmDrawer = info => {
form.value[info.key] = `${newObj.region} ${newObj.city} ${newObj.street} ${newObj.location}`
saveKey.value[info.key] = newObj
newObj.addressString = `${newObj.region} ${newObj.city} ${newObj.street} ${newObj.location}`
// 检查数组中是否已存在key的地址
// debugger
const existinAddressIndex = addressQuickList.value.findIndex(
item => item.type === newObj.type
)
if (existinAddressIndex !== -1) {
// 更新已存在的电话
addressQuickList.value[existinAddressIndex] = {
...newObj
}
} else {
// 添加新的快捷电话
addressQuickList.value.push(newObj)
}
addressQuickList.value = removeDuplicates(addressQuickList.value, 'addressString')
console.log('填写地址', addressQuickList.value)
break
case 'country':
info.objType = drawerInfo.value.drawerType
......@@ -861,13 +877,10 @@ const handleSelectChange = (father, child) => {
case 'customerType':
if (form.value[child.key] == 'COMPANY') {
showContacts.value = false
console.log(' commonObj.value', commonObj.value)
for (const section of processedinsuredData.value) {
if (section.key == 'company') {
console.log(11111)
for (const key1 in saveKey.value) {
for (const key2 in commonObj.value) {
console.log(key1, key2)
if (key1 == key2) {
saveKey.value[key1]['fatherKey'] = section.key
}
......@@ -908,6 +921,32 @@ const handleSelectChange = (father, child) => {
}
break
case 'policyholderRel':
//选择投保人为本人得时候,带入投保人得信息
if (form.value['policyholderRel'] == 'MYSELF') {
form.value = JSON.parse(JSON.stringify(props.currentPolicyholderInfo.form))
saveKey.value = JSON.parse(JSON.stringify(props.currentPolicyholderInfo.saveKey))
form.value['policyholderRel'] = 'MYSELF'
if (form.value['customerType'] == 'COMPANY') {
resetShow({ type: 'father', key: 'person', status: false })
resetShow({ type: 'father', key: 'company', status: true })
} else if (form.value['customerType'] == 'INDIVIDUAL') {
resetShow({ type: 'father', key: 'person', status: true })
resetShow({ type: 'father', key: 'company', status: false })
}
// 选择吸烟,展示吸烟数量
if (form.value['smokingAllowed'] == '1') {
resetShow({ type: 'child', key: 'smokingVolume', status: true })
} else {
resetShow({ type: 'child', key: 'smokingVolume', status: false })
}
if (form.value['isVip'] == '1') {
resetShow({ type: 'child', key: 'vipRemark', status: true })
} else {
resetShow({ type: 'child', key: 'vipRemark', status: false })
}
}
break
default:
break
......@@ -956,7 +995,7 @@ const getInvalidFields = fields => {
if (fields[field] && fields[field].length > 0) {
errors.push({
field: field,
message: fields[field][0].message
message: `签约信息模块-${fields[field][0].message}`
})
}
}
......@@ -1077,8 +1116,10 @@ const submitForm = () => {
proxy.$refs['insuredInfoFormRef'].validate((valid, fields) => {
if (valid) {
let submitObj = handleFormValues()
if (props.idsObj.appointmentBizId&&
(props.pageSource == 'fnaList' || props.pageSource == 'appointmentList')) {
if (
props.idsObj.appointmentBizId &&
(props.pageSource == 'fnaList' || props.pageSource == 'appointmentList')
) {
submitObj['appointmentBizId'] = props.apiInsurantInfoDto.appointmentBizId
submitObj['id'] = props.apiInsurantInfoDto.id
submitObj['insurantBizId'] = props.apiInsurantInfoDto.insurantBizId
......@@ -1112,22 +1153,6 @@ const submitForm = () => {
}
})
}
const resetForm = () => {
proxy.$modal
.confirm('是否确认撤销所作操作?')
.then(function () {
if (props.customerBizId) {
form.value = { ...oldObjInfo.value }
editStatus.value = true
} else {
// resetShow('smokeQuantity', false)
proxy.$refs['insuredInfoFormRef'].resetFields()
}
processedinsuredData.value = JSON.parse(JSON.stringify(oldAppointmentData.value))
})
.catch(() => {})
}
watch(
() => props.activeName,
......@@ -1153,6 +1178,9 @@ watch(
})
}
}, 500)
console.log('====================================')
console.log('受保人拿到投保人信息', props.currentPolicyholderInfo)
console.log('====================================')
}
}
)
......
......@@ -211,7 +211,7 @@ const props = defineProps({
idsObj: { type: Object, default: () => ({}) }, //父组件传递过来的id对象
apiPolicyholderInfoDto: { type: Object, default: () => ({}) }, //父组件传递过来的预约信息的详情
appointmentStatus: { type: Number }, //父组件传递过来的预约的状态
customerInfo: { type: Object, default: () => ({}) }, //客户详情回显表单用
customerInfo: { type: Object, default: () => ({}) }, //客户详情新增时带入表单
showSubmitBtn: { type: Boolean, default: false }, //父组件状态,新增、修改
pageSource: { type: String, default: '' } //页面来源
})
......@@ -343,16 +343,7 @@ const getRules = child => {
return rules
}
const handleInputChange = (father, child) => {
switch (child.key) {
case 'nameEn':
validateEnglish(form.value[child.key], child.key)
break
default:
break
}
}
const handleDateChange = child => {
let age = null
if (child.key == 'birthday') {
......@@ -409,7 +400,6 @@ const handleExport = row => {
customerInfo: row,
exportValue: true
})
// setFormValue(row, processedPolicyData.value, true)
openList.value = false
}
......@@ -569,7 +559,11 @@ const processFormData = async obj => {
for (const key1 in field) {
if (obj.customerInfo.addressList && obj.customerInfo.addressList.length > 0) {
obj.customerInfo.addressList.forEach(item => {
if (key1 == item.type) {
// 之前得写法
// if (key1 == item.type) {
// addressObj = item
// }
if (field.customerKey == item.type) {
addressObj = item
}
})
......@@ -641,7 +635,7 @@ const processFormData = async obj => {
}
}
}
// props.idsObj.appointmentBizId
if (props.showSubmitBtn) {
field.disabled = true
} else {
......@@ -801,6 +795,25 @@ const confirmDrawer = info => {
newObj.type = drawerInfo.value.key
form.value[info.key] = `${newObj.region} ${newObj.city} ${newObj.street} ${newObj.location}`
saveKey.value[info.key] = newObj
newObj.addressString = `${newObj.region} ${newObj.city} ${newObj.street} ${newObj.location}`
// 检查数组中是否已存在key的地址
// debugger
const existinAddressIndex = addressQuickList.value.findIndex(
item => item.type === newObj.type
)
if (existinAddressIndex !== -1) {
// 更新已存在的电话
addressQuickList.value[existinAddressIndex] = {
...newObj
}
} else {
// 添加新的快捷电话
addressQuickList.value.push(newObj)
}
addressQuickList.value = removeDuplicates(addressQuickList.value, 'addressString')
console.log('填写地址', addressQuickList.value)
break
case 'country':
info.objType = drawerInfo.value.drawerType
......@@ -1056,6 +1069,12 @@ const handleFormValues = () => {
return submitObj
}
const providePolicyholderInfoData = () => {
return {
form: JSON.parse(JSON.stringify(form.value)),
saveKey: JSON.parse(JSON.stringify(saveKey.value))
}
}
// 表单提交
const submitForm = () => {
proxy.$refs['policyholderInfoFormRef'].validate((valid, fields) => {
......@@ -1099,22 +1118,6 @@ const submitForm = () => {
}
})
}
const resetForm = () => {
proxy.$modal
.confirm('是否确认撤销所作操作?')
.then(function () {
if (props.customerBizId) {
form.value = { ...oldObjInfo.value }
editStatus.value = true
} else {
// resetShow('smokeQuantity', false)
proxy.$refs['policyholderInfoFormRef'].resetFields()
}
processedPolicyData.value = JSON.parse(JSON.stringify(oldAppointmentData.value))
})
.catch(() => {})
}
watch(
() => props.activeName,
......@@ -1136,6 +1139,7 @@ watch(
} else {
processFormData({
domdata: policyDomData,
customerInfo: props.customerInfo,
exportValue: null
})
}
......@@ -1147,7 +1151,8 @@ watch(
// 暴露给父组件
defineExpose({
handleFormValues,
handleEditStatus
handleEditStatus,
providePolicyholderInfoData
})
</script>
<style lang="scss" scoped>
......
......@@ -493,10 +493,7 @@ const handleEditStatus = () => {
}
processedFanFormData.value = processedData
}
//给表单赋值 方便表单回显 obj 为表单数据
const setFormValue = (obj, formData) => {
loading.value = true
}
// 获取校验失败的字段信息
const getInvalidFields = fields => {
......
......@@ -29,8 +29,8 @@
/>
</template>
</el-table-column>
<!-- sortable -->
<el-table-column label="来佣金额" prop="amount" align="center" width="120">
<!-- <el-table-column label="来佣金额" prop="amount" align="center" width="120">
<template #default="scope">
<el-input
v-model="scope.row.amount"
......@@ -39,8 +39,17 @@
placeholder="请输入"
/>
</template>
</el-table-column> -->
<el-table-column label="来佣比例" prop="commissionRatio" align="center" width="120">
<template #default="scope">
<el-input
v-model="scope.row.commissionRatio"
size="default"
type="number"
placeholder="请输入"
/>
</template>
</el-table-column>
<!-- sortable -->
<el-table-column label="佣金期数" prop="commissionPeriod" align="center" width="120">
<template #default="scope">
<el-input
......@@ -519,11 +528,15 @@ watch(
newVal => {
if (newVal === 'expectedCommission') {
searchOptions.value['reconciliationCompany'] = dictStore.insureCompanyList
if (props.policyNo) {
getTableList()
}
}
}
)
getTableList()
if (props.policyNo) {
getTableList()
}
</script>
<style lang="scss" scoped>
.domEmpty {
......
......@@ -9,6 +9,7 @@
</div>
</template>
<script setup name="FollowUpDetail">
import { processUserName } from '@/utils/common'
import AppointmentEdit from '@/views/sign/appointment/appointmentEdit'
import { getPolicyfollow, getPolicyInfo } from '@/api/sign/underwritingMain'
import useUserStore from '@/store/modules/user'
......@@ -39,7 +40,14 @@ const getDictsData = async () => {
}
const response1 = await listTenantUser(params1)
if (response1.code == 200) {
dictStore.setTenantUserList(response1.data.records)
let result = processUserName(response1.data.records)
result = result.map(item => {
return {
value: item.userBizId,
label: item.showName
}
})
dictStore.setTenantUserList(result)
}
const params2 = {
loginTenantBizId: userStore.projectInfo.tenantBizId,
......@@ -169,11 +177,11 @@ if (route.query.type == 'edit') {
getDictsData()
</script>
<style lang="scss" scoped>
::v-deep .el-card {
:deep(.el-card ) {
border: none !important;
}
::v-deep .el-input-group__append,
.el-input-group__prepend {
:deep(.el-input-group__append ),
:deep(.el-input-group__prepend ) {
background-color: #fff !important;
}
......
......@@ -1226,20 +1226,20 @@ getUserList()
}
/* 选项卡头部:消除默认样式,重构凹陷效果 */
::v-deep .el-tabs__header {
:deep(.el-tabs__header ) {
line-height: 1; /* 重置行高,避免影响高度计算 */
padding: 0; /* 清除默认内边距 */
}
/* 选项卡导航容器:核心凹陷逻辑 */
::v-deep .el-tabs__nav {
:deep(.el-tabs__nav ) {
display: flex;
margin: 0; /* 清除默认 margin */
border-bottom: 1px solid var(--border-color); /* 底部边框 */
}
/* 选项卡 item 基础样式 */
::v-deep .el-tabs__item {
:deep(.el-tabs__item ) {
position: relative;
padding: 12px 24px;
margin-right: 0; /* 清除默认间距 */
......@@ -1253,7 +1253,7 @@ getUserList()
}
/* 激活态:凹陷 + 高亮 */
::v-deep .el-tabs__item.is-active {
:deep(.el-tabs__item.is-active ) {
color: var(--active-tab-color);
background-color: var(--active-tab-bg);
/* 上、左、右边框 + 底部无边框,模拟“凹陷” */
......@@ -1266,23 +1266,23 @@ getUserList()
}
/* 未激活态:悬浮效果 */
::v-deep .el-tabs__item:not(.is-active):hover {
:deep(.el-tabs__item:not(.is-active):hover ) {
color: #409eff;
background-color: #eaf2ff;
}
/* 左侧边框处理(仅第一个 tab 左侧圆角) */
::v-deep .el-tabs__item:first-child {
:deep(.el-tabs__item:first-child ) {
border-top-left-radius: var(--radius);
}
/* 右侧边框处理(仅最后一个 tab 右侧圆角) */
::v-deep .el-tabs__item:last-child {
:deep(.el-tabs__item:last-child ) {
border-top-right-radius: var(--radius);
}
/* 激活态:覆盖父容器的底部边框,实现“凹陷” */
::v-deep .el-tabs__item.is-active::before {
:deep(.el-tabs__item.is-active::before ) {
content: "";
position: absolute;
bottom: -1px; /* 覆盖父容器的 border-bottom */
......@@ -1293,7 +1293,7 @@ getUserList()
}
/* 内容区域:和选项卡无缝衔接 */
::v-deep .el-tabs__content {
:deep(.el-tabs__content ) {
padding: 20px;
background-color: #fff;
border: 1px solid var(--border-color);
......
This diff is collapsed. Click to expand it.
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