Commit f1c03b86 by zhangxingmin

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

parents 60967843 e51523a1
import request from '@/utils/request'
// 获取制作薪资单列表
export function getSalaryList(data) {
return request({
url: '/csf/api/salary/page',
method: 'post',
data: data
})
}
// 批量新增薪资单
export function batchAddSalary(data) {
return request({
url: '/csf/api/salary/batch/add',
method: 'post',
data: data
})
}
// 计算实发金额
export function calculatePaidAmount(data) {
return request({
url: '/csf/api/salary/calculate/paidAmount',
method: 'post',
data: data
})
}
// 计算汇率
export function getExchangeRate(data) {
return request({
url: '/csf/api/salary/get/exchangeRate',
method: 'post',
data: data
})
}
// 计算实发金额
export function grossAmountApi(data) {
return request({
url: '/csf/api/salary/get/grossAmount',
method: 'post',
data: data
})
}
// 获取制作薪资单详情
export function updateSalaryDetail(salaryBizId) {
return request({
url: '/csf/api/salary/detail?salaryBizId=' + salaryBizId,
method: 'get'
})
}
// 计算信息汇款明细总金额
export function calculateTotalAmountApi(data) {
return request({
url: '/csf/api/salary/calculate/totalAmount',
method: 'post',
data: data
})
}
// 修改单个薪资单
export function editSingleSalaryApi(data) {
return request({
url: '/csf/api/salary/edit',
method: 'put',
data: data
})
}
// 新增单个薪资单
export function addSingleSalaryApi(data) {
return request({
url: '/csf/api/salary/add',
method: 'post',
data: data
})
}
// 取消薪资单
export function cancelSalaryApi(data) {
return request({
url: '/csf/api/salary/cancel',
method: 'put',
data: data
})
}
//删除银行
export function delSalaryApi(salaryRemittanceBizId) {
return request({
url: `/csf/api/salary/delSalaryRemittance?salaryRemittanceBizId=${salaryRemittanceBizId}`,
method: 'delete'
})
}
// 电子薪资单列表
export function electronicSalaryListApi(data) {
return request({
url: '/csf/api/salary/push/page',
method: 'post',
data: data
})
}
//提交核对
export function submitCheckApi(data) {
return request({
url: '/csf/api/salary/submit/check',
method: 'put',
data: data
})
}
//推送电子薪资单
export function pushSalaryApi(data) {
return request({
url: '/csf/api/salary/push',
method: 'put',
data: data
})
}
//核对/退回电子薪资单
export function checkSalaryApi(data) {
return request({
url: '/csf/api/salary/check',
method: 'put',
data: data
})
}
// 下载选中的发佣数据
export function downloadPolicyFortune(data) {
return request({
url: '/csf/api/fortune/download/raw',
method: 'post',
data: data,
responseType: 'blob'
})
}
// 应收款导出
export function exportReceivedFortune(data) {
return request({
url: '/csf/api/CommissionExpected/export',
method: 'post',
data: data,
responseType: 'blob'
})
}
......@@ -42,29 +42,6 @@
"
:parser="value => value.replace(/\$\s?|(,*)/g, '')"
/>
<!-- Select (支持 dictType / api / options) -->
<!-- <el-select
v-else-if="item.type === 'select'"
v-model="localModel[item.prop]"
:multiple="!!item.multiple"
:placeholder="item.placeholder || `请选择${item.label}`"
:clearable="true"
filterable
:disabled="item.disabled"
:loading="remoteLoading[item.prop] || false"
@change="val => handleModelChange(val, item)"
@focus="() => loadRemoteOptions(item)"
@filter-change="keyword => handleFilterChange(keyword, item)"
>
<el-option
v-for="opt in getSelectOptions(item)"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select> -->
<!-- @focus="() => loadRemoteOptions(item, '')" -->
<el-select
v-else-if="item.type === 'select'"
:ref="
......@@ -630,10 +607,15 @@ function syncModelFromProps(newModelValue, newConfig) {
if (idValue && typeof idValue === 'object') {
sourceObj = idValue
} else if (Array.isArray(item.options) && idValue !== undefined && idValue !== null) {
} else if (idValue !== undefined && idValue !== null) {
const opts = getSelectOptions(item)
const valueKey = item.valueKey || 'value'
sourceObj = item.options.find(opt => opt[valueKey] === idValue)
sourceObj = opts.find(opt => opt[valueKey] === 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)) {
......@@ -764,24 +746,53 @@ async function loadDictOptions(dictType) {
}
// ==================== 获取 select 选项 ====================
// 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 []
// }
// 替换原有的 getSelectOptions 函数
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
}))
// 静态选项或函数选项
if (typeof item.options === 'function') {
// 传入当前表单数据(localModel.value)作为 row
const opts = item.options(localModel.value)
return opts.map(opt => ({
value: opt.value,
label: opt.label,
raw: opt.raw || opt
}))
} else if (Array.isArray(item.options)) {
return item.options.map(opt => ({
value: opt.value,
label: opt.label,
raw: opt.raw || opt
}))
}
}
return []
}
// 初始化加载远程 API 选项(无搜索词)
async function loadRemoteOptionsForInit(item) {
const { prop, api, requestParams } = item
......@@ -821,41 +832,35 @@ async function loadRemoteOptionsForInit(item) {
}
}
// 加载远程 API 选项(focus 时调用,无搜索词)
// async function loadRemoteOptions(item) {
// const { prop, api, requestParams } = item
// 修改原有的 loadRemoteOptions,使其支持关键词参数
// async function loadRemoteOptions(item, keyword = '') {
// const { prop, api, requestParams, keywordField = 'keyword' } = item
// if (!api) return
// // 如果已经有选项且不是强制刷新,可跳过;但为了保证初次加载,remoteOptions[prop] 为空时才加载
// if (remoteOptions.value[prop] && remoteOptions.value[prop].length > 0) return
// try {
// remoteLoading.value[prop] = true
// const payload = {
// ...(typeof requestParams === 'function' ? requestParams() : requestParams || {})
// let params = typeof requestParams === 'function' ? requestParams() : requestParams || {}
// if (keyword) {
// params = { ...params, [keywordField]: keyword }
// } else {
// // 可选:重置分页参数为第一页
// params = { ...params, pageNo: 1 }
// }
// const res = await request({
// url: api,
// method: 'post',
// data: payload
// })
// const res = await request({ url: api, method: 'post', data: params })
// const list =
// typeof item.transform === 'function'
// ? item.transform(res)
// : res.data?.records || res.data || []
// const newOptions = list.map(i => ({
// remoteOptions.value[prop] = list.map(i => ({
// value: String(i[item.valueKey || 'value']),
// label: i[item.labelKey || 'label'],
// raw: i
// }))
// remoteOptions.value[prop] = newOptions
// markDictLoaded(prop)
// // 同步 extra 字段
// const currentVal = localModel.value[prop]
// if (currentVal !== undefined && currentVal !== null && currentVal !== '') {
// syncExtraFieldsForProp(prop, currentVal)
......@@ -867,9 +872,9 @@ async function loadRemoteOptionsForInit(item) {
// remoteLoading.value[prop] = false
// }
// }
// 修改原有的 loadRemoteOptions,使其支持关键词参
// 修改 loadRemoteOptions 函
async function loadRemoteOptions(item, keyword = '') {
const { prop, api, requestParams, keywordField = 'keyword' } = item
const { prop, api, requestParams, keywordField = 'keyword', method = 'post' } = item
if (!api) return
try {
......@@ -879,11 +884,22 @@ async function loadRemoteOptions(item, keyword = '') {
if (keyword) {
params = { ...params, [keywordField]: keyword }
} else {
// 可选:重置分页参数为第一页
params = { ...params, pageNo: 1 }
}
const res = await request({ url: api, method: 'post', data: params })
// 根据 method 决定参数位置
const requestConfig = {
url: api,
method: method
}
if (method.toLowerCase() === 'get') {
requestConfig.params = params
} else {
requestConfig.data = params
}
const res = await request(requestConfig)
const list =
typeof item.transform === 'function'
? item.transform(res)
......@@ -1044,10 +1060,11 @@ onMounted(async () => {
)
} else if (item.type === 'select' && item.api) {
apiPromises.push(loadRemoteOptions(item, ''))
} else if (item.type === 'select' && item.options) {
remoteOptions.value[key] = [...item.options]
markDictLoaded(key)
}
} else if (item.type === 'select' && item.options) {
// 支持 options 为数组或函数(函数调用时传入当前表单数据)
remoteOptions.value[key] = getSelectOptions(item)
markDictLoaded(key)
}
}
if (Object.keys(initialData).length > 0) {
......@@ -1100,42 +1117,6 @@ defineExpose({
formRef.value?.clearValidate()
})
},
// resetForm() {
// const resetData = {}
// internalConfig.value.forEach(item => {
// const key = item.prop
// if (['checkbox-group', 'daterange', 'monthrange'].includes(item.type) || item.multiple) {
// resetData[key] = item.defaultValue ?? []
// } else if (item.type === 'upload') {
// resetData[key] = item.defaultValue ?? []
// } else {
// resetData[key] = item.defaultValue ?? ''
// }
// })
// localModel.value = { ...resetData }
// nextTick(() => formRef.value?.clearValidate())
// },
// async refreshRemoteOptions(targetProp) {
// const item = internalConfig.value.find(i => i.prop === targetProp)
// if (!item) {
// console.warn(`[SearchForm] 未找到 prop 为 ${targetProp} 的配置项`)
// return
// }
// if (item.type !== 'select' || !item.api) {
// console.warn(`[SearchForm] 字段 ${targetProp} 不是远程 Select 或没有 API`)
// return
// }
// remoteOptions.value[targetProp] = []
// remoteLoading.value[targetProp] = true
// try {
// await loadRemoteOptions(item)
// } catch (error) {
// console.error(`[SearchForm] ${targetProp} 加载失败`, error)
// throw error
// } finally {
// remoteLoading.value[targetProp] = false
// }
// }
async refreshRemoteOptions(targetProp) {
const item = internalConfig.value.find(i => i.prop === targetProp)
if (!item || item.type !== 'select' || !item.api) return
......
......@@ -24,7 +24,6 @@
</template>
<template #default="{ row, $index }">
<!-- 可编辑列:始终显示编辑组件 -->
<div v-if="col.editable !== false">
<el-form-item
:rules="getColumnRules(col)"
......@@ -57,10 +56,9 @@
:loading="remoteLoading[col.prop]"
:disabled="col.disabled"
@update:model-value="val => handleSelectChange(row, col, val, $index)"
@focus="() => loadRemoteOptions(col)"
>
<el-option
v-for="opt in getSelectOptions(col)"
v-for="opt in getSelectOptions(col, row)"
:key="opt.value"
:label="opt.label"
:value="opt.value"
......@@ -174,7 +172,7 @@
<div v-else>
<slot :name="`column-${col.prop}`" :row="row" :column="col" :value="row[col.prop]">
<span v-if="col.editType === 'select'">
{{ getSelectLabel(col, row[col.prop]) }}
{{ getSelectLabel(col, row[col.prop], row) }}
</span>
<span v-else-if="col.editType === 'upload' && row[col.prop]">
<span v-for="(file, idx) in getFileListDisplay(row[col.prop])" :key="idx">
......@@ -333,7 +331,7 @@ function getColumnRules(col) {
}
// 获取 Select 选项列表
function getSelectOptions(col) {
function getSelectOptions(col, row) {
const key = col.prop
if (col.dictType) {
const dictStore = useDictStore()
......@@ -348,14 +346,24 @@ function getSelectOptions(col) {
}))
}
if (col.options) {
return col.options.map(opt => ({ value: opt.value, label: opt.label, raw: opt.raw }))
if (typeof col.options === 'function') {
// 传入当前行数据,返回选项数组(可以是原始对象数组,也可以是 {value, label} 数组)
const result = col.options(row)
return result.map(opt => ({
value: opt.value ?? opt,
label: opt.label ?? opt,
raw: opt.raw ?? opt
}))
} else {
return col.options.map(opt => ({ value: opt.value, label: opt.label, raw: opt.raw }))
}
}
return []
}
// 获取 Select 选中项的 label
function getSelectLabel(col, value) {
const options = getSelectOptions(col)
function getSelectLabel(col, value, row) {
const options = getSelectOptions(col, row)
if (value === undefined || value === null) return '-'
if (col.multiple && Array.isArray(value)) {
return value.map(v => options.find(opt => opt.value === v)?.label || v).join(', ')
......@@ -543,10 +551,9 @@ function handleCellChange(row, col, newValue, rowIndex) {
function handleSelectChange(row, col, value, rowIndex) {
const newTableData = [...props.tableData]
const updatedRow = { ...row, [col.prop]: value }
// 处理额外字段赋值
if (col.onChangeExtraFields) {
const options = getSelectOptions(col)
const options = getSelectOptions(col, row)
const opt = options.find(o => o.value === value)
if (opt?.raw) {
for (const [targetProp, sourceKey] of Object.entries(col.onChangeExtraFields)) {
......@@ -557,7 +564,7 @@ function handleSelectChange(row, col, value, rowIndex) {
newTableData[rowIndex] = updatedRow
emit('update:tableData', newTableData)
emit('selectChange', col.prop, value, col)
emit('selectChange', { prop: col.prop, value, col, rowIndex })
validateField(updatedRow, col.prop)
}
......@@ -679,8 +686,32 @@ async function loadDictOptions(dictType) {
}
}
// async function loadRemoteOptions(col) {
// const { prop, api, requestParams } = col
// if (!api) return
// if (remoteOptions.value[prop]?.length > 0) return
// try {
// remoteLoading.value[prop] = true
// const payload = {
// ...(typeof requestParams === 'function' ? requestParams() : requestParams || {})
// }
// const res = await request({ url: api, method: 'post', data: payload })
// const list =
// typeof col.transform === 'function' ? col.transform(res) : res.data?.records || res.data || []
// remoteOptions.value[prop] = list.map(i => ({
// value: String(i[col.valueKey || 'value']),
// label: i[col.labelKey || 'label'],
// raw: i
// }))
// } catch (err) {
// ElMessage.error(`加载 ${col.label} 失败`)
// remoteOptions.value[prop] = []
// } finally {
// remoteLoading.value[prop] = false
// }
// }
async function loadRemoteOptions(col) {
const { prop, api, requestParams } = col
const { prop, api, requestParams, method = 'post' } = col
if (!api) return
if (remoteOptions.value[prop]?.length > 0) return
try {
......@@ -688,7 +719,17 @@ async function loadRemoteOptions(col) {
const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {})
}
const res = await request({ url: api, method: 'post', data: payload })
const config = {
url: api,
method: method.toLowerCase()
}
// GET 请求参数放在 params,其他放在 data
if (config.method === 'get') {
config.params = payload
} else {
config.data = payload
}
const res = await request(config)
const list =
typeof col.transform === 'function' ? col.transform(res) : res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
......@@ -703,20 +744,63 @@ async function loadRemoteOptions(col) {
remoteLoading.value[prop] = false
}
}
let searchTimeouts = {}
// async function handleRemoteSearch(keyword, col) {
// const { prop, api, requestParams, keywordField = 'keyword', debounceWait = 300 } = col
// if (!api) return
// clearTimeout(searchTimeouts[prop])
// searchTimeouts[prop] = setTimeout(async () => {
// try {
// remoteLoading.value[prop] = true
// const payload = {
// ...(typeof requestParams === 'function' ? requestParams() : requestParams || {}),
// [keywordField]: keyword
// }
// const res = await request({ url: api, method: 'post', data: payload })
// const list =
// typeof col.transform === 'function'
// ? col.transform(res)
// : res.data?.records || res.data || []
// remoteOptions.value[prop] = list.map(i => ({
// value: String(i[col.valueKey || 'value']),
// label: i[col.labelKey || 'label'],
// raw: i
// }))
// } catch (err) {
// ElMessage.error(`搜索 ${col.label} 失败`)
// } finally {
// remoteLoading.value[prop] = false
// }
// }, debounceWait)
// }
async function handleRemoteSearch(keyword, col) {
const { prop, api, requestParams, keywordField = 'keyword', debounceWait = 300 } = col
const {
prop,
api,
requestParams,
keywordField = 'keyword',
debounceWait = 300,
method = 'post'
} = col
if (!api) return
clearTimeout(searchTimeouts[prop])
searchTimeouts[prop] = setTimeout(async () => {
try {
remoteLoading.value[prop] = true
// remoteLoading.value[prop] = true
const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {}),
[keywordField]: keyword
}
const res = await request({ url: api, method: 'post', data: payload })
const config = {
url: api,
method: method.toLowerCase()
}
if (config.method === 'get') {
config.params = payload
} else {
config.data = payload
}
const res = await request(config)
const list =
typeof col.transform === 'function'
? col.transform(res)
......@@ -733,7 +817,6 @@ async function handleRemoteSearch(keyword, col) {
}
}, debounceWait)
}
function getDisabledDateFn(col) {
const { minDate, maxDate } = col
if (minDate == null && maxDate == null) return () => false
......@@ -879,9 +962,10 @@ onMounted(async () => {
dictLoaded.value.add(col.prop)
} else if (col.api && col.preload !== false) {
await loadRemoteOptions(col)
} else if (col.options) {
remoteOptions.value[col.prop] = [...col.options]
}
// else if (col.options) {
// remoteOptions.value[col.prop] = [...col.options]
// }
}
}
})
......
<template>
<div class="common-table">
<div class="addBox">
<el-button v-if="showAdd" type="primary" @click="addTableData">
{{ addBtnTxt }}
</el-button>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="rowKey"
v-bind="$attrs"
>
<!-- 树形模式:展开列 -->
<el-table-column v-if="treeMode" type="expand" width="50">
<template #default="{ row, $index }">
<div class="sub-table-wrapper">
<div class="sub-table-header">
<span class="sub-table-title">{{ subTableTitle }}</span>
<el-button type="primary" size="small" @click="addSubRow(row, $index)">
{{ subTableAddBtnText }}
</el-button>
</div>
<el-table
:data="getSubData(row)"
border
size="small"
class="sub-table"
>
<!-- 动态子表列 -->
<el-table-column
v-for="subCol in subTableColumns"
:key="subCol.prop"
:prop="subCol.prop"
:label="subCol.label"
:width="subCol.width"
:min-width="subCol.minWidth"
:align="subCol.align || 'left'"
>
<template #default="{ row: subRow, $index: subIndex }">
<el-form-item
:rules="getSubColumnRules(subCol)"
:error="getSubRowError(row, $index, subIndex, subCol.prop)"
style="margin-bottom: 0"
>
<!-- 输入框 -->
<el-input
v-if="subCol.editType === 'input'"
v-model="subRow[subCol.prop]"
:placeholder="subCol.placeholder || `请输入${subCol.label}`"
:clearable="true"
:type="subCol.inputType === 'textarea' ? 'textarea' : 'text'"
:rows="subCol.rows || 2"
:disabled="subCol.disabled"
@blur="validateSubField(row, $index, subIndex, subCol.prop)"
/>
<!-- 下拉选择 -->
<el-select
v-else-if="subCol.editType === 'select'"
v-model="subRow[subCol.prop]"
:placeholder="subCol.placeholder || `请选择${subCol.label}`"
:clearable="true"
filterable
:disabled="subCol.disabled"
@change="validateSubField(row, $index, subIndex, subCol.prop)"
>
<el-option
v-for="opt in getSubSelectOptions(subCol)"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="subCol.editType === 'date'"
v-model="subRow[subCol.prop]"
type="date"
:placeholder="`选择${subCol.label}`"
:value-format="subCol.valueFormat || 'YYYY-MM-DD'"
style="width: 100%"
:disabled="subCol.disabled"
/>
<!-- 月份选择器 -->
<el-date-picker
v-else-if="subCol.editType === 'month'"
v-model="subRow[subCol.prop]"
type="month"
:placeholder="`选择${subCol.label}`"
:value-format="subCol.valueFormat || 'YYYY-MM'"
style="width: 100%"
:disabled="subCol.disabled"
/>
<!-- 日期范围 -->
<el-date-picker
v-else-if="subCol.editType === 'daterange'"
v-model="subRow[subCol.prop]"
type="daterange"
range-separator="至"
:start-placeholder="subCol.startPlaceholder || '开始日期'"
:end-placeholder="subCol.endPlaceholder || '结束日期'"
:value-format="subCol.valueFormat || 'YYYY-MM-DD'"
style="width: 100%"
:disabled="subCol.disabled"
/>
<!-- 月份范围 -->
<el-date-picker
v-else-if="subCol.editType === 'monthrange'"
v-model="subRow[subCol.prop]"
type="monthrange"
range-separator="至"
:start-placeholder="subCol.startPlaceholder || '开始月份'"
:end-placeholder="subCol.endPlaceholder || '结束月份'"
:value-format="subCol.valueFormat || 'YYYY-MM'"
style="width: 100%"
:disabled="subCol.disabled"
/>
<!-- 普通文本(不可编辑) -->
<span v-else>{{ subRow[subCol.prop] || '-' }}</span>
</el-form-item>
</template>
</el-table-column>
<!-- 子表操作列 -->
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ $index: subIndex }">
<el-button link type="danger" size="small" @click="deleteSubRow(row, $index, subIndex)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="!getSubData(row).length" class="sub-table-empty">暂无明细数据,点击上方按钮添加</div>
</div>
</template>
</el-table-column>
<!-- 动态列(主表) -->
<el-table-column
v-for="col in visibleColumns"
:key="col.prop"
:prop="col.prop"
:width="col.width"
:min-width="col.minWidth"
:fixed="col.fixed"
:align="col.align || 'left'"
:show-overflow-tooltip="col.showOverflowTooltip"
>
<!-- 自定义表头:显示必填红色星号 -->
<template #header>
<span v-if="checkColumnRequired(col)" class="required-star"> *</span>
<span>{{ col.label }}</span>
</template>
<template #default="{ row, $index }">
<div v-if="col.editable !== false">
<el-form-item
:rules="getColumnRules(col)"
:error="getRowError(row, col.prop)"
style="margin-bottom: 0"
>
<!-- 输入框(文本/数字/textarea) -->
<el-input
v-if="col.editType === 'input'"
:model-value="getInputDisplayValue(row, col)"
:placeholder="col.placeholder || `请输入${col.label}`"
:clearable="true"
:type="col.inputType === 'textarea' ? 'textarea' : 'text'"
:rows="col.rows || 2"
:disabled="col.disabled"
@update:model-value="val => handleInputChange(row, col, val, $index)"
@blur="handleInputBlur(row, col, $index)"
/>
<!-- 下拉选择 -->
<el-select
v-else-if="col.editType === 'select'"
:model-value="row[col.prop]"
:placeholder="col.placeholder || `请选择${col.label}`"
:clearable="true"
filterable
:allow-create="col.allowCreate"
:default-first-option="col.allowCreate"
:remote="!!col.api"
:remote-method="keyword => handleRemoteSearch(keyword, col)"
:loading="remoteLoading[col.prop]"
:disabled="col.disabled"
@update:model-value="val => handleSelectChange(row, col, val, $index)"
@focus="() => loadRemoteOptions(col)"
>
<el-option
v-for="opt in getSelectOptions(col)"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="col.editType === 'date'"
:model-value="row[col.prop]"
type="date"
:placeholder="`选择${col.label}`"
:value-format="col.valueFormat || 'YYYY-MM-DD'"
style="width: 100%"
:disabled="col.disabled"
:disabled-date="getDisabledDateFn(col)"
@update:model-value="val => handleCellChange(row, col, val, $index)"
/>
<!-- 月份选择器 -->
<el-date-picker
v-else-if="col.editType === 'month'"
:model-value="row[col.prop]"
type="month"
:placeholder="`选择${col.label}`"
:value-format="col.valueFormat || 'YYYY-MM'"
style="width: 100%"
:disabled="col.disabled"
:disabled-date="getDisabledDateFn(col)"
@update:model-value="val => handleCellChange(row, col, val, $index)"
/>
<!-- 日期范围 -->
<el-date-picker
v-else-if="col.editType === 'daterange'"
:model-value="row[col.prop]"
type="daterange"
range-separator="至"
:start-placeholder="col.startPlaceholder || '开始日期'"
:end-placeholder="col.endPlaceholder || '结束日期'"
:value-format="col.valueFormat || 'YYYY-MM-DD'"
style="width: 100%"
:disabled="col.disabled"
:disabled-date="getDisabledDateFn(col)"
@update:model-value="val => handleCellChange(row, col, val, $index)"
/>
<!-- 月份范围 -->
<el-date-picker
v-else-if="col.editType === 'monthrange'"
:model-value="row[col.prop]"
type="monthrange"
range-separator="至"
:start-placeholder="col.startPlaceholder || '开始月份'"
:end-placeholder="col.endPlaceholder || '结束月份'"
:value-format="col.valueFormat || 'YYYY-MM'"
style="width: 100%"
:disabled="col.disabled"
:disabled-date="getDisabledDateFn(col)"
@update:model-value="val => handleCellChange(row, col, val, $index)"
/>
<!-- 上传组件 -->
<div v-else-if="col.editType === 'upload'" class="table-upload-wrapper">
<el-upload
:file-list="getFileListDisplay(row[col.prop])"
:action="col.action || '/api/upload'"
:headers="col.headers"
:multiple="col.multiple || false"
:limit="col.limit || 1"
:accept="col.accept"
:disabled="col.disabled"
:auto-upload="true"
:show-file-list="false"
:before-upload="file => beforeUpload(file, col)"
:on-success="
(res, file, fileList) => handleUploadSuccess(row, col, res, file, fileList)
"
:on-error="
(err, file, fileList) => handleUploadError(row, col, err, file, fileList)
"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<div class="upload-file-list" v-if="getFileListDisplay(row[col.prop]).length">
<div
v-for="(file, idx) in getFileListDisplay(row[col.prop])"
:key="idx"
class="file-item"
>
<span class="file-name">{{ file.name || file.url }}</span>
<el-button link type="danger" size="small" @click="removeFile(row, col, idx)"
>删除</el-button
>
</div>
</div>
</div>
<!-- 自定义渲染(插槽) -->
<slot
v-else
:name="`edit-${col.prop}`"
:row="row"
:column="col"
:updateValue="val => handleCellChange(row, col, val, $index)"
/>
</el-form-item>
</div>
<!-- 不可编辑列:只读显示 -->
<div v-else>
<slot :name="`column-${col.prop}`" :row="row" :column="col" :value="row[col.prop]">
<span v-if="col.editType === 'select'">
{{ getSelectLabel(col, row[col.prop]) }}
</span>
<span v-else-if="col.editType === 'upload' && row[col.prop]">
<span v-for="(file, idx) in getFileListDisplay(row[col.prop])" :key="idx">
<el-link type="primary" @click="previewFile(file)">{{
file.name || file.url
}}</el-link>
<span v-if="idx < getFileListDisplay(row[col.prop]).length - 1">, </span>
</span>
</span>
<span v-else-if="col.formatter">
{{ col.formatter(row[col.prop], row) }}
</span>
<span v-else>{{ row[col.prop] || '-' }}</span>
</slot>
</div>
</template>
</el-table-column>
<!-- 操作栏(主表) -->
<el-table-column
v-if="showOperation"
:label="operationLabel"
:width="operationWidth"
:fixed="operationFixed"
align="center"
>
<template #default="{ row, $index }">
<div class="operation-buttons">
<!-- 树形模式:添加子行按钮 -->
<el-button
v-if="treeMode && showAddChild"
link
type="primary"
size="small"
@click="addSubRow(row, $index)"
>
{{ addChildBtnText }}
</el-button>
<slot name="operation" :row="row" :index="$index">
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</slot>
</div>
</template>
</el-table-column>
<!-- 额外插槽列 -->
<slot name="append" />
</el-table>
<!-- 分页组件 -->
<div v-if="showPagination" class="table-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
:background="paginationBackground"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 文件预览弹窗 -->
<el-dialog v-model="previewDialogVisible" :title="previewFileName" width="60%" destroy-on-close>
<div class="preview-container">
<img v-if="previewFileType === 'image'" :src="previewUrl" style="max-width: 100%" />
<iframe
v-else-if="previewFileType === 'pdf'"
:src="previewUrl"
style="width: 100%; height: 70vh"
></iframe>
<div v-else class="preview-unsupported">暂不支持预览此类型文件</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getDicts } from '@/api/system/dict/data'
import useDictStore from '@/store/modules/dict'
import request from '@/utils/request'
import dayjs from 'dayjs'
// ==================== Props ====================
const props = defineProps({
// 基础 props
tableData: { type: Array, default: () => [] },
columns: { type: Array, default: () => [] },
rowIdKey: { type: String, default: 'id' },
showOperation: { type: Boolean, default: true },
operationLabel: { type: String, default: '操作' },
operationWidth: { type: String, default: '120' },
operationFixed: { type: String, default: 'right' },
showPagination: { type: Boolean, default: false },
total: { type: Number, default: 0 },
page: { type: Number, default: 1 },
limit: { type: Number, default: 20 },
pageSizes: { type: Array, default: () => [10, 20, 30, 50] },
paginationBackground: { type: Boolean, default: true },
loading: { type: Boolean, default: false },
showAdd: { type: Boolean, default: true },
addBtnTxt: { type: String, default: '新增' },
// 树形模式新增 props
treeMode: { type: Boolean, default: false },
subDataProp: { type: String, default: 'children' },
subTableColumns: { type: Array, default: () => [] },
subTableTitle: { type: String, default: '明细信息' },
subTableAddBtnText: { type: String, default: '添加明细' },
showAddChild: { type: Boolean, default: true },
addChildBtnText: { type: String, default: '增加孩子' }
})
const emit = defineEmits([
'update:page',
'update:limit',
'update:tableData',
'delete',
'selectChange',
'uploadSuccess',
'add',
'cell-change',
'sub-row-add',
'sub-row-delete',
'sub-cell-change'
])
// ==================== 内部状态 ====================
const rowErrors = ref({})
const remoteOptions = ref({})
const remoteLoading = ref({})
const dictLoaded = ref(new Set())
const currentPage = ref(props.page)
const pageSize = ref(props.limit)
// 预览弹窗状态
const previewDialogVisible = ref(false)
const previewUrl = ref('')
const previewFileName = ref('')
const previewFileType = ref('')
// 数字输入临时编辑值
const editingTempValue = ref({})
// 子表格错误信息
const subRowErrors = ref({})
// ==================== 计算属性 ====================
const visibleColumns = computed(() => {
return props.columns.filter(col => {
if (typeof col.visible === 'function') return col.visible()
return col.visible !== false
})
})
// 检查列是否必填
function checkColumnRequired(col) {
if (col.required === true) return true
if (col.rules && Array.isArray(col.rules)) {
return col.rules.some(rule => rule.required === true)
}
return false
}
// 获取主行错误信息
function getRowError(row, prop) {
const rowId = row[props.rowIdKey]
return rowErrors.value[rowId]?.[prop] || ''
}
// 获取主列校验规则
function getColumnRules(col) {
if (col.rules) return col.rules
if (col.required) {
return [{ required: true, message: `${col.label}不能为空`, trigger: 'blur' }]
}
return []
}
// 获取 Select 选项列表(主表)
function getSelectOptions(col) {
const key = col.prop
if (col.dictType) {
const dictStore = useDictStore()
const dictOpts = dictStore.getDict(col.dictType) || []
return dictOpts.map(opt => ({ value: opt.value, label: opt.label, raw: opt }))
}
if (col.api) {
return (remoteOptions.value[key] || []).map(opt => ({
value: opt.value,
label: opt.label,
raw: opt.raw
}))
}
if (col.options) {
return col.options.map(opt => ({ value: opt.value, label: opt.label, raw: opt.raw }))
}
return []
}
// 获取 Select 选中项的 label(主表)
function getSelectLabel(col, value) {
const options = getSelectOptions(col)
if (value === undefined || value === null) return '-'
if (col.multiple && Array.isArray(value)) {
return value.map(v => options.find(opt => opt.value === v)?.label || v).join(', ')
}
const found = options.find(opt => opt.value === value)
return found ? found.label : value
}
// ==================== 数字输入格式化与解析 ====================
function getInputDisplayValue(row, col) {
const rowId = row[props.rowIdKey]
const editingKey = `${rowId}_${col.prop}`
if (editingTempValue.value[editingKey] !== undefined) {
return editingTempValue.value[editingKey]
}
const { inputType } = col
let rawValue = row[col.prop]
if (!['integer', 'decimalNumber', 'decimal'].includes(inputType)) {
return rawValue ?? ''
}
if (rawValue === undefined || rawValue === null) return ''
let num = typeof rawValue === 'number' ? rawValue : parseFloat(rawValue)
if (isNaN(num)) return ''
const { decimalDigits = 2 } = col
num = parseFloat(num.toFixed(decimalDigits))
const parts = num.toString().split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
function handleInputChange(row, col, rawValue, rowIndex) {
const { inputType, decimalDigits = 2 } = col
let filtered = rawValue
if (inputType === 'integer') {
filtered = rawValue.replace(/[^-\d]/g, '')
if (filtered === '-') {
// 保留负号
} else if (filtered !== '') {
let num = parseInt(filtered, 10)
if (isNaN(num)) filtered = ''
else filtered = num.toString()
}
} else if (inputType === 'decimalNumber') {
let temp = rawValue.replace(/[^-\d.]/g, '')
if (temp.startsWith('-')) {
temp = '-' + temp.slice(1).replace(/-/g, '')
} else {
temp = temp.replace(/-/g, '')
}
if (temp === '-') {
filtered = '-'
} else {
const pointIndex = temp.indexOf('.')
if (pointIndex !== -1) {
const intPart = temp.slice(0, pointIndex).replace(/[^\d-]/g, '')
let decPart = temp.slice(pointIndex + 1).replace(/\D/g, '')
if (decPart.length > decimalDigits) decPart = decPart.slice(0, decimalDigits)
filtered = intPart + (decPart ? '.' + decPart : '.')
} else {
filtered = temp
}
}
} else if (inputType === 'decimal') {
let temp = rawValue.replace(/[^\d.]/g, '')
const pointIndex = temp.indexOf('.')
if (pointIndex !== -1) {
const intPart = temp.slice(0, pointIndex).replace(/\D/g, '')
let decPart = temp.slice(pointIndex + 1).replace(/\D/g, '')
if (decPart.length > decimalDigits) decPart = decPart.slice(0, decimalDigits)
filtered = intPart + (decPart ? '.' + decPart : '.')
} else {
filtered = temp
}
} else {
filtered = rawValue
}
const rowId = row[props.rowIdKey]
const editingKey = `${rowId}_${col.prop}`
editingTempValue.value[editingKey] = filtered
}
function handleInputBlur(row, col, rowIndex) {
const rowId = row[props.rowIdKey]
const editingKey = `${rowId}_${col.prop}`
const rawStr = editingTempValue.value[editingKey]
if (rawStr === undefined) {
return
}
delete editingTempValue.value[editingKey]
if (rawStr === '') {
const newTableData = [...props.tableData]
newTableData[rowIndex] = { ...row, [col.prop]: null }
emit('update:tableData', newTableData)
validateField(newTableData[rowIndex], col.prop)
return
}
const { inputType, decimalDigits = 2 } = col
let finalValue = null
if (inputType === 'integer') {
const match = rawStr.match(/^-?\d+/)
if (match) {
const num = parseInt(match[0], 10)
if (!isNaN(num)) finalValue = num
}
} else if (inputType === 'decimalNumber') {
const num = parseFloat(rawStr)
if (!isNaN(num)) finalValue = parseFloat(num.toFixed(decimalDigits))
} else if (inputType === 'decimal') {
const num = parseFloat(rawStr)
if (!isNaN(num) && num >= 0) finalValue = parseFloat(num.toFixed(decimalDigits))
} else {
finalValue = rawStr
}
const newTableData = [...props.tableData]
newTableData[rowIndex] = { ...row, [col.prop]: finalValue }
emit('update:tableData', newTableData)
emit('cell-change', {
row: newTableData[rowIndex],
col,
newValue: finalValue,
oldValue: row[col.prop],
rowIndex,
prop: col.prop
})
validateField(newTableData[rowIndex], col.prop)
}
// ==================== 数据更新与校验(主表) ====================
function handleCellChange(row, col, newValue, rowIndex) {
const oldValue = row[col.prop]
if (oldValue === newValue) return
const newTableData = [...props.tableData]
newTableData[rowIndex] = { ...row, [col.prop]: newValue }
emit('update:tableData', newTableData)
emit('cell-change', {
row: newTableData[rowIndex],
col,
newValue,
oldValue,
rowIndex,
prop: col.prop
})
validateField(newTableData[rowIndex], col.prop)
}
function handleSelectChange(row, col, value, rowIndex) {
const newTableData = [...props.tableData]
const updatedRow = { ...row, [col.prop]: value }
if (col.onChangeExtraFields) {
const options = getSelectOptions(col)
const opt = options.find(o => o.value === value)
if (opt?.raw) {
for (const [targetProp, sourceKey] of Object.entries(col.onChangeExtraFields)) {
updatedRow[targetProp] = opt.raw[sourceKey]
}
}
}
newTableData[rowIndex] = updatedRow
emit('update:tableData', newTableData)
emit('selectChange', col.prop, value, col)
validateField(updatedRow, col.prop)
}
async function validateField(row, prop) {
const col = props.columns.find(c => c.prop === prop)
if (!col) return true
const rules = getColumnRules(col)
if (!rules.length) return true
const value = row[prop]
const rowId = row[props.rowIdKey]
let errorMsg = ''
for (const rule of rules) {
if (rule.required && (value === undefined || value === null || value === '')) {
errorMsg = rule.message || `${col.label}不能为空`
break
}
if (rule.pattern && !rule.pattern.test(String(value))) {
errorMsg = rule.message || `${col.label}格式不正确`
break
}
if (rule.validator && typeof rule.validator === 'function') {
try {
await rule.validator(null, value, () => {})
} catch (err) {
errorMsg = err.message
break
}
}
}
if (!rowErrors.value[rowId]) {
rowErrors.value[rowId] = {}
}
rowErrors.value[rowId][prop] = errorMsg
return !errorMsg
}
// ==================== 子表格相关函数 ====================
function getSubData(row) {
return row[props.subDataProp] || []
}
function getSubColumnRules(col) {
if (col.rules) return col.rules
if (col.required) {
return [{ required: true, message: `${col.label}不能为空`, trigger: 'blur' }]
}
return []
}
function getSubSelectOptions(col) {
if (col.dictType) {
const dictStore = useDictStore()
const dictOpts = dictStore.getDict(col.dictType) || []
return dictOpts.map(opt => ({ value: opt.value, label: opt.label, raw: opt }))
}
if (col.options) {
return col.options.map(opt => ({ value: opt.value, label: opt.label, raw: opt.raw }))
}
return []
}
function getSubRowError(mainRow, mainRowIndex, subIndex, prop) {
const rowId = mainRow[props.rowIdKey] || mainRowIndex
return subRowErrors.value[rowId]?.[subIndex]?.[prop] || ''
}
async function validateSubField(mainRow, mainRowIndex, subIndex, prop) {
const subCol = props.subTableColumns.find(c => c.prop === prop)
if (!subCol) return true
const rules = getSubColumnRules(subCol)
if (!rules.length) return true
const subData = getSubData(mainRow)
const subRow = subData[subIndex]
if (!subRow) return true
const value = subRow[prop]
const rowId = mainRow[props.rowIdKey] || mainRowIndex
let errorMsg = ''
for (const rule of rules) {
if (rule.required && (value === undefined || value === null || value === '')) {
errorMsg = rule.message || `${subCol.label}不能为空`
break
}
if (rule.pattern && !rule.pattern.test(String(value))) {
errorMsg = rule.message || `${subCol.label}格式不正确`
break
}
}
if (!subRowErrors.value[rowId]) {
subRowErrors.value[rowId] = {}
}
if (!subRowErrors.value[rowId][subIndex]) {
subRowErrors.value[rowId][subIndex] = {}
}
subRowErrors.value[rowId][subIndex][prop] = errorMsg
return !errorMsg
}
function addSubRow(mainRow, mainRowIndex) {
const newSubRow = {}
for (const col of props.subTableColumns) {
if (col.defaultValue !== undefined) {
newSubRow[col.prop] = typeof col.defaultValue === 'function' ? col.defaultValue() : col.defaultValue
} else {
newSubRow[col.prop] = null
}
}
newSubRow._tempId = Date.now() + Math.random()
const currentSubData = getSubData(mainRow)
const newSubData = [...currentSubData, newSubRow]
const newTableData = [...props.tableData]
newTableData[mainRowIndex] = {
...mainRow,
[props.subDataProp]: newSubData
}
emit('update:tableData', newTableData)
emit('sub-row-add', { mainRow, mainRowIndex, newRow: newSubRow })
}
function deleteSubRow(mainRow, mainRowIndex, subIndex) {
const currentSubData = getSubData(mainRow)
const newSubData = [...currentSubData]
const deletedRow = newSubData.splice(subIndex, 1)[0]
const newTableData = [...props.tableData]
newTableData[mainRowIndex] = {
...mainRow,
[props.subDataProp]: newSubData
}
emit('update:tableData', newTableData)
emit('sub-row-delete', { mainRow, mainRowIndex, subIndex, deletedRow })
const rowId = mainRow[props.rowIdKey] || mainRowIndex
if (subRowErrors.value[rowId]) {
delete subRowErrors.value[rowId][subIndex]
const newErrors = {}
for (let i = 0; i < newSubData.length; i++) {
if (subRowErrors.value[rowId][i] !== undefined) {
newErrors[i] = subRowErrors.value[rowId][i]
}
}
subRowErrors.value[rowId] = newErrors
}
}
// ==================== 全局校验方法 ====================
function validateAll() {
const errors = []
let allValid = true
// 校验主表
for (let i = 0; i < props.tableData.length; i++) {
const row = props.tableData[i]
const rowId = row[props.rowIdKey] || i
for (const col of visibleColumns.value) {
if (col.editable === false) continue
const rules = getColumnRules(col)
if (rules.length === 0) continue
const value = row[col.prop]
let errorMsg = ''
for (const rule of rules) {
if (rule.required && (value === undefined || value === null || value === '')) {
errorMsg = rule.message || `${col.label}不能为空`
break
}
if (rule.pattern && !rule.pattern.test(String(value))) {
errorMsg = rule.message || `${col.label}格式不正确`
break
}
}
if (errorMsg) {
allValid = false
errors.push({
type: 'main',
rowIndex: i,
rowId,
prop: col.prop,
label: col.label,
message: errorMsg,
row
})
if (!rowErrors.value[rowId]) rowErrors.value[rowId] = {}
rowErrors.value[rowId][col.prop] = errorMsg
} else if (rowErrors.value[rowId]) {
rowErrors.value[rowId][col.prop] = ''
}
}
// 校验子表格
if (props.treeMode) {
const subData = getSubData(row)
for (let j = 0; j < subData.length; j++) {
const subRow = subData[j]
for (const subCol of props.subTableColumns) {
const rules = getSubColumnRules(subCol)
if (rules.length === 0) continue
const value = subRow[subCol.prop]
let errorMsg = ''
for (const rule of rules) {
if (rule.required && (value === undefined || value === null || value === '')) {
errorMsg = rule.message || `${subCol.label}不能为空`
break
}
if (rule.pattern && !rule.pattern.test(String(value))) {
errorMsg = rule.message || `${subCol.label}格式不正确`
break
}
}
if (errorMsg) {
allValid = false
errors.push({
type: 'sub',
mainRowIndex: i,
subIndex: j,
prop: subCol.prop,
label: subCol.label,
message: errorMsg,
row: subRow
})
if (!subRowErrors.value[rowId]) subRowErrors.value[rowId] = {}
if (!subRowErrors.value[rowId][j]) subRowErrors.value[rowId][j] = {}
subRowErrors.value[rowId][j][subCol.prop] = errorMsg
} else if (subRowErrors.value[rowId]?.[j]?.[subCol.prop]) {
subRowErrors.value[rowId][j][subCol.prop] = ''
}
}
}
}
}
return { valid: allValid, errors }
}
async function validateAndShow() {
const { valid, errors } = validateAll()
if (!valid && errors.length > 0) {
ElMessage.warning(errors[0].message)
return false
}
return true
}
// ==================== 远程加载 ====================
async function loadDictOptions(dictType) {
const dictStore = useDictStore()
let options = dictStore.getDict(dictType)
if (options && options.length > 0) 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) {
ElMessage.error(`字典 ${dictType} 加载失败`)
return []
}
}
async function loadRemoteOptions(col) {
const { prop, api, requestParams } = col
if (!api) return
if (remoteOptions.value[prop]?.length > 0) return
try {
remoteLoading.value[prop] = true
const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {})
}
const res = await request({ url: api, method: 'post', data: payload })
const list =
typeof col.transform === 'function' ? col.transform(res) : res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
value: String(i[col.valueKey || 'value']),
label: i[col.labelKey || 'label'],
raw: i
}))
} catch (err) {
ElMessage.error(`加载 ${col.label} 失败`)
remoteOptions.value[prop] = []
} finally {
remoteLoading.value[prop] = false
}
}
let searchTimeouts = {}
async function handleRemoteSearch(keyword, col) {
const { prop, api, requestParams, keywordField = 'keyword', debounceWait = 300 } = col
if (!api) return
clearTimeout(searchTimeouts[prop])
searchTimeouts[prop] = setTimeout(async () => {
try {
remoteLoading.value[prop] = true
const payload = {
...(typeof requestParams === 'function' ? requestParams() : requestParams || {}),
[keywordField]: keyword
}
const res = await request({ url: api, method: 'post', data: payload })
const list =
typeof col.transform === 'function'
? col.transform(res)
: res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
value: String(i[col.valueKey || 'value']),
label: i[col.labelKey || 'label'],
raw: i
}))
} catch (err) {
ElMessage.error(`搜索 ${col.label} 失败`)
} finally {
remoteLoading.value[prop] = false
}
}, debounceWait)
}
function getDisabledDateFn(col) {
const { minDate, maxDate } = col
if (minDate == null && maxDate == null) return () => false
return date => {
const current = dayjs(date).startOf('day')
let minD = null,
maxD = null
if (minDate != null) {
const val = typeof minDate === 'function' ? minDate() : minDate
minD = val ? dayjs(val).startOf('day') : null
}
if (maxDate != null) {
const val = typeof maxDate === 'function' ? maxDate() : maxDate
maxD = val ? dayjs(val).startOf('day') : null
}
return (minD && current.isBefore(minD)) || (maxD && current.isAfter(maxD))
}
}
// ==================== 上传相关 ====================
function getFileListDisplay(val) {
if (!val) return []
if (Array.isArray(val)) return val
try {
return JSON.parse(val)
} catch {
return [{ url: val, name: '文件' }]
}
}
function beforeUpload(file, col) {
if (col.maxSize && file.size > col.maxSize) {
ElMessage.error(`文件 ${file.name} 超出大小限制`)
return false
}
if (col.accept) {
const ext = '.' + file.name.split('.').pop().toLowerCase()
const allowed = col.accept.split(',').map(s => s.trim().toLowerCase())
if (!allowed.includes(ext)) {
ElMessage.error(`文件类型不支持,仅支持:${col.accept}`)
return false
}
}
return true
}
function handleUploadSuccess(row, col, response, file, fileList) {
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
}
const currentList = getFileListDisplay(row[col.prop])
const newFile = { uid: file.uid, url, name }
const newList = [...currentList, newFile]
const rowIndex = props.tableData.findIndex(r => r[props.rowIdKey] === row[props.rowIdKey])
if (rowIndex !== -1) {
const newTableData = [...props.tableData]
newTableData[rowIndex] = { ...row, [col.prop]: newList }
emit('update:tableData', newTableData)
}
ElMessage.success(`${file.name} 上传成功`)
emit('uploadSuccess', col.prop, newList)
}
function handleUploadError(row, col, error, file, fileList) {
ElMessage.error(`文件 ${file.name} 上传失败`)
}
function removeFile(row, col, index) {
const currentList = getFileListDisplay(row[col.prop])
const newList = [...currentList]
newList.splice(index, 1)
const rowIndex = props.tableData.findIndex(r => r[props.rowIdKey] === row[props.rowIdKey])
if (rowIndex !== -1) {
const newTableData = [...props.tableData]
newTableData[rowIndex] = { ...row, [col.prop]: newList }
emit('update:tableData', newTableData)
}
}
function previewFile(file) {
const url = file.url
if (!url) return
const ext = (file.name || '').split('.').pop().toLowerCase()
previewUrl.value = url
previewFileName.value = file.name || '文件'
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
previewFileType.value = 'image'
previewDialogVisible.value = true
} else if (ext === 'pdf') {
previewFileType.value = 'pdf'
previewDialogVisible.value = true
} else {
window.open(url, '_blank')
}
}
// ==================== 行操作 ====================
function handleDelete(row) {
emit('delete', row)
}
function addTableData() {
emit('add')
}
// ==================== 分页处理 ====================
function handleSizeChange(val) {
pageSize.value = val
emit('update:limit', val)
emit('update:page', currentPage.value)
}
function handleCurrentChange(val) {
currentPage.value = val
emit('update:page', val)
}
watch(
() => props.page,
val => {
currentPage.value = val
}
)
watch(
() => props.limit,
val => {
pageSize.value = val
}
)
// ==================== 初始化 ====================
onMounted(async () => {
for (const col of props.columns) {
if (col.editType === 'select') {
if (col.dictType) {
const opts = await loadDictOptions(col.dictType)
remoteOptions.value[col.prop] = opts
dictLoaded.value.add(col.prop)
} else if (col.api && col.preload !== false) {
await loadRemoteOptions(col)
} else if (col.options) {
remoteOptions.value[col.prop] = [...col.options]
}
}
}
})
// 暴露方法给父组件
defineExpose({
validateAll,
validateAndShow,
refreshRemoteOptions: async prop => {
const col = props.columns.find(c => c.prop === prop)
if (col && col.api) {
remoteOptions.value[prop] = []
await loadRemoteOptions(col)
}
},
clearErrors: () => {
rowErrors.value = {}
subRowErrors.value = {}
}
})
</script>
<style scoped>
.common-table {
width: 100%;
}
.addBox {
margin-bottom: 10px;
}
.table-pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.operation-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
.table-upload-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.upload-file-list {
max-height: 80px;
overflow-y: auto;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 4px 8px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
padding: 2px 0;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-container {
text-align: center;
}
.preview-unsupported {
padding: 40px;
color: #909399;
}
.required-star {
color: #f56c6c;
margin-left: 2px;
}
.sub-table-wrapper {
padding: 12px 20px;
background-color: #fafafa;
border-radius: 4px;
}
.sub-table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.sub-table-title {
font-weight: 500;
color: #303133;
}
.sub-table {
width: 100%;
}
.sub-table-empty {
text-align: center;
padding: 20px;
color: #909399;
background-color: #fff;
border: 1px dashed #dcdfe6;
border-radius: 4px;
margin-top: 8px;
}
</style>
<!--
使用实例:
<treeEditTable
:tableData="addSalaryData"
:columns="mainColumns"
:treeMode="true"
:subDataProp="'bankDetails'"
:subTableColumns="subColumns"
subTableTitle="收款银行明细"
subTableAddBtnText="添加收款账户"
addChildBtnText="增加孩子"
@update:tableData="handleAddSalaryDataUpdate"
></treeEditTable>
const mainColumns = [
{ prop: 'name', label: '姓名', editType: 'input', required: true },
{ prop: 'internalNo', label: '内部编号', editType: 'input' },
{ prop: 'salaryMonth', label: '薪资月份', editType: 'month' },
{ prop: 'amount', label: '应发金额', editType: 'input', inputType: 'decimal' }
// ... 其他主表列
]
const subColumns = [
{
prop: 'bankName',
label: '收款银行',
editType: 'select',
options: [{ value: 'ICBC', label: '工商银行' }],
required: true
},
{ prop: 'accountTail', label: '账户尾号', editType: 'input', required: true },
{
prop: 'currency',
label: '币种',
editType: 'select',
options: [{ value: 'CNY', label: '人民币' }]
},
{ prop: 'payAmount', label: '发放金额', editType: 'input', inputType: 'decimal' },
{ prop: 'exchangeRate', label: '汇率', editType: 'input', inputType: 'decimal' },
{ prop: 'remark', label: '备注', editType: 'input' }
]
const addSalaryData = ref([
{
id: 1,
name: '张三',
internalNo: '001',
salaryMonth: '2025-06',
amount: 10000,
bankDetails: [
{
bankName: 'ICBC',
accountTail: '1234',
currency: 'CNY',
payAmount: 5000,
exchangeRate: 1,
remark: ''
},
{
bankName: 'ICBC',
accountTail: '5678',
currency: 'CNY',
payAmount: 5000,
exchangeRate: 1,
remark: ''
}
]
}
])
-->
......@@ -445,6 +445,13 @@ const searchConfig = ref([
},
{
type: 'monthrange',
prop: 'actualPayoutDate',
label: '出账月(实)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'monthrange',
prop: 'payoutDate',
label: '出账月(估)',
startPlaceholder: '开始时间',
......@@ -1044,9 +1051,6 @@ const addSpiltRecord = () => {
if (selectedRow.value.originalCurrency && selectedRow.value.originalCurrency == 'HKD') {
newRow.originalToHkdRate = '1'
}
console.log('====================================')
console.log('newRow', newRow)
console.log('====================================')
// 插入到表格数据最前面
splitTableData.value.push(newRow)
}
......@@ -1376,6 +1380,9 @@ const loadTableData = async (searchParams = {}) => {
...searchParams,
payoutDateStart: searchParams.payoutDate?.[0] || undefined,
payoutDateEnd: searchParams.payoutDate?.[1] || undefined,
actualPayoutDateStart: searchParams.actualPayoutDate?.[0] || undefined,
actualPayoutDateEnd: searchParams.actualPayoutDate?.[1] || undefined,
actualPayoutDate: undefined,
payoutDate: undefined
}
const res = await getPolicyFortuneList(params)
......
......@@ -42,21 +42,21 @@
:border="true"
>
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="businessNo" label="业务编号" min-width="120" />
<el-table-column prop="businessNo" label="业务编号" min-width="150" />
<!-- <el-table-column prop="fortuneAccountBizId" label="业务ID" min-width="120" sortable /> -->
<el-table-column prop="broker" label="转介人" min-width="120" sortable />
<el-table-column prop="team" label="所属团队" min-width="120" sortable />
<el-table-column prop="hkdAmount" label="港币金额" width="120" sortable>
<el-table-column prop="broker" label="转介人" min-width="100" sortable />
<el-table-column prop="team" label="所属团队" min-width="100" sortable />
<el-table-column prop="hkdAmount" label="港币金额" width="150" sortable>
<template #default="scope">
{{ formatCurrency(scope.row.hkdAmount, '', 4) }}
</template>
</el-table-column>
<el-table-column prop="payoutCurrency" label="发放币种" width="120" sortable>
<el-table-column prop="payoutCurrency" label="发放币种" width="110" sortable>
<template #default="scope">
<dict-tag :options="currencyTypeOptions" :value="scope.row.payoutCurrency" />
</template>
</el-table-column>
<el-table-column prop="payoutAmount" label="发放金额" width="120" sortable>
<el-table-column prop="payoutAmount" label="发放金额" width="150" sortable>
<template #default="scope">
{{ formatCurrency(scope.row.payoutAmount, '', 4) }}
</template>
......@@ -77,7 +77,7 @@
<el-table-column
prop="fortuneAccountMonth"
label="出账年月"
min-width="150"
min-width="100"
show-overflow-tooltip
/>
<!-- <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip /> -->
......
<template>
<div>
<CommonPage
:operationBtnList="operationBtnList"
:visibleDefaultButtons="visibleDefaultButtons"
:showSearchForm="true"
:show-pagination="true"
:total="pageTotal"
:current-page="currentPage"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<!-- 搜索区域 -->
<template #searchForm>
<SearchForm ref="searchFormRef" :config="searchConfig" />
</template>
<!-- 列表区域 -->
<template #table>
<!-- 制作薪资单管理列表 -->
<el-table
:data="tableData"
height="400"
border
highlight-current-row
style="width: 100%"
v-loading="loading"
ref="tableRef"
row-key="salaryBizId"
:reserve-selection="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column
v-for="(column, index) in electronicSalaryTableColumns"
:key="index"
:fixed="column.fixed"
:prop="column.prop"
:label="column.label"
:width="column.width"
:sortable="column.sortable"
:formatter="column.formatter"
/>
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="{ row }">
<el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-icon>
<MoreFilled />
</el-icon>
</template>
<el-menu @select="handleSelect($event, row)" popper-class="custom-menu">
<el-menu-item
:index="item.value"
v-for="item in getOperateItems(row)"
:key="item.value"
>{{ item.label }}</el-menu-item
>
</el-menu>
</el-popover>
</template>
</el-table-column>
</el-table>
</template>
</CommonPage>
<!-- 新增,修改,查看薪资单 -->
<CommonDialog
:dialogTitle="commonDialogTit"
dialogWidth="80%"
:openDialog="detailDialogVisible"
:showAction="false"
:showClose="true"
@close="detailDialogVisible = false"
@confirm="handleConfirmSingleSalary"
>
<div class="btnBox">
<el-button type="primary" @click="passCheck('1')"> 核对通过 </el-button>
<el-button type="warning" @click="handleReturn"> 退回修改 </el-button>
</div>
<SearchForm
ref="singleSalaryFormRef"
:config="personSaleryFormConfig"
v-model="personSaleryFormModel"
@selectChange="handleFormSelectChange"
@inputChange="handleFormInputChange"
/>
<div class="statistics-container" v-if="personSaleryFormModel.apiSalaryRemittanceFzDTO">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
<el-statistic
:value="personSaleryFormModel.apiSalaryRemittanceFzDTO.totalAmount"
:formatter="value => formatCurrency(value)"
>
<template #title>
<div style="display: inline-flex; align-items: center">折合港币合计</div>
</template>
</el-statistic>
</el-col>
</el-row>
</div>
<div>
<div style="font-size: 16px; font-weight: bold; margin: 10px 0; color: black">打款明细</div>
<editTable
ref="bankTableRef"
:showAdd="false"
:showOperation="false"
addBtnTxt="添加打款账户"
v-model:table-data="salaryRemittanceDTOList"
:columns="bankTableColumns"
row-id-key="signleId"
@delete="handleBankDelete"
@add="addSalaryRemittanceRecord"
@selectChange="handleBankSelectChange"
@cell-change="handleSingleSalaryCellChange"
/>
</div>
</CommonDialog>
<!-- '批量制作薪资单 -->
<CommonDialog
dialogTitle="批量制作薪资单"
dialogWidth="95%"
:openDialog="multipAddSalaryDialog"
@close="((multipAddSalaryDialog = false), resetAddReceivablesForm())"
@confirm="handleConfirmAddSalary"
>
<editTable
ref="editTableRef"
addBtnTxt="添加薪资单"
v-model:table-data="apiSalaryBatchAddDTOList"
:columns="salaryTableColumns"
row-id-key="multipleId"
show-operation
@delete="handleMultipDelete"
@add="addSalaryRecord"
@cell-change="handleSplitCellChange"
@selectChange="handleSalarySelectChange"
/>
</CommonDialog>
<!-- 取消薪资单 -->
<CommonDialog
dialogTitle="退回修改"
dialogWidth="80%"
:openDialog="returnDialogVisible"
:showAction="true"
:showClose="true"
@close="returnDialogVisible = false"
@confirm="passCheck('2')"
>
<SearchForm ref="returnFormRef" :config="returnFormConfig" v-model="returnFormModel" />
</CommonDialog>
</div>
</template>
<script setup name="ElectronicSalary">
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { MoreFilled } from '@element-plus/icons-vue'
import { exportReceivedFortune } from '@/api/financial/commission'
import {
batchAddSalary,
calculatePaidAmount,
getExchangeRate,
grossAmountApi,
updateSalaryDetail,
calculateTotalAmountApi,
editSingleSalaryApi,
addSingleSalaryApi,
cancelSalaryApi,
delSalaryApi,
electronicSalaryListApi,
pushSalaryApi,
checkSalaryApi
} from '@/api/salary'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
import { loadDicts, getDictLabel } from '@/utils/useDict'
import { safeDownload } from '@/utils/safeDownload'
import { formatCurrency } from '@/utils/number'
import editTable from '@/components/Table/editTable.vue'
// 添加表格引用
const tableRef = ref()
// 存储所有选中的行数据(用于跨页保持选择)
const allSelectedRows = ref(new Map())
const selectedRows = ref([])
const multipAddSalaryDialog = ref(false)
const addRecordRef = ref(null)
const apiSalaryBatchAddDTOList = ref([{}]) //批量新增薪资单
const salaryRemittanceDTOList = ref([{}]) //银行数据
const editTableRef = ref(null)
const bankTableRef = ref(null)
const singleSalaryFormRef = ref(null)
const personSaleryFormModel = ref({ apiSalaryRemittanceFzDTO: {} })
const commonDialogTit = ref('')
const commonDialogType = ref('')
const returnDialogVisible = ref(false)
const returnFormRef = ref(null)
const returnFormModel = ref({})
// 批量薪资单表格配置
const salaryTableColumns = ref([
{
prop: 'brokerBizId',
label: '姓名',
editType: 'select',
api: '/csf/api/salary/broker/list', // 接口地址 这个是get请求,目前表格是post请求,所以需要在完善一下表格的请求方式以及请求参数
method: 'get', // ← 指定请求方式为 GET
valueKey: 'brokerBizId', // 选项 value 字段名
labelKey: 'broker', // 选项 label 字段名
preload: true, // 初始化时自动加载
width: 150,
requestParams: { name: '' }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'name',
onChangeExtraFields: {
brokerName: 'broker', // 将选项原始对象的 broker 字段赋值给行数据的 brokerName
monthList: 'monthList' // 存储月份列表,供月份列使用
}
// debounceWait: 300
},
{
editType: 'select',
prop: 'month',
label: '薪资月份',
// options: [],//根据选择得转介人数据动态获取
// placeholder: monthList.value.length > 0 ? '请选择月份' : '请先选择转介人',
width: 150,
// 动态选项,依赖行数据中的 monthList
options: row => {
const list = row.monthList || []
return list.map(m => ({ value: m, label: m })) // 格式:{ value, label }
}
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
// {
// prop: 'splitRatio',
// label: '内部编号',
// editType: 'input',
// // required: true,
// placeholder: '请输入内部编号',
// width: 150
// },
{
editType: 'input',
prop: 'grossAmount',
label: '应发金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
prop: 'mpfAmount',
label: 'MPF事项',
editType: 'input',
// required: true,
placeholder: '请输入MPF事项',
width: 150
},
{
editType: 'input',
prop: 'otherAmount',
label: '其他调整总金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
prop: 'otherRemark',
label: '备注',
editType: 'input',
// required: true,
placeholder: '请输入备注',
width: 150
},
{
editType: 'input',
prop: 'paidAmount',
label: '实发金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
prop: 'bank1',
label: '收款银行1',
editType: 'select',
api: '/base/api/bank/page', // 接口地址
valueKey: 'bankBizId', // 选项 value 字段名
labelKey: 'bankName', // 选项 label 字段名
preload: true, // 初始化时自动加载
width: 150,
requestParams: { pageNo: 1, pageSize: 20 }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'queryContent'
// debounceWait: 300
},
{
prop: 'accountEndNo1',
label: '账户尾号1',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 150
},
{
editType: 'select',
prop: 'currency1',
label: '币种1',
dictType: 'bx_currency_type',
width: 150
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
editType: 'input',
prop: 'amount1',
label: '发放金额1',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
editType: 'input',
prop: 'exchangeRate1',
label: '汇率1',
inputType: 'decimal',
// required: true,
width: 150
},
{
prop: 'remark1',
label: '备注1',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 180
},
{
prop: 'bank2',
label: '收款银行2',
editType: 'select',
api: '/base/api/bank/page', // 接口地址
valueKey: 'bankBizId', // 选项 value 字段名
labelKey: 'bankName', // 选项 label 字段名
preload: true, // 初始化时自动加载
width: 150,
requestParams: { pageNo: 1, pageSize: 20 }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'queryContent'
// debounceWait: 300
},
{
prop: 'accountEndNo2',
label: '账户尾号2',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 150
},
{
editType: 'select',
prop: 'currency2',
label: '币种2',
width: 150,
dictType: 'bx_currency_type'
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
editType: 'input',
prop: 'amount2',
label: '发放金额2',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
editType: 'input',
prop: 'exchangeRate2',
label: '汇率2',
inputType: 'decimal',
// required: true,
width: 150
},
{
prop: 'remark2',
label: '备注2',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 180
}
])
//单个薪资单的表格配置
const bankTableColumns = ref([
{
prop: 'bank',
label: '收款银行',
editType: 'select',
api: '/base/api/bank/page', // 接口地址
valueKey: 'bankBizId', // 选项 value 字段名
labelKey: 'bankName', // 选项 label 字段名
preload: true, // 初始化时自动加载
requestParams: { pageNo: 1, pageSize: 20 }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'queryContent'
// debounceWait: 300
},
{
prop: 'accountEndNo',
label: '账户尾号',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 150
},
{
editType: 'select',
prop: 'currency',
label: '币种',
dictType: 'bx_currency_type',
width: 150
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
editType: 'input',
prop: 'amount',
label: '发放金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
editType: 'input',
prop: 'exchangeRate',
label: '汇率',
inputType: 'decimal',
// required: true,
width: 150
},
{
prop: 'remark',
label: '备注',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 180
}
])
//单个薪资单的表单配置
const personSaleryFormConfig = ref([
{
prop: 'brokerBizId',
label: '姓名',
type: 'select',
api: '/csf/api/salary/broker/list', // 接口地址 这个是get请求,目前表格是post请求,所以需要在完善一下表格的请求方式以及请求参数
method: 'get', // ← 指定请求方式为 GET
valueKey: 'brokerBizId', // 选项 value 字段名
labelKey: 'broker', // 选项 label 字段名
preload: true, // 初始化时自动加载
requestParams: { name: '' }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'name',
onChangeExtraFields: {
brokerName: 'broker', // 将选项原始对象的 broker 字段赋值给行数据的 brokerName
monthList: 'monthList' // 存储月份列表,供月份列使用
},
rules: [{ required: true, message: '姓名必填', trigger: 'blur' }]
// debounceWait: 300
},
{
type: 'select',
prop: 'month',
label: '薪资月份',
// 动态选项,依赖行数据中的 monthList
options: row => {
const list = row.monthList || []
return list.map(m => ({ value: m, label: m })) // 格式:{ value, label }
},
rules: [{ required: true, message: '薪资月份必填', trigger: 'blur' }]
},
{
type: 'input',
prop: 'grossAmount',
label: '应发金额',
inputType: 'decimalNumber'
// required: true,
},
{
prop: 'mpfAmount',
label: 'MPF事项',
type: 'input',
// required: true,
placeholder: '请输入MPF事项'
},
{
type: 'input',
prop: 'otherAmount',
label: '其他调整总金额',
inputType: 'decimalNumber'
// required: true,
},
{
prop: 'otherRemark',
label: '其他调整备注',
type: 'input',
// required: true,
placeholder: '请输入'
},
{
prop: 'remark',
label: '备注',
type: 'input',
// required: true,
placeholder: '请输入备注'
},
{
type: 'input',
prop: 'paidAmount',
label: '实发金额',
inputType: 'decimalNumber'
// required: true,
}
])
//单个薪资单的表单配置
const returnFormConfig = ref([
{
prop: 'returnRemark',
label: '备注',
type: 'input',
// required: true,
placeholder: '请输入备注',
span: 12
}
])
const handleReturn = () => {
returnDialogVisible.value = true
returnFormModel.value = {}
}
const passCheck = async checkType => {
let msg = checkType === '1' ? '核对' : '退回'
let params = {
salaryBizId: selectedRow.value.salaryBizId,
status: checkType
}
if (checkType == '2') {
let formData = await returnFormRef.value.validate()
params.returnRemark = formData.returnRemark
}
try {
await checkSalaryApi(params)
ElMessage.success(`${msg}成功`)
if (checkType == '1') {
detailDialogVisible.value = false
} else if (checkType == '2') {
returnDialogVisible.value = false
}
const searchParams = searchFormRef.value.getFormData()
loadTableData(searchParams)
} catch (error) {
ElMessage.error(`${msg}失败`)
if (checkType == '1') {
detailDialogVisible.value = true
} else if (checkType == '2') {
returnDialogVisible.value = true
}
if (error.message && error.message.includes('Validation')) {
ElMessage.error('必填项不能为空')
}
}
}
//==============表格复选框开始===========
// 选择行变化
const handleSelectionChange = selection => {
selectedRows.value = selection
// 更新全局选中状态
updateAllSelectedRows()
}
// 更新全局选中状态
const updateAllSelectedRows = () => {
// 更新当前页的选中状态
tableData.value.forEach(row => {
const isSelected = selectedRows.value.some(
selectedRow => selectedRow.salaryBizId === row.salaryBizId
)
if (isSelected) {
allSelectedRows.value.set(row.salaryBizId, row)
} else {
allSelectedRows.value.delete(row.salaryBizId)
}
})
}
// 设置当前页的选中状态
const setCurrentPageSelection = () => {
if (!tableRef.value) return
// 清除当前页的选择
// tableRef.value.clearSelection()
// 设置当前页应该选中的行
tableData.value.forEach(row => {
if (allSelectedRows.value.has(row.salaryBizId)) {
tableRef.value.toggleRowSelection(row, true)
}
})
}
// 清空所有选择
const clearAllSelection = () => {
allSelectedRows.value.clear()
selectedRows.value = []
if (tableRef.value) {
tableRef.value.clearSelection()
}
}
//==============表格复选框结束===========
const addSalaryRecord = () => {
apiSalaryBatchAddDTOList.value.push({
multipleId: Date.now() + Math.random() // 或使用 uuid
})
}
const addSalaryRemittanceRecord = () => {
salaryRemittanceDTOList.value.push({
signleId: Date.now() + Math.random() // 或使用 uuid
})
}
const handleSalarySelectChange = async ({ prop, value, col, rowIndex }) => {
const currentRow = apiSalaryBatchAddDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
//计算应发金额
if (prop === 'month' || prop === 'brokerBizId') {
if (currentRow.brokerBizId && currentRow.month) {
try {
let params = {
brokerBizId: currentRow.brokerBizId,
month: currentRow.month
}
const res = await grossAmountApi(params)
updatedRow.grossAmount = res.data
updatedRow.mpfAmount = 0
updatedRow.otherAmount = 0
updatedRow.paidAmount = res.data
needUpdate = true
} catch (error) {
console.log('应发金额计算失败', error)
ElMessage.error('应发金额计算失败')
}
}
}
if (prop === 'currency1' || prop === 'currency2') {
if (value) {
try {
let params = {
fromCurrency: value,
toCurrency: 'HKD'
}
const res = await getExchangeRate(params)
if (prop === 'currency1') {
updatedRow.exchangeRate1 = res.data
} else if (prop === 'currency2') {
updatedRow.exchangeRate2 = res.data
}
needUpdate = true
} catch (error) {
console.log('汇率计算失败', error)
ElMessage.error('汇率计算失败')
}
}
}
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...apiSalaryBatchAddDTOList.value]
newData[rowIndex] = updatedRow
apiSalaryBatchAddDTOList.value = newData
}
}
const handleSplitCellChange = async ({ row, col, newValue, oldValue, rowIndex, prop }) => {
console.log('单元格改变', row, col, newValue, oldValue, rowIndex, prop)
const currentRow = apiSalaryBatchAddDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
//计算实发金额
if (prop === 'grossAmount' || prop === 'mpfAmount' || prop === 'otherAmount') {
if (currentRow.grossAmount && currentRow.mpfAmount && currentRow.otherAmount) {
try {
let params = {
grossAmount: currentRow.grossAmount,
mpfAmount: currentRow.mpfAmount,
otherAmount: currentRow.otherAmount
}
const res = await calculatePaidAmount(params)
updatedRow.paidAmount = res.data
needUpdate = true
} catch (error) {
console.log('实发金额计算失败', error)
ElMessage.error('实发金额计算失败')
}
}
}
// 如果用户手动修改了 hkdAmount,不做反向联动(符合需求)
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...apiSalaryBatchAddDTOList.value]
newData[rowIndex] = updatedRow
apiSalaryBatchAddDTOList.value = newData
}
}
// 弹窗表单重置
const resetAddReceivablesForm = () => {
// addRecordRef.value.resetForm()
}
const handleConfirmAddSalary = async () => {
// 调用组件的校验方法
const isValid = await editTableRef.value.validateAndShow()
if (isValid) {
try {
let params = {
apiSalaryBatchAddDTOList: apiSalaryBatchAddDTOList.value
}
const res = await batchAddSalary(params)
multipAddSalaryDialog.value = false
ElMessage.success('批量添加薪资单成功')
const searchParams = searchFormRef.value.getFormData()
loadTableData(searchParams)
} catch (error) {
multipAddSalaryDialog.value = true
console.log('批量添加薪资单错误', error)
ElMessage.error('批量添加薪资单失败')
}
}
}
// 单个薪资单下拉框改变
const handleBankSelectChange = async ({ prop, value, col, rowIndex }) => {
const currentRow = salaryRemittanceDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
if (prop === 'currency') {
if (value) {
try {
let params = {
fromCurrency: value,
toCurrency: 'HKD'
}
const res = await getExchangeRate(params)
updatedRow.exchangeRate = res.data
needUpdate = true
} catch (error) {
console.log('汇率计算失败', error)
ElMessage.error('汇率计算失败')
}
}
}
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...salaryRemittanceDTOList.value]
newData[rowIndex] = updatedRow
salaryRemittanceDTOList.value = newData
}
//计算折合港币总金额,改变币种汇率也会改变,所以等表格更新完在更新折合港币总金额
if (prop === 'currency') {
if (value) {
if (currentRow.amount && currentRow.exchangeRate && currentRow.currency) {
try {
let calculateTotalAmountDTOList = JSON.parse(
JSON.stringify(salaryRemittanceDTOList.value)
)
let params = calculateTotalAmountDTOList.reduce((acc, item) => {
if (item.amount && item.exchangeRate && item.currency) {
acc.push({ ...item })
}
return acc
}, [])
const res = await calculateTotalAmountApi({ calculateTotalAmountDTOList: params })
personSaleryFormModel.value.apiSalaryRemittanceFzDTO.totalAmount = res.data
} catch (error) {
console.log('折合港币总金额计算失败', error)
ElMessage.error('折合港币总金额计算失败')
}
}
}
}
}
// 单个薪资单输入框改变
const handleSingleSalaryCellChange = async ({ row, col, newValue, oldValue, rowIndex, prop }) => {
// console.log('单元格改变', row, col, newValue, oldValue, rowIndex, prop)
const currentRow = salaryRemittanceDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
// 如果用户手动修改了 hkdAmount,不做反向联动(符合需求)
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...salaryRemittanceDTOList.value]
newData[rowIndex] = updatedRow
salaryRemittanceDTOList.value = newData
}
//计算折合港币总金额,改变币种汇率也会改变,所以等表格更新完在更新折合港币总金额
if (prop === 'amount' || prop === 'exchangeRate') {
if (currentRow.amount && currentRow.exchangeRate && currentRow.currency) {
try {
let calculateTotalAmountDTOList = JSON.parse(JSON.stringify(salaryRemittanceDTOList.value))
let params = calculateTotalAmountDTOList.reduce((acc, item) => {
if (item.amount && item.exchangeRate && item.currency) {
acc.push({ ...item })
}
return acc
}, [])
const res = await calculateTotalAmountApi({ calculateTotalAmountDTOList: params })
personSaleryFormModel.value.apiSalaryRemittanceFzDTO.totalAmount = res.data
} catch (error) {
console.log('折合港币总金额计算失败', error)
ElMessage.error('折合港币总金额计算失败')
}
}
}
}
// 单个薪资单的新增,修改
const handleConfirmSingleSalary = async () => {
let formData = await singleSalaryFormRef.value.validate()
try {
if (formData.apiSalaryRemittanceFzDTO) {
formData.apiSalaryRemittanceFzDTO.salaryRemittanceDTOList = JSON.parse(
JSON.stringify(salaryRemittanceDTOList.value)
)
}
if (commonDialogType.value == 'edit') {
const res = await editSingleSalaryApi(formData)
ElMessage.success('薪资单修改成功')
detailDialogVisible.value = false
} else if (commonDialogType.value == 'add') {
const res = await addSingleSalaryApi(formData)
ElMessage.success('薪资单新增成功')
}
detailDialogVisible.value = false
const searchParams = searchFormRef.value.getFormData()
loadTableData(searchParams)
} catch (error) {
if (commonDialogType.value == 'edit') {
ElMessage.error('薪资单修改失败')
} else if (commonDialogType.value == 'add') {
ElMessage.error('薪资单新增失败')
}
detailDialogVisible.value = true
if (error.message && error.message.includes('Validation')) {
ElMessage.error('必填项不能为空')
}
}
}
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'input',
prop: 'brokerName',
label: '转介人'
},
{
type: 'month',
prop: 'month',
label: '薪资年月',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'select',
prop: 'status',
label: '薪资单状态',
// multiple: true,
dictType: 'salary_status'
}
])
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
// 动态生成操作菜单项(根据行数据)
const getOperateItems = row => {
const items = []
// 条件:isPart == 1 时不显示分期出账(使用宽松相等,兼容字符串 '1')
// if (row.type == 1) {
// items.push({ label: '设置状态', value: 'setStatus' })
// items.push({ label: '更新数据', value: 'updateData' })
// }
// 始终显示的菜单项(保持原始顺序)
items.push({ label: '预览并核对', value: 'preview' })
items.push({ label: '退回修改', value: 'retreat' })
// items.push({ label: '取消薪资单', value: 'salaryCancel' })
return items
}
const handleFormInputChange = async (prop, value, item) => {
console.log('表单输入框改变', prop, value, item)
await nextTick()
//计算实发金额
if (prop === 'grossAmount' || prop === 'mpfAmount' || prop === 'otherAmount') {
if (
personSaleryFormModel.value.grossAmount &&
personSaleryFormModel.value.mpfAmount &&
personSaleryFormModel.value.otherAmount
) {
try {
let params = {
grossAmount: personSaleryFormModel.value.grossAmount,
mpfAmount: personSaleryFormModel.value.mpfAmount,
otherAmount: personSaleryFormModel.value.otherAmount
}
const res = await calculatePaidAmount(params)
personSaleryFormModel.value.paidAmount = res.data
} catch (error) {
console.log('实发金额计算失败', error)
ElMessage.error('实发金额计算失败')
}
}
}
}
const handleFormSelectChange = async (prop, value, item) => {
console.log('表单选中项改变', prop, value, item)
await nextTick()
//计算应发金额
if (prop === 'month' || prop === 'brokerBizId') {
if (personSaleryFormModel.value.brokerBizId && personSaleryFormModel.value.month) {
try {
let params = {
brokerBizId: personSaleryFormModel.value.brokerBizId,
month: personSaleryFormModel.value.month
}
const res = await grossAmountApi(params)
personSaleryFormModel.value.grossAmount = res.data
personSaleryFormModel.value.mpfAmount = 0
personSaleryFormModel.value.otherAmount = 0
personSaleryFormModel.value.paidAmount = res.data
} catch (error) {
console.log('应发金额计算失败', error)
ElMessage.error('应发金额计算失败')
}
}
}
}
//设置单个薪资单的表单编辑状态
const setSignleSalaryFormStatus = () => {
personSaleryFormConfig.value = personSaleryFormConfig.value.map(item => {
if (commonDialogType.value == 'preview') {
item.disabled = true
}
return item
})
bankTableColumns.value = bankTableColumns.value.map(item => {
if (commonDialogType.value == 'preview') {
item.disabled = true
}
return item
})
}
const handleSingleAdd = () => {
commonDialogTit.value = '增加薪资单'
commonDialogType.value = 'add'
personSaleryFormModel.value = { apiSalaryRemittanceFzDTO: {} }
salaryRemittanceDTOList.value = []
setSignleSalaryFormStatus()
detailDialogVisible.value = true
}
const handleSelect = (e, row) => {
console.log('选中行:', e, row)
selectedRow.value = row
if (e === 'preview') {
commonDialogTit.value = '预览并核对'
commonDialogType.value = 'preview'
detailDialogVisible.value = true
setSignleSalaryFormStatus()
// updateSalary(row)
} else if (e === 'retreat') {
handleReturn()
}
}
const handleMultipDelete = async row => {
try {
// 弹出确认框
await ElMessageBox.confirm('确定要删除该记录吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 用户点击确定 → 执行删除逻辑
const index = apiSalaryBatchAddDTOList.value.findIndex(
item => item.multipleId == row.multipleId
)
if (index !== -1) {
apiSalaryBatchAddDTOList.value.splice(index, 1)
ElMessage.success('删除成功')
}
} catch {
// 用户取消或关闭弹窗,不做任何操作
console.log('取消删除')
}
}
const handleBankDelete = async row => {
try {
// 弹出确认框
await ElMessageBox.confirm('确定要删除该记录吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (row.salaryRemittanceBizId) {
await delSalaryApi(row.salaryRemittanceBizId)
}
// 用户点击确定 → 执行删除逻辑
const index = salaryRemittanceDTOList.value.findIndex(item => item.signleId == row.signleId)
if (index !== -1) {
salaryRemittanceDTOList.value.splice(index, 1)
ElMessage.success('删除成功')
}
} catch {
// 用户取消或关闭弹窗,不做任何操作
console.log('取消删除')
}
}
const selectedRow = ref(null)
// 表格数据
const tableData = ref([])
// 按钮事件处理
const handlePush = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要推送的记录')
return
}
try {
await ElMessageBox.confirm(
`确认完成 ${selectedRows.value.length} 条记录的推送操作吗?`,
'提示',
{ type: 'warning' }
)
console.log(selectedRows.value)
const arr = selectedRows.value.map(item => item.salaryBizId).join(',')
await pushSalaryApi({ salaryBizId: arr })
const params = searchFormRef.value.getFormData()
loadTableData(params)
ElMessage.success('推送操作成功')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('推送操作失败')
}
}
}
const handleImport = () => ElMessage.info('点击导入按钮')
const handleExport = async () => {
ElMessage.info('导出待开发')
return
// 获取搜索参数
const params = searchFormRef.value?.getFormData() || {}
const response = await exportReceivedFortune(params)
// 文件名设置为应收款导出_yyyy-MM-dd hh:mm:ss.xlsx,不需要-,用字符串
const fileName = `应收款导出_${new Date().toLocaleString().replace(/\//g, '').replace(/:/g, '').replace(/\s/g, '')}.xlsx`
await safeDownload(response, fileName, 'application/vnd.ms-excel;charset=utf-8')
}
const handleReset = () => {
// 重置搜索表单
searchFormRef.value.resetForm()
searchParams.value = {}
clearAllSelection()
console.log('表单已重置')
loadTableData()
}
const handleQuery = () => {
currentPage.value = 1
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
const visibleDefaultButtons = ref(['add', 'export', 'reset', 'query'])
// 按钮配置
const operationBtnList = ref([
{ key: 'add', direction: 'left', label: '确认推送', type: 'warning', click: handlePush },
// { key: 'import', direction: 'left', click: handleImport },
{ key: 'export', direction: 'right', click: handleExport },
{ key: 'reset', direction: 'right', click: handleReset },
{ key: 'query', direction: 'right', click: handleQuery }
])
// 分页事件
const handleSizeChange = val => {
pageSize.value = val
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
const handleCurrentChange = val => {
currentPage.value = val
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
// 加载表格数据
const loadTableData = async searchParams => {
loading.value = true
try {
const params = {
...searchParams,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await electronicSalaryListApi(params)
tableData.value = response.data.records || []
pageTotal.value = response.data.total || 0
pageSize.value = response.data.size || 50
// 数据加载完成后,设置当前页的选中状态
nextTick(() => {
setCurrentPageSelection()
})
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 获取入账状态,字典值转化方法
onMounted(async () => {
try {
await loadDicts(['salary_status'])
// getStatics()
loadTableData()
} catch (error) {
console.error('字典加载失败', error)
} finally {
loading.value = false
}
})
// 格式化函数(每次渲染都会调用,所以能拿到最新字典)
const formatStatus = (row, column) => {
return getDictLabel('salary_status', row.status) // 实时查缓存
}
const detailDialogVisible = ref(false)
const electronicSalaryTableColumns = ref([
{
prop: 'status',
label: '薪资单状态',
sortable: true,
width: '120',
formatter: row => formatStatus(row) || '-'
},
{
prop: 'brokerName',
label: '姓名',
sortable: true,
width: '120',
formatter: row => row.brokerName || '-'
},
{
prop: 'month',
label: '薪资月份',
sortable: true,
width: '120',
formatter: row => row.month || '-'
},
{
prop: 'paidAmount',
label: '实发金额',
sortable: true,
width: '120',
formatter: row => formatCurrency(row.paidAmount || 0)
},
{
prop: 'grossAmount',
label: '应发合计',
sortable: true,
width: '120',
formatter: row => formatCurrency(row.grossAmount || 0)
},
{
prop: 'totalDeductions',
label: '扣款合计',
sortable: true,
width: '120',
formatter: row => formatCurrency(row.totalDeductions || 0)
},
{
prop: 'otherRemark',
label: '其他调整备注',
sortable: true,
width: '150',
formatter: row => row.otherRemark || '-'
},
{
prop: 'creatorName',
label: '备注',
sortable: true,
width: '150',
formatter: row => row.creatorName || '-'
},
{
prop: 'creatorName',
label: '制作人',
sortable: true,
width: '120',
formatter: row => row.creatorName || '-'
},
{
prop: 'createTime',
label: '制作时间',
sortable: true,
width: '180',
formatter: row => row.createTime || '-'
}
])
const updateSalary = async row => {
try {
const response = await updateSalaryDetail(row.salaryBizId)
personSaleryFormModel.value = response.data
if (personSaleryFormModel.value.apiSalaryRemittanceFzDTO) {
salaryRemittanceDTOList.value =
personSaleryFormModel.value.apiSalaryRemittanceFzDTO.salaryRemittanceDTOList.map(item => {
return {
...item,
signleId: Date.now() + Math.random()
}
})
}
detailDialogVisible.value = true
} catch (error) {
console.error('获取薪资单详情失败:', error)
ElMessage.error('获取薪资单详情失败')
}
}
</script>
<style scoped lang="scss">
/* 使用 :deep 穿透 scoped 样式,保证对 el-table__row 生效 */
:deep(.estimated-row) {
background-color: #f0f9ff; /* 浅蓝色,可根据需要修改 */
}
.btnBox {
display: flex;
align-items: center;
justify-content: flex-end;
}
</style>
<template>
<div>
<CommonPage
:operationBtnList="operationBtnList"
:visibleDefaultButtons="visibleDefaultButtons"
:showSearchForm="true"
:show-pagination="true"
:total="pageTotal"
:current-page="currentPage"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<!-- 搜索区域 -->
<template #searchForm>
<SearchForm ref="searchFormRef" :config="searchConfig" />
</template>
<!-- 列表区域 -->
<template #table>
<!-- 制作薪资单管理列表 -->
<el-table
:data="tableData"
height="400"
border
highlight-current-row
style="width: 100%"
v-loading="loading"
ref="tableRef"
row-key="salaryBizId"
:reserve-selection="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column
v-for="(column, index) in workSalaryTableColumns"
:key="index"
:fixed="column.fixed"
:prop="column.prop"
:label="column.label"
:width="column.width"
:sortable="column.sortable"
:formatter="column.formatter"
/>
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="{ row }">
<el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-icon>
<MoreFilled />
</el-icon>
</template>
<el-menu @select="handleSelect($event, row)" popper-class="custom-menu">
<el-menu-item
:index="item.value"
v-for="item in getOperateItems(row)"
:key="item.value"
>{{ item.label }}</el-menu-item
>
</el-menu>
</el-popover>
</template>
</el-table-column>
</el-table>
</template>
</CommonPage>
<!-- 新增,修改,查看薪资单 -->
<CommonDialog
:dialogTitle="commonDialogTit"
dialogWidth="80%"
:openDialog="detailDialogVisible"
:showAction="commonDialogType == 'view' ? false : true"
:showClose="true"
@close="detailDialogVisible = false"
@confirm="handleConfirmSingleSalary"
>
<SearchForm
ref="singleSalaryFormRef"
:config="personSaleryFormConfig"
v-model="personSaleryFormModel"
@selectChange="handleFormSelectChange"
@inputChange="handleFormInputChange"
/>
<div class="statistics-container" v-if="personSaleryFormModel.apiSalaryRemittanceFzDTO">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
<el-statistic
:value="personSaleryFormModel.apiSalaryRemittanceFzDTO.totalAmount"
:formatter="value => formatCurrency(value)"
>
<template #title>
<div style="display: inline-flex; align-items: center">折合港币合计</div>
</template>
</el-statistic>
</el-col>
</el-row>
</div>
<div>
<editTable
ref="bankTableRef"
:showAdd="commonDialogType == 'view' ? false : true"
:showOperation="commonDialogType == 'view' ? false : true"
addBtnTxt="添加打款账户"
v-model:table-data="salaryRemittanceDTOList"
:columns="bankTableColumns"
row-id-key="signleId"
@delete="handleBankDelete"
@add="addSalaryRemittanceRecord"
@selectChange="handleBankSelectChange"
@cell-change="handleSingleSalaryCellChange"
/>
</div>
</CommonDialog>
<!-- '批量制作薪资单 -->
<CommonDialog
dialogTitle="批量制作薪资单"
dialogWidth="95%"
:openDialog="multipAddSalaryDialog"
@close="((multipAddSalaryDialog = false), resetAddReceivablesForm())"
@confirm="handleConfirmAddSalary"
>
<editTable
ref="editTableRef"
addBtnTxt="添加薪资单"
v-model:table-data="apiSalaryBatchAddDTOList"
:columns="salaryTableColumns"
row-id-key="multipleId"
show-operation
@delete="handleMultipDelete"
@add="addSalaryRecord"
@cell-change="handleSplitCellChange"
@selectChange="handleSalarySelectChange"
/>
</CommonDialog>
<!-- 取消薪资单 -->
<CommonDialog
dialogTitle="取消薪资单"
dialogWidth="80%"
:openDialog="cancelDialogVisible"
:showAction="true"
:showClose="true"
@close="cancelDialogVisible = false"
@confirm="handleConfirmCancel"
>
<SearchForm ref="cancelFormRef" :config="cancelFormConfig" v-model="cancelFormModel" />
</CommonDialog>
</div>
</template>
<script setup name="WordReferenceSalary">
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { MoreFilled } from '@element-plus/icons-vue'
import { exportReceivedFortune } from '@/api/financial/commission'
import {
getSalaryList,
batchAddSalary,
calculatePaidAmount,
getExchangeRate,
grossAmountApi,
updateSalaryDetail,
calculateTotalAmountApi,
editSingleSalaryApi,
addSingleSalaryApi,
cancelSalaryApi,
delSalaryApi,
submitCheckApi
} from '@/api/salary'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
import { loadDicts, getDictLabel } from '@/utils/useDict'
import { safeDownload } from '@/utils/safeDownload'
import { formatCurrency } from '@/utils/number'
import editTable from '@/components/Table/editTable.vue'
// 添加表格引用
const tableRef = ref()
// 存储所有选中的行数据(用于跨页保持选择)
const allSelectedRows = ref(new Map())
const selectedRows = ref([])
const multipAddSalaryDialog = ref(false)
const addRecordRef = ref(null)
const apiSalaryBatchAddDTOList = ref([{}]) //批量新增薪资单
const salaryRemittanceDTOList = ref([{}]) //银行数据
const editTableRef = ref(null)
const bankTableRef = ref(null)
const singleSalaryFormRef = ref(null)
const personSaleryFormModel = ref({ apiSalaryRemittanceFzDTO: {} })
const commonDialogTit = ref('')
const commonDialogType = ref('')
const cancelDialogVisible = ref(false)
const cancelFormRef = ref(null)
const cancelFormModel = ref({})
// 批量薪资单表格配置
const salaryTableColumns = ref([
{
prop: 'brokerBizId',
label: '姓名',
editType: 'select',
api: '/csf/api/salary/broker/list', // 接口地址 这个是get请求,目前表格是post请求,所以需要在完善一下表格的请求方式以及请求参数
method: 'get', // ← 指定请求方式为 GET
valueKey: 'brokerBizId', // 选项 value 字段名
labelKey: 'broker', // 选项 label 字段名
preload: true, // 初始化时自动加载
width: 150,
requestParams: { name: '' }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'name',
onChangeExtraFields: {
brokerName: 'broker', // 将选项原始对象的 broker 字段赋值给行数据的 brokerName
monthList: 'monthList' // 存储月份列表,供月份列使用
}
// debounceWait: 300
},
{
editType: 'select',
prop: 'month',
label: '薪资月份',
// options: [],//根据选择得转介人数据动态获取
// placeholder: monthList.value.length > 0 ? '请选择月份' : '请先选择转介人',
width: 150,
// 动态选项,依赖行数据中的 monthList
options: row => {
const list = row.monthList || []
return list.map(m => ({ value: m, label: m })) // 格式:{ value, label }
}
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
prop: 'splitRatio',
label: '内部编号',
editType: 'input',
// required: true,
placeholder: '请输入内部编号',
width: 150
},
{
editType: 'input',
prop: 'grossAmount',
label: '应发金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
prop: 'mpfAmount',
label: 'MPF事项',
editType: 'input',
// required: true,
placeholder: '请输入MPF事项',
width: 150
},
{
editType: 'input',
prop: 'otherAmount',
label: '其他调整总金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
prop: 'otherRemark',
label: '备注',
editType: 'input',
// required: true,
placeholder: '请输入备注',
width: 150
},
{
editType: 'input',
prop: 'paidAmount',
label: '实发金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
prop: 'bank1',
label: '收款银行1',
editType: 'select',
api: '/base/api/bank/page', // 接口地址
valueKey: 'bankBizId', // 选项 value 字段名
labelKey: 'bankName', // 选项 label 字段名
preload: true, // 初始化时自动加载
width: 150,
requestParams: { pageNo: 1, pageSize: 20 }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'queryContent'
// debounceWait: 300
},
{
prop: 'accountEndNo1',
label: '账户尾号1',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 150
},
{
editType: 'select',
prop: 'currency1',
label: '币种1',
dictType: 'bx_currency_type',
width: 150
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
editType: 'input',
prop: 'amount1',
label: '发放金额1',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
editType: 'input',
prop: 'exchangeRate1',
label: '汇率1',
inputType: 'decimal',
// required: true,
width: 150
},
{
prop: 'remark1',
label: '备注1',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 180
},
{
prop: 'bank2',
label: '收款银行2',
editType: 'select',
api: '/base/api/bank/page', // 接口地址
valueKey: 'bankBizId', // 选项 value 字段名
labelKey: 'bankName', // 选项 label 字段名
preload: true, // 初始化时自动加载
width: 150,
requestParams: { pageNo: 1, pageSize: 20 }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'queryContent'
// debounceWait: 300
},
{
prop: 'accountEndNo2',
label: '账户尾号2',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 150
},
{
editType: 'select',
prop: 'currency2',
label: '币种2',
width: 150,
dictType: 'bx_currency_type'
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
editType: 'input',
prop: 'amount2',
label: '发放金额2',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
editType: 'input',
prop: 'exchangeRate2',
label: '汇率2',
inputType: 'decimal',
// required: true,
width: 150
},
{
prop: 'remark2',
label: '备注2',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 180
}
])
//单个薪资单的表格配置
const bankTableColumns = ref([
{
prop: 'bank',
label: '收款银行',
editType: 'select',
api: '/base/api/bank/page', // 接口地址
valueKey: 'bankBizId', // 选项 value 字段名
labelKey: 'bankName', // 选项 label 字段名
preload: true, // 初始化时自动加载
requestParams: { pageNo: 1, pageSize: 20 }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'queryContent'
// debounceWait: 300
},
{
prop: 'accountEndNo',
label: '账户尾号',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 150
},
{
editType: 'select',
prop: 'currency',
label: '币种',
dictType: 'bx_currency_type',
width: 150
// rules: [{ required: true, message: '必填项', trigger: 'blur' }]
},
{
editType: 'input',
prop: 'amount',
label: '发放金额',
inputType: 'decimalNumber',
// required: true,
width: 150
},
{
editType: 'input',
prop: 'exchangeRate',
label: '汇率',
inputType: 'decimal',
// required: true,
width: 150
},
{
prop: 'remark',
label: '备注',
editType: 'input',
// required: true,
placeholder: '请输入',
width: 180
}
])
//单个薪资单的表单配置
const personSaleryFormConfig = ref([
{
prop: 'brokerBizId',
label: '姓名',
type: 'select',
api: '/csf/api/salary/broker/list', // 接口地址 这个是get请求,目前表格是post请求,所以需要在完善一下表格的请求方式以及请求参数
method: 'get', // ← 指定请求方式为 GET
valueKey: 'brokerBizId', // 选项 value 字段名
labelKey: 'broker', // 选项 label 字段名
preload: true, // 初始化时自动加载
requestParams: { name: '' }, // 固定参数
// 远程搜索配置
filterable: true,
keywordField: 'name',
onChangeExtraFields: {
brokerName: 'broker', // 将选项原始对象的 broker 字段赋值给行数据的 brokerName
monthList: 'monthList' // 存储月份列表,供月份列使用
},
rules: [{ required: true, message: '姓名必填', trigger: 'blur' }]
// debounceWait: 300
},
{
type: 'select',
prop: 'month',
label: '薪资月份',
// 动态选项,依赖行数据中的 monthList
options: row => {
const list = row.monthList || []
return list.map(m => ({ value: m, label: m })) // 格式:{ value, label }
},
rules: [{ required: true, message: '薪资月份必填', trigger: 'blur' }]
},
{
prop: 'splitRatio',
label: '内部编号',
type: 'input',
// required: true,
placeholder: '请输入内部编号'
},
{
type: 'input',
prop: 'grossAmount',
label: '应发金额',
inputType: 'decimalNumber'
// required: true,
},
{
prop: 'mpfAmount',
label: 'MPF事项',
type: 'input',
// required: true,
placeholder: '请输入MPF事项'
},
{
type: 'input',
prop: 'otherAmount',
label: '其他调整总金额',
inputType: 'decimalNumber'
// required: true,
},
{
prop: 'otherRemark',
label: '其他调整备注',
type: 'input',
// required: true,
placeholder: '请输入'
},
{
prop: 'remark',
label: '备注',
type: 'input',
// required: true,
placeholder: '请输入备注'
},
{
type: 'input',
prop: 'paidAmount',
label: '实发金额',
inputType: 'decimalNumber'
// required: true,
}
])
//单个薪资单的表单配置
const cancelFormConfig = ref([
{
prop: 'cancelRemark',
label: '备注',
type: 'input',
// required: true,
placeholder: '请输入备注',
span: 12
}
])
// 提交核对
const handleSubmitCheck = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要核对的记录')
return
}
try {
await ElMessageBox.confirm(
`确认完成 ${selectedRows.value.length} 条记录的核对操作吗?`,
'提示',
{ type: 'warning' }
)
console.log(selectedRows.value)
const arr = selectedRows.value.map(item => item.salaryBizId).join(',')
// 调用出账API
await submitCheckApi({ salaryBizId: arr })
const params = searchFormRef.value.getFormData()
loadTableData(params)
ElMessage.success('核对操作成功')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('核对操作失败')
}
}
}
//==============表格复选框开始===========
// 选择行变化
const handleSelectionChange = selection => {
selectedRows.value = selection
// 更新全局选中状态
updateAllSelectedRows()
}
// 更新全局选中状态
const updateAllSelectedRows = () => {
// 更新当前页的选中状态
tableData.value.forEach(row => {
const isSelected = selectedRows.value.some(
selectedRow => selectedRow.salaryBizId === row.salaryBizId
)
if (isSelected) {
allSelectedRows.value.set(row.salaryBizId, row)
} else {
allSelectedRows.value.delete(row.salaryBizId)
}
})
}
// 设置当前页的选中状态
const setCurrentPageSelection = () => {
if (!tableRef.value) return
// 清除当前页的选择
// tableRef.value.clearSelection()
// 设置当前页应该选中的行
tableData.value.forEach(row => {
if (allSelectedRows.value.has(row.salaryBizId)) {
tableRef.value.toggleRowSelection(row, true)
}
})
}
// 清空所有选择
const clearAllSelection = () => {
allSelectedRows.value.clear()
selectedRows.value = []
if (tableRef.value) {
tableRef.value.clearSelection()
}
}
//==============表格复选框结束===========
const addSalaryRecord = () => {
apiSalaryBatchAddDTOList.value.push({
multipleId: Date.now() + Math.random() // 或使用 uuid
})
}
const addSalaryRemittanceRecord = () => {
salaryRemittanceDTOList.value.push({
signleId: Date.now() + Math.random() // 或使用 uuid
})
}
const handleSalarySelectChange = async ({ prop, value, col, rowIndex }) => {
const currentRow = apiSalaryBatchAddDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
//计算应发金额
if (prop === 'month' || prop === 'brokerBizId') {
if (currentRow.brokerBizId && currentRow.month) {
try {
let params = {
brokerBizId: currentRow.brokerBizId,
month: currentRow.month
}
const res = await grossAmountApi(params)
updatedRow.grossAmount = res.data
updatedRow.mpfAmount = 0
updatedRow.otherAmount = 0
updatedRow.paidAmount = res.data
needUpdate = true
} catch (error) {
console.log('应发金额计算失败', error)
ElMessage.error('应发金额计算失败')
}
}
}
if (prop === 'currency1' || prop === 'currency2') {
if (value) {
try {
let params = {
fromCurrency: value,
toCurrency: 'HKD'
}
const res = await getExchangeRate(params)
if (prop === 'currency1') {
updatedRow.exchangeRate1 = res.data
} else if (prop === 'currency2') {
updatedRow.exchangeRate2 = res.data
}
needUpdate = true
} catch (error) {
console.log('汇率计算失败', error)
ElMessage.error('汇率计算失败')
}
}
}
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...apiSalaryBatchAddDTOList.value]
newData[rowIndex] = updatedRow
apiSalaryBatchAddDTOList.value = newData
}
}
const handleSplitCellChange = async ({ row, col, newValue, oldValue, rowIndex, prop }) => {
console.log('单元格改变', row, col, newValue, oldValue, rowIndex, prop)
const currentRow = apiSalaryBatchAddDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
//计算实发金额
if (prop === 'grossAmount' || prop === 'mpfAmount' || prop === 'otherAmount') {
if (currentRow.grossAmount && currentRow.mpfAmount && currentRow.otherAmount) {
try {
let params = {
grossAmount: currentRow.grossAmount,
mpfAmount: currentRow.mpfAmount,
otherAmount: currentRow.otherAmount
}
const res = await calculatePaidAmount(params)
updatedRow.paidAmount = res.data
needUpdate = true
} catch (error) {
console.log('实发金额计算失败', error)
ElMessage.error('实发金额计算失败')
}
}
}
// 如果用户手动修改了 hkdAmount,不做反向联动(符合需求)
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...apiSalaryBatchAddDTOList.value]
newData[rowIndex] = updatedRow
apiSalaryBatchAddDTOList.value = newData
}
}
// 弹窗表单重置
const resetAddReceivablesForm = () => {
// addRecordRef.value.resetForm()
}
const handleConfirmAddSalary = async () => {
// 调用组件的校验方法
const isValid = await editTableRef.value.validateAndShow()
if (isValid) {
try {
let params = {
apiSalaryBatchAddDTOList: apiSalaryBatchAddDTOList.value
}
const res = await batchAddSalary(params)
multipAddSalaryDialog.value = false
ElMessage.success('批量添加薪资单成功')
const searchParams = searchFormRef.value.getFormData()
loadTableData(searchParams)
} catch (error) {
multipAddSalaryDialog.value = true
console.log('批量添加薪资单错误', error)
ElMessage.error('批量添加薪资单失败')
}
}
}
// 单个薪资单下拉框改变
const handleBankSelectChange = async ({ prop, value, col, rowIndex }) => {
const currentRow = salaryRemittanceDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
if (prop === 'currency') {
if (value) {
try {
let params = {
fromCurrency: value,
toCurrency: 'HKD'
}
const res = await getExchangeRate(params)
updatedRow.exchangeRate = res.data
needUpdate = true
} catch (error) {
console.log('汇率计算失败', error)
ElMessage.error('汇率计算失败')
}
}
}
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...salaryRemittanceDTOList.value]
newData[rowIndex] = updatedRow
salaryRemittanceDTOList.value = newData
}
//计算折合港币总金额,改变币种汇率也会改变,所以等表格更新完在更新折合港币总金额
if (prop === 'currency') {
if (value) {
if (currentRow.amount && currentRow.exchangeRate && currentRow.currency) {
try {
let calculateTotalAmountDTOList = JSON.parse(
JSON.stringify(salaryRemittanceDTOList.value)
)
let params = calculateTotalAmountDTOList.reduce((acc, item) => {
if (item.amount && item.exchangeRate && item.currency) {
acc.push({ ...item })
}
return acc
}, [])
const res = await calculateTotalAmountApi({ calculateTotalAmountDTOList: params })
personSaleryFormModel.value.apiSalaryRemittanceFzDTO.totalAmount = res.data
} catch (error) {
console.log('折合港币总金额计算失败', error)
ElMessage.error('折合港币总金额计算失败')
}
}
}
}
}
// 单个薪资单输入框改变
const handleSingleSalaryCellChange = async ({ row, col, newValue, oldValue, rowIndex, prop }) => {
// console.log('单元格改变', row, col, newValue, oldValue, rowIndex, prop)
const currentRow = salaryRemittanceDTOList.value[rowIndex]
if (!currentRow) return
let needUpdate = false
const updatedRow = { ...currentRow }
// 如果用户手动修改了 hkdAmount,不做反向联动(符合需求)
if (needUpdate) {
// 更新表格数据(整体替换,触发视图更新)
const newData = [...salaryRemittanceDTOList.value]
newData[rowIndex] = updatedRow
salaryRemittanceDTOList.value = newData
}
//计算折合港币总金额,改变币种汇率也会改变,所以等表格更新完在更新折合港币总金额
if (prop === 'amount' || prop === 'exchangeRate') {
if (currentRow.amount && currentRow.exchangeRate && currentRow.currency) {
try {
let calculateTotalAmountDTOList = JSON.parse(JSON.stringify(salaryRemittanceDTOList.value))
let params = calculateTotalAmountDTOList.reduce((acc, item) => {
if (item.amount && item.exchangeRate && item.currency) {
acc.push({ ...item })
}
return acc
}, [])
const res = await calculateTotalAmountApi({ calculateTotalAmountDTOList: params })
personSaleryFormModel.value.apiSalaryRemittanceFzDTO.totalAmount = res.data
} catch (error) {
console.log('折合港币总金额计算失败', error)
ElMessage.error('折合港币总金额计算失败')
}
}
}
}
// 单个薪资单的新增,修改
const handleConfirmSingleSalary = async () => {
let formData = await singleSalaryFormRef.value.validate()
try {
if (formData.apiSalaryRemittanceFzDTO) {
formData.apiSalaryRemittanceFzDTO.salaryRemittanceDTOList = JSON.parse(
JSON.stringify(salaryRemittanceDTOList.value)
)
}
if (commonDialogType.value == 'edit') {
const res = await editSingleSalaryApi(formData)
ElMessage.success('薪资单修改成功')
detailDialogVisible.value = false
} else if (commonDialogType.value == 'add') {
const res = await addSingleSalaryApi(formData)
ElMessage.success('薪资单新增成功')
}
detailDialogVisible.value = false
const searchParams = searchFormRef.value.getFormData()
loadTableData(searchParams)
} catch (error) {
if (commonDialogType.value == 'edit') {
ElMessage.error('薪资单修改失败')
} else if (commonDialogType.value == 'add') {
ElMessage.error('薪资单新增失败')
}
detailDialogVisible.value = true
if (error.message && error.message.includes('Validation')) {
ElMessage.error('必填项不能为空')
}
}
}
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'input',
prop: 'brokerName',
label: '转介人'
},
{
type: 'month',
prop: 'month',
label: '薪资年月',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'select',
prop: 'status',
label: '薪资单状态',
// multiple: true,
dictType: 'salary_status'
}
])
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
// 动态生成操作菜单项(根据行数据)
const getOperateItems = row => {
const items = []
// 条件:isPart == 1 时不显示分期出账(使用宽松相等,兼容字符串 '1')
// if (row.type == 1) {
// items.push({ label: '设置状态', value: 'setStatus' })
// items.push({ label: '更新数据', value: 'updateData' })
// }
// 始终显示的菜单项(保持原始顺序)
items.push({ label: '修改薪资单', value: 'salaryEdit' })
items.push({ label: '查看薪资单', value: 'salaryView' })
items.push({ label: '取消薪资单', value: 'salaryCancel' })
return items
}
const handleFormInputChange = async (prop, value, item) => {
console.log('表单输入框改变', prop, value, item)
await nextTick()
//计算实发金额
if (prop === 'grossAmount' || prop === 'mpfAmount' || prop === 'otherAmount') {
if (
personSaleryFormModel.value.grossAmount &&
personSaleryFormModel.value.mpfAmount &&
personSaleryFormModel.value.otherAmount
) {
try {
let params = {
grossAmount: personSaleryFormModel.value.grossAmount,
mpfAmount: personSaleryFormModel.value.mpfAmount,
otherAmount: personSaleryFormModel.value.otherAmount
}
const res = await calculatePaidAmount(params)
personSaleryFormModel.value.paidAmount = res.data
} catch (error) {
console.log('实发金额计算失败', error)
ElMessage.error('实发金额计算失败')
}
}
}
}
const handleFormSelectChange = async (prop, value, item) => {
console.log('表单选中项改变', prop, value, item)
await nextTick()
//计算应发金额
if (prop === 'month' || prop === 'brokerBizId') {
if (personSaleryFormModel.value.brokerBizId && personSaleryFormModel.value.month) {
try {
let params = {
brokerBizId: personSaleryFormModel.value.brokerBizId,
month: personSaleryFormModel.value.month
}
const res = await grossAmountApi(params)
personSaleryFormModel.value.grossAmount = res.data
personSaleryFormModel.value.mpfAmount = 0
personSaleryFormModel.value.otherAmount = 0
personSaleryFormModel.value.paidAmount = res.data
} catch (error) {
console.log('应发金额计算失败', error)
ElMessage.error('应发金额计算失败')
}
}
}
}
//设置单个薪资单的表单编辑状态
const setSignleSalaryFormStatus = () => {
personSaleryFormConfig.value = personSaleryFormConfig.value.map(item => {
if (commonDialogType.value == 'add' || commonDialogType.value == 'edit') {
item.disabled = false
} else {
item.disabled = true
}
return item
})
bankTableColumns.value = bankTableColumns.value.map(item => {
if (commonDialogType.value == 'add' || commonDialogType.value == 'edit') {
item.disabled = false
} else {
item.disabled = true
}
return item
})
}
const handleSingleAdd = () => {
commonDialogTit.value = '增加薪资单'
commonDialogType.value = 'add'
personSaleryFormModel.value = { apiSalaryRemittanceFzDTO: {} }
salaryRemittanceDTOList.value = []
setSignleSalaryFormStatus()
detailDialogVisible.value = true
}
const handleSelect = (e, row) => {
console.log('选中行:', e, row)
selectedRow.value = row
if (e === 'salaryEdit') {
commonDialogTit.value = '修改薪资单'
commonDialogType.value = 'edit'
setSignleSalaryFormStatus()
updateSalary(row)
} else if (e === 'salaryView') {
commonDialogTit.value = '查看薪资单'
commonDialogType.value = 'view'
setSignleSalaryFormStatus()
updateSalary(row)
} else if (e === 'salaryCancel') {
cancelDialogVisible.value = true
cancelFormModel.value = JSON.parse(JSON.stringify(row))
}
}
const handleConfirmCancel = async () => {
let formData = await cancelFormRef.value.validate()
try {
await cancelSalaryApi(formData)
ElMessage.success('薪资单取消成功')
cancelDialogVisible.value = false
const searchParams = searchFormRef.value.getFormData()
loadTableData(searchParams)
} catch (error) {
ElMessage.error('取消薪资单失败')
cancelDialogVisible.value = true
if (error.message && error.message.includes('Validation')) {
ElMessage.error('必填项不能为空')
}
}
}
const handleMultipDelete = async row => {
try {
// 弹出确认框
await ElMessageBox.confirm('确定要删除该记录吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 用户点击确定 → 执行删除逻辑
const index = apiSalaryBatchAddDTOList.value.findIndex(
item => item.multipleId == row.multipleId
)
if (index !== -1) {
apiSalaryBatchAddDTOList.value.splice(index, 1)
ElMessage.success('删除成功')
}
} catch {
// 用户取消或关闭弹窗,不做任何操作
console.log('取消删除')
}
}
const handleBankDelete = async row => {
try {
// 弹出确认框
await ElMessageBox.confirm('确定要删除该记录吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (row.salaryRemittanceBizId) {
await delSalaryApi(row.salaryRemittanceBizId)
}
// 用户点击确定 → 执行删除逻辑
const index = salaryRemittanceDTOList.value.findIndex(item => item.signleId == row.signleId)
if (index !== -1) {
salaryRemittanceDTOList.value.splice(index, 1)
ElMessage.success('删除成功')
}
} catch {
// 用户取消或关闭弹窗,不做任何操作
console.log('取消删除')
}
}
const selectedRow = ref(null)
// 表格数据
const tableData = ref([])
// 按钮事件处理
const handleAdd = () => {
multipAddSalaryDialog.value = true
if (addRecordRef.value) {
addRecordRef.value.resetForm()
}
apiSalaryBatchAddDTOList.value = [{}]
}
const handleExport = async () => {
ElMessage.info('导出待开发')
return
// 获取搜索参数
const params = searchFormRef.value?.getFormData() || {}
const response = await exportReceivedFortune(params)
// 文件名设置为应收款导出_yyyy-MM-dd hh:mm:ss.xlsx,不需要-,用字符串
const fileName = `应收款导出_${new Date().toLocaleString().replace(/\//g, '').replace(/:/g, '').replace(/\s/g, '')}.xlsx`
await safeDownload(response, fileName, 'application/vnd.ms-excel;charset=utf-8')
}
const handleReset = () => {
// 重置搜索表单
searchFormRef.value.resetForm()
searchParams.value = {}
clearAllSelection()
console.log('表单已重置')
loadTableData()
}
const handleQuery = () => {
currentPage.value = 1
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
const visibleDefaultButtons = ref(['add', 'export', 'reset', 'query'])
// 按钮配置
const operationBtnList = ref([
{ key: 'add', direction: 'left', label: '批量制作薪资单', click: handleAdd },
{ key: 'singleAdd', direction: 'left', label: '制作薪资单', click: handleSingleAdd },
{ key: 'check', direction: 'left', label: '提交核对', type: 'warning', click: handleSubmitCheck },
// { key: 'import', direction: 'left', click: handleImport },
{ key: 'export', direction: 'right', click: handleExport },
{ key: 'reset', direction: 'right', click: handleReset },
{ key: 'query', direction: 'right', click: handleQuery }
])
// 分页事件
const handleSizeChange = val => {
pageSize.value = val
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
const handleCurrentChange = val => {
currentPage.value = val
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
// 加载表格数据
const loadTableData = async searchParams => {
loading.value = true
try {
const params = {
...searchParams,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await getSalaryList(params)
tableData.value = response.data.records || []
pageTotal.value = response.data.total || 0
pageSize.value = response.data.size || 50
// 数据加载完成后,设置当前页的选中状态
nextTick(() => {
setCurrentPageSelection()
})
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 获取入账状态,字典值转化方法
onMounted(async () => {
try {
await loadDicts(['salary_status'])
loadTableData()
} catch (error) {
console.error('字典加载失败', error)
} finally {
loading.value = false
}
})
// 格式化函数(每次渲染都会调用,所以能拿到最新字典)
const formatStatus = (row, column) => {
return getDictLabel('salary_status', row.status) // 实时查缓存
}
const detailDialogVisible = ref(false)
const workSalaryTableColumns = ref([
{
prop: 'salaryNo',
label: '发放编号',
sortable: true,
width: '150',
fixed: 'left',
formatter: row => row.salaryNo || '-'
},
{
prop: 'salaryBizId',
label: '业务编号',
sortable: true,
width: '220',
formatter: row => row.salaryBizId || '-'
},
{
prop: 'brokerName',
label: '转介人',
sortable: true,
width: '100',
formatter: row => row.brokerName || '-'
},
{
prop: 'internalNumber',
label: '内部编号',
sortable: true,
width: '150',
formatter: row => row.internalNumber || '-'
},
{
prop: 'teamName',
label: '所属团队',
sortable: true,
width: '130',
formatter: row => row.teamName || '-'
},
{
prop: 'paidAmount',
label: '发放金额',
sortable: true,
width: '150',
formatter: row => formatCurrency(row.paidAmount || 0)
},
{
prop: 'currencyList',
label: '发放币种',
sortable: true,
width: '150',
formatter: row => row.currencyList || '-'
},
{
prop: 'month',
label: '出账月(实)',
sortable: true,
width: '120',
formatter: row => row.month || '-'
},
{
prop: 'status',
label: '薪资单状态',
sortable: true,
width: '120',
formatter: row => formatStatus(row) || '-'
},
{
prop: 'creatorName',
label: '制作人',
sortable: true,
width: '120',
formatter: row => row.creatorName || '-'
},
{
prop: 'createTime',
label: '制作时间',
sortable: true,
width: '180',
formatter: row => row.createTime || '-'
}
])
const updateSalary = async row => {
try {
const response = await updateSalaryDetail(row.salaryBizId)
personSaleryFormModel.value = response.data
if (personSaleryFormModel.value.apiSalaryRemittanceFzDTO) {
salaryRemittanceDTOList.value =
personSaleryFormModel.value.apiSalaryRemittanceFzDTO.salaryRemittanceDTOList.map(item => {
return {
...item,
signleId: Date.now() + Math.random()
}
})
}
detailDialogVisible.value = true
} catch (error) {
console.error('获取薪资单详情失败:', error)
ElMessage.error('获取薪资单详情失败')
}
}
</script>
<style scoped lang="scss">
/* 使用 :deep 穿透 scoped 样式,保证对 el-table__row 生效 */
:deep(.estimated-row) {
background-color: #f0f9ff; /* 浅蓝色,可根据需要修改 */
}
</style>
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