Commit f43a9d6e by yuzhenWang

Merge branch 'test' into 'feature-20250827wyz-写业务'

Test

See merge request !32
parents 4548137e c58ec623
...@@ -33,6 +33,9 @@ ...@@ -33,6 +33,9 @@
color: #383838; color: #383838;
padding: 0.2em 0; padding: 0.2em 0;
} }
.el-dialog__header.dialog-header.show-close {
padding-right: 0 ;
}
#loader-wrapper { #loader-wrapper {
position: fixed; position: fixed;
......
...@@ -42,13 +42,16 @@ ...@@ -42,13 +42,16 @@
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.0.3",
"@vitejs/plugin-vue": "5.2.4", "@vitejs/plugin-vue": "5.2.4",
"sass-embedded": "1.89.1", "sass-embedded": "1.89.1",
"typescript": "^5.9.3",
"unplugin-auto-import": "0.18.6", "unplugin-auto-import": "0.18.6",
"unplugin-vue-setup-extend-plus": "1.0.1", "unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "6.3.5", "vite": "6.3.5",
"vite-plugin-compression": "0.5.1", "vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1" "vite-plugin-svg-icons": "2.0.1",
"vue-tsc": "^3.1.8"
}, },
"overrides": { "overrides": {
"quill": "2.0.2" "quill": "2.0.2"
......
<template> <template>
<router-view />
<el-watermark :font="font" :content="content">
<router-view />
</el-watermark>
</template> </template>
<script setup> <script setup>
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme' import { handleThemeStyle } from '@/utils/theme'
import { reactive } from 'vue'
const font = reactive({
color: 'rgba(0, 0, 0, .15)',
})
const content = reactive('csf')
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
......
...@@ -256,3 +256,19 @@ export function expectedFortuneStatistics(data) { ...@@ -256,3 +256,19 @@ export function expectedFortuneStatistics(data) {
data: data data: data
}) })
} }
// 入账记录查询
export function commissionEntryRecord(data) {
return request({
url: '/csf/api/commission/compare/records',
method: 'post',
data: data
})
}
// 入账操作记录查询
export function commissionEntryEditRecords(data) {
return request({
url: '/csf/api/commission/edit/records',
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
<!-- 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>
<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>
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
:close-on-click-modal="closeOnClickModal" :close-on-click-modal="closeOnClickModal"
:before-close="handleClose" :before-close="handleClose"
center center
:show-close="false" :show-close="showClose"
header-class="dialog-header"
> >
<template #header> <template #header>
<div class="titleBox">{{ dialogTitle }}</div> <div class="titleBox">{{ dialogTitle }}</div>
...@@ -18,7 +19,7 @@ ...@@ -18,7 +19,7 @@
</div> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer" v-if="showAction">
<!-- 取消按钮 --> <!-- 取消按钮 -->
<el-button type="info" plain v-if="showCancle" @click="close">{{ cancleText }}</el-button> <el-button type="info" plain v-if="showCancle" @click="close">{{ cancleText }}</el-button>
<!-- 确认按钮 --> <!-- 确认按钮 -->
...@@ -50,7 +51,14 @@ const props = defineProps({ ...@@ -50,7 +51,14 @@ const props = defineProps({
type: String, type: String,
default: '确认' default: '确认'
}, },
showAction: {
type: Boolean,
default: true
},
showClose: {
type: Boolean,
default: false
},
showConfirm: { showConfirm: {
type: Boolean, type: Boolean,
default: true default: true
...@@ -78,11 +86,9 @@ const props = defineProps({ ...@@ -78,11 +86,9 @@ const props = defineProps({
const showDialog = ref(props.openDialog) const showDialog = ref(props.openDialog)
const emit = defineEmits(['confirm', 'close']) const emit = defineEmits(['confirm', 'close'])
const close = () => { const close = () => {
showDialog.value = false
emit('close') emit('close')
} }
const confirm = () => { const confirm = () => {
showDialog.value = false
emit('confirm') emit('confirm')
} }
const handleClose = done => { const handleClose = done => {
...@@ -109,6 +115,7 @@ watch( ...@@ -109,6 +115,7 @@ watch(
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 15px 0; padding: 15px 0;
color: rgba(0,0,0,0.9);
} }
.content { .content {
padding: 0 15px; padding: 0 15px;
...@@ -120,4 +127,5 @@ watch( ...@@ -120,4 +127,5 @@ watch(
align-items: center; align-items: center;
padding: 20px 0; padding: 20px 0;
} }
</style> </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
// src/types/search-form.ts
export type FormFieldType =
| 'input' // 输入框
| 'textarea' // 文本域
| 'select' // 普通下拉框
| 'multi-select' // 多选下拉框
| 'remote-select' // 远程搜索下拉框(单选)
| 'remote-multi-select' // 远程搜索下拉框(多选)
| 'date' // 日期选择器
| 'daterange' // 日期范围选择器
| 'datetime' // 日期时间选择器
| 'datetimerange' // 日期时间范围选择器
| 'cascader' // 级联选择器
| 'radio' // 单选框
| 'checkbox' // 复选框
| 'switch' // 开关
| 'number' // 数字输入框
| 'password' // 密码输入框
| 'custom' // 自定义组件
export interface FormOption {
label: string
value: string | number
disabled?: boolean
children?: FormOption[]
[key: string]: any
}
export interface RemoteSearchConfig {
type: string
apiMethod: (params: any) => Promise<any>
formatResult?: (data: any[]) => FormOption[]
cacheKey?: string
debounceDelay?: number
defaultOptions?: FormOption[]
params?: Record<string, any>
}
export interface ValidationRule {
required?: boolean
message?: string
trigger?: 'blur' | 'change' | ['blur', 'change']
validator?: (rule: any, value: any, callback: (error?: Error) => void) => void
pattern?: RegExp
min?: number
max?: number
len?: number
type?: 'string' | 'number' | 'boolean' | 'method' | 'regexp' | 'integer' | 'float' | 'array' | 'object' | 'enum' | 'date' | 'url' | 'hex' | 'email'
enum?: Array<string | number>
transform?: (value: any) => any
}
export interface FormField {
// 基础配置
type: FormFieldType
field: string
label: string
defaultValue?: any
// 显示配置
placeholder?: string
width?: string | number
colSpan?: number // 栅格占据的列数 (1-24)
hidden?: boolean
disabled?: boolean
readonly?: boolean
clearable?: boolean
// 选项配置(用于select/radio/checkbox等)
options?: FormOption[]
optionLabel?: string
optionValue?: string
// 远程搜索配置
remoteConfig?: RemoteSearchConfig
// 特殊类型配置
dateFormat?: string
rangeSeparator?: string
startPlaceholder?: string
endPlaceholder?: string
showPassword?: boolean
minRows?: number
maxRows?: number
showWordLimit?: boolean
maxlength?: number
step?: number
precision?: number
// 校验规则
rules?: ValidationRule[]
// 自定义组件
component?: any
props?: Record<string, any>
slots?: Record<string, any>
// 事件
events?: Record<string, Function>
// 样式
class?: string
style?: Record<string, string | number>
}
export interface SearchFormProps {
modelValue: Record<string, any>
fields: FormField[]
labelWidth?: string | number
labelPosition?: 'left' | 'right' | 'top'
inline?: boolean
size?: 'large' | 'default' | 'small'
disabled?: boolean
showReset?: boolean
showSearch?: boolean
resetText?: string
searchText?: string
gutter?: number
rowClass?: string
formClass?: string
// 自定义插槽
prefixSlot?: boolean
suffixSlot?: boolean
extraSlot?: boolean
// 搜索和重置前的钩子
beforeSearch?: (formData: Record<string, any>) => boolean | Promise<boolean>
beforeReset?: () => boolean | Promise<boolean>
}
export interface SearchFormEmits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', formData: Record<string, any>): void
(e: 'reset', formData: Record<string, any>): void
(e: 'change', field: string, value: any): void
(e: 'field-validate', field: string, isValid: boolean, message?: string): void
}
\ No newline at end of file
// 格式化金额为货币格式
export function formatCurrency(value,currency='') {
if (value === undefined || value === null) return currency + '0.00'
return currency + value.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
}
export default {
formatCurrency
}
\ 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"
>
<!-- 搜索区域 -->
<template #searchForm>
<SearchForm
ref="searchFormRef"
v-model="searchFormData"
:fields="searchFields"
label-position="top"
:label-width="null"
:inline="false"
:gutter="20"
class="custom-search-form"
/>
</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">
<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>
</template>
<script setup name="Payables">
import CommonPage from '@/components/commonPage'
import { ref, reactive, onMounted, watch } 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: '',
})
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: '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: '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: 'remote-multi-select',
field: 'insurerBizId',
label: '保险公司',
placeholder: '请输入关键词搜索保险公司',
colSpan: 6,
remoteConfig: {
type: 'insurer',
apiMethod: (params) => searchCompanies({ ...params, type: 'insurer' }),
defaultOptions: []
}
},
{
type: 'remote-multi-select',
field: 'productLaunchBizId',
label: '产品计划',
placeholder: '请输入关键词搜索产品计划',
colSpan: 6,
remoteConfig: {
type: 'product',
apiMethod: (params) => searchCompanies({ ...params, type: 'product' }),
defaultOptions: []
}
},
{
type: 'select',
field: 'commissionBizType',
label: '应收款类型',
placeholder: '请选择应收款类型',
colSpan: 6,
options: [
{ label: '全部', value: '' },
{ label: '关联保单应收单', value: '1' },
{ label: '非关联保单应收单', value: '2' }
]
},
{
type: 'remote-multi-select',
field: 'teamBizId',
label: '出单团队',
placeholder: '请输入关键词搜索出单团队',
colSpan: 6,
remoteConfig: {
type: 'team',
apiMethod: (params) => searchCompanies({ ...params, type: 'team' }),
defaultOptions: []
}
}
])
// 表格数据
const tableData = ref([])
// 统计信息
const statisticsData = ref({
totalExpectedAmount: 0,
totalPaidAmount: 0,
totalUnpaidAmount: 0,
paidAmountRatio: 0,
totalPolicyCount: 0,
totalPremiumAmount: 0
})
// 按钮事件处理
const handleAdd = () => {
ElMessage.info('点击新增按钮')
}
const handleImport = () => {
ElMessage.info('点击导入按钮')
}
const handleExport = () => {
ElMessage.info('点击导出按钮')
}
const handleReset = () => {
// 重置搜索表单
Object.keys(searchFormData).forEach(key => {
if (Array.isArray(searchFormData[key])) {
searchFormData[key] = []
} else {
searchFormData[key] = ''
}
})
ElMessage.success('搜索条件已重置')
// 重新加载数据
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([
{
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
}
])
// 分页事件
const handleSizeChange = (val) => {
pageSize.value = val
loadTableData()
}
const handleCurrentChange = (val) => {
currentPage.value = val
loadTableData()
}
// 加载表格数据
const loadTableData = async () => {
loading.value = true
try {
const params = {
...searchFormData,
currentPage: currentPage.value,
pageSize: pageSize.value
}
const response = await expectedFortuneList(params)
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
}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 初始化加载数据
onMounted(() => {
loadTableData()
})
</script>
<style scoped lang="scss">
.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;
}
</style>
\ No newline at end of file
<template> statistics-cards<template>
<CommonPage <CommonPage :operationBtnList="operationBtnList" :showSearchForm="true" :show-pagination="true" :total="pageTotal"
:operationBtnList="operationBtnList" :current-page="currentPage" :page-size="pageSize" @size-change="handleSizeChange"
:visibleDefaultButtons="visibleDefaultButtons" @current-change="handleCurrentChange">
:showSearchForm="true" <!-- 搜索区域 -->
:show-pagination="true" <template #searchForm>
> <SearchForm ref="searchFormRef" v-model="searchFormData" :fields="searchFields" label-position="top"
<!-- 搜索区域 --> :label-width="null" :inline="false" :gutter="20" class="custom-search-form" />
<template #searchForm> </template>
<el-form <!-- 列表区域 -->
:model="searchFormData" <template #table>
ref="searchFormRef" <!-- 统计信息卡片 -->
label-width="120px" <div class="statistics-cards" v-if="statisticsData.totalPolicyCount > 0">
label-position="top" <el-row :gutter="20">
class="search-form" <el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
> <el-statistic title="应收款总金额" :value="statisticsData.totalAmount" />
<el-row :gutter="20"> </el-col>
<el-col <el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
:xs="24" :sm="12" :md="8" :lg="6" :xl="6" <el-statistic title="已入账金额" :value="statisticsData.totalPaidAmount" />
v-for="item in searchFormItems" </el-col>
:key="item.value" <el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
> <el-statistic title="待入账金额(估)" :value="statisticsData.pendingPaidAmount" />
<el-form-item </el-col>
:label="item.label" <el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
:prop="item.value" <el-statistic title="已入账比例" :value="statisticsData.paidAmountRatio" />
:rules="item.rules" </el-col>
> <el-col :xs="24" :sm="12" :md="4" class="text-center mb-4">
<template v-if="item.type === 'input'"> <el-statistic title="总保单数" :value="statisticsData.totalPolicyCount" />
<el-input </el-col>
v-model="searchFormData[item.value]" </el-row>
:placeholder="item.placeholder" </div>
size="large" <!-- 应收款管理列表 -->
clearable <el-table :data="tableData" height="400" border highlight-current-row style="width: 100%"
/> v-loading="loading">
</template> <el-table-column prop="commissionBizType" label="应收款类型" width="120" fixed="left" sortable />
<el-table-column prop="receivableNo" label="应收款编号" width="120" />
<template v-else-if="item.type === 'select'"> <el-table-column prop="policyNo" label="保单号" width="120" />
<el-select <el-table-column prop="reconciliationCompany" label="对账公司" width="120" sortable />
v-model="searchFormData[item.value]" <el-table-column prop="status" label="入账状态" width="120" sortable>
:placeholder="item.placeholder" <template #default="{ row }">
size="large" <el-tag :type="row.status === '1' ? 'success' : 'warning'">
clearable {{ row.status === '1' ? '已入账' : '待入账' }}
> </el-tag>
<el-option </template>
v-for="option in item.options" </el-table-column>
:key="option.value" <el-table-column prop="commissionPeriod" label="入账期数" width="120" sortable />
:label="option.label" <el-table-column prop="totalPeriod" label="入账总期数" width="120" sortable />
:value="option.value" <el-table-column prop="commissionType" 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 }">
{{ (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="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) }}
</template>
</el-table-column>
<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="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" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button link type="primary" size="small" @click="handleClick">
查看比对记录
</el-button>
</template>
</el-table-column>
</el-table>
</CommonDialog>
<!-- 操作记录表格弹窗-->
<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>
</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-select>
</template>
<template v-else-if="item.type === 'daterange'">
<el-date-picker
v-model="searchFormData[item.value]"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="large"
clearable
style="width: 100%"
/>
</template>
</el-form-item> </el-form-item>
</el-col> <el-form-item label="修改理由" :label-width="120">
</el-row> <el-input v-model="form.desc" type="textarea" />
</el-form> </el-form-item>
</template> </el-form>
<template #footer>
<!-- 列表区域 --> <div class="dialog-footer">
<template #table> <el-button @click="setStatusDialogTableVisible = false">取消</el-button>
<!-- 统计信息卡片 --> <el-button type="primary" @click="setStatusDialogTableVisible = false">
<div class="statistics-cards" v-if="statisticsData.totalPolicyCount > 0"> 确认
<el-row :gutter="20"> </el-button>
<el-col :xs="24" :sm="12" :md="6" :lg="6"> </div>
<el-card shadow="hover" class="statistics-card"> </template>
<div class="card-content"> </CommonDialog>
<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="6" :lg="6">
<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="6" :lg="6">
<el-card shadow="hover" class="statistics-card">
<div class="card-content">
<div class="card-label">待入账金额</div>
<div class="card-value">{{ formatCurrency(statisticsData.pendingAmount) }}</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6" :lg="6">
<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="350"
border
highlight-current-row
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="commissionBizType" label="应收款类型" width="120" fixed="left" />
<el-table-column prop="receivableNo" label="应收款编号" width="120" />
<el-table-column prop="policyNo" label="保单号" width="120" />
<el-table-column prop="reconciliationCompany" label="对账公司" width="120" />
<el-table-column prop="status" label="入账状态" width="100">
<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="100" />
<el-table-column prop="totalPeriod" label="入账总期数" width="100" />
<el-table-column prop="commissionType" label="入账项目" width="100" />
<el-table-column prop="commissionDate" label="入账日(估)" width="120" />
<el-table-column prop="commissionRatio" label="入账比例(估)" width="120">
<template #default="{ row }">
{{ (row.commissionRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="expectedAmount" label="入账金额(估)" width="120">
<template #default="{ row }">
{{ formatCurrency(row.expectedAmount) }}
</template>
</el-table-column>
<el-table-column prop="paidRatio" label="已入账比例" width="120">
<template #default="{ row }">
{{ (row.paidRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="paidAmount" label="已入账金额" width="120">
<template #default="{ row }">
{{ formatCurrency(row.paidAmount) }}
</template>
</el-table-column>
<el-table-column prop="pendingRatio" label="待入账比例" width="120">
<template #default="{ row }">
{{ (row.pendingRatio || 0) + '%' }}
</template>
</el-table-column>
<el-table-column prop="pendingAmount" label="待入账金额(估)" width="120">
<template #default="{ row }">
{{ formatCurrency(row.pendingAmount) }}
</template>
</el-table-column>
<el-table-column prop="defaultExchangeRate" label="结算汇率(估)" width="120" />
<el-table-column prop="insurerBizId" label="保险公司" width="120" />
<el-table-column prop="productLaunchBizId" label="产品计划" width="120" />
<el-table-column prop="premium" label="期交保费" width="120">
<template #default="{ row }">
{{ formatCurrency(row.premium) }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="150" />
<el-table-column fixed="right" label="操作" width="150">
<template #default="{ row }">
<el-button
link
type="primary"
size="small"
@click="handleViewDetail(row)"
>
查看
</el-button>
<el-button
link
type="danger"
size="small"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
</CommonPage>
</template> </template>
<script setup lang="ts"> <script setup name="Receivables">
import { ref, reactive, onMounted, watch } from 'vue'
import CommonPage from '@/components/commonPage' import CommonPage from '@/components/commonPage'
import type { OperationButton } from '@/components/commonPage' import CommonDialog from '@/components/commonDialog'
import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' 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 currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
const pageTotal = ref(0) const pageTotal = ref(0)
const loading = ref(false) 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([])
const form = reactive({
status: '',
desc: '',
})
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' }
]
// 加载入账记录-假数据等待接口修改
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
}
}
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({ const searchFormData = reactive({
policyNo: '', policyNo: '',
incomeDateRange: [] as string[], incomeDateRange: [], // 改为数组格式
incomeStatus: '', statusList: [],
incomeTerm: '', commissionNameList: [], // 修改字段名
incomeItem: '', commissionPeriod: '',
outTeam: '', reconciliationCompanyList: [], // 修改字段名
insurer: '', insurerBizId: [],
productPlan: '', productLaunchBizId: [],
paymentType: '', commissionBizType: '',
intermediary: '', teamBizId: '',
signer: ''
}) })
// 搜索表单项配置 const searchFields = ref([
const searchFormItems = ref([ {
{ type: 'input',
label: '保单号', field: 'policyNo',
value: 'policyNo', label: '保单号',
type: 'input', placeholder: '请输入保单号',
placeholder: '请输入保单号' colSpan: 6,
}, clearable: true,
{ rules: [
label: '入账日(估)', { max: 50, message: '保单号长度不能超过50个字符', trigger: 'blur' }
value: 'incomeDateRange', ]
type: 'daterange', },
placeholder: '请选择入账日(估)' {
}, type: 'daterange',
{ field: 'incomeDateRange',
label: '入账状态', label: '入账日期',
value: 'incomeStatus', startPlaceholder: '开始日期',
type: 'select', endPlaceholder: '结束日期',
placeholder: '请选择入账状态', colSpan: 6,
options: [ dateFormat: 'YYYY-MM-DD',
{ label: '全部', value: '' }, props: {
{ label: '已入账', value: '1' }, valueFormat: 'YYYY-MM-DD',
{ label: '待入账', value: '0' } style: 'width: 100%'
] }
}, },
{ {
label: '入账期数', type: 'multi-select',
value: 'incomeTerm', field: 'statusList',
type: 'input', label: '入账状态',
placeholder: '请输入入账期数' placeholder: '请选择入账状态',
}, colSpan: 6,
{ remoteConfig: {
label: '入账项目', type: 'commissionType',
value: 'incomeItem', apiMethod: searchCommissionTypes,
type: 'select', formatResult: (data) => data.map(item => ({
placeholder: '请选择入账项目', label: item.typeName,
options: [ value: item.typeCode,
{ label: '全部', value: '' }, remark: item.remark
{ label: '佣金', value: '1' }, })),
{ label: '服务费', value: '2' } defaultOptions: [
] { label: '已入账', value: '1' },
}, { label: '待入账', value: '0' },
{ { label: '部分入账', value: '2' }
label: '出单团队', ]
value: 'outTeam', }
type: 'select', },
placeholder: '请选择出单团队', {
options: [ type: 'input',
{ label: '全部', value: '' }, field: 'commissionNameList',
{ label: '团队A', value: '1' }, label: '入账项目',
{ label: '团队B', value: '2' } placeholder: '请输入关键词搜索',
] colSpan: 6,
}, },
{ {
label: '保险公司', type: 'remote-multi-select',
value: 'insurer', field: 'reconciliationCompanyList',
type: 'select', label: '对账公司',
placeholder: '请选择保险公司', placeholder: '请输入关键词搜索',
options: [ colSpan: 6,
{ label: '全部', value: '' }, remoteConfig: {
{ label: '保险公司A', value: '1' }, type: 'company',
{ label: '保险公司B', value: '2' } apiMethod: (params) => searchCompanies({ ...params, type: 'reconciliation' }),
] defaultOptions: []
}, }
{ },
label: '产品计划', {
value: 'productPlan', type: 'input',
type: 'select', field: 'commissionPeriod',
placeholder: '请选择产品计划', label: '入账期数',
options: [ placeholder: '请输入期数',
{ label: '全部', value: '' }, colSpan: 6,
{ label: '计划A', value: '1' }, },
{ label: '计划B', value: '2' } {
] type: 'input',
}, field: 'insurerBizId',
{ label: '保险公司',
label: '应收款类型', placeholder: '请输入关键词搜索保险公司',
value: 'paymentType', colSpan: 6,
type: 'select', },
placeholder: '请选择应收款类型', {
options: [ type: 'input',
{ label: '全部', value: '' }, field: 'productLaunchBizId',
{ label: '关联保单应收单', value: '1' }, label: '产品计划',
{ label: '非关联保单应收单', value: '2' } placeholder: '请输入关键词搜索产品计划',
] colSpan: 6,
}, },
{ {
label: '转介人', type: 'select',
value: 'intermediary', field: 'commissionBizType',
type: 'select', label: '应收款类型',
placeholder: '请选择转介人', placeholder: '请选择应收款类型',
options: [ colSpan: 6,
{ label: '全部', value: '' }, options: [
{ label: '转介人A', value: '1' }, { label: '全部', value: '' },
{ label: '转介人B', value: '2' } { label: '关联保单应收单', value: '1' },
] { label: '非关联保单应收单', value: '2' }
}, ]
{ },
label: '签单员', {
value: 'signer', type: 'input',
type: 'select', field: 'teamBizId',
placeholder: '请选择签单员', label: '出单团队',
options: [ placeholder: '请输入关键词搜索出单团队',
{ label: '全部', value: '' }, colSpan: 6,
{ label: '签单员A', value: '1' }, }
{ label: '签单员B', value: '2' }
]
}
]) ])
// 表格数据 // 表格数据
const tableData = ref<any[]>([ const tableData = ref([])
// 示例数据
{
id: 1,
commissionBizType: '佣金',
receivableNo: 'YSK001',
policyNo: 'POL001',
reconciliationCompany: '对账公司A',
status: '1',
commissionPeriod: 1,
totalPeriod: 12,
commissionType: '首期佣金',
commissionDate: '2024-01-15',
commissionRatio: 8.5,
expectedAmount: 8500,
paidRatio: 100,
paidAmount: 8500,
pendingRatio: 0,
pendingAmount: 0,
defaultExchangeRate: 1.0,
insurerBizId: '保险公司A',
productLaunchBizId: '计划A',
premium: 100000,
remark: '备注信息'
},
{
id: 2,
commissionBizType: '服务费',
receivableNo: 'YSK002',
policyNo: 'POL002',
reconciliationCompany: '对账公司B',
status: '0',
commissionPeriod: 2,
totalPeriod: 12,
commissionType: '服务费',
commissionDate: '2024-02-15',
commissionRatio: 5.0,
expectedAmount: 5000,
paidRatio: 50,
paidAmount: 2500,
pendingRatio: 50,
pendingAmount: 2500,
defaultExchangeRate: 1.0,
insurerBizId: '保险公司B',
productLaunchBizId: '计划B',
premium: 50000,
remark: ''
}
])
// 统计信息 // 统计信息
const statisticsData = ref({ const statisticsData = ref({
totalAmount: 13500, totalAmount: 0,
totalPaidAmount: 11000, totalPaidAmount: 0,
pendingAmount: 2500, pendingPaidAmount: 0,
paidRatio: 81.48, // (11000/13500*100) paidAmountRatio: 0,
totalPolicyCount: 2 totalPolicyCount: 0,
paidAmountRatio: 0
}) })
// 货币格式化
const formatCurrency = (value: number) => {
if (value === undefined || value === null) return '¥0.00'
return '¥' + value.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
}
// 按钮事件处理 // 按钮事件处理
const handleAdd = () => { const handleAdd = () => {
ElMessage.info('点击新增按钮') ElMessage.info('点击新增按钮')
// 这里可以打开新增弹窗
// proxy.$refs.addForm.resetFields()
} }
const handleImport = () => { const handleImport = () => {
ElMessage.info('点击导入按钮') ElMessage.info('点击导入按钮')
// 这里可以打开导入弹窗
// proxy.$refs.importForm.resetFields()
} }
const handleExport = () => { const handleExport = () => {
ElMessage.info('点击导出按钮') ElMessage.info('点击导出按钮')
// 这里可以执行导出逻辑
// proxy.$refs.exportForm.resetFields()
} }
const handleReset = () => { const handleReset = () => {
// 重置搜索表单 // 重置搜索表单
Object.keys(searchFormData).forEach(key => { Object.keys(searchFormData).forEach(key => {
if (Array.isArray(searchFormData[key])) { if (Array.isArray(searchFormData[key])) {
searchFormData[key] = [] searchFormData[key] = []
} else { } else {
searchFormData[key] = '' searchFormData[key] = ''
} }
}) })
ElMessage.success('搜索条件已重置') ElMessage.success('搜索条件已重置')
// 重新加载数据 // 重新加载数据
loadTableData() loadTableData()
} }
const handleQuery = async () => { const handleQuery = async () => {
// 表单验证 loadTableData()
// const valid = await proxy.$refs.searchFormRef.validate()
// if (!valid) return
ElMessage.info('执行查询操作')
loadTableData()
} }
// 控制要显示的默认按钮 // 控制要显示的默认按钮
const visibleDefaultButtons = ref(['add', 'import', 'export']) // 只显示新增和查询两个默认按钮 const visibleDefaultButtons = ref(['add', 'import', 'export']) // 只显示新增和查询两个默认按钮
// 按钮配置 // 按钮配置
const operationBtnList = ref<OperationButton[]>([ const operationBtnList = ref([
{ {
key: 'add', key: 'add',
direction: 'left', direction: 'left',
click: handleAdd click: handleAdd
}, },
{ {
key: 'import', key: 'import',
direction: 'left', direction: 'left',
click: handleImport click: handleImport
}, },
{ {
key: 'export', key: 'export',
direction: 'right', direction: 'right',
click: handleExport click: handleExport
}, },
{ {
key: 'reset', key: 'reset',
direction: 'right', direction: 'right',
click: handleReset click: handleReset
}, },
{ {
key: 'query', key: 'query',
direction: 'right', direction: 'right',
click: handleQuery click: handleQuery
} }
]) ])
// 表格操作
const handleViewDetail = (row: any) => {
ElMessage.info(`查看应收款详情:${row.receivableNo}`)
// 这里可以打开详情弹窗或跳转到详情页
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(
`确定要删除应收款 "${row.receivableNo}" 吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 执行删除操作
tableData.value = tableData.value.filter(item => item.id !== row.id)
ElMessage.success('删除成功')
// 重新计算统计信息
calculateStatistics()
} catch {
// 用户取消了操作
}
}
// 分页事件 // 分页事件
const handleSizeChange = (val: number) => { const handleSizeChange = (val) => {
pageSize.value = val pageSize.value = val
loadTableData() loadTableData()
} }
const handleCurrentChange = (val: number) => { const handleCurrentChange = (val) => {
currentPage.value = val currentPage.value = val
loadTableData() loadTableData()
} }
// 加载表格数据 // 加载表格数据
const loadTableData = async () => { const loadTableData = async () => {
loading.value = true loading.value = true
try { try {
// 模拟API调用 const params = {
await new Promise(resolve => setTimeout(resolve, 500)) ...searchFormData,
currentPage: currentPage.value,
// 这里应该调用真实的API接口 pageSize: pageSize.value
// const params = { }
// ...searchFormData, const response = await receivedFortuneList(params)
// currentPage: currentPage.value, tableData.value = response.data.page.records
// pageSize: pageSize.value pageTotal.value = response.data.page.total
// } pageSize.value = response.data.page.size
// const response = await api.getReceivablesList(params)
// tableData.value = response.data.list // 统计信息
// pageTotal.value = response.data.total statisticsData.value = {
totalAmount: response.data.expectedStatisticsVO.totalAmount,
// 计算统计信息 totalPaidAmount: response.data.expectedStatisticsVO.totalPaidAmount,
calculateStatistics() pendingPaidAmount: response.data.expectedStatisticsVO.pendingPaidAmount,
paidAmountRatio: response.data.expectedStatisticsVO.paidAmountRatio,
} catch (error) { totalPolicyCount: response.data.expectedStatisticsVO.totalPolicyCount,
console.error('加载数据失败:', error) paidAmountRatio: response.data.expectedStatisticsVO.paidAmountRatio
ElMessage.error('加载数据失败') }
} finally {
loading.value = false } catch (error) {
} console.error('加载数据失败:', error)
// ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
} }
// 计算统计信息 // 入账记录查询
const calculateStatistics = () => { const loadEntryRecordData = async (cbd) => {
const data = tableData.value loading.value = true
const totalAmount = data.reduce((sum, item) => sum + (item.expectedAmount || 0), 0) try {
const totalPaidAmount = data.reduce((sum, item) => sum + (item.paidAmount || 0), 0) const params = {
const pendingAmount = totalAmount - totalPaidAmount commissionBizId: cbd
const paidRatio = totalAmount > 0 ? (totalPaidAmount / totalAmount * 100) : 0 }
const response = await commissionEntryRecord(params)
statisticsData.value = { const records = response.data.records
totalAmount, return records
totalPaidAmount, } catch (error) {
pendingAmount, console.error('加载数据失败:', error)
paidRatio: Number(paidRatio.toFixed(2)), // ElMessage.error('加载数据失败')
totalPolicyCount: data.length } finally {
} loading.value = false
}
}
// 入账操作记录查询
const loadEntryEditRecordData = async (cbd) => {
loading.value = true
try {
const params = {
commissionBizId: cbd
}
const response = await commissionEntryEditRecords(params)
const records = response.data.records
return records
} catch (error) {
console.error('加载数据失败:', error)
// ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
} }
// 监听搜索条件变化(可选:自动查询)
watch(
() => searchFormData,
() => {
// 如果需要自动查询,可以在这里调用
// debounce(() => loadTableData(), 500)
},
{ deep: true }
)
// 初始化加载数据 // 初始化加载数据
onMounted(() => { onMounted(() => {
loadTableData() loadTableData()
calculateStatistics()
}) })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.search-form {
padding: 10px 0;
:deep(.el-form-item) {
margin-bottom: 20px;
.el-form-item__label {
font-weight: 500;
}
}
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid var(--el-border-color-lighter);
}
.statistics-cards { .statistics-cards {
margin-top: 20px; 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;
} }
.statistics-card { .search-actions {
height: 120px; margin-top: 20px;
margin-bottom: 20px; padding-top: 20px;
border-top: 1px solid #ebeef5;
.card-content { text-align: right;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.card-label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
}
.card-value {
font-size: 24px;
font-weight: 600;
color: var(--el-color-primary);
}
}
} }
// 响应式调整 .el-menu {
@media (max-width: 768px) { border: none;
.search-form {
:deep(.el-col) {
margin-bottom: 10px;
}
}
.statistics-cards {
.el-col {
margin-bottom: 15px;
}
}
} }
</style> </style>
\ No newline at end of file
{
"compilerOptions": {
"target": "es2020",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["es2020", "dom", "dom.iterable"],
"skipLibCheck": true,
// 重要:必须和 vite.config.js 中的别名对应
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"exclude": ["node_modules", "dist", "**/*.js"]
}
\ 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