feat(creditMpCustomer): 添加步骤筛选功能并优化文件展示
- 在模型中新增 step 字段用于标识处理进度 - 添加步骤筛选下拉框支持按状态搜索 - 重构文件展示逻辑支持图片预览和文件链接 - 添加步骤标签颜色区分不同处理阶段 - 优化表格列宽度布局提升显示效果 - 实现文件路径解析和缓存机制
This commit is contained in:
@@ -26,6 +26,8 @@ export interface CreditMpCustomer {
|
|||||||
region?: string;
|
region?: string;
|
||||||
// 文件路径
|
// 文件路径
|
||||||
files?: string;
|
files?: string;
|
||||||
|
// 步骤, 0未受理1已受理2材料提交3合同签订4执行回款5完结
|
||||||
|
step?: number;
|
||||||
// 是否有数据
|
// 是否有数据
|
||||||
hasData?: string;
|
hasData?: string;
|
||||||
// 备注
|
// 备注
|
||||||
@@ -60,4 +62,5 @@ export interface CreditMpCustomer {
|
|||||||
export interface CreditMpCustomerParam extends PageParam {
|
export interface CreditMpCustomerParam extends PageParam {
|
||||||
id?: number;
|
id?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
|
step?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<template>
|
<template>
|
||||||
<a-space :size="10" style="flex-wrap: wrap">
|
<a-space :size="10" style="flex-wrap: wrap">
|
||||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
|
||||||
<template #icon>
|
<!-- <template #icon>-->
|
||||||
<PlusOutlined />
|
<!-- <PlusOutlined />-->
|
||||||
</template>
|
<!-- </template>-->
|
||||||
<span>添加</span>
|
<!-- <span>添加</span>-->
|
||||||
</a-button>
|
<!-- </a-button>-->
|
||||||
<a-button class="ele-btn-icon" @click="exportData">
|
<a-button class="ele-btn-icon" @click="exportData">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<CloudDownloadOutlined />
|
<CloudDownloadOutlined />
|
||||||
@@ -32,6 +32,21 @@
|
|||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@pressEnter="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>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,8 +81,18 @@
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const keywords = ref('');
|
const keywords = ref('');
|
||||||
|
const step = ref<number | undefined>(undefined);
|
||||||
const selection = computed(() => props.selection || []);
|
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 = () => {
|
const add = () => {
|
||||||
emit('add');
|
emit('add');
|
||||||
@@ -75,7 +100,7 @@
|
|||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
emit('search', { keywords: keywords.value });
|
emit('search', { keywords: keywords.value || undefined, step: step.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'userInfo'">
|
<template v-if="column.key === 'userInfo'">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<a-avatar :size="48" :src="record.avatar">
|
<a-avatar :size="42" :src="record.avatar">
|
||||||
<template v-if="!record.avatar" #icon>
|
<template v-if="!record.avatar" #icon>
|
||||||
<UserOutlined/>
|
<UserOutlined/>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,21 +39,36 @@
|
|||||||
<a-image :src="record.image" :width="50"/>
|
<a-image :src="record.image" :width="50"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'files'">
|
<template v-if="column.key === 'files'">
|
||||||
<a-space>
|
<a-space :size="8" style="flex-wrap: wrap">
|
||||||
|
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
|
||||||
<a-image
|
<a-image
|
||||||
v-for="(file, index) in record.files"
|
v-if="file.isImage"
|
||||||
:key="index"
|
:src="file.thumbnail || file.url"
|
||||||
:src="file"
|
|
||||||
:width="50"
|
: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>
|
</a-space>
|
||||||
</template>
|
</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'">
|
<template v-if="column.key === 'status'">
|
||||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||||
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<a-space>
|
|
||||||
<a @click="openEdit(record)">修改</a>
|
<a @click="openEdit(record)">修改</a>
|
||||||
<a-divider type="vertical"/>
|
<a-divider type="vertical"/>
|
||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
@@ -62,7 +77,6 @@
|
|||||||
>
|
>
|
||||||
<a class="ele-text-danger">删除</a>
|
<a class="ele-text-danger">删除</a>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-space>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</ele-pro-table>
|
</ele-pro-table>
|
||||||
@@ -95,6 +109,13 @@ import {
|
|||||||
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
|
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
|
||||||
import {exportCreditData} from '../utils/export';
|
import {exportCreditData} from '../utils/export';
|
||||||
|
|
||||||
|
type NormalizedFile = {
|
||||||
|
name?: string;
|
||||||
|
url: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
isImage: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// 表格实例
|
// 表格实例
|
||||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||||
|
|
||||||
@@ -110,6 +131,7 @@ const showMove = ref(false);
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const searchText = ref('');
|
const searchText = ref('');
|
||||||
|
const searchStep = ref<number | undefined>(undefined);
|
||||||
|
|
||||||
// 表格数据源
|
// 表格数据源
|
||||||
const datasource: DatasourceFunction = ({
|
const datasource: DatasourceFunction = ({
|
||||||
@@ -122,6 +144,8 @@ const datasource: DatasourceFunction = ({
|
|||||||
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
|
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
|
||||||
if (filters) {
|
if (filters) {
|
||||||
(params as any).status = filters.status;
|
(params as any).status = filters.status;
|
||||||
|
const stepFilter = (filters as any).step;
|
||||||
|
(params as any).step = Array.isArray(stepFilter) ? stepFilter[0] : stepFilter;
|
||||||
}
|
}
|
||||||
return pageCreditMpCustomer({
|
return pageCreditMpCustomer({
|
||||||
...params,
|
...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[]>([
|
const allColumns = ref<ColumnItem[]>([
|
||||||
{
|
{
|
||||||
@@ -143,7 +268,7 @@ const allColumns = ref<ColumnItem[]>([
|
|||||||
title: '用户信息',
|
title: '用户信息',
|
||||||
dataIndex: 'userInfo',
|
dataIndex: 'userInfo',
|
||||||
key: 'userInfo',
|
key: 'userInfo',
|
||||||
width: 300,
|
width: 240,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '拖欠方',
|
title: '拖欠方',
|
||||||
@@ -187,6 +312,14 @@ const allColumns = ref<ColumnItem[]>([
|
|||||||
key: 'city',
|
key: 'city',
|
||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '步骤',
|
||||||
|
dataIndex: 'step',
|
||||||
|
key: 'step',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// title: '所在辖区',
|
// title: '所在辖区',
|
||||||
// dataIndex: 'region',
|
// dataIndex: 'region',
|
||||||
@@ -243,7 +376,7 @@ const allColumns = ref<ColumnItem[]>([
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 180,
|
width: 120,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
hideInSetting: true
|
hideInSetting: true
|
||||||
@@ -258,6 +391,7 @@ const defaultVisibleColumns = [
|
|||||||
'price',
|
'price',
|
||||||
'years',
|
'years',
|
||||||
'city',
|
'city',
|
||||||
|
'step',
|
||||||
'files',
|
'files',
|
||||||
// 'status',
|
// 'status',
|
||||||
'createTime',
|
'createTime',
|
||||||
@@ -276,7 +410,10 @@ const reload = (where?: CreditMpCustomerParam) => {
|
|||||||
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
|
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
|
||||||
searchText.value = 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 = [];
|
selection.value = [];
|
||||||
tableRef?.value?.reload({where: targetWhere});
|
tableRef?.value?.reload({where: targetWhere});
|
||||||
};
|
};
|
||||||
@@ -307,6 +444,7 @@ const exportData = () => {
|
|||||||
{title: '所在省份', dataIndex: 'province'},
|
{title: '所在省份', dataIndex: 'province'},
|
||||||
{title: '所在城市', dataIndex: 'city'},
|
{title: '所在城市', dataIndex: 'city'},
|
||||||
{title: '所在辖区', dataIndex: 'region'},
|
{title: '所在辖区', dataIndex: 'region'},
|
||||||
|
{title: '步骤', dataIndex: 'step'},
|
||||||
{title: '文件路径', dataIndex: 'files'},
|
{title: '文件路径', dataIndex: 'files'},
|
||||||
{title: '是否有数据', dataIndex: 'hasData'},
|
{title: '是否有数据', dataIndex: 'hasData'},
|
||||||
{title: '备注', dataIndex: 'comments'},
|
{title: '备注', dataIndex: 'comments'},
|
||||||
@@ -320,7 +458,8 @@ const exportData = () => {
|
|||||||
],
|
],
|
||||||
fetchData: () =>
|
fetchData: () =>
|
||||||
listCreditMpCustomer({
|
listCreditMpCustomer({
|
||||||
keywords: searchText.value || undefined
|
keywords: searchText.value || undefined,
|
||||||
|
step: searchStep.value
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user