Commit f97f6052 by Sweet Zhang

搜索条件表单封装

parent 220d00dc
......@@ -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;
......
......@@ -226,7 +226,7 @@ export function policyData(data) {
// 应付款管理列表
export function expectedFortuneList(data) {
return request({
url: '/csf/api/expectedFortune/page',
url: '/csf/api/expectedFortune/list',
method: 'post',
data: data
})
......
// src/api/search.ts
import request from '@/utils/request'
// 特定模块搜索
// 搜索保险公司
export function searchInsurers(params) {
return request({
url: '/insurance/base/api/insuranceCompany/page',
method: 'POST',
params
})
}
// 搜索产品
export function searchProducts(params) {
return request({
url: '/product/base/api/product/search',
method: 'POST',
params
})
}
// 获取对账公司
export function searchReconciliationCompanies(params) {
return request({
url: '/insurance/base/api/insuranceReconciliationCompany/page',
method: 'POST',
params
})
}
// 获取签单员列表
export function searchSigners(params) {
return request({
url: '/insurance/base/api/userSignExpand/page',
method: 'POST',
params
})
}
// 获取转介人列表
export function searchIntermediaries(params) {
return request({
url: '/insurance/base/api/userSaleExpand/page',
method: 'POST',
params
})
}
<!-- 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
<!-- components/SearchForm.vue -->
<template>
<el-form :model="formModel" label-width="100px" size="default" label-position="top">
<el-row :gutter="20">
<template v-for="item in config" :key="item.prop">
<!-- 所有字段都用相同的响应式布局 -->
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-if="item.type === 'input'">
<el-form-item :label="item.label" :prop="item.prop">
<el-input
v-model="formModel[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`"
clearable
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-else-if="item.type === 'select' && item.api">
<el-form-item :label="item.label" :prop="item.prop">
<el-select
v-model="formModel[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
clearable
filterable
:filter-method="getFilterMethod(item)"
:loading="item.loading"
@focus="handleFocus(item)"
>
<el-option
v-for="opt in item.options"
:key="opt[item.valueKey || 'value']"
:label="opt[item.labelKey || 'label']"
:value="opt[item.valueKey || 'value']"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-else-if="item.type === 'select'">
<el-form-item :label="item.label" :prop="item.prop">
<el-select
v-model="formModel[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
clearable
:filterable="item.filterable !== false"
:multiple="item.multiple"
>
<el-option
v-for="opt in item.options"
:key="opt[item.valueKey || 'value']"
:label="opt[item.labelKey || 'label']"
:value="opt[item.valueKey || 'value']"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-else-if="item.type === 'checkbox-group'">
<el-form-item :label="item.label" :prop="item.prop">
<el-checkbox-group v-model="formModel[item.prop]">
<el-checkbox
v-for="opt in item.options"
:key="opt[item.valueKey || 'value']"
:label="opt[item.valueKey || 'value']"
>
{{ opt[item.labelKey || 'label'] }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-else-if="item.type === 'date'">
<el-form-item :label="item.label" :prop="item.prop">
<el-date-picker
v-model="formModel[item.prop]"
type="date"
:placeholder="item.placeholder || `请选择${item.label}`"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
clearable
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-else-if="item.type === 'daterange'">
<el-form-item :label="item.label" :prop="item.prop">
<el-date-picker
v-model="formModel[item.prop]"
type="daterange"
:range-separator="item.rangeSeparator || '至'"
:start-placeholder="item.startPlaceholder || '开始日期'"
:end-placeholder="item.endPlaceholder || '结束日期'"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</template>
<script setup>
import { reactive, onMounted } from 'vue'
import request from '@/utils/request'
function debounce(func, wait) {
let timeout
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, args), wait)
}
}
const props = defineProps({
config: { type: Array, required: true }
})
const formModel = reactive({})
// ========================
// 公共请求方法
// ========================
async function fetchOptions(item, keyword = '') {
const payload = {
...(item.requestParams || {}),
pageNo: item.pageNo || 1,
pageSize: item.pageSize || 20
}
if (keyword) {
const keyField = item.keywordField || 'keyword'
payload[keyField] = keyword.trim()
}
const res = await request({
url: item.api,
method: item.method || 'POST',
data: payload
})
let list = []
if (typeof item.transform === 'function') {
list = item.transform(res)
} else if (res?.data?.records) {
list = res.data.records
} else if (res?.data?.list) {
list = res.data.list
} else if (Array.isArray(res?.data)) {
list = res.data
} else if (res?.data?.data && Array.isArray(res.data.data)) {
list = res.data.data
}
return list
}
// ========================
// 创建搜索函数(带防抖)
// ========================
function createSearchFn(item) {
const search = async (keyword) => {
item.loading = true
try {
if (!keyword) {
// 关键词清空 → 恢复默认选项(从缓存)
item.options = item._defaultOptions ? [...item._defaultOptions] : []
} else {
// 有关键词 → 远程搜索
const list = await fetchOptions(item, keyword)
item.options = list
}
} catch (e) {
console.error(`${item.label} 搜索失败`, e)
item.options = []
} finally {
item.loading = false
}
}
return debounce(search, item.debounceWait ?? 300)
}
// ========================
// 初始化
// ========================
onMounted(async () => {
for (const item of props.config) {
// 初始化表单值
if (['checkbox-group', 'daterange'].includes(item.type)) {
formModel[item.prop] = item.defaultValue ?? []
} else {
formModel[item.prop] = item.defaultValue ?? ''
}
// 处理带 api 的 select(hybrid 模式)
if (item.type === 'select' && item.api) {
item.options = []
item.loading = true
try {
// 1. 加载默认数据(不传 keyword)
const defaultList = await fetchOptions(item, '')
item._defaultOptions = [...defaultList] // 缓存
item.options = defaultList
} catch (e) {
console.error(`加载 ${item.label} 默认选项失败`, e)
item.options = []
} finally {
item.loading = false
}
}
}
})
// ========================
// 提供给 el-select 的 filter-method
// ========================
function getFilterMethod(item) {
if (!item._searchFn) {
item._searchFn = createSearchFn(item)
}
return (keyword) => {
item._searchFn(keyword)
return true // 必须返回 true,否则下拉框会关闭
}
}
// 可选:聚焦时确保有数据(一般不需要,因为 onMounted 已加载)
function handleFocus(item) {
// 如果意外为空,可重试
if (item.type === 'select' && item.api && (!item.options || item.options.length === 0)) {
// 可加 loading 重试逻辑(略)
}
}
// 暴露方法
defineExpose({
getSearchParams() {
const params = {}
for (const key in formModel) {
const val = formModel[key]
if (val === '' || val === null || val === undefined) continue
if (Array.isArray(val) && val.length === 0) continue
params[key] = val
}
return params
},
resetForm() {
props.config.forEach(item => {
if (['checkbox-group', 'daterange'].includes(item.type)) {
formModel[item.prop] = []
} else {
formModel[item.prop] = ''
}
})
},
getRawForm() {
return { ...formModel }
}
})
</script>
\ 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>
// 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
......@@ -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, ',')
}
<template>
<div class='app-container'>
<CommonPage :operationBtnList='operationBtnList' :showSearchForm='true' :show-pagination='true' :total='pageTotal'
:current-page='currentPage' :page-size='pageSize' @size-change='handleSizeChange'
@current-change='handleCurrentChange'>
<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>
<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="保单号" label-position="top">
<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="转介人" label-position="top">
<el-input v-model="queryParams.broker" placeholder="请输入转介人" clearable />
<el-select v-model="queryParams.broker" placeholder="请选择转介人" clearable :remote-method="loadBrokers"
:loading="searchLoading" filterable remote reserve-keyword>
<el-option v-for="item in brokerOptions" :key="item.userSaleBizId" :label="item.realName + '(' + item.phone + ')'"
:value="item.realName"/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
......@@ -36,28 +35,33 @@
</template>
<!-- 列表区域 -->
<template #table>
<el-table
:data="tableData"
@selection-change="handleSelectionChange"
v-loading="loading"
ref="tableRef"
row-key="fortuneAccountBizId"
:reserve-selection="true"
>
<!-- 统计数据 -->
<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 :xs="24" :sm="12" :md="6" class="text-center mb-4">
<el-statistic title="总人数" :value="statisticInfo.brokerCount" />
</el-col>
</el-row>
</div>
<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="150">
<el-table-column prop="status" label="出账状态" width="150" sortable>
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ convertStatusToDict(row.status) }}
......@@ -65,55 +69,12 @@
</template>
</el-table-column>
<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>
</template>
</CommonPage>
<!-- 弹窗-->
<CommonDialog dialogTitle='新建薪资记录' dialogWidth='80%' :openDialog=createDialogVisible :showAction='false' :showClose='true'
@close='createDialogVisible = false'>
<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>
</CommonDialog>
<CommonDialog
dialogTitle='出账完成' dialogWidth='80%' :openDialog=billingCompleteDialogVisible :showAction='false' :showClose='true'
@close='billingCompleteDialogVisible = false'>
<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 />
......@@ -121,9 +82,8 @@
<p style="margin-top: 16px; font-size: 16px">出账操作已完成!</p>
</div>
</CommonDialog>
<CommonDialog
dialogTitle='修订记录' dialogWidth='80%' :openDialog=showReviseRecord :showAction='false' :showClose='true'
@close='showReviseRecord = false'>
<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">
......@@ -140,77 +100,51 @@
</template>
<script setup>
import { ref, reactive, computed, watch } from '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 {searchIntermediaries} from '@/api/search'
import { searchIntermediaries } from '@/api/search'
import { formatCurrency } from '@/utils/number'
import { debounce } from '@/utils/index';
import {ElMessageBox} from 'element-plus'
// 添加表格引用
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 statisticInfo = ref({
totalAmount: 0,
brokerCount: 0
})
// 查询参数
const queryParams = reactive({
broker: '',
accountDate: [],
accountDateStart: '',
accountDateEnd: '',
pageNo: 1,
pageSize: 10,
sortField: '',
sortOrder: 'desc'
sortOrder: 'desc',
status: '6'
})
// 表格数据
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 handleAdd = () => {
ElMessage.info('点击新增按钮')
}
const handleExport = () => {
ElMessage.info('点击导出按钮')
}
// 批量导入
const handleImport = () => {
ElMessage.info('批量导入功能待实现')
}
// 分页事件
const handleSizeChange = (val) => {
......@@ -221,33 +155,7 @@ const handleCurrentChange = (val) => {
currentPage.value = val
getList()
}
// 转介人选项
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 setCurrentPageSelection = () => {
if (!tableRef.value) return
......@@ -268,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(
......@@ -302,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
......@@ -322,20 +218,7 @@ 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 () => {
......@@ -345,17 +228,21 @@ const getList = async () => {
queryParams.accountDateStart = queryParams.accountDate[0]
queryParams.accountDateEnd = queryParams.accountDate[1]
}
const response = await getReferrerFortuneList(queryParams)
const params = {
...queryParams,
accountDate:undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
}
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]
})
statisticInfo.value = response.data.statisticsVO
}
// 数据加载完成后,设置当前页的选中状态
nextTick(() => {
......@@ -366,6 +253,7 @@ const getList = async () => {
// ElMessage.error('获取数据失败')
}
}
getList()
const getStatusType = status => {
const types = {
......@@ -397,8 +285,6 @@ const handleReset = () => {
broker: '',
accountDateStart: '',
accountDateEnd: '',
pageNo: 1,
pageSize: 10,
sortField: '',
sortOrder: 'desc'
})
......@@ -415,51 +301,6 @@ 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('删除失败')
}
}
}
// 完成出账
......@@ -487,50 +328,6 @@ 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([])
// 获取出账状态字典值
......@@ -565,6 +362,7 @@ const getDictLists = () => {
})
})
}
getDictLists()
// 返回数据中状态需要转换为字典值
const convertStatusToDict = status => {
......@@ -572,24 +370,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 => {
......@@ -605,45 +385,36 @@ const fetchCompletePolicyFortune = async row => {
} else {
ElMessage.error(res.msg)
}
} catch (error) {}
} catch (error) { }
}
// 删除出账
import { deletePolicyFortune } from '@/api/financial/commission'
// 删除出账
const deleteFortune = async row => {
const searchLoading = ref(false)
const brokerOptions = ref([])
// 获取转介人列表
const loadBrokers = async (query='') => {
searchLoading.value = true
const params = {
realName: query,
pageNo: 1,
pageSize: 1000,
}
try {
const res = await deletePolicyFortune({
fortuneAccountBizId: row
})
console.log(res)
const res = await searchIntermediaries(params)
if (res.code === 200) {
// 显示完成弹窗
billingCompleteDialogVisible.value = true
brokerOptions.value = res.data.records || []
searchLoading.value = false
} else {
ElMessage.error(res.msg)
brokerOptions.value = []
searchLoading.value = false
}
} catch (error) {}
} catch (error) { }
}
debounce(loadBrokers, 500,false)
const visibleDefaultButtons = ref(['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
......@@ -652,6 +423,15 @@ const operationBtnList = ref([
key: 'query',
direction: 'right',
click: handleQuery
},
{
key: 'custom',
label: '完成出账',
icon: 'Check',
type: 'warning',
direction: 'right',
size: 'default',
click: completeBilling
}
])
......
<template>
<div>
<CommonPage
:operationBtnList="operationBtnList"
:showSearchForm="true"
:show-pagination="true"
:total="pageTotal"
:current-page="currentPage"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<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" :config="searchConfig" />
</template>
<!-- 列表区域 -->
<template #table>
<!-- 统计信息卡片 -->
<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">
<div class="card-content">
<div class="card-label">应出款总金额</div>
<div class="card-value">{{ formatCurrency(statisticsData.totalAmount) }}</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.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.pendingPaidAmount) }}</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">{{ statisticsData.paidAmountRatio }}</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">{{ statisticsData.totalPolicyCount }}</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">{{ statisticsData.totalPolicyCount }}</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 应付款管理列表 -->
<el-table
:data="tableData"
height="400"
border
highlight-current-row
style="width: 100%"
v-loading="loading"
>
<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" />
......@@ -97,76 +135,131 @@
</el-table>
</template>
</CommonPage>
<!-- 统计信息卡片 -->
<div class="statistics-cards" 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">
<div class="card-content">
<div class="card-label">应出款总金额</div>
<div class="card-value">{{ formatCurrency(statisticsData.totalAmount) }}</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.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.pendingPaidAmount) }}</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">{{ statisticsData.paidAmountRatio }}</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">{{ statisticsData.totalPolicyCount }}</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">{{ statisticsData.totalPolicyCount }}</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 出账记录表格弹窗-->
<CommonDialog dialogTitle="出账记录" dialogWidth="80%" :openDialog="payRecordDialogTableVisible"
:showAction="false" :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" />
<!-- <CommonDialog dialogTitle="出账记录" dialogWidth="80%" :openDialog="payRecordDialogTableVisible" :showAction="false"
: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> -->
</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, payRecordList } from '@/api/financial/commission'
import SearchForm from '@/components/SearchForm/SearchForm.vue'
const searchFormRef = ref(null)
const searchParams = ref({})
const searchConfig = ref([
{
type: 'input',
prop: 'policyNo',
label: '保单号'
},
{
type: 'daterange',
prop: 'payoutDate',
label: '出账日(估)',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
{
type: 'select',
prop: 'statusList',
label: '出账状态',
multiple: true,
options: [
{ value: '1', label: '启用' },
{ value: '0', label: '禁用' }
]
},
{
type: 'input',
prop: 'fortunePeriod',
label: '出账期数'
},
{
type: 'select',
prop: 'fortuneName',
label: '出账项目',
options: [
{ value: '1', label: '启用' },
{ value: '0', label: '禁用' }
]
},
{
type: 'select',
prop: 'insurerBizId',
label: '对账公司',
api: '/insurance/base/api/insuranceReconciliationCompany/page',
keywordField: 'name',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入对账公司名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'reconciliationCompanyBizId',
labelKey: 'name',
transform: (res) => {
return res?.data.records || []
}
},
{
type: 'select',
prop: 'insurerBizId',
label: '保险公司',
api: '/insurance/base/api/insuranceCompany/page',
keywordField: 'queryContent',
requestParams: { pageNo: 1, pageSize: 20 },
placeholder: '输入保险公司名称搜索',
debounceWait: 500, // 自定义防抖时间
valueKey: 'insuranceCompanyBizId',
labelKey: 'abbreviation',
transform: (res) => {
return res?.data.records || []
}
}, {
type: 'select',
prop: 'productLaunchBizId',
label: '产品计划',
api: '/product/api/relProjectProductLaunch/parameter/page',
keywordField: 'productName',
requestParams: { 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 currentPage = ref(1)
const pageSize = ref(10)
......@@ -178,19 +271,6 @@ const dropdownItems = [
{ label: '设置状态', value: 'setStatus' }
]
// 搜索表单数据 - 修正字段名
const searchFormData = reactive({
policyNo: '',
incomeDateRange: [], // 改为数组格式
statusList: [],
fortuneName: [], // 修改字段名
fortunePeriod: '',
insurerBizId: [],
productLaunchBizId: [],
commissionBizType: '',
teamBizId: '',
})
// 表格数据
const tableData = ref([])
......@@ -219,20 +299,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 () => {
loadTableData()
const params = searchFormRef.value.getSearchParams()
console.log('父组件发起查询:', params)
loadTableData(params)
}
// 按钮配置
......@@ -276,29 +353,30 @@ const handleCurrentChange = (val) => {
}
// 加载表格数据
const loadTableData = async () => {
const loadTableData = async (searchParams={}) => {
loading.value = true
try {
const params = {
// ...searchFormData,
// payoutDateStart: searchFormData.incomeDateRange[0],
// payoutDateEnd: searchFormData.incomeDateRange[1],
...searchParams,
payoutDateStart: searchParams.payoutDate?.[0] || undefined,
payoutDateEnd: searchParams.payoutDate?.[1] || undefined,
payoutDate:undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await expectedFortuneList(params)
tableData.value = response.data.records
pageTotal.value = response.data.total
pageSize.value = response.data.size
tableData.value = response.data.page.records
pageTotal.value = response.data.page.total
pageSize.value = response.data.page.size
// 统计信息
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) {
......@@ -331,7 +409,7 @@ const loadPayRecordTableData = async () => {
loadTableData()
loadTableData()
</script>
<style scoped lang="scss">
......@@ -348,5 +426,4 @@ const loadPayRecordTableData = async () => {
border-top: 1px solid #ebeef5;
text-align: right;
}
</style>
\ No newline at end of file
<template>
<div>
<CommonPage :operationBtnList="operationBtnList" :showSearchForm="true" :show-pagination="true"
:total="pageTotal" :current-page="currentPage" :page-size="pageSize" @size-change="handleSizeChange"
@current-change="handleCurrentChange">
<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" />
{{ searchFormData }}
<el-form :model="searchFormData" label-width="120px">
<el-row :gutter="20">
<!-- 原有搜索项 -->
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="保单号" prop="policyNo" label-position="top">
<el-input v-model="searchFormData.policyNo" placeholder="请输入保单号" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="入账日(估)" prop="incomeDateRange" label-position="top">
<el-date-picker
v-model="searchFormData.incomeDateRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="入账状态" prop="statusList" label-position="top">
<el-select v-model="searchFormData.statusList" placeholder="请选择入账状态" clearable multiple>
<el-option label="待入账" value="0" />
<el-option label="完成入账" value="1" />
<el-option label="部分入账" value="2" />
<el-option label="已失效" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="入账期数" prop="commissionPeriod" label-position="top">
<el-input v-model="searchFormData.commissionPeriod" placeholder="请输入入账期数" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="入账项目" prop="commissionName" label-position="top">
<el-input v-model="searchFormData.commissionName" placeholder="请输入入账项目" clearable />
</el-form-item>
</el-col>
<!-- 新增:保险公司远程搜索下拉 -->
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="保险公司" prop="insurerBizId" label-position="top">
<el-select
v-model="searchFormData.insurerBizId"
placeholder="请选择保险公司"
clearable
:remote-method="(keyword) => remoteMethod('insurer', keyword)"
:loading="options.insurer.loading"
filterable
remote
reserve-keyword
>
<el-option
v-for="item in options.insurer.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 新增:产品计划远程搜索下拉 -->
<!-- <el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="产品计划" prop="productLaunchBizId" label-position="top">
<el-select
v-model="searchFormData.productLaunchBizId"
placeholder="请选择产品计划"
clearable
:remote-method="(keyword) => remoteMethod('product', keyword)"
:loading="options.product.loading"
filterable
remote
reserve-keyword
>
<el-option
v-for="item in options.product.options"
:key="item.bizId"
:label="item.productName"
:value="item.bizId"
/>
</el-select>
</el-form-item>
</el-col> -->
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="应收单类型" prop="commissionBizType" label-position="top">
<el-select v-model="searchFormData.commissionBizType" placeholder="请选择应收单类型" clearable>
<el-option label="关联保单应收单" value="R" />
<el-option label="非关联保单应收单" value="U" />
</el-select>
</el-form-item>
</el-col>
<!-- 新增:出单团队远程搜索下拉 -->
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="对账公司" prop="reconciliationCompanyBizId" label-position="top">
<el-select
v-model="searchFormData.reconciliationCompany"
placeholder="请选择对账公司"
clearable
:remote-method="(keyword) => remoteMethod('reconciliationCompany', keyword)"
:loading="options.reconciliationCompany.loading"
filterable
remote
reserve-keyword
>
<el-option
v-for="item in options.reconciliationCompany.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- 新增:出单团队远程搜索下拉 -->
<!-- <el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="出单团队" prop="outTeamBizId" label-position="top">
<el-select
v-model="searchFormData.outTeamBizId"
placeholder="请选择出单团队"
clearable
:remote-method="(keyword) => remoteMethod('team', keyword)"
:loading="options.team.loading"
filterable
remote
reserve-keyword
>
<el-option
v-for="item in options.team.options"
:key="item.teamBizId"
:label="item.teamName"
:value="item.teamBizId"
/>
</el-select>
</el-form-item>
</el-col> -->
</el-row>
</el-form>
</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" />
......@@ -31,17 +176,15 @@
</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 :data="tableData" height="400" border highlight-current-row style="width: 100%" v-loading="loading">
<el-table-column prop="commissionBizType" label="应收款类型" width="120" sortable />
<el-table-column prop="receivableNo" label="应收款编号" width="120" />
<el-table-column prop="policyNo" label="保单号" width="120" />
<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>
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'warning'">
{{ row.status === '1' ? '已入账' : '待入账' }}
<el-tag :type="row.status === '1' ? 'success' : row.status === '0' ? 'warning' : row.status === '2' ? 'info' : 'danger'">
{{ row.status === '1' ? '完成入账' : row.status === '0' ? '待入账' : row.status === '2' ? '部分入账' : '已失效' }}
</el-tag>
</template>
</el-table-column>
......@@ -56,17 +199,17 @@
</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.ratio || 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>
......@@ -76,15 +219,16 @@
</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="premium" label="期交保费" width="120" sortable>
<template #default="{ row }">
{{ formatCurrency(row.premium) }}
{{ numberWithCommas(row.premium) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="150" />
......@@ -92,28 +236,37 @@
<template #default="{ row }">
<el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-icon>
<MoreFilled />
</el-icon>
<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-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="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" />
<!-- 原有弹窗(不变) -->
<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"
: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">
......@@ -124,23 +277,39 @@
</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" />
<el-table-column
v-for="item in actionRecordsDialogTableColumns"
:key="item.property"
:prop="item.property"
:label="item.label"
:width="item.width"
/>
</el-table>
</CommonDialog>
<!-- 设置状态 弹窗-->
<CommonDialog dialogTitle="设置入账状态" dialogWidth="80%" :openDialog="setStatusDialogTableVisible"
@close="setStatusDialogTableVisible = false" @confirm="setStatusDialogTableVisible = false">
<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-option label="待入账" value="0" />
<el-option label="完成入账" value="1" />
<el-option label="部分入账" value="2" />
<el-option label="已失效" value="3" />
</el-select>
</el-form-item>
<el-form-item label="修改理由" :label-width="120">
......@@ -150,220 +319,77 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="setStatusDialogTableVisible = false">取消</el-button>
<el-button type="primary" @click="setStatusDialogTableVisible = false">
确认
</el-button>
<el-button type="primary" @click="handleConfirmSetStatus">确认</el-button>
</div>
</template>
</CommonDialog>
</div>
</template>
<script setup name="Receivables">
import CommonPage from '@/components/commonPage'
import CommonDialog from '@/components/commonDialog'
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatCurrency } from '@/utils/number'
import { MoreFilled } from '@element-plus/icons-vue'
import { numberWithCommas, debounce } from '@/utils/index'
import { receivedFortuneList, commissionEntryRecord, commissionEntryEditRecords } from '@/api/financial/commission'
import SearchForm from '@/components/SearchForm/index.vue'
// 查询下拉列表数据
import { searchInsurers,searchReconciliationCompanies } 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' }
]
// 弹窗状态
const entryRecordDialogTableVisible = ref(false)
const setStatusDialogTableVisible = ref(false)
const actionRecordsDialogVisible = ref(false)
// 弹窗表格数据
const actionRecordsDialogTableData = ref([])
const entryRecordDialogTableData = ref([])
const entryRecordDialogTableColumns = ref([])
const actionRecordsDialogTableColumns = ref([])
// 新增:下拉框选项状态管理(loading + 选项列表)
const options = reactive({
insurer: { loading: false, options: [] }, // 保险公司
product: { loading: false, options: [] }, // 产品计划
reconciliationCompany: { loading: false, options: [] }, // 对账公司
})
// 设置入账状态表单
const form = reactive({
status: '',
desc: '',
})
const selectedRow = ref(null)
const handleSelect = (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' }
]
// 加载入账记录-假数据等待接口修改
loadEntryRecordData(selectedRow.value.commissionExpectedBizId).then(records => {
console.log(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
}
}
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 searchFormData = reactive({
policyNo: '',
incomeDateRange: [], // 改为数组格式
incomeDateRange: [],
statusList: [],
commissionNameList: [], // 修改字段名
commissionName: '',
commissionPeriod: '',
reconciliationCompanyList: [], // 修改字段名
insurerBizId: [],
productLaunchBizId: [],
reconciliationCompany: '',
insurerBizId: '', // 保险公司ID
productLaunchBizId: '', // 产品计划ID
commissionBizType: '',
teamBizId: '',
outTeamBizId: '' // 出单团队ID(替换原outTeam)
})
const searchFields = ref([
{
type: 'input',
field: 'policyNo',
label: '保单号',
placeholder: '请输入保单号',
colSpan: 6,
clearable: true,
rules: [
{ max: 50, message: '保单号长度不能超过50个字符', trigger: 'blur' }
]
},
{
type: 'daterange',
field: 'incomeDateRange',
label: '入账日期',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
colSpan: 6,
dateFormat: 'YYYY-MM-DD',
props: {
valueFormat: 'YYYY-MM-DD',
style: 'width: 100%'
}
},
{
type: 'multi-select',
field: 'statusList',
label: '入账状态',
placeholder: '请选择入账状态',
colSpan: 6,
options: [
{ label: '已入账', value: '1' },
{ label: '待入账', value: '0' },
{ label: '部分入账', value: '2' }
]
},
{
type: 'input',
field: 'commissionNameList',
label: '入账项目',
placeholder: '请输入关键词搜索',
colSpan: 6,
},
{
type: 'remote-multi-select',
field: 'reconciliationCompanyList',
label: '对账公司',
placeholder: '请输入关键词搜索',
colSpan: 6,
remoteConfig: {
type: 'company',
defaultOptions: []
}
},
{
type: 'input',
field: 'commissionPeriod',
label: '入账期数',
placeholder: '请输入期数',
colSpan: 6,
},
{
type: 'input',
field: 'insurerBizId',
label: '保险公司',
placeholder: '请输入关键词搜索保险公司',
colSpan: 6,
},
{
type: 'input',
field: 'productLaunchBizId',
label: '产品计划',
placeholder: '请输入关键词搜索产品计划',
colSpan: 6,
},
{
type: 'select',
field: 'commissionBizType',
label: '应收款类型',
placeholder: '请选择应收款类型',
colSpan: 6,
options: [
{ label: '关联保单应收单', value: '1' },
{ label: '非关联保单应收单', value: '2' }
]
},
{
type: 'input',
field: 'teamBizId',
label: '出单团队',
placeholder: '请输入关键词搜索出单团队',
colSpan: 6,
}
])
// 表格数据
const tableData = ref([])
......@@ -373,22 +399,64 @@ const statisticsData = ref({
totalPaidAmount: 0,
pendingPaidAmount: 0,
paidAmountRatio: 0,
totalPolicyCount: 0,
paidAmountRatio: 0
totalPolicyCount: 0
})
// 按钮事件处理
const handleAdd = () => {
ElMessage.info('点击新增按钮')
}
// 新增:防抖处理远程搜索(延迟300ms,避免频繁请求)
const debouncedRemoteMethod = debounce(async (type, keyword) => {
try {
// 根据类型设置加载状态
options[type].loading = true
let res = []
// 不同类型调用不同接口
switch (type) {
case 'insurer':
res = await searchInsurers({ queryContent: keyword, pageSize: 20 }) // 保险公司搜索
options.insurer.options = res.data.records.map(item=>{
return {
label: item.abbreviation,
value: item.insuranceCompanyBizId
}
}) || []
break
case 'reconciliationCompany':
res = await searchReconciliationCompanies({ name: keyword, pageSize: 20 }) // 对账公司搜索
options.reconciliationCompany.options = res.data.records.map(item=>{
return {
label: item.name,
value: item.reconciliationCompanyBizId
}
}) || []
break
default:
break
}
} catch (error) {
console.error(`搜索${type}失败:`, error)
ElMessage.error(`加载${type === 'insurer' ? '保险公司' : type === 'product' ? '产品计划' : '出单团队'}失败`)
} finally {
// 关闭加载状态
options[type].loading = false
}
console.log(options)
}, 300)
const handleImport = () => {
ElMessage.info('点击导入按钮')
// 新增:远程搜索方法(对外暴露)
const remoteMethod = (type, keyword) => {
// if (!keyword) {
// // 关键词为空时清空选项(可选)
// options[type].options = []
// return
// }
debouncedRemoteMethod(type, keyword)
}
// remoteMethod('reconciliationCompany', searchFormData.reconciliationCompany)
// remoteMethod('insurer', searchFormData.insurerBizId)
const handleExport = () => {
ElMessage.info('点击导出按钮')
}
// 按钮事件处理
const handleAdd = () => ElMessage.info('点击新增按钮')
const handleImport = () => ElMessage.info('点击导入按钮')
const handleExport = () => ElMessage.info('点击导出按钮')
const handleReset = () => {
// 重置搜索表单
......@@ -399,42 +467,23 @@ const handleReset = () => {
searchFormData[key] = ''
}
})
// 重置下拉框选项
Object.keys(options).forEach(key => {
options[key].options = []
})
ElMessage.success('搜索条件已重置')
// 重新加载数据
loadTableData()
}
const handleQuery = async () => {
loadTableData()
}
const handleQuery = () => loadTableData()
// 按钮配置
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 }
])
// 分页事件
......@@ -450,12 +499,14 @@ const handleCurrentChange = (val) => {
// 加载表格数据
const loadTableData = async () => {
console.log(searchFormData)
loading.value = true
try {
const params = {
...searchFormData,
currentPage: currentPage.value,
commissionDateStart: searchFormData.incomeDateRange[0] || '',
commissionDateEnd: searchFormData.incomeDateRange[1] || '',
incomeDateRange: undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
}
const response = await receivedFortuneList(params)
......@@ -469,13 +520,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
}
......@@ -485,66 +534,123 @@ const loadTableData = async () => {
const loadEntryRecordData = async (cbd) => {
loading.value = true
try {
const params = {
commissionBizId: cbd
}
const params = { commissionBizId: cbd }
const response = await commissionEntryRecord(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 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: '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: 'commissionRatio', label: '入账比例', width: '150' },
{ property: 'commissionDate', label: '入账日', width: '150' },
{ property: 'status', label: '入账状态', width: '150' }
]
// 加载真实数据
const records = await loadEntryRecordData(row.commissionExpectedBizId)
entryRecordDialogTableData.value = records.length ? records : [{
commissionPeriod: '2023-08',
totalPeriod: '10',
exchangeRate: '1.2345',
currency: 'CNY',
amount: '10000.00',
commissionRatio: '10%',
commissionDate: '2023-08-01',
status: '已入账'
}]
} else if (e === 'setStatus') {
// 回显当前行状态
form.status = row.status
form.desc = ''
setStatusDialogTableVisible.value = true
}
}
// 查看比对记录
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 : [{
checkMonth: '2023-08',
compareStatus: '比对成功',
entryRatio: '10%',
shouldEntryRatio: '10%',
reconciliationCompany: 'XX保险公司',
createTime: '2023-08-01 10:00:00',
userName: '张三'
}]
}
// 新增:设置状态确认事件
const handleConfirmSetStatus = () => {
if (!form.status) {
return ElMessage.warning('请选择入账状态')
}
if (!form.desc) {
return ElMessage.warning('请输入修改理由')
}
// 调用接口修改状态(示例)
ElMessageBox.confirm('确定要修改入账状态吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// 此处替换为真实接口调用
ElMessage.success('状态修改成功')
setStatusDialogTableVisible.value = false
loadTableData() // 重新加载表格
}).catch(() => {
ElMessage.info('已取消修改')
})
}
// 初始化加载数据
loadTableData()
onMounted(() => {
loadTableData()
})
</script>
<style scoped lang="scss">
.statistics-cards {
margin-bottom: 5px;
background: RGBA(0, 82, 217, 0.03);
border-radius: 8px;
padding: 10px 20px;
}
.page-search-container {
padding: 20px;
background: #fff;
border-radius: 8px;
margin-bottom: 20px;
}
.search-actions {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
text-align: right;
}
.el-menu {
border: none;
}
</style>
\ No newline at end of file
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