Commit e451de86 by Sweet Zhang

对接新单跟进

parent defd162d
<template>
<div class="editable-table">
<el-form
ref="formRef"
:model="{}"
label-width="120px"
size="small"
>
<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="primary"
size="small"
style="margin-top: 12px"
@click="addRow"
:disabled="disabled"
>
添加一行
</el-button>
<!-- <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
// src/composables/useSearchFormLogic.js
import { ref, watch, computed, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import useDictStore from '@/store/modules/dict'
import { getDicts } from '@/api/system/dict/data'
import request from '@/utils/request'
import dayjs from 'dayjs'
// ==================== 工具函数 ====================
function deepCloneConfig(obj) {
if (obj === null || typeof obj !== 'object') return obj
if (Array.isArray(obj)) return obj.map(deepCloneConfig)
const cloned = {}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const val = obj[key]
cloned[key] = typeof val === 'function' ? val : deepCloneConfig(val)
}
}
return cloned
}
function parseToDate(str) {
if (!str) return null
if (str === 'today') return dayjs().startOf('day')
if (typeof str === 'string') {
const d = dayjs(str)
return d.isValid() ? d.startOf('day') : null
}
if (str instanceof Date) return dayjs(str).startOf('day')
return null
}
function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
function isEqualShallow(a, b) {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (let key of keysA) {
if (a[key] !== b[key]) return false
}
return true
}
// ==================== 主逻辑 ====================
export function useSearchFormLogic(props, emit) {
// === Refs ===
const formRef = ref(null)
const localModel = ref({ ...props.modelValue })
const dictLoaded = ref(new Set())
const internalConfig = ref([])
const remoteOptions = ref({})
const remoteLoading = ref({})
// === Computed ===
const visibleConfig = computed(() => {
return internalConfig.value.filter(item => {
if (typeof item.visible === 'function') return item.visible(localModel.value)
return true
})
})
const formRules = computed(() => {
const rules = {}
visibleConfig.value.forEach(item => {
if (item.rules) rules[item.prop] = item.rules
})
return rules
})
// === 模型同步 ===
function syncModelFromProps(newModelValue, newConfig) {
if (!newModelValue || !newConfig) return {}
const synced = {}
// 1. 主字段
for (const item of newConfig) {
const key = item.prop
if (newModelValue.hasOwnProperty(key)) {
synced[key] = newModelValue[key]
} else if (item.multiple || ['checkbox-group', 'daterange'].includes(item.type)) {
synced[key] = item.defaultValue ?? []
} else {
synced[key] = item.defaultValue ?? ''
}
}
// 2. extra 字段(通过 options 反查)
for (const item of newConfig) {
const extraMap = item.onChangeExtraFields
if (!extraMap || typeof extraMap !== 'object') continue
const prop = item.prop
const idValue = newModelValue[prop]
let sourceObj = null
if (idValue && typeof idValue === 'object') {
sourceObj = idValue
} else if (Array.isArray(item.options) && idValue !== undefined && idValue !== null) {
const valueKey = item.valueKey || 'value'
sourceObj = item.options.find(opt => opt[valueKey] === idValue)
}
if (sourceObj && typeof sourceObj === 'object') {
for (const [targetKey, subPath] of Object.entries(extraMap)) {
const val = getNestedValue(sourceObj, subPath)
if (val !== undefined) synced[targetKey] = val
}
}
}
// 3. 保留 localModel 中的 extra(当 sourceField 未更新)
for (const item of newConfig) {
const sourceField = item.prop
const extraMap = item.onChangeExtraFields
if (!extraMap || typeof extraMap !== 'object') continue
if (newModelValue[sourceField] === undefined) {
for (const [targetKey, subPath] of Object.entries(extraMap)) {
if (localModel.value.hasOwnProperty(targetKey)) {
synced[targetKey] = localModel.value[targetKey]
}
}
}
}
// 4. 保留其他字段
for (const key in newModelValue) {
if (synced.hasOwnProperty(key)) continue
const isExtraTarget = newConfig.some(
item => item.onChangeExtraFields && item.onChangeExtraFields.hasOwnProperty(key)
)
if (isExtraTarget || !newConfig.some(item => item.prop === key)) {
synced[key] = newModelValue[key]
}
}
return synced
}
// === Watchers ===
watch(
() => props.config,
newConfig => {
if (!newConfig || newConfig.length === 0) return
internalConfig.value = deepCloneConfig(newConfig)
localModel.value = syncModelFromProps(props.modelValue, internalConfig.value)
},
{ immediate: true }
)
watch(
() => props.modelValue,
newVal => {
if (!newVal || !internalConfig.value) return
localModel.value = syncModelFromProps(newVal, internalConfig.value)
},
{ deep: true }
)
// === 核心方法 ===
function handleModelChange(value, item) {
const newModel = { ...localModel.value, [item.prop]: value }
if (item?.type === 'select' && item.onChangeExtraFields) {
const options = getSelectOptions(item)
const opt = options.find(o => o.value === value)
if (opt?.raw) {
for (const [targetProp, sourceKey] of Object.entries(item.onChangeExtraFields)) {
newModel[targetProp] = opt.raw[sourceKey]
}
}
}
localModel.value = newModel
nextTick(() => {
if (!isEqualShallow(props.modelValue, newModel)) {
emit('update:modelValue', newModel)
}
})
if (item.type === 'select') {
emit('selectChange', item.prop, value, item)
} else if (item.type === 'upload') {
emit('uploadSuccess', item.prop, newModel)
}
}
function getSelectOptions(item) {
const key = item.prop
if (item.dictType || item.api) {
return (remoteOptions.value[key] || []).map(opt => ({
value: opt.value,
label: opt.label,
raw: opt.raw
}))
} else if (item.options) {
return item.options.map(opt => ({
value: opt.value,
label: opt.label,
raw: opt.raw
}))
}
return []
}
function getDisabledDateFn(item) {
const { minDate, maxDate } = item
if (minDate == null && maxDate == null) return () => false
return date => {
const currentDate = dayjs(date).startOf('day')
let minD = null, maxD = null
if (minDate != null) {
const val = typeof minDate === 'function' ? minDate(localModel.value) : minDate
minD = parseToDate(val)
}
if (maxDate != null) {
const val = typeof maxDate === 'function' ? maxDate(localModel.value) : maxDate
maxD = parseToDate(val)
}
if (minD && currentDate.isBefore(minD)) return true
if (maxD && currentDate.isAfter(maxD)) return true
return false
}
}
// === 远程加载 ===
async function loadDictOptions(dictType) {
const dictStore = useDictStore()
let options = dictStore.getDict(dictType)
if (options?.length) return options
try {
const resp = await getDicts(dictType)
options = resp.data.map(p => ({ label: p.itemLabel, value: p.itemValue, raw: p }))
dictStore.setDict(dictType, options)
return options
} catch (err) {
console.error(`加载字典 ${dictType} 失败`, err)
ElMessage.error(`字典 ${dictType} 加载失败`)
return []
}
}
function markDictLoaded(prop) {
dictLoaded.value.add(prop)
if (props.modelValue?.[prop] !== undefined) {
localModel.value[prop] = props.modelValue[prop]
}
}
async function loadRemoteOptionsForInit(item) {
const { prop, api, requestParams = {} } = item
try {
const res = await request({ url: api, method: 'post', data: requestParams })
const list = typeof item.transform === 'function'
? item.transform(res)
: res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
value: String(i[item.valueKey || 'value']),
label: i[item.labelKey || 'label'],
raw: i
}))
markDictLoaded(prop)
} catch (err) {
ElMessage.error(`预加载 ${item.label} 失败`)
remoteOptions.value[prop] = []
}
}
async function loadRemoteOptions(item) {
const { prop, api } = item
if (!api || remoteOptions.value[prop]?.length > 0) return
try {
remoteLoading.value[prop] = true
const res = await request({ url: api, method: 'post', data: item.requestParams || {} })
const list = typeof item.transform === 'function'
? item.transform(res)
: res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
value: String(i[item.valueKey || 'value']),
label: i[item.labelKey || 'label'],
raw: i
}))
markDictLoaded(prop)
} catch (err) {
ElMessage.error(`加载 ${item.label} 失败`)
remoteOptions.value[prop] = []
} finally {
remoteLoading.value[prop] = false
}
}
let searchTimeout = null
function handleFilterChange(keyword, item) {
const { prop, api, requestParams = {}, keywordField = 'keyword', debounceWait = 300 } = item
if (!api) return
clearTimeout(searchTimeout)
searchTimeout = setTimeout(async () => {
try {
remoteLoading.value[prop] = true
const res = await request({
url: api,
method: 'post',
data: { ...(requestParams || {}), [keywordField]: keyword }
})
const list = typeof item.transform === 'function'
? item.transform(res)
: res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
value: i[item.valueKey || 'value'],
label: i[item.labelKey || 'label'],
raw: i
}))
} catch (err) {
ElMessage.error(`搜索 ${item.label} 失败`)
} finally {
remoteLoading.value[prop] = false
}
}, debounceWait)
}
// === 初始化 ===
async function init() {
internalConfig.value = deepCloneConfig(props.config)
const dictPromises = []
const apiPromises = []
for (const item of internalConfig.value) {
const key = item.prop
if (localModel.value[key] == null) {
if (item.multiple || ['checkbox-group', 'daterange'].includes(item.type)) {
localModel.value[key] = item.defaultValue ?? []
} else {
localModel.value[key] = item.defaultValue ?? ''
}
}
if (item.type === 'select') {
if (item.dictType) {
dictPromises.push(loadDictOptions(item.dictType).then(opts => {
remoteOptions.value[key] = opts
markDictLoaded(key)
}))
} else if (item.api) {
apiPromises.push(loadRemoteOptionsForInit(item))
} else if (item.options) {
remoteOptions.value[key] = [...item.options]
markDictLoaded(key)
}
}
}
await Promise.allSettled([...dictPromises, ...apiPromises])
}
// === 暴露给 UI 和父组件 ===
return {
// refs
formRef,
localModel,
// computed
visibleConfig,
formRules,
// methods for FieldRenderer
handleModelChange,
getSelectOptions,
getDisabledDateFn,
loadRemoteOptions,
handleFilterChange,
remoteLoading,
// exposed methods for parent
getFormData: () => ({ ...localModel.value }),
async validate() {
return new Promise((resolve, reject) => {
if (!formRef.value) return resolve(localModel.value)
formRef.value.validate(valid => {
valid ? resolve({ ...localModel.value }) : reject(new Error('Validation failed'))
})
})
},
resetForm() {
const resetData = {}
internalConfig.value.forEach(item => {
const key = item.prop
if (['checkbox-group', 'daterange'].includes(item.type) || item.multiple || item.type === 'upload') {
resetData[key] = item.defaultValue ?? []
} else {
resetData[key] = item.defaultValue ?? ''
}
})
localModel.value = { ...resetData }
nextTick(() => formRef.value?.clearValidate())
},
// init
init
}
}
\ No newline at end of file
// 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
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<el-table-column prop="policyNo" label="保单号" width="200" sortable /> <el-table-column prop="policyNo" label="保单号" width="200" sortable />
<el-table-column prop="status" label="新单状态" width="120" sortable> <el-table-column prop="status" label="新单状态" width="120" sortable>
<template #default="{ row }"> <template #default="{ row }">
{{ getDictLabel('csf_policy_follow_status', row.status) }} {{ getDictLabel('csf_policy_follow_status_new', row.status) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="appointmentNo" label="预约编号" width="200" sortable /> <el-table-column prop="appointmentNo" label="预约编号" width="200" sortable />
...@@ -68,9 +68,11 @@ ...@@ -68,9 +68,11 @@
</CommonDialog> </CommonDialog>
<!-- 查看详情、更新数据 --> <!-- 查看详情、更新数据 -->
<CommonDialog :dialogTitle='mode === "viewDetail" ? "查看详情" : "更新数据"' dialogWidth='80%' :openDialog=viewDetailDialogFlag :showAction='false' <CommonDialog :dialogTitle='mode === "viewDetail" ? "查看详情" : "更新数据"' dialogWidth='80%'
:showClose='true' @close='viewDetailDialogFlag = false' @confirm='handleUpdateSubmit'> :openDialog=viewDetailDialogFlag :showAction='false' :showClose='true' @close='viewDetailDialogFlag = false'
<PolicyDetail v-model="policyDetailFormData" ref="policyDetailFormRef" @submit="onSubmit" @cancel="showDialog = false"/> @confirm='handleUpdateSubmit'>
<PolicyDetail v-model="policyDetailFormData" ref="policyDetailFormRef" @submit="onSubmit"
@cancel="viewDetailDialogFlag = false" />
</CommonDialog> </CommonDialog>
...@@ -78,7 +80,7 @@ ...@@ -78,7 +80,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, watch ,nextTick} from 'vue' import { ref, reactive, computed, watch, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import CommonPage from '@/components/commonPage' import CommonPage from '@/components/commonPage'
import SearchForm from '@/components/SearchForm/SearchForm.vue' import SearchForm from '@/components/SearchForm/SearchForm.vue'
...@@ -90,7 +92,8 @@ import { ...@@ -90,7 +92,8 @@ import {
getPolicyFollowList, getPolicyFollowList,
getExpectedCommissionList, getExpectedCommissionList,
changePolicyStatus, changePolicyStatus,
policyFollowReport policyFollowReport,
saveMailingInfo, updatePolicyfollow, batchSaveBrokers, saveInitialPayment
} from '@/api/sign/underwritingMain' } from '@/api/sign/underwritingMain'
import PolicyDetail from './policyDetail.vue' import PolicyDetail from './policyDetail.vue'
...@@ -115,7 +118,7 @@ const editStatusFormConfig = ref([ ...@@ -115,7 +118,7 @@ const editStatusFormConfig = ref([
type: 'select', type: 'select',
prop: 'status', prop: 'status',
label: '新单状态', label: '新单状态',
dictType: 'csf_policy_follow_status' dictType: 'csf_policy_follow_status_new'
}, { }, {
type: 'date', type: 'date',
prop: 'policyEffectiveDate', prop: 'policyEffectiveDate',
...@@ -155,20 +158,94 @@ const handleEditStatusSubmit = async () => { ...@@ -155,20 +158,94 @@ const handleEditStatusSubmit = async () => {
// 查看详情、更新数据 // 查看详情、更新数据
const viewDetailDialogFlag = ref(false) const viewDetailDialogFlag = ref(false)
const onSubmit = (data) => { const onSubmit = async (data) => {
console.log('提交的数据:', data) console.log('提交的数据:', data)
// 调用 API 保存 // 调用 API 保存
alert('提交成功!') // alert('提交成功!')
viewDetailDialogFlag.value = false let params = {}
if (data.activeTab === 'postal') {
params = {
policyBizId: selectedRow.value.policyBizId,
...data,
activeTab: undefined
}
const res = await saveMailingInfo(params)
if (res.code === 200) {
ElMessage.success('保存邮寄信息成功')
// viewDetailDialogFlag.value = false
} else {
ElMessage.error(res.msg || '保存邮寄信息失败')
}
} else if (data.activeTab === 'basic' || data.activeTab === 'productPlan') {
params = {
policyBizId: selectedRow.value.policyBizId,
...data,
activeTab: undefined
}
const res = await updatePolicyfollow(params)
if (res.code === 200) {
ElMessage.success('保存基本信息成功')
// viewDetailDialogFlag.value = false
} else {
ElMessage.error(res.msg || '保存基本信息失败')
}
} else if (data.activeTab === 'introducer') {
params = {
policyBizId: selectedRow.value.policyBizId,
brokerList: normalizeIntroducerData(data),
activeTab: undefined
}
const res = await batchSaveBrokers(params)
if (res.code === 200) {
ElMessage.success('保存转介人成功')
// viewDetailDialogFlag.value = false
} else {
ElMessage.error(res.msg || '保存转介人失败')
}
} else if (data.activeTab === 'attachment') {
} else if (data.activeTab === 'firstPayment') {
try {
params = {
policyBizId: selectedRow.value.policyBizId,
...data,
activeTab: undefined
}
const res = await saveInitialPayment(params)
if (res.code === 200) {
ElMessage.success('保存首期缴费成功')
// viewDetailDialogFlag.value = false
} else {
ElMessage.error(res.msg || '保存首期缴费失败')
}
} catch (error) {
console.error('首期缴费表单验证失败', error)
return
}
}
} }
const handleClose = () => { const handleClose = () => {
// 可选:清空数据 // 可选:清空数据
} }
const normalizeIntroducerData = (data) => {
if (Array.isArray(data)) return data
if (data && typeof data === 'object') {
const keys = Object.keys(data).filter(k => /^\d+$/.test(k))
if (keys.length > 0) {
return keys.sort((a, b) => a - b).map(k => data[k])
}
}
return []
}
// 获取新单状态,字典值转化方法 // 获取新单状态,字典值转化方法
onMounted(async () => { onMounted(async () => {
try { try {
await loadDicts(['csf_policy_follow_status']) await loadDicts(['csf_policy_follow_status_new'])
} catch (error) { } catch (error) {
console.error('字典加载失败', error) console.error('字典加载失败', error)
} finally { } finally {
...@@ -234,7 +311,7 @@ const searchConfig = ref([ ...@@ -234,7 +311,7 @@ const searchConfig = ref([
type: 'select', type: 'select',
prop: 'status', prop: 'status',
label: '新单状态', label: '新单状态',
dictType: 'csf_policy_follow_status' dictType: 'csf_policy_follow_status_new'
}, },
{ {
type: 'input', type: 'input',
...@@ -344,7 +421,7 @@ const handleSelect = async (e, row) => { ...@@ -344,7 +421,7 @@ const handleSelect = async (e, row) => {
viewDetailDialogFlag.value = true viewDetailDialogFlag.value = true
// 等待 DOM 更新完成 // 等待 DOM 更新完成
await nextTick() await nextTick()
if(policyDetailFormRef.value) { if (policyDetailFormRef.value) {
// 调用子组件详情查询接口 // 调用子组件详情查询接口
const data = await policyDetailFormRef.value.getPolicyfollowDetail(row.policyBizId) const data = await policyDetailFormRef.value.getPolicyfollowDetail(row.policyBizId)
} }
...@@ -412,7 +489,34 @@ const handleUpdateSubmit = async () => { ...@@ -412,7 +489,34 @@ const handleUpdateSubmit = async () => {
} }
} }
// 保存邮寄信息
const saveMailingInfoapi = async () => {
if (!basicInfoFormData.value.policyNo) {
ElMessage.error('请先输入保单号')
return
}
try {
const params = {
policyBizId: basicInfoFormData.value.policyBizId,
mailingMethod: postalFormData.value.mailingMethod,
deliveryNo: postalFormData.value.deliveryNo,
brokerSignDate: postalFormData.value.brokerSignDate,
customerSignDate: postalFormData.value.customerSignDate,
}
const res = await savePostalInfo(params)
if (res.code === 200) {
ElMessage.success('保存邮寄信息成功')
} else {
ElMessage.error(res.msg || '保存邮寄信息失败')
}
} catch (error) {
console.error('保存邮寄信息失败', error)
} finally {
}
}
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<!-- Tabs --> <!-- Tabs -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick"> <el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="基础信息" name="basic"></el-tab-pane> <el-tab-pane label="基础信息" name="basic"></el-tab-pane>
<el-tab-pane label="产品计划" name="productPlan"></el-tab-pane>
<el-tab-pane label="首期缴费" name="firstPayment"></el-tab-pane> <el-tab-pane label="首期缴费" name="firstPayment"></el-tab-pane>
<el-tab-pane label="介绍人" name="introducer"></el-tab-pane> <el-tab-pane label="介绍人" name="introducer"></el-tab-pane>
<el-tab-pane label="邮寄信息" name="postal"></el-tab-pane> <el-tab-pane label="邮寄信息" name="postal"></el-tab-pane>
...@@ -26,6 +27,10 @@ ...@@ -26,6 +27,10 @@
<SearchForm ref="policyInfoFormRef" :config="policyInfoFormConfig" v-model="policyInfoFormData" /> <SearchForm ref="policyInfoFormRef" :config="policyInfoFormConfig" v-model="policyInfoFormData" />
</div> </div>
</div>
<!-- 产品计划 Tab 内容 -->
<div v-else-if="activeTab === 'productPlan'" class="tab-content">
<!-- 基本计划 --> <!-- 基本计划 -->
<div class="section"> <div class="section">
<h3 class="sectionTitle">基本计划</h3> <h3 class="sectionTitle">基本计划</h3>
...@@ -83,7 +88,6 @@ ...@@ -83,7 +88,6 @@
</el-table> </el-table>
</div> </div>
</div> </div>
<!-- 首期缴费 Tab 内容 --> <!-- 首期缴费 Tab 内容 -->
<div v-else-if="activeTab === 'firstPayment'" class="tab-content"> <div v-else-if="activeTab === 'firstPayment'" class="tab-content">
<!-- 签单信息 --> <!-- 签单信息 -->
...@@ -109,27 +113,8 @@ ...@@ -109,27 +113,8 @@
<div class="section"> <div class="section">
<h3 class="sectionTitle">介绍人信息</h3> <h3 class="sectionTitle">介绍人信息</h3>
<h5>第一位默认是客户主要负责人,客户资料出现在介绍人(主)账号下,其他介绍人不会看到客户信息</h5> <h5>第一位默认是客户主要负责人,客户资料出现在介绍人(主)账号下,其他介绍人不会看到客户信息</h5>
<el-button type="primary"> <EditableTable v-model="introducerTableData" :row-config="introducerConfig"
<el-icon class="el-icon--right"> @batch-save="handleBatchSave" />
<Upload />
</el-icon>新增
</el-button>
<el-table :data="firstPremiumTableData" border style="width: 100%">
<el-table-column prop="date" label="介绍人姓名" width="180" />
<el-table-column prop="name" label="性别" width="180" />
<el-table-column prop="address" label="内部编号" width="180" />
<el-table-column prop="address" label="所属团队" width="180" />
<el-table-column prop="address" label="分配比例" width="180" />
<el-table-column prop="address" label="备注" width="180" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" size="small" @click="handleClick">
Detail
</el-button>
<el-button link type="primary" size="small">Edit</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
</div> </div>
<!-- 邮寄信息 Tab 内容 --> <!-- 邮寄信息 Tab 内容 -->
...@@ -144,14 +129,15 @@ ...@@ -144,14 +129,15 @@
<div class="section"> <div class="section">
<h3 class="sectionTitle">关联记录</h3> <h3 class="sectionTitle">关联记录</h3>
<el-table :data="relatedTableData" border style="width: 100%"> <el-table :data="relatedTableData" border style="width: 100%">
<el-table-column prop="date" label="流程编号" width="180" /> <el-table-column prop="fnaNo" label="流程编号" width="180" />
<el-table-column prop="policyNo" label="保单号" width="180" /> <el-table-column prop="policyNo" label="保单号" width="180" />
<el-table-column prop="name" label="客户姓名" width="180" /> <el-table-column prop="customerName" label="客户姓名" width="180" />
<el-table-column prop="address" label="创建时间" width="180" /> <el-table-column prop="createTime" label="创建时间" width="180"
:formatter="(row) => formatToDate(row.createTime)" />
<el-table-column fixed="right" label="操作" min-width="120"> <el-table-column fixed="right" label="操作" min-width="120">
<template #default> <template #default>
<el-button link type="primary" size="small" @click="handleClick"> <el-button link type="primary" size="small" @click="handleClick">
Detail 查看
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
...@@ -201,9 +187,105 @@ ...@@ -201,9 +187,105 @@
import { ref, reactive, watch, nextTick, onMounted } from 'vue' import { ref, reactive, watch, nextTick, onMounted } from 'vue'
import SearchForm from '@/components/SearchForm/SearchForm.vue' import SearchForm from '@/components/SearchForm/SearchForm.vue'
import { Delete, Edit, Search, Share, Upload } from '@element-plus/icons-vue' import { Delete, Edit, Search, Share, Upload } from '@element-plus/icons-vue'
import { getPolicyfollow, updatePolicyfollow, saveMailingInfo, batchSaveBrokers, saveInitialPayment } from '@/api/sign/underwritingMain' import { getPolicyfollow, getAttachmentList } from '@/api/sign/underwritingMain'
import { getProcessDetail } from '@/api/sign/fna'
import { premiumReconciliationList } from '@/api/sign/policy' import { premiumReconciliationList } from '@/api/sign/policy'
import { loadDicts, getDictLabel } from '@/utils/useDict' import { loadDicts, getDictLabel } from '@/utils/useDict'
import { formatToDate } from '@/utils/date'
import EditableTable from '@/components/csf-common/EditableTable.vue'
const introducerTableData = ref([])
const introducerConfig = [
{
type: 'select',
prop: 'brokerBizId',
label: '转介人',
api: '/insurance/base/api/userSaleExpand/page',
keywordField: 'realName',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入转介人名称搜索',
debounceWait: 500, // 自定义防抖时间
onChangeExtraFields: {
broker: 'realName',// 选中后自动填 broker = raw.realName
internalCode: 'code',
team: 'teamName',
phone: 'phone'
},
transform: (res) => {
return (res?.data.records || []).map(item => ({
value: item.userSaleBizId,
label: item.realName,
// 👇 把 extra 字段挂到 option 上
realName: item.realName,
code: item.code,
teamName: item.teamName,
phone: item.phone
}))
},
},
// {
// type: 'select',
// prop: 'gender',
// label: '性别',
// span: 4,
// options: [
// { value: 'male', label: '男' },
// { value: 'female', label: '女' }
// ]
// },
{
type: 'input',
prop: 'phone',
label: '手机号码',
span: 4,
disabled: true
},
{
type: 'input',
prop: 'internalCode',
label: '内部编号',
span: 4,
disabled: true
},
{
type: 'input',
prop: 'team',
label: '所属团队',
span: 4,
disabled: true
}, {
type: 'input',
prop: 'brokerRatio',
label: '分配比例%',
span: 4,
inputType: 'integer',
rules: [
{ required: true, message: '请输入比例' },
{
validator: (rule, value, cb) => {
const n = Number(value)
if (isNaN(n) || n < 0 || n > 100) cb(new Error('0~100'))
else cb()
}
}
]
}, {
type: 'textarea',
prop: 'remark',
label: '备注',
span: 4
},
]
function handleBatchSave(validRows) {
console.log('批量提交:', validRows)
// 调用 API
introducerTableData.value = validRows
}
const newOrderData = ref({})
const basicInfoFormRef = ref() const basicInfoFormRef = ref()
const basicInfoFormData = ref({}) const basicInfoFormData = ref({})
const basicInfoFormConfig = ref([ const basicInfoFormConfig = ref([
...@@ -213,7 +295,7 @@ const basicInfoFormConfig = ref([ ...@@ -213,7 +295,7 @@ const basicInfoFormConfig = ref([
label: '签单日', label: '签单日',
}, { }, {
type: 'select', type: 'select',
prop: 'signerBizId', prop: 'signer',
label: '签单员', label: '签单员',
api: '/insurance/base/api/userSignExpand/page', api: '/insurance/base/api/userSignExpand/page',
keywordField: 'realName', keywordField: 'realName',
...@@ -221,7 +303,8 @@ const basicInfoFormConfig = ref([ ...@@ -221,7 +303,8 @@ const basicInfoFormConfig = ref([
placeholder: '输入签单员姓名搜索', placeholder: '输入签单员姓名搜索',
debounceWait: 500, // 自定义防抖时间 debounceWait: 500, // 自定义防抖时间
onChangeExtraFields: { onChangeExtraFields: {
signer: 'realName',// 自动同步 raw.name 到 reconciliationCompany signerBizId: 'signerBizId',
practiceCode: 'practiceCode',
}, },
valueKey: 'userSignBizId', valueKey: 'userSignBizId',
labelKey: 'realName', labelKey: 'realName',
...@@ -235,14 +318,14 @@ const basicInfoFormConfig = ref([ ...@@ -235,14 +318,14 @@ const basicInfoFormConfig = ref([
}, { }, {
type: 'input', type: 'input',
prop: 'policyNo', prop: 'practiceCode',
label: '签单员执业编号', label: '签单员执业编号',
}, { }, {
type: 'select', type: 'select',
prop: 'signLocation', prop: 'signLocation',
label: '签单地点', label: '签单地点',
dictType: 'bx_currency_type' dictType: 'csf_ap_meeting_point'
}, },
]) ])
...@@ -296,7 +379,11 @@ const policyInfoFormConfig = ref([ ...@@ -296,7 +379,11 @@ const policyInfoFormConfig = ref([
}, { }, {
type: 'input', type: 'input',
prop: 'gracePeriod', prop: 'gracePeriod',
label: '宽限期', label: '宽限期(天)',
inputType: 'decimal',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, { }, {
type: 'select', type: 'select',
prop: 'isJoin', prop: 'isJoin',
...@@ -311,16 +398,15 @@ const basicPlanFormData = ref({}) ...@@ -311,16 +398,15 @@ const basicPlanFormData = ref({})
const basicPlanFormConfig = ref([ const basicPlanFormConfig = ref([
{ {
type: 'select', type: 'select',
prop: 'insuranceCompanyBizIdList', prop: 'insuranceCompanyBizId',
label: '保险公司', label: '保险公司',
api: '/insurance/base/api/insuranceCompany/page', api: '/insurance/base/api/insuranceCompany/page',
keywordField: 'queryContent', keywordField: 'queryContent',
requestParams: { pageNo: 1, pageSize: 20 }, requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入保险公司名称搜索', placeholder: '输入保险公司名称搜索',
debounceWait: 500, // 自定义防抖时间 debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'insuranceCompanyBizId', valueKey: 'insuranceCompanyBizId',
labelKey: 'abbreviation', labelKey: 'fullName',
transform: (res) => { transform: (res) => {
console.log(res) console.log(res)
return res?.data.records || [] return res?.data.records || []
...@@ -388,7 +474,7 @@ const basicPlanFormConfig = ref([ ...@@ -388,7 +474,7 @@ const basicPlanFormConfig = ref([
label: '是否追溯', label: '是否追溯',
dictType: 'sys_no_yes' dictType: 'sys_no_yes'
}, { }, {
type: 'select', type: 'date',
prop: 'retroactiveDate', prop: 'retroactiveDate',
label: '回溯日期', label: '回溯日期',
}, { }, {
...@@ -469,13 +555,15 @@ const firstPremiumFormConfig = ref([ ...@@ -469,13 +555,15 @@ const firstPremiumFormConfig = ref([
{ required: true, message: '请输入金额', trigger: 'blur' }, { required: true, message: '请输入金额', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '最多两位小数', trigger: 'blur' } { pattern: /^\d+(\.\d{1,2})?$/, message: '最多两位小数', trigger: 'blur' }
] ]
}, { },
type: 'select', // {
prop: 'initialPaymentStatus', // type: 'select',
label: '缴费状态', // prop: 'initialPaymentStatus',
multiple: true, // label: '缴费状态',
dictType: 'csf_fortune_status' // multiple: true,
}, { // dictType: 'reconciliation_status'
// },
{
type: 'date', type: 'date',
prop: 'latestPaymentDate', prop: 'latestPaymentDate',
label: '最晚缴费日', label: '最晚缴费日',
...@@ -498,14 +586,15 @@ const postalFormRef = ref(null) ...@@ -498,14 +586,15 @@ const postalFormRef = ref(null)
const postalFormData = ref({}) const postalFormData = ref({})
const postalFormConfig = ref([ const postalFormConfig = ref([
{ {
type: 'input', type: 'select',
prop: 'mailingMethod', prop: 'mailingMethod',
label: '寄送方式', label: '寄送方式',
dictType: 'csf_ap_mailing_method' dictType: 'csf_mailing_method'
}, { }, {
type: 'input', type: 'input',
prop: 'deliveryNo', prop: 'deliveryNo',
label: '快递单号' label: '快递单号',
visible: (formData) => formData.mailingMethod == '2'
}, { }, {
type: 'date', type: 'date',
prop: 'brokerSignDate', prop: 'brokerSignDate',
...@@ -536,11 +625,10 @@ const props = defineProps({ ...@@ -536,11 +625,10 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['update:modelValue', 'submit', 'cancel']) const emit = defineEmits(['update:modelValue', 'submit', 'cancel', 'saveRow'])
// ===== 本地响应式数据 ===== // ===== 本地响应式数据 =====
const defaultFormData = () => ({ const defaultFormData = () => ({})
})
// ✅ 使用 ref 而不是 reactive // ✅ 使用 ref 而不是 reactive
...@@ -577,14 +665,34 @@ const deleteRow = (index) => { ...@@ -577,14 +665,34 @@ const deleteRow = (index) => {
const handleTabClick = (tab) => { const handleTabClick = (tab) => {
if (tab.props.name === 'firstPayment') { if (tab.props.name === 'firstPayment') {
getPremiumReconciliationList(localData.value.policyNo) if (!basicInfoFormData.value.policyNo) return
getPremiumReconciliationList(basicInfoFormData.value.policyNo)
} else if (tab.props.name === 'postal') {
} else if (tab.props.name === 'related') {
getRelationRecord(newOrderData.value.fnaBizId)
} else if (tab.props.name === 'attachment') {
getAttachmentListDetail(newOrderData.value.policyBizId)
} }
} }
const handleSubmit = () => { const handleSubmit = () => {
formRef.value?.validate((valid) => { formRef.value?.validate((valid) => {
if (valid) { if (valid) {
emit('submit', { ...localData }) if (activeTab.value === 'postal') {
emit('submit', { ...postalFormData.value, activeTab: activeTab.value })
} else if (activeTab.value === 'firstPayment') {
emit('submit', { ...firstPremiumFormData.value, activeTab: activeTab.value })
} else if (activeTab.value === 'attachment') {
emit('submit', { ...attachmentFormData.value, activeTab: activeTab.value })
} else if (activeTab.value === 'introducer') {
emit('submit', { ...introducerTableData.value, activeTab: activeTab.value })
} else if (activeTab.value === 'basic') {
emit('submit', { ...basicInfoFormData.value, activeTab: activeTab.value, ...policyInfoFormData.value })
} else if (activeTab.value === 'productPlan') {
emit('submit', { ...basicPlanFormData.value, activeTab: activeTab.value, ...localData.additionalPlans })
}
} }
}) })
} }
...@@ -614,6 +722,8 @@ const getPolicyfollowDetail = (policyBizId) => { ...@@ -614,6 +722,8 @@ const getPolicyfollowDetail = (policyBizId) => {
getPolicyfollow(policyBizId).then(res => { getPolicyfollow(policyBizId).then(res => {
if (res.code === 200) { if (res.code === 200) {
Object.assign(localData, defaultFormData(), res.data) Object.assign(localData, defaultFormData(), res.data)
console.log('localData', localData.value)
newOrderData.value = res.data
policyInfoFormData.value = transformToFormData(res.data, policyInfoFormConfig.value); policyInfoFormData.value = transformToFormData(res.data, policyInfoFormConfig.value);
basicPlanFormData.value = transformToFormData(res.data, basicPlanFormConfig.value); basicPlanFormData.value = transformToFormData(res.data, basicPlanFormConfig.value);
basicInfoFormData.value = transformToFormData(res.data, basicInfoFormConfig.value); basicInfoFormData.value = transformToFormData(res.data, basicInfoFormConfig.value);
...@@ -627,6 +737,27 @@ const getPolicyfollowDetail = (policyBizId) => { ...@@ -627,6 +737,27 @@ const getPolicyfollowDetail = (policyBizId) => {
}) })
} }
// 查询附件列表
const getAttachmentListDetail = (policyBizId) => {
if (!policyBizId) {
return
}
const params = {
policyBizId: policyBizId,
pageNo: 1,
pageSize: 100,
}
getAttachmentList(params).then(res => {
if (res.code === 200) {
attachmentTableData.value = res.data.records || []
console.log('attachmentTableData', res.data)
}
})
}
// 附件上传方法
// 组装表单数据 // 组装表单数据
const transformToFormData = (apiData, formConfig) => { const transformToFormData = (apiData, formConfig) => {
const formData = {}; const formData = {};
...@@ -674,6 +805,15 @@ const getPremiumReconciliationList = (policyNo) => { ...@@ -674,6 +805,15 @@ const getPremiumReconciliationList = (policyNo) => {
}) })
} }
// 获取关联流程记录
const getRelationRecord = (fnaBizId) => {
getProcessDetail(fnaBizId).then(res => {
if (res.code === 200) {
relatedTableData.value = [res.data] || []
console.log('relationRecordData', res.data)
}
})
}
// 页面加载时调用 // 页面加载时调用
onMounted(async () => { onMounted(async () => {
...@@ -730,7 +870,7 @@ defineExpose({ ...@@ -730,7 +870,7 @@ defineExpose({
} }
.section h5 { .section h5 {
margin: 0 0 15px 0; margin: 0;
font-size: 14px; font-size: 14px;
line-height: 1; line-height: 1;
position: relative; position: relative;
......
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