feat(creditMpCustomer): 添加步骤筛选功能并优化文件展示

- 在模型中新增 step 字段用于标识处理进度
- 添加步骤筛选下拉框支持按状态搜索
- 重构文件展示逻辑支持图片预览和文件链接
- 添加步骤标签颜色区分不同处理阶段
- 优化表格列宽度布局提升显示效果
- 实现文件路径解析和缓存机制
This commit is contained in:
2026-03-19 16:18:23 +08:00
parent 1b115edabe
commit 72c431967d
3 changed files with 188 additions and 21 deletions

View File

@@ -26,6 +26,8 @@ export interface CreditMpCustomer {
region?: string;
// 文件路径
files?: string;
// 步骤, 0未受理1已受理2材料提交3合同签订4执行回款5完结
step?: number;
// 是否有数据
hasData?: string;
// 备注
@@ -60,4 +62,5 @@ export interface CreditMpCustomer {
export interface CreditMpCustomerParam extends PageParam {
id?: number;
keywords?: string;
step?: number;
}

View File

@@ -1,12 +1,12 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<a-button class="ele-btn-icon" @click="exportData">
<template #icon>
<CloudDownloadOutlined />
@@ -32,6 +32,21 @@
@search="handleSearch"
@pressEnter="handleSearch"
/>
<a-select
v-model:value="step"
allow-clear
placeholder="步骤"
style="width: 140px"
@change="handleSearch"
>
<a-select-option
v-for="item in stepOptions"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-select-option>
</a-select>
</a-space>
</template>
@@ -66,8 +81,18 @@
}>();
const keywords = ref('');
const step = ref<number | undefined>(undefined);
const selection = computed(() => props.selection || []);
const stepOptions = [
{ value: 0, text: '未受理' },
{ value: 1, text: '已受理' },
{ value: 2, text: '材料提交' },
{ value: 3, text: '合同签订' },
{ value: 4, text: '执行回款' },
{ value: 5, text: '完结' }
];
// 新增
const add = () => {
emit('add');
@@ -75,7 +100,7 @@
// 搜索
const handleSearch = () => {
emit('search', { keywords: keywords.value });
emit('search', { keywords: keywords.value || undefined, step: step.value });
};
// 导出

View File

@@ -24,7 +24,7 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="48" :src="record.avatar">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
@@ -39,21 +39,36 @@
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space>
<a-image
v-for="(file, index) in record.files"
:key="index"
:src="file"
:width="50"
/>
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="file.url"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
@@ -62,7 +77,6 @@
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
@@ -95,6 +109,13 @@ import {
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../utils/export';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -110,6 +131,7 @@ const showMove = ref(false);
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
@@ -122,6 +144,8 @@ const datasource: DatasourceFunction = ({
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
(params as any).status = filters.status;
const stepFilter = (filters as any).step;
(params as any).step = Array.isArray(stepFilter) ? stepFilter[0] : stepFilter;
}
return pageCreditMpCustomer({
...params,
@@ -131,6 +155,107 @@ const datasource: DatasourceFunction = ({
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
return {
url: item,
thumbnail: item,
name: guessNameFromUrl(item),
isImage: isImageUrl(item)
} satisfies NormalizedFile;
}
const url = item.url || item.path || item.href;
if (!url || typeof url !== 'string') return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url, thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
@@ -143,7 +268,7 @@ const allColumns = ref<ColumnItem[]>([
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 300,
width: 240,
},
{
title: '拖欠方',
@@ -187,6 +312,14 @@ const allColumns = ref<ColumnItem[]>([
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
@@ -243,7 +376,7 @@ const allColumns = ref<ColumnItem[]>([
{
title: '操作',
key: 'action',
width: 180,
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
@@ -258,6 +391,7 @@ const defaultVisibleColumns = [
'price',
'years',
'city',
'step',
'files',
// 'status',
'createTime',
@@ -276,7 +410,10 @@ const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
const targetWhere = where ?? {keywords: searchText.value || undefined};
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
@@ -307,6 +444,7 @@ const exportData = () => {
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
@@ -320,7 +458,8 @@ const exportData = () => {
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};