Commit 20e7b750 by yuzhenWang

解决冲突

parents 1f7ca662 ef6e03ce
......@@ -36,7 +36,12 @@
.el-dialog__header.dialog-header.show-close {
padding-right: 0 ;
}
.statistics-container {
padding: 10px;
background-color: rgba(0,82,217,0.03);
border-radius: 4px;
margin-bottom: 5px;
}
#loader-wrapper {
position: fixed;
top: 0;
......
......@@ -151,3 +151,11 @@ export function getInsuranceCategory(data) {
method: 'post'
})
}
// 通用excel导入
export function importExcel(data) {
return request({
url: '/oss/api/excel/import',
method: 'post',
data: data
})
}
......@@ -9,14 +9,6 @@ export function getPolicyCommissionList(data) {
})
}
// 更新保单来佣信息
export function updatePolicyCommission(data) {
return request({
url: '/csf/api/commission/update',
method: 'post',
data: data
})
}
// 生成可出账记录
export function generateCommissionRecord(data) {
......@@ -64,7 +56,6 @@ export function downloadPolicyFortuneAccount(data) {
url: '/csf/api/fortune/download/account',
method: 'post',
data: data,
responseType: 'blob'
})
}
......@@ -256,7 +247,7 @@ export function expectedFortuneStatistics(data) {
data: data
})
}
// 入账记录查询
// 入账比对记录查询
export function commissionEntryRecord(data) {
return request({
url: '/csf/api/commission/compare/records',
......@@ -272,3 +263,143 @@ export function commissionEntryEditRecords(data) {
data: data
})
}
// 获取保单发佣列表
export function policyNoCommissionPayRecord(data) {
return request({
url: '/csf/api/fortune/list/page/vo',
method: 'post',
data: data
})
}
// 应收款导出
export function exportReceivedFortune(data) {
return request({
url: '/csf/api/CommissionExpected/export',
method: 'post',
data: data,
responseType: 'blob'
})
}
// 入账记录查询
export function commissionExpectedRecord(data) {
return request({
url: '/csf/api/commission/pageByCommissionexpectedBizId',
method: 'post',
data: data
})
}
// 出账记录查询
export function payRecordList(data) {
return request({
url: '/csf/api/fortune/pageByExpectedFortuneBizId',
method: 'post',
data: data
})
}
// 更新入账信息
export function updateCommissionExpected(data) {
return request({
url: '/csf/api/CommissionExpected/update',
method: 'post',
data: data
})
}
// 修改出账状态
export function updataPayrollStatus(data){
return request({
url: '/csf/api/fortune/update/status',
method: 'post',
data: data
})
}
// 批量新增检核记录
export function addPayrollCheckRecord(data){
return request({
url: '/csf/api/commission/addBatch',
method: 'post',
data: data
})
}
// 新增应收款
export function addReceivedFortune(data){
return request({
url: '/csf/api/CommissionExpected/add',
method: 'post',
data: data
})
}
// 新增出账记录
export function addPayRecord(data){
return request({
url: '/csf/api/expectedFortune/add',
method: 'post',
data: data
})
}
// 获取销售员详情
export function userSaleExpandDetail(data){
return request({
url: '/insurance/base/api/userSaleExpand/detail?userSaleBizId=' + data,
method: 'get',
})
}
// 更新比对状态
export function updateCompareStatus(data){
return request({
url: '/csf/api/commission/updateCompareStatus',
method: 'post',
data: data
})
}
// 更新数据
export function updateCommissionRecord(data){
return request({
url: '/csf/api/commission/update',
method: 'post',
data: data
})
}
// 新增出账检核记录
export function addCheckRecordaddBatch(data){
return request({
url: '/csf/api/fortune/addBatch',
method: 'post',
data: data
})
}
// 设置本期出账金额
export function updatePayoutAmount(data){
return request({
url: '/csf/api/fortune/update',
method: 'post',
data: data
})
}
// 同步预计来佣
export function syncExpectedCommission(data){
return request({
url: '/csf/api/commission/addToExpected',
method: 'post',
data: data
})
}
// 更新出账记录
export function updatePayRecord(data){
return request({
url: '/csf/api/expectedFortune/update',
method: 'post',
data: data
})
}
// src/api/search.ts
import request from '@/utils/request'
// 通用搜索接口
export function commonSearch(params: {
module: string
keyword?: string
pageSize?: number
}) {
return request({
url: '/common/search',
method: 'get',
params
})
}
// 特定模块搜索
export function searchCompanies(params: {
type?: string
keyword?: string
pageSize?: number
}) {
return request({
url: '/company/search',
method: 'get',
params
})
}
export function searchCommissionTypes(params: {
keyword?: string
pageSize?: number
}) {
return request({
url: '/commission/type/search',
method: 'get',
params
})
}
export function searchInsurers(params: {
keyword?: string
pageSize?: number
}) {
return request({
url: '/insurer/search',
method: 'get',
params
})
}
export function searchProducts(params: {
keyword?: string
pageSize?: number
}) {
return request({
url: '/product/search',
method: 'get',
params
})
}
\ No newline at end of file
......@@ -14,6 +14,7 @@
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
:drag="drag"
v-if="!disabled"
>
<!-- 上传按钮 -->
......
<!-- src/components/RemoteMultiSelect.vue -->
<template>
<el-select
v-model="selectedValues"
:placeholder="placeholder"
:multiple="multiple"
:filterable="remote || filterable"
:remote="remote"
:remote-method="handleRemoteSearch"
:loading="loading"
:reserve-keyword="false"
:clearable="clearable"
:collapse-tags="collapseTags"
:max-collapse-tags="maxCollapseTags"
:size="size"
:popper-class="['remote-multi-select', popperClass]"
:disabled="disabled"
@change="handleChange"
@visible-change="handleVisibleChange"
>
<!-- 自定义下拉头部:全选功能 -->
<template #header v-if="showCheckAll && multiple">
<div class="select-header">
<el-checkbox
v-model="checkAll"
:indeterminate="indeterminate"
@change="handleCheckAllChange"
:disabled="disabled"
>
{{ checkAllLabel }}
</el-checkbox>
</div>
</template>
<!-- 选项列表 -->
<el-option
v-for="option in options"
:key="getOptionKey(option)"
:label="getOptionLabel(option)"
:value="getOptionValue(option)"
:disabled="option.disabled"
/>
<!-- 无数据时的提示 -->
<template #empty>
<div class="empty-options">
<span v-if="loading">加载中...</span>
<span v-else-if="options.length === 0 && !hasSearched">请输入关键词搜索</span>
<span v-else>无匹配数据</span>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import { computed, ref, watch, toRefs, defineProps, defineEmits } from 'vue'
import { useRemoteSearch } from '@/hooks/useRemoteSearch'
import type { RemoteSearchConfig } from '@/hooks/useRemoteSearch'
interface OptionItem {
label: string
value: string | number
disabled?: boolean
[key: string]: any
}
interface Props {
modelValue: (string | number)[] | string | number
config: RemoteSearchConfig
placeholder?: string
multiple?: boolean
clearable?: boolean
collapseTags?: boolean
maxCollapseTags?: number
size?: 'large' | 'default' | 'small'
disabled?: boolean
showCheckAll?: boolean
checkAllLabel?: string
popperClass?: string
// 自定义选项键名
labelKey?: string
valueKey?: string
optionKey?: string
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => ([]),
placeholder: '请选择',
multiple: true,
clearable: true,
collapseTags: true,
maxCollapseTags: 1,
size: 'large',
disabled: false,
showCheckAll: true,
checkAllLabel: '全选',
labelKey: 'label',
valueKey: 'value',
optionKey: 'value'
})
const emit = defineEmits<{
'update:modelValue': [value: (string | number)[] | string | number]
'change': [value: (string | number)[] | string | number]
'search': [query: string]
}>()
const {
config,
multiple,
showCheckAll,
checkAllLabel,
labelKey,
valueKey,
optionKey
} = toRefs(props)
// 远程搜索实例
const remoteSearch = useRemoteSearch(config.value)
// 本地状态
const selectedValues = ref<(string | number)[] | string | number>(
Array.isArray(props.modelValue) ? [...props.modelValue] : props.modelValue
)
const hasSearched = ref(false)
const currentQuery = ref('')
// 计算属性
const loading = computed(() => remoteSearch.state.loading)
const options = computed(() => remoteSearch.state.options)
const checkAll = computed({
get() {
if (!props.multiple || !Array.isArray(selectedValues.value)) return false
const enabledOptions = options.value.filter(opt => !opt.disabled)
return selectedValues.value.length === enabledOptions.length && enabledOptions.length > 0
},
set(val: boolean) {
handleCheckAllChange(val)
}
})
const indeterminate = computed(() => {
if (!props.multiple || !Array.isArray(selectedValues.value)) return false
const enabledOptions = options.value.filter(opt => !opt.disabled)
return selectedValues.value.length > 0 && selectedValues.value.length < enabledOptions.length
})
// 方法
const getOptionKey = (option: OptionItem) => {
return option[optionKey.value] ?? option.value
}
const getOptionLabel = (option: OptionItem) => {
return option[labelKey.value] ?? option.label
}
const getOptionValue = (option: OptionItem) => {
return option[valueKey.value] ?? option.value
}
const handleRemoteSearch = async (query: string) => {
currentQuery.value = query
hasSearched.value = true
emit('search', query)
await remoteSearch.search(query)
}
const handleChange = (value: (string | number)[] | string | number) => {
selectedValues.value = value
emit('update:modelValue', value)
emit('change', value)
}
const handleCheckAllChange = (checked: boolean) => {
if (!props.multiple) return
if (checked) {
// 全选:只选择非禁用的选项
selectedValues.value = options.value
.filter(opt => !opt.disabled)
.map(opt => getOptionValue(opt))
} else {
selectedValues.value = []
}
emit('update:modelValue', selectedValues.value)
emit('change', selectedValues.value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && options.value.length === 0 && !hasSearched.value) {
// 下拉框打开时,如果没有搜索过,执行一次搜索
handleRemoteSearch('')
}
}
// 监听外部值变化
watch(() => props.modelValue, (newVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(selectedValues.value)) {
selectedValues.value = newVal
}
}, { deep: true })
// 监听配置变化
watch(() => props.config, (newConfig) => {
// 重新初始化远程搜索实例(实际实现中可能需要更复杂的处理)
Object.assign(remoteSearch, useRemoteSearch(newConfig))
}, { deep: true })
// 初始化
if (config.value.defaultOptions?.length > 0) {
remoteSearch.setDefaultOptions(config.value.defaultOptions)
}
// 暴露方法给父组件
defineExpose({
search: handleRemoteSearch,
clearCache: remoteSearch.clearCache,
preload: remoteSearch.preload
})
</script>
<style scoped>
.select-header {
padding: 8px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
background-color: var(--el-bg-color);
.el-checkbox {
width: 100%;
.el-checkbox__label {
font-weight: 500;
color: var(--el-color-primary);
}
}
}
.empty-options {
padding: 8px 12px;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 14px;
}
</style>
<style>
.remote-multi-select .el-select-dropdown__list {
padding-top: 0;
}
.remote-multi-select .el-select-dropdown__item {
padding: 8px 12px;
}
.remote-multi-select .el-select-dropdown__item.selected {
background-color: var(--el-color-primary-light-9);
}
.remote-multi-select .el-select-dropdown__item.is-disabled {
opacity: 0.6;
}
</style>
\ No newline at end of file
<template>
<el-select
ref="selectRef"
v-model="selectedValue"
:placeholder="placeholder"
:clearable="clearable"
:filterable="true"
:remote="true"
:remote-method="handleRemoteSearch"
:loading="loading"
:reserve-keyword="false"
:disabled="disabled"
:size="size"
:popper-class="['remote-select', popperClass]"
@change="handleChange"
@visible-change="handleVisibleChange"
>
<el-option
v-for="option in options"
:key="getOptionKey(option)"
:label="getOptionLabel(option)"
:value="getOptionValue(option)"
:disabled="option.disabled"
/>
<template #empty>
<div class="empty-options">
<span v-if="loading">加载中...</span>
<span v-else-if="options.length === 0 && !hasSearched">请输入关键词搜索</span>
<span v-else>无匹配数据</span>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useRemoteSearch } from '@/hooks/useRemoteSearch'
import type { RemoteSearchConfig, FormOption } from '@/types/search-form'
interface Props {
modelValue: string | number
config: RemoteSearchConfig
placeholder?: string
clearable?: boolean
disabled?: boolean
size?: 'large' | 'default' | 'small'
popperClass?: string
optionLabel?: string
optionValue?: string
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
placeholder: '请搜索选择',
clearable: true,
disabled: false,
size: 'default',
popperClass: '',
optionLabel: 'label',
optionValue: 'value'
})
const emit = defineEmits<{
'update:modelValue': [value: string | number]
'change': [value: string | number]
}>()
// 远程搜索实例
const remoteSearch = useRemoteSearch(props.config)
// 本地状态
const selectRef = ref()
const selectedValue = ref(props.modelValue)
const hasSearched = ref(false)
const currentQuery = ref('')
// 计算属性
const loading = computed(() => remoteSearch.state.loading)
const options = computed(() => remoteSearch.state.options)
// 方法
const getOptionKey = (option: FormOption) => {
return option[props.optionValue] ?? option.value
}
const getOptionLabel = (option: FormOption) => {
return option[props.optionLabel] ?? option.label
}
const getOptionValue = (option: FormOption) => {
return option[props.optionValue] ?? option.value
}
const handleRemoteSearch = async (query: string) => {
currentQuery.value = query
hasSearched.value = true
await remoteSearch.search(query)
}
const handleChange = (value: string | number) => {
selectedValue.value = value
emit('update:modelValue', value)
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && options.value.length === 0 && !hasSearched.value) {
handleRemoteSearch('')
}
}
// 监听外部值变化
watch(() => props.modelValue, (newVal) => {
if (newVal !== selectedValue.value) {
selectedValue.value = newVal
}
})
// 暴露方法
defineExpose({
search: handleRemoteSearch,
clearCache: remoteSearch.clearCache,
focus: () => selectRef.value?.focus()
})
// 初始化
onMounted(() => {
if (props.config.defaultOptions?.length > 0) {
remoteSearch.setDefaultOptions(props.config.defaultOptions)
}
})
</script>
\ No newline at end of file
<template>
<el-form ref="formRef" :model="localModel" :rules="formRules" label-width="auto" v-bind="$attrs" :validate-on-rule-change="false">
<el-row :gutter="20">
<el-col v-for="item in visibleConfig" :key="item.prop" :span="item.span || 6">
<el-form-item :label="item.label" :prop="item.prop" :class="{ 'search-form-item': isSearch }">
<!-- Input -->
<el-input v-if="item.type === 'input'" v-model="localModel[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`" :clearable="true"
@input="(val) => handleNumberInput(val, item)"
@change="(val) => handleModelChange(val, item)" />
<!-- 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 :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>
<!-- Date -->
<el-date-picker v-else-if="item.type === 'date'" v-model="localModel[item.prop]" type="date"
:placeholder="`选择${item.label}`" :value-format="item.valueFormat || 'YYYY-MM-DD'"
style="width: 100%" :disabled-date="getDisabledDateFn(item)"
@change="(val) => handleModelChange(val, item)" />
<!-- Month -->
<el-date-picker v-else-if="item.type === 'month'" v-model="localModel[item.prop]" type="month"
:placeholder="`选择${item.label}`" :value-format="item.valueFormat || 'YYYY-MM'"
style="width: 100%" :disabled-date="getDisabledDateFn(item)"
@change="(val) => handleModelChange(val, item)" />
<!-- Daterange -->
<el-date-picker v-else-if="item.type === 'daterange'" v-model="localModel[item.prop]"
type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"
:value-format="item.valueFormat || 'YYYY-MM-DD'" :disabled-date="getDisabledDateFn(item)"
style="width: 100%" @change="(val) => handleModelChange(val, item)" />
<!-- Checkbox Group -->
<el-checkbox-group v-else-if="item.type === 'checkbox-group'" v-model="localModel[item.prop]"
@change="(val) => handleModelChange(val, item)">
<el-checkbox v-for="opt in getSelectOptions(item)" :key="opt.value" :label="opt.value">
{{ opt.label }}
</el-checkbox>
</el-checkbox-group>
<!-- textarea -->
<el-input v-else-if="item.type === 'textarea'" v-model="localModel[item.prop]" style="width: 240px"
autosize type="textarea" placeholder="请输入" :clearable="true"
@change="(val) => handleModelChange(val, item)" />
<span v-else>不支持的类型: {{ item.type }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import { ref, watch, onMounted, nextTick, computed } from 'vue'
import { ElMessage } from 'element-plus'
// 🔑 引入你的字典方法
import useDictStore from '@/store/modules/dict'
import { getDicts } from '@/api/system/dict/data'
import request from '@/utils/request'
import dayjs from 'dayjs'
// ==================== 工具函数:深拷贝配置(保留函数) ====================
function deepCloneConfig(obj) {
if (obj === null || typeof obj !== 'object') return obj
if (Array.isArray(obj)) return obj.map(deepCloneConfig)
const cloned = {}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const val = obj[key]
cloned[key] = typeof val === 'function' ? val : deepCloneConfig(val)
}
}
return cloned
}
function parseToDate(str) {
if (!str) return null
if (str === 'today') {
return dayjs().startOf('day')
}
if (typeof str === 'string') {
const d = dayjs(str)
return d.isValid() ? d.startOf('day') : null
}
if (str instanceof Date) {
return dayjs(str).startOf('day')
}
return null
}
// ==================== 生成 disabledDate 函数 ====================
function getDisabledDateFn(item) {
const { minDate, maxDate } = item
// 如果都没有限制,返回 null(不禁用任何日期)
if (minDate == null && maxDate == null) {
return () => false
}
return (date) => {
const currentDate = dayjs(date).startOf('day')
let minD = null
let maxD = null
// 解析最小日期
if (minDate != null) {
if (typeof minDate === 'function') {
const val = minDate(localModel.value)
minD = parseToDate(val)
} else {
minD = parseToDate(minDate)
}
}
// 解析最大日期
if (maxDate != null) {
if (typeof maxDate === 'function') {
const val = maxDate(localModel.value)
maxD = parseToDate(val)
} else {
maxD = parseToDate(maxDate)
}
}
// 判断是否被禁用
if (minD && currentDate.isBefore(minD)) {
return true
}
if (maxD && currentDate.isAfter(maxD)) {
return true
}
return false
}
}
// ==================== Props & Emits ====================
const props = defineProps({
modelValue: { type: Object, default: () => ({}) },
config: { type: Array, default: () => [] },
isSearch: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'update'])
// ==================== Refs ====================
const formRef = ref(null)
// 使用 shallowRef 避免深层响应式(表单通常扁平)
const localModel = ref({ ...props.modelValue })
// 记录哪些字段的字典已加载
const dictLoaded = ref(new Set())
const internalConfig = ref([])
const remoteOptions = ref({}) // { prop: [options] }
const remoteLoading = ref({}) // { prop: boolean }
// ==================== 条件显隐 & 规则 ====================
const visibleConfig = computed(() => {
return internalConfig.value.filter(item => {
if (typeof item.visible === 'function') {
return item.visible(localModel.value)
}
return true
})
})
const formRules = computed(() => {
const rules = {}
visibleConfig.value.forEach(item => {
if (item.rules) rules[item.prop] = item.rules
})
return rules
})
// 1. 外部 modelValue 变化时,安全同步(仅当内容不同时)
// 监听 config 变化(支持动态 config)
watch(
() => props.config,
(newConfig) => {
if (!newConfig || newConfig.length === 0) return
internalConfig.value = deepCloneConfig(newConfig)
// 构建初始模型
const initialModel = {}
for (const item of internalConfig.value) {
const key = item.prop
// 优先用父传值,否则用默认值
if (props.modelValue?.[key] !== undefined) {
initialModel[key] = props.modelValue[key]
} else if (item.multiple || ['checkbox-group', 'daterange'].includes(item.type)) {
initialModel[key] = item.defaultValue ?? []
} else {
initialModel[key] = item.defaultValue ?? ''
}
}
localModel.value = initialModel
},
{ immediate: true }
)
watch(
() => props.modelValue,
(newVal) => {
if (!newVal) return
// 只同步存在的字段,避免污染
for (const item of internalConfig.value) {
const key = item.prop
if (newVal.hasOwnProperty(key)) {
localModel.value[key] = newVal[key]
}
}
}
)
// 当字典加载完成时,触发同步
function markDictLoaded(prop) {
dictLoaded.value.add(prop)
// 尝试同步该字段
if (props.modelValue?.[prop] !== undefined) {
localModel.value[prop] = props.modelValue[prop]
}
}
// 2. 用户操作导致 localModel 变化时,emit(防抖可选)
function handleModelChange(value, item) {
console.log('✅ handleModelChange 被调用', { prop: item?.prop, value })
// 同步额外字段
if (item?.type === 'select' && item.onChangeExtraFields) {
const options = getSelectOptions(item)
const opt = options.find(o => o.value === value) // ✅ 现在 value 和 o.value 类型一致
if (opt) {
for (const [targetProp, sourceKey] of Object.entries(item.onChangeExtraFields)) {
localModel.value[targetProp] = opt.raw[sourceKey]
console.log(`✅ 同步 ${targetProp} =`, opt.raw[sourceKey])
}
}
}
// emit 更新
nextTick(() => {
if (!isEqualShallow(props.modelValue, localModel.value)) {
console.log('准备 emit modelValue:', localModel.value)
emit('update:modelValue', { ...localModel.value })
}
})
}
// 辅助函数:浅比较两个对象
function isEqualShallow(a, b) {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (let key of keysA) {
if (a[key] !== b[key]) return false
}
return true
}
// ==================== 加载字典选项 ====================
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) {
console.error(`加载字典 ${dictType} 失败`, err)
ElMessage.error(`字典 ${dictType} 加载失败`)
return []
}
}
// ==================== 初始化 ====================
onMounted(async () => {
internalConfig.value = deepCloneConfig(props.config)
const initialData = {}
const dictTypePromises = []
const apiPromises = [] // ← 新增:收集 api 加载 promise
for (const item of internalConfig.value) {
const key = item.prop
if (localModel.value[key] == null) {
if (item.multiple) {
initialData[key] = item.defaultValue ?? []
} else if (['checkbox-group', 'daterange'].includes(item.type)) {
initialData[key] = item.defaultValue ?? []
} else {
initialData[key] = item.defaultValue ?? ''
}
}
// 预加载 dictType
if (item.type === 'select' && item.dictType) {
dictTypePromises.push(
loadDictOptions(item.dictType).then(opts => {
remoteOptions.value[key] = opts
markDictLoaded(key) // ← 立即标记已加载
})
)
} // 预加载 api(远程接口)← 关键新增!
else if (item.type === 'select' && item.api) {
apiPromises.push(
loadRemoteOptionsForInit(item) // ← 专门用于初始化的加载函数
)
} else if (item.type === 'select' && item.options) {
remoteOptions.value[key] = [...item.options]
markDictLoaded(key)
}
// api 类型:延迟加载(focus 时)
}
if (Object.keys(initialData).length > 0) {
localModel.value = { ...localModel.value, ...initialData }
}
// 等待所有字典加载完成
await Promise.allSettled(dictTypePromises)
})
// ==================== 获取 select 选项 ====================
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 // ← 必须存 raw
}))
} else if (item.options) {
// 静态选项s
return item.options.map(opt => ({
value: opt.value,
label: opt.label,
raw: opt.raw // 保留原始
}))
}
return []
}
async function loadRemoteOptionsForInit(item) {
const { prop, api, requestParams = {} } = item
try {
// 构造请求体:只传 requestParams,不传 keyword
const payload = {
...(requestParams || {})
}
const res = await request({
url: api,
method: 'post',
data: payload
})
const list = typeof item.transform === 'function'
? item.transform(res)
: res.data?.records || res.data || []
// 建议:统一转成字符串(或根据 item.valueType 判断)
const newOptions = list.map(i => ({
value: String(i[item.valueKey || 'value']), // ← 强制转字符串
label: i[item.labelKey || 'label'],
raw: i
}))
remoteOptions.value[prop] = newOptions
markDictLoaded(prop) // ← 关键:标记已加载
} catch (err) {
ElMessage.error(`预加载 ${item.label} 失败`)
remoteOptions.value[prop] = []
}
}
// ==================== 加载远程 API 选项(首次 focus 时加载,无搜索词) ====================
async function loadRemoteOptions(item) {
const { prop, api, requestParams = {}, keywordField, debounceWait, ...rest } = item
if (!api || remoteOptions.value[prop]?.length > 0) return
try {
remoteLoading.value[prop] = true
// 构造请求体:合并 requestParams + 分页(默认第一页)
const payload = {
...(requestParams || {})
// 注意:此时无 keyword,所以不加 keywordField
}
const res = await request({
url: api,
method: 'post', // ← 改为 POST
data: payload // ← 参数放 body
})
const list = typeof item.transform === 'function'
? item.transform(res)
: res.data?.records || res.data || []
// 建议:统一转成字符串(或根据 item.valueType 判断)
const newOptions = list.map(i => ({
value: String(i[item.valueKey || 'value']), // ← 强制转字符串
label: i[item.labelKey || 'label'],
raw: i
}))
remoteOptions.value[prop] = newOptions
// ✅ 关键:标记该字段字典已加载
markDictLoaded(prop)
} catch (err) {
ElMessage.error(`加载 ${item.label} 失败`)
remoteOptions.value[prop] = []
} finally {
remoteLoading.value[prop] = false
}
}
// ==================== 远程搜索(带关键词,防抖) ====================
let searchTimeout = null
function handleFilterChange(keyword, item) {
const { prop, api, requestParams = {}, keywordField = 'keyword', debounceWait = 300 } = item
if (!api) return
clearTimeout(searchTimeout)
searchTimeout = setTimeout(async () => {
try {
remoteLoading.value[prop] = true
// 构造请求体:requestParams + 分页 + 动态关键词字段
const payload = {
...(requestParams || {}),
[keywordField]: keyword // ← 动态字段名,如 name / companyName
}
const res = await request({
url: api,
method: 'post', // ← POST 请求
data: payload // ← body 传参
})
const list = typeof item.transform === 'function'
? item.transform(res)
: res.data?.records || res.data || []
remoteOptions.value[prop] = list.map(i => ({
value: i[item.valueKey || 'value'],
label: i[item.labelKey || 'label'],
raw: i // ← 保存完整对象
}))
} catch (err) {
ElMessage.error(`搜索 ${item.label} 失败`)
} finally {
remoteLoading.value[prop] = false
}
}, debounceWait)
}
// ==================== 数字输入处理 ====================
function handleNumberInput(value, item) {
const { inputType = 'text', decimalDigits = 2, prop } = item
if (!prop) return
let result = String(value ?? '').trim()
if (inputType === 'integer') {
// 只保留数字
result = result.replace(/[^\d]/g, '')
} else if (inputType === 'decimal') {
// 1. 只保留数字和小数点
result = result.replace(/[^\d.]/g, '')
// 2. 去掉开头的小数点(不允许 ".5" → 改为 "0.5" 更好,但这里先简单处理)
if (result.startsWith('.')) {
result = '0.' + result.slice(1)
}
// 3. 保证最多一个小数点
const parts = result.split('.')
if (parts.length > 2) {
result = parts[0] + '.' + parts.slice(1).join('')
}
// 4. 限制小数位数(但保留结尾的小数点!)
if (result.includes('.')) {
const [intPart, decPart] = result.split('.')
// 如果小数部分超过限制,截断
if (decPart.length > decimalDigits) {
result = intPart + '.' + decPart.slice(0, decimalDigits)
}
// ✅ 不再删除结尾的 '.'
}
}
// 防止重复赋值(可选优化)
if (localModel.value[prop] !== result) {
localModel.value = { ...localModel.value, [prop]: result }
}
}
// ==================== 暴露方法 ====================
defineExpose({
getFormData() {
return { ...localModel.value }
},
async validate() {
return new Promise((resolve, reject) => {
formRef.value?.validate((valid) => {
if (valid) resolve(localModel.value)
else reject(new Error('Validation failed'))
})
})
},
resetForm() {
const resetData = {}
internalConfig.value.forEach(item => {
const key = item.prop
if (['checkbox-group', 'daterange'].includes(item.type) || item.multiple) {
resetData[key] = item.defaultValue ?? []
} else {
resetData[key] = item.defaultValue ?? ''
}
})
localModel.value = { ...resetData }
nextTick(() => formRef.value?.clearValidate())
}
})
</script>
<style scoped>
.search-form-item {
margin-bottom: 20px;
}
</style>
\ No newline at end of file
<template>
<div class="search-form-container">
<el-form
ref="formRef"
:model="formData"
:label-width="labelWidth"
:label-position="labelPosition"
:inline="inline"
:size="size"
:disabled="disabled"
:class="formClass"
>
<!-- 前置插槽 -->
<slot v-if="prefixSlot" name="prefix"></slot>
<el-row :gutter="gutter" :class="rowClass">
<template v-for="field in visibleFields" :key="field.field">
<el-col
:span="getColSpan(field, 'span')"
:xs="getColSpan(field, 'xs')"
:sm="getColSpan(field, 'sm')"
:md="getColSpan(field, 'md')"
:lg="getColSpan(field, 'lg')"
:xl="getColSpan(field, 'xl')"
>
<el-form-item
:label="field.label"
:prop="field.field"
:rules="getRules(field)"
:class="field.class"
:style="field.style"
:label-width="field.labelWidth"
>
<!-- 输入框 -->
<template v-if="field.type === 'input'">
<el-input
v-model="formData[field.field]"
:placeholder="field.placeholder || `请输入${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:readonly="field.readonly"
:show-password="field.type === 'password'"
:show-word-limit="field.showWordLimit"
:maxlength="field.maxlength"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 文本域 -->
<template v-else-if="field.type === 'textarea'">
<el-input
v-model="formData[field.field]"
type="textarea"
:placeholder="field.placeholder || `请输入${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:readonly="field.readonly"
:rows="field.minRows || 3"
:max-rows="field.maxRows"
:show-word-limit="field.showWordLimit"
:maxlength="field.maxlength"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 数字输入框 -->
<template v-else-if="field.type === 'number'">
<el-input-number
v-model="formData[field.field]"
:placeholder="field.placeholder || `请输入${field.label}`"
:disabled="field.disabled"
:readonly="field.readonly"
:step="field.step"
:precision="field.precision"
:min="field.min"
:max="field.max"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 普通下拉框(单选) -->
<template v-else-if="field.type === 'select'">
<el-select
v-model="formData[field.field]"
:placeholder="field.placeholder || `请选择${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
>
<el-option
v-for="option in getOptions(field)"
:key="getOptionValue(option, field)"
:label="getOptionLabel(option, field)"
:value="getOptionValue(option, field)"
:disabled="option.disabled"
/>
</el-select>
</template>
<!-- 多选下拉框 -->
<template v-else-if="field.type === 'multi-select'">
<el-select
v-model="formData[field.field]"
:placeholder="field.placeholder || `请选择${field.label}`"
:clearable="field.clearable ?? true"
:multiple="true"
:collapse-tags="true"
:max-collapse-tags="1"
:disabled="field.disabled"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
>
<el-option
v-for="option in getOptions(field)"
:key="getOptionValue(option, field)"
:label="getOptionLabel(option, field)"
:value="getOptionValue(option, field)"
:disabled="option.disabled"
/>
</el-select>
</template>
<!-- 远程搜索下拉框(单选) -->
<template v-else-if="field.type === 'remote-select'">
<RemoteSelect
v-model="formData[field.field]"
:config="field.remoteConfig!"
:placeholder="field.placeholder || `请搜索选择${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:style="{ width: getFieldWidth(field) }"
:option-label="field.optionLabel"
:option-value="field.optionValue"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 远程搜索下拉框(多选) -->
<template v-else-if="field.type === 'remote-multi-select'">
<RemoteMultiSelect
v-model="formData[field.field]"
:config="field.remoteConfig!"
:placeholder="field.placeholder || `请搜索选择${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:style="{ width: getFieldWidth(field) }"
:option-label="field.optionLabel"
:option-value="field.optionValue"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 日期选择器 -->
<template v-else-if="field.type === 'date'">
<el-date-picker
v-model="formData[field.field]"
type="date"
:placeholder="field.placeholder || `请选择${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:readonly="field.readonly"
:format="field.dateFormat"
:value-format="field.dateFormat"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 日期范围选择器 -->
<template v-else-if="field.type === 'daterange'">
<el-date-picker
v-model="formData[field.field]"
type="daterange"
:range-separator="field.rangeSeparator || '至'"
:start-placeholder="field.startPlaceholder || '开始日期'"
:end-placeholder="field.endPlaceholder || '结束日期'"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:readonly="field.readonly"
:format="field.dateFormat"
:value-format="field.dateFormat"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 日期时间选择器 -->
<template v-else-if="field.type === 'datetime'">
<el-date-picker
v-model="formData[field.field]"
type="datetime"
:placeholder="field.placeholder || `请选择${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:readonly="field.readonly"
:format="field.dateFormat"
:value-format="field.dateFormat"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 日期时间范围选择器 -->
<template v-else-if="field.type === 'datetimerange'">
<el-date-picker
v-model="formData[field.field]"
type="datetimerange"
:range-separator="field.rangeSeparator || '至'"
:start-placeholder="field.startPlaceholder || '开始时间'"
:end-placeholder="field.endPlaceholder || '结束时间'"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:readonly="field.readonly"
:format="field.dateFormat"
:value-format="field.dateFormat"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 单选框 -->
<template v-else-if="field.type === 'radio'">
<el-radio-group
v-model="formData[field.field]"
:disabled="field.disabled"
@change="handleChange(field.field, $event)"
v-bind="field.props"
>
<el-radio
v-for="option in getOptions(field)"
:key="getOptionValue(option, field)"
:label="getOptionValue(option, field)"
:disabled="option.disabled"
>
{{ getOptionLabel(option, field) }}
</el-radio>
</el-radio-group>
</template>
<!-- 复选框 -->
<template v-else-if="field.type === 'checkbox'">
<el-checkbox-group
v-model="formData[field.field]"
:disabled="field.disabled"
@change="handleChange(field.field, $event)"
v-bind="field.props"
>
<el-checkbox
v-for="option in getOptions(field)"
:key="getOptionValue(option, field)"
:label="getOptionValue(option, field)"
:disabled="option.disabled"
>
{{ getOptionLabel(option, field) }}
</el-checkbox>
</el-checkbox-group>
</template>
<!-- 开关 -->
<template v-else-if="field.type === 'switch'">
<el-switch
v-model="formData[field.field]"
:disabled="field.disabled"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 级联选择器 -->
<template v-else-if="field.type === 'cascader'">
<el-cascader
v-model="formData[field.field]"
:options="getOptions(field)"
:placeholder="field.placeholder || `请选择${field.label}`"
:clearable="field.clearable ?? true"
:disabled="field.disabled"
:style="{ width: getFieldWidth(field) }"
@change="handleChange(field.field, $event)"
v-bind="field.props"
/>
</template>
<!-- 自定义组件 -->
<template v-else-if="field.type === 'custom'">
<component
:is="field.component"
v-model="formData[field.field]"
v-bind="field.props"
@change="handleChange(field.field, $event)"
>
<template v-for="(slotContent, slotName) in field.slots" #[slotName]>
<component :is="slotContent" />
</template>
</component>
</template>
<!-- 其他类型可以在这里扩展 -->
</el-form-item>
</el-col>
</template>
</el-row>
<!-- 后置插槽 -->
<slot v-if="suffixSlot" name="suffix"></slot>
<!-- 操作按钮 -->
<el-form-item v-if="showSearch || showReset" class="form-actions">
<el-button
v-if="showSearch"
type="primary"
:loading="searchLoading"
@click="handleSearch"
>
{{ searchText }}
</el-button>
<el-button
v-if="showReset"
@click="handleReset"
>
{{ resetText }}
</el-button>
</el-form-item>
<!-- 额外插槽 -->
<slot v-if="extraSlot" name="extra"></slot>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, toRefs } from 'vue'
import type { FormInstance } from 'element-plus'
import RemoteSelect from './RemoteSelect.vue'
import RemoteMultiSelect from './RemoteMultiSelect.vue'
import type {
SearchFormProps,
SearchFormEmits,
FormField,
FormOption
} from '@/types/search-form'
const props = withDefaults(defineProps<SearchFormProps>(), {
labelWidth: '150px',
labelPosition: 'right',
inline: false,
size: 'default',
disabled: false,
showReset: false,
showSearch: false,
resetText: '重置',
searchText: '搜索',
gutter: 20,
rowClass: '',
formClass: '',
prefixSlot: false,
suffixSlot: false,
extraSlot: false
})
const emit = defineEmits<SearchFormEmits>()
// 表单引用
const formRef = ref<FormInstance>()
const searchLoading = ref(false)
// 表单数据
const formData = reactive<Record<string, any>>({})
// 初始化表单数据
const initFormData = () => {
// 设置默认值
props.fields.forEach(field => {
if (field.field in props.modelValue) {
formData[field.field] = props.modelValue[field.field]
} else if (field.defaultValue !== undefined) {
formData[field.field] = field.defaultValue
} else {
// 根据类型设置默认值
switch (field.type) {
case 'input':
case 'textarea':
case 'password':
formData[field.field] = ''
break
case 'select':
case 'radio':
formData[field.field] = ''
break
case 'multi-select':
case 'checkbox':
case 'remote-multi-select':
formData[field.field] = []
break
case 'switch':
formData[field.field] = false
break
case 'number':
formData[field.field] = null
break
case 'date':
case 'datetime':
formData[field.field] = ''
break
case 'daterange':
case 'datetimerange':
formData[field.field] = []
break
case 'cascader':
formData[field.field] = []
break
default:
formData[field.field] = null
}
}
})
}
// 计算可见的字段
const visibleFields = computed(() => {
return props.fields.filter(field => !field.hidden)
})
// 获取字段宽度
const getFieldWidth = (field: FormField) => {
if (field.width) {
return typeof field.width === 'number' ? `${field.width}px` : field.width
}
return '100%'
}
// 获取栅格跨度
const getColSpan = (field: FormField, type: 'span' | 'xs' | 'sm' | 'md' | 'lg' | 'xl') => {
const userColSpan = field.colSpan || 6 // 默认6(大屏4列)
switch (type) {
case 'xs': // 超小屏幕(手机)
return 24 // 1列
case 'sm': // 小屏幕(平板)
return userColSpan * 2 // 转换为栅格跨度
case 'md': // 中等屏幕
return userColSpan
case 'lg': // 大屏幕
return userColSpan
case 'xl': // 超大屏幕
return userColSpan
case 'span': // 默认
return userColSpan
}
}
// 获取选项列表
const getOptions = (field: FormField): FormOption[] => {
return field.options || []
}
// 获取选项标签
const getOptionLabel = (option: FormOption, field: FormField) => {
if (field.optionLabel && option[field.optionLabel]) {
return option[field.optionLabel]
}
return option.label
}
// 获取选项值
const getOptionValue = (option: FormOption, field: FormField) => {
if (field.optionValue && option[field.optionValue]) {
return option[field.optionValue]
}
return option.value
}
// 获取校验规则
const getRules = (field: FormField) => {
const rules = field.rules || []
// 自动添加必填校验
if (field.rules?.some(rule => rule.required)) {
return rules
}
// 如果有required属性但没有规则,自动创建
if (field.props?.required) {
return [
{
required: true,
message: `${field.label}不能为空`,
trigger: field.type.includes('select') ? 'change' : 'blur'
},
...rules
]
}
return rules
}
// 处理字段变化
const handleChange = (field: string, value: any) => {
emit('change', field, value)
emit('update:modelValue', { ...formData })
}
// 处理搜索
const handleSearch = async () => {
if (!formRef.value) return
try {
searchLoading.value = true
// 执行表单验证
const isValid = await formRef.value.validate()
if (!isValid) return
// 执行前置钩子
if (props.beforeSearch) {
const canProceed = await props.beforeSearch(formData)
if (!canProceed) return
}
// 触发搜索事件
emit('search', formData)
} catch (error) {
console.error('表单验证失败:', error)
} finally {
searchLoading.value = false
}
}
// 处理重置
const handleReset = async () => {
if (!formRef.value) return
// 执行前置钩子
if (props.beforeReset) {
const canProceed = await props.beforeReset()
if (!canProceed) return
}
// 重置表单
formRef.value.resetFields()
// 重置为默认值
initFormData()
// 触发重置事件
emit('reset', formData)
emit('update:modelValue', { ...formData })
}
// 监听外部modelValue变化
watch(() => props.modelValue, (newVal) => {
Object.keys(formData).forEach(key => {
if (key in newVal) {
formData[key] = newVal[key]
}
})
}, { deep: true })
// 监听字段配置变化
watch(() => props.fields, () => {
initFormData()
}, { deep: true })
// 暴露方法给父组件
defineExpose({
validate: () => formRef.value?.validate(),
resetFields: () => {
formRef.value?.resetFields()
initFormData()
},
clearValidate: () => formRef.value?.clearValidate(),
getFormData: () => ({ ...formData }),
getFormRef: () => formRef.value
})
// 初始化
initFormData()
</script>
<style scoped lang="scss">
.search-form-container {
width: 100%;
.form-actions {
margin-bottom: 0;
margin-top: 20px;
.el-button + .el-button {
margin-left: 10px;
}
}
}
// 统一所有表单项的宽度和高度
:deep(.el-form) {
&.el-form--label-top {
.el-form-item {
margin-bottom: 24px;
.el-form-item__label {
display: block;
text-align: left;
margin-bottom: 8px;
padding-bottom: 0;
line-height: 1.4;
font-weight: 500;
color: #606266;
font-size: 14px;
height: 20px;
}
.el-form-item__content {
margin-left: 0 !important;
height: 40px; // 固定内容高度
width: 100%; // 确保内容区域宽度100%
// 基础容器样式
> * {
display: block !important;
width: 100% !important;
box-sizing: border-box !important;
}
// 日期选择器 - 修复日期区间宽度
.el-date-editor {
&.el-range-editor {
// 日期区间选择器的特殊处理
height: 40px !important;
width: 100% !important;
line-height: 38px !important;
display: flex !important;
align-items: center !important;
padding: 0 !important;
// 修复内部flex布局
.el-range-input {
height: 38px !important;
line-height: 38px !important;
flex: 1 !important; // 让两个输入框平分剩余空间
min-width: 0 !important; // 防止内容溢出
padding: 0 8px !important;
font-size: 14px !important;
border: none !important;
background: transparent !important;
outline: none !important;
// 修复placeholder样式
&::placeholder {
color: var(--el-text-color-placeholder);
font-size: 14px;
}
}
// 分隔符
.el-range-separator {
height: 38px !important;
line-height: 38px !important;
padding: 0 4px !important;
font-size: 14px !important;
color: var(--el-text-color-placeholder);
flex: none !important;
width: auto !important;
min-width: 20px !important;
}
// 关闭图标
.el-range__close-icon {
height: 38px !important;
line-height: 38px !important;
flex: none !important;
width: 30px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
.el-icon {
font-size: 14px !important;
}
}
// 日历图标
.el-range__icon {
height: 38px !important;
line-height: 38px !important;
flex: none !important;
width: 30px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
.el-icon {
font-size: 14px !important;
}
}
// 悬停和聚焦状态
&:hover,
&:focus-within {
.el-range-input {
background: transparent !important;
}
}
// 禁用的日期区间
&.is-disabled {
.el-range-input {
color: var(--el-disabled-text-color) !important;
background: var(--el-disabled-bg-color) !important;
}
}
}
// 单个日期选择器
&:not(.el-range-editor) {
height: 40px;
width: 100% !important;
.el-input__wrapper {
height: 40px;
width: 100%;
padding: 1px 30px 1px 11px; // 为图标留出空间
box-sizing: border-box;
.el-input__inner {
height: 38px;
line-height: 38px;
width: 100%;
padding-right: 20px; // 为图标留出空间
}
}
}
// 日期时间选择器
&.el-date-editor--datetime {
.el-input__wrapper {
padding-right: 30px;
}
}
}
// 其他组件样式保持不变...
// 输入框
.el-input {
height: 40px;
width: 100%;
.el-input__wrapper {
height: 40px;
width: 100%;
padding: 1px 11px;
border-radius: 4px;
box-sizing: border-box;
.el-input__inner {
height: 38px;
line-height: 38px;
width: 100%;
}
}
}
// 下拉选择框(单选)
.el-select:not(.is-multiple) {
height: 40px;
width: 100%;
.el-select__wrapper {
height: 40px;
width: 100%;
padding: 1px 30px 1px 11px; // 为下拉箭头留出空间
border-radius: 4px;
box-sizing: border-box;
.el-select__placeholder,
.el-select__selected-item {
line-height: 38px;
width: 100%;
padding-right: 20px; // 为下拉箭头留出空间
}
// 下拉箭头
.el-select__suffix {
right: 8px;
}
}
}
// 下拉选择框(多选)
.el-select.is-multiple {
height: auto;
min-height: 40px;
width: 100%;
.el-select__wrapper {
min-height: 40px;
width: 100%;
padding: 3px 30px 3px 11px;
border-radius: 4px;
box-sizing: border-box;
.el-select__tags-wrapper {
min-height: 32px;
line-height: 32px;
width: 100%;
}
}
}
// 数字输入框
.el-input-number {
height: 40px;
width: 100% !important;
.el-input {
height: 40px;
width: 100%;
.el-input__wrapper {
height: 40px;
width: 100%;
padding: 1px 40px 1px 11px; // 左右都为按钮留出空间
box-sizing: border-box;
.el-input__inner {
height: 38px;
line-height: 38px;
width: 100%;
text-align: left;
}
}
}
}
// 确保远程搜索组件继承样式
.remote-select,
.remote-multi-select {
width: 100% !important;
.el-select {
width: 100% !important;
}
}
}
}
}
}
// 响应式调整
@media (max-width: 768px) {
:deep(.el-form) {
&.el-form--label-top {
.el-form-item {
margin-bottom: 20px;
.el-form-item__content {
height: 38px;
.el-date-editor {
&.el-range-editor {
height: 38px !important;
.el-range-input {
height: 36px !important;
line-height: 36px !important;
padding: 0 6px !important;
font-size: 13px !important;
}
.el-range-separator {
height: 36px !important;
line-height: 36px !important;
padding: 0 2px !important;
font-size: 13px !important;
}
.el-range__close-icon,
.el-range__icon {
height: 36px !important;
line-height: 36px !important;
width: 26px !important;
}
}
}
.el-input,
.el-select,
.el-input-number {
height: 38px;
.el-input__wrapper,
.el-select__wrapper {
height: 38px;
padding: 1px 8px;
.el-input__inner,
.el-select__placeholder,
.el-select__selected-item {
height: 36px;
line-height: 36px;
}
}
}
}
}
}
}
}
</style>
......@@ -490,8 +490,5 @@ onMounted(() => {
margin-bottom: 8px;
}
.pagination-container {
justify-content: center;
}
}
</style>
<!-- ExcelUploadPreview.vue -->
<template>
<div class="excel-upload-preview">
<!-- 文件上传 -->
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv"
:before-upload="handleBeforeUpload" @change="handleFileChange">
<el-button type="primary">选择 Excel 文件</el-button>
<span v-if="fileName" class="ml-2">已选择:{{ fileName }}</span>
</el-upload>
<!-- 解析消息 -->
<el-alert v-if="parseMessage" :title="parseMessage" :type="parsedValid ? 'success' : 'warning'"
style="margin-top: 12px" />
<!-- 错误信息 -->
<el-alert v-if="errorMessages.length" :closable="false" type="error" style="margin-top: 12px">
<template #default>
<div v-for="(msg, idx) in errorMessages" :key="idx">{{ msg }}</div>
</template>
</el-alert>
<!-- 表格 -->
<el-table v-if="editableData.length" :data="editableData" style="width: 100%; margin-top: 16px" border
max-height="400" size="small">
<!-- 动态列 -->
<el-table-column v-for="header in headers" :key="header" :prop="header" :label="getHeaderLabel(header)"
min-width="120">
<template #default="{ row, $index }">
<el-input v-if="editRowIndex === $index && editColumn === header" v-model="row[header]"
@blur="saveEdit" @keyup.enter="saveEdit" ref="currentInput" />
<span v-else @click="startEdit($index, header)">
{{ row[header] ?? '' }}
</span>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ $index }">
<el-button v-if="editRowIndex === $index" type="text" size="small" @click="cancelEdit">
取消
</el-button>
<el-button v-else type="text" size="small" @click="startEdit($index, headers[0])">
编辑
</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="deleteRow($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 提交按钮 -->
<el-button v-if="showConfirm && editableData.length" type="success" style="margin-top: 16px"
:loading="submitting" @click="handleSubmit">
确认提交(共 {{ editableData.length }} 条)
</el-button>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
const props = defineProps({
showConfirm: { type: Boolean, default: true },
beforeUpload: { type: Function, default: null },
headerRow: { type: Number, default: 0 },
checkStartRow: { type: Number, default: 3},
requiredFields: { type: String, default: '' },
useChineseHeader: { type: Boolean, default: true },
transformSubmitData: {
type: Function,
default: (rows) => rows
}
})
const emit = defineEmits(['submit', 'parsed'])
// refs
const uploadRef = ref(null)
const currentInput = ref(null)
const fileName = ref('')
const headers = ref([])
const chineseHeadersMap = ref({})
const editableData = ref([]) // ✅ 所有操作在此数组上进行
const editRowIndex = ref(-1)
const editColumn = ref(null)
const parseMessage = ref('')
const parsedValid = ref(true)
const errorMessages = ref([])
const submitting = ref(false)
// 获取表头显示文本
const getHeaderLabel = (header) => {
return props.useChineseHeader ? (chineseHeadersMap.value[header] || header) : header
}
// 上传前校验
const handleBeforeUpload = (file) => {
if (props.beforeUpload && !props.beforeUpload(file)) return false
if (!/\.(xlsx|xls|csv)$/i.test(file.name)) {
ElMessage.error('仅支持 Excel 或 CSV 文件')
return false
}
return true
}
// 解析文件
const handleFileChange = async (uploadFile) => {
const file = uploadFile.raw
if (!file) return
fileName.value = file.name
parseMessage.value = ''
parsedValid.value = true
errorMessages.value = []
chineseHeadersMap.value = {}
const formData = new FormData()
formData.append('file', file)
formData.append('headerRow', props.headerRow.toString())
formData.append('checkStartRow', props.checkStartRow.toString())
if (props.requiredFields.length > 0) {
formData.append('requiredFields', props.requiredFields)
}
try {
const res = await request.post('/oss/api/excel/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
if (res.code !== 200 || !res.data?.success) {
throw new Error(res.msg || '解析失败')
}
const parsed = res.data
parseMessage.value = parsed.message || ''
parsedValid.value = parsed.valid !== false
errorMessages.value = parsed.errorMessages || []
const rawData = parsed.data || []
headers.value = parsed.headers || []
if (rawData.length === 0) {
editableData.value = []
} else {
if (props.useChineseHeader) {
const firstRow = rawData[0]
const map = {}
headers.value.forEach(key => {
map[key] = firstRow[key] || key
})
chineseHeadersMap.value = map
editableData.value = rawData.slice(1).map(row => ({ ...row }))
} else {
editableData.value = rawData.map(row => ({ ...row }))
}
}
emit('parsed', editableData.value)
uploadRef.value.clearFiles()
} catch (err) {
console.error('Parse error:', err)
ElMessage.error('文件解析失败:' + (err.message || ''))
}
}
// 开始编辑
const startEdit = (index, column) => {
editRowIndex.value = index
editColumn.value = column
nextTick(() => {
currentInput.value?.focus()
})
}
// 保存编辑
const saveEdit = () => {
editRowIndex.value = -1
editColumn.value = null
}
// 取消编辑
const cancelEdit = () => {
// 无需回滚,因为我们直接操作 editableData(无原始快照)
// 如果需要“撤销到初始状态”,可保留 originalData
saveEdit()
}
// 删除行
const deleteRow = (index) => {
ElMessageBox.confirm('确定删除此行?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
editableData.value.splice(index, 1)
// 重置编辑状态(避免索引错乱)
if (editRowIndex.value === index) {
editRowIndex.value = -1
editColumn.value = null
} else if (editRowIndex.value > index) {
editRowIndex.value-- // 索引前移
}
}).catch(() => { })
}
// 提交最终数据
const handleSubmit = () => {
if (editableData.value.length === 0) {
ElMessage.warning('没有可提交的数据')
return
}
submitting.value = true
try {
// 调用 transformSubmitData 进行格式转换(如类型转换)
const finalData = props.transformSubmitData([...editableData.value])
emit('submit', finalData)
} catch (err) {
ElMessage.error('提交失败:' + (err.message || ''))
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.ml-2 {
margin-left: 8px;
}
</style>
\ No newline at end of file
// src/hooks/useRemoteSearch.ts
import { ref, reactive, onUnmounted } from 'vue'
import type { Ref } from 'vue'
export interface SearchOption {
label: string
value: string | number
[key: string]: any
}
export interface RemoteSearchConfig {
type: string
apiMethod: (params: any) => Promise<any>
formatResult?: (data: any[]) => SearchOption[]
cacheKey?: string
debounceDelay?: number
defaultOptions?: SearchOption[]
}
export interface RemoteSearchState {
loading: boolean
options: SearchOption[]
cache: Map<string, SearchOption[]>
}
export function useRemoteSearch(config: RemoteSearchConfig) {
const {
type,
apiMethod,
formatResult = defaultFormatResult,
cacheKey = type,
debounceDelay = 500,
defaultOptions = []
} = config
// 状态
const state = reactive<RemoteSearchState>({
loading: false,
options: [...defaultOptions],
cache: new Map()
})
// 防抖相关
let debounceTimer: NodeJS.Timeout | null = null
let lastQuery = ''
// 默认格式化函数
function defaultFormatResult(data: any[]): SearchOption[] {
return data.map(item => ({
label: item.name || item.label || item.text || String(item.value),
value: item.id || item.value || item.key,
...item
}))
}
// 从缓存获取
function getFromCache(query: string): SearchOption[] | null {
const cacheKeyWithQuery = `${cacheKey}:${query || 'all'}`
return state.cache.get(cacheKeyWithQuery) || null
}
// 保存到缓存
function saveToCache(query: string, data: SearchOption[]) {
const cacheKeyWithQuery = `${cacheKey}:${query || 'all'}`
state.cache.set(cacheKeyWithQuery, data)
// 限制缓存大小(最多100条记录)
if (state.cache.size > 100) {
const firstKey = state.cache.keys().next().value
state.cache.delete(firstKey)
}
}
// 执行搜索
async function performSearch(query: string): Promise<SearchOption[]> {
// 检查缓存
const cached = getFromCache(query)
if (cached && cached.length > 0) {
return cached
}
state.loading = true
try {
// 调用API
const params = query ? { keyword: query, pageSize: 50 } : { pageSize: 100 }
const response = await apiMethod(params)
// 格式化结果
const result = formatResult(response.data || response.list || response.records || [])
// 保存到缓存
saveToCache(query, result)
return result
} catch (error) {
console.error(`远程搜索失败 [${type}]:`, error)
throw error
} finally {
state.loading = false
}
}
// 搜索方法(带防抖)
async function search(query: string = ''): Promise<SearchOption[]> {
// 清除之前的定时器
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
// 如果查询相同,直接返回当前选项
if (query === lastQuery) {
return state.options
}
lastQuery = query
// 如果是空查询且有默认选项,直接返回
if (!query && defaultOptions.length > 0) {
state.options = defaultOptions
return defaultOptions
}
return new Promise((resolve) => {
debounceTimer = setTimeout(async () => {
try {
const result = await performSearch(query)
state.options = result
resolve(result)
} catch (error) {
state.options = []
resolve([])
}
}, debounceDelay)
})
}
// 预加载数据(初始化时调用)
async function preload(): Promise<void> {
if (state.options.length === 0) {
await search('')
}
}
// 清空缓存
function clearCache(): void {
state.cache.clear()
state.options = [...defaultOptions]
}
// 设置默认选项
function setDefaultOptions(options: SearchOption[]): void {
defaultOptions.length = 0
defaultOptions.push(...options)
if (state.options.length === 0) {
state.options = [...options]
}
}
// 组件卸载时清理
onUnmounted(() => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
})
return {
state,
search,
preload,
clearCache,
setDefaultOptions,
loading: () => state.loading,
options: () => state.options
}
}
// 创建多搜索实例的管理器
export function useRemoteSearchManager() {
const instances = new Map<string, ReturnType<typeof useRemoteSearch>>()
function getInstance(config: RemoteSearchConfig) {
const { type } = config
if (!instances.has(type)) {
instances.set(type, useRemoteSearch(config))
}
return instances.get(type)!
}
function clearAllCache() {
instances.forEach(instance => {
instance.clearCache()
})
}
return {
getInstance,
clearAllCache
}
}
\ No newline at end of file
......@@ -52,24 +52,3 @@ export function useDictLists(typeLists) {
})
})()
}
\ No newline at end of file
// /**
// * 获取字典数据
// */
// export function useDict(...args) {
// const res = ref({})
// return (() => {
// args.forEach((dictType, index) => {
// res.value[dictType] = []
// const dicts = useDictStore().getDict(dictType)
// if (dicts) {
// res.value[dictType] = dicts
// } else {
// getDicts(dictType).then(resp => {
// res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
// useDictStore().setDict(dictType, res.value[dictType])
// })
// }
// })
// return toRefs(res.value)
// })()
// }
......@@ -210,46 +210,79 @@ export function getTime(type) {
}
/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
* 修复后的防抖函数(支持立即执行、手动取消、返回值)
* @param {Function} func 要防抖的函数
* @param {number} [wait=300] 延迟时间(毫秒)
* @param {boolean} [immediate=false] 是否立即执行
* @returns {Function} 包装后的防抖函数(包含cancel方法)
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
export function debounce(func, wait = 300, immediate = false) {
let timeout = null; // 定时器标识
let args = null; // 缓存函数参数
let context = null; // 缓存函数上下文
let timestamp = 0; // 最后一次触发的时间戳
let result = undefined; // 函数执行结果
// 延迟执行的核心逻辑
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 计算最后一次触发与现在的时间差
const last = Date.now() - timestamp;
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
// 如果时间差小于wait且大于0,说明还在防抖窗口期,重新设置定时器
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
timeout = setTimeout(later, wait - last);
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
// 窗口期结束,清空定时器
timeout = null;
// 非立即执行的情况,在此处执行原函数
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
result = func.apply(context, args);
// 执行后清空上下文和参数,避免内存泄漏
if (!timeout) context = args = null;
}
}
};
// 包装后的防抖函数
const debounced = function(...params) {
// 实时捕获当前的上下文和参数
context = this;
args = params;
// 记录最后一次触发的时间戳
timestamp = Date.now();
// 判断是否需要立即执行
const callNow = immediate && !timeout;
// 清除旧的定时器,避免多次执行
if (timeout) clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(later, wait);
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
// 立即执行的情况,直接调用原函数
if (callNow) {
result = func.apply(context, args)
context = args = null
result = func.apply(context, args);
// 清空上下文和参数
context = args = null;
}
return result
}
// 返回函数执行结果
return result;
};
// 手动取消防抖的方法
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
context = args = null;
timestamp = 0;
};
return debounced;
}
/**
* This is just a simple version of deep copy
* Has a lot of edge cases bug
......@@ -390,6 +423,6 @@ export function isNumberStr(str) {
// 数字千分位格式化,保留2位小数
export function numberWithCommas(x, fixed = 2) {
if (!x) return '0.00'
return x.toFixed(fixed).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
......@@ -149,6 +149,7 @@ service.interceptors.response.use(
// 通用下载方法
export function download(url, params, filename, config) {
console.log(url, params, filename, config)
downloadLoadingInstance = ElLoading.service({
text: '正在下载数据,请稍候',
background: 'rgba(0, 0, 0, 0.7)'
......@@ -160,7 +161,7 @@ export function download(url, params, filename, config) {
return tansParams(params)
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/json' },
responseType: 'blob',
...config
})
......
// utils/safeDownload.js
import { ElMessage } from 'element-plus'
/**
* 安全下载函数:适用于后端返回 Blob 文件流 或 JSON 错误 的场景
* @param {Blob} blobData - 接口返回的响应数据(必须是 responseType: 'blob')
* @param {string} defaultFilename - 默认文件名(如 'data.xlsx')
* @param {string} [mimeType] - MIME 类型(用于兜底创建 Blob)
*/
export async function safeDownload(blobData, defaultFilename, mimeType = 'application/octet-stream') {
if (!(blobData instanceof Blob)) {
ElMessage.error('无效的下载数据')
return
}
try {
// 👇 关键:先 peek 前 100 字节,判断是否是 JSON 错误
const firstChunk = await blobData.slice(0, 100).text()
const trimmed = firstChunk.trim()
// 如果看起来像 JSON(以 { 或 [ 开头),尝试解析
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
const fullText = await blobData.text()
let parsed
try {
parsed = JSON.parse(fullText)
} catch (e) {
// 解析失败,当作正常文件(比如内容就是纯文本)
parsed = null
}
// 如果解析成功,且包含错误字段(根据你后端约定)
if (parsed && (parsed.code !== undefined || parsed.msg || parsed.message)) {
const errorMsg = parsed.msg || parsed.message || '导出失败'
ElMessage.error(errorMsg)
return
}
}
// ✅ 是合法文件流,执行下载(使用你已验证的逻辑)
const url = window.URL.createObjectURL(blobData)
const link = document.createElement('a')
link.href = url
link.download = defaultFilename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('safeDownload error:', error)
ElMessage.error('下载过程中发生错误')
}
}
\ No newline at end of file
// dictUtils.js
import request from '@/utils/request'
// 全局缓存:key 为 dictType,value 为 { label: value } 映射对象
const dictCache = new Map()
// 批量加载字典(支持多个 type)
export async function loadDicts(typeList) {
// 过滤已缓存的类型,避免重复请求
const needLoadTypes = typeList.filter(type => !dictCache.has(type))
if (needLoadTypes.length === 0) return
try {
const res = await request({
url: '/user/api/sysDict/type/list', // 替换为你的实际接口地址
method: 'POST',
data: {
typeList: needLoadTypes
}
})
// 替换 loadDicts 中的缓存部分
if (res.code === 200 && Array.isArray(res.data)) {
for (const dict of res.data) {
const { dictType, dictItemList = [] } = dict
if (!dictType) continue
// 缓存完整列表(并可选排序 + 过滤)
const validItems = dictItemList
.filter(item => item.status === 1) // 只取启用的
.sort((a, b) => a.orderNum - b.orderNum) // 按序号排序
dictCache.set(dictType, validItems) // 👈 缓存完整 item 列表
}
}
} catch (error) {
console.error('字典加载失败:', error)
throw error
}
}
// 可选:提供清除缓存方法(用于权限切换等场景)
export function clearDictCache() {
dictCache.clear()
}
export function getDictOptions(dictType) {
const items = dictCache.get(dictType) || []
// console.log('getDictOptions',items)
return items.map(item => ({
value: item.itemValue,
label: item.itemLabel
// 如果需要,还可以加其他字段:disabled, key 等
}))
}
// 同时更新 getDictLabel
export function getDictLabel(dictType, value) {
const items = dictCache.get(dictType) || []
const item = items.find(i => i.itemValue === value)
return item ? item.itemLabel : value
}
\ No newline at end of file
<template>
<div class="financial-billing-page app-container">
<!-- 查询区域 -->
<el-card class="search-card">
<el-form :model="queryParams" label-width="80px">
<div class='container'>
<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>
<!-- 统计信息卡片 -->
<div class="statistics-container" v-if="statisticsData.totalInAmount > 0">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="保单号">
<el-input v-model="queryParams.policyNo" placeholder="请输入保单号" clearable />
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">入账金额</div>
<div class="card-value">{{ statisticsData.totalInAmount }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="出账日期">
<el-date-picker
v-model="queryParams.billingDate"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">总保单数</div>
<div class="card-value">{{ statisticsData.totalPolicyCount }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">总保费(HKD)</div>
<div class="card-value">{{ statisticsData.totalPremium }}</div>
</div>
</el-card>
</el-col>
</el-row>
</el-form>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">待出账金额</div>
<div class="card-value">{{ statisticsData.pendingOutAmount }}</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card>
<el-row :gutter="20">
<el-text class="mx-1" type="danger" style="margin-right: 20px"
>勾选出账记录,点击“下载选中项”按钮,下载导入模版</el-text
>
</el-row>
<div class="button-row" style="display: flex; gap: 1rem; justify-content: end">
<el-button type="primary" :disabled="selectedRows.length === 0" @click="downloadSelected">
下载选中项
</el-button>
<FileUpload
:fileType="['xlsx', 'xls']"
:action="'/csf/api/fortune/upload/excel'"
:uploadBtnText="'批量导入'"
:isShowTip="false"
@uploadEnd="getUploadFileFunc"
:responseType="'onlyStatus'"
/>
<el-button
type="success"
:disabled="selectedRows.length === 0"
@click="generateBillingList"
>
生成出账清单
</el-button>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">可出账金额</div>
<div class="card-value">{{ statisticsData.availableOutAmount }}</div>
</div>
<div class="totalData" v-if="getAllSelectedRows().length > 0 || isSearch">
<span v-for="(item, index) in statisticList">
<span :style="{ marginLeft: index == 0 ? ' 0' : '20px' }">{{ item.name }}</span>
<span>{{ item.format ? formatCurrency(item.value) : item.value }}</span>
</span>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">差额(估)</div>
<div class="card-value">{{ formatCurrency(statisticsData.differenceAmount) }}</div>
</div>
<el-table
:data="tableData"
@selection-change="handleSelectionChange"
v-loading="loading"
row-key="fortuneBizId"
:reserve-selection="true"
ref="tableRef"
>
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="policyNo" label="保单号" min-width="120" />
<el-table-column prop="commissionAmount" label="本次已来佣金额" width="150">
</el-card>
</el-col>
</el-row>
</div>
<el-table :data="tableData" @selection-change="handleSelectionChange" height="400" border highlight-current-row
style="width: 100%" v-loading="loading">
<el-table-column type="selection" width="40" />
<el-table-column prop="fortuneBizType" label="应付单类型" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.commissionAmount) }}
{{ getFortuneBizTypeLabel(row.fortuneBizType) }}
</template>
</el-table-column>
<el-table-column prop="fortunePeriod" label="出账期数" width="100" />
<el-table-column prop="fortuneTotalPeriod" label="预计总期数" width="100" />
<el-table-column prop="fortuneName" label="出账项目" min-width="120" />
<el-table-column prop="broker" label="转介人" min-width="100" />
<el-table-column prop="team" label="所属团队" min-width="120" />
<el-table-column prop="amount" label="出账金额" width="120">
<el-table-column prop="policyNo" label="保单号" width="120" sortable />
<el-table-column prop="insuranceCompany" label="保险公司" width="120" sortable />
<el-table-column prop="commissionPaidAmount" label="累积已入账金额" width="120" sortable />
<el-table-column prop="commissionPaidRatio" label="累积已入账比例" width="120" sortable />
<el-table-column prop="fortuneName" label="出账项目" width="130" sortable />
<el-table-column prop="fortunePeriod" label="出账期数" width="130" sortable />
<el-table-column prop="fortuneTotalPeriod" label="总期数" width="120" sortable />
<el-table-column prop="broker" label="转介人" width="130" sortable />
<el-table-column prop="team" label="所属团队" width="120" sortable />
<el-table-column prop="amount" label="应出账金额" width="140" sortable />
<el-table-column prop="currency" label="出账币种" width="130" sortable />
<el-table-column prop="fortunePaidAmount" label="已出账金额" width="120" sortable />
<el-table-column prop="fortuneUnpaidAmount" label="剩余出账金额" width="120" sortable />
<el-table-column prop="currentPaymentAmount" label="本期出账金额" width="120" sortable />
<el-table-column prop="fortuneUnpaidRatio" label="剩余出账比例" width="120" sortable />
<el-table-column prop="status" label="出账状态" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
{{ getDictLabel('csf_fortune_status', row.status) }}
</template>
</el-table-column>
<el-table-column prop="currency" label="出账币种" width="100" />
<el-table-column prop="status" label="出账状态" width="100">
<el-table-column prop="premium" label="期交保费" width="120" sortable />
<el-table-column prop="payoutDate" label="出账日(实)" width="120" sortable />
<el-table-column prop="remark" label="备注" width="120" sortable />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ convertStatusToDict(row.status) }}
</el-tag>
<el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-icon>
<MoreFilled />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<!-- <el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">修改</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
<el-button size="small" type="info" @click="handleView(row)">查看</el-button>
<el-menu @select="handleSelect($event, row)" popper-class="custom-menu">
<el-menu-item :index="item.value" v-for="item in dropdownItems" :key="item.value">{{
item.label
}}</el-menu-item>
</el-menu>
</el-popover>
</template>
</el-table-column> -->
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList"
/>
</el-card>
<!-- 新建/编辑对话框 -->
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form :model="formData" label-width="100px">
<el-form-item label="保单号" required>
<el-input v-model="formData.policyNo" />
</el-form-item>
<el-form-item label="对账公司" required>
<el-select v-model="formData.company">
<el-option
v-for="item in companyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="出账期数" required>
<el-input-number v-model="formData.billingPeriod" :min="1" />
</el-form-item>
<el-form-item label="预计总期数" required>
<el-input-number v-model="formData.totalPeriods" :min="1" />
</el-form-item>
<el-form-item label="出账项目" required>
<el-input v-model="formData.billingItem" />
</el-form-item>
<el-form-item label="转介人">
<el-input v-model="formData.referrer" />
</el-form-item>
<el-form-item label="所属团队">
<el-input v-model="formData.team" />
</el-form-item>
<el-form-item label="出账金额" required>
<el-input-number v-model="formData.amount" :min="0" :precision="2" />
</el-form-item>
<el-form-item label="出账币种" required>
<el-select v-model="formData.currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
<el-option label="港币" value="HKD" />
</el-select>
</el-form-item>
<el-form-item label="出账状态" required>
<el-select v-model="formData.status">
<el-option label="待出账" value="pending" />
<el-option label="已出账" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确认</el-button>
<!-- 表格操作菜单 -->
<div class="tableOptionContainer">
<el-button type="primary" :icon="Select" :disabled="updatePayRollStatusDisable"
@click="downloadPolicyFortuneAccountapi">完成检核</el-button>
</div>
</template>
</el-dialog>
</CommonPage>
<!-- 新增出账检核页面-->
<CommonDialog dialogTitle='新增出账检核' dialogWidth='80%' :openDialog='addCheckRecordFormDialogFlag' :showAction='true'
:showClose='true' @close='addCheckRecordFormDialogFlag = false' @confirm='addCheckRecordaddBatchapi'>
<SearchForm ref="addCheckRecordFormRef" :config="addCheckRecordConfig" v-model="addCheckRecordFormModel" />
</CommonDialog>
<!-- 导入出账检核页面 -->
<CommonDialog dialogTitle='新增出账检核' dialogWidth='80%' :openDialog='importCheckRecordFlag' :showAction='true'
:showClose='true' @close='importCheckRecordFlag = false'>
<FileUploadPreview :header-row="0" :required-fields="'amount,exchangeRate'" :use-chinese-header="true"
:transform-submit-data="transformToBackend" @submit="onSubmit" />
</CommonDialog>
<!-- 设置本期出账金额 -->
<CommonDialog dialogTitle='设置本期出账金额' dialogWidth='80%' :openDialog='setPayoutAmountDialogFlag' :showAction='true'
:showClose='true' @close='setPayoutAmountDialogFlag = false' @confirm='updatePayoutAmountapi'>
<SearchForm ref="setPayoutAmountFormRef" :config="setPayoutAmountConfig" v-model="setPayoutAmountFormModel" />
</CommonDialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 引入API
import {
getPolicyFortuneList,
downloadPolicyFortune,
downloadPolicyFortuneAccount,
billStatistics
} from '@/api/financial/commission'
// 添加表格引用
const tableRef = ref()
// 存储所有选中的行数据(用于跨页保持选择)
const allSelectedRows = ref(new Map())
const isSearch = ref(false)
const statisticList = ref([
{ name: '总出账金额', value: '0', key: 'totalOutAmount', format: true },
{ name: '总入账金额', value: '0', key: 'totalInAmount', format: true },
{ name: '总保单数', value: '0', key: 'totalPolicyCount', format: false }
])
// 查询参数
const queryParams = reactive({
policyNo: '',
company: '',
billingDate: [],
pageNum: 1,
pageSize: 100
// pageSize: 10
})
// 表格数据
const tableData = ref([])
const total = ref(0)
import { ref, reactive } from 'vue'
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
import { ElMessage } from 'element-plus'
import { formatCurrency } from '@/utils/number'
// 接口
import { getPolicyFortuneList, addCheckRecordaddBatch, updatePayoutAmount, downloadPolicyFortuneAccount } from '@/api/financial/commission'
import useUserStore from '@/store/modules/user'
import { loadDicts, getDictLabel } from '@/utils/useDict'
const userStore = useUserStore()
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
const selectedRows = ref([])
// 对话框相关
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formData = reactive({
policyNo: '',
company: '',
billingPeriod: 1,
totalPeriods: 1,
billingItem: '',
referrer: '',
team: '',
amount: 0,
currency: 'CNY',
status: 'pending',
remark: ''
})
// 设置当前页的选中状态
const setCurrentPageSelection = () => {
if (!tableRef.value) return
// 清除当前页的选择
// tableRef.value.clearSelection()
// 设置当前页应该选中的行
tableData.value.forEach(row => {
if (allSelectedRows.value.has(row.fortuneBizId)) {
tableRef.value.toggleRowSelection(row, true)
const selectedRow = ref(null)
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'input',
prop: 'policyNo',
label: '保单号'
}, {
type: 'select',
prop: 'statusList',
label: '出账状态',
multiple: true,
dictType: 'csf_fortune_status'
}, {
type: 'select',
prop: 'insuranceCompanyBizIdList',
label: '保险公司',
api: '/insurance/base/api/insuranceCompany/page',
keywordField: 'queryContent',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入保险公司名称搜索',
debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'insuranceCompanyBizId',
labelKey: 'abbreviation',
transform: (res) => {
console.log(res)
return res?.data.records || []
}
})
}
// 获取所有选中的行(包含跨页选择的)
const getAllSelectedRows = () => {
return Array.from(allSelectedRows.value.values())
}
// 更新全局选中状态
const updateAllSelectedRows = () => {
// 获取当前页的所有行的 key(fortuneBizId)
const currentPageKeys = tableData.value.map(row => row.fortuneBizId)
// 从全局选中中移除当前页不再存在的行
// for (const key of allSelectedRows.value.keys()) {
// if (!currentPageKeys.includes(key)) {
// allSelectedRows.value.delete(key)
// }
// }
// 更新当前页的选中状态
tableData.value.forEach(row => {
const isSelected = selectedRows.value.some(
selectedRow => selectedRow.fortuneBizId === row.fortuneBizId
)
if (isSelected) {
allSelectedRows.value.set(row.fortuneBizId, row)
} else {
allSelectedRows.value.delete(row.fortuneBizId)
}, {
type: 'select',
prop: 'productLaunchBizIdList',
label: '产品计划',
api: '/product/api/relProjectProductLaunch/parameter/page',
keywordField: 'productName',
requestParams: {
tenantBizId: userStore.projectInfo.tenantBizId || '',
projectBizId: userStore.projectInfo.projectBizId || '', fieldBizId: 'field_olk1qZe81qHHKXbw', fieldValueBizId: 'field_value_uOfJH5ucA2YwJpbn', pageNo: 1, pageSize: 20
},
placeholder: '输入产品计划名称搜索',
debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'productLaunchBizId',
labelKey: 'productName',
transform: (res) => {
return res?.data.records || []
}
})
// 当勾选项发生变化的时候,拿到最新的统计数据
getStatistics()
}, {
type: 'daterange',
prop: 'payoutDate',
label: '出账日(估)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
// {
// type: 'select',
// prop: 'status',
// label: '入账状态',
// multiple: true,
// dictType: 'csf_expected_commission_status'
// },
])
// 表格操作菜单
const dropdownItems = [
{ label: '设置本期出账金额', value: 'setPayRoll' },
// { label: '更新', value: 'editRecord' },
// { label: '查看记录', value: 'viewRecord' }
]
// 应收单类型
const fortuneBizTypeOptions = [
{ value: 'R', label: '关联保单应付单' },
{ value: 'U', label: '非关联保单应付单' }
]
// 应付单类型通过value转成label
const getFortuneBizTypeLabel = (value) => {
const item = fortuneBizTypeOptions.find(item => item.value === value)
return item?.label || ''
}
const getStatistics = async () => {
try {
let fortuneIdList = getAllSelectedRows().map(item => item.id)
if (fortuneIdList.length > 0) {
// 调用接口获取来佣列表
const res = await billStatistics({
fortuneIdList: fortuneIdList
})
if (res.data) {
statisticList.value.forEach(item => {
item.value = res.data[item.key]
})
// 新增出账检核
const addCheckRecordFormModel = ref({})
const addCheckRecordFormRef = ref(null)
const addCheckRecordFormDialogFlag = ref(false)
const addCheckRecordConfig = [
{
type: 'select',
prop: 'fortuneBizType',
label: '应付单类型',
options: fortuneBizTypeOptions
}, {
type: 'select',
prop: 'status',
label: '出账状态',
dictType: 'csf_expected_fortune_status'
}, {
type: 'input',
prop: 'policyNo',
label: '关联保单号',
visible: (formData) => formData.fortuneBizType === 'R',
}, {
type: 'input',
prop: 'fortunePeriod',
label: '佣金期数',
inputType: 'decimal',
visible: (formData) => formData.fortuneBizType === 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'input',
prop: 'fortuneTotalPeriod',
label: '总期数',
inputType: 'decimal',
visible: (formData) => formData.fortuneBizType === 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'date',
prop: 'actualPayoutDate',
label: '出账日(实)',
placeholder: '请选择',
maxDate: 'today'
}, {
type: 'input',
prop: 'amount',
label: '出账金额',
inputType: 'decimal',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'select',
prop: 'currency',
label: '出账币种',
dictType: 'bx_currency_type'
}, {
type: 'select',
prop: 'fortuneType',
label: '出账项目',
dictType: 'csf_fortune_type'
}, {
type: 'select',
prop: 'brokerBizId',
label: '转介人',
api: '/insurance/base/api/userSaleExpand/page',
keywordField: 'realName',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入转介人名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'userSaleBizId',
labelKey: 'realName',
onChangeExtraFields: {
broker: 'realName',// 自动同步 raw.name 到 reconciliationCompany
reconciliationCompanyCode: 'code'
},
transform: (res) => {
return res?.data.records || []
}
} else {
isSearch.value = false
}
} catch (error) {
console.error('获取数据失败:', error)
ElMessage.error('统计失败')
]
// 设置本期出账金额
const setPayoutAmountFormModel = ref({})
const setPayoutAmountFormRef = ref(null)
const setPayoutAmountDialogFlag = ref(false)
const setPayoutAmountConfig = [
{
type: 'input',
prop: 'currentPaymentAmount',
label: '出账金额',
inputType: 'decimal',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'select',
prop: 'currency',
label: '出账币种',
dictType: 'bx_currency_type'
}, {
type: 'select',
prop: 'status',
label: '出账状态',
dictType: 'csf_expected_fortune_status'
}
]
// 统计信息
const statisticsData = ref({})
// 弹窗相关
const dialogFlag = ref(false)
// 按钮事件处理
const handleAdd = () => {
addCheckRecordFormDialogFlag.value = true
}
// 清空所有选择
const clearAllSelection = () => {
allSelectedRows.value.clear()
selectedRows.value = []
if (tableRef.value) {
tableRef.value.clearSelection()
}
const handleImport = () => {
importCheckRecordFlag.value = true
}
// 下载选中项
const downloadSelected = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择要下载的项')
return
}
try {
// API调用
const response = await downloadPolicyFortune({
fortuneBizIdList: selectedRows.value.map(row => row.fortuneBizId)
})
if (response) {
ElMessage.success('下载成功')
// 触发文件下载
// 处理下载响应
const blob =
response instanceof Blob
? response
: new Blob([response], { type: 'application/vnd.ms-excel;charset=utf-8' })
// 创建Blob对象,指定正确的MIME类型
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 不需要指定文件名,浏览器会自动使用默认文件名
link.download = `出账管理_${new Date().getTime()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} else {
ElMessage.error('下载失败')
}
} catch (error) {
ElMessage.error('下载失败')
}
const handleExport = () => {
ElMessage.info('点击导出按钮')
}
const getUploadFileFunc = data => {
console.log(data)
if (data === 200) {
ElMessage.success('上传成功')
getList()
} else {
ElMessage.error('上传失败')
}
const handleReset = () => {
// 重置搜索表单
searchFormRef.value.resetForm()
console.log('表单已重置')
}
// 获取数据列表
const getList = async () => {
const handleQuery = async () => {
const params = searchFormRef.value.getFormData()
loadTableData(params)
}
const visibleDefaultButtons = ref(['add', 'import', 'export', 'reset', 'query'])
// 按钮配置
const operationBtnList = ref([
{
key: 'add',
direction: 'left',
label: '新增出账',
click: handleAdd
}, {
key: 'import',
direction: 'left',
label: '导入出账',
click: handleImport
},
{
key: 'export',
direction: 'right',
click: handleExport
},
{
key: 'reset',
direction: 'right',
click: handleReset
},
{
key: 'query',
direction: 'right',
click: handleQuery
}
])
// 加载表格数据
const loadTableData = async (searchParams = {}) => {
loading.value = true
try {
// API调用
const response = await getPolicyFortuneList(queryParams)
if (response.data.page) {
tableData.value = response.data.page.records
total.value = response.data.page.total
loading.value = false
}
if (response.data.statisticsVO && isSearch.value) {
statisticList.value.forEach(item => {
item.value = response.data.statisticsVO[item.key]
})
const params = {
pageNo: currentPage.value,
pageSize: pageSize.value,
...searchParams,
payoutDateStart: searchParams.payoutDate?.[0] || undefined,
payoutDateEnd: searchParams.payoutDate?.[1] || undefined,
payoutDate: undefined
}
// 数据加载完成后,设置当前页的选中状态
nextTick(() => {
setCurrentPageSelection()
})
const res = await getPolicyFortuneList(params)
tableData.value = res.data.page.records || []
pageTotal.value = res.data.page.total || 0
pageSize.value = res.data.page.size || 0
// 统计信息
statisticsData.value = res.data.statisticsVO || {}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
tableData.value = []
console.log('获取列表失败', error)
// ElMessage.error('获取数据失败')
}
}
// 查询
const handleQuery = () => {
queryParams.pageNum = 1
Object.values(queryParams).some(value => {
if (Array.isArray(value) && value.length > 0) {
isSearch.value = true
}
if (value !== '' && value != null) {
isSearch.value = true
}
})
clearAllSelection()
getList()
loadTableData()
// 分页事件
const handleSizeChange = (val) => {
pageSize.value = val
loadTableData()
}
// 重置查询
const resetQuery = () => {
Object.assign(queryParams, {
policyNo: '',
company: '',
billingDate: [],
pageNum: 1,
pageSize: 100
})
isSearch.value = false
// 清空选择
clearAllSelection()
getList()
}
// 选择行变化
const handleSelectionChange = selection => {
selectedRows.value = selection
updateAllSelectedRows()
const handleCurrentChange = (val) => {
currentPage.value = val
loadTableData()
}
// 表格数据
const tableData = ref([])
// 新建出账记录
const handleCreate = () => {
dialogTitle.value = '新建出账记录'
Object.assign(formData, {
policyNo: '',
company: '',
billingPeriod: 1,
totalPeriods: 1,
billingItem: '',
referrer: '',
team: '',
amount: 0,
currency: 'CNY',
status: 'pending',
remark: ''
})
dialogVisible.value = true
}
const handleSelect = (e, row) => {
console.log('选中行:', e, row)
selectedRow.value = row
if (e === 'setPayRoll') {
setPayoutAmountDialogFlag.value = true
}
// 编辑出账记录
const handleEdit = row => {
dialogTitle.value = '编辑出账记录'
Object.assign(formData, { ...row })
dialogVisible.value = true
}
// 删除出账记录
const handleDelete = async row => {
const addCheckRecordaddBatchapi = async (data) => {
const formData = addCheckRecordFormRef.value.getFormData()
console.log('新增出账检核记录:', formData)
const params = [
{ ...formData }
]
try {
await ElMessageBox.confirm('确认删除这条出账记录吗?', '提示', {
type: 'warning'
})
// await api.deleteBilling(row.id)
ElMessage.success('删除成功')
getList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
const res = await addCheckRecordaddBatch(params)
if (res.code === 200) {
ElMessage.success('新增出账检核记录成功')
addCheckRecordFormDialogFlag.value = false
addCheckRecordFormRef.value.resetForm()
loadTableData()
} else {
ElMessage.error(res.msg || '新增出账检核记录失败')
}
} catch (error) {
console.error('新增出账检核记录失败:', error)
ElMessage.error(error.message || '新增出账检核记录失败')
}
}
// 查看详情
const handleView = row => {
// 这里可以跳转到详情页面或者打开详情对话框
ElMessage.info(`查看保单号:${row.policyNo}`)
}
const generateBillingList = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要生成清单的记录')
return
// 设置本期出账金额
const updatePayoutAmountapi = async (data) => {
console.log('设置本期出账金额:', data)
const params = {
...setPayoutAmountFormRef.value.getFormData(),
fortuneBizId: selectedRow.value.fortuneBizId
}
ElMessageBox.confirm(`确认生成 ${selectedRows.value.length} 条记录的出账清单吗?`, '提示', {
type: 'warning'
})
.then(async () => {
console.log('开始生成出账清单')
const response = await downloadPolicyFortuneAccount({
fortuneBizIdList: selectedRows.value.map(item => item.fortuneBizId)
})
console.log(response)
// 调用下载API
if (response) {
// 处理下载响应
const blob =
response instanceof Blob
? response
: new Blob([response], { type: 'application/vnd.ms-excel;charset=utf-8' })
// 创建Blob对象,指定正确的MIME类型
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 不需要指定文件名,浏览器会自动使用默认文件名
link.download = `出账清单_${new Date().getTime()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
try {
const res = await updatePayoutAmount(params)
if (res.code === 200) {
ElMessage.success('设置本期出账金额成功')
loadTableData()
} else {
ElMessage.error('下载失败')
ElMessage.error(res.msg || '设置本期出账金额失败')
}
selectedRows.value = []
})
.catch(error => {
if (error !== 'cancel') {
console.log('生成出账清单出错:', error)
} catch (error) {
console.error('设置本期出账金额失败:', error)
ElMessage.error(error.message || '设置本期出账金额失败')
}
})
}
const updatePayRollStatusDisable = ref(true)
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
console.log('全选:', val)
// 完成检核按钮是否禁用
updatePayRollStatusDisable.value = val.length === 0
}
// 提交表单
const submitForm = async () => {
const downloadPolicyFortuneAccountapi = async (data) => {
console.log('生成出账清单:', data)
try {
// 表单验证
if (!formData.policyNo || !formData.company || !formData.billingItem) {
ElMessage.warning('请填写必填字段')
return
const res = await downloadPolicyFortuneAccount({
fortuneBizIdList: multipleSelection.value.map(item => item.fortuneBizId)
})
if (res.code === 200) {
ElMessage.success('完成检核,等待关账')
loadTableData()
}
// 调用保存API
// if (formData.id) {
// await api.updateBilling(formData)
// } else {
// await api.createBilling(formData)
// }
ElMessage.success('保存成功')
dialogVisible.value = false
getList()
} catch (error) {
ElMessage.error('保存失败')
console.error('检核失败:', error)
ElMessage.error(error.response?.data?.msg || '检核失败')
}
}
// 格式化金额
const formatCurrency = amount => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount)
import FileUploadPreview from '@/components/fileUploadPreview/fileUploadPreview.vue'
const importCheckRecordFlag = ref(false)
// 如果后端接收的就是当前格式,可直接透传
const formatForBackend = (rows) => {
return rows.map(row => ({
...row,
amount: Number(row.amount) || 0,
exchangeRate: Number(row.exchangeRate) || 1
}))
}
// 获取状态标签类型
const getStatusType = status => {
const types = {
0: 'warning',
1: 'success',
2: 'info'
}
return types[status] || 'info'
}
import { listType } from '@/api/system/dict/type'
const dictLists = ref([])
// 获取出账状态字典值
const getDictLists = () => {
return new Promise((resolve, reject) => {
listType({ typeList: ['csf_fortune_status'] })
.then(res => {
if (res.code === 200 && res.data) {
const dictData = res.data.find(item => item.dictType === 'csf_fortune_status')
dictLists.value = dictData?.dictItemList || []
console.log('获取到的字典数据:', dictLists.value)
resolve(dictLists.value)
} else {
dictLists.value = []
resolve([])
}
})
.catch(error => {
console.error('获取状态列表失败:', error)
dictLists.value = []
reject(error)
})
})
const onSubmit = (data) => {
console.log('提交给后端的数据:', data)
// 调用保存 API
}
// 返回数据中状态需要转换为字典值
const convertStatusToDict = status => {
const dictItem = dictLists.value.find(item => item.itemValue == status)
return dictItem?.itemLabel ?? status
}
// 初始化
onMounted(() => {
getDictLists()
getList()
// 获取入账状态,字典值转化方法
onMounted(async () => {
try {
await loadDicts(['csf_fortune_status'])
} catch (error) {
console.error('字典加载失败', error)
} finally {
loading.value = false
}
})
</script>
<style scoped>
.totalData {
margin-bottom: 10px;
font-size: 14px;
color: #000;
}
.financial-billing-page {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.operation-card {
margin-bottom: 20px;
}
.el-table {
margin-bottom: 20px;
}
.el-pagination {
.tableOptionContainer {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.financial-billing-page {
padding: 10px;
}
.search-card {
margin-bottom: 15px;
}
.operation-card {
margin-bottom: 15px;
}
.el-col {
margin-bottom: 10px;
}
margin-top: 10px;
}
</style>
\ No newline at end of file
<template>
<div class="financial-income-page">
<!-- 查询区域 -->
<el-card class="search-card">
<el-form :model="searchForm" label-width="100px">
<div class='container'>
<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>
<!-- 统计信息卡片 -->
<div class="statistics-container" v-if="statisticsData.totalPolicyCount > 0">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="保单号">
<el-input v-model="searchForm.policyNo" placeholder="请输入保单号" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="对账公司">
<el-select
v-model="searchForm.reconciliationCompany"
placeholder="请选择对账公司"
clearable
>
<el-option
v-for="item in companyOptions"
:key="item.value"
:label="item.label"
:value="item.label"
/>
</el-select>
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">总保单数</div>
<div class="card-value">{{ statisticsData.totalPolicyCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-form-item label="入账日期">
<el-date-picker
v-model="searchForm.incomeDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">总保费(HKD)</div>
<div class="card-value">{{ formatCurrency(statisticsData.totalPremium) }}</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-form-item label="预计入账日期">
<el-date-picker
v-model="searchForm.expectedDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">对账公司数</div>
<div class="card-value">{{ statisticsData.reconciliationCompanyCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-form-item label="比对状态" prop="status">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option
v-for="item in dictLists"
:key="item.itemValue"
:label="item.itemLabel"
:value="item.itemValue"
/>
</el-select>
</el-form-item>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">比对记录总条数</div>
<div class="card-value">{{ statisticsData.totalCompareCommissionCount }}</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<div class="search-buttons">
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button :icon="RefreshLeft" @click="resetForm">重置</el-button>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">比对成功数</div>
<div class="card-value">{{ statisticsData.successCompareCommissionCount }}</div>
</div>
</el-card>
</el-col>
</el-row>
</el-form>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">比对失败数</div>
<div class="card-value">{{ statisticsData.failedCompareCommissionCount }}</div>
</div>
</el-card>
<!-- 操作区域 -->
<div class="action-area">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增入账</el-button>
<!-- 导入区域 -->
<el-card class="import-card">
<div class="import-content">
<div class="import-actions">
<FileUpload
:fileType="['xlsx', 'xls']"
:action="'/csf/api/commission/upload/excel'"
@uploadEnd="getUploadFileFunc"
:responseType="'onlyStatus'"
/>
<el-button text @click="downloadTemplate" size="small" class="download-template-btn">
下载模板
</el-button>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">入账金额(实)</div>
<div class="card-value">{{ formatCurrency(statisticsData.totalPaidAmount) }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">入账金额(估)</div>
<div class="card-value">{{ formatCurrency(statisticsData.expectePaidAmount) }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">差额(估)</div>
<div class="card-value">{{ formatCurrency(statisticsData.differenceAmount) }}</div>
</div>
<!-- 列表区域 -->
<el-card class="table-card">
<div class="totalData" v-if="getAllSelectedRows().length > 0 || isSearch">
<span v-for="(item, index) in statisticList">
<span :style="{ marginLeft: index == 0 ? ' 0' : '20px' }">{{ item.name }}</span>
<span>{{ item.format ? formatCurrency(item.value) : item.value }}</span>
</span>
</el-card>
</el-col>
</el-row>
</div>
<el-table
ref="tableRef"
v-loading="tableLoading"
:data="tableData"
border
style="width: 100%"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
row-key="commissionBizId"
:reserve-selection="true"
>
<el-table-column
type="selection"
width="55"
align="center"
:reserve-selection="true"
></el-table-column
>/>
<el-table-column prop="policyNo" label="保单号" min-width="120" align="center" />
<el-table-column prop="expectedAmount" label="总金额" min-width="120" align="center" />
<el-table-column
prop="currentCommissionRatio"
label="当前来佣比例"
min-width="120"
align="center"
/>
<el-table-column
prop="periodPaidRatio"
label="已入账来佣比例"
min-width="120"
align="center"
/>
<el-table-column
prop="periodPendingRatio"
label="当期剩余来佣比例"
min-width="120"
align="center"
/>
<el-table-column prop="paidAmount" label="已入账金额" min-width="120" align="center" />
<el-table-column prop="pendingAmount" label="待入账金额" min-width="120" align="center" />
<el-table-column
prop="reconciliationCompany"
label="对账公司"
min-width="120"
align="center"
/>
<el-table-column
prop="commissionPeriod"
label="当前入账期数"
min-width="100"
align="center"
/>
<el-table-column prop="totalPeriod" label="预计总期数" min-width="100" align="center" />
<el-table-column prop="commissionName" label="入账项目" min-width="120" align="center" />
<el-table-column prop="amount" label="入账金额" min-width="100" align="center" />
<el-table-column prop="currency" label="入账币种" min-width="80" align="center">
<template #default="scope">
<dict-tag :options="currencyTypeOptions" :value="scope.row.currency" />
<el-table :data="tableData" ref="multipleTableRef" height="400" row-key="id" border highlight-current-row
style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="40" :selectable="selectableFn" />
<el-table-column prop="commissionStatus" label="比对状态" width="120" sortable fixed="left">
<template #default="{ row }">
{{ getDictLabel('csf_commission_status', row.commissionStatus) }}
</template>
</el-table-column>
<el-table-column prop="commissionDate" label="入账日期" min-width="120" align="center" />
<el-table-column prop="status" label="比对状态" min-width="100" align="center">
<template #default="scope">
<span>{{ convertStatusToDict(scope.row.status) }}</span>
<el-table-column prop="commissionBizType" label="业务类型" width="120" sortable>
<template #default="{ row }">
{{ getCommissionBizTypeLabel(row.commissionBizType) }}
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="150"
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<div class="btnCon">
<el-button text size="small" type="primary" @click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button text size="small" type="danger" @click="handleDelete(scope.row)"
>删除</el-button
>
<el-dropdown placement="bottom" style="margin-left: 10px">
<el-button type="primary" link size="small">
更多 <el-icon><ArrowDown /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleAlignment(scope.row)"
>比对记录</el-dropdown-item
>
<el-dropdown-item @click="operationRecord(scope.row)"
>操作记录</el-dropdown-item
>
</el-dropdown-menu>
<el-table-column prop="reconciliationYearMonth" label="检核年月" width="120" sortable />
<el-table-column prop="policyNo" label="保单号" width="120" sortable />
<el-table-column prop="reconciliationCompany" label="对账公司" width="120" sortable />
<el-table-column prop="commissionDate" label="入账日" width="120" sortable />
<el-table-column prop="commissionExpectedStatus" label="入账状态" width="120" sortable
:formatter="formatStatus" />
<el-table-column prop="currentCommissionRatio" label="本次入账比例" width="130" sortable />
<el-table-column prop="periodPaidRatio" label="累积入账比例" width="130" sortable />
<el-table-column prop="periodPendingRatio" label="待入账比例" width="120" sortable />
<el-table-column prop="paidAmount" label="本次入账金额" width="130" sortable />
<el-table-column prop="currency" label="入账币种" width="120" sortable />
<el-table-column prop="exchangeRate" label="结算汇率(实)" width="140" sortable />
<el-table-column prop="commissionPeriod" label="本次入账期数" width="130" sortable />
<el-table-column prop="totalPeriod" label="总期数" width="120" sortable />
<el-table-column prop="commissionName" label="入账项目" width="120" sortable />
<el-table-column prop="premium" label="期交保费" width="120" sortable />
<el-table-column prop="remark" label="备注" width="120" sortable />
<el-table-column prop="isDeleted" label="记录状态" width="120" sortable>
<template #default="{ row }">
{{ row.isDeleted === 1 ? '无效' : '有效' }}
</template>
</el-dropdown>
</div>
</el-table-column>
<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 dropdownItems" :key="item.value">
<el-text class="mx-1" v-if="!item.confirm">{{item.label}}</el-text>
<el-popconfirm v-if="item.confirm" confirm-button-text="Yes" cancel-button-text="No" :icon="InfoFilled" icon-color="#626AEF"
:title="item.confirm" @confirm="handleMenuConfirm(item.value, row)" width="300" placement="left-end">
<template #reference>
<el-text class="mx-1">{{item.label}}</el-text>
</template>
</el-popconfirm>
</el-menu-item>
</el-menu>
</el-popover>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 底部确认按钮 -->
<div class="footer-actions" v-if="selectedRows.length > 0">
<el-button
type="primary"
size="large"
@click="handleGenerateBilling"
:disabled="selectedRows.length === 0"
>
确认生成可出账记录
</el-button>
</div>
<!-- 编辑对话框 -->
<!-- 入账记录对话框(新增/编辑共用) -->
<el-dialog
v-model="incomeDialogVisible"
:title="incomeDialogTitle"
width="600px"
:before-close="handleCloseDialog"
>
<el-form :model="incomeForm" label-width="100px" :rules="incomeFormRules" ref="incomeFormRef">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="保单号" prop="policyNo">
<el-select
v-model="incomeForm.policyNo"
filterable
remote
reserve-keyword
placeholder="请输入保单号"
:remote-method="searchPolicy"
:loading="policyLoading"
@change="plilcyChange"
remote-show-suffix
>
<el-option
v-if="policyOptions.length === 0"
disabled
value=""
label="暂无数据"
style="color: #909399; text-align: center"
/>
<el-option
v-for="item in policyOptions"
:key="item.policyBizId"
:label="item.policyNo"
:value="item.policyNo"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 表格操作菜单 -->
<div class="tableOptionContainer">
<el-row justify="space-between">
<el-col :span="12">
<el-form-item label="对账公司" prop="reconciliationCompany">
<el-select v-model="incomeForm.reconciliationCompany" placeholder="请选择对账公司">
<el-option
v-for="item in companyOptions"
:key="item.value"
:label="item.label"
:value="item.label"
/>
</el-select>
</el-form-item>
<el-text type="danger">*非关联保单应收单以及比对失败的不可生成可出账记录</el-text>
</el-col>
<el-col :span="12">
<el-form-item label="当前期数" prop="commissionPeriod">
<el-input-number
v-model="incomeForm.commissionPeriod"
:min="1"
@change="periodChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="总期数" prop="totalPeriod">
<el-input-number v-model="incomeForm.totalPeriod" :min="1" :disabled="true" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入账项目" prop="commissionName">
<el-input v-model="incomeForm.commissionName" placeholder="请输入入账项目" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入账金额" prop="amount">
<el-input v-model="incomeForm.amount" placeholder="请输入金额" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="币种" prop="currency">
<el-select v-model="incomeForm.currency" placeholder="请选择币种">
<el-option
v-for="item in currencyTypeOptions"
:key="item.itemValue"
:label="item.itemLabel"
:value="item.itemValue"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入账日期" prop="commissionDate">
<el-date-picker
v-model="incomeForm.commissionDate"
type="date"
placeholder="选择日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-col :span="4">
<el-button type="primary" :icon="Select" :disabled="updatePayRollStatusDisable"
@click="generateCommissionRecordapi">{{ multipleSelection.length }} 条数据,生成可出账记录</el-button>
</el-col>
<el-col :span="12">
<el-form-item label="结算汇率" prop="exchangeRate">
<el-input v-model="incomeForm.exchangeRate" placeholder="请输入汇率" />
</el-form-item>
</el-row>
</div>
</template>
</CommonPage>
<!-- 开始检核弹窗-->
<CommonDialog dialogTitle='开始检核' dialogWidth='80%' :openDialog=dialogFlag :showAction='false' :showClose='true'
@close="closthDialog()">
<el-row>
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<SearchForm ref="checkFormRef" :config="checkConfig" v-model="checkFormData" />
</el-col>
<el-col :span="12">
<el-form-item label="保费" prop="premium">
<el-input v-model="incomeForm.premium" placeholder="请输入保费" />
</el-form-item>
</el-row>
<el-row :gutter="10">
<el-col :xs="24" :sm="12" :md="3" :lg="3">
<el-button type="primary" :icon="Plus" :disabled="!checkFormData?.reconciliationYearMonth"
@click="addCheckRecordDialogFlag = true;">新增</el-button>
</el-col>
<template v-if="editStatus == 'edit'">
<el-col :span="12">
<el-form-item label="比对状态" prop="status">
<el-select v-model="incomeForm.status" placeholder="请选择状态">
<el-option
v-for="item in dictLists"
:key="item.itemValue"
:label="item.itemLabel"
:value="item.itemValue"
/>
</el-select>
</el-form-item>
<el-col :xs="24" :sm="24" :md="3" :lg="3">
<el-button type="primary" :icon="Upload" :disabled="!checkFormData?.reconciliationYearMonth"
@click="fileUploadDialogFlag = true;">导入</el-button>
</el-col>
</template>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model="incomeForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</el-form-item>
<el-col :xs="24" :sm="24" :md="3" :lg="3">
<el-button link type="primary">下载导入模板 </el-button>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="incomeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitIncomeForm" :loading="incomeLoading">
{{ isEditMode ? '更新' : '新增' }}
</el-button>
</span>
</template>
</el-dialog>
<el-dialog title="比对记录" v-model="showAlignmentRecord" width="1000px" append-to-body>
<el-table :data="alignmentList" border>
<el-table-column prop="createTime" label="比对时间" align="center" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
<el-row :gutter="10">
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<el-table :data="checkRecordTableData" style="width: 100%" height="400">
<el-table-column prop="commissionBizType" label="应收款类型" width="120">
<template #default="{ row }">
{{ getCommissionBizTypeLabel(row.commissionBizType) }}
</template>
</el-table-column>
<el-table-column label="佣金期数" prop="commissionPeriod" align="center" fixed="left">
</el-table-column>
<el-table-column label="总期数" prop="totalPeriod" align="center"></el-table-column>
<el-table-column prop="status" label="比对状态" min-width="100" align="center">
<template #default="scope">
<span>{{ convertStatusToDict(scope.row.status) }}</span>
<!-- <el-table-column prop="receivableNo" label="应收款编号" width="120" /> -->
<el-table-column prop="policyNo" label="关联保单号" width="120" />
<el-table-column prop="commissionStatus" label="比对状态" width="120" fixed="left">
<template #default="{ row }">
{{ getDictLabel('csf_commission_status', row.commissionStatus) }}
</template>
</el-table-column>
<el-table-column label="来佣币种" prop="currency" align="center"
><template #default="scope">
<dict-tag :options="currencyTypeOptions" :value="scope.row.currency" /> </template
></el-table-column>
<el-table-column label="入账金额" prop="amount" align="center"></el-table-column>
<el-table-column label="结算汇率" prop="exchangeRate" align="center"></el-table-column>
</el-table>
<el-pagination
v-show="alignmentPages.total >= 0"
:total="alignmentPages.total"
:current-page="alignmentPages.pageNo"
:page-size="alignmentPages.pageSize"
:page-sizes="[5, 10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleAlignmentSizeChange"
@current-change="handleAlignmentCurrentChange"
style="margin-top: 10px"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">关 闭</el-button>
</div>
<el-table-column prop="commissionPeriod" label="佣金期数" width="120" />
<el-table-column prop="totalPeriod" label="总期数" width="120" />
<el-table-column prop="commissionDate" label="入账日(实)" width="120" />
<el-table-column prop="amount" label="入账金额" width="120" />
<el-table-column prop="currency" label="入账币种" width="120" />
<el-table-column prop="exchangeRate" label="结算汇率" width="120" />
<el-table-column prop="commissionName" label="入账项目" width="120" />
<el-table-column prop="reconciliationCompany" label="对账公司" width="120" />
<!-- <el-table-column prop="zip" label="入账状态" width="120" /> -->
<el-table-column fixed="right" label="操作" min-width="180">
<template #default="{ row }">
<el-button type="primary" size="default" @click="checkRecordEdit(row)">
修改
</el-button>
<el-popconfirm confirm-button-text="Yes" cancel-button-text="No" :icon="InfoFilled" icon-color="#626AEF"
title="确认要删除吗?" @confirm="deletePolicyCommissionApi(row)">
<template #reference>
<el-button>删除</el-button>
</template>
</el-dialog>
<el-dialog title="操作记录" v-model="showOperationRecord" width="1000px" append-to-body>
<el-table :data="operationList" border>
<el-table-column label="变更字段" prop="field" align="center" width="150"></el-table-column>
<el-table-column
label="变更前信息"
prop="beforeChange"
align="center"
width="200"
></el-table-column>
<el-table-column label="变更后信息" prop="afterChange" align="center" width="200">
</el-table-column>
<el-table-column label="变更人" prop="userName" align="center" width="150">
</el-table-column>
<el-table-column prop="createTime" label="变更时间" align="center">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="operationPages.total >= 0"
:total="operationPages.total"
:current-page="operationPages.pageNo"
:page-size="operationPages.pageSize"
:page-sizes="[5, 10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleOperationSizeChange"
@current-change="handleOperationCurrentChange"
style="margin-top: 10px"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">关 闭</el-button>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<div class="pagination-container">
<el-pagination v-model:current-page="checkRecordPageInfo.currentPage"
v-model:page-size="checkRecordPageInfo.pageSize" :page-sizes="[50, 100, 200, 300]"
:total="checkRecordPageInfo.total" @size-change="checkRecordPageHandleSizeChange"
@current-change="checkRecordPageHandleCurrentChange" layout="total, sizes, prev, pager, next, jumper" />
</div>
</template>
</el-dialog>
</el-col>
</el-row>
</CommonDialog>
<!-- 新增检核记录弹窗 -->
<CommonDialog :dialogTitle='editStatus.value == "add" ? "新增检核记录" : "修改检核记录"' dialogWidth='80%'
:openDialog=addCheckRecordDialogFlag :showAction='true' :showClose='true'
@close='addCheckRecordDialogFlag = false;editStatus = "add"' @confirm='handleAddCheckRecord()'>
<el-row>
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<SearchForm ref="addCheckRecordFormRef" :config="addCheckRecordConfig" v-model="addReceivablesFormModel" />
</el-col>
</el-row>
</CommonDialog>
<CommonDialog dialogTitle='文件导入' dialogWidth='80%' :openDialog=fileUploadDialogFlag :showAction='true'
:showClose='true' @close='fileUploadDialogFlag = false; files = ""'
@confirm='fileUploadDialogFlag = false; files = ""'>
<FileUpload v-model="files" :data="{ reconciliationYearMonth: checkFormData.reconciliationYearMonth }"
:file-type="['xlsx', 'xls']" :action="'/csf/api/commission/upload/excel'" :limit="1" :fileSize="15" @uploadEnd="handleUploadEnd"/>
</CommonDialog>
<!-- 设置比对状态 -->
<CommonDialog dialogTitle='设置比对状态' dialogWidth='80%' :openDialog=setCompareStatusDialogFlag :showAction='true'
:showClose='true' @close='setCompareStatusDialogFlag = false;' @confirm='setCompareStatusapi'>
<SearchForm ref="setCompareStatusFormRef" :config="setCompareStatusConfig" v-model="setCompareStatusFormModel" />
</CommonDialog>
<!-- 更新数据 -->
<CommonDialog dialogTitle='更新数据' dialogWidth='80%' :openDialog=updateDataDialogFlag :showAction='true'
:showClose='true' @close='updateDataDialogFlag = false;' @confirm='updateDataapi'>
<SearchForm ref="updateDataFormRef" :config="updateDataConfig" v-model="updateDataFormModel" />
</CommonDialog>
<!-- 查看记录 -->
<CommonDialog dialogTitle='查看记录' dialogWidth='80%' :openDialog=viewRecordDialogFlag :showAction='true'
:showClose='true' @close='viewRecordDialogFlag = false;'>
</CommonDialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { Search, RefreshLeft, UploadFilled, Plus, Document } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 导入有关来佣的接口
import { ref, reactive, onMounted } from 'vue'
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
import { ElMessage } from 'element-plus'
import { formatCurrency } from '@/utils/number'
// 接口
import {
getPolicyCommissionList,
downloadPolicyFortune,
generateCommissionRecord,
updatePolicyCommission,
incomeStatistics,
incomeEditRecords,
incomeCompareRecords,
policyData
getPolicyCommissionList, generateCommissionRecord,
addPayrollCheckRecord, commissionExpectedRecord, updateCompareStatus, updateCommissionRecord, deletePolicyCommission, syncExpectedCommission
} from '@/api/financial/commission'
import FileUpload from '@/components/FileUpload/index'
import { useRouter } from 'vue-router'
const { proxy } = getCurrentInstance()
const router = useRouter()
import { listType } from '@/api/system/dict/type'
// 添加表格引用
const tableRef = ref()
const statisticList = ref([
{ name: '合计应入账金额', value: '0', key: 'totalAmount', format: true },
{ name: '合计实际入账金额', value: '0', key: 'totalPaidAmount', format: true },
{ name: '待入账金额', value: '0', key: 'pendingPaidAmount', format: true },
{ name: '已入账比例', value: '0', key: 'paidAmountRatio', format: false },
{ name: '总保单数', value: '0', key: 'totalPolicyCount', format: false }
])
// 存储所有选中的行数据(用于跨页保持选择)
const allSelectedRows = ref(new Map())
const alignmentList = ref([]) //比对记录列表
const operationList = ref([]) //操作记录列表
const showAlignmentRecord = ref(false) // 是否显示比对记录列表
const showOperationRecord = ref(false) // 是否显示比对记录列表
// 通过dictType=csf_commission_status获取比对状态字典值,获取对象中的dictItemList
const dictLists = ref([])
const currencyTypeOptions = ref([])
const alignmentPages = reactive({
total: 0,
pageNo: 1,
pageSize: 5
import { Plus, Upload, Select } from '@element-plus/icons-vue'
import FileUpload from '@/components/FileUpload/index.vue'
import { loadDicts, getDictLabel } from '@/utils/useDict'
const files = ref('')
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
// 弹窗页面检核记录分页
const checkRecordPageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const operationPages = reactive({
total: 0,
pageNo: 1,
pageSize: 5
const checkRecordPageHandleSizeChange = (val) => {
checkRecordPageInfo.pageSize = val
checkRecordQuery()
}
const checkRecordPageHandleCurrentChange = (val) => {
checkRecordPageInfo.currentPage = val
checkRecordQuery()
}
const loading = ref(false)
const searchFormRef = ref(null)
const searchConfig = ref([
{
type: 'input',
prop: 'policyNo',
label: '保单号'
}, {
type: 'select',
prop: 'statusList',
label: '比对状态',
multiple: true,
dictType: 'csf_commission_status'
}, {
type: 'select',
prop: 'reconciliationCompanyBizIdList',
label: '对账公司',
api: '/insurance/base/api/insuranceReconciliationCompany/page',
keywordField: 'name',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入对账公司名称搜索',
debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'reconciliationCompanyBizId',
labelKey: 'name',
transform: (res) => {
console.log(res)
return res?.data.records || []
}
}, {
type: 'date',
prop: 'expectedDate',
label: '入账日(估)',
placeholder: '请选择'
}, {
type: 'daterange',
prop: 'commissionDate',
label: '入账日(实)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
}, {
type: 'select',
prop: 'insuranceCompanyBizIdList',
label: '保险公司',
api: '/insurance/base/api/insuranceCompany/page',
keywordField: 'queryContent',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入保险公司名称搜索',
debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'insuranceCompanyBizId',
labelKey: 'abbreviation',
transform: (res) => {
console.log(res)
return res?.data.records || []
}
}, {
type: 'month',
prop: 'reconciliationYearMonth',
label: '检核年月',
placeholder: '检核年月',
}
])
const selectedRow = ref(null)
// 设置比对状态弹窗
const setCompareStatusDialogFlag = ref(false)
const setCompareStatusFormRef = ref(null)
const setCompareStatusConfig = [
{
type: 'select',
prop: 'status',
label: '比对状态',
dictType: 'csf_commission_status'
}
]
const setCompareStatusFormModel = reactive({
status: ''
})
const currentRowOperation = ref({})
const isSearch = ref(false)
// 当比对状态为成功时可以选择
const selectableFn = (row) => row.commissionStatus == '1' && row.commissionBizType == 'R'
// 设置编辑状态,是新增还是修改
const editStatus = ref('add')
const policyLoading = ref(false)
const policyOptions = ref([])
// 搜索保单方法
const searchPolicy = query => {
policyLoading.value = true
try {
const params = {
policyNo: query,
pageNo: 1,
pageSize: 10
}
policyData(params).then(response => {
policyOptions.value = response.data.records
})
} catch (error) {
console.error('保单号搜索失败', error)
policyOptions.value = []
} finally {
policyLoading.value = false
}
}
const plilcyChange = () => {
policyOptions.value.forEach(item => {
if (incomeForm.policyNo == item.policyNo) {
incomeForm.premium = item.premium
incomeForm.totalPeriod = item.totalPeriod
}
})
}
const periodChange = () => {
if (
incomeForm.totalPeriod &&
incomeForm.commissionPeriod &&
incomeForm.commissionPeriod > incomeForm.totalPeriod
) {
ElMessage.error('当前期数不能大于总期数')
incomeForm.commissionPeriod = '1'
}
}
const getAlignmentData = () => {
let data = {
commissionBizId: currentRowOperation.value.commissionBizId,
pageNo: alignmentPages.pageNo,
pageSize: alignmentPages.pageSize
}
incomeCompareRecords(data).then(response => {
if (response.code == 200) {
alignmentList.value = response.data.records
alignmentPages.total = response.data.total
showAlignmentRecord.value = true
} else {
ElMessage.error(response.msg)
showAlignmentRecord.value = false
alignmentPages.total = 0
alignmentList.value = []
}
})
}
const getOperationData = () => {
let data = {
commissionBizId: currentRowOperation.value.commissionBizId,
pageNo: operationPages.pageNo,
pageSize: operationPages.pageSize
}
incomeEditRecords(data).then(response => {
if (response.code == 200) {
operationList.value = response.data.records
operationPages.total = response.data.total
showOperationRecord.value = true
} else {
ElMessage.error(response.msg)
showOperationRecord.value = false
operationPages.total = 0
operationList.value = []
}
})
}
// 比对记录分页事件
const handleAlignmentSizeChange = val => {
alignmentPages.pageSize = val
alignmentPages.pageNo = 1
getAlignmentData()
}
const handleAlignmentCurrentChange = val => {
alignmentPages.pageNo = val
getAlignmentData()
}
// 更新数据
const updateDataDialogFlag = ref(false)
// 查看记录
const viewRecordDialogFlag = ref(false)
// 操作记录分页事件
const handleOperationSizeChange = val => {
operationPages.pageSize = val
operationPages.pageNo = 1
getOperationData()
}
const handleOperationCurrentChange = val => {
operationPages.pageNo = val
getOperationData()
}
const closeDialog = () => {
showAlignmentRecord.value = false
alignmentList.value = []
alignmentPages.value = {
pageNo: 1,
pageSize: 5,
total: 0
}
showOperationRecord.value = false
operationList.value = []
operationPages.value = {
pageNo: 1,
pageSize: 5,
total: 0
}
}
// 设置当前页的选中状态
const setCurrentPageSelection = () => {
if (!tableRef.value) return
// 清除当前页的选择
// tableRef.value.clearSelection()
const fileUploadDialogFlag = ref(false)
const checkRecordTableData = ref([])
// 设置当前页应该选中的行
tableData.value.forEach(row => {
if (allSelectedRows.value.has(row.commissionBizId)) {
tableRef.value.toggleRowSelection(row, true)
const selectedRowCheck = ref(null)
const checkFormRef = ref(null)
const checkConfig = ref([
{
type: 'month',
prop: 'reconciliationYearMonth',
label: '检核年月',
placeholder: '检核年月',
rules: [{ required: true, message: '请选择检核年月', trigger: 'blur' }]
}
})
}
// 获取所有选中的行(包含跨页选择的)
const getAllSelectedRows = () => {
return Array.from(allSelectedRows.value.values())
])
const checkRecordEdit = (row) => {
console.log('查看记录', row)
selectedRowCheck.value = {...row}
editStatus.value = 'edit'
addReceivablesFormModel.value = { ...row }
addCheckRecordDialogFlag.value = true
// console.log('父组件赋值',addReceivablesFormModel.value)
}
// 更新全局选中状态
const updateAllSelectedRows = () => {
// 获取当前页的所有行的 key(commissionBizId)
const currentPageKeys = tableData.value.map(row => row.commissionBizId)
// 从全局选中中移除当前页不再存在的行
// for (const key of allSelectedRows.value.keys()) {
// if (!currentPageKeys.includes(key)) {
// allSelectedRows.value.delete(key)
// }
// }
const checkFormData = ref({
reconciliationYearMonth: ''
})
// 更新当前页的选中状态
tableData.value.forEach(row => {
const isSelected = selectedRows.value.some(
selectedRow => selectedRow.commissionBizId === row.commissionBizId
)
// 监听 checkFormData 变化,自动重新查询
watch(
() => ({ ...checkFormData.value }), // 深度监听对象内容
(newVal) => {
// console.log('搜索条件变化:', newVal)
// ✅ 在这里调用查询接口 或 触发列表刷新
if (isSelected) {
allSelectedRows.value.set(row.commissionBizId, row)
} else {
allSelectedRows.value.delete(row.commissionBizId)
if (newVal.reconciliationYearMonth) {
checkRecordQuery()
}
})
getStatistics()
}
const getStatistics = async () => {
},
{ immediate: true } // 首次加载也执行(可选)
)
// 删除检核记录
const deletePolicyCommissionApi = async (row) => {
console.log(row)
try {
let commissionIds = getAllSelectedRows().map(item => item.id)
if (commissionIds.length > 0) {
// 调用接口获取来佣列表
const res = await incomeStatistics({
commissionIds: commissionIds
})
if (res.data) {
statisticList.value.forEach(item => {
item.value = res.data[item.key]
})
}
const res = await deletePolicyCommission({ commissionBizId: row.commissionBizId })
if (res.code === 200) {
ElMessage.success('删除成功')
checkRecordQuery()
return true
} else {
isSearch.value = false
ElMessage.error(res.msg || '删除失败')
return false
}
} catch (error) {
console.error('获取数据失败:', error)
ElMessage.error('统计失败')
}
}
// 清空所有选择
const clearAllSelection = () => {
allSelectedRows.value.clear()
selectedRows.value = []
if (tableRef.value) {
tableRef.value.clearSelection()
ElMessage.error('删除失败')
return false
}
}
// 格式化金额
const formatCurrency = amount => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount)
}
const getLists = () => {
return new Promise((resolve, reject) => {
listType({ typeList: ['csf_commission_status', 'bx_currency_type'] })
.then(res => {
if (res.code === 200 && res.data) {
const dictData = res.data.find(item => item.dictType === 'csf_commission_status')
dictLists.value = dictData?.dictItemList || []
// 处理币种字典值
const currencyData = res.data.find(item => item.dictType === 'bx_currency_type')
if (currencyData) {
currencyData?.dictItemList.forEach(item => {
item.value = item.itemValue
item.label = item.itemLabel
})
}
currencyTypeOptions.value = currencyData?.dictItemList || []
resolve(dictLists.value, currencyTypeOptions.value)
// 生成可出账记录按钮
const updatePayRollStatusDisable = ref(true)
// 获取入账状态,字典值转化方法
onMounted(async () => {
try {
await loadDicts(['csf_expected_commission_status', 'csf_commission_status'])
} catch (error) {
console.error('字典加载失败', error)
} finally {
loading.value = false
}
})
// 格式化函数(每次渲染都会调用,所以能拿到最新字典)
const formatStatus = (row, column) => {
return getDictLabel('csf_expected_commission_status', row.status) // 实时查缓存
}
// 应收单类型
const commissionBizTypeOptions = [
{ value: 'R', label: '关联保单应收单' },
{ value: 'U', label: '非关联保单应收单' }
]
// 应收单类型通过value转成label
const getCommissionBizTypeLabel = (value) => {
const item = commissionBizTypeOptions.find(item => item.value === value)
return item?.label || ''
}
// 新增应收款管理
const addReceivablesFormModel = ref({})
// 新增检核记录表单
const addCheckRecordFormRef = ref(null)
const addCheckRecordConfig = ref([
{
type: 'select',
prop: 'commissionBizType',
label: '应收单类型',
placeholder: '应收单类型',
options: commissionBizTypeOptions,
rules: [
{ required: true, message: '请选择应收单类型', trigger: 'blur' },
]
},
{
type: 'input',
prop: 'policyNo',
label: '保单号',
visible: (formData) => formData.commissionBizType == 'R',
rules: [
{ required: true, message: '请输入保单号', trigger: 'blur' },
]
},
{
type: 'input',
prop: 'commissionPeriod',
label: '佣金期数',
inputType: 'decimal',
visible: (formData) => formData.commissionBizType == 'R',
rules: [
{ required: true, message: '请输入佣金期数', trigger: 'blur' },
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'input',
prop: 'totalPeriod',
label: '总期数',
inputType: 'decimal',
visible: (formData) => formData.commissionBizType == 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'date',
prop: 'commissionDate',
label: '入账日(实)',
placeholder: '请选择',
maxDate: 'today',
rules: [
{ required: true, message: '请选择入账日(实)', trigger: 'blur' },
]
}, {
type: 'input',
prop: 'amount',
label: '入账金额',
inputType: 'decimal',
decimalDigits: 2,
rules: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '最多两位小数', trigger: 'blur' }
]
}, {
type: 'select',
prop: 'currency',
label: '入账币种',
dictType: 'bx_currency_type',
rules: [
{ required: true, message: '请选择入账币种', trigger: 'blur' },
]
}, {
type: 'select',
prop: 'commissionType',
label: '入账项目',
dictType: 'csf_commission_type',
rules: [
{ required: true, message: '请选择入账项目', trigger: 'blur' },
],
onChangeExtraFields: {
commissionName: 'itemLabel'
},
}, {
type: 'select',
prop: 'reconciliationCompanyBizId',
label: '对账公司',
api: '/insurance/base/api/insuranceReconciliationCompany/page',
keywordField: 'name',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入对账公司名称搜索',
debounceWait: 500, // 自定义防抖时间
onChangeExtraFields: {
reconciliationCompany: 'name',// 自动同步 raw.name 到 reconciliationCompany
reconciliationCompanyCode: 'code'
},
valueKey: 'reconciliationCompanyBizId',
labelKey: 'name',
transform: (res) => {
console.log(res)
return res?.data.records || []
},
rules: [
{ required: true, message: '请选择对账公司', trigger: 'blur' },
]
}, {
type: 'input',
prop: 'exchangeRate',
label: '结算汇率(实)',
inputType: 'decimal',
decimalDigits: 2,
rules: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '最多两位小数', trigger: 'blur' }
]
},
])
const addCheckRecordDialogFlag = ref(false)
const handleAddCheckRecord = async () => {
try {
const params = ref({})
if (editStatus.value === 'edit') {
params.value = {
...addReceivablesFormModel.value,
commissionBizId: selectedRowCheck.value.commissionBizId
}
console.log(params)
await updateCommissionRecord(params.value)
ElMessage.success('更新检核记录成功')
} else {
dictLists.value = []
currencyTypeOptions.value = []
resolve([], [])
const addCheckSearchParams = addCheckRecordFormRef.value.getFormData()
console.log('新增检核记录', addCheckSearchParams)
params.value = {
...addCheckSearchParams,
reconciliationYearMonth: checkFormData.value.reconciliationYearMonth
}
await addPayrollCheckRecord([params.value])
ElMessage.success('新增检核记录成功')
addCheckRecordDialogFlag.value = false
checkRecordTableData.value = []
}
checkRecordQuery()
} catch (error) {
console.error('新增检核记录失败', error)
ElMessage.error('新增检核记录失败')
}
})
.catch(error => {
console.error('获取状态列表失败:', error)
dictLists.value = []
reject(error)
})
})
}
// 返回数据中状态需要转换为字典值
const convertStatusToDict = status => {
const dictItem = dictLists.value.find(item => item.itemValue == status)
return dictItem?.itemLabel ?? status
}
// 增加通过表格排序,排序字段sortField,升降序sortOrder
const handleSortChange = column => {
pagination.sortField = column.prop
pagination.sortOrder = column.order === 'ascending' ? 'ascend' : 'descend'
fetchTableData()
}
// 搜索表单数据
const searchForm = reactive({
policyNo: '',
reconciliationCompany: '',
incomeDateRange: [],
status: '',
expectedDate: ''
})
// 对账公司选项
const companyOptions = ref([])
// 获取对账公司列表
import { getReconciliationCompanyList } from '@/api/financial/commission'
import { getAllCompanys } from '@/api/common'
const fetchReconciliationCompanyList = async () => {
// 生成可出账记录
const generateCommissionRecordapi = async () => {
try {
const response = await getAllCompanys({
pageNo: 1,
pageSize: 1000
const res = await generateCommissionRecord({
commissionBizIdList: multipleSelection.value.map(item => item.commissionBizId)
})
if (response.code === 200) {
companyOptions.value = response.data.records.map(item => ({
label: item.deptName,
value: item.deptBizId
}))
if (res.code === 200) {
ElMessage.success('生成可出账记录成功')
} else {
ElMessage.error(response.msg)
ElMessage.error(res.msg || '生成可出账记录失败')
}
} catch (error) {
ElMessage.error('获取对账公司列表失败')
console.error('生成可出账记录失败', error)
ElMessage.error('生成可出账记录失败')
}
}
// 表格数据
const tableData = ref([])
const tableLoading = ref(false)
const selectedRows = ref([])
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
onMounted(() => {
// 获取字典值后,再调用接口获取来佣列表
initialData()
})
const initialData = async () => {
await getLists()
fetchTableData()
fetchReconciliationCompanyList()
}
const getUploadFileFunc = data => {
console.log(data)
if (data === 200) {
ElMessage.success('上传成功')
fetchTableData()
} else {
ElMessage.error('上传失败')
}
}
// 获取列表数据
// 调用接口获取入账列表
const fetchTableData = async () => {
tableLoading.value = true
// 表格操作菜单
const dropdownItems = [
{ label: '设置比对状态', value: 'setStatus' ,confirm:''},
{ label: '同步到应收款管理', value: 'syncToReceivable',confirm:'确认要同步到应收款管理吗?' },
// { label: '更新数据', value: 'editRecord' },
// { label: '查看记录', value: 'viewRecord' }
]
const handleMenuConfirm = async (action, row) => {
console.log('点击了操作菜单', action, row)
if (action === 'syncToReceivable') {
try {
// 调用接口获取来佣列表
const res = await getPolicyCommissionList({
pageNo: pagination.currentPage,
pageSize: pagination.pageSize,
policyNo: searchForm.policyNo,
status: searchForm.status,
expectedDate: searchForm.expectedDate ? proxy.parseTime(searchForm.expectedDate) : '',
reconciliationCompany: searchForm.reconciliationCompany,
startTime: searchForm.incomeDateRange[0] ? `${searchForm.incomeDateRange[0]} 00:00:00` : '',
endTime: searchForm.incomeDateRange[1] ? `${searchForm.incomeDateRange[1]} 23:59:59` : ''
})
if (res.data.page) {
tableData.value = res.data.page.records
pagination.total = res.data.page.total
if(row.commissionStatus !== '3'){
ElMessage.error('应收款管理已有这条记录,无需同步')
return
}
if (res.data.commissionStatisticsVO && isSearch.value) {
statisticList.value.forEach(item => {
item.value = res.data.commissionStatisticsVO[item.key]
const res = await syncExpectedCommission({
commissionBizId: row.commissionBizId
})
if (res.code === 200) {
ElMessage.success('同步到应收款管理成功')
} else {
ElMessage.error(res.msg || '同步到应收款管理失败')
}
// 数据加载完成后,设置当前页的选中状态
nextTick(() => {
setCurrentPageSelection()
})
} catch (error) {
console.error('获取数据失败:', error)
ElMessage.error('获取数据失败')
} finally {
tableLoading.value = false
console.error('同步到应收款管理失败', error)
ElMessage.error('同步到应收款管理失败')
}
}
// 处理查询
const handleSearch = () => {
pagination.currentPage = 1
Object.values(searchForm).some(value => {
if (Array.isArray(value) && value.length > 0) {
isSearch.value = true
}
if (value !== '' && value != null) {
isSearch.value = true
}
})
clearAllSelection()
fetchTableData()
ElMessage.success('查询成功')
}
// 重置表单
const resetForm = () => {
searchForm.policyNo = ''
searchForm.reconciliationCompany = ''
searchForm.status = ''
searchForm.expectedDate = ''
searchForm.incomeDateRange = []
isSearch.value = false
pagination.currentPage = 1
pagination.pageSize = 10
// 清空选择
clearAllSelection()
ElMessage.success('已重置筛选条件')
fetchTableData()
}
// 处理分页大小变化
const handleSizeChange = val => {
pagination.pageSize = val
fetchTableData()
}
// 处理分页页码变化
const handleCurrentChange = val => {
pagination.currentPage = val
fetchTableData()
}
// 统计信息
const statisticsData = ref({})
// 弹窗相关
const dialogFlag = ref(false)
// 处理表格选择变化
const handleSelectionChange = rows => {
selectedRows.value = rows
// 更新全局选中状态
updateAllSelectedRows()
// 按钮事件处理
const handleAdd = () => {
// 开始检核
dialogFlag.value = true
}
// 下载模板
const downloadTemplate = () => {
// 下载地址
const templateUrl =
'https://yd-ali-oss.oss-cn-shanghai-finance-1-pub.aliyuncs.com/xlsx/2025/10/14/54ce715eabab4f1abd8652ba0fca0c51.xlsx'
// 修改下载文件名
const fileName = '对账单导入模板.xlsx'
// 下载文件
window.open(templateUrl, '_blank', `download=${fileName}-${new Date().getTime()}`)
const handleExport = () => {
ElMessage.info('点击导出按钮')
}
// 新增入账
const handleAdd = () => {
incomeDialogTitle.value = '新增入账记录'
isEditMode.value = false
editStatus.value = 'add'
// 重置表单数据
Object.assign(incomeForm, {
commissionBizId: '',
policyNo: '',
reconciliationCompany: '',
commissionPeriod: 1,
totalPeriod: 1,
commissionName: '',
amount: 0,
currency: 'HKD',
commissionDate: '',
remark: '',
exchangeRate: '7.80',
premium: '',
status: ''
})
// 重置表单验证
if (incomeFormRef.value) {
incomeFormRef.value.clearValidate()
}
incomeDialogVisible.value = true
const handleReset = () => {
// 重置搜索表单
searchFormRef.value.resetForm()
checkFormData.value = {}
console.log('表单已重置')
}
import { deletePolicyCommission } from '@/api/financial/commission'
// 删除
const handleDelete = row => {
ElMessageBox.confirm(`确定要删除保单 ${row.policyNo} 的入账记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 调用删除接口
deletePolicyCommission({
commissionBizId: row.commissionBizId
})
.then(res => {
if (res.code === 200) {
ElMessage.success('删除成功')
fetchTableData()
} else {
ElMessage.error('删除失败')
}
})
.catch(() => {
ElMessage.error('删除失败')
})
})
const handleQuery = async () => {
const params = searchFormRef.value.getFormData()
console.log('父组件发起查询:', params)
loadTableData(params)
}
// 生成可出账记录
// 调用generateCommissionRecord接口
const handleGenerateBilling = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择要生成可出账记录的记录')
return
}
// 查询检核记录
const checkRecordQuery = async () => {
try {
console.log(selectedRows.value)
await generateCommissionRecord({
commissionBizIdList: selectedRows.value.map(row => row.commissionBizId)
})
ElMessage.success('生成成功')
// 这里可以添加路由跳转到 financialBilling 组件
// router.push('/financialCenter/financial/billing')
const params = {
reconciliationYearMonth: checkFormData.value.reconciliationYearMonth,
pageNo: checkRecordPageInfo.currentPage,
pageSize: checkRecordPageInfo.pageSize
}
const res = await commissionExpectedRecord(params)
checkRecordTableData.value = res.data.records || []
checkRecordPageInfo.total = res.data.total || 0
checkRecordPageInfo.currentPage = res.data.current || 0
checkRecordPageInfo.pageSizes = res.data.size || 0
} catch (error) {
console.error('生成失败:', error)
ElMessage.error('生成失败')
console.error('查询检核记录失败', error)
ElMessage.error('查询检核记录失败')
}
}
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
console.log('全选:', val)
// 生成可出账记录按钮是否禁用
updatePayRollStatusDisable.value = val.length === 0
}
const visibleDefaultButtons = ref(['add', 'reset', 'query'])
// 按钮配置
const operationBtnList = ref([
{
key: 'add',
direction: 'left',
label: '开始检核',
click: handleAdd
},
// {
// key: 'export',
// direction: 'right',
// click: handleExport
// },
{
key: 'reset',
direction: 'right',
click: handleReset
},
{
key: 'query',
direction: 'right',
click: handleQuery
}
}
// 入账记录对话框相关
const incomeDialogVisible = ref(false)
const incomeDialogTitle = ref('入账记录')
const incomeLoading = ref(false)
const isEditMode = ref(false)
const incomeFormRef = ref()
// 入账表单数据
const incomeForm = reactive({
commissionBizId: '',
policyNo: '',
reconciliationCompany: '',
commissionPeriod: 1,
totalPeriod: 1,
commissionName: '',
amount: 0,
currency: 'CNY',
commissionDate: '',
remark: '',
expectedDate: '',
exchangeRate: '',
premium: '',
status: ''
})
// 表单验证规则
const incomeFormRules = {
policyNo: [{ required: true, message: '请输入保单号', trigger: 'blur' }],
reconciliationCompany: [{ required: true, message: '请选择对账公司', trigger: 'change' }],
commissionName: [{ required: true, message: '请输入入账项目', trigger: 'blur' }],
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
commissionDate: [{ required: true, message: '请选择入账日期', trigger: 'change' }]
}
// 编辑入账记录
const handleEdit = row => {
incomeDialogTitle.value = `编辑入账记录 - ${row.policyNo}`
isEditMode.value = true
editStatus.value = 'edit'
console.log('row', row)
// 填充表单数据
Object.assign(incomeForm, {
commissionBizId: row.commissionBizId,
policyNo: row.policyNo,
reconciliationCompany: row.reconciliationCompany,
commissionPeriod: row.commissionPeriod || 1,
totalPeriod: row.totalPeriod || 1,
commissionName: row.commissionName,
amount: row.amount,
currency: row.currency || 'CNY',
commissionDate: row.commissionDate,
remark: row.remark || '',
exchangeRate: row.exchangeRate || '',
premium: row.premium || '',
status: row.status || ''
})
])
// 重置表单验证
if (incomeFormRef.value) {
incomeFormRef.value.clearValidate()
// 加载表格数据
const loadTableData = async (searchParams = {}) => {
loading.value = true
try {
const params = {
pageNo: currentPage.value,
pageSize: pageSize.value,
...searchParams,
commissionDateStart: searchParams.commissionDate?.[0] || undefined,
commissionDateEnd: searchParams.commissionDate?.[1] || undefined,
commissionDate: undefined
}
const res = await getPolicyCommissionList(params)
tableData.value = res.data.page.records || []
pageTotal.value = res.data.page.total || 0
pageSize.value = res.data.page.size || 0
// 统计信息
statisticsData.value = res.data.commissionStatisticsVO || {}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
incomeDialogVisible.value = true
}
const handleAlignment = row => {
currentRowOperation.value = row
getAlignmentData()
loadTableData()
// 分页事件
const handleSizeChange = (val) => {
pageSize.value = val
loadTableData()
}
const operationRecord = row => {
currentRowOperation.value = row
getOperationData()
const handleCurrentChange = (val) => {
currentPage.value = val
loadTableData()
}
// 提交入账表单(新增/编辑共用)
import { addPolicyCommission } from '@/api/financial/commission'
import { computed } from 'vue'
const submitIncomeForm = async () => {
if (!incomeFormRef.value) {
ElMessage.error('请先初始化表单')
// 表格数据
const tableData = ref([])
const handleSelect = (e, row) => {
selectedRow.value = row
console.log('选中行:', e, row)
if (e === 'setStatus') {
setCompareStatusDialogFlag.value = true
return
}
} else if (e === 'editRecord') {
updateDataDialogFlag.value = true
return
} else if (e === 'viewRecord') {
viewRecordDialogFlag.value = true
return
}else if (e === 'syncToReceivable') {
try {
// 表单验证
const valid = await incomeFormRef.value.validate()
if (!valid) {
ElMessage.warning('请完善表单信息')
return
}
}
incomeLoading.value = true
// 设置比对状态api
if (isEditMode.value) {
// 编辑模式 - 调用更新接口
await updatePolicyCommission(incomeForm)
ElMessage.success('更新成功')
const setCompareStatusapi = async () => {
try {
const res = await updateCompareStatus({
...setCompareStatusFormRef.value.getFormData(),
commissionBizId: selectedRow.value.commissionBizId
})
if (res.code === 200) {
ElMessage.success('设置比对状态成功')
setCompareStatusDialogFlag.value = false
setCompareStatusFormRef.value.resetForm()
loadTableData()
} else {
// 新增模式 - 调用新增接口
// 注意:这里需要根据实际的新增API进行调整
await addPolicyCommission(incomeForm)
ElMessage.success('新增成功')
ElMessage.error(res.msg || '设置比对状态失败')
}
incomeDialogVisible.value = false
fetchTableData() // 刷新表格数据
} catch (error) {
if (error && error.errorFields) {
ElMessage.warning('请完善表单信息')
console.error('设置比对状态失败', error)
ElMessage.error('设置比对状态失败')
}
}
// 更新数据api
const updateDataFormRef = ref(null)
const updateDataFormModel = ref({
commissionAmount: '',
commissionStatus: ''
})
const updateDataConfig = addCheckRecordConfig
const updateDataapi = async () => {
try {
const res = await updateCommissionRecord({
...updateDataFormRef.value.getFormData(),
commissionBizId: selectedRow.value.commissionBizId
})
if (res.code === 200) {
ElMessage.success('更新数据成功')
updateDataDialogFlag.value = false
updateDataFormRef.value.resetForm()
loadTableData()
} else {
console.error(isEditMode.value ? '更新失败:' : '新增失败:', error)
ElMessage.error(isEditMode.value ? '更新失败' : '新增失败')
ElMessage.error(res.msg || '更新数据失败')
}
} finally {
incomeLoading.value = false
} catch (error) {
console.error('更新数据失败', error)
ElMessage.error('更新数据失败')
}
}
// 关闭对话框处理
const handleCloseDialog = done => {
if (incomeLoading.value) {
ElMessage.info('正在保存,请稍候...')
return
const handleUploadEnd = (code) => {
if (code === 200) {
ElMessage.success('上传文件成功')
fileUploadDialogFlag.value = false
files.value = ''
checkRecordQuery()
} else {
ElMessage.error('上传文件失败')
}
}
ElMessageBox.confirm('确定要关闭吗?未保存的修改将会丢失', '提示', {
type: 'warning'
})
.then(() => {
done()
})
.catch(() => {})
// 关闭弹窗后的操作
const closthDialog = () => {
dialogFlag.value = false
checkFormRef.value.resetForm()
checkFormData.value = {}
checkRecordTableData.value = []
}
</script>
<style scoped>
.btnCon {
display: flex;
align-items: center;
justify-content: center;
}
.financial-income-page {
padding: 20px;
max-width: 1600px;
margin: 0 auto;
}
.search-card {
margin-bottom: 20px;
padding: 15px 20px;
}
.search-buttons {
display: flex;
gap: 10px;
align-items: center;
height: 32px;
}
.action-area {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.import-card {
padding: 20px;
background-color: #f6ffed;
border: 1px solid #b7eb8f;
}
.import-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.import-actions {
display: flex;
align-items: center;
gap: 10px;
}
.upload-excel {
display: inline-block;
}
/* 文件信息显示样式 */
.file-info {
margin-top: 15px;
padding: 12px 16px;
background-color: #f5f7fa;
border-radius: 4px;
border: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
}
.file-info-content {
display: flex;
align-items: center;
gap: 8px;
}
.file-info-content .el-icon {
color: #409eff;
font-size: 16px;
}
.file-name {
font-weight: 500;
color: #303133;
}
.file-size {
color: #909399;
font-size: 12px;
}
.confirm-import-btn {
margin-left: 12px;
}
/* 下载模板按钮样式 */
.download-template-btn {
color: #409eff;
font-size: 12px;
padding: 8px 12px;
}
.download-template-btn:hover {
background-color: #ecf5ff;
}
.table-card {
padding: 15px 20px;
}
.totalData {
margin-bottom: 10px;
font-size: 14px;
color: #000;
}
.pagination {
margin-top: 15px;
text-align: right;
}
.footer-actions {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.import-actions {
flex-wrap: wrap;
}
.download-template-btn {
.tableOptionContainer {
margin-top: 10px;
}
.file-info {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.confirm-import-btn {
margin-left: 0;
align-self: flex-end;
}
}
@media (max-width: 992px) {
.search-card .el-col {
margin-bottom: 15px;
}
.search-buttons {
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.search-card .el-col {
width: 100%;
}
.import-actions {
flex-direction: column;
align-items: flex-start;
}
.download-template-btn {
margin-top: 10px;
}
.el-row,
.el-col {
margin-bottom: 5px;
}
</style>
\ No newline at end of file
<template>
<div class="financial-salary-page">
<!-- 查询区域 -->
<el-card class="search-card">
<el-form :model="queryParams" label-width="80px">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="保单号">
<el-input v-model="queryParams.policyNo" placeholder="请输入保单号" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="转介人">
<el-input v-model="queryParams.broker" placeholder="请输入转介人" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="出账日期">
<el-date-picker
v-model="queryParams.accountDate"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="出账状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option
v-for="item in dictLists"
:key="item.itemValue"
:label="item.itemLabel"
:value="item.itemValue"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作区域 -->
<el-card class="operation-card">
<el-row :gutter="10">
<el-col :span="12">
<el-button type="primary" @click="handleCreate">新建薪资记录</el-button>
<el-button @click="handleImport">批量导入</el-button>
<div class='app-container'>
<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>
<!-- 统计数据 -->
<div class="statistics-container">
<el-row :gutter="16">
<el-col :xs="24" :sm="12" :md="6" class="text-center mb-4">
<el-statistic title="总金额" :value="statisticInfo.totalAmount" />
</el-col>
<el-col :span="12" style="text-align: right">
<el-button type="success" :disabled="selectedRows.length === 0" @click="completeBilling">
完成出账
</el-button>
<el-col :xs="24" :sm="12" :md="6" class="text-center mb-4">
<el-statistic title="总人数" :value="statisticInfo.brokerCount" />
</el-col>
</el-row>
</el-card>
<!-- 数据表格 -->
<el-card>
<div class="totalData" v-if="getAllSelectedRows().length > 0 || isSearch">
<span v-for="(item, index) in statisticList">
<span :style="{ marginLeft: index == 0 ? ' 0' : '20px' }">{{ item.name }}</span>
<span>{{ item.format ? formatCurrency(item.value) : item.value }}</span>
</span>
</div>
<el-table
:data="tableData"
@selection-change="handleSelectionChange"
v-loading="loading"
ref="tableRef"
row-key="fortuneAccountBizId"
:reserve-selection="true"
>
<el-table :data="tableData" @selection-change="handleSelectionChange" v-loading="loading" ref="tableRef"
row-key="fortuneAccountBizId" :reserve-selection="true" :border="true">
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="broker" label="转介人" min-width="120" />
<el-table-column prop="team" label="所属团队" min-width="120" />
<el-table-column prop="amount" label="出账金额" width="120">
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
<el-table-column prop="broker" label="转介人" min-width="120" sortable />
<el-table-column prop="team" label="所属团队" min-width="120" sortable />
<el-table-column prop="amount" label="出账金额" width="120" sortable>
<template #default="scope">
{{ formatCurrency(scope.row.amount) }}
</template>
</el-table-column>
<el-table-column prop="currency" label="出账币种" width="100">
<el-table-column prop="currency" label="出账币种" width="120" sortable>
<template #default="scope">
<dict-tag :options="currencyTypeOptions" :value="scope.row.currency" />
</template>
</el-table-column>
<el-table-column prop="status" label="出账状态" width="100">
<el-table-column prop="status" label="出账状态" width="150" sortable>
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ convertStatusToDict(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fortuneAccountDate" label="出账日" min-width="150" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" @click="handleEdit(row)">修改</el-button>
<el-button size="small" type="primary" @click="handleRevise(row)">修订记录</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
<!-- <el-button size="small" type="info" @click="generateSalarySlip(row)">生成薪资单</el-button> -->
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList"
/>
</el-card>
<!-- 新建薪资记录对话框 -->
<el-dialog title="新建薪资记录" v-model="createDialogVisible" width="500px">
<el-form :model="createFormData" label-width="100px">
<el-form-item label="转介人" required>
<el-select v-model="createFormData.referrer" placeholder="请选择转介人">
<el-option
v-for="item in referrerOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="出账金额" required>
<el-input-number
v-model="createFormData.amount"
:min="-10000000000000"
:precision="2"
placeholder="请输入出账金额"
/>
</el-form-item>
<el-form-item label="出账项目" required>
<el-input v-model="createFormData.billingItem" placeholder="请输入出账项目" />
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="createFormData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCreateForm">确认</el-button>
</template>
</el-dialog>
<!-- 出账完成弹窗 -->
<el-dialog title="出账完成" v-model="billingCompleteDialogVisible" width="400px">
</CommonPage>
<!-- 弹窗-->
<CommonDialog dialogTitle='出账完成' dialogWidth='80%' :openDialog=billingCompleteDialogVisible :showAction='false'
:showClose='true' @close='billingCompleteDialogVisible = false'>
<div style="text-align: center">
<el-icon size="48" color="#67C23A">
<SuccessFilled />
</el-icon>
<p style="margin-top: 16px; font-size: 16px">出账操作已完成!</p>
</div>
<template #footer>
<!-- <el-button type="primary" @click="generateSalarySlips">生成薪资单</el-button> -->
<el-button @click="billingCompleteDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog title="修订记录" v-model="showReviseRecord" width="1000px" append-to-body>
</CommonDialog>
<CommonDialog dialogTitle='修订记录' dialogWidth='80%' :openDialog=showReviseRecord :showAction='false'
:showClose='true' @close='showReviseRecord = false'>
<el-table :data="reviseList" border>
<el-table-column prop="createTime" label="修订时间" align="center" width="180">
<template #default="scope">
......@@ -186,107 +70,92 @@
<el-table-column label="修订内容" prop="editContent" align="center"></el-table-column>
</el-table>
<pagination
v-show="revisePages.total >= 0"
:total="revisePages.total"
v-model:page="revisePages.pageNo"
v-model:limit="revisePages.pageSize"
@pagination="getReviseData"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">关 闭</el-button>
</div>
</template>
</el-dialog>
</CommonDialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { SuccessFilled } from '@element-plus/icons-vue'
import { ref, reactive } from 'vue'
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import {
getReferrerFortuneList,
updatePolicyFortuneStatus,
salaryStatistics,
salaryEditRecords
} from '@/api/financial/commission'
import { formatCurrency } from '@/utils/number'
import { ElMessageBox, ElMessage } from 'element-plus'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'select',
prop: 'brokerBizIdList',
label: '转介人',
api: '/insurance/base/api/userSaleExpand/page',
keywordField: 'realName',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入转介人名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'userSaleBizId',
labelKey: 'realName',
multiple: true,
transform: (res) => {
return res?.data.records || []
}
},
{
type: 'daterange',
prop: 'payoutDate',
label: '出账日(实)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'select',
prop: 'statusList',
label: '出账状态',
dictType: 'csf_expected_fortune_status',
multiple: true,
defaultValue: ['6'],//默认查询待出账,已检核的数据
}
])
// 添加表格引用
const tableRef = ref()
// 存储所有选中的行数据(用于跨页保持选择)
const allSelectedRows = ref(new Map())
const isSearch = ref(false)
const statisticList = ref([
{ name: '总金额', value: '0', key: 'totalAmount', format: true },
{ name: '总人数', value: '0', key: 'brokerCount', format: false }
])
// 查询参数
const queryParams = reactive({
broker: '',
accountDate: [],
accountDateStart: '',
accountDateEnd: '',
pageNo: 1,
pageSize: 10,
sortField: '',
sortOrder: 'desc'
const statisticInfo = ref({
totalAmount: 0,
brokerCount: 0
})
// 表格数据
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const selectedRows = ref([])
const showReviseRecord = ref(false)
const reviseList = ref([])
const currencyTypeOptions = ref([])
const revisePages = reactive({
total: 0,
pageNo: 1,
pageSize: 5
})
// 对话框相关
const createDialogVisible = ref(false)
const billingCompleteDialogVisible = ref(false)
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
// 新建表单数据
const createFormData = reactive({
referrer: '',
amount: 0,
billingItem: '',
remark: ''
})
// 转介人选项
const referrerOptions = [
{ label: '张三', value: 'zhangsan' },
{ label: '李四', value: 'lisi' },
{ label: '王五', value: 'wangwu' },
{ label: '赵六', value: 'zhaoliu' }
]
const currentRow = ref({})
const getReviseData = () => {
let data = {
pageNo: revisePages.pageNo,
pageSize: revisePages.pageSize,
fortuneAccountBizId: currentRow.value.fortuneAccountBizId
}
salaryEditRecords(data).then(response => {
if (response.code == 200) {
reviseList.value = response.data.records
revisePages.total = response.data.total
showReviseRecord.value = true
} else {
ElMessage.error(response.msg)
showReviseRecord.value = false
revisePages.total = 0
reviseList.value = []
}
})
// 分页事件
const handleSizeChange = (val) => {
pageSize.value = val
getList()
}
const handleCurrentChange = (val) => {
currentPage.value = val
getList()
}
// 设置当前页的选中状态
const setCurrentPageSelection = () => {
if (!tableRef.value) return
......@@ -307,16 +176,6 @@ const getAllSelectedRows = () => {
}
// 更新全局选中状态
const updateAllSelectedRows = () => {
// 获取当前页的所有行的 key(fortuneAccountBizId)
const currentPageKeys = tableData.value.map(row => row.fortuneAccountBizId)
// 从全局选中中移除当前页不再存在的行
// for (const key of allSelectedRows.value.keys()) {
// if (!currentPageKeys.includes(key)) {
// allSelectedRows.value.delete(key)
// }
// }
// 更新当前页的选中状态
tableData.value.forEach(row => {
const isSelected = selectedRows.value.some(
......@@ -341,9 +200,7 @@ const getStatistics = async () => {
fortuneAccountIdList: fortuneAccountIdList
})
if (res.data) {
statisticList.value.forEach(item => {
item.value = res.data[item.key]
})
statisticInfo.value = res.data
}
} else {
isSearch.value = false
......@@ -361,40 +218,29 @@ const clearAllSelection = () => {
tableRef.value.clearSelection()
}
}
const closeDialog = () => {
showReviseRecord.value = false
reviseList.value = []
revisePages.value = {
pageNo: 1,
pageSize: 5,
total: 0
}
}
// 初始化
onMounted(() => {
getDictLists()
getList()
})
// 获取数据列表
const getList = async () => {
const getList = async (searchParams = {}) => {
loading.value = true
try {
if (queryParams.accountDate.length > 0) {
queryParams.accountDateStart = queryParams.accountDate[0]
queryParams.accountDateEnd = queryParams.accountDate[1]
const params = {
...searchParams,
accountDateStart: searchParams.payoutDate?.[0] || undefined,
accountDateEnd: searchParams.payoutDate?.[1] || undefined,
payoutDate:undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await getReferrerFortuneList(queryParams)
const response = await getReferrerFortuneList(params)
if (response.data.page) {
tableData.value = response.data.page.records
total.value = response.data.page.total
pageTotal.value = response.data.page.total
loading.value = false
}
if (response.data.statisticsVO && isSearch.value) {
statisticList.value.forEach(item => {
item.value = response.data.statisticsVO[item.key]
})
if (response.data.statisticsVO) {
statisticInfo.value = response.data.statisticsVO
}
// 数据加载完成后,设置当前页的选中状态
nextTick(() => {
......@@ -405,6 +251,7 @@ const getList = async () => {
// ElMessage.error('获取数据失败')
}
}
getList()
const getStatusType = status => {
const types = {
......@@ -417,34 +264,19 @@ const getStatusType = status => {
// 查询
const handleQuery = () => {
queryParams.pageNo = 1
Object.values(queryParams).some(value => {
if (Array.isArray(value) && value.length > 0) {
isSearch.value = true
}
if (value !== '' && value != null) {
isSearch.value = true
}
})
const params = searchFormRef.value.getFormData()
console.log('父组件发起查询:', params)
clearAllSelection()
getList()
getList(params)
}
// 重置查询
const resetQuery = () => {
Object.assign(queryParams, {
broker: '',
accountDateStart: '',
accountDateEnd: '',
pageNo: 1,
pageSize: 10,
sortField: '',
sortOrder: 'desc'
})
isSearch.value = false
// 清空选择
clearAllSelection()
getList()
const handleReset = () => {
// 重置搜索表单
searchFormRef.value.resetForm()
searchParams.value = {}
console.log('表单已重置')
getList(searchParams.value)
}
// 选择行变化
......@@ -454,55 +286,7 @@ const handleSelectionChange = selection => {
updateAllSelectedRows()
}
// 新建薪资记录
const handleCreate = () => {
Object.assign(createFormData, {
referrer: '',
amount: 0,
billingItem: '',
remark: ''
})
createDialogVisible.value = true
}
// 编辑薪资记录
const handleEdit = row => {
// 这里可以打开编辑对话框
ElMessage.info(`编辑转介人:${row.referrer}`)
}
const handleRevise = row => {
currentRow.value = row
getReviseData()
}
const deletePolicyFortuneO = async fortuneAccountBizId => {
try {
await deletePolicyFortune({ fortuneAccountBizId })
} catch (error) {
ElMessage.error('删除出账失败')
}
}
// 删除薪资记录
const handleDelete = async row => {
try {
await ElMessageBox.confirm('确认删除这条薪资记录吗?', '提示', {
type: 'warning'
})
await deletePolicyFortuneO(row.fortuneAccountBizId)
ElMessage.success('删除成功')
getList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 批量导入
const handleImport = () => {
ElMessage.info('批量导入功能待实现')
}
// 完成出账
const completeBilling = async () => {
......@@ -529,59 +313,15 @@ const completeBilling = async () => {
}
}
// 生成薪资单(单条)
const generateSalarySlip = row => {
// 调用生成薪资单API
// await api.generateSalarySlip(row.id)
ElMessage.success(`已为 ${row.referrer} 生成薪资单`)
}
// 生成薪资单(批量)
const generateSalarySlips = () => {
// 调用批量生成薪资单API
// await api.generateSalarySlips(selectedRows.value.map(item => item.id))
ElMessage.success(`已为 ${selectedRows.value.length} 条记录生成薪资单`)
billingCompleteDialogVisible.value = false
selectedRows.value = []
}
// 提交新建表单
const submitCreateForm = async () => {
try {
// 表单验证
if (!createFormData.referrer || !createFormData.amount || !createFormData.billingItem) {
ElMessage.warning('请填写必填字段')
return
}
// 调用保存API
// await api.createSalary(createFormData)
ElMessage.success('新建成功')
createDialogVisible.value = false
getList()
} catch (error) {
ElMessage.error('新建失败')
}
}
// 格式化金额
const formatCurrency = amount => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount)
}
import { listType } from '@/api/system/dict/type'
const dictLists = ref([])
// 获取出账状态字典值
const getDictLists = () => {
return new Promise((resolve, reject) => {
listType({ typeList: ['csf_fortune_status', 'bx_currency_type'] })
listType({ typeList: ['csf_expected_fortune_status', 'bx_currency_type'] })
.then(res => {
if (res.code === 200 && res.data) {
const dictData = res.data.find(item => item.dictType === 'csf_fortune_status')
const dictData = res.data.find(item => item.dictType === 'csf_expected_fortune_status')
dictLists.value = dictData?.dictItemList || []
console.log('获取到的字典数据:', dictLists.value)
// 处理币种字典值
......@@ -607,6 +347,7 @@ const getDictLists = () => {
})
})
}
getDictLists()
// 返回数据中状态需要转换为字典值
const convertStatusToDict = status => {
......@@ -614,24 +355,6 @@ const convertStatusToDict = status => {
return dictItem?.itemLabel ?? status
}
// 更新出账状态
const updateStatus = async row => {
try {
const res = await updatePolicyFortuneStatus({
fortuneBizIdList: row,
status: 2
})
console.log(res)
if (res.code === 200) {
// 显示完成弹窗
billingCompleteDialogVisible.value = true
getList()
} else {
ElMessage.error(res.msg)
}
} catch (error) {}
}
// 完成出账
import { completePolicyFortune } from '@/api/financial/commission'
const fetchCompletePolicyFortune = async row => {
......@@ -647,71 +370,34 @@ const fetchCompletePolicyFortune = async row => {
} else {
ElMessage.error(res.msg)
}
} catch (error) {}
}
// 删除出账
import { deletePolicyFortune } from '@/api/financial/commission'
// 删除出账
const deleteFortune = async row => {
try {
const res = await deletePolicyFortune({
fortuneAccountBizId: row
})
console.log(res)
if (res.code === 200) {
// 显示完成弹窗
billingCompleteDialogVisible.value = true
} else {
ElMessage.error(res.msg)
}
} catch (error) {}
}
</script>
<style scoped>
.totalData {
margin-bottom: 10px;
font-size: 14px;
color: #000;
}
.financial-salary-page {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.operation-card {
margin-bottom: 20px;
}
.el-table {
margin-bottom: 20px;
}
.el-pagination {
justify-content: flex-end;
margin-top: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.financial-salary-page {
padding: 10px;
}
.search-card {
margin-bottom: 15px;
} catch (error) { }
}
const visibleDefaultButtons = ref(['reset', 'query'])
// 按钮配置
const operationBtnList = ref([
{
key: 'reset',
direction: 'right',
click: handleReset
},
{
key: 'query',
direction: 'right',
click: handleQuery
},
{
key: 'custom',
label: '完成出账',
icon: 'Check',
type: 'warning',
direction: 'right',
size: 'default',
click: completeBilling
}
])
.operation-card {
margin-bottom: 15px;
}
</script>
.el-col {
margin-bottom: 10px;
}
}
</style>
<style scoped></style>
\ No newline at end of file
<template>
<CommonPage
:operationBtnList="operationBtnList"
:showSearchForm="true"
:show-pagination="true"
:total="pageTotal"
:current-page="currentPage"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<div>
<CommonPage :operationBtnList="operationBtnList" :showSearchForm="true" :show-pagination="true" :total="pageTotal"
:current-page="currentPage" :page-size="pageSize" @size-change="handleSizeChange"
@current-change="handleCurrentChange">
<!-- 搜索区域 -->
<template #searchForm>
<SearchForm
ref="searchFormRef"
v-model="searchFormData"
:fields="searchFields"
label-position="top"
:label-width="null"
:inline="false"
:gutter="20"
class="custom-search-form"
/>
<SearchForm ref="searchFormRef" :config="searchConfig" />
</template>
<!-- 列表区域 -->
<template #table>
<!-- 应付款管理列表 -->
<el-table
:data="tableData"
height="400"
border
highlight-current-row
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="commissionBizType" label="应付款类型" width="120" fixed="left" sortable />
<el-table-column prop="payableNo" label="应付款编号" width="120" />
<el-table-column prop="policyNo" label="保单号" width="120" />
<el-table-column prop="status" label="出账状态" width="120" sortable>
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'warning'">
{{ row.status === '1' ? '已入账' : '待入账' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="commissionPeriod" label="出账期数" width="120" sortable />
<el-table-column prop="totalPeriod" label="出账总期数" width="120" sortable />
<el-table-column prop="commissionType" label="出账项目" width="120" sortable />
<el-table-column prop="paymentDate" label="出账日(估)" width="120" sortable />
<el-table-column prop="commissionRatio" label="出账比例(估)" width="140" sortable>
<template #default="{ row }">
{{ (row.commissionRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="expectedAmount" label="出账金额(估)" width="140" sortable>
<template #default="{ row }">
{{ formatCurrency(row.expectedAmount) }}
</template>
</el-table-column>
<el-table-column prop="paidAmountRatio" label="已出账比例" width="120" sortable>
<template #default="{ row }">
{{ (row.paidAmountRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="paidAmount" label="已出账金额" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.paidAmount) }}
</template>
</el-table-column>
<el-table-column prop="pendingRatio" label="待出账比例" width="120" sortable>
<template #default="{ row }">
{{ (row.pendingRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="pendingPaidAmount" label="待出账金额(估)" width="160" sortable>
<template #default="{ row }">
{{ formatCurrency(row.pendingPaidAmount) }}
</template>
</el-table-column>
<el-table-column prop="insurerBizId" label="保险公司" width="120" sortable />
<el-table-column prop="productLaunchBizId" label="产品计划" width="120" sortable />
<el-table-column prop="premium" label="期交保费" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.premium) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="150" />
</el-table>
</template>
</CommonPage>
<!-- 统计信息卡片 -->
<div class="statistics-cards" v-if="statisticsData.totalPolicyCount > 0">
<div class="statistics-container" v-if="statisticsData.totalPolicyCount > 0">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="4" :lg="4">
<el-card shadow="hover" class="statistics-card">
......@@ -142,178 +62,439 @@
</el-col>
</el-row>
</div>
<!-- 应付款管理列表 -->
<el-table :data="tableData" height="400" border highlight-current-row style="width: 100%" v-loading="loading">
<el-table-column prop="fortuneBizType" label="应付款类型" width="120" fixed="left" sortable>
<template #default="{ row }">
{{ getFortuneBizTypeLabel(row.fortuneBizType) }}
</template>
</el-table-column>
<el-table-column prop="payableNo" label="应付款编号" width="120" sortable />
<el-table-column prop="policyNo" label="保单号" width="120" sortable />
<el-table-column prop="broker" label="转介人" width="120" sortable />
<el-table-column prop="status" label="出账状态" width="120" sortable>
<template #default="{ row }">
{{ getDictLabel('csf_expected_fortune_status', row.status) }}
</template>
</el-table-column>
<el-table-column prop="currency" label="出账币种" width="120" sortable />
<el-table-column prop="fortunePeriod" label="出账期数" width="120" sortable />
<el-table-column prop="fortuneTotalPeriod" label="出账总期数" width="120" sortable />
<el-table-column prop="fortuneName" label="出账项目" width="120" sortable />
<el-table-column prop="payoutDate" label="出账日(估)" width="120" sortable />
<el-table-column prop="actualPayoutDate" label="出账日(实)" width="120" sortable />
<el-table-column prop="commissionRatio" label="出账比例(估)" width="140" sortable>
<template #default="{ row }">
{{ (row.commissionRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="amount" label="出账金额(估)" width="140" sortable>
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
</template>
</el-table-column>
<el-table-column prop="paidRatio" label="已出账比例" width="120" sortable>
<template #default="{ row }">
{{ (row.paidRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="paidAmount" label="已出账金额" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.paidAmount) }}
</template>
</el-table-column>
<el-table-column prop="pendingRatio" label="待出账比例" width="120" sortable>
<template #default="{ row }">
{{ (row.pendingRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="unpaidAmount" label="待出账金额(估)" width="160" sortable>
<template #default="{ row }">
{{ formatCurrency(row.unpaidAmount) }}
</template>
</el-table-column>
<el-table-column prop="broker_ratio" label="持有比例" width="120" sortable />
<el-table-column prop="insurerBizId" label="保险公司" width="120" sortable />
<el-table-column prop="productLaunchBizId" label="产品计划" width="120" sortable />
<el-table-column prop="premium" label="期交保费" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.premium) }}
</template>
</el-table-column>
<el-table-column prop="statusDesc" label="修改理由" width="150" />
<el-table-column prop="remark" label="备注" width="150" />
<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 dropdownItems" :key="item.value">{{
item.label
}}</el-menu-item>
</el-menu>
</el-popover>
</template>
</el-table-column>
</el-table>
</template>
</CommonPage>
<!-- 出账记录表格弹窗-->
<CommonDialog dialogTitle="出账记录" dialogWidth="80%" :openDialog="payRecordDialogTableVisible" :showAction="true"
:showClose="true" @close="payRecordDialogTableVisible = false">
<el-table :data="payRecordDialogTableData" border style="width: 100%">
<el-table-column v-for="item in payRecordDialogTableColumns" :key="item.property" :property="item.property"
:label="item.label" :width="item.width" />
</el-table>
</CommonDialog>
<!-- 新增出账记录 -->
<CommonDialog :dialogTitle="editStatus === 'add' ? '新增出账记录' : '修改出账记录'" dialogWidth="80%"
:openDialog="addPayRecordDialogVisible" :showAction="true" :showClose="true"
@close="addPayRecordDialogVisible = false; editStatus = 'add'" @confirm="handleConfirmAddPayRecord">
<SearchForm ref="addPayRecordFormRef" :config="addPayRecordFormConfig" v-model="addPayRecordFormModel" />
</CommonDialog>
<!-- 设置出账状态 -->
<CommonDialog dialogTitle="设置出账状态" dialogWidth="80%" :openDialog="setPayRecordStatusDialogVisible"
:showAction="false" :showClose="true" @close="setPayRecordStatusDialogVisible = false"
@confirm="handleConfirmSetPayRecordStatus">
<SearchForm ref="setPayRecordStatusFormRef" :config="setPayRecordStatusFormConfig" />
</CommonDialog>
</div>
</template>
<script setup name="Payables">
import CommonPage from '@/components/commonPage'
import { ref, reactive, onMounted, watch } from 'vue'
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatCurrency } from '@/utils/number'
import { expectedFortuneList } from '@/api/financial/commission'
import SearchForm from '@/components/SearchForm/index.vue'
import { searchCompanies, searchCommissionTypes } from '@/api/search'
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
// 搜索表单数据 - 修正字段名
const searchFormData = reactive({
policyNo: '',
incomeDateRange: [], // 改为数组格式
statusList: [],
fortuneName: [], // 修改字段名
fortunePeriod: '',
insurerBizId: [],
productLaunchBizId: [],
commissionBizType: '',
teamBizId: '',
})
import { expectedFortuneList, payRecordList, addPayRecord, updatePayRecord } from '@/api/financial/commission'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
import CommonDialog from '@/components/commonDialog'
import { loadDicts, getDictLabel } from '@/utils/useDict'
import useUserStore from '@/store/modules/user'
// 设置出账状态
const setPayRecordStatusDialogVisible = ref(false)
const selectedRow = ref(null)
const setPayRecordStatusFormRef = ref(null)
const setPayRecordStatusFormConfig = ref([
{
type: 'select',
prop: 'status',
label: '出账状态',
dictType: 'csf_expected_fortune_status'
}, {
type: 'textarea',
prop: 'statusDesc',
label: '修改理由',
},
])
const searchFields = ref([
const userStore = useUserStore()
// 应收单类型
const fortuneBizTypeOptions = [
{ value: 'R', label: '关联保单应付单' },
{ value: 'U', label: '非关联保单应付单' }
]
// 应付单类型通过value转成label
const getFortuneBizTypeLabel = (value) => {
const item = fortuneBizTypeOptions.find(item => item.value === value)
return item?.label || ''
}
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'input',
field: 'policyNo',
label: '保单号',
placeholder: '请输入保单号',
colSpan: 6,
clearable: true,
rules: [
{ max: 50, message: '保单号长度不能超过50个字符', trigger: 'blur' }
]
prop: 'policyNo',
label: '保单号'
},
{
type: 'daterange',
field: 'incomeDateRange',
label: '入账日期',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
colSpan: 6,
dateFormat: 'YYYY-MM-DD',
props: {
valueFormat: 'YYYY-MM-DD',
style: 'width: 100%'
}
prop: 'payoutDate',
label: '出账日(估)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'multi-select',
field: 'statusList',
label: '入账状态',
placeholder: '请选择入账状态',
colSpan: 6,
options: [
{ label: '已入账', value: '1' },
{ label: '待入账', value: '0' },
{ label: '部分入账', value: '2' }
]
type: 'select',
prop: 'statusList',
label: '出账状态',
multiple: true,
dictType: 'csf_expected_fortune_status'
},
{
type: 'remote-multi-select',
field: 'commissionNameList',
label: '入账项目',
placeholder: '请输入关键词搜索',
colSpan: 6,
remoteConfig: {
type: 'commissionType',
apiMethod: searchCommissionTypes,
formatResult: (data) => data.map(item => ({
label: item.typeName,
value: item.typeCode,
remark: item.remark
})),
defaultOptions: [
{ label: '佣金', value: 'COMMISSION' },
{ label: '服务费', value: 'SERVICE_FEE' }
type: 'input',
prop: 'fortunePeriod',
label: '出账期数',
inputType: 'decimal',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}
},
{
type: 'remote-multi-select',
field: 'reconciliationCompanyList',
label: '对账公司',
placeholder: '请输入关键词搜索',
colSpan: 6,
remoteConfig: {
type: 'company',
apiMethod: (params) => searchCompanies({ ...params, type: 'reconciliation' }),
defaultOptions: []
}
},
{
type: 'input',
field: 'commissionPeriod',
label: '入账期数',
placeholder: '请输入期数',
colSpan: 6,
type: 'select',
prop: 'fortuneName',
label: '出账项目',
dictType: 'csf_fortune_type'
},
{
type: 'remote-multi-select',
field: 'insurerBizId',
type: 'select',
prop: 'insurerBizId',
label: '保险公司',
placeholder: '请输入关键词搜索保险公司',
colSpan: 6,
remoteConfig: {
type: 'insurer',
apiMethod: (params) => searchCompanies({ ...params, type: 'insurer' }),
defaultOptions: []
api: '/insurance/base/api/insuranceCompany/page',
keywordField: 'queryContent',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入保险公司名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'insuranceCompanyBizId',
labelKey: 'abbreviation',
transform: (res) => {
console.log(res)
return res?.data.records || []
}
},
{
type: 'remote-multi-select',
field: 'productLaunchBizId',
}, {
type: 'select',
prop: 'productLaunchBizId',
label: '产品计划',
placeholder: '请输入关键词搜索产品计划',
colSpan: 6,
remoteConfig: {
type: 'product',
apiMethod: (params) => searchCompanies({ ...params, type: 'product' }),
defaultOptions: []
api: '/product/api/relProjectProductLaunch/parameter/page',
keywordField: 'productName',
requestParams: {
tenantBizId: userStore.projectInfo.tenantBizId || '',
projectBizId: userStore.projectInfo.projectBizId || '', fieldBizId: 'field_olk1qZe81qHHKXbw', fieldValueBizId: 'field_value_uOfJH5ucA2YwJpbn', pageNo: 1, pageSize: 20
},
placeholder: '输入产品计划名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'productLaunchBizId',
labelKey: 'productName',
transform: (res) => {
return res?.data.records || []
}
}, {
type: 'select',
prop: 'fortuneBizType',
label: '应付单类型',
options: [
{ value: 'R', label: '关联保单应付单' },
{ value: 'U', label: '非关联保单应付单' }
]
}, {
type: 'select',
prop: 'teamBizId',
label: '出单团队',
api: '/csf/api/team/page',
keywordField: 'teamName',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入出单团队名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'teamBizId',
labelKey: 'teamName',
transform: (res) => {
return res?.data.records || []
}
},
])
const payRecordDialogTableVisible = ref(false)
// 新增出账记录
const addPayRecordFormModel = ref({
fortuneBizType: 'U',
})
const addPayRecordDialogVisible = ref(false)
const addPayRecordFormRef = ref()
const addPayRecordFormConfig = [
{
type: 'select',
field: 'commissionBizType',
label: '应收款类型',
placeholder: '请选择应收款类型',
colSpan: 6,
prop: 'fortuneBizType',
label: '应付单类型',
options: [
{ label: '全部', value: '' },
{ label: '关联保单应收单', value: '1' },
{ label: '非关联保单应收单', value: '2' }
{ value: 'R', label: '关联保单应付单' },
{ value: 'U', label: '非关联保单应付单' }
]
}, {
type: 'input',
prop: 'policyNo',
label: '关联保单号',
visible: (formData) => formData.fortuneBizType === 'R',
}, {
type: 'input',
prop: 'fortunePeriod',
label: '佣金期数',
inputType: 'decimal',
visible: (formData) => formData.fortuneBizType === 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'input',
prop: 'fortuneTotalPeriod',
label: '总期数',
inputType: 'decimal',
visible: (formData) => formData.fortuneBizType === 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'date',
prop: 'payoutDate',
label: '出账日(估)',
placeholder: '请选择'
}, {
type: 'date',
prop: 'actualPayoutDate',
label: '出账日(实)',
placeholder: '请选择',
maxDate: 'today'
}, {
type: 'input',
prop: 'amount',
label: '出账金额',
inputType: 'decimal',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'select',
prop: 'currency',
label: '出账币种',
dictType: 'bx_currency_type'
}, {
type: 'select',
prop: 'fortuneType',
label: '出账项目',
dictType: 'csf_fortune_type'
}, {
type: 'select',
prop: 'brokerBizId',
label: '转介人',
api: '/insurance/base/api/userSaleExpand/page',
keywordField: 'realName',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入转介人名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'userSaleBizId',
labelKey: 'realName',
onChangeExtraFields: {
broker: 'realName',// 自动同步 raw.name 到 reconciliationCompany
reconciliationCompanyCode: 'code'
},
transform: (res) => {
return res?.data.records || []
}
}, {
type: 'select',
prop: 'status',
label: '出账状态',
dictType: 'csf_expected_fortune_status'
}
]
const handleConfirmAddPayRecord = async () => {
if (editStatus.value === 'add') {
const handleConfirmAddPayRecordparams = addPayRecordFormRef.value.getFormData()
try {
await addPayRecord([handleConfirmAddPayRecordparams])
ElMessage.success('新增出账记录成功')
addPayRecordDialogVisible.value = false
addPayRecordFormRef.value.resetForm()
} catch (error) {
ElMessage.error(error.message)
}
} else if (editStatus.value === 'edit') {
try {
const params = {
expectedFortuneBizId: selectedRow.value.expectedFortuneBizId,
...addPayRecordFormModel.value
}
await updatePayRecord(params)
ElMessage.success('更新出账记录成功')
addPayRecordDialogVisible.value = false
addPayRecordFormRef.value.resetForm()
editStatus.value = 'add'
} catch (error) {
ElMessage.error(error.message)
}
}
}
const editStatus = ref('add')
// 出账记录
const payRecordDialogTableData = ref([])
const payRecordDialogTableColumns = ref([])
const handleSelect = async (e, row) => {
selectedRow.value = { ...row }
if (e === 'payRecord') {
payRecordDialogTableVisible.value = true
loadPayRecordTableData(selectedRow.value.expectedFortuneBizId);
payRecordDialogTableColumns.value = [
{ property: 'broker', label: '转介人', width: '100', },
{ property: 'fortuneName', label: '出账项目', width: '150' },
{ property: 'amount', label: '出账金额', width: '150' },
{ property: 'currency', label: '出账币种', width: '150' },
{ property: 'amount', label: '出账比例', width: '150' },
{ property: 'currentCommissionRatio', label: '待出账比例', width: '150' },
{ property: 'fortunePeriod', label: '佣金期数', width: '150' },
{ property: 'fortuneTotalPeriod', label: '总期数', width: '150' },
{ property: 'updaterId', label: '操作人', width: '150' },
{ property: 'updateTime', label: '操作时间', width: '150' }
]
} else if (e === 'setStatus') {
setPayRecordStatusDialogVisible.value = true
setPayRecordStatusFormConfig.value = [
{
type: 'remote-multi-select',
field: 'teamBizId',
label: '出单团队',
placeholder: '请输入关键词搜索出单团队',
colSpan: 6,
remoteConfig: {
type: 'team',
apiMethod: (params) => searchCompanies({ ...params, type: 'team' }),
defaultOptions: []
type: 'select',
prop: 'status',
label: '出账状态',
dictType: 'csf_expected_fortune_status',
defaultValue: selectedRow.value.status || ''
}, {
type: 'textarea',
prop: 'statusDesc',
label: '修改理由',
defaultValue: selectedRow.value.statusDesc || ''
},
]
} else if (e === 'updateData') {
editStatus.value = 'edit'
addPayRecordDialogVisible.value = true
if (addPayRecordFormRef.value) {
addPayRecordFormModel.value = {
...selectedRow.value
}
}
])
console.log(addPayRecordFormModel.value)
}
}
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
// 表格操作菜单
const dropdownItems = [
{ label: '出账记录', value: 'payRecord' },
{ label: '设置状态', value: 'setStatus' },
{ label: '更新数据', value: 'updateData' }
]
// 表格数据
const tableData = ref([])
// 统计信息
const statisticsData = ref({
totalExpectedAmount: 0,
totalPaidAmount: 0,
totalUnpaidAmount: 0,
paidAmountRatio: 0,
totalPolicyCount: 0,
totalPremiumAmount: 0
})
const statisticsData = ref({})
// 按钮事件处理
const handleAdd = () => {
ElMessage.info('点击新增按钮')
// ElMessage.info('点击新增按钮')
addPayRecordDialogVisible.value = true
}
const handleImport = () => {
......@@ -326,28 +507,17 @@ const handleExport = () => {
const handleReset = () => {
// 重置搜索表单
Object.keys(searchFormData).forEach(key => {
if (Array.isArray(searchFormData[key])) {
searchFormData[key] = []
} else {
searchFormData[key] = ''
}
})
ElMessage.success('搜索条件已重置')
searchFormRef.value.resetForm()
searchParams.value = {}
console.log('表单已重置')
// 重新加载数据
loadTableData()
}
const handleQuery = async () => {
// 表单验证
// const valid = await proxy.$refs.searchFormRef.validate()
// if (!valid) return
ElMessage.info('执行查询操作')
loadTableData()
}
// 控制要显示的默认按钮
// const visibleDefaultButtons = ref(['add', 'import', 'export']) // 只显示新增和查询两个默认按钮
// 按钮配置
const operationBtnList = ref([
{
......@@ -390,11 +560,15 @@ const handleCurrentChange = (val) => {
// 加载表格数据
const loadTableData = async () => {
const searchParams = searchFormRef.value.getFormData() || {}
loading.value = true
try {
const params = {
...searchFormData,
currentPage: currentPage.value,
...searchParams,
payoutDateStart: searchParams.payoutDate?.[0] || undefined,
payoutDateEnd: searchParams.payoutDate?.[1] || undefined,
payoutDate: undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await expectedFortuneList(params)
......@@ -404,28 +578,80 @@ const loadTableData = async () => {
// 统计信息
statisticsData.value = {
totalExpectedAmount: response.data.expectedStatisticsVO.totalExpectedAmount,
totalPaidAmount: response.data.expectedStatisticsVO.totalPaidAmount,
totalUnpaidAmount: response.data.expectedStatisticsVO.totalUnpaidAmount,
paidAmountRatio: response.data.expectedStatisticsVO.paidAmountRatio,
totalPolicyCount: response.data.expectedStatisticsVO.totalPolicyCount,
totalPremiumAmount: response.data.expectedStatisticsVO.totalPremiumAmount
totalExpectedAmount: response.data.statisticsVO.totalExpectedAmount,
totalPaidAmount: response.data.statisticsVO.totalPaidAmount,
totalUnpaidAmount: response.data.statisticsVO.totalUnpaidAmount,
paidAmountRatio: response.data.statisticsVO.paidAmountRatio,
totalPolicyCount: response.data.statisticsVO.totalPolicyCount,
totalPremiumAmount: response.data.statisticsVO.totalPremiumAmount
}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
// ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 出账记录查询
const loadPayRecordTableData = async (expectedFortuneBizId) => {
loading.value = true
try {
const params = {
expectedFortuneBizId: expectedFortuneBizId
}
const response = await payRecordList(params)
payRecordDialogTableData.value = response.data.records
} catch (error) {
console.error('加载数据失败:', error)
// ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 初始化加载数据
onMounted(() => {
// 设置出账状态确认
const handleConfirmSetPayRecordStatus = async () => {
const formData = setPayRecordStatusFormRef.value.getFormData() || {}
if (!formData.status) {
ElMessage.error('请选择出账状态')
return
}
if (!formData.statusDesc) {
ElMessage.error('请输入修改理由')
return
}
try {
const params = {
...formData,
fortuneBizIdList: [selectedRow.value.expectedFortuneBizId]
}
await updataPayrollStatus(params)
ElMessage.success('设置出账状态成功')
setPayRecordStatusDialogVisible.value = false
loadTableData()
} catch (error) {
console.error('设置出账状态失败:', error)
ElMessage.error('设置出账状态失败')
}
}
// 获取入账状态,字典值转化方法
onMounted(async () => {
try {
await loadDicts(['csf_expected_fortune_status'])
} catch (error) {
console.error('字典加载失败', error)
} finally {
loading.value = false
}
})
// 格式化函数(每次渲染都会调用,所以能拿到最新字典)
const formatStatus = (row, column) => {
return getDictLabel('csf_expected_fortune_status', row.status) // 实时查缓存
}
</script>
<style scoped lang="scss">
......@@ -442,5 +668,4 @@ onMounted(() => {
border-top: 1px solid #ebeef5;
text-align: right;
}
</style>
\ No newline at end of file
statistics-cards<template>
<CommonPage :operationBtnList="operationBtnList" :showSearchForm="true" :show-pagination="true" :total="pageTotal"
:current-page="currentPage" :page-size="pageSize" @size-change="handleSizeChange"
@current-change="handleCurrentChange">
<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" v-model="searchFormData" :fields="searchFields" label-position="top"
:label-width="null" :inline="false" :gutter="20" class="custom-search-form" />
<SearchForm ref="searchFormRef" :config="searchConfig" />
</template>
<!-- 列表区域 -->
<template #table>
<!-- 统计信息卡片 -->
<div class="statistics-cards" v-if="statisticsData.totalPolicyCount > 0">
<div class="statistics-container" v-if="statisticsData.totalPolicyCount > 0">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
<el-statistic title="应收款总金额" :value="statisticsData.totalAmount" />
......@@ -29,23 +29,20 @@ statistics-cards<template>
</el-col>
</el-row>
</div>
<!-- 应收款管理列表 -->
<el-table :data="tableData" height="400" border highlight-current-row style="width: 100%"
v-loading="loading">
<el-table-column prop="commissionBizType" label="应收款类型" width="120" fixed="left" sortable />
<el-table-column prop="receivableNo" label="应收款编号" width="120" />
<el-table-column prop="policyNo" label="保单号" width="120" />
<el-table-column prop="reconciliationCompany" label="对账公司" width="120" sortable />
<el-table-column prop="status" label="入账状态" width="120" sortable>
<el-table-column prop="commissionBizType" label="应收单类型" width="130" sortable>
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'warning'">
{{ row.status === '1' ? '已入账' : '待入账' }}
</el-tag>
{{ getCommissionBizTypeLabel(row.commissionBizType) }}
</template>
</el-table-column>
<el-table-column prop="receivableNo" label="应收款编号" width="150" sortable />
<el-table-column prop="policyNo" label="保单号" width="120" fixed="left" sortable />
<el-table-column prop="reconciliationCompany" label="对账公司" width="120" sortable />
<el-table-column prop="status" label="入账状态" width="120" sortable :formatter="formatStatus" />
<el-table-column prop="commissionPeriod" label="入账期数" width="120" sortable />
<el-table-column prop="totalPeriod" label="入账总期数" width="120" sortable />
<el-table-column prop="commissionType" label="入账项目" width="120" sortable />
<el-table-column prop="commissionName" label="入账项目" width="120" sortable />
<el-table-column prop="commissionDate" label="入账日(估)" width="120" sortable />
<el-table-column prop="commissionRatio" label="入账比例(估)" width="140" sortable>
<template #default="{ row }">
......@@ -54,17 +51,17 @@ statistics-cards<template>
</el-table-column>
<el-table-column prop="expectedAmount" label="入账金额(估)" width="140" sortable>
<template #default="{ row }">
{{ formatCurrency(row.expectedAmount) }}
{{ numberWithCommas(row.expectedAmount) }}
</template>
</el-table-column>
<el-table-column prop="paidAmountRatio" label="已入账比例" width="120" sortable>
<el-table-column prop="paidRatio" label="已入账比例" width="120" sortable>
<template #default="{ row }">
{{ (row.paidAmountRatio || 0) + '%' }}
{{ (row.paidRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="paidAmount" label="已入账金额" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.paidAmount) }}
{{ numberWithCommas(row.paidAmount) }}
</template>
</el-table-column>
<el-table-column prop="pendingRatio" label="待入账比例" width="120" sortable>
......@@ -74,17 +71,19 @@ statistics-cards<template>
</el-table-column>
<el-table-column prop="pendingPaidAmount" label="待入账金额(估)" width="160" sortable>
<template #default="{ row }">
{{ formatCurrency(row.pendingPaidAmount) }}
{{ numberWithCommas(row.pendingPaidAmount) }}
</template>
</el-table-column>
<el-table-column prop="currency" label="入账币种" width="100" />
<el-table-column prop="defaultExchangeRate" label="结算汇率(估)" width="120" />
<el-table-column prop="insurerBizId" label="保险公司" width="120" sortable />
<el-table-column prop="productLaunchBizId" label="产品计划" width="120" sortable />
<el-table-column prop="insuranceCompany" label="保险公司" width="120" sortable />
<el-table-column prop="productName" label="产品计划" width="120" sortable />
<el-table-column prop="premium" label="期交保费" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.premium) }}
{{ numberWithCommas(row.premium) }}
</template>
</el-table-column>
<el-table-column prop="statusDesc" label="入账状态修改理由" width="150" />
<el-table-column prop="remark" label="备注" width="150" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="{ row }">
......@@ -95,9 +94,9 @@ statistics-cards<template>
</el-icon>
</template>
<el-menu @select="handleSelect($event, row)" popper-class="custom-menu">
<el-menu-item :index="item.value" v-for="item in dropdownItems" :key="item.value">{{
item.label
}}</el-menu-item>
<el-menu-item :index="item.value" v-for="item in dropdownItems" :key="item.value">
{{ item.label }}
</el-menu-item>
</el-menu>
</el-popover>
</template>
......@@ -105,11 +104,13 @@ statistics-cards<template>
</el-table>
</template>
</CommonPage>
<!-- 比对状态表格弹窗-->
<CommonDialog dialogTitle="入账记录" dialogWidth="80%" :openDialog="entryRecordDialogTableVisible" :showAction="false" :showClose="true" @close="entryRecordDialogTableVisible=false">
<!-- 原有弹窗(不变) -->
<CommonDialog dialogTitle="入账记录" dialogWidth="80%" :openDialog="entryRecordDialogTableVisible"
:showAction="false" :showClose="true" @close="entryRecordDialogTableVisible = false">
<el-table :data="entryRecordDialogTableData" border style="width: 100%">
<el-table-column v-for="item in entryRecordDialogTableColumns" :key="item.property"
:property="item.property" :label="item.label" :width="item.width" />
:prop="item.property" :label="item.label" :width="item.width" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" size="small" @click="handleClick">
......@@ -120,252 +121,351 @@ statistics-cards<template>
</el-table>
</CommonDialog>
<!-- 操作记录表格弹窗-->
<CommonDialog dialogTitle="操作记录" dialogWidth="80%" :openDialog="actionRecordsDialogVisible" :showAction="false" :showClose="true" @close="actionRecordsDialogVisible=false">
<CommonDialog dialogTitle="操作记录" dialogWidth="80%" :openDialog="actionRecordsDialogVisible" :showAction="false"
:showClose="true" @close="actionRecordsDialogVisible = false">
<el-table :data="actionRecordsDialogTableData" border style="width: 100%">
<el-table-column v-for="item in actionRecordsDialogTableColumns" :key="item.property"
:property="item.property" :label="item.label" :width="item.width" />
:prop="item.property" :label="item.label" :width="item.width" />
</el-table>
</CommonDialog>
<!-- 设置状态 弹窗-->
<CommonDialog dialogTitle="设置入账状态" dialogWidth="80%" :openDialog="setStatusDialogTableVisible" @close="setStatusDialogTableVisible=false" @confirm="setStatusDialogTableVisible=false">
<el-form :model="form">
<el-form-item label="入账状态" label-width="120">
<el-select v-model="form.status" placeholder="请选择入账状态">
<el-option label="Zone No.1" value="shanghai" />
<el-option label="Zone No.2" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="修改理由" :label-width="120">
<el-input v-model="form.desc" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="setStatusDialogTableVisible = false">取消</el-button>
<el-button type="primary" @click="setStatusDialogTableVisible = false">
确认
</el-button>
</div>
</template>
<CommonDialog dialogTitle="设置入账状态" dialogWidth="80%" :openDialog="setStatusDialogTableVisible"
@close="setStatusDialogTableVisible = false" @confirm="handleConfirmSetStatus">
<SearchForm ref="setCommissionRecordStatusFormRef" :config="setCommissionRecordStatusFormConfig" />
</CommonDialog>
<!-- 新增应收款管理 -->
<CommonDialog :dialogTitle="editStatus === 'add' ? '新增应收款' : '更新应收款'" dialogWidth="80%"
:openDialog="addReceivablesDialogVisible"
@close="addReceivablesDialogVisible = false; resetAddReceivablesForm()"
@confirm="handleConfirmAddReceivables">
<SearchForm ref="addRecordRef" :config="addReceivablesFormConfig" v-model="addReceivablesFormModel" />
</CommonDialog>
</div>
</template>
<script setup name="Receivables">
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import { ref, reactive, onMounted, watch } from 'vue'
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatCurrency } from '@/utils/number'
import { receivedFortuneList, commissionEntryRecord, commissionEntryEditRecords } from '@/api/financial/commission'
import SearchForm from '@/components/SearchForm/index.vue'
import { searchCompanies, searchCommissionTypes } from '@/api/search'
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
// 表格操作菜单
const dropdownItems = [
{ label: '入账记录', value: 'entryRecord' },
{ label: '设置状态', value: 'setStatus' }
import { MoreFilled } from '@element-plus/icons-vue'
import { receivedFortuneList, updateCommissionExpected, commissionEntryEditRecords, exportReceivedFortune, commissionExpectedRecord, addReceivedFortune } from '@/api/financial/commission'
import { numberWithCommas } from '@/utils/index'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
import { loadDicts, getDictLabel } from '@/utils/useDict'
import { safeDownload } from '@/utils/safeDownload'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
// 应收单类型
const commissionBizTypeOptions = [
{ value: 'R', label: '关联保单应收单' },
{ value: 'U', label: '非关联保单应收单' }
]
const entryRecordDialogTableVisible = ref(false)
const setStatusDialogTableVisible = ref(false)
const actionRecordsDialogVisible = ref(false)
const actionRecordsDialogTableData = ref([])
const entryRecordDialogTableData = ref([])
const entryRecordDialogTableColumns = ref([])
const actionRecordsDialogTableColumns = ref([])
const editStatus = ref('add')
const form = reactive({
status: '',
desc: '',
// 新增应收款管理
const addReceivablesFormModel = ref({
commissionBizType: 'U',
})
const selectedRow = ref(null)
const handleSelect = (e, row) => {
console.log(e, row)
selectedRow.value = row
if (e === 'entryRecord') {
entryRecordDialogTableVisible.value = true
entryRecordDialogTableColumns.value = [
{ property: 'commissionPeriod', label: '佣金期数', width: '100' },
{ property: 'totalPeriod', label: '总期数', width: '150' },
{ property: 'exchangeRate', label: '结算汇率(实)', width: '150' },
{ property: 'currency', label: '入账币种', width: '150' },
{ property: 'amount', label: '入账金额', width: '150' },
{ property: 'commissionPeriod', label: '入账比例', width: '150' },
{ property: 'commissionPeriod', label: '入账日', width: '150' },
{ property: 'status', label: '入账状态', width: '150' }
const addReceivablesDialogVisible = ref(false)
const addRecordRef = ref(null)
const addReceivablesFormConfig = [
{
type: 'select',
prop: 'commissionBizType',
label: '应收单类型',
placeholder: '应收单类型',
options: commissionBizTypeOptions
},
{
type: 'input',
prop: 'policyNo',
label: '保单号',
visible: (formData) => formData.commissionBizType == 'R'
}, {
type: 'input',
prop: 'commissionPeriod',
label: '佣金期数',
inputType: 'decimal',
visible: (formData) => formData.commissionBizType === 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
// 加载入账记录-假数据等待接口修改
loadEntryRecordData(selectedRow.value.commissionExpectedBizId).then(records => {
entryRecordDialogTableData.value = records =
[{
commissionPeriod: '2023-08-01',
totalPeriod: '2023-08-31',
exchangeRate: '1.2345',
currency: '假数据,等接口改CNY',
amount: '10000.00',
commissionPeriod: '10%',
commissionPeriod: '2023-08-01',
status: '已入账'
}]
})
} else if (e === 'setStatus') {
setStatusDialogTableVisible.value = true
},
{
type: 'input',
prop: 'totalPeriod',
label: '总期数',
inputType: 'decimal',
visible: (formData) => formData.commissionBizType === 'R',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
}, {
type: 'date',
prop: 'commissionDate',
label: '入账日(估)',
placeholder: '请选择'
},
// {
// type: 'date',
// prop: 'actualCommissionDate',
// label: '入账日(实)',
// placeholder: '请选择',
// maxDate: 'today'
// },
{
type: 'input',
prop: 'amount',
label: '入账金额',
inputType: 'decimal',
decimalDigits: 2,
visible: (formData) => formData.commissionBizType === 'U',
rules: [
{ required: true, message: '请输入金额', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '最多两位小数', trigger: 'blur' }
]
}, {
type: 'input',
prop: 'commissionRatio',
label: '入账比例(%)',
inputType: 'decimal',
decimalDigits: 2,
visible: (formData) => formData.commissionBizType === 'R',
rules: [
{ required: true, message: '请输入入账比例', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '最多两位小数', trigger: 'blur' }
]
}, {
type: 'select',
prop: 'currency',
label: '入账币种',
dictType: 'bx_currency_type'
}, {
type: 'select',
prop: 'commissionType',
label: '入账项目',
dictType: 'csf_commission_type',
onChangeExtraFields: {
commissionName: 'itemLabel',
},
}, {
type: 'select',
prop: 'reconciliationCompanyBizId',
label: '对账公司',
api: '/insurance/base/api/insuranceReconciliationCompany/page',
keywordField: 'name',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入对账公司名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'reconciliationCompanyBizId',
labelKey: 'name',
onChangeExtraFields: {
reconciliationCompany: 'name',// 自动同步 raw.name 到 reconciliationCompany
reconciliationCompanyCode: 'code'
},
transform: (res) => {
console.log('对账公司', res)
return res.data.records || []
}
}
]
// 弹窗表单重置
const resetAddReceivablesForm = () => {
addRecordRef.value.resetForm()
editStatus.value = 'add'
}
const handleClick = () => {
actionRecordsDialogVisible.value = true
actionRecordsDialogTableColumns.value = [
{ property: 'commissionPeriod', label: '检核年月', width: '100' },
{ property: 'totalPeriod', label: '比对状态', width: '150' },
{ property: 'exchangeRate', label: '入账比例', width: '150' },
{ property: 'currency', label: '应入账比例', width: '150' },
{ property: 'amount', label: '对账公司', width: '150' },
{ property: 'createTime', label: '比对时间', width: '150' },
{ property: 'userName', label: '比对人', width: '150' }
]
// 加载入账操作记录-假数据等待接口修改
loadEntryEditRecordData(selectedRow.value.commissionExpectedBizId).then(records => {
actionRecordsDialogTableData.value = records =
[{
commissionPeriod: '2023-08-01',
totalPeriod: '2023-08-31',
exchangeRate: '1.2345',
currency: '假数据,等接口改CNY',
amount: '10000.00',
commissionPeriod: '10%',
commissionPeriod: '2023-08-01',
status: '已入账'
}]
const handleConfirmAddReceivables = async () => {
if (editStatus.value === 'add') {
const p = addRecordRef.value.getFormData()
try {
await addReceivedFortune({
commissionExpectedAddDtoList: [p]
})
ElMessage.success('新增应收款成功')
addReceivablesDialogVisible.value = false
resetAddReceivablesForm()
handleQuery()
} catch (error) {
ElMessage.error(error.message)
}
} else {
try {
const res = await updateCommissionExpected({
commissionExpectedBizId: selectedRow.value.commissionExpectedBizId,
...addReceivablesFormModel.value
})
if (res.code === 200) {
ElMessage.success('应收款修改成功')
addReceivablesDialogVisible.value = false
resetAddReceivablesForm()
loadTableData() // 重新加载表格
} else {
ElMessage.error(res.msg || '应收款修改失败')
}
} catch (error) {
console.error('修改应收款失败:', error)
ElMessage.error('修改应收款失败')
}
}
}
// 搜索表单数据 - 修正字段名
const searchFormData = reactive({
policyNo: '',
incomeDateRange: [], // 改为数组格式
statusList: [],
commissionNameList: [], // 修改字段名
commissionPeriod: '',
reconciliationCompanyList: [], // 修改字段名
insurerBizId: [],
productLaunchBizId: [],
commissionBizType: '',
teamBizId: '',
})
const searchFields = ref([
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'input',
field: 'policyNo',
label: '保单号',
placeholder: '请输入保单号',
colSpan: 6,
clearable: true,
rules: [
{ max: 50, message: '保单号长度不能超过50个字符', trigger: 'blur' }
]
prop: 'policyNo',
label: '保单号'
},
{
type: 'daterange',
field: 'incomeDateRange',
label: '入账日期',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
colSpan: 6,
dateFormat: 'YYYY-MM-DD',
props: {
valueFormat: 'YYYY-MM-DD',
style: 'width: 100%'
}
prop: 'entryDate',
label: '入账日(估)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'multi-select',
field: 'statusList',
type: 'select',
prop: 'statusList',
label: '入账状态',
placeholder: '请选择入账状态',
colSpan: 6,
remoteConfig: {
type: 'commissionType',
apiMethod: searchCommissionTypes,
formatResult: (data) => data.map(item => ({
label: item.typeName,
value: item.typeCode,
remark: item.remark
})),
defaultOptions: [
{ label: '已入账', value: '1' },
{ label: '待入账', value: '0' },
{ label: '部分入账', value: '2' }
]
}
multiple: true,
dictType: 'csf_expected_commission_status'
},
{
type: 'input',
field: 'commissionNameList',
prop: 'commissionPeriod',
label: '入账期数',
inputType: 'decimal',
rules: [
{ pattern: /^\d+$/, message: '只能输入正整数', trigger: 'blur' }
]
},
{
type: 'select',
prop: 'fortuneName',
label: '入账项目',
placeholder: '请输入关键词搜索',
colSpan: 6,
dictType: 'csf_commission_type'
},
{
type: 'remote-multi-select',
field: 'reconciliationCompanyList',
type: 'select',
prop: 'reconciliationCompanyBizIdList',
label: '对账公司',
placeholder: '请输入关键词搜索',
colSpan: 6,
remoteConfig: {
type: 'company',
apiMethod: (params) => searchCompanies({ ...params, type: 'reconciliation' }),
defaultOptions: []
api: '/insurance/base/api/insuranceReconciliationCompany/page',
keywordField: 'name',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入对账公司名称搜索',
debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'reconciliationCompanyBizId',
labelKey: 'name',
transform: (res) => {
console.log(res)
return res?.data.records || []
}
},
{
type: 'input',
field: 'commissionPeriod',
label: '入账期数',
placeholder: '请输入期数',
colSpan: 6,
},
{
type: 'input',
field: 'insurerBizId',
type: 'select',
prop: 'insurerCompanyBizIdList',
label: '保险公司',
placeholder: '请输入关键词搜索保险公司',
colSpan: 6,
},
{
type: 'input',
field: 'productLaunchBizId',
api: '/insurance/base/api/insuranceCompany/page',
keywordField: 'queryContent',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入保险公司名称搜索',
debounceWait: 500, // 自定义防抖时间
multiple: true,
valueKey: 'insuranceCompanyBizId',
labelKey: 'abbreviation',
transform: (res) => {
return res?.data.records || []
}
}, {
type: 'select',
prop: 'productLaunchBizId',
label: '产品计划',
placeholder: '请输入关键词搜索产品计划',
colSpan: 6,
api: '/product/api/relProjectProductLaunch/parameter/page',
keywordField: 'productName',
requestParams: {
tenantBizId: userStore.projectInfo.tenantBizId || '',
projectBizId: userStore.projectInfo.projectBizId || '',
fieldBizId: 'field_olk1qZe81qHHKXbw',
fieldValueBizId: 'field_value_uOfJH5ucA2YwJpbn',
pageNo: 1,
pageSize: 20
},
{
placeholder: '输入产品计划名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'productLaunchBizId',
labelKey: 'productName',
transform: (res) => {
return res?.data.records || []
}
}, {
type: 'select',
field: 'commissionBizType',
label: '应收款类型',
placeholder: '请选择应收款类型',
colSpan: 6,
options: [
{ label: '全部', value: '' },
{ label: '关联保单应收单', value: '1' },
{ label: '非关联保单应收单', value: '2' }
]
},
{
type: 'input',
field: 'teamBizId',
prop: 'commissionBizType',
label: '应收单类型',
options: commissionBizTypeOptions,
}, {
type: 'select',
prop: 'teamBizId',
label: '出单团队',
placeholder: '请输入关键词搜索出单团队',
colSpan: 6,
api: '/csf/api/team/page',
keywordField: 'teamName',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入出单团队名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'teamBizId',
labelKey: 'teamName',
transform: (res) => {
return res?.data.records || []
}
},
])
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageTotal = ref(0)
const loading = ref(false)
// 应收单类型通过value转成label
const getCommissionBizTypeLabel = (value) => {
const item = commissionBizTypeOptions.find(item => item.value === value)
return item?.label || ''
}
// 表格操作菜单
const dropdownItems = [
{ label: '入账记录', value: 'entryRecord' },
{ label: '设置状态', value: 'setStatus' },
{ label: '更新数据', value: 'updateData' },
]
// 弹窗状态
const entryRecordDialogTableVisible = ref(false)
const setStatusDialogTableVisible = ref(false)
const actionRecordsDialogVisible = ref(false)
// 弹窗表格数据
const actionRecordsDialogTableData = ref([])
const entryRecordDialogTableData = ref([])
const entryRecordDialogTableColumns = ref([])
const actionRecordsDialogTableColumns = ref([])
// 设置入账状态表单
const setCommissionRecordStatusFormRef = ref(null)
const setCommissionRecordStatusFormConfig = ref([])
const selectedRow = ref(null)
// 表格数据
const tableData = ref([])
......@@ -375,69 +475,56 @@ const statisticsData = ref({
totalPaidAmount: 0,
pendingPaidAmount: 0,
paidAmountRatio: 0,
totalPolicyCount: 0,
paidAmountRatio: 0
totalPolicyCount: 0
})
// 按钮事件处理
const handleAdd = () => {
ElMessage.info('点击新增按钮')
addReceivablesDialogVisible.value = true
if (addRecordRef.value) {
addRecordRef.value.resetForm()
}
addReceivablesFormModel.value = {}
}
const handleImport = () => {
ElMessage.info('点击导入按钮')
}
const handleImport = () => ElMessage.info('点击导入按钮')
const handleExport = async () => {
// 获取搜索参数
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 handleExport = () => {
ElMessage.info('点击导出按钮')
}
const handleReset = () => {
// 重置搜索表单
Object.keys(searchFormData).forEach(key => {
if (Array.isArray(searchFormData[key])) {
searchFormData[key] = []
} else {
searchFormData[key] = ''
}
})
ElMessage.success('搜索条件已重置')
// 重新加载数据
searchFormRef.value.resetForm()
searchParams.value = {}
console.log('表单已重置')
loadTableData()
}
const handleQuery = async () => {
const handleQuery = () => {
loadTableData()
}
// 控制要显示的默认按钮
const visibleDefaultButtons = ref(['add', 'import', 'export']) // 只显示新增和查询两个默认按钮
const visibleDefaultButtons = ref([
'add', 'export', 'reset', 'query'
])
// 按钮配置
const operationBtnList = ref([
{
key: 'add',
direction: 'left',
click: handleAdd
},
{
key: 'import',
direction: 'left',
click: handleImport
},
{
key: 'export',
direction: 'right',
click: handleExport
},
{
key: 'reset',
direction: 'right',
click: handleReset
},
{
key: 'query',
direction: 'right',
click: handleQuery
}
{ key: 'add', direction: 'left', click: handleAdd },
// { key: 'import', direction: 'left', click: handleImport },
{ key: 'export', direction: 'right', click: handleExport },
{ key: 'reset', direction: 'right', click: handleReset },
{ key: 'query', direction: 'right', click: handleQuery }
])
// 分页事件
......@@ -453,17 +540,21 @@ const handleCurrentChange = (val) => {
// 加载表格数据
const loadTableData = async () => {
const searchParams = searchFormRef.value.getFormData() || {}
loading.value = true
try {
const params = {
...searchFormData,
currentPage: currentPage.value,
...searchParams,
commissionDateStart: searchParams?.entryDate?.[0] || undefined,
commissionDateEnd: searchParams?.entryDate?.[1] || undefined,
entryDate: undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await receivedFortuneList(params)
tableData.value = response.data.page.records
pageTotal.value = response.data.page.total
pageSize.value = response.data.page.size
tableData.value = response.data.page.records || []
pageTotal.value = response.data.page.total || 0
pageSize.value = response.data.page.size || 10
// 统计信息
statisticsData.value = {
......@@ -471,13 +562,11 @@ const loadTableData = async () => {
totalPaidAmount: response.data.expectedStatisticsVO.totalPaidAmount,
pendingPaidAmount: response.data.expectedStatisticsVO.pendingPaidAmount,
paidAmountRatio: response.data.expectedStatisticsVO.paidAmountRatio,
totalPolicyCount: response.data.expectedStatisticsVO.totalPolicyCount,
paidAmountRatio: response.data.expectedStatisticsVO.paidAmountRatio
totalPolicyCount: response.data.expectedStatisticsVO.totalPolicyCount
}
} catch (error) {
console.error('加载数据失败:', error)
// ElMessage.error('加载数据失败')
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
......@@ -487,67 +576,150 @@ const loadTableData = async () => {
const loadEntryRecordData = async (cbd) => {
loading.value = true
try {
const params = {
commissionBizId: cbd
}
const response = await commissionEntryRecord(params)
const records = response.data.records
return records
const params = { commissionExpectedBizId: cbd }
const response = await commissionExpectedRecord(params)
return response.data.records || []
} catch (error) {
console.error('加载数据失败:', error)
// ElMessage.error('加载数据失败')
console.error('加载入账记录失败:', error)
ElMessage.error('加载入账记录失败')
return []
} finally {
loading.value = false
}
}
// 入账操作记录查询
const loadEntryEditRecordData = async (cbd) => {
loading.value = true
try {
const params = {
commissionBizId: cbd
}
const params = { commissionBizId: cbd }
const response = await commissionEntryEditRecords(params)
const records = response.data.records
return records
return response.data.records || []
} catch (error) {
console.error('加载数据失败:', error)
// ElMessage.error('加载数据失败')
console.error('加载操作记录失败:', error)
ElMessage.error('加载操作记录失败')
return []
} finally {
loading.value = false
}
}
// 操作菜单选择事件
const handleSelect = async (e, row) => {
selectedRow.value = { ...row }
if (e === 'entryRecord') {
entryRecordDialogTableVisible.value = true
entryRecordDialogTableColumns.value = [
{ property: 'reconciliationYearMonth', label: '检核年月', width: '100' },
{ property: 'commissionPeriod', label: '佣金期数', width: '100' },
{ property: 'totalPeriod', label: '总期数', width: '150' },
{ property: 'exchangeRate', label: '结算汇率(实)', width: '150' },
{ property: 'currency', label: '入账币种', width: '150' },
{ property: 'amount', label: '入账金额', width: '150' },
{ property: 'currentCommissionRatio', label: '入账比例', width: '150' },
{ property: 'commissionDate', label: '入账日', width: '150' },
{ property: 'status', label: '入账状态', width: '150' }
]
// 加载真实数据
const records = await loadEntryRecordData(row.commissionExpectedBizId)
entryRecordDialogTableData.value = records.length ? records : []
} else if (e === 'setStatus') {
console.log(selectedRow.value.status)
setCommissionRecordStatusFormConfig.value = [
{
type: 'select',
prop: 'status',
label: '入账状态',
dictType: 'csf_expected_commission_status',
defaultValue: selectedRow.value.status || ''
}, {
type: 'textarea',
prop: 'statusDesc',
label: '修改理由',
defaultValue: selectedRow.value.statusDesc || ''
},
]
setStatusDialogTableVisible.value = true
} else if (e === 'updateData') {
editStatus.value = 'update'
addReceivablesDialogVisible.value = true
if (addRecordRef.value) {
addReceivablesFormModel.value = {
...selectedRow.value
}
}
console.log('更新数据', selectedRow.value)
}
}
// 初始化加载数据
onMounted(() => {
loadTableData()
})
</script>
// 查看比对记录
const handleClick = async () => {
actionRecordsDialogVisible.value = true
actionRecordsDialogTableColumns.value = [
{ property: 'checkMonth', label: '检核年月', width: '100' },
{ property: 'compareStatus', label: '比对状态', width: '150' },
{ property: 'entryRatio', label: '入账比例', width: '150' },
{ property: 'shouldEntryRatio', label: '应入账比例', width: '150' },
{ property: 'reconciliationCompany', label: '对账公司', width: '150' },
{ property: 'createTime', label: '比对时间', width: '150' },
{ property: 'userName', label: '比对人', width: '150' }
]
// 加载真实数据
const records = await loadEntryEditRecordData(selectedRow.value.commissionExpectedBizId)
actionRecordsDialogTableData.value = records.length ? records : []
}
<style scoped lang="scss">
.statistics-cards {
margin-bottom: 5px;
background: RGBA(0,82,217,0.03);
border-radius: 8px;
padding: 10px 20px;
// 新增:设置状态确认事件
const handleConfirmSetStatus = () => {
const formData = setCommissionRecordStatusFormRef.value.getFormData()
if (!formData.status) {
return ElMessage.warning('请选择入账状态')
}
if (!formData.statusDesc) {
return ElMessage.warning('请输入修改理由')
}
// 调用接口修改状态(示例)
ElMessageBox.confirm('确定要修改入账状态吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await updateCommissionExpected({
commissionExpectedBizId: selectedRow.value.commissionExpectedBizId,
...formData
})
if (res.code === 200) {
ElMessage.success('状态修改成功')
setStatusDialogTableVisible.value = false
loadTableData() // 重新加载表格
} else {
ElMessage.error(res.msg || '状态修改失败')
}
} catch (error) {
console.error('修改状态失败:', error)
ElMessage.error('修改状态失败')
}
}).catch(() => {
ElMessage.info('已取消修改')
})
}
.page-search-container {
padding: 20px;
background: #fff;
border-radius: 8px;
margin-bottom: 20px;
// 获取入账状态,字典值转化方法
onMounted(async () => {
try {
await loadDicts(['csf_expected_commission_status'])
} catch (error) {
console.error('字典加载失败', error)
} finally {
loading.value = false
}
})
// 格式化函数(每次渲染都会调用,所以能拿到最新字典)
const formatStatus = (row, column) => {
return getDictLabel('csf_expected_commission_status', row.status) // 实时查缓存
}
.search-actions {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
text-align: right;
}
</script>
.el-menu {
border: none;
}
</style>
\ No newline at end of file
<style scoped lang="scss"></style>
\ No newline at end of file
......@@ -52,6 +52,17 @@ export default defineConfig(({ mode, command }) => {
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
},
'/csf': {
target: 'http://139.224.145.34:9002',
changeOrigin: true,
secure: false,
// 如果后端需要 host 头
// configure: (proxy, options) => {
// proxy.on('proxyReq', (proxyReq, req, res) => {
// proxyReq.setHeader('host', '139.224.145.34:9002')
// })
// }
},
// springdoc proxy
'^/v3/api-docs/(.*)': {
target: baseUrl,
......
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