feat(creditMpCustomer): 添加步骤筛选功能并优化文件展示
- 在模型中新增 step 字段用于标识处理进度 - 添加步骤筛选下拉框支持按状态搜索 - 重构文件展示逻辑支持图片预览和文件链接 - 添加步骤标签颜色区分不同处理阶段 - 优化表格列宽度布局提升显示效果 - 实现文件路径解析和缓存机制
This commit is contained in:
@@ -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
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user