Commit 8e482ad9 by yuzhenWang

发布测试

parent feb487f6
...@@ -146,44 +146,6 @@ ...@@ -146,44 +146,6 @@
@change="val => handleModelChange(val, item)" @change="val => handleModelChange(val, item)"
/> />
<!-- Upload 回显值得时候数据格式至少是[{url: '必须要传', name: 'name不是必须的根据需要传值'}]--> <!-- Upload 回显值得时候数据格式至少是[{url: '必须要传', name: 'name不是必须的根据需要传值'}]-->
<!-- <el-upload
v-else-if="item.type === 'upload'"
v-model:file-list="localModel[item.prop]"
:action="item.action"
:headers="item.headers"
:multiple="!!item.multiple"
:limit="item.limit || (item.multiple ? 999 : 1)"
:accept="item.accept"
:list-type="item.listType || 'text'"
:disabled="item.disabled"
:auto-upload="true"
:show-file-list="item.showFileList"
:on-exceed="handleExceed"
:before-upload="file => beforeUpload(file, item)"
:on-success="(res, file, fileList) => handleUploadSuccess(res, file, fileList, item)"
:on-error="(err, file, fileList) => handleUploadError(err, file, fileList, item)"
:on-remove="(file, fileList) => handleUploadRemove(file, fileList, item)"
>
<el-icon class="iconStyle" :size="20" v-if="item.uploadType === 'image'">
<Upload />
</el-icon>
<el-button
v-else
size="small"
type="primary"
:link="item.link"
:disabled="item.disabled"
>
{{ '点击上传文件' }}
</el-button>
<template #tip v-if="item.maxSize || item.accept">
<div class="el-upload__tip">
<span v-if="item.maxSize">大小不超过 {{ formatFileSize(item.maxSize) }}</span>
<span v-if="item.accept">支持格式:{{ item.accept }}</span>
</div>
</template>
</el-upload> -->
<!-- Upload 回显值时数据格式至少是 [{url: '必须', name: '可选'}] -->
<template v-else-if="item.type === 'upload'"> <template v-else-if="item.type === 'upload'">
<!-- 🔽 默认模式:完整使用 el-upload(含自带文件列表) --> <!-- 🔽 默认模式:完整使用 el-upload(含自带文件列表) -->
<el-upload <el-upload
...@@ -325,14 +287,12 @@ import useDictStore from '@/store/modules/dict' ...@@ -325,14 +287,12 @@ import useDictStore from '@/store/modules/dict'
import { getDicts } from '@/api/system/dict/data' import { getDicts } from '@/api/system/dict/data'
import request from '@/utils/request' import request from '@/utils/request'
import dayjs from 'dayjs' import dayjs from 'dayjs'
// ==================== 上传文件 ====================
// ==================== 文件预览弹窗 ==================== // ==================== 文件预览弹窗 ====================
const previewDialogVisible = ref(false) const previewDialogVisible = ref(false)
const previewUrl = ref('') const previewUrl = ref('')
const previewFileName = ref('') const previewFileName = ref('')
const previewFileType = ref('') // 'image', 'pdf', 'unsupported' const previewFileType = ref('') // 'image', 'pdf', 'unsupported'
//新增文件上传自定义方法开始
// 预览文件(支持图片和PDF)
// 预览文件(页面内弹窗,不打开新窗口) // 预览文件(页面内弹窗,不打开新窗口)
function previewFile(file, item) { function previewFile(file, item) {
...@@ -364,7 +324,6 @@ function removeFile(file, item) { ...@@ -364,7 +324,6 @@ function removeFile(file, item) {
const newList = fileList.filter(f => f.uid !== file.uid) const newList = fileList.filter(f => f.uid !== file.uid)
handleUploadRemove(file, newList, item) // 调用原有的删除处理函数 handleUploadRemove(file, newList, item) // 调用原有的删除处理函数
} }
//新增文件上传自定义方法结束
// 文件大小格式化 // 文件大小格式化
function formatFileSize(bytes) { function formatFileSize(bytes) {
...@@ -374,6 +333,7 @@ function formatFileSize(bytes) { ...@@ -374,6 +333,7 @@ function formatFileSize(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
} }
function beforeUpload(file, item) { function beforeUpload(file, item) {
// 检查文件大小 // 检查文件大小
if (item.maxSize && file.size > item.maxSize) { if (item.maxSize && file.size > item.maxSize) {
...@@ -393,10 +353,8 @@ function beforeUpload(file, item) { ...@@ -393,10 +353,8 @@ function beforeUpload(file, item) {
return true return true
} }
function handleUploadSuccess(response, file, fileList, item) {
// 假设你的后端返回格式:{ code: 200, data: { url: '...', name: 'xxx.pdf' } }
// 你可以通过 item.responseMap 自定义映射,这里先用通用方式
function handleUploadSuccess(response, file, fileList, item) {
const data = response.data || response const data = response.data || response
const url = data.url || data.fileUrl || data.path const url = data.url || data.fileUrl || data.path
const name = data.name || data.fileName || file.name const name = data.name || data.fileName || file.name
...@@ -416,9 +374,8 @@ function handleUploadSuccess(response, file, fileList, item) { ...@@ -416,9 +374,8 @@ function handleUploadSuccess(response, file, fileList, item) {
// 触发 model 更新 // 触发 model 更新
handleModelChange([...fileList], item) handleModelChange([...fileList], item)
ElMessage.success(`文件 ${file.name} 上传成功`) ElMessage.success(`文件 ${file.name} 上传成功`)
// console.log('上传成功', item)
} }
function handleExceed(files, fileList) { function handleExceed(files, fileList) {
ElMessage.warning('超出文件数量限制') ElMessage.warning('超出文件数量限制')
} }
...@@ -432,6 +389,7 @@ function handleUploadRemove(file, fileList, item) { ...@@ -432,6 +389,7 @@ function handleUploadRemove(file, fileList, item) {
// 用户删除文件时,同步更新 model // 用户删除文件时,同步更新 model
handleModelChange([...fileList], item) handleModelChange([...fileList], item)
} }
// ==================== 工具函数:深拷贝配置(保留函数) ==================== // ==================== 工具函数:深拷贝配置(保留函数) ====================
function deepCloneConfig(obj) { function deepCloneConfig(obj) {
if (obj === null || typeof obj !== 'object') return obj if (obj === null || typeof obj !== 'object') return obj
...@@ -445,6 +403,7 @@ function deepCloneConfig(obj) { ...@@ -445,6 +403,7 @@ function deepCloneConfig(obj) {
} }
return cloned return cloned
} }
function parseToDate(str) { function parseToDate(str) {
if (!str) return null if (!str) return null
if (str === 'today') { if (str === 'today') {
...@@ -459,6 +418,7 @@ function parseToDate(str) { ...@@ -459,6 +418,7 @@ function parseToDate(str) {
} }
return null return null
} }
// ==================== 生成 disabledDate 函数 ==================== // ==================== 生成 disabledDate 函数 ====================
function getDisabledDateFn(item) { function getDisabledDateFn(item) {
const { minDate, maxDate } = item const { minDate, maxDate } = item
...@@ -503,6 +463,7 @@ function getDisabledDateFn(item) { ...@@ -503,6 +463,7 @@ function getDisabledDateFn(item) {
return false return false
} }
} }
// ==================== Props & Emits ==================== // ==================== Props & Emits ====================
const props = defineProps({ const props = defineProps({
modelValue: { type: Object, default: () => ({}) }, modelValue: { type: Object, default: () => ({}) },
...@@ -520,9 +481,7 @@ const emit = defineEmits([ ...@@ -520,9 +481,7 @@ const emit = defineEmits([
// ==================== Refs ==================== // ==================== Refs ====================
const formRef = ref(null) const formRef = ref(null)
// 使用 shallowRef 避免深层响应式(表单通常扁平)
const localModel = ref({ ...props.modelValue }) const localModel = ref({ ...props.modelValue })
// 记录哪些字段的字典已加载
const dictLoaded = ref(new Set()) const dictLoaded = ref(new Set())
const internalConfig = ref([]) const internalConfig = ref([])
const remoteOptions = ref({}) // { prop: [options] } const remoteOptions = ref({}) // { prop: [options] }
...@@ -546,7 +505,31 @@ const formRules = computed(() => { ...@@ -546,7 +505,31 @@ const formRules = computed(() => {
return rules return rules
}) })
// 1. 外部 modelValue 变化时,安全同步(仅当内容不同时) // 同步 extra 字段:根据当前选中的 option 填充 extra 字段(不触发外部 emit)
function syncExtraFieldsForProp(prop, value) {
const item = internalConfig.value.find(i => i.prop === prop)
if (!item || item.type !== 'select' || !item.onChangeExtraFields) return false
const options = getSelectOptions(item)
const selectedOption = options.find(opt => String(opt.value) === String(value))
if (selectedOption && selectedOption.raw) {
let needUpdate = false
const newModel = { ...localModel.value }
for (const [targetProp, sourceKey] of Object.entries(item.onChangeExtraFields)) {
const extraValue = selectedOption.raw[sourceKey]
if (newModel[targetProp] !== extraValue) {
newModel[targetProp] = extraValue
needUpdate = true
}
}
if (needUpdate) {
localModel.value = newModel
return true
}
}
return false
}
// 监听 config 变化(支持动态 config) // 监听 config 变化(支持动态 config)
watch( watch(
() => props.config, () => props.config,
...@@ -559,7 +542,6 @@ watch( ...@@ -559,7 +542,6 @@ watch(
const initialModel = {} const initialModel = {}
for (const item of internalConfig.value) { for (const item of internalConfig.value) {
const key = item.prop const key = item.prop
// 优先用父传值,否则用默认值
if (props.modelValue?.[key] !== undefined) { if (props.modelValue?.[key] !== undefined) {
initialModel[key] = props.modelValue[key] initialModel[key] = props.modelValue[key]
} else if ( } else if (
...@@ -572,27 +554,22 @@ watch( ...@@ -572,27 +554,22 @@ watch(
} }
} }
// ✅ 在这里同步 modelValue(包括 extra 字段)
localModel.value = syncModelFromProps(props.modelValue, internalConfig.value) localModel.value = syncModelFromProps(props.modelValue, internalConfig.value)
// console.log('子组件监测config变化', localModel.value)
}, },
{ immediate: true } { immediate: true }
) )
// console.log('🚀 子组件 props.modelValue 初始值:', props.modelValue)
// 监听 modelValue(用于后续外部更新) // 监听 modelValue(用于后续外部更新)
watch( watch(
() => props.modelValue, () => props.modelValue,
newVal => { newVal => {
if (!newVal || !internalConfig.value) return if (!newVal || !internalConfig.value) return
// ✅ 同样使用 sync 函数
localModel.value = syncModelFromProps(newVal, internalConfig.value) localModel.value = syncModelFromProps(newVal, internalConfig.value)
// console.log('子组件监测 modelValue 变化:', localModel.value)
}, },
{ deep: true } { deep: true }
) )
// 提取同步逻辑 // 从 props 同步模型数据
function syncModelFromProps(newModelValue, newConfig) { function syncModelFromProps(newModelValue, newConfig) {
if (!newModelValue || !newConfig) return {} if (!newModelValue || !newConfig) return {}
...@@ -614,22 +591,17 @@ function syncModelFromProps(newModelValue, newConfig) { ...@@ -614,22 +591,17 @@ function syncModelFromProps(newModelValue, newConfig) {
if (!extraMap || typeof extraMap !== 'object') continue if (!extraMap || typeof extraMap !== 'object') continue
const prop = item.prop const prop = item.prop
const idValue = newModelValue[prop] // e.g. 2 const idValue = newModelValue[prop]
let sourceObj = null let sourceObj = null
// 情况1: 如果 newModelValue[prop] 本身就是对象 → 直接用(兼容旧逻辑)
if (idValue && typeof idValue === 'object') { if (idValue && typeof idValue === 'object') {
sourceObj = idValue sourceObj = idValue
} } else if (Array.isArray(item.options) && idValue !== undefined && idValue !== null) {
// 情况2: 如果是 primitive(string/number),且有 options → 反查
else if (Array.isArray(item.options) && idValue !== undefined && idValue !== null) {
// 默认用 option.value 匹配,可配置 valueKey
const valueKey = item.valueKey || 'value' const valueKey = item.valueKey || 'value'
sourceObj = item.options.find(opt => opt[valueKey] === idValue) sourceObj = item.options.find(opt => opt[valueKey] === idValue)
} }
// 如果找到了 sourceObj,就提取 extra 字段
if (sourceObj && typeof sourceObj === 'object') { if (sourceObj && typeof sourceObj === 'object') {
for (const [targetKey, subPath] of Object.entries(extraMap)) { for (const [targetKey, subPath] of Object.entries(extraMap)) {
const val = getNestedValue(sourceObj, subPath) const val = getNestedValue(sourceObj, subPath)
...@@ -646,9 +618,7 @@ function syncModelFromProps(newModelValue, newConfig) { ...@@ -646,9 +618,7 @@ function syncModelFromProps(newModelValue, newConfig) {
const extraMap = item.onChangeExtraFields const extraMap = item.onChangeExtraFields
if (!extraMap || typeof extraMap !== 'object') continue if (!extraMap || typeof extraMap !== 'object') continue
// 如果 newModelValue 中没有 sourceField,说明没有重新计算
if (newModelValue[sourceField] === undefined) { if (newModelValue[sourceField] === undefined) {
// 那么保留 localModel 中对应的 extra 字段
for (const [targetKey, subPath] of Object.entries(extraMap)) { for (const [targetKey, subPath] of Object.entries(extraMap)) {
if (localModel.value.hasOwnProperty(targetKey)) { if (localModel.value.hasOwnProperty(targetKey)) {
synced[targetKey] = localModel.value[targetKey] synced[targetKey] = localModel.value[targetKey]
...@@ -659,79 +629,60 @@ function syncModelFromProps(newModelValue, newConfig) { ...@@ -659,79 +629,60 @@ function syncModelFromProps(newModelValue, newConfig) {
// 4. 保留 newModelValue 中已有的 extra 字段和其他额外字段 // 4. 保留 newModelValue 中已有的 extra 字段和其他额外字段
for (const key in newModelValue) { for (const key in newModelValue) {
// 如果已经同步过了(比如主字段或第2步写入的extra),跳过
if (synced.hasOwnProperty(key)) continue if (synced.hasOwnProperty(key)) continue
// 判断是否是某个 extra 目标字段
const isExtraTarget = newConfig.some( const isExtraTarget = newConfig.some(
item => item.onChangeExtraFields && item.onChangeExtraFields.hasOwnProperty(key) item => item.onChangeExtraFields && item.onChangeExtraFields.hasOwnProperty(key)
) )
// 如果是 extra 字段,且 newModelValue 里有值 → 保留它!
if (isExtraTarget) { if (isExtraTarget) {
synced[key] = newModelValue[key] synced[key] = newModelValue[key]
} } else if (!newConfig.some(item => item.prop === key)) {
// 如果不是 extra,也不是主字段 → 也保留(兼容 hidden 字段等)
else if (!newConfig.some(item => item.prop === key)) {
synced[key] = newModelValue[key] synced[key] = newModelValue[key]
} }
} }
// console.log('🚀 子组件 进行modelvalue处理:', synced)
return synced return synced
} }
function getNestedValue(obj, path) { function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj) return path.split('.').reduce((current, key) => current?.[key], obj)
} }
// 当字典加载完成时,触发同步
function markDictLoaded(prop) { function markDictLoaded(prop) {
dictLoaded.value.add(prop) dictLoaded.value.add(prop)
// 尝试同步该字段
if (props.modelValue?.[prop] !== undefined) { if (props.modelValue?.[prop] !== undefined) {
localModel.value[prop] = props.modelValue[prop] localModel.value[prop] = props.modelValue[prop]
} }
} }
// 2. 用户操作导致 localModel 变化时,emit(防抖可选)
// 用户操作导致 localModel 变化时,emit
function handleModelChange(value, item) { function handleModelChange(value, item) {
console.group('用户操作导致 localModel 变化时,emit(防抖可选)')
const newModel = { ...localModel.value, [item.prop]: value } const newModel = { ...localModel.value, [item.prop]: value }
if (item?.type === 'select' && item.onChangeExtraFields) { if (item?.type === 'select' && item.onChangeExtraFields) {
const options = getSelectOptions(item) const options = getSelectOptions(item)
// console.log('可用 options:', options) const opt = options.find(o => String(o.value) === String(value))
const opt = options.find(o => o.value === value)
// console.log('匹配的 opt:', opt)
if (opt?.raw) { if (opt?.raw) {
for (const [targetProp, sourceKey] of Object.entries(item.onChangeExtraFields)) { for (const [targetProp, sourceKey] of Object.entries(item.onChangeExtraFields)) {
const extraValue = opt.raw[sourceKey] newModel[targetProp] = opt.raw[sourceKey]
newModel[targetProp] = extraValue
// console.log(`✅ 设置 ${targetProp} =`, extraValue)
} }
} }
} }
localModel.value = newModel localModel.value = newModel
// console.log('子组件用户操作后,modelvalue值==', newModel)
nextTick(() => { nextTick(() => {
if (!isEqualShallow(props.modelValue, newModel)) { if (!isEqualShallow(props.modelValue, newModel)) {
// console.log('如果新旧值不一样,反馈给父组件', newModel)
emit('update:modelValue', newModel) emit('update:modelValue', newModel)
} else {
console.log('🚫 跳过 emit:认为相等')
} }
}) })
if (item.type === 'select') { if (item.type === 'select') {
// console.log('如果是select类型,反馈给父组件', item.prop, value, item)
emit('selectChange', item.prop, value, item) emit('selectChange', item.prop, value, item)
} else if (item.type == 'upload') { } else if (item.type == 'upload') {
// 传给父组件最新的上传值newModel
emit('uploadSuccess', item.prop, newModel) emit('uploadSuccess', item.prop, newModel)
} else if (item.type == 'input') { } else if (item.type == 'input') {
emit('inputChange', item.prop, value, item) emit('inputChange', item.prop, value, item)
} }
console.groupEnd()
} }
// 辅助函数:浅比较两个对象
function isEqualShallow(a, b) { function isEqualShallow(a, b) {
const keysA = Object.keys(a) const keysA = Object.keys(a)
const keysB = Object.keys(b) const keysB = Object.keys(b)
...@@ -761,84 +712,34 @@ async function loadDictOptions(dictType) { ...@@ -761,84 +712,34 @@ async function loadDictOptions(dictType) {
dictStore.setDict(dictType, options) dictStore.setDict(dictType, options)
return options return options
} catch (err) { } catch (err) {
// console.error(`加载字典 ${dictType} 失败`, err)
ElMessage.error(`字典 ${dictType} 加载失败`) ElMessage.error(`字典 ${dictType} 加载失败`)
return [] return []
} }
} }
// ==================== 初始化 ====================
onMounted(async () => {
internalConfig.value = deepCloneConfig(props.config)
const initialData = {}
const dictTypePromises = []
const apiPromises = [] // ← 新增:收集 api 加载 promise
for (const item of internalConfig.value) {
const key = item.prop
if (localModel.value[key] == null) {
if (item.multiple) {
initialData[key] = item.defaultValue ?? []
} else if (['checkbox-group', 'daterange', 'monthrange'].includes(item.type)) {
initialData[key] = item.defaultValue ?? []
} else {
initialData[key] = item.defaultValue ?? ''
}
}
// 预加载 dictType
if (item.type === 'select' && item.dictType) {
dictTypePromises.push(
loadDictOptions(item.dictType).then(opts => {
remoteOptions.value[key] = opts
markDictLoaded(key) // ← 立即标记已加载
})
)
} // 预加载 api(远程接口)← 关键新增!
else if (item.type === 'select' && item.api) {
apiPromises.push(
loadRemoteOptionsForInit(item) // ← 专门用于初始化的加载函数
)
} else if (item.type === 'select' && item.options) {
remoteOptions.value[key] = [...item.options]
markDictLoaded(key)
}
// api 类型:延迟加载(focus 时)
}
if (Object.keys(initialData).length > 0) {
localModel.value = { ...localModel.value, ...initialData }
}
// 等待所有字典加载完成
await Promise.allSettled(dictTypePromises)
})
// ==================== 获取 select 选项 ==================== // ==================== 获取 select 选项 ====================
function getSelectOptions(item) { function getSelectOptions(item) {
const key = item.prop const key = item.prop
// 字典选项
if (item.dictType || item.api) { if (item.dictType || item.api) {
// 字典选项
return (remoteOptions.value[key] || []).map(opt => ({ return (remoteOptions.value[key] || []).map(opt => ({
value: opt.value, value: opt.value,
label: opt.label, label: opt.label,
raw: opt.raw // ← 必须存 raw raw: opt.raw
})) }))
} else if (item.options) { } else if (item.options) {
// 静态选项s
return item.options.map(opt => ({ return item.options.map(opt => ({
value: opt.value, value: opt.value,
label: opt.label, label: opt.label,
raw: opt.raw // 保留原始 raw: opt.raw
})) }))
} }
return [] return []
} }
// 初始化加载远程 API 选项(无搜索词)
async function loadRemoteOptionsForInit(item) { async function loadRemoteOptionsForInit(item) {
const { prop, api, requestParams } = item const { prop, api, requestParams } = item
try { try {
// 构造请求体:只传 requestParams,不传 keyword
const payload = { const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {}) ...(typeof requestParams === 'function' ? requestParams() : requestParams || {})
} }
...@@ -854,38 +755,45 @@ async function loadRemoteOptionsForInit(item) { ...@@ -854,38 +755,45 @@ async function loadRemoteOptionsForInit(item) {
? item.transform(res) ? item.transform(res)
: res.data?.records || res.data || [] : res.data?.records || res.data || []
// 建议:统一转成字符串(或根据 item.valueType 判断)
const newOptions = list.map(i => ({ const newOptions = list.map(i => ({
value: String(i[item.valueKey || 'value']), // ← 强制转字符串 value: String(i[item.valueKey || 'value']),
label: i[item.labelKey || 'label'], label: i[item.labelKey || 'label'],
raw: i raw: i
})) }))
remoteOptions.value[prop] = newOptions remoteOptions.value[prop] = newOptions
markDictLoaded(prop) // ← 关键:标记已加载 markDictLoaded(prop)
// 同步 extra 字段
const currentVal = localModel.value[prop]
if (currentVal !== undefined && currentVal !== null && currentVal !== '') {
syncExtraFieldsForProp(prop, currentVal)
}
} catch (err) { } catch (err) {
ElMessage.error(`预加载 ${item.label} 失败`) ElMessage.error(`预加载 ${item.label} 失败`)
remoteOptions.value[prop] = [] remoteOptions.value[prop] = []
} }
} }
// ==================== 加载远程 API 选项(首次 focus 时加载,无搜索词) ====================
// 加载远程 API 选项(focus 时调用,无搜索词)
async function loadRemoteOptions(item) { async function loadRemoteOptions(item) {
const { prop, api, requestParams, keywordField, debounceWait, ...rest } = item const { prop, api, requestParams } = item
if (!api) return if (!api) return
// 如果已经有选项且不是强制刷新,可跳过;但为了保证初次加载,remoteOptions[prop] 为空时才加载
if (remoteOptions.value[prop] && remoteOptions.value[prop].length > 0) return
try { try {
remoteLoading.value[prop] = true remoteLoading.value[prop] = true
// 构造请求体:合并 requestParams + 分页(默认第一页)
const payload = { const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {}) ...(typeof requestParams === 'function' ? requestParams() : requestParams || {})
// 注意:此时无 keyword,所以不加 keywordField
} }
const res = await request({ const res = await request({
url: api, url: api,
method: 'post', // ← 改为 POST method: 'post',
data: payload // ← 参数放 body data: payload
}) })
const list = const list =
...@@ -893,15 +801,19 @@ async function loadRemoteOptions(item) { ...@@ -893,15 +801,19 @@ async function loadRemoteOptions(item) {
? item.transform(res) ? item.transform(res)
: res.data?.records || res.data || [] : res.data?.records || res.data || []
// 建议:统一转成字符串(或根据 item.valueType 判断)
const newOptions = list.map(i => ({ const newOptions = list.map(i => ({
value: String(i[item.valueKey || 'value']), // ← 强制转字符串 value: String(i[item.valueKey || 'value']),
label: i[item.labelKey || 'label'], label: i[item.labelKey || 'label'],
raw: i raw: i
})) }))
remoteOptions.value[prop] = newOptions remoteOptions.value[prop] = newOptions
// ✅ 关键:标记该字段字典已加载
markDictLoaded(prop) markDictLoaded(prop)
// 同步 extra 字段
const currentVal = localModel.value[prop]
if (currentVal !== undefined && currentVal !== null && currentVal !== '') {
syncExtraFieldsForProp(prop, currentVal)
}
} catch (err) { } catch (err) {
ElMessage.error(`加载 ${item.label} 失败`) ElMessage.error(`加载 ${item.label} 失败`)
remoteOptions.value[prop] = [] remoteOptions.value[prop] = []
...@@ -910,7 +822,7 @@ async function loadRemoteOptions(item) { ...@@ -910,7 +822,7 @@ async function loadRemoteOptions(item) {
} }
} }
// ==================== 远程搜索(带关键词,防抖) ==================== // 远程搜索(带关键词,防抖)
let searchTimeout = null let searchTimeout = null
function handleFilterChange(keyword, item) { function handleFilterChange(keyword, item) {
const { prop, api, requestParams, keywordField = 'keyword', debounceWait = 300 } = item const { prop, api, requestParams, keywordField = 'keyword', debounceWait = 300 } = item
...@@ -921,16 +833,15 @@ function handleFilterChange(keyword, item) { ...@@ -921,16 +833,15 @@ function handleFilterChange(keyword, item) {
try { try {
remoteLoading.value[prop] = true remoteLoading.value[prop] = true
// 构造请求体:requestParams + 分页 + 动态关键词字段
const payload = { const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {}), ...(typeof requestParams === 'function' ? requestParams() : requestParams || {}),
[keywordField]: keyword // ← 动态字段名,如 name / companyName [keywordField]: keyword
} }
const res = await request({ const res = await request({
url: api, url: api,
method: 'post', // ← POST 请求 method: 'post',
data: payload // ← body 传参 data: payload
}) })
const list = const list =
...@@ -939,9 +850,9 @@ function handleFilterChange(keyword, item) { ...@@ -939,9 +850,9 @@ function handleFilterChange(keyword, item) {
: res.data?.records || res.data || [] : res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({ remoteOptions.value[prop] = list.map(i => ({
value: i[item.valueKey || 'value'], value: String(i[item.valueKey || 'value']),
label: i[item.labelKey || 'label'], label: i[item.labelKey || 'label'],
raw: i // ← 保存完整对象 raw: i
})) }))
} catch (err) { } catch (err) {
ElMessage.error(`搜索 ${item.label} 失败`) ElMessage.error(`搜索 ${item.label} 失败`)
...@@ -952,46 +863,6 @@ function handleFilterChange(keyword, item) { ...@@ -952,46 +863,6 @@ function handleFilterChange(keyword, item) {
} }
// ==================== 数字输入处理 ==================== // ==================== 数字输入处理 ====================
// function handleNumberInput(value, item) {
// const { inputType = 'text', decimalDigits = 2, prop } = item
// if (!prop) return
// let result = String(value ?? '').trim()
// if (inputType === 'integer') {
// // 只保留数字
// result = result.replace(/[^\d]/g, '')
// } else if (inputType === 'decimal') {
// // 1. 只保留数字和小数点
// result = result.replace(/[^\d.]/g, '')
// // 2. 去掉开头的小数点(不允许 ".5" → 改为 "0.5" 更好,但这里先简单处理)
// if (result.startsWith('.')) {
// result = '0.' + result.slice(1)
// }
// // 3. 保证最多一个小数点
// const parts = result.split('.')
// if (parts.length > 2) {
// result = parts[0] + '.' + parts.slice(1).join('')
// }
// // 4. 限制小数位数(但保留结尾的小数点!)
// if (result.includes('.')) {
// const [intPart, decPart] = result.split('.')
// // 如果小数部分超过限制,截断
// if (decPart.length > decimalDigits) {
// result = intPart + '.' + decPart.slice(0, decimalDigits)
// }
// // ✅ 不再删除结尾的 '.'
// }
// }
// // 防止重复赋值(可选优化)
// if (localModel.value[prop] !== result) {
// localModel.value = { ...localModel.value, [prop]: result }
// }
// }
function handleNumberInput(value, item) { function handleNumberInput(value, item) {
const { inputType = 'text', decimalDigits = 2, prop } = item const { inputType = 'text', decimalDigits = 2, prop } = item
if (!prop) return if (!prop) return
...@@ -999,83 +870,109 @@ function handleNumberInput(value, item) { ...@@ -999,83 +870,109 @@ function handleNumberInput(value, item) {
let result = String(value ?? '').trim() let result = String(value ?? '').trim()
if (inputType === 'integer') { if (inputType === 'integer') {
// 只保留数字和负号
result = result.replace(/[^-\d]/g, '') result = result.replace(/[^-\d]/g, '')
// 如果有多个负号或者负号不在开头,则移除多余的负号
if ((result.match(/-/g) || []).length > 1) { if ((result.match(/-/g) || []).length > 1) {
result = result.replace(/-/g, '').replace(/^/, '-') // 仅保留一个负号在最前面 result = result.replace(/-/g, '').replace(/^/, '-')
} }
} else if (inputType === 'decimalNumber') { } else if (inputType === 'decimalNumber') {
// 可以输入正数,负数,小数
// 1. 只保留数字、小数点和负号
result = result.replace(/[^-\d.]/g, '') result = result.replace(/[^-\d.]/g, '')
// 2. 处理负号:确保最多只有一个负号且必须在开头
if ((result.match(/-/g) || []).length > 1) { if ((result.match(/-/g) || []).length > 1) {
result = result.replace(/-/g, '') // 移除所有负号 result = result.replace(/-/g, '')
if (result.startsWith('-')) { if (result.startsWith('-')) {
result = '-' + result.slice(1) // 确保负号在最前面 result = '-' + result.slice(1)
} else { } else {
result = '-' + result // 如果原本没有负号但需要保留数值,可以省略此步骤 result = '-' + result
} }
} }
// 3. 去掉开头的小数点(不允许 ".5" → 改为 "0.5" 更好)
if (result.startsWith('.')) { if (result.startsWith('.')) {
result = '0' + result result = '0' + result
} else if (result.startsWith('-.')) { } else if (result.startsWith('-.')) {
result = '-0' + result.slice(2) result = '-0' + result.slice(2)
} }
// 4. 保证最多一个小数点
const parts = result.split('.') const parts = result.split('.')
if (parts.length > 2) { if (parts.length > 2) {
result = parts[0] + '.' + parts.slice(1).join('') result = parts[0] + '.' + parts.slice(1).join('')
} }
// 5. 限制小数位数(但保留结尾的小数点!)
if (result.includes('.')) { if (result.includes('.')) {
const [intPart, decPart] = result.split('.') const [intPart, decPart] = result.split('.')
// 如果小数部分超过限制,截断
if (decPart.length > decimalDigits) { if (decPart.length > decimalDigits) {
result = intPart + '.' + decPart.slice(0, decimalDigits) result = intPart + '.' + decPart.slice(0, decimalDigits)
} }
// ✅ 不再删除结尾的 '.'
} }
} else if (inputType === 'decimal') { } else if (inputType === 'decimal') {
// 可以输入正整数和小数
// 1. 只保留数字和小数点
result = result.replace(/[^\d.]/g, '') result = result.replace(/[^\d.]/g, '')
// 2. 去掉开头的小数点(不允许 ".5" → 改为 "0.5" 更好,但这里先简单处理)
if (result.startsWith('.')) { if (result.startsWith('.')) {
result = '0.' + result.slice(1) result = '0.' + result.slice(1)
} }
// 3. 保证最多一个小数点
const parts = result.split('.') const parts = result.split('.')
if (parts.length > 2) { if (parts.length > 2) {
result = parts[0] + '.' + parts.slice(1).join('') result = parts[0] + '.' + parts.slice(1).join('')
} }
// 4. 限制小数位数(但保留结尾的小数点!)
if (result.includes('.')) { if (result.includes('.')) {
const [intPart, decPart] = result.split('.') const [intPart, decPart] = result.split('.')
// 如果小数部分超过限制,截断
if (decPart.length > decimalDigits) { if (decPart.length > decimalDigits) {
result = intPart + '.' + decPart.slice(0, decimalDigits) result = intPart + '.' + decPart.slice(0, decimalDigits)
} }
// ✅ 不再删除结尾的 '.'
} }
} }
// 防止重复赋值(可选优化)
if (localModel.value[prop] !== result) { if (localModel.value[prop] !== result) {
localModel.value = { ...localModel.value, [prop]: result } localModel.value = { ...localModel.value, [prop]: result }
} }
} }
// ==================== 初始化 ====================
onMounted(async () => {
internalConfig.value = deepCloneConfig(props.config)
const initialData = {}
const dictTypePromises = []
const apiPromises = []
for (const item of internalConfig.value) {
const key = item.prop
if (localModel.value[key] == null) {
if (item.multiple) {
initialData[key] = item.defaultValue ?? []
} else if (['checkbox-group', 'daterange', 'monthrange'].includes(item.type)) {
initialData[key] = item.defaultValue ?? []
} else {
initialData[key] = item.defaultValue ?? ''
}
}
if (item.type === 'select' && item.dictType) {
dictTypePromises.push(
loadDictOptions(item.dictType).then(opts => {
remoteOptions.value[key] = opts
markDictLoaded(key)
})
)
} else if (item.type === 'select' && item.api) {
apiPromises.push(loadRemoteOptionsForInit(item))
} else if (item.type === 'select' && item.options) {
remoteOptions.value[key] = [...item.options]
markDictLoaded(key)
}
}
if (Object.keys(initialData).length > 0) {
localModel.value = { ...localModel.value, ...initialData }
}
// 等待所有字典和远程选项加载完成
await Promise.allSettled([...dictTypePromises, ...apiPromises])
// 所有 select 选项加载完成后,为有 onChangeExtraFields 且有值的字段填充 extra 字段
for (const item of internalConfig.value) {
if (item.type === 'select' && item.onChangeExtraFields) {
const currentVal = localModel.value[item.prop]
if (currentVal !== undefined && currentVal !== null && currentVal !== '') {
syncExtraFieldsForProp(item.prop, currentVal)
}
}
}
})
// ==================== 暴露方法 ==================== // ==================== 暴露方法 ====================
defineExpose({ defineExpose({
getFormData() { getFormData() {
...@@ -1096,7 +993,7 @@ defineExpose({ ...@@ -1096,7 +993,7 @@ defineExpose({
if (['checkbox-group', 'daterange', 'monthrange'].includes(item.type) || item.multiple) { if (['checkbox-group', 'daterange', 'monthrange'].includes(item.type) || item.multiple) {
resetData[key] = item.defaultValue ?? [] resetData[key] = item.defaultValue ?? []
} else if (item.type === 'upload') { } else if (item.type === 'upload') {
resetData[key] = item.defaultValue ?? [] // upload 也是数组 resetData[key] = item.defaultValue ?? []
} else { } else {
resetData[key] = item.defaultValue ?? '' resetData[key] = item.defaultValue ?? ''
} }
...@@ -1104,36 +1001,20 @@ defineExpose({ ...@@ -1104,36 +1001,20 @@ defineExpose({
localModel.value = { ...resetData } localModel.value = { ...resetData }
nextTick(() => formRef.value?.clearValidate()) nextTick(() => formRef.value?.clearValidate())
}, },
// ✅ 新增:强制刷新某个字段的远程选项
async refreshRemoteOptions(targetProp) { async refreshRemoteOptions(targetProp) {
console.log(`[SearchForm] 收到刷新请求: ${targetProp}`)
// 1. 查找配置项
const item = internalConfig.value.find(i => i.prop === targetProp) const item = internalConfig.value.find(i => i.prop === targetProp)
if (!item) { if (!item) {
console.warn(`[SearchForm] 未找到 prop 为 ${targetProp} 的配置项`) console.warn(`[SearchForm] 未找到 prop 为 ${targetProp} 的配置项`)
return return
} }
if (item.type !== 'select' || !item.api) { if (item.type !== 'select' || !item.api) {
console.warn(`[SearchForm] 字段 ${targetProp} 不是远程 Select 或没有 API`) console.warn(`[SearchForm] 字段 ${targetProp} 不是远程 Select 或没有 API`)
return return
} }
console.log(`[SearchForm] 开始强制加载 ${targetProp} 的数据,API: ${item.api}`)
// 2. 关键:在调用前,先清空旧数据,防止子组件内部的 "已加载则跳过" 逻辑生效
// 如果你的 loadRemoteOptions 里有 "if (remoteOptions.value[prop]?.length > 0) return"
// 这里必须先清空
remoteOptions.value[targetProp] = [] remoteOptions.value[targetProp] = []
remoteLoading.value[targetProp] = true // 手动开启 loading remoteLoading.value[targetProp] = true
try { try {
// 3. 调用内部加载函数
// 注意:直接调用 loadRemoteOptions,它会读取最新的 requestParams
await loadRemoteOptions(item) await loadRemoteOptions(item)
console.log(`[SearchForm] ${targetProp} 加载完成`)
} catch (error) { } catch (error) {
console.error(`[SearchForm] ${targetProp} 加载失败`, error) console.error(`[SearchForm] ${targetProp} 加载失败`, error)
throw error throw error
...@@ -1145,34 +1026,28 @@ defineExpose({ ...@@ -1145,34 +1026,28 @@ defineExpose({
</script> </script>
<style scoped> <style scoped>
/* 预览弹窗样式 */
.preview-container { .preview-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 400px; min-height: 400px;
} }
.preview-image-wrapper { .preview-image-wrapper {
text-align: center; text-align: center;
} }
.preview-image { .preview-image {
max-width: 100%; max-width: 100%;
max-height: 70vh; max-height: 70vh;
object-fit: contain; object-fit: contain;
} }
.preview-pdf { .preview-pdf {
width: 100%; width: 100%;
height: 70vh; height: 70vh;
} }
.preview-unsupported { .preview-unsupported {
text-align: center; text-align: center;
padding: 40px; padding: 40px;
} }
.preview-unsupported p { .preview-unsupported p {
margin: 16px 0; margin: 16px 0;
color: #909399; color: #909399;
...@@ -1180,14 +1055,12 @@ defineExpose({ ...@@ -1180,14 +1055,12 @@ defineExpose({
.custom-upload-wrapper { .custom-upload-wrapper {
width: 100%; width: 100%;
} }
.custom-file-list { .custom-file-list {
margin-top: 12px; margin-top: 12px;
border: 1px solid #dcdfe6; border: 1px solid #dcdfe6;
border-radius: 4px; border-radius: 4px;
background-color: #fff; background-color: #fff;
} }
.file-item { .file-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
...@@ -1195,11 +1068,9 @@ defineExpose({ ...@@ -1195,11 +1068,9 @@ defineExpose({
padding: 8px 12px; padding: 8px 12px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
.file-item:last-child { .file-item:last-child {
border-bottom: none; border-bottom: none;
} }
.file-name { .file-name {
flex: 1; flex: 1;
font-size: 14px; font-size: 14px;
...@@ -1209,7 +1080,6 @@ defineExpose({ ...@@ -1209,7 +1080,6 @@ defineExpose({
white-space: nowrap; white-space: nowrap;
margin-right: 16px; margin-right: 16px;
} }
.file-actions { .file-actions {
display: flex; display: flex;
gap: 12px; gap: 12px;
...@@ -1217,11 +1087,9 @@ defineExpose({ ...@@ -1217,11 +1087,9 @@ defineExpose({
.formBox { .formBox {
box-sizing: border-box; box-sizing: border-box;
} }
.search-form-item { .search-form-item {
margin-bottom: 20px; margin-bottom: 20px;
} }
.iconStyle { .iconStyle {
color: #409eff; color: #409eff;
} }
......
...@@ -434,9 +434,9 @@ const searchConfig = ref([ ...@@ -434,9 +434,9 @@ const searchConfig = ref([
} }
}, },
{ {
type: 'daterange', type: 'monthrange',
prop: 'payoutDate', prop: 'payoutDate',
label: '出账(估)', label: '出账(估)',
startPlaceholder: '开始时间', startPlaceholder: '开始时间',
endPlaceholder: '结束时间' endPlaceholder: '结束时间'
}, },
...@@ -712,7 +712,7 @@ const confirmRateExchange = async () => { ...@@ -712,7 +712,7 @@ const confirmRateExchange = async () => {
rateExchangeFlag.value = false rateExchangeFlag.value = false
loadTableData() loadTableData()
} catch (error) { } catch (error) {
ElMessage.success('结算汇率修改失败') ElMessage.error('结算汇率修改失败')
rateExchangeFlag.value = true rateExchangeFlag.value = true
if (error.message && error.message.includes('Validation')) { if (error.message && error.message.includes('Validation')) {
ElMessage.error('必填项不能为空') ElMessage.error('必填项不能为空')
...@@ -1099,12 +1099,20 @@ const addCheckRecordConfig = [ ...@@ -1099,12 +1099,20 @@ const addCheckRecordConfig = [
}, },
{ {
type: 'date', type: 'month',
prop: 'payoutDate', prop: 'payoutDate',
label: '出账日期', label: '出账月(估)',
placeholder: '请选择',
maxDate: 'today',
rules: [{ required: true, message: '出账月(估)必填', trigger: 'blur' }]
},
{
type: 'month',
prop: 'actualPayoutDate',
label: '出账月(实)',
placeholder: '请选择', placeholder: '请选择',
maxDate: 'today', maxDate: 'today',
rules: [{ required: true, message: '出账日期必填', trigger: 'blur' }] rules: [{ required: true, message: '出账月(实)必填', trigger: 'blur' }]
}, },
// { // {
// type: 'input', // type: 'input',
......
...@@ -156,7 +156,7 @@ ...@@ -156,7 +156,7 @@
/> />
<el-table-column fixed="right" label="操作" min-width="120"> <el-table-column fixed="right" label="操作" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-popover placement="right" :width="200" trigger="click"> <el-popover placement="right" :width="200" trigger="click" v-if="row.type == '1'">
<template #reference> <template #reference>
<el-icon> <el-icon>
<MoreFilled /> <MoreFilled />
...@@ -797,16 +797,16 @@ const updatePayRecordFormConfig = [ ...@@ -797,16 +797,16 @@ const updatePayRecordFormConfig = [
prop: 'fortunePeriod', prop: 'fortunePeriod',
label: '佣金期数', label: '佣金期数',
inputType: 'decimal', inputType: 'decimal',
visible: formData => formData.fortuneBizType === 'R', visible: formData => formData.fortuneBizType === 'R'
rules: [{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }] // rules: [{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }]
}, },
{ {
type: 'input', type: 'input',
prop: 'fortuneTotalPeriod', prop: 'fortuneTotalPeriod',
label: '总期数', label: '总期数',
inputType: 'decimal', inputType: 'decimal',
visible: formData => formData.fortuneBizType === 'R', visible: formData => formData.fortuneBizType === 'R'
rules: [{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }] // rules: [{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }]
}, },
{ {
type: 'select', type: 'select',
...@@ -1333,7 +1333,13 @@ const handleConfirmAddPayRecord = async () => { ...@@ -1333,7 +1333,13 @@ const handleConfirmAddPayRecord = async () => {
const handleConfirmUpdatePayRecord = async () => { const handleConfirmUpdatePayRecord = async () => {
if (selectedRow.value.type == '1') { if (selectedRow.value.type == '1') {
try { try {
const formData = updatePayRecordFormRef.value.getFormData() const formData = await updatePayRecordFormRef.value.validate()
console.log('====================================')
console.log('formData', formData)
console.log('====================================')
if (!formData) {
return
}
const params = { const params = {
...formData, ...formData,
expectedFortuneBizId: selectedRow.value.expectedFortuneBizId expectedFortuneBizId: selectedRow.value.expectedFortuneBizId
...@@ -1346,7 +1352,10 @@ const handleConfirmUpdatePayRecord = async () => { ...@@ -1346,7 +1352,10 @@ const handleConfirmUpdatePayRecord = async () => {
loadPayRecordTableData(selectedRow.value.expectedFortuneBizId) loadPayRecordTableData(selectedRow.value.expectedFortuneBizId)
expectedFortuneListData() expectedFortuneListData()
} catch (error) { } catch (error) {
ElMessage.error(error.message) if (error.message && error.message.includes('Validation')) {
ElMessage.error('必填项不能为空')
}
ElMessage.error('更新失败')
} }
} }
} }
......
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