提交
This commit is contained in:
4
.env
4
.env
@@ -2,7 +2,7 @@
|
|||||||
VITE_PORT = 8001
|
VITE_PORT = 8001
|
||||||
|
|
||||||
# spa-title
|
# spa-title
|
||||||
VITE_GLOB_APP_TITLE = AdminPro
|
VITE_GLOB_APP_TITLE = FinLoan
|
||||||
|
|
||||||
# spa shortname
|
# spa shortname
|
||||||
VITE_GLOB_APP_SHORT_NAME = AdminPro
|
VITE_GLOB_APP_SHORT_NAME = FinLoan
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ VITE_PORT = 8001
|
|||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# 是否开启 mock
|
# 是否开启 mock
|
||||||
VITE_USE_MOCK = false
|
VITE_USE_MOCK = mock
|
||||||
|
|
||||||
# 是否开启控制台打印 mock 请求信息
|
# 是否开启控制台打印 mock 请求信息
|
||||||
VITE_LOGGER_MOCK = true
|
VITE_LOGGER_MOCK = true
|
||||||
@@ -15,13 +15,13 @@ VITE_DROP_CONSOLE = true
|
|||||||
|
|
||||||
# 跨域代理,可以配置多个,请注意不要换行
|
# 跨域代理,可以配置多个,请注意不要换行
|
||||||
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
|
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
|
||||||
VITE_PROXY=[["/api","http://192.168.1.26:7060"]]
|
VITE_PROXY=[["/fg-api","https://fin-loan.mmlizi.com"]]
|
||||||
|
|
||||||
# API 接口地址
|
# API 接口地址
|
||||||
#VITE_GLOB_API_URL = http://192.168.1.26:7060
|
VITE_GLOB_API_URL = /
|
||||||
|
|
||||||
# 接口前缀
|
# 接口前缀
|
||||||
#VITE_GLOB_API_URL_PREFIX =
|
#VITE_GLOB_API_URL_PREFIX = /fg-api
|
||||||
|
|
||||||
# 文件上传地址
|
# 文件上传地址
|
||||||
VITE_GLOB_UPLOAD_URL=
|
VITE_GLOB_UPLOAD_URL=
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# 是否开启mock
|
# 是否开启mock
|
||||||
VITE_USE_MOCK = true
|
VITE_USE_MOCK = false
|
||||||
|
|
||||||
# 网站根目录
|
# 网站根目录
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /fg
|
||||||
|
|
||||||
# 是否删除console
|
# 是否删除console
|
||||||
VITE_DROP_CONSOLE = true
|
VITE_DROP_CONSOLE = true
|
||||||
|
|
||||||
# API
|
# API
|
||||||
VITE_GLOB_API_URL =
|
VITE_GLOB_API_URL = /fg-api
|
||||||
|
|
||||||
# 接口前缀
|
# 接口前缀
|
||||||
VITE_GLOB_API_URL_PREFIX = /api
|
VITE_GLOB_API_URL_PREFIX =
|
||||||
|
|
||||||
# 图片上传地址
|
# 图片上传地址
|
||||||
VITE_GLOB_UPLOAD_URL=
|
VITE_GLOB_UPLOAD_URL=
|
||||||
|
|||||||
182
src/api/loan/application.ts
Normal file
182
src/api/loan/application.ts
Normal 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')),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ export interface LogoutRes {
|
|||||||
* @description: 获取用户信息
|
* @description: 获取用户信息
|
||||||
*/
|
*/
|
||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return Alova.Get<UserInfoRes>('/api/user/info', {
|
return Alova.Get<UserInfoRes>('/user/info', {
|
||||||
meta: {
|
meta: {
|
||||||
isReturnNativeResponse: true,
|
isReturnNativeResponse: true,
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ export function getUserInfo() {
|
|||||||
*/
|
*/
|
||||||
export function login(params: LoginParams) {
|
export function login(params: LoginParams) {
|
||||||
return Alova.Post<LoginRes>(
|
return Alova.Post<LoginRes>(
|
||||||
'/api/user/login',
|
'/fg-api/user/login',
|
||||||
params,
|
params,
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
@@ -82,7 +82,7 @@ export function changePassword(params, uid) {
|
|||||||
* @description: 用户登出
|
* @description: 用户登出
|
||||||
*/
|
*/
|
||||||
export function logout() {
|
export function logout() {
|
||||||
return Alova.Get<LogoutRes>('/api/user/logout', {
|
return Alova.Get<LogoutRes>('/user/logout', {
|
||||||
meta: {
|
meta: {
|
||||||
isReturnNativeResponse: true,
|
isReturnNativeResponse: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -239,16 +239,29 @@
|
|||||||
cacheRoutes = [simpleRoute];
|
cacheRoutes = [simpleRoute];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将最新的路由信息同步到 localStorage 中
|
// 将最新的路由信息同步到 localStorage 中,并清掉已移除菜单留下的旧标签
|
||||||
const routes = router.getRoutes();
|
const routes = router.getRoutes();
|
||||||
cacheRoutes.forEach((cacheRoute) => {
|
cacheRoutes = cacheRoutes
|
||||||
const route = routes.find((route) => route.path === cacheRoute.path);
|
.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) {
|
if (route) {
|
||||||
cacheRoute.meta = route.meta || cacheRoute.meta;
|
cacheRoute.meta = route.meta || cacheRoute.meta;
|
||||||
cacheRoute.name = (route.name || cacheRoute.name) as string;
|
cacheRoute.name = (route.name || cacheRoute.name) as string;
|
||||||
}
|
}
|
||||||
|
return cacheRoute;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!cacheRoutes.length) {
|
||||||
|
cacheRoutes = [simpleRoute];
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化标签页
|
// 初始化标签页
|
||||||
tabsViewStore.initTabs(cacheRoutes);
|
tabsViewStore.initTabs(cacheRoutes);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { App } from 'vue';
|
import { App } from 'vue';
|
||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
|
||||||
import { RedirectRoute } from '@/router/base';
|
import { RedirectRoute } from '@/router/base';
|
||||||
import { PageEnum } from '@/enums/pageEnum';
|
import { PageEnum } from '@/enums/pageEnum';
|
||||||
import { createRouterGuards } from './guards';
|
import { createRouterGuards } from './guards';
|
||||||
import type { IModuleType } from './types';
|
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 routeModuleList: RouteRecordRaw[] = Object.keys(modules).reduce((list, key) => {
|
||||||
const mod = modules[key].default ?? {};
|
const mod = modules[key].default ?? {};
|
||||||
@@ -43,11 +43,16 @@ export const asyncRoutes = [...routeModuleList];
|
|||||||
//普通路由 无需验证权限
|
//普通路由 无需验证权限
|
||||||
export const constantRouter: RouteRecordRaw[] = [LoginRoute, RootRoute, RedirectRoute];
|
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({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
routes: constantRouter,
|
routes: constantRouter,
|
||||||
strict: true,
|
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setupRouter(app: App) {
|
export function setupRouter(app: App) {
|
||||||
|
|||||||
41
src/router/modules/loan.ts
Normal file
41
src/router/modules/loan.ts
Normal 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
12
src/utils/encrypt.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { createAlova } from 'alova';
|
|||||||
import VueHook from 'alova/vue';
|
import VueHook from 'alova/vue';
|
||||||
import adapterFetch from 'alova/fetch';
|
import adapterFetch from 'alova/fetch';
|
||||||
import { createAlovaMockAdapter } from '@alova/mock';
|
import { createAlovaMockAdapter } from '@alova/mock';
|
||||||
import { isString } from 'lodash-es';
|
|
||||||
import mocks from './mocks';
|
import mocks from './mocks';
|
||||||
import { useUser } from '@/store/modules/user';
|
import { useUser } from '@/store/modules/user';
|
||||||
import { storage } from '@/utils/Storage';
|
import { storage } from '@/utils/Storage';
|
||||||
@@ -77,9 +76,6 @@ export const Alova = createAlova({
|
|||||||
if (!isUrlStr && urlPrefix) {
|
if (!isUrlStr && urlPrefix) {
|
||||||
method.url = `${urlPrefix}${method.url}`;
|
method.url = `${urlPrefix}${method.url}`;
|
||||||
}
|
}
|
||||||
if (!isUrlStr && apiUrl && isString(apiUrl)) {
|
|
||||||
method.url = `${apiUrl}${method.url}`;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
responded: {
|
responded: {
|
||||||
onSuccess: async (response, method) => {
|
onSuccess: async (response, method) => {
|
||||||
|
|||||||
224
src/views/loan/application/detail.vue
Normal file
224
src/views/loan/application/detail.vue
Normal 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>
|
||||||
541
src/views/loan/application/index.vue
Normal file
541
src/views/loan/application/index.vue
Normal 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>
|
||||||
@@ -76,8 +76,8 @@
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const autoLogin = ref(true);
|
const autoLogin = ref(true);
|
||||||
const formInline = reactive({
|
const formInline = reactive({
|
||||||
username: 'FinAdmin',
|
username: '',
|
||||||
password: 'Fin9527',
|
password: '',
|
||||||
isCaptcha: true,
|
isCaptcha: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user