Commit 20e7b750 by yuzhenWang

解决冲突

parents 1f7ca662 ef6e03ce
......@@ -36,7 +36,12 @@
.el-dialog__header.dialog-header.show-close {
padding-right: 0 ;
}
.statistics-container {
padding: 10px;
background-color: rgba(0,82,217,0.03);
border-radius: 4px;
margin-bottom: 5px;
}
#loader-wrapper {
position: fixed;
top: 0;
......
......@@ -151,3 +151,11 @@ export function getInsuranceCategory(data) {
method: 'post'
})
}
// 通用excel导入
export function importExcel(data) {
return request({
url: '/oss/api/excel/import',
method: 'post',
data: data
})
}
......@@ -9,14 +9,6 @@ export function getPolicyCommissionList(data) {
})
}
// 更新保单来佣信息
export function updatePolicyCommission(data) {
return request({
url: '/csf/api/commission/update',
method: 'post',
data: data
})
}
// 生成可出账记录
export function generateCommissionRecord(data) {
......@@ -64,7 +56,6 @@ export function downloadPolicyFortuneAccount(data) {
url: '/csf/api/fortune/download/account',
method: 'post',
data: data,
responseType: 'blob'
})
}
......@@ -256,7 +247,7 @@ export function expectedFortuneStatistics(data) {
data: data
})
}
// 入账记录查询
// 入账比对记录查询
export function commissionEntryRecord(data) {
return request({
url: '/csf/api/commission/compare/records',
......@@ -272,3 +263,143 @@ export function commissionEntryEditRecords(data) {
data: data
})
}
// 获取保单发佣列表
export function policyNoCommissionPayRecord(data) {
return request({
url: '/csf/api/fortune/list/page/vo',
method: 'post',
data: data
})
}
// 应收款导出
export function exportReceivedFortune(data) {
return request({
url: '/csf/api/CommissionExpected/export',
method: 'post',
data: data,
responseType: 'blob'
})
}
// 入账记录查询
export function commissionExpectedRecord(data) {
return request({
url: '/csf/api/commission/pageByCommissionexpectedBizId',
method: 'post',
data: data
})
}
// 出账记录查询
export function payRecordList(data) {
return request({
url: '/csf/api/fortune/pageByExpectedFortuneBizId',
method: 'post',
data: data
})
}
// 更新入账信息
export function updateCommissionExpected(data) {
return request({
url: '/csf/api/CommissionExpected/update',
method: 'post',
data: data
})
}
// 修改出账状态
export function updataPayrollStatus(data){
return request({
url: '/csf/api/fortune/update/status',
method: 'post',
data: data
})
}
// 批量新增检核记录
export function addPayrollCheckRecord(data){
return request({
url: '/csf/api/commission/addBatch',
method: 'post',
data: data
})
}
// 新增应收款
export function addReceivedFortune(data){
return request({
url: '/csf/api/CommissionExpected/add',
method: 'post',
data: data
})
}
// 新增出账记录
export function addPayRecord(data){
return request({
url: '/csf/api/expectedFortune/add',
method: 'post',
data: data
})
}
// 获取销售员详情
export function userSaleExpandDetail(data){
return request({
url: '/insurance/base/api/userSaleExpand/detail?userSaleBizId=' + data,
method: 'get',
})
}
// 更新比对状态
export function updateCompareStatus(data){
return request({
url: '/csf/api/commission/updateCompareStatus',
method: 'post',
data: data
})
}
// 更新数据
export function updateCommissionRecord(data){
return request({
url: '/csf/api/commission/update',
method: 'post',
data: data
})
}
// 新增出账检核记录
export function addCheckRecordaddBatch(data){
return request({
url: '/csf/api/fortune/addBatch',
method: 'post',
data: data
})
}
// 设置本期出账金额
export function updatePayoutAmount(data){
return request({
url: '/csf/api/fortune/update',
method: 'post',
data: data
})
}
// 同步预计来佣
export function syncExpectedCommission(data){
return request({
url: '/csf/api/commission/addToExpected',
method: 'post',
data: data
})
}
// 更新出账记录
export function updatePayRecord(data){
return request({
url: '/csf/api/expectedFortune/update',
method: 'post',
data: data
})
}
// src/api/search.ts
import request from '@/utils/request'
// 通用搜索接口
export function commonSearch(params: {
module: string
keyword?: string
pageSize?: number
}) {
return request({
url: '/common/search',
method: 'get',
params
})
}
// 特定模块搜索
export function searchCompanies(params: {
type?: string
keyword?: string
pageSize?: number
}) {
return request({
url: '/company/search',
method: 'get',
params
})
}
export function searchCommissionTypes(params: {
keyword?: string
pageSize?: number
}) {
return request({
url: '/commission/type/search',
method: 'get',
params
})
}
export function searchInsurers(params: {
keyword?: string
pageSize?: number
}) {
return request({
url: '/insurer/search',
method: 'get',
params
})
}
export function searchProducts(params: {
keyword?: string
pageSize?: number
}) {
return request({
url: '/product/search',
method: 'get',
params
})
}
\ No newline at end of file
......@@ -14,6 +14,7 @@
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
:drag="drag"
v-if="!disabled"
>
<!-- 上传按钮 -->
......
<!-- src/components/RemoteMultiSelect.vue -->
<template>
<el-select
v-model="selectedValues"
:placeholder="placeholder"
:multiple="multiple"
:filterable="remote || filterable"
:remote="remote"
:remote-method="handleRemoteSearch"
:loading="loading"
:reserve-keyword="false"
:clearable="clearable"
:collapse-tags="collapseTags"
:max-collapse-tags="maxCollapseTags"
:size="size"
:popper-class="['remote-multi-select', popperClass]"
:disabled="disabled"
@change="handleChange"
@visible-change="handleVisibleChange"
>
<!-- 自定义下拉头部:全选功能 -->
<template #header v-if="showCheckAll && multiple">
<div class="select-header">
<el-checkbox
v-model="checkAll"
:indeterminate="indeterminate"
@change="handleCheckAllChange"
:disabled="disabled"
>
{{ checkAllLabel }}
</el-checkbox>
</div>
</template>
<!-- 选项列表 -->
<el-option
v-for="option in options"
:key="getOptionKey(option)"
:label="getOptionLabel(option)"
:value="getOptionValue(option)"
:disabled="option.disabled"
/>
<!-- 无数据时的提示 -->
<template #empty>
<div class="empty-options">
<span v-if="loading">加载中...</span>
<span v-else-if="options.length === 0 && !hasSearched">请输入关键词搜索</span>
<span v-else>无匹配数据</span>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import { computed, ref, watch, toRefs, defineProps, defineEmits } from 'vue'
import { useRemoteSearch } from '@/hooks/useRemoteSearch'
import type { RemoteSearchConfig } from '@/hooks/useRemoteSearch'
interface OptionItem {
label: string
value: string | number
disabled?: boolean
[key: string]: any
}
interface Props {
modelValue: (string | number)[] | string | number
config: RemoteSearchConfig
placeholder?: string
multiple?: boolean
clearable?: boolean
collapseTags?: boolean
maxCollapseTags?: number
size?: 'large' | 'default' | 'small'
disabled?: boolean
showCheckAll?: boolean
checkAllLabel?: string
popperClass?: string
// 自定义选项键名
labelKey?: string
valueKey?: string
optionKey?: string
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => ([]),
placeholder: '请选择',
multiple: true,
clearable: true,
collapseTags: true,
maxCollapseTags: 1,
size: 'large',
disabled: false,
showCheckAll: true,
checkAllLabel: '全选',
labelKey: 'label',
valueKey: 'value',
optionKey: 'value'
})
const emit = defineEmits<{
'update:modelValue': [value: (string | number)[] | string | number]
'change': [value: (string | number)[] | string | number]
'search': [query: string]
}>()
const {
config,
multiple,
showCheckAll,
checkAllLabel,
labelKey,
valueKey,
optionKey
} = toRefs(props)
// 远程搜索实例
const remoteSearch = useRemoteSearch(config.value)
// 本地状态
const selectedValues = ref<(string | number)[] | string | number>(
Array.isArray(props.modelValue) ? [...props.modelValue] : props.modelValue
)
const hasSearched = ref(false)
const currentQuery = ref('')
// 计算属性
const loading = computed(() => remoteSearch.state.loading)
const options = computed(() => remoteSearch.state.options)
const checkAll = computed({
get() {
if (!props.multiple || !Array.isArray(selectedValues.value)) return false
const enabledOptions = options.value.filter(opt => !opt.disabled)
return selectedValues.value.length === enabledOptions.length && enabledOptions.length > 0
},
set(val: boolean) {
handleCheckAllChange(val)
}
})
const indeterminate = computed(() => {
if (!props.multiple || !Array.isArray(selectedValues.value)) return false
const enabledOptions = options.value.filter(opt => !opt.disabled)
return selectedValues.value.length > 0 && selectedValues.value.length < enabledOptions.length
})
// 方法
const getOptionKey = (option: OptionItem) => {
return option[optionKey.value] ?? option.value
}
const getOptionLabel = (option: OptionItem) => {
return option[labelKey.value] ?? option.label
}
const getOptionValue = (option: OptionItem) => {
return option[valueKey.value] ?? option.value
}
const handleRemoteSearch = async (query: string) => {
currentQuery.value = query
hasSearched.value = true
emit('search', query)
await remoteSearch.search(query)
}
const handleChange = (value: (string | number)[] | string | number) => {
selectedValues.value = value
emit('update:modelValue', value)
emit('change', value)
}
const handleCheckAllChange = (checked: boolean) => {
if (!props.multiple) return
if (checked) {
// 全选:只选择非禁用的选项
selectedValues.value = options.value
.filter(opt => !opt.disabled)
.map(opt => getOptionValue(opt))
} else {
selectedValues.value = []
}
emit('update:modelValue', selectedValues.value)
emit('change', selectedValues.value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && options.value.length === 0 && !hasSearched.value) {
// 下拉框打开时,如果没有搜索过,执行一次搜索
handleRemoteSearch('')
}
}
// 监听外部值变化
watch(() => props.modelValue, (newVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(selectedValues.value)) {
selectedValues.value = newVal
}
}, { deep: true })
// 监听配置变化
watch(() => props.config, (newConfig) => {
// 重新初始化远程搜索实例(实际实现中可能需要更复杂的处理)
Object.assign(remoteSearch, useRemoteSearch(newConfig))
}, { deep: true })
// 初始化
if (config.value.defaultOptions?.length > 0) {
remoteSearch.setDefaultOptions(config.value.defaultOptions)
}
// 暴露方法给父组件
defineExpose({
search: handleRemoteSearch,
clearCache: remoteSearch.clearCache,
preload: remoteSearch.preload
})
</script>
<style scoped>
.select-header {
padding: 8px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
background-color: var(--el-bg-color);
.el-checkbox {
width: 100%;
.el-checkbox__label {
font-weight: 500;
color: var(--el-color-primary);
}
}
}
.empty-options {
padding: 8px 12px;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 14px;
}
</style>
<style>
.remote-multi-select .el-select-dropdown__list {
padding-top: 0;
}
.remote-multi-select .el-select-dropdown__item {
padding: 8px 12px;
}
.remote-multi-select .el-select-dropdown__item.selected {
background-color: var(--el-color-primary-light-9);
}
.remote-multi-select .el-select-dropdown__item.is-disabled {
opacity: 0.6;
}
</style>
\ No newline at end of file
<template>
<el-select
ref="selectRef"
v-model="selectedValue"
:placeholder="placeholder"
:clearable="clearable"
:filterable="true"
:remote="true"
:remote-method="handleRemoteSearch"
:loading="loading"
:reserve-keyword="false"
:disabled="disabled"
:size="size"
:popper-class="['remote-select', popperClass]"
@change="handleChange"
@visible-change="handleVisibleChange"
>
<el-option
v-for="option in options"
:key="getOptionKey(option)"
:label="getOptionLabel(option)"
:value="getOptionValue(option)"
:disabled="option.disabled"
/>
<template #empty>
<div class="empty-options">
<span v-if="loading">加载中...</span>
<span v-else-if="options.length === 0 && !hasSearched">请输入关键词搜索</span>
<span v-else>无匹配数据</span>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useRemoteSearch } from '@/hooks/useRemoteSearch'
import type { RemoteSearchConfig, FormOption } from '@/types/search-form'
interface Props {
modelValue: string | number
config: RemoteSearchConfig
placeholder?: string
clearable?: boolean
disabled?: boolean
size?: 'large' | 'default' | 'small'
popperClass?: string
optionLabel?: string
optionValue?: string
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
placeholder: '请搜索选择',
clearable: true,
disabled: false,
size: 'default',
popperClass: '',
optionLabel: 'label',
optionValue: 'value'
})
const emit = defineEmits<{
'update:modelValue': [value: string | number]
'change': [value: string | number]
}>()
// 远程搜索实例
const remoteSearch = useRemoteSearch(props.config)
// 本地状态
const selectRef = ref()
const selectedValue = ref(props.modelValue)
const hasSearched = ref(false)
const currentQuery = ref('')
// 计算属性
const loading = computed(() => remoteSearch.state.loading)
const options = computed(() => remoteSearch.state.options)
// 方法
const getOptionKey = (option: FormOption) => {
return option[props.optionValue] ?? option.value
}
const getOptionLabel = (option: FormOption) => {
return option[props.optionLabel] ?? option.label
}
const getOptionValue = (option: FormOption) => {
return option[props.optionValue] ?? option.value
}
const handleRemoteSearch = async (query: string) => {
currentQuery.value = query
hasSearched.value = true
await remoteSearch.search(query)
}
const handleChange = (value: string | number) => {
selectedValue.value = value
emit('update:modelValue', value)
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && options.value.length === 0 && !hasSearched.value) {
handleRemoteSearch('')
}
}
// 监听外部值变化
watch(() => props.modelValue, (newVal) => {
if (newVal !== selectedValue.value) {
selectedValue.value = newVal
}
})
// 暴露方法
defineExpose({
search: handleRemoteSearch,
clearCache: remoteSearch.clearCache,
focus: () => selectRef.value?.focus()
})
// 初始化
onMounted(() => {
if (props.config.defaultOptions?.length > 0) {
remoteSearch.setDefaultOptions(props.config.defaultOptions)
}
})
</script>
\ No newline at end of file
......@@ -490,8 +490,5 @@ onMounted(() => {
margin-bottom: 8px;
}
.pagination-container {
justify-content: center;
}
}
</style>
<!-- ExcelUploadPreview.vue -->
<template>
<div class="excel-upload-preview">
<!-- 文件上传 -->
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv"
:before-upload="handleBeforeUpload" @change="handleFileChange">
<el-button type="primary">选择 Excel 文件</el-button>
<span v-if="fileName" class="ml-2">已选择:{{ fileName }}</span>
</el-upload>
<!-- 解析消息 -->
<el-alert v-if="parseMessage" :title="parseMessage" :type="parsedValid ? 'success' : 'warning'"
style="margin-top: 12px" />
<!-- 错误信息 -->
<el-alert v-if="errorMessages.length" :closable="false" type="error" style="margin-top: 12px">
<template #default>
<div v-for="(msg, idx) in errorMessages" :key="idx">{{ msg }}</div>
</template>
</el-alert>
<!-- 表格 -->
<el-table v-if="editableData.length" :data="editableData" style="width: 100%; margin-top: 16px" border
max-height="400" size="small">
<!-- 动态列 -->
<el-table-column v-for="header in headers" :key="header" :prop="header" :label="getHeaderLabel(header)"
min-width="120">
<template #default="{ row, $index }">
<el-input v-if="editRowIndex === $index && editColumn === header" v-model="row[header]"
@blur="saveEdit" @keyup.enter="saveEdit" ref="currentInput" />
<span v-else @click="startEdit($index, header)">
{{ row[header] ?? '' }}
</span>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ $index }">
<el-button v-if="editRowIndex === $index" type="text" size="small" @click="cancelEdit">
取消
</el-button>
<el-button v-else type="text" size="small" @click="startEdit($index, headers[0])">
编辑
</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="deleteRow($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 提交按钮 -->
<el-button v-if="showConfirm && editableData.length" type="success" style="margin-top: 16px"
:loading="submitting" @click="handleSubmit">
确认提交(共 {{ editableData.length }} 条)
</el-button>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
const props = defineProps({
showConfirm: { type: Boolean, default: true },
beforeUpload: { type: Function, default: null },
headerRow: { type: Number, default: 0 },
checkStartRow: { type: Number, default: 3},
requiredFields: { type: String, default: '' },
useChineseHeader: { type: Boolean, default: true },
transformSubmitData: {
type: Function,
default: (rows) => rows
}
})
const emit = defineEmits(['submit', 'parsed'])
// refs
const uploadRef = ref(null)
const currentInput = ref(null)
const fileName = ref('')
const headers = ref([])
const chineseHeadersMap = ref({})
const editableData = ref([]) // ✅ 所有操作在此数组上进行
const editRowIndex = ref(-1)
const editColumn = ref(null)
const parseMessage = ref('')
const parsedValid = ref(true)
const errorMessages = ref([])
const submitting = ref(false)
// 获取表头显示文本
const getHeaderLabel = (header) => {
return props.useChineseHeader ? (chineseHeadersMap.value[header] || header) : header
}
// 上传前校验
const handleBeforeUpload = (file) => {
if (props.beforeUpload && !props.beforeUpload(file)) return false
if (!/\.(xlsx|xls|csv)$/i.test(file.name)) {
ElMessage.error('仅支持 Excel 或 CSV 文件')
return false
}
return true
}
// 解析文件
const handleFileChange = async (uploadFile) => {
const file = uploadFile.raw
if (!file) return
fileName.value = file.name
parseMessage.value = ''
parsedValid.value = true
errorMessages.value = []
chineseHeadersMap.value = {}
const formData = new FormData()
formData.append('file', file)
formData.append('headerRow', props.headerRow.toString())
formData.append('checkStartRow', props.checkStartRow.toString())
if (props.requiredFields.length > 0) {
formData.append('requiredFields', props.requiredFields)
}
try {
const res = await request.post('/oss/api/excel/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
if (res.code !== 200 || !res.data?.success) {
throw new Error(res.msg || '解析失败')
}
const parsed = res.data
parseMessage.value = parsed.message || ''
parsedValid.value = parsed.valid !== false
errorMessages.value = parsed.errorMessages || []
const rawData = parsed.data || []
headers.value = parsed.headers || []
if (rawData.length === 0) {
editableData.value = []
} else {
if (props.useChineseHeader) {
const firstRow = rawData[0]
const map = {}
headers.value.forEach(key => {
map[key] = firstRow[key] || key
})
chineseHeadersMap.value = map
editableData.value = rawData.slice(1).map(row => ({ ...row }))
} else {
editableData.value = rawData.map(row => ({ ...row }))
}
}
emit('parsed', editableData.value)
uploadRef.value.clearFiles()
} catch (err) {
console.error('Parse error:', err)
ElMessage.error('文件解析失败:' + (err.message || ''))
}
}
// 开始编辑
const startEdit = (index, column) => {
editRowIndex.value = index
editColumn.value = column
nextTick(() => {
currentInput.value?.focus()
})
}
// 保存编辑
const saveEdit = () => {
editRowIndex.value = -1
editColumn.value = null
}
// 取消编辑
const cancelEdit = () => {
// 无需回滚,因为我们直接操作 editableData(无原始快照)
// 如果需要“撤销到初始状态”,可保留 originalData
saveEdit()
}
// 删除行
const deleteRow = (index) => {
ElMessageBox.confirm('确定删除此行?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
editableData.value.splice(index, 1)
// 重置编辑状态(避免索引错乱)
if (editRowIndex.value === index) {
editRowIndex.value = -1
editColumn.value = null
} else if (editRowIndex.value > index) {
editRowIndex.value-- // 索引前移
}
}).catch(() => { })
}
// 提交最终数据
const handleSubmit = () => {
if (editableData.value.length === 0) {
ElMessage.warning('没有可提交的数据')
return
}
submitting.value = true
try {
// 调用 transformSubmitData 进行格式转换(如类型转换)
const finalData = props.transformSubmitData([...editableData.value])
emit('submit', finalData)
} catch (err) {
ElMessage.error('提交失败:' + (err.message || ''))
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.ml-2 {
margin-left: 8px;
}
</style>
\ No newline at end of file
// src/hooks/useRemoteSearch.ts
import { ref, reactive, onUnmounted } from 'vue'
import type { Ref } from 'vue'
export interface SearchOption {
label: string
value: string | number
[key: string]: any
}
export interface RemoteSearchConfig {
type: string
apiMethod: (params: any) => Promise<any>
formatResult?: (data: any[]) => SearchOption[]
cacheKey?: string
debounceDelay?: number
defaultOptions?: SearchOption[]
}
export interface RemoteSearchState {
loading: boolean
options: SearchOption[]
cache: Map<string, SearchOption[]>
}
export function useRemoteSearch(config: RemoteSearchConfig) {
const {
type,
apiMethod,
formatResult = defaultFormatResult,
cacheKey = type,
debounceDelay = 500,
defaultOptions = []
} = config
// 状态
const state = reactive<RemoteSearchState>({
loading: false,
options: [...defaultOptions],
cache: new Map()
})
// 防抖相关
let debounceTimer: NodeJS.Timeout | null = null
let lastQuery = ''
// 默认格式化函数
function defaultFormatResult(data: any[]): SearchOption[] {
return data.map(item => ({
label: item.name || item.label || item.text || String(item.value),
value: item.id || item.value || item.key,
...item
}))
}
// 从缓存获取
function getFromCache(query: string): SearchOption[] | null {
const cacheKeyWithQuery = `${cacheKey}:${query || 'all'}`
return state.cache.get(cacheKeyWithQuery) || null
}
// 保存到缓存
function saveToCache(query: string, data: SearchOption[]) {
const cacheKeyWithQuery = `${cacheKey}:${query || 'all'}`
state.cache.set(cacheKeyWithQuery, data)
// 限制缓存大小(最多100条记录)
if (state.cache.size > 100) {
const firstKey = state.cache.keys().next().value
state.cache.delete(firstKey)
}
}
// 执行搜索
async function performSearch(query: string): Promise<SearchOption[]> {
// 检查缓存
const cached = getFromCache(query)
if (cached && cached.length > 0) {
return cached
}
state.loading = true
try {
// 调用API
const params = query ? { keyword: query, pageSize: 50 } : { pageSize: 100 }
const response = await apiMethod(params)
// 格式化结果
const result = formatResult(response.data || response.list || response.records || [])
// 保存到缓存
saveToCache(query, result)
return result
} catch (error) {
console.error(`远程搜索失败 [${type}]:`, error)
throw error
} finally {
state.loading = false
}
}
// 搜索方法(带防抖)
async function search(query: string = ''): Promise<SearchOption[]> {
// 清除之前的定时器
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
// 如果查询相同,直接返回当前选项
if (query === lastQuery) {
return state.options
}
lastQuery = query
// 如果是空查询且有默认选项,直接返回
if (!query && defaultOptions.length > 0) {
state.options = defaultOptions
return defaultOptions
}
return new Promise((resolve) => {
debounceTimer = setTimeout(async () => {
try {
const result = await performSearch(query)
state.options = result
resolve(result)
} catch (error) {
state.options = []
resolve([])
}
}, debounceDelay)
})
}
// 预加载数据(初始化时调用)
async function preload(): Promise<void> {
if (state.options.length === 0) {
await search('')
}
}
// 清空缓存
function clearCache(): void {
state.cache.clear()
state.options = [...defaultOptions]
}
// 设置默认选项
function setDefaultOptions(options: SearchOption[]): void {
defaultOptions.length = 0
defaultOptions.push(...options)
if (state.options.length === 0) {
state.options = [...options]
}
}
// 组件卸载时清理
onUnmounted(() => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
})
return {
state,
search,
preload,
clearCache,
setDefaultOptions,
loading: () => state.loading,
options: () => state.options
}
}
// 创建多搜索实例的管理器
export function useRemoteSearchManager() {
const instances = new Map<string, ReturnType<typeof useRemoteSearch>>()
function getInstance(config: RemoteSearchConfig) {
const { type } = config
if (!instances.has(type)) {
instances.set(type, useRemoteSearch(config))
}
return instances.get(type)!
}
function clearAllCache() {
instances.forEach(instance => {
instance.clearCache()
})
}
return {
getInstance,
clearAllCache
}
}
\ No newline at end of file
......@@ -52,24 +52,3 @@ export function useDictLists(typeLists) {
})
})()
}
\ No newline at end of file
// /**
// * 获取字典数据
// */
// export function useDict(...args) {
// const res = ref({})
// return (() => {
// args.forEach((dictType, index) => {
// res.value[dictType] = []
// const dicts = useDictStore().getDict(dictType)
// if (dicts) {
// res.value[dictType] = dicts
// } else {
// getDicts(dictType).then(resp => {
// res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
// useDictStore().setDict(dictType, res.value[dictType])
// })
// }
// })
// return toRefs(res.value)
// })()
// }
......@@ -210,46 +210,79 @@ export function getTime(type) {
}
/**
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
* @return {*}
* 修复后的防抖函数(支持立即执行、手动取消、返回值)
* @param {Function} func 要防抖的函数
* @param {number} [wait=300] 延迟时间(毫秒)
* @param {boolean} [immediate=false] 是否立即执行
* @returns {Function} 包装后的防抖函数(包含cancel方法)
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
export function debounce(func, wait = 300, immediate = false) {
let timeout = null; // 定时器标识
let args = null; // 缓存函数参数
let context = null; // 缓存函数上下文
let timestamp = 0; // 最后一次触发的时间戳
let result = undefined; // 函数执行结果
// 延迟执行的核心逻辑
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 计算最后一次触发与现在的时间差
const last = Date.now() - timestamp;
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
// 如果时间差小于wait且大于0,说明还在防抖窗口期,重新设置定时器
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
timeout = setTimeout(later, wait - last);
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
// 窗口期结束,清空定时器
timeout = null;
// 非立即执行的情况,在此处执行原函数
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
result = func.apply(context, args);
// 执行后清空上下文和参数,避免内存泄漏
if (!timeout) context = args = null;
}
}
};
// 包装后的防抖函数
const debounced = function(...params) {
// 实时捕获当前的上下文和参数
context = this;
args = params;
// 记录最后一次触发的时间戳
timestamp = Date.now();
// 判断是否需要立即执行
const callNow = immediate && !timeout;
// 清除旧的定时器,避免多次执行
if (timeout) clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(later, wait);
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
// 立即执行的情况,直接调用原函数
if (callNow) {
result = func.apply(context, args)
context = args = null
result = func.apply(context, args);
// 清空上下文和参数
context = args = null;
}
return result
}
// 返回函数执行结果
return result;
};
// 手动取消防抖的方法
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
context = args = null;
timestamp = 0;
};
return debounced;
}
/**
* This is just a simple version of deep copy
* Has a lot of edge cases bug
......@@ -390,6 +423,6 @@ export function isNumberStr(str) {
// 数字千分位格式化,保留2位小数
export function numberWithCommas(x, fixed = 2) {
if (!x) return '0.00'
return x.toFixed(fixed).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
......@@ -149,6 +149,7 @@ service.interceptors.response.use(
// 通用下载方法
export function download(url, params, filename, config) {
console.log(url, params, filename, config)
downloadLoadingInstance = ElLoading.service({
text: '正在下载数据,请稍候',
background: 'rgba(0, 0, 0, 0.7)'
......@@ -160,7 +161,7 @@ export function download(url, params, filename, config) {
return tansParams(params)
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/json' },
responseType: 'blob',
...config
})
......
// utils/safeDownload.js
import { ElMessage } from 'element-plus'
/**
* 安全下载函数:适用于后端返回 Blob 文件流 或 JSON 错误 的场景
* @param {Blob} blobData - 接口返回的响应数据(必须是 responseType: 'blob')
* @param {string} defaultFilename - 默认文件名(如 'data.xlsx')
* @param {string} [mimeType] - MIME 类型(用于兜底创建 Blob)
*/
export async function safeDownload(blobData, defaultFilename, mimeType = 'application/octet-stream') {
if (!(blobData instanceof Blob)) {
ElMessage.error('无效的下载数据')
return
}
try {
// 👇 关键:先 peek 前 100 字节,判断是否是 JSON 错误
const firstChunk = await blobData.slice(0, 100).text()
const trimmed = firstChunk.trim()
// 如果看起来像 JSON(以 { 或 [ 开头),尝试解析
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
const fullText = await blobData.text()
let parsed
try {
parsed = JSON.parse(fullText)
} catch (e) {
// 解析失败,当作正常文件(比如内容就是纯文本)
parsed = null
}
// 如果解析成功,且包含错误字段(根据你后端约定)
if (parsed && (parsed.code !== undefined || parsed.msg || parsed.message)) {
const errorMsg = parsed.msg || parsed.message || '导出失败'
ElMessage.error(errorMsg)
return
}
}
// ✅ 是合法文件流,执行下载(使用你已验证的逻辑)
const url = window.URL.createObjectURL(blobData)
const link = document.createElement('a')
link.href = url
link.download = defaultFilename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('safeDownload error:', error)
ElMessage.error('下载过程中发生错误')
}
}
\ No newline at end of file
// dictUtils.js
import request from '@/utils/request'
// 全局缓存:key 为 dictType,value 为 { label: value } 映射对象
const dictCache = new Map()
// 批量加载字典(支持多个 type)
export async function loadDicts(typeList) {
// 过滤已缓存的类型,避免重复请求
const needLoadTypes = typeList.filter(type => !dictCache.has(type))
if (needLoadTypes.length === 0) return
try {
const res = await request({
url: '/user/api/sysDict/type/list', // 替换为你的实际接口地址
method: 'POST',
data: {
typeList: needLoadTypes
}
})
// 替换 loadDicts 中的缓存部分
if (res.code === 200 && Array.isArray(res.data)) {
for (const dict of res.data) {
const { dictType, dictItemList = [] } = dict
if (!dictType) continue
// 缓存完整列表(并可选排序 + 过滤)
const validItems = dictItemList
.filter(item => item.status === 1) // 只取启用的
.sort((a, b) => a.orderNum - b.orderNum) // 按序号排序
dictCache.set(dictType, validItems) // 👈 缓存完整 item 列表
}
}
} catch (error) {
console.error('字典加载失败:', error)
throw error
}
}
// 可选:提供清除缓存方法(用于权限切换等场景)
export function clearDictCache() {
dictCache.clear()
}
export function getDictOptions(dictType) {
const items = dictCache.get(dictType) || []
// console.log('getDictOptions',items)
return items.map(item => ({
value: item.itemValue,
label: item.itemLabel
// 如果需要,还可以加其他字段:disabled, key 等
}))
}
// 同时更新 getDictLabel
export function getDictLabel(dictType, value) {
const items = dictCache.get(dictType) || []
const item = items.find(i => i.itemValue === value)
return item ? item.itemLabel : value
}
\ No newline at end of file
......@@ -52,6 +52,17 @@ export default defineConfig(({ mode, command }) => {
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
},
'/csf': {
target: 'http://139.224.145.34:9002',
changeOrigin: true,
secure: false,
// 如果后端需要 host 头
// configure: (proxy, options) => {
// proxy.on('proxyReq', (proxyReq, req, res) => {
// proxyReq.setHeader('host', '139.224.145.34:9002')
// })
// }
},
// springdoc proxy
'^/v3/api-docs/(.*)': {
target: baseUrl,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment