This commit is contained in:
2026-04-23 10:01:09 +08:00
parent 188b396fea
commit 7793b9d4ec
13 changed files with 1042 additions and 28 deletions

182
src/api/loan/application.ts Normal file
View File

@@ -0,0 +1,182 @@
import { Alova } from '@/utils/http/alova/index';
import { ResultEnum } from '@/enums/httpEnum';
import { PageEnum } from '@/enums/pageEnum';
import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage';
const LOAN_FILE_HOST = 'http://192.168.1.26:7060';
export interface LoanApplicationQueryListReq {
pageNum: number;
pageSize: number;
yearMonth: string;
name?: string;
contact?: string;
idCard?: string;
isProcessed?: boolean;
}
export interface LoanApplicationHandleReq {
idList: number[];
}
export interface LoanFileObj {
suffix: string;
fileName: string;
original: string;
url: string;
}
export interface LoanApplicationQueryListRes {
id: number;
name: string;
contact: string;
idCard: string;
businessAddress: string;
businessItem: string;
businessDuration: number;
annualIncome: number;
hasDebt: boolean;
loanAmount: number;
familyAssets: string[];
businessLicense?: LoanFileObj;
auxiliaryMaterials?: LoanFileObj[];
loanType: string;
isProcessed: boolean;
createTime: string;
updateTime: string;
}
export interface LoanApplicationPageRes {
records: LoanApplicationQueryListRes[];
pageNumber: number;
pageSize: number;
totalPage: number;
totalRow: number;
optimizeCountQuery: boolean;
}
export function getLoanFileUrl(file?: LoanFileObj) {
if (!file?.url) return '';
if (/^https?:\/\//i.test(file.url)) return file.url;
return `${LOAN_FILE_HOST}${file.url.startsWith('/') ? file.url : `/${file.url}`}`;
}
interface FinResult<T> {
code: number;
message: string;
data: T;
}
function isSuccessCode(code: number) {
return code === ResultEnum.SUCCESS || code === 0;
}
function getEmptyPage(params: LoanApplicationQueryListReq): LoanApplicationPageRes {
return {
records: [],
pageNumber: params.pageNum,
pageSize: params.pageSize,
totalPage: 0,
totalRow: 0,
optimizeCountQuery: true,
};
}
export async function getLoanApplicationList(params: LoanApplicationQueryListReq) {
const response = await Alova.Post<FinResult<LoanApplicationPageRes>>(
'/loan/application/applicationList',
params,
{
meta: {
isReturnNativeResponse: true,
},
}
);
if (!isSuccessCode(response.code)) {
throw new Error(response.message || '获取申请贷款列表失败');
}
return response.data || getEmptyPage(params);
}
export async function handleLoanApplication(params: LoanApplicationHandleReq) {
const response = await Alova.Post<FinResult<string>>(
'/loan/application/handleApplication',
params,
{
meta: {
isReturnNativeResponse: true,
},
}
);
if (!isSuccessCode(response.code)) {
throw new Error(response.message || '处理申请贷款失败');
}
return response.data;
}
function redirectToLogin() {
const { pathname, search, hash } = window.location;
const currentPath = `${pathname}${search}${hash}`;
const redirectPath =
pathname === PageEnum.BASE_LOGIN
? PageEnum.BASE_LOGIN
: `${PageEnum.BASE_LOGIN}?redirect=${encodeURIComponent(currentPath)}`;
storage.clear();
window.location.replace(redirectPath);
}
function getExportFileName(disposition: string | null) {
if (!disposition) return `loan-applications-${Date.now()}.xlsx`;
const utf8FileName = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8FileName?.[1]) {
return decodeURIComponent(utf8FileName[1]);
}
const fileName = disposition.match(/filename="?([^"]+)"?/i);
if (fileName?.[1]) {
return decodeURIComponent(fileName[1]);
}
return `loan-applications-${Date.now()}.xlsx`;
}
export async function exportLoanApplicationList(params: LoanApplicationQueryListReq) {
const token = storage.get(ACCESS_TOKEN, '');
const response = await fetch('/loan/application/export', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(params),
});
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const result = (await response.json()) as FinResult<unknown>;
if (Number(result.code) === ResultEnum.LOGIN_EXPIRED) {
redirectToLogin();
throw new Error(result.message || '请先登录');
}
if (!isSuccessCode(Number(result.code))) {
throw new Error(result.message || '导出申请贷款失败');
}
throw new Error('导出接口未返回文件');
}
if (!response.ok) {
throw new Error('导出申请贷款失败');
}
return {
blob: await response.blob(),
fileName: getExportFileName(response.headers.get('content-disposition')),
};
}

View File

@@ -49,7 +49,7 @@ export interface LogoutRes {
* @description: 获取用户信息
*/
export function getUserInfo() {
return Alova.Get<UserInfoRes>('/api/user/info', {
return Alova.Get<UserInfoRes>('/user/info', {
meta: {
isReturnNativeResponse: true,
},
@@ -61,7 +61,7 @@ export function getUserInfo() {
*/
export function login(params: LoginParams) {
return Alova.Post<LoginRes>(
'/api/user/login',
'/fg-api/user/login',
params,
{
meta: {
@@ -82,7 +82,7 @@ export function changePassword(params, uid) {
* @description: 用户登出
*/
export function logout() {
return Alova.Get<LogoutRes>('/api/user/logout', {
return Alova.Get<LogoutRes>('/user/logout', {
meta: {
isReturnNativeResponse: true,
},

View File

@@ -239,15 +239,28 @@
cacheRoutes = [simpleRoute];
}
// 将最新的路由信息同步到 localStorage 中
// 将最新的路由信息同步到 localStorage 中,并清掉已移除菜单留下的旧标签
const routes = router.getRoutes();
cacheRoutes.forEach((cacheRoute) => {
const route = routes.find((route) => route.path === cacheRoute.path);
cacheRoutes = cacheRoutes
.filter((cacheRoute) =>
routes.some(
(route) => route.path === cacheRoute.path || route.name === cacheRoute.name
)
)
.map((cacheRoute) => {
const route = routes.find(
(route) => route.path === cacheRoute.path || route.name === cacheRoute.name
);
if (route) {
cacheRoute.meta = route.meta || cacheRoute.meta;
cacheRoute.name = (route.name || cacheRoute.name) as string;
}
});
return cacheRoute;
});
if (!cacheRoutes.length) {
cacheRoutes = [simpleRoute];
}
// 初始化标签页
tabsViewStore.initTabs(cacheRoutes);

View File

@@ -1,11 +1,11 @@
import { App } from 'vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { RedirectRoute } from '@/router/base';
import { PageEnum } from '@/enums/pageEnum';
import { createRouterGuards } from './guards';
import type { IModuleType } from './types';
const modules = import.meta.glob<IModuleType>('./modules/**/*.ts', { eager: true });
const modules = import.meta.glob<IModuleType>('./modules/loan.ts', { eager: true });
const routeModuleList: RouteRecordRaw[] = Object.keys(modules).reduce((list, key) => {
const mod = modules[key].default ?? {};
@@ -43,11 +43,16 @@ export const asyncRoutes = [...routeModuleList];
//普通路由 无需验证权限
export const constantRouter: RouteRecordRaw[] = [LoginRoute, RootRoute, RedirectRoute];
// const router = createRouter({
// history: createWebHistory(import.meta.env.BASE_URL),
// routes: constantRouter,
// strict: true,
// scrollBehavior: () => ({ left: 0, top: 0 }),
// });
const router = createRouter({
history: createWebHistory(),
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: constantRouter,
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
});
export function setupRouter(app: App) {

View File

@@ -0,0 +1,41 @@
import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant';
import { TableOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
const routes: Array<RouteRecordRaw> = [
{
path: '/loan',
name: 'Loan',
redirect: '/loan/application',
component: Layout,
meta: {
title: '贷款管理',
icon: renderIcon(TableOutlined),
sort: 0,
},
children: [
{
path: 'application',
name: 'LoanApplication',
meta: {
title: '申请贷款列表',
affix: true,
},
component: () => import('@/views/loan/application/index.vue'),
},
{
path: 'application/:id',
name: 'LoanApplicationDetail',
meta: {
title: '申请贷款详情',
hidden: true,
activeMenu: 'LoanApplication',
},
component: () => import('@/views/loan/application/detail.vue'),
},
],
},
];
export default routes;

12
src/utils/encrypt.ts Normal file
View File

@@ -0,0 +1,12 @@
import JSEncrypt from 'jsencrypt';
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALDoU+fLIG+nGS5CugznjdUjym6tBIoz
CxLmUifF2Sb3MkzgxVvz5k1fDLKlCrztSRDioHaYo+N/co7hIjlpbtECAwEAAQ==
-----END PUBLIC KEY-----`;
export function encryptPassword(password: string) {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(PUBLIC_KEY);
return encryptor.encrypt(password);
}

View File

@@ -2,7 +2,6 @@ import { createAlova } from 'alova';
import VueHook from 'alova/vue';
import adapterFetch from 'alova/fetch';
import { createAlovaMockAdapter } from '@alova/mock';
import { isString } from 'lodash-es';
import mocks from './mocks';
import { useUser } from '@/store/modules/user';
import { storage } from '@/utils/Storage';
@@ -77,9 +76,6 @@ export const Alova = createAlova({
if (!isUrlStr && urlPrefix) {
method.url = `${urlPrefix}${method.url}`;
}
if (!isUrlStr && apiUrl && isString(apiUrl)) {
method.url = `${apiUrl}${method.url}`;
}
},
responded: {
onSuccess: async (response, method) => {

View File

@@ -0,0 +1,224 @@
<template>
<n-card :bordered="false">
<n-space justify="space-between" align="center">
<n-h3 class="m-0">申请贷款详情</n-h3>
<n-button @click="goBack">返回列表</n-button>
</n-space>
</n-card>
<template v-if="application">
<n-card :bordered="false" class="mt-3" title="申请信息">
<n-descriptions bordered :column="2" label-placement="left">
<n-descriptions-item label="姓名">{{ application.name || '-' }}</n-descriptions-item>
<n-descriptions-item label="联系方式">{{ application.contact || '-' }}</n-descriptions-item>
<n-descriptions-item label="身份证号码">{{ application.idCard || '-' }}</n-descriptions-item>
<n-descriptions-item label="贷款类型">{{ loanTypeText }}</n-descriptions-item>
<n-descriptions-item label="经营地址">{{ application.businessAddress || '-' }}</n-descriptions-item>
<n-descriptions-item label="经营项目">{{ application.businessItem || '-' }}</n-descriptions-item>
<n-descriptions-item label="经营时间">
{{ application.businessDuration ?? 0 }}
</n-descriptions-item>
<n-descriptions-item label="经营收入">
{{ application.annualIncome ?? 0 }} 万元/
</n-descriptions-item>
<n-descriptions-item label="是否有负债">
<n-tag :type="application.hasDebt ? 'warning' : 'success'" size="small">
{{ application.hasDebt ? '有' : '无' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="贷款需求金额">
{{ application.loanAmount ?? 0 }} 万元
</n-descriptions-item>
<n-descriptions-item label="家庭主要资产" :span="2">
<n-space v-if="familyAssets.length" size="small">
<n-tag v-for="asset in familyAssets" :key="asset" size="small">
{{ asset }}
</n-tag>
</n-space>
<span v-else>-</span>
</n-descriptions-item>
<n-descriptions-item label="处理状态">
<n-tag :type="application.isProcessed ? 'success' : 'warning'" size="small">
{{ application.isProcessed ? '已处理' : '未处理' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="创建时间">
{{ formatBackendDateTime(application.createTime) }}
</n-descriptions-item>
<n-descriptions-item label="更新时间" :span="2">
{{ formatBackendDateTime(application.updateTime) }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card :bordered="false" class="mt-3" title="营业执照">
<div v-if="businessLicenseFile" class="image-panel">
<n-image
v-if="businessLicenseFile.isImage"
width="360"
object-fit="contain"
:src="businessLicenseFile.url"
:alt="businessLicenseFile.name"
/>
<div v-else class="file-card">
<n-tag size="small">{{ businessLicenseFile.suffix || 'FILE' }}</n-tag>
<div class="file-name">{{ businessLicenseFile.name }}</div>
</div>
<n-button text tag="a" :href="businessLicenseFile.url" target="_blank">
{{ businessLicenseFile.isImage ? '查看原图' : '打开文件' }}
</n-button>
</div>
<n-empty v-else description="暂无营业执照" />
</n-card>
<n-card :bordered="false" class="mt-3" title="辅助材料">
<n-grid v-if="auxiliaryFiles.length" :cols="3" :x-gap="16" :y-gap="16" responsive="screen">
<n-gi v-for="file in auxiliaryFiles" :key="file.url">
<div class="image-panel">
<n-image
v-if="file.isImage"
width="280"
object-fit="contain"
:src="file.url"
:alt="file.name"
/>
<div v-else class="file-card">
<n-tag size="small">{{ file.suffix || 'FILE' }}</n-tag>
<div class="file-name">{{ file.name }}</div>
</div>
<n-button text tag="a" :href="file.url" target="_blank">
{{ file.isImage ? file.name : '打开文件' }}
</n-button>
</div>
</n-gi>
</n-grid>
<n-empty v-else description="暂无辅助材料" />
</n-card>
</template>
<n-card v-else :bordered="false" class="mt-3">
<n-empty description="未找到申请详情,请从列表页重新进入" />
</n-card>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getLoanFileUrl } from '@/api/loan/application';
import type { LoanApplicationQueryListRes, LoanFileObj } from '@/api/loan/application';
import { formatBackendDateTime } from '@/utils/dateUtil';
const DETAIL_STORAGE_PREFIX = 'LOAN_APPLICATION_DETAIL';
const IMAGE_SUFFIXES = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'tif', 'tiff'];
const familyAssetMap: Record<string, string> = {
'0': '无',
'1': '商品房',
'2': '自建房国有',
'3': '自建房集体',
'4': '商铺',
'5': '土地',
'6': '车辆',
'7': '其他',
};
const loanTypeMap: Record<string, string> = {
'0': '信用贷',
'1': '抵押贷',
};
const route = useRoute();
const router = useRouter();
const application = ref<LoanApplicationQueryListRes | null>(null);
const loanTypeText = computed(() => {
const value = application.value?.loanType;
return value === undefined || value === null ? '-' : loanTypeMap[String(value)] || '-';
});
const familyAssets = computed(() => {
return (application.value?.familyAssets || []).map((asset) => familyAssetMap[String(asset)] || asset);
});
function getFileSuffix(file: LoanFileObj) {
const rawSuffix =
file.suffix ||
file.url?.split('?')[0].split('.').pop() ||
file.original?.split('.').pop() ||
file.fileName?.split('.').pop() ||
'';
return rawSuffix.replace(/^\./, '').toLowerCase();
}
function getDisplayFile(file: LoanFileObj) {
const suffix = getFileSuffix(file);
return {
url: getLoanFileUrl(file),
name: file.original || file.fileName || '材料文件',
suffix,
isImage: IMAGE_SUFFIXES.includes(suffix),
};
}
const businessLicenseFile = computed(() => {
const file = application.value?.businessLicense;
if (!file) return null;
const displayFile = getDisplayFile(file);
return displayFile.url ? displayFile : null;
});
const auxiliaryFiles = computed(() => {
return (application.value?.auxiliaryMaterials || [])
.map((file) => getDisplayFile(file))
.filter((file) => file.url);
});
function loadDetail() {
const id = String(route.params.id || '');
const raw = sessionStorage.getItem(`${DETAIL_STORAGE_PREFIX}:${id}`);
if (!raw) return;
try {
application.value = JSON.parse(raw);
} catch (error) {
application.value = null;
}
}
function goBack() {
router.push({ name: 'LoanApplication' });
}
onMounted(() => {
loadDetail();
});
</script>
<style lang="less" scoped>
.image-panel {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.file-card {
width: 280px;
min-height: 120px;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fafafa;
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
}
.file-name {
color: #333;
line-height: 1.5;
word-break: break-all;
}
</style>

View File

@@ -0,0 +1,541 @@
<template>
<n-card :bordered="false">
<n-form label-placement="left" :label-width="92" :model="queryForm" :show-feedback="false">
<n-grid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
<n-gi>
<n-form-item label="年月" path="yearMonth">
<n-date-picker
v-model:value="monthValue"
type="month"
clearable
class="w-full"
placeholder="请选择年月"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="姓名" path="name">
<n-input v-model:value="queryForm.name" clearable placeholder="请输入姓名" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="联系方式" path="contact">
<n-input v-model:value="queryForm.contact" clearable placeholder="请输入联系方式" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="身份证号" path="idCard">
<n-input v-model:value="queryForm.idCard" clearable placeholder="请输入身份证号码" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="处理状态" path="isProcessed">
<n-select
v-model:value="queryForm.isProcessed"
:options="processedOptions"
class="w-full"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-space justify="end">
<n-button @click="handleReset">
<template #icon>
<n-icon>
<ReloadOutlined />
</n-icon>
</template>
重置
</n-button>
<n-button type="primary" @click="handleSearch">
<template #icon>
<n-icon>
<SearchOutlined />
</n-icon>
</template>
查询
</n-button>
</n-space>
</n-form>
</n-card>
<n-card :bordered="false" class="mt-3" title="申请贷款列表">
<template #header-extra>
<n-space>
<n-button
type="primary"
secondary
:disabled="!checkedRowKeys.length"
:loading="handleLoading"
@click="confirmHandleSelected"
>
<template #icon>
<n-icon>
<CheckOutlined />
</n-icon>
</template>
批量处理
</n-button>
<n-button type="info" :loading="exportLoading" @click="handleExport">
<template #icon>
<n-icon>
<DownloadOutlined />
</n-icon>
</template>
导出
</n-button>
</n-space>
</template>
<n-data-table
remote
striped
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="getRowKey"
:checked-row-keys="checkedRowKeys"
:scroll-x="1700"
:single-line="false"
@update:checked-row-keys="handleCheckedRowKeys"
/>
</n-card>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { format } from 'date-fns';
import { useRouter } from 'vue-router';
import type { DataTableColumns, DataTableRowKey } from 'naive-ui';
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui';
import { CheckOutlined, DownloadOutlined, ReloadOutlined, SearchOutlined } from '@vicons/antd';
import {
exportLoanApplicationList,
getLoanApplicationList,
handleLoanApplication,
} from '@/api/loan/application';
import type {
LoanApplicationQueryListReq,
LoanApplicationQueryListRes,
} from '@/api/loan/application';
import { formatBackendDateTime } from '@/utils/dateUtil';
const DETAIL_STORAGE_PREFIX = 'LOAN_APPLICATION_DETAIL';
const familyAssetMap: Record<string, string> = {
'0': '无',
'1': '商品房',
'2': '自建房国有',
'3': '自建房集体',
'4': '商铺',
'5': '土地',
'6': '车辆',
'7': '其他',
};
const loanTypeMap: Record<string, string> = {
'0': '信用贷',
'1': '抵押贷',
};
type ProcessedFilterValue = '' | 'true' | 'false';
const processedOptions = [
{
label: '全部',
value: '',
},
{
label: '已处理',
value: 'true',
},
{
label: '未处理',
value: 'false',
},
];
const message = useMessage();
const dialog = useDialog();
const router = useRouter();
const loading = ref(false);
const handleLoading = ref(false);
const exportLoading = ref(false);
const tableData = ref<LoanApplicationQueryListRes[]>([]);
const checkedRowKeys = ref<DataTableRowKey[]>([]);
const monthValue = ref<number | null>(Date.now());
const queryForm = reactive({
name: '',
contact: '',
idCard: '',
isProcessed: '' as ProcessedFilterValue,
});
const yearMonth = computed(() => {
return monthValue.value ? format(monthValue.value, 'yyyy-MM') : '';
});
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
pageSizes: [10, 20, 30, 50],
showSizePicker: true,
showQuickJumper: true,
prefix: ({ itemCount }: { itemCount: number }) => `${itemCount}`,
onChange: (page: number) => {
pagination.page = page;
fetchList();
},
onUpdatePageSize: (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
fetchList();
},
});
const columns: DataTableColumns<LoanApplicationQueryListRes> = [
{
type: 'selection',
fixed: 'left',
width: 48,
},
{
title: '姓名',
key: 'name',
width: 100,
fixed: 'left',
ellipsis: {
tooltip: true,
},
},
{
title: '联系方式',
key: 'contact',
width: 130,
},
{
title: '身份证号码',
key: 'idCard',
width: 180,
ellipsis: {
tooltip: true,
},
},
{
title: '经营地址',
key: 'businessAddress',
width: 180,
ellipsis: {
tooltip: true,
},
},
{
title: '经营项目',
key: 'businessItem',
width: 140,
ellipsis: {
tooltip: true,
},
},
{
title: '经营时间',
key: 'businessDuration',
width: 110,
render: (row) => `${row.businessDuration ?? 0}`,
},
{
title: '年收入',
key: 'annualIncome',
width: 120,
render: (row) => `${row.annualIncome ?? 0} 万元`,
},
{
title: '负债',
key: 'hasDebt',
width: 90,
render: (row) => renderBooleanTag(row.hasDebt, '有', '无'),
},
{
title: '需求金额',
key: 'loanAmount',
width: 120,
render: (row) => `${row.loanAmount ?? 0} 万元`,
},
{
title: '家庭资产',
key: 'familyAssets',
width: 220,
render: (row) => renderFamilyAssets(row.familyAssets),
},
{
title: '贷款类型',
key: 'loanType',
width: 110,
render: (row) => renderLoanType(row.loanType),
},
{
title: '处理状态',
key: 'isProcessed',
width: 110,
render: (row) => renderBooleanTag(row.isProcessed, '已处理', '未处理', true),
},
{
title: '创建时间',
key: 'createTime',
width: 180,
render: (row) => formatBackendDateTime(row.createTime),
ellipsis: {
tooltip: true,
},
},
{
title: '操作',
key: 'actions',
width: 170,
fixed: 'right',
render: (row) =>
h(
NSpace,
{
size: 8,
},
{
default: () => [
h(
NButton,
{
size: 'small',
secondary: true,
type: 'info',
onClick: () => openDetail(row),
},
{ default: () => '详情' }
),
h(
NButton,
{
size: 'small',
type: 'primary',
disabled: row.isProcessed,
loading: handleLoading.value,
onClick: () => confirmHandle([row.id]),
},
{ default: () => (row.isProcessed ? '已处理' : '处理') }
),
],
}
),
},
];
function normalizeText(value?: string) {
return value?.trim() || undefined;
}
function buildParams(): LoanApplicationQueryListReq {
const params: LoanApplicationQueryListReq = {
pageNum: pagination.page,
pageSize: pagination.pageSize,
yearMonth: yearMonth.value,
name: normalizeText(queryForm.name),
contact: normalizeText(queryForm.contact),
idCard: normalizeText(queryForm.idCard),
};
if (queryForm.isProcessed) {
params.isProcessed = queryForm.isProcessed === 'true';
}
return params;
}
function getRowKey(row: LoanApplicationQueryListRes) {
return row.id;
}
function renderBooleanTag(value: boolean, positiveText: string, negativeText: string, reverse = false) {
const type = reverse
? value
? 'success'
: 'warning'
: value
? 'warning'
: 'success';
return h(
NTag,
{
type,
size: 'small',
},
{ default: () => (value ? positiveText : negativeText) }
);
}
function renderLoanType(value: string) {
return h(
NTag,
{
type: value === '1' ? 'info' : 'default',
size: 'small',
},
{ default: () => loanTypeMap[String(value)] || '-' }
);
}
function renderFamilyAssets(values?: string[]) {
if (!values?.length) return '-';
return h(
NSpace,
{
size: 4,
},
{
default: () =>
values.map((value) =>
h(
NTag,
{
size: 'small',
},
{ default: () => familyAssetMap[String(value)] || value }
)
),
}
);
}
async function fetchList() {
if (!yearMonth.value) {
message.warning('请选择年月');
return;
}
loading.value = true;
try {
const data = await getLoanApplicationList(buildParams());
tableData.value = data.records || [];
pagination.page = Number(data.pageNumber || pagination.page);
pagination.pageSize = Number(data.pageSize || pagination.pageSize);
pagination.itemCount = Number(data.totalRow || 0);
checkedRowKeys.value = [];
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取申请贷款列表失败';
message.error(errorMessage);
tableData.value = [];
pagination.itemCount = 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
pagination.page = 1;
fetchList();
}
function handleReset() {
queryForm.name = '';
queryForm.contact = '';
queryForm.idCard = '';
queryForm.isProcessed = '';
monthValue.value = Date.now();
handleSearch();
}
function handleCheckedRowKeys(keys: DataTableRowKey[]) {
checkedRowKeys.value = keys;
}
function openDetail(row: LoanApplicationQueryListRes) {
sessionStorage.setItem(`${DETAIL_STORAGE_PREFIX}:${row.id}`, JSON.stringify(row));
router.push({
name: 'LoanApplicationDetail',
params: {
id: row.id,
},
});
}
function confirmHandleSelected() {
confirmHandle(checkedRowKeys.value.map(Number));
}
function confirmHandle(idList: number[]) {
if (!idList.length) {
message.warning('请选择要处理的申请');
return;
}
dialog.warning({
title: '确认处理',
content: `确定处理选中的 ${idList.length} 条贷款申请吗?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: () => processApplications(idList),
});
}
async function processApplications(idList: number[]) {
handleLoading.value = true;
try {
await handleLoanApplication({ idList });
message.success('处理成功');
await fetchList();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '处理申请贷款失败';
message.error(errorMessage);
} finally {
handleLoading.value = false;
}
}
function downloadBlob(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
async function handleExport() {
if (!yearMonth.value) {
message.warning('请选择年月');
return;
}
exportLoading.value = true;
try {
const { blob, fileName } = await exportLoanApplicationList(buildParams());
downloadBlob(blob, fileName);
message.success('导出成功');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '导出申请贷款失败';
message.error(errorMessage);
} finally {
exportLoading.value = false;
}
}
onMounted(() => {
fetchList();
});
</script>
<style lang="less" scoped>
.file-link {
color: #1677ff;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
</style>

View File

@@ -76,8 +76,8 @@
const loading = ref(false);
const autoLogin = ref(true);
const formInline = reactive({
username: 'FinAdmin',
password: 'Fin9527',
username: '',
password: '',
isCaptcha: true,
});