diff --git a/.env b/.env index 52922f5..fbbbfed 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ VITE_PORT = 8001 # spa-title -VITE_GLOB_APP_TITLE = AdminPro +VITE_GLOB_APP_TITLE = FinLoan # spa shortname -VITE_GLOB_APP_SHORT_NAME = AdminPro +VITE_GLOB_APP_SHORT_NAME = FinLoan diff --git a/.env.development b/.env.development index 596466c..5ed83fa 100644 --- a/.env.development +++ b/.env.development @@ -5,7 +5,7 @@ VITE_PORT = 8001 VITE_PUBLIC_PATH = / # 是否开启 mock -VITE_USE_MOCK = false +VITE_USE_MOCK = mock # 是否开启控制台打印 mock 请求信息 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=[["/api","http://192.168.1.26:7060"]] +VITE_PROXY=[["/fg-api","https://fin-loan.mmlizi.com"]] # 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= diff --git a/.env.production b/.env.production index 401d8ba..f66d74d 100644 --- a/.env.production +++ b/.env.production @@ -1,17 +1,17 @@ # 是否开启mock -VITE_USE_MOCK = true +VITE_USE_MOCK = false # 网站根目录 -VITE_PUBLIC_PATH = / +VITE_PUBLIC_PATH = /fg # 是否删除console VITE_DROP_CONSOLE = true # 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= diff --git a/src/api/loan/application.ts b/src/api/loan/application.ts new file mode 100644 index 0000000..a73b915 --- /dev/null +++ b/src/api/loan/application.ts @@ -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 { + 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>( + '/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>( + '/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; + 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')), + }; +} diff --git a/src/api/system/user.ts b/src/api/system/user.ts index 12d10f9..9720132 100644 --- a/src/api/system/user.ts +++ b/src/api/system/user.ts @@ -49,7 +49,7 @@ export interface LogoutRes { * @description: 获取用户信息 */ export function getUserInfo() { - return Alova.Get('/api/user/info', { + return Alova.Get('/user/info', { meta: { isReturnNativeResponse: true, }, @@ -61,7 +61,7 @@ export function getUserInfo() { */ export function login(params: LoginParams) { return Alova.Post( - '/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('/api/user/logout', { + return Alova.Get('/user/logout', { meta: { isReturnNativeResponse: true, }, diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue index c6a2ffe..487e57e 100644 --- a/src/layout/components/TagsView/index.vue +++ b/src/layout/components/TagsView/index.vue @@ -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); diff --git a/src/router/index.ts b/src/router/index.ts index a1d909c..8c57d47 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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('./modules/**/*.ts', { eager: true }); +const modules = import.meta.glob('./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) { diff --git a/src/router/modules/loan.ts b/src/router/modules/loan.ts new file mode 100644 index 0000000..fc8985d --- /dev/null +++ b/src/router/modules/loan.ts @@ -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 = [ + { + 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; diff --git a/src/utils/encrypt.ts b/src/utils/encrypt.ts new file mode 100644 index 0000000..ca28c11 --- /dev/null +++ b/src/utils/encrypt.ts @@ -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); +} diff --git a/src/utils/http/alova/index.ts b/src/utils/http/alova/index.ts index b453d9c..bb1c3b3 100644 --- a/src/utils/http/alova/index.ts +++ b/src/utils/http/alova/index.ts @@ -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) => { diff --git a/src/views/loan/application/detail.vue b/src/views/loan/application/detail.vue new file mode 100644 index 0000000..bfe65a6 --- /dev/null +++ b/src/views/loan/application/detail.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/src/views/loan/application/index.vue b/src/views/loan/application/index.vue new file mode 100644 index 0000000..c4067d0 --- /dev/null +++ b/src/views/loan/application/index.vue @@ -0,0 +1,541 @@ + + + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 193db45..c3ce8fe 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -76,8 +76,8 @@ const loading = ref(false); const autoLogin = ref(true); const formInline = reactive({ - username: 'FinAdmin', - password: 'Fin9527', + username: '', + password: '', isCaptcha: true, });