diff --git a/apps/finance/.env b/apps/finance/.env
new file mode 100644
index 0000000..729c093
--- /dev/null
+++ b/apps/finance/.env
@@ -0,0 +1,8 @@
+# 应用标题
+VITE_APP_TITLE=财务系统
+
+# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
+VITE_APP_NAMESPACE=finance
+
+# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
+VITE_APP_STORE_SECURE_KEY=finance
diff --git a/apps/finance/.env.analyze b/apps/finance/.env.analyze
new file mode 100644
index 0000000..ffafa8d
--- /dev/null
+++ b/apps/finance/.env.analyze
@@ -0,0 +1,7 @@
+# public path
+VITE_BASE=/
+
+# Basic interface address SPA
+VITE_GLOB_API_URL=/api
+
+VITE_VISUALIZER=true
diff --git a/apps/finance/.env.development b/apps/finance/.env.development
new file mode 100644
index 0000000..39e8c54
--- /dev/null
+++ b/apps/finance/.env.development
@@ -0,0 +1,16 @@
+# 端口号
+VITE_PORT=9000
+
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=http://192.168.110.100:9000/finance
+
+# 是否开启 Nitro Mock服务,true 为开启,false 为关闭
+VITE_NITRO_MOCK=false
+
+# 是否打开 devtools,true 为打开,false 为关闭
+VITE_DEVTOOLS=false
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
diff --git a/apps/finance/.env.production b/apps/finance/.env.production
new file mode 100644
index 0000000..cfc9cb1
--- /dev/null
+++ b/apps/finance/.env.production
@@ -0,0 +1,19 @@
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=http://192.168.110.100:9000/finance
+
+# 是否开启压缩,可以设置为 none, brotli, gzip
+VITE_COMPRESS=none
+
+# 是否开启 PWA
+VITE_PWA=false
+
+# vue-router 的模式
+VITE_ROUTER_HISTORY=hash
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
+
+# 打包后是否生成dist.zip
+VITE_ARCHIVER=true
diff --git a/apps/finance/index.html b/apps/finance/index.html
new file mode 100644
index 0000000..480eb84
--- /dev/null
+++ b/apps/finance/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+ <%= VITE_APP_TITLE %>
+
+
+
+
+
+
+
+
diff --git a/apps/finance/package.json b/apps/finance/package.json
new file mode 100644
index 0000000..aeceab3
--- /dev/null
+++ b/apps/finance/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@zmzm/finance",
+ "version": "0.0.1",
+ "homepage": "",
+ "repository": {
+ "type": "git",
+ "url": "git+https://git.nuttyreading.com/zm/finance-master.git",
+ "directory": "apps/finance"
+ },
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "build": "pnpm vite build --mode production",
+ "build:analyze": "pnpm vite build --mode analyze",
+ "dev": "pnpm vite --mode development",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit --skipLibCheck"
+ },
+ "imports": {
+ "#/*": "./src/*"
+ },
+ "dependencies": {
+ "@vben-core/shadcn-ui": "workspace:*",
+ "@vben-core/shared": "workspace:*",
+ "@vben-core/typings": "workspace:*",
+ "@vben/access": "workspace:*",
+ "@vben/common-ui": "workspace:*",
+ "@vben/constants": "workspace:*",
+ "@vben/hooks": "workspace:*",
+ "@vben/icons": "workspace:*",
+ "@vben/layouts": "workspace:*",
+ "@vben/locales": "workspace:*",
+ "@vben/plugins": "workspace:*",
+ "@vben/preferences": "workspace:*",
+ "@vben/request": "workspace:*",
+ "@vben/stores": "workspace:*",
+ "@vben/styles": "workspace:*",
+ "@vben/types": "workspace:*",
+ "@vben/utils": "workspace:*",
+ "@vueuse/core": "catalog:",
+ "ant-design-vue": "catalog:",
+ "dayjs": "catalog:",
+ "pinia": "catalog:",
+ "vue": "catalog:",
+ "vue-router": "catalog:"
+ }
+}
diff --git a/apps/finance/postcss.config.mjs b/apps/finance/postcss.config.mjs
new file mode 100644
index 0000000..3d80704
--- /dev/null
+++ b/apps/finance/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';
diff --git a/apps/finance/src/adapter/component/index.ts b/apps/finance/src/adapter/component/index.ts
new file mode 100644
index 0000000..cde8369
--- /dev/null
+++ b/apps/finance/src/adapter/component/index.ts
@@ -0,0 +1,383 @@
+/**
+ * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
+ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
+ */
+
+import type { UploadChangeParam, UploadFile, UploadProps } from 'ant-design-vue';
+
+import type { Component, Ref } from 'vue';
+
+import type { BaseFormComponentType } from '@vben/common-ui';
+import type { Recordable } from '@vben/types';
+
+import { defineAsyncComponent, defineComponent, h, ref, render, unref, watch } from 'vue';
+
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+import { $t } from '@vben/locales';
+import { isEmpty } from '@vben/utils';
+
+import { notification } from 'ant-design-vue';
+
+const AutoComplete = defineAsyncComponent(() => import('ant-design-vue/es/auto-complete'));
+const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
+const Checkbox = defineAsyncComponent(() => import('ant-design-vue/es/checkbox'));
+const CheckboxGroup = defineAsyncComponent(() =>
+ import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
+);
+const DatePicker = defineAsyncComponent(() => import('ant-design-vue/es/date-picker'));
+const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
+const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
+const InputNumber = defineAsyncComponent(() => import('ant-design-vue/es/input-number'));
+const InputPassword = defineAsyncComponent(() =>
+ import('ant-design-vue/es/input').then((res) => res.InputPassword),
+);
+const Mentions = defineAsyncComponent(() => import('ant-design-vue/es/mentions'));
+const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
+const RadioGroup = defineAsyncComponent(() =>
+ import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
+);
+const RangePicker = defineAsyncComponent(() =>
+ import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
+);
+const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
+const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
+const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
+const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
+const Textarea = defineAsyncComponent(() =>
+ import('ant-design-vue/es/input').then((res) => res.Textarea),
+);
+const TimePicker = defineAsyncComponent(() => import('ant-design-vue/es/time-picker'));
+const TreeSelect = defineAsyncComponent(() => import('ant-design-vue/es/tree-select'));
+const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
+const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
+const PreviewGroup = defineAsyncComponent(() =>
+ import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
+);
+
+const withDefaultPlaceholder = (
+ component: T,
+ type: 'input' | 'select',
+ componentProps: Recordable = {},
+) => {
+ return defineComponent({
+ name: component.name,
+ inheritAttrs: false,
+ setup: (props: any, { attrs, expose, slots }) => {
+ const placeholder = props?.placeholder || attrs?.placeholder || $t(`ui.placeholder.${type}`);
+ // 透传组件暴露的方法
+ const innerRef = ref();
+ expose(
+ new Proxy(
+ {},
+ {
+ get: (_target, key) => innerRef.value?.[key],
+ has: (_target, key) => key in (innerRef.value || {}),
+ },
+ ),
+ );
+ return () =>
+ h(component, { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef }, slots);
+ },
+ });
+};
+
+const withPreviewUpload = () => {
+ return defineComponent({
+ name: Upload.name,
+ emits: ['change', 'update:modelValue'],
+ setup: (props: any, { attrs, slots, emit }: { attrs: any; emit: any; slots: any }) => {
+ const previewVisible = ref(false);
+
+ const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
+
+ const listType = attrs?.listType || attrs?.['list-type'] || 'text';
+
+ const fileList = ref(attrs?.fileList || attrs?.['file-list'] || []);
+
+ const handleChange = async (event: UploadChangeParam) => {
+ fileList.value = event.fileList;
+ emit('change', event);
+ emit('update:modelValue', event.fileList?.length ? fileList.value : undefined);
+ };
+
+ const handlePreview = async (file: UploadFile) => {
+ previewVisible.value = true;
+ await previewImage(file, previewVisible, fileList);
+ };
+
+ const renderUploadButton = (): any => {
+ const isDisabled = attrs.disabled;
+
+ // 如果禁用,不渲染上传按钮
+ if (isDisabled) {
+ return null;
+ }
+
+ // 否则渲染默认上传按钮
+ return isEmpty(slots) ? createDefaultSlotsWithUpload(listType, placeholder) : slots;
+ };
+
+ // 可以监听到表单API设置的值
+ watch(
+ () => attrs.modelValue,
+ (res) => {
+ fileList.value = res;
+ },
+ );
+
+ return () =>
+ h(
+ Upload,
+ {
+ ...props,
+ ...attrs,
+ fileList: fileList.value,
+ onChange: handleChange,
+ onPreview: handlePreview,
+ },
+ renderUploadButton(),
+ );
+ },
+ });
+};
+
+const createDefaultSlotsWithUpload = (listType: string, placeholder: string) => {
+ switch (listType) {
+ case 'picture-card': {
+ return {
+ default: () => placeholder,
+ };
+ }
+ default: {
+ return {
+ default: () =>
+ h(
+ Button,
+ {
+ icon: h(IconifyIcon, {
+ icon: 'ant-design:upload-outlined',
+ class: 'mb-1 size-4',
+ }),
+ },
+ () => placeholder,
+ ),
+ };
+ }
+ }
+};
+
+const previewImage = async (
+ file: UploadFile,
+ visible: Ref,
+ fileList: Ref,
+) => {
+ // 检查是否为图片文件的辅助函数
+ const isImageFile = (file: UploadFile): boolean => {
+ const imageExtensions = new Set(['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp']);
+ if (file.url) {
+ const ext = file.url?.split('.').pop()?.toLowerCase();
+ return ext ? imageExtensions.has(ext) : false;
+ }
+ if (!file.type) {
+ const ext = file.name?.split('.').pop()?.toLowerCase();
+ return ext ? imageExtensions.has(ext) : false;
+ }
+ return file.type.startsWith('image/');
+ };
+
+ // 如果当前文件不是图片,直接打开
+ if (!isImageFile(file)) {
+ if (file.url) {
+ window.open(file.url, '_blank');
+ } else if (file.preview) {
+ window.open(file.preview, '_blank');
+ } else {
+ console.warn('无法打开文件,没有可用的URL或预览地址');
+ }
+ return;
+ }
+
+ // 对于图片文件,继续使用预览组
+ const [ImageComponent, PreviewGroupComponent] = await Promise.all([Image, PreviewGroup]);
+
+ const getBase64 = (file: File) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.addEventListener('load', () => resolve(reader.result));
+ reader.addEventListener('error', (error) => reject(error));
+ });
+ };
+ // 从fileList中过滤出所有图片文件
+ const imageFiles = (unref(fileList) || []).filter((element) => isImageFile(element));
+
+ // 为所有没有预览地址的图片生成预览
+ for (const imgFile of imageFiles) {
+ if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
+ imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
+ }
+ }
+ const container: HTMLElement | null = document.createElement('div');
+ document.body.append(container);
+
+ // 用于追踪组件是否已卸载
+ let isUnmounted = false;
+
+ const PreviewWrapper = {
+ setup() {
+ return () => {
+ if (isUnmounted) return null;
+ return h(
+ PreviewGroupComponent,
+ {
+ class: 'hidden',
+ preview: {
+ visible: visible.value,
+ // 设置初始显示的图片索引
+ current: imageFiles.findIndex((f) => f.uid === file.uid),
+ onVisibleChange: (value: boolean) => {
+ visible.value = value;
+ if (!value) {
+ // 延迟清理,确保动画完成
+ setTimeout(() => {
+ if (!isUnmounted && container) {
+ isUnmounted = true;
+ render(null, container);
+ container.remove();
+ }
+ }, 300);
+ }
+ },
+ },
+ },
+ () =>
+ // 渲染所有图片文件
+ imageFiles.map((imgFile) =>
+ h(ImageComponent, {
+ key: imgFile.uid,
+ src: imgFile.url || imgFile.preview,
+ }),
+ ),
+ );
+ };
+ },
+ };
+
+ render(h(PreviewWrapper), container);
+};
+
+// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
+export type ComponentType =
+ | 'ApiSelect'
+ | 'ApiTreeSelect'
+ | 'AutoComplete'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'DatePicker'
+ | 'DefaultButton'
+ | 'Divider'
+ | 'IconPicker'
+ | 'Input'
+ | 'InputNumber'
+ | 'InputPassword'
+ | 'Mentions'
+ | 'PrimaryButton'
+ | 'Radio'
+ | 'RadioGroup'
+ | 'RangePicker'
+ | 'Rate'
+ | 'Select'
+ | 'Space'
+ | 'Switch'
+ | 'Textarea'
+ | 'TimePicker'
+ | 'TreeSelect'
+ | 'Upload'
+ | BaseFormComponentType;
+
+async function initComponentAdapter() {
+ const components: Partial> = {
+ // 如果你的组件体积比较大,可以使用异步加载
+ // Button: () =>
+ // import('xxx').then((res) => res.Button),
+ ApiSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiSelect',
+ },
+ 'select',
+ {
+ component: Select,
+ loadingSlot: 'suffixIcon',
+ visibleEvent: 'onDropdownVisibleChange',
+ modelPropName: 'value',
+ },
+ ),
+ ApiTreeSelect: withDefaultPlaceholder(
+ {
+ ...ApiComponent,
+ name: 'ApiTreeSelect',
+ },
+ 'select',
+ {
+ component: TreeSelect,
+ fieldNames: { label: 'label', value: 'value', children: 'children' },
+ loadingSlot: 'suffixIcon',
+ modelPropName: 'value',
+ optionsPropName: 'treeData',
+ visibleEvent: 'onVisibleChange',
+ },
+ ),
+ AutoComplete,
+ Checkbox,
+ CheckboxGroup,
+ DatePicker,
+ // 自定义默认按钮
+ DefaultButton: (props, { attrs, slots }) => {
+ return h(Button, { ...props, attrs, type: 'default' }, slots);
+ },
+ Divider,
+ IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+ iconSlot: 'addonAfter',
+ inputComponent: Input,
+ modelValueProp: 'value',
+ }),
+ Input: withDefaultPlaceholder(Input, 'input'),
+ InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
+ InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
+ Mentions: withDefaultPlaceholder(Mentions, 'input'),
+ // 自定义主要按钮
+ PrimaryButton: (props, { attrs, slots }) => {
+ return h(Button, { ...props, attrs, type: 'primary' }, slots);
+ },
+ Radio,
+ RadioGroup,
+ RangePicker,
+ Rate,
+ Select: withDefaultPlaceholder(Select, 'select'),
+ Space,
+ Switch,
+ Textarea: withDefaultPlaceholder(Textarea, 'input'),
+ TimePicker,
+ TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
+ Upload: withPreviewUpload(),
+ };
+
+ // 将组件注册到全局共享状态中
+ globalShareState.setComponents(components);
+
+ // 定义全局共享状态中的消息提示
+ globalShareState.defineMessage({
+ // 复制成功消息提示
+ copyPreferencesSuccess: (title, content) => {
+ notification.success({
+ description: content,
+ message: title,
+ placement: 'bottomRight',
+ });
+ },
+ });
+}
+
+export { initComponentAdapter };
diff --git a/apps/finance/src/adapter/form.ts b/apps/finance/src/adapter/form.ts
new file mode 100644
index 0000000..ced23aa
--- /dev/null
+++ b/apps/finance/src/adapter/form.ts
@@ -0,0 +1,46 @@
+import type { VbenFormSchema as FormSchema, VbenFormProps } from '@vben/common-ui';
+
+import type { ComponentType } from './component';
+
+import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+async function initSetupVbenForm() {
+ setupVbenForm({
+ config: {
+ // ant design vue组件库默认都是 v-model:value
+ baseModelPropName: 'value',
+
+ // 一些组件是 v-model:checked 或者 v-model:fileList
+ modelPropNameMap: {
+ Checkbox: 'checked',
+ Radio: 'checked',
+ Switch: 'checked',
+ Upload: 'fileList',
+ },
+ },
+ defineRules: {
+ // 输入项目必填国际化适配
+ required: (value, _params, ctx) => {
+ if (value === undefined || value === null || value.length === 0) {
+ return $t('ui.formRules.required', [ctx.label]);
+ }
+ return true;
+ },
+ // 选择项目必填国际化适配
+ selectRequired: (value, _params, ctx) => {
+ if (value === undefined || value === null) {
+ return $t('ui.formRules.selectRequired', [ctx.label]);
+ }
+ return true;
+ },
+ },
+ });
+}
+
+const useVbenForm = useForm;
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema = FormSchema;
+export type { VbenFormProps };
diff --git a/apps/finance/src/adapter/vxe-table.ts b/apps/finance/src/adapter/vxe-table.ts
new file mode 100644
index 0000000..b0a0c12
--- /dev/null
+++ b/apps/finance/src/adapter/vxe-table.ts
@@ -0,0 +1,66 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import { h } from 'vue';
+
+import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
+
+import { Button, Image } from 'ant-design-vue';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+ configVxeTable: (vxeUI) => {
+ vxeUI.setConfig({
+ grid: {
+ align: 'center',
+ border: false,
+ columnConfig: {
+ resizable: true,
+ },
+ minHeight: 180,
+ formConfig: {
+ // 全局禁用vxe-table的表单配置,使用formOptions
+ enabled: false,
+ },
+ proxyConfig: {
+ autoLoad: true,
+ response: {
+ result: 'items',
+ total: 'total',
+ list: 'items',
+ },
+ showActiveMsg: true,
+ showResponseMsg: false,
+ },
+ round: true,
+ showOverflow: true,
+ size: 'small',
+ } as VxeTableGridOptions,
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellImage' },
+ vxeUI.renderer.add('CellImage', {
+ renderTableDefault(renderOpts, params) {
+ const { props } = renderOpts;
+ const { column, row } = params;
+ return h(Image, { src: row[column.field], ...props });
+ },
+ });
+
+ // 表格配置项可以用 cellRender: { name: 'CellLink' },
+ vxeUI.renderer.add('CellLink', {
+ renderTableDefault(renderOpts) {
+ const { props } = renderOpts;
+ return h(Button, { size: 'small', type: 'link' }, { default: () => props?.text });
+ },
+ });
+
+ // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+ // vxeUI.formats.add
+ },
+ useVbenForm,
+});
+
+export { useVbenVxeGrid };
+
+export type * from '@vben/plugins/vxe-table';
diff --git a/apps/finance/src/api/core/auth.ts b/apps/finance/src/api/core/auth.ts
new file mode 100644
index 0000000..7907d2f
--- /dev/null
+++ b/apps/finance/src/api/core/auth.ts
@@ -0,0 +1,69 @@
+import type { BasicUserInfo } from '@vben-core/typings';
+
+import { baseRequestClient, requestClient } from '#/api/request';
+
+export namespace AuthApi {
+ /** 登录接口参数 */
+ export interface LoginParams {
+ password?: string;
+ username?: string;
+ }
+
+ export interface UserInfo extends BasicUserInfo {
+ /**
+ * 最后登录时间
+ */
+ lastTime: string;
+ }
+
+ /** 登录接口返回值 */
+ export interface LoginResult {
+ userToken: T;
+ userEntity: U;
+ }
+
+ export interface UserToken {
+ token: string;
+ userId: string;
+ expireTime: string;
+ updateTime: string;
+ }
+
+ export interface RefreshTokenResult {
+ data: string;
+ status: number;
+ }
+}
+
+/**
+ * 登录
+ */
+export async function loginApi(data: AuthApi.LoginParams) {
+ return requestClient.post>('/auth/login', data);
+}
+
+/**
+ * 刷新accessToken
+ */
+export async function refreshTokenApi() {
+ return baseRequestClient.post('/auth/refresh', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 退出登录
+ */
+export async function logoutApi() {
+ return baseRequestClient.post('/auth/logout', {
+ withCredentials: true,
+ });
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodesApi() {
+ // return requestClient.get('/auth/codes');
+ return [];
+}
diff --git a/apps/finance/src/api/core/index.ts b/apps/finance/src/api/core/index.ts
new file mode 100644
index 0000000..28a5aef
--- /dev/null
+++ b/apps/finance/src/api/core/index.ts
@@ -0,0 +1,3 @@
+export * from './auth';
+export * from './menu';
+export * from './user';
diff --git a/apps/finance/src/api/core/menu.ts b/apps/finance/src/api/core/menu.ts
new file mode 100644
index 0000000..9ef60b1
--- /dev/null
+++ b/apps/finance/src/api/core/menu.ts
@@ -0,0 +1,10 @@
+import type { RouteRecordStringComponent } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户所有菜单
+ */
+export async function getAllMenusApi() {
+ return requestClient.get('/menu/all');
+}
diff --git a/apps/finance/src/api/core/user.ts b/apps/finance/src/api/core/user.ts
new file mode 100644
index 0000000..7e28ea8
--- /dev/null
+++ b/apps/finance/src/api/core/user.ts
@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/api/request';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfoApi() {
+ return requestClient.get('/user/info');
+}
diff --git a/apps/finance/src/api/index.ts b/apps/finance/src/api/index.ts
new file mode 100644
index 0000000..4b0e041
--- /dev/null
+++ b/apps/finance/src/api/index.ts
@@ -0,0 +1 @@
+export * from './core';
diff --git a/apps/finance/src/api/permission/user.ts b/apps/finance/src/api/permission/user.ts
new file mode 100644
index 0000000..d7262a6
--- /dev/null
+++ b/apps/finance/src/api/permission/user.ts
@@ -0,0 +1,24 @@
+import { requestClient } from '#/api/request';
+
+export const userApi = {
+ /**
+ * 获取文件列表
+ */
+ getUserList: (data: any) => {
+ return requestClient.post('common/sysUser/getSysUserList', data);
+ },
+
+ /**
+ * 创建用户
+ */
+ createUser: (data: any) => {
+ return requestClient.post('common/sysUser/addSysUser', data);
+ },
+
+ /**
+ * 修改用户
+ */
+ updateUser: (data: any) => {
+ return requestClient.post('common/user/addUser', data);
+ },
+};
diff --git a/apps/finance/src/api/request.ts b/apps/finance/src/api/request.ts
new file mode 100644
index 0000000..d5f9415
--- /dev/null
+++ b/apps/finance/src/api/request.ts
@@ -0,0 +1,223 @@
+/**
+ * 该文件可自行根据业务逻辑进行调整
+ */
+import type { RequestClientOptions } from '@vben/request';
+
+import { useAppConfig } from '@vben/hooks';
+import { preferences } from '@vben/preferences';
+import {
+ authenticateResponseInterceptor,
+ defaultResponseInterceptor,
+ errorMessageResponseInterceptor,
+ RequestClient,
+} from '@vben/request';
+import { useAccessStore } from '@vben/stores';
+
+import { message } from 'ant-design-vue';
+
+import { useAuthStore } from '#/store';
+
+import { refreshTokenApi } from './core';
+
+const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
+
+function createRequestClient(baseURL: string, options?: RequestClientOptions) {
+ const client = new RequestClient({
+ ...options,
+ baseURL,
+ });
+
+ /**
+ * 重新认证逻辑
+ */
+ async function doReAuthenticate() {
+ console.warn('Access token or refresh token is invalid or expired. ');
+ const accessStore = useAccessStore();
+ const authStore = useAuthStore();
+ accessStore.setAccessToken(null);
+
+ if (preferences.app.loginExpiredMode === 'modal' && accessStore.isAccessChecked) {
+ accessStore.setLoginExpired(true);
+ } else {
+ // 显示登录过期提示
+ message.error({
+ content: '您的登录状态已过期,请重新登录',
+ duration: 3,
+ });
+ // 短暂延迟后跳转,让用户看到提示
+ setTimeout(() => {
+ authStore.logout();
+ }, 1000);
+ }
+ }
+
+ /**
+ * 刷新token逻辑
+ */
+ async function doRefreshToken() {
+ const accessStore = useAccessStore();
+ const resp = await refreshTokenApi();
+ const newToken = resp.data;
+ accessStore.setAccessToken(newToken);
+ return newToken;
+ }
+
+ function formatToken(token: null | string) {
+ return token ? `${token}` : null;
+ }
+
+ // 处理请求参数中的undefined值,将其替换为空字符串
+ function handleUndefinedParams(params: any): any {
+ if (!params || typeof params !== 'object') {
+ return params;
+ }
+
+ const handleUndefined = (obj: any): any => {
+ if (Array.isArray(obj)) {
+ return obj.map((item) => handleUndefined(item));
+ }
+
+ if (obj !== null && typeof obj === 'object') {
+ const result: any = {};
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ const value = obj[key];
+ if (value === undefined) {
+ result[key] = '';
+ } else if (value !== null && typeof value === 'object') {
+ result[key] = handleUndefined(value);
+ } else {
+ result[key] = value;
+ }
+ }
+ }
+ return result;
+ }
+
+ return obj;
+ };
+
+ return handleUndefined(params);
+ }
+
+ // 请求头处理
+ client.addRequestInterceptor({
+ fulfilled: async (config) => {
+ const accessStore = useAccessStore();
+
+ config.headers.token = formatToken(accessStore.accessToken);
+ config.headers['Accept-Language'] = preferences.app.locale;
+
+ // 处理请求参数中的undefined值
+ if (config.params) {
+ config.params = handleUndefinedParams(config.params);
+ }
+
+ // 处理请求体中的undefined值
+ if (config.data && !(config.data instanceof FormData)) {
+ // 如果是FormData,不进行处理,避免丢失参数
+ // FormData不需要处理undefined值,因为浏览器会自动处理
+ config.data = handleUndefinedParams(config.data);
+ }
+
+ return config;
+ },
+ });
+
+ // 自定义拦截器,处理后端返回的status为200但code不为0的情况
+ client.addResponseInterceptor({
+ fulfilled: (response) => {
+ const { data: responseData, status } = response;
+
+ // 检查HTTP状态码为200但响应数据中code不为0的情况
+ if (status === 200 && responseData?.code !== 0) {
+ // 创建一个错误对象,模拟HTTP错误响应
+ const error = new Error(`Request failed with code ${responseData.code}`);
+ Object.assign(error, {
+ response: {
+ ...response,
+ status: responseData.code, // 将后端返回的code作为HTTP状态码
+ data: responseData,
+ },
+ });
+
+ // 对于特定的错误码,执行特殊处理
+ if (responseData.code === 401) {
+ // 登录失效的情况,触发重新认证逻辑
+ // 不要等待doReAuthenticate完成,因为它会跳转页面,不需要等待
+ doReAuthenticate().catch((error_) => {
+ console.error('doReAuthenticate error:', error_);
+ });
+ // 对于401错误,不抛出错误,而是返回一个特殊标记的响应
+ // 这样可以避免控制台显示未捕获的错误
+ return {
+ ...response,
+ __isLoginExpired: true,
+ } as any;
+ }
+
+ // 抛出错误,让后续的错误处理拦截器处理
+ throw error;
+ }
+
+ // 对于其他情况,正常返回响应
+ return response;
+ },
+ });
+
+ // 处理返回的响应数据格式
+ client.addResponseInterceptor(
+ defaultResponseInterceptor({
+ codeField: 'code',
+ dataField: 'data',
+ successCode: 0,
+ }),
+ );
+
+ // token过期的处理
+ client.addResponseInterceptor(
+ authenticateResponseInterceptor({
+ client,
+ doReAuthenticate,
+ doRefreshToken,
+ enableRefreshToken: preferences.app.enableRefreshToken,
+ formatToken,
+ }),
+ );
+
+ // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
+ client.addResponseInterceptor(
+ errorMessageResponseInterceptor((msg: string, error) => {
+ // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
+ // 当前接口返回的错误字段是 error 或者 message
+ const responseData = error?.response?.data ?? {};
+ const errorMessage = responseData?.error ?? responseData?.message ?? responseData?.msg ?? '';
+ const responseCode = error?.response?.status ?? responseData?.code;
+
+ // 如果是401错误,已经在自定义拦截器中处理了,这里不需要额外提示
+ if (responseCode === 401) {
+ return Promise.reject(error);
+ }
+
+ // 如果有后端返回的错误信息,优先显示
+ if (errorMessage) {
+ message.error(errorMessage);
+ } else if (msg) {
+ // 否则显示默认的错误信息
+ message.error(msg);
+ }
+ }),
+ );
+
+ return client;
+}
+
+export const requestClient = createRequestClient(apiURL, {
+ responseReturn: 'data',
+ timeout: 0, // 设置为0表示没有超时时间限制
+});
+
+export const baseRequestClient = new RequestClient({
+ baseURL: apiURL,
+ timeout: 0, // 设置为0表示没有超时时间限制
+});
diff --git a/apps/finance/src/app.vue b/apps/finance/src/app.vue
new file mode 100644
index 0000000..bbaccce
--- /dev/null
+++ b/apps/finance/src/app.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/finance/src/bootstrap.ts b/apps/finance/src/bootstrap.ts
new file mode 100644
index 0000000..8e0c2ff
--- /dev/null
+++ b/apps/finance/src/bootstrap.ts
@@ -0,0 +1,75 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui/es/loading';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+import '@vben/styles/antd';
+
+import { useTitle } from '@vueuse/core';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
+import App from './app.vue';
+import { router } from './router';
+
+async function bootstrap(namespace: string) {
+ // 初始化组件适配器
+ await initComponentAdapter();
+
+ // 初始化表单组件
+ await initSetupVbenForm();
+
+ // // 设置弹窗的默认配置
+ // setDefaultModalProps({
+ // fullscreenButton: false,
+ // });
+ // // 设置抽屉的默认配置
+ // setDefaultDrawerProps({
+ // zIndex: 1020,
+ // });
+
+ const app = createApp(App);
+
+ // 注册v-loading指令
+ registerLoadingDirective(app, {
+ loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+ spinning: 'spinning',
+ });
+
+ // 国际化 i18n 配置
+ await setupI18n(app);
+
+ // 配置 pinia-tore
+ await initStores(app, { namespace });
+
+ // 安装权限指令
+ registerAccessDirective(app);
+
+ // 初始化 tippy
+ const { initTippy } = await import('@vben/common-ui/es/tippy');
+ initTippy(app);
+
+ // 配置路由及路由守卫
+ app.use(router);
+
+ // 配置Motion插件
+ const { MotionPlugin } = await import('@vben/plugins/motion');
+ app.use(MotionPlugin);
+
+ // 动态更新标题
+ watchEffect(() => {
+ if (preferences.app.dynamicTitle) {
+ const routeTitle = router.currentRoute.value.meta?.title;
+ const pageTitle = (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ useTitle(pageTitle);
+ }
+ });
+
+ app.mount('#app');
+}
+
+export { bootstrap };
diff --git a/apps/finance/src/layouts/auth.vue b/apps/finance/src/layouts/auth.vue
new file mode 100644
index 0000000..0c6da02
--- /dev/null
+++ b/apps/finance/src/layouts/auth.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/apps/finance/src/layouts/basic.vue b/apps/finance/src/layouts/basic.vue
new file mode 100644
index 0000000..08c9c23
--- /dev/null
+++ b/apps/finance/src/layouts/basic.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/finance/src/layouts/index.ts b/apps/finance/src/layouts/index.ts
new file mode 100644
index 0000000..a432078
--- /dev/null
+++ b/apps/finance/src/layouts/index.ts
@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };
diff --git a/apps/finance/src/locales/README.md b/apps/finance/src/locales/README.md
new file mode 100644
index 0000000..7b45103
--- /dev/null
+++ b/apps/finance/src/locales/README.md
@@ -0,0 +1,3 @@
+# locale
+
+每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
diff --git a/apps/finance/src/locales/index.ts b/apps/finance/src/locales/index.ts
new file mode 100644
index 0000000..18a15fa
--- /dev/null
+++ b/apps/finance/src/locales/index.ts
@@ -0,0 +1,95 @@
+import type { Locale } from 'ant-design-vue/es/locale';
+
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import { ref } from 'vue';
+
+import { $t, setupI18n as coreSetup, loadLocalesMapFromDir } from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+import antdEnLocale from 'ant-design-vue/es/locale/en_US';
+import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
+import dayjs from 'dayjs';
+
+const antdLocale = ref(antdDefaultLocale);
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(/\.\/langs\/([^/]+)\/(.*)\.json$/, modules);
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+ const [appLocaleMessages] = await Promise.all([
+ localesMap[lang]?.(),
+ loadThirdPartyMessage(lang),
+ ]);
+ return appLocaleMessages?.default;
+}
+
+/**
+ * 加载第三方组件库的语言包
+ * @param lang
+ */
+async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
+ await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
+}
+
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+ let locale;
+ switch (lang) {
+ case 'en-US': {
+ locale = await import('dayjs/locale/en');
+ break;
+ }
+ case 'zh-CN': {
+ locale = await import('dayjs/locale/zh-cn');
+ break;
+ }
+ // 默认使用英语
+ default: {
+ locale = await import('dayjs/locale/en');
+ }
+ }
+ if (locale) {
+ dayjs.locale(locale);
+ } else {
+ console.error(`Failed to load dayjs locale for ${lang}`);
+ }
+}
+
+/**
+ * 加载antd的语言包
+ * @param lang
+ */
+async function loadAntdLocale(lang: SupportedLanguagesType) {
+ switch (lang) {
+ case 'en-US': {
+ antdLocale.value = antdEnLocale;
+ break;
+ }
+ case 'zh-CN': {
+ antdLocale.value = antdDefaultLocale;
+ break;
+ }
+ }
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+ await coreSetup(app, {
+ defaultLocale: preferences.app.locale,
+ loadMessages,
+ missingWarn: !import.meta.env.PROD,
+ ...options,
+ });
+}
+
+export { $t, antdLocale, setupI18n };
diff --git a/apps/finance/src/locales/langs/en-US/demos.json b/apps/finance/src/locales/langs/en-US/demos.json
new file mode 100644
index 0000000..12a3b71
--- /dev/null
+++ b/apps/finance/src/locales/langs/en-US/demos.json
@@ -0,0 +1,13 @@
+{
+ "title": "Demos",
+ "antd": "Ant Design Vue",
+ "vben": {
+ "title": "Project",
+ "about": "About",
+ "document": "Document",
+ "antdv": "Ant Design Vue Version",
+ "naive-ui": "Naive UI Version",
+ "element-plus": "Element Plus Version",
+ "tdesign": "TDesign Vue Version"
+ }
+}
diff --git a/apps/finance/src/locales/langs/en-US/page.json b/apps/finance/src/locales/langs/en-US/page.json
new file mode 100644
index 0000000..39f1641
--- /dev/null
+++ b/apps/finance/src/locales/langs/en-US/page.json
@@ -0,0 +1,15 @@
+{
+ "auth": {
+ "login": "Login",
+ "register": "Register",
+ "codeLogin": "Code Login",
+ "qrcodeLogin": "Qr Code Login",
+ "forgetPassword": "Forget Password",
+ "profile": "Profile"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "analytics": "Analytics",
+ "workspace": "Workspace"
+ }
+}
diff --git a/apps/finance/src/locales/langs/zh-CN/demos.json b/apps/finance/src/locales/langs/zh-CN/demos.json
new file mode 100644
index 0000000..b5007ea
--- /dev/null
+++ b/apps/finance/src/locales/langs/zh-CN/demos.json
@@ -0,0 +1,13 @@
+{
+ "title": "演示",
+ "antd": "Ant Design Vue",
+ "vben": {
+ "title": "项目",
+ "about": "关于",
+ "document": "文档",
+ "antdv": "Ant Design Vue 版本",
+ "naive-ui": "Naive UI 版本",
+ "element-plus": "Element Plus 版本",
+ "tdesign": "TDesign Vue 版本"
+ }
+}
diff --git a/apps/finance/src/locales/langs/zh-CN/page.json b/apps/finance/src/locales/langs/zh-CN/page.json
new file mode 100644
index 0000000..2192d1d
--- /dev/null
+++ b/apps/finance/src/locales/langs/zh-CN/page.json
@@ -0,0 +1,15 @@
+{
+ "auth": {
+ "login": "登录",
+ "register": "注册",
+ "codeLogin": "验证码登录",
+ "qrcodeLogin": "二维码登录",
+ "forgetPassword": "忘记密码",
+ "profile": "个人中心"
+ },
+ "dashboard": {
+ "title": "概览",
+ "analytics": "分析页",
+ "workspace": "工作台"
+ }
+}
diff --git a/apps/finance/src/main.ts b/apps/finance/src/main.ts
new file mode 100644
index 0000000..5d728a0
--- /dev/null
+++ b/apps/finance/src/main.ts
@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+ // name用于指定项目唯一标识
+ // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+ const env = import.meta.env.PROD ? 'prod' : 'dev';
+ const appVersion = import.meta.env.VITE_APP_VERSION;
+ const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+ // app偏好设置初始化
+ await initPreferences({
+ namespace,
+ overrides: overridesPreferences,
+ });
+
+ // 启动应用并挂载
+ // vue应用主要逻辑及视图
+ const { bootstrap } = await import('./bootstrap');
+ await bootstrap(namespace);
+
+ // 移除并销毁loading
+ unmountGlobalLoading();
+}
+
+initApplication();
diff --git a/apps/finance/src/preferences.ts b/apps/finance/src/preferences.ts
new file mode 100644
index 0000000..6a67dc9
--- /dev/null
+++ b/apps/finance/src/preferences.ts
@@ -0,0 +1,20 @@
+import { defineOverridesPreferences } from '@vben/preferences';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+ // overrides
+ app: {
+ enableCheckUpdates: false,
+ name: import.meta.env.VITE_APP_TITLE,
+ },
+ widget: {
+ languageToggle: false,
+ notification: false,
+ timezone: false,
+ showCopyPreferences: false,
+ },
+});
diff --git a/apps/finance/src/router/access.ts b/apps/finance/src/router/access.ts
new file mode 100644
index 0000000..3a48be2
--- /dev/null
+++ b/apps/finance/src/router/access.ts
@@ -0,0 +1,42 @@
+import type {
+ ComponentRecordType,
+ GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { message } from 'ant-design-vue';
+
+import { getAllMenusApi } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+ const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+ const layoutMap: ComponentRecordType = {
+ BasicLayout,
+ IFrameView,
+ };
+
+ return await generateAccessible(preferences.app.accessMode, {
+ ...options,
+ fetchMenuListAsync: async () => {
+ message.loading({
+ content: `${$t('common.loadingMenu')}...`,
+ duration: 1.5,
+ });
+ return await getAllMenusApi();
+ },
+ // 可以指定没有权限跳转403页面
+ forbiddenComponent,
+ // 如果 route.meta.menuVisibleWithForbidden = true
+ layoutMap,
+ pageMap,
+ });
+}
+
+export { generateAccess };
diff --git a/apps/finance/src/router/guard.ts b/apps/finance/src/router/guard.ts
new file mode 100644
index 0000000..a1ad6d8
--- /dev/null
+++ b/apps/finance/src/router/guard.ts
@@ -0,0 +1,133 @@
+import type { Router } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+ // 记录已经加载的页面
+ const loadedPaths = new Set();
+
+ router.beforeEach((to) => {
+ to.meta.loaded = loadedPaths.has(to.path);
+
+ // 页面加载进度条
+ if (!to.meta.loaded && preferences.transition.progress) {
+ startProgress();
+ }
+ return true;
+ });
+
+ router.afterEach((to) => {
+ // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+ loadedPaths.add(to.path);
+
+ // 关闭页面加载进度条
+ if (preferences.transition.progress) {
+ stopProgress();
+ }
+ });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+ router.beforeEach(async (to, from) => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const authStore = useAuthStore();
+
+ // 基本路由,这些路由不需要进入权限拦截
+ if (coreRouteNames.includes(to.name as string)) {
+ if (to.path === LOGIN_PATH && accessStore.accessToken) {
+ return decodeURIComponent(
+ (to.query?.redirect as string) ||
+ userStore.userInfo?.homePath ||
+ preferences.app.defaultHomePath,
+ );
+ }
+ return true;
+ }
+
+ // accessToken 检查
+ if (!accessStore.accessToken) {
+ // 明确声明忽略权限访问权限,则可以访问
+ if (to.meta.ignoreAccess) {
+ return true;
+ }
+
+ // 没有访问权限,跳转登录页面
+ if (to.fullPath !== LOGIN_PATH) {
+ return {
+ path: LOGIN_PATH,
+ // 如不需要,直接删除 query
+ query:
+ to.fullPath === preferences.app.defaultHomePath
+ ? {}
+ : { redirect: encodeURIComponent(to.fullPath) },
+ // 携带当前跳转的页面,登录后重新跳转该页面
+ replace: true,
+ };
+ }
+ return to;
+ }
+
+ // 是否已经生成过动态路由
+ if (accessStore.isAccessChecked) {
+ return true;
+ }
+
+ // 生成路由表
+ // 当前登录用户拥有的角色标识列表
+ const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
+ const userRoles = userInfo.roles ?? [];
+
+ // 生成菜单和路由
+ const { accessibleMenus, accessibleRoutes } = await generateAccess({
+ roles: userRoles,
+ router,
+ // 则会在菜单中显示,但是访问会被重定向到403
+ routes: accessRoutes,
+ });
+
+ // 保存菜单信息和路由信息
+ accessStore.setAccessMenus(accessibleMenus);
+ accessStore.setAccessRoutes(accessibleRoutes);
+ accessStore.setIsAccessChecked(true);
+ const redirectPath = (from.query.redirect ??
+ (to.path === preferences.app.defaultHomePath
+ ? userInfo.homePath || preferences.app.defaultHomePath
+ : to.fullPath)) as string;
+
+ return {
+ ...router.resolve(decodeURIComponent(redirectPath)),
+ replace: true,
+ };
+ });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+ /** 通用 */
+ setupCommonGuard(router);
+ /** 权限访问 */
+ setupAccessGuard(router);
+}
+
+export { createRouterGuard };
diff --git a/apps/finance/src/router/index.ts b/apps/finance/src/router/index.ts
new file mode 100644
index 0000000..359eeb5
--- /dev/null
+++ b/apps/finance/src/router/index.ts
@@ -0,0 +1,33 @@
+import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ * @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+ history:
+ import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+ ? createWebHashHistory(import.meta.env.VITE_BASE)
+ : createWebHistory(import.meta.env.VITE_BASE),
+ // 应该添加到路由的初始路由列表。
+ routes,
+ scrollBehavior: (to, _from, savedPosition) => {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+ },
+ // 是否应该禁止尾部斜杠。
+ // strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };
diff --git a/apps/finance/src/router/routes/core.ts b/apps/finance/src/router/routes/core.ts
new file mode 100644
index 0000000..0bcef92
--- /dev/null
+++ b/apps/finance/src/router/routes/core.ts
@@ -0,0 +1,95 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+ component: () => import('#/views/_core/fallback/not-found.vue'),
+ meta: {
+ hideInBreadcrumb: true,
+ hideInMenu: true,
+ hideInTab: true,
+ title: '404',
+ },
+ name: 'FallbackNotFound',
+ path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+ /**
+ * 根路由
+ * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+ * 此路由必须存在,且不应修改
+ */
+ {
+ component: BasicLayout,
+ meta: {
+ hideInBreadcrumb: true,
+ title: 'Root',
+ },
+ name: 'Root',
+ path: '/',
+ redirect: preferences.app.defaultHomePath,
+ children: [],
+ },
+ {
+ component: AuthPageLayout,
+ meta: {
+ hideInTab: true,
+ title: 'Authentication',
+ },
+ name: 'Authentication',
+ path: '/auth',
+ redirect: LOGIN_PATH,
+ children: [
+ {
+ name: 'Login',
+ path: 'login',
+ component: () => import('#/views/_core/authentication/login.vue'),
+ meta: {
+ title: $t('page.auth.login'),
+ },
+ },
+ {
+ name: 'CodeLogin',
+ path: 'code-login',
+ component: () => import('#/views/_core/authentication/code-login.vue'),
+ meta: {
+ title: $t('page.auth.codeLogin'),
+ },
+ },
+ {
+ name: 'QrCodeLogin',
+ path: 'qrcode-login',
+ component: () => import('#/views/_core/authentication/qrcode-login.vue'),
+ meta: {
+ title: $t('page.auth.qrcodeLogin'),
+ },
+ },
+ {
+ name: 'ForgetPassword',
+ path: 'forget-password',
+ component: () => import('#/views/_core/authentication/forget-password.vue'),
+ meta: {
+ title: $t('page.auth.forgetPassword'),
+ },
+ },
+ {
+ name: 'Register',
+ path: 'register',
+ component: () => import('#/views/_core/authentication/register.vue'),
+ meta: {
+ title: $t('page.auth.register'),
+ },
+ },
+ ],
+ },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };
diff --git a/apps/finance/src/router/routes/index.ts b/apps/finance/src/router/routes/index.ts
new file mode 100644
index 0000000..ba7bf72
--- /dev/null
+++ b/apps/finance/src/router/routes/index.ts
@@ -0,0 +1,33 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+ eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
+// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+const staticRoutes: RouteRecordRaw[] = [];
+const externalRoutes: RouteRecordRaw[] = [];
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ * 无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [...coreRoutes, ...externalRoutes, fallbackNotFoundRoute];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };
diff --git a/apps/finance/src/router/routes/modules/dashboard.ts b/apps/finance/src/router/routes/modules/dashboard.ts
new file mode 100644
index 0000000..c1385ff
--- /dev/null
+++ b/apps/finance/src/router/routes/modules/dashboard.ts
@@ -0,0 +1,18 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { $t } from '#/locales';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'lucide:layout-dashboard',
+ order: -1,
+ title: $t('page.dashboard.title'),
+ },
+ name: 'Dashboard',
+ path: '/dashboard',
+ component: () => import('#/views/dashboard/analytics/index.vue'),
+ },
+];
+
+export default routes;
diff --git a/apps/finance/src/router/routes/modules/system.ts b/apps/finance/src/router/routes/modules/system.ts
new file mode 100644
index 0000000..801fcf3
--- /dev/null
+++ b/apps/finance/src/router/routes/modules/system.ts
@@ -0,0 +1,34 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+const routes: RouteRecordRaw[] = [
+ {
+ meta: {
+ icon: 'ic:baseline-view-in-ar',
+ keepAlive: true,
+ order: 1000,
+ title: '系统管理',
+ },
+ name: 'System',
+ path: '/system',
+ children: [
+ {
+ meta: {
+ title: '用户管理',
+ },
+ name: 'Users',
+ path: '/system/users',
+ component: () => import('#/views/permission/users/index.vue'),
+ },
+ // {
+ // meta: {
+ // title: '用户管理2',
+ // },
+ // name: 'Users2',
+ // path: '/system/users2',
+ // component: () => import('#/components/CardListDemo.vue'),
+ // },
+ ],
+ },
+];
+
+export default routes;
diff --git a/apps/finance/src/store/auth.ts b/apps/finance/src/store/auth.ts
new file mode 100644
index 0000000..daac071
--- /dev/null
+++ b/apps/finance/src/store/auth.ts
@@ -0,0 +1,112 @@
+import type { Recordable, UserInfo } from '@vben/types';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { notification } from 'ant-design-vue';
+import { defineStore } from 'pinia';
+
+import { loginApi, logoutApi } from '#/api';
+import { $t } from '#/locales';
+
+export const useAuthStore = defineStore('auth', () => {
+ const accessStore = useAccessStore();
+ const userStore = useUserStore();
+ const router = useRouter();
+
+ const loginLoading = ref(false);
+
+ /**
+ * 异步处理登录操作
+ * Asynchronously handle the login process
+ * @param params 登录表单数据
+ */
+ async function authLogin(params: Recordable, onSuccess?: () => Promise | void) {
+ // 异步处理用户登录操作并获取 accessToken
+ let userInfo: null | UserInfo = null;
+ try {
+ loginLoading.value = true;
+ const { userToken, userEntity } = await loginApi(params);
+
+ // 如果成功获取到 accessToken
+ if (userToken) {
+ accessStore.setAccessToken(userToken.token);
+
+ // 获取用户信息并存储到 accessStore 中
+ // const [fetchUserInfoResult, accessCodes] = await Promise.all([
+ // fetchUserInfo(),
+ // getAccessCodesApi(),
+ // ]);
+
+ userInfo = userEntity;
+
+ userStore.setUserInfo(userInfo);
+ // accessStore.setAccessCodes(accessCodes);
+
+ if (accessStore.loginExpired) {
+ accessStore.setLoginExpired(false);
+ } else {
+ onSuccess
+ ? await onSuccess?.()
+ : await router.push(userInfo?.homePath || preferences.app.defaultHomePath);
+ }
+
+ if (userInfo?.name) {
+ notification.success({
+ description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.name}`,
+ duration: 3,
+ message: $t('authentication.loginSuccess'),
+ });
+ }
+ }
+ } finally {
+ loginLoading.value = false;
+ }
+
+ return {
+ userInfo,
+ };
+ }
+
+ async function logout(redirect: boolean = true) {
+ try {
+ await logoutApi();
+ } catch {
+ // 不做任何处理
+ }
+ resetAllStores();
+ accessStore.setLoginExpired(false);
+
+ // 回登录页带上当前路由地址
+ await router.replace({
+ path: LOGIN_PATH,
+ query: redirect
+ ? {
+ redirect: encodeURIComponent(router.currentRoute.value.fullPath),
+ }
+ : {},
+ });
+ }
+
+ async function fetchUserInfo() {
+ let userInfo: null | UserInfo = null;
+ userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
+ return userInfo;
+ }
+
+ function $reset() {
+ loginLoading.value = false;
+ }
+
+ return {
+ $reset,
+ authLogin,
+ fetchUserInfo,
+ loginLoading,
+ logout,
+ };
+});
diff --git a/apps/finance/src/store/index.ts b/apps/finance/src/store/index.ts
new file mode 100644
index 0000000..de566a1
--- /dev/null
+++ b/apps/finance/src/store/index.ts
@@ -0,0 +1,2 @@
+export * from './auth';
+export * from './sys';
diff --git a/apps/finance/src/store/sys.ts b/apps/finance/src/store/sys.ts
new file mode 100644
index 0000000..69606be
--- /dev/null
+++ b/apps/finance/src/store/sys.ts
@@ -0,0 +1,92 @@
+import { defineStore } from 'pinia';
+
+export const useSysStore = defineStore('sys', () => {
+ const staticDictMap = {
+ payment: {
+ '0': '微信',
+ '1': '支付宝',
+ '2': '银行',
+ },
+ checkoff: {
+ '0': '未对账',
+ '1': '对账失败',
+ '2': '对账成功',
+ },
+ } as const;
+
+ // 自动推断字典类型
+ type DictType = keyof typeof staticDictMap;
+
+ const getDictMap = (dictType: DictType, dictValue: number | string) => {
+ const dict = staticDictMap[dictType];
+ const key = String(dictValue);
+
+ // 使用 Record 类型来避免索引签名问题
+ const record = dict as Record;
+ return record[key] ?? '';
+ };
+
+ // 定义字典项类型
+ type DictItem = {
+ color?: string;
+ label: string;
+ value: string;
+ };
+ const staticDictList: Record> = {
+ payment: [
+ {
+ label: '微信',
+ value: '0',
+ },
+ {
+ label: '支付宝',
+ value: '1',
+ },
+ {
+ label: '银行',
+ value: '2',
+ },
+ ],
+ checkoff: [
+ {
+ label: '未对账',
+ value: '0',
+ color: 'warning',
+ },
+ {
+ label: '对账失败',
+ value: '1',
+ color: 'error',
+ },
+ {
+ label: '对账成功',
+ value: '2',
+ color: 'success',
+ },
+ ],
+ } as const;
+
+ // 自动推断字典列表类型
+ type DictListType = keyof typeof staticDictList;
+
+ const getDictList = (dictType: DictListType) => {
+ return staticDictList[dictType] ?? [];
+ };
+
+ const getDictByList = (dictType: DictListType, dictValue: number | string) => {
+ const dictList = getDictList(dictType);
+ const key = String(dictValue);
+
+ // 确保返回的对象始终包含 color 属性
+ return dictList.find((item: DictItem) => item.value === key) ?? { label: '', value: '' };
+ };
+
+ return {
+ staticDictMap,
+ staticDictList,
+ getDictMap,
+ getDictList,
+ getDictByList,
+ $reset: () => {}, // sys store 只包含静态数据,不需要重置
+ };
+});
diff --git a/apps/finance/src/views/_core/README.md b/apps/finance/src/views/_core/README.md
new file mode 100644
index 0000000..8248afe
--- /dev/null
+++ b/apps/finance/src/views/_core/README.md
@@ -0,0 +1,3 @@
+# \_core
+
+此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
diff --git a/apps/finance/src/views/_core/authentication/code-login.vue b/apps/finance/src/views/_core/authentication/code-login.vue
new file mode 100644
index 0000000..acfd1fd
--- /dev/null
+++ b/apps/finance/src/views/_core/authentication/code-login.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/authentication/forget-password.vue b/apps/finance/src/views/_core/authentication/forget-password.vue
new file mode 100644
index 0000000..fef0d42
--- /dev/null
+++ b/apps/finance/src/views/_core/authentication/forget-password.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/authentication/login.vue b/apps/finance/src/views/_core/authentication/login.vue
new file mode 100644
index 0000000..cc026dd
--- /dev/null
+++ b/apps/finance/src/views/_core/authentication/login.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/authentication/qrcode-login.vue b/apps/finance/src/views/_core/authentication/qrcode-login.vue
new file mode 100644
index 0000000..23f5f2d
--- /dev/null
+++ b/apps/finance/src/views/_core/authentication/qrcode-login.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/authentication/register.vue b/apps/finance/src/views/_core/authentication/register.vue
new file mode 100644
index 0000000..b1a5de7
--- /dev/null
+++ b/apps/finance/src/views/_core/authentication/register.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/fallback/coming-soon.vue b/apps/finance/src/views/_core/fallback/coming-soon.vue
new file mode 100644
index 0000000..f394930
--- /dev/null
+++ b/apps/finance/src/views/_core/fallback/coming-soon.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/fallback/forbidden.vue b/apps/finance/src/views/_core/fallback/forbidden.vue
new file mode 100644
index 0000000..8ea65fe
--- /dev/null
+++ b/apps/finance/src/views/_core/fallback/forbidden.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/fallback/internal-error.vue b/apps/finance/src/views/_core/fallback/internal-error.vue
new file mode 100644
index 0000000..819a47d
--- /dev/null
+++ b/apps/finance/src/views/_core/fallback/internal-error.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/fallback/not-found.vue b/apps/finance/src/views/_core/fallback/not-found.vue
new file mode 100644
index 0000000..4d178e9
--- /dev/null
+++ b/apps/finance/src/views/_core/fallback/not-found.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/fallback/offline.vue b/apps/finance/src/views/_core/fallback/offline.vue
new file mode 100644
index 0000000..5de4a88
--- /dev/null
+++ b/apps/finance/src/views/_core/fallback/offline.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/profile/base-setting.vue b/apps/finance/src/views/_core/profile/base-setting.vue
new file mode 100644
index 0000000..aa8a4c2
--- /dev/null
+++ b/apps/finance/src/views/_core/profile/base-setting.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/apps/finance/src/views/_core/profile/index.vue b/apps/finance/src/views/_core/profile/index.vue
new file mode 100644
index 0000000..8740894
--- /dev/null
+++ b/apps/finance/src/views/_core/profile/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/finance/src/views/_core/profile/notification-setting.vue b/apps/finance/src/views/_core/profile/notification-setting.vue
new file mode 100644
index 0000000..324a4b3
--- /dev/null
+++ b/apps/finance/src/views/_core/profile/notification-setting.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/apps/finance/src/views/_core/profile/password-setting.vue b/apps/finance/src/views/_core/profile/password-setting.vue
new file mode 100644
index 0000000..b246bc3
--- /dev/null
+++ b/apps/finance/src/views/_core/profile/password-setting.vue
@@ -0,0 +1,66 @@
+
+
+
+
diff --git a/apps/finance/src/views/_core/profile/security-setting.vue b/apps/finance/src/views/_core/profile/security-setting.vue
new file mode 100644
index 0000000..be30db5
--- /dev/null
+++ b/apps/finance/src/views/_core/profile/security-setting.vue
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/analytics/analytics-trends.vue b/apps/finance/src/views/dashboard/analytics/analytics-trends.vue
new file mode 100644
index 0000000..f1f0b23
--- /dev/null
+++ b/apps/finance/src/views/dashboard/analytics/analytics-trends.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/analytics/analytics-visits-data.vue b/apps/finance/src/views/dashboard/analytics/analytics-visits-data.vue
new file mode 100644
index 0000000..190fb41
--- /dev/null
+++ b/apps/finance/src/views/dashboard/analytics/analytics-visits-data.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/finance/src/views/dashboard/analytics/analytics-visits-sales.vue
new file mode 100644
index 0000000..6ff5208
--- /dev/null
+++ b/apps/finance/src/views/dashboard/analytics/analytics-visits-sales.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/analytics/analytics-visits-source.vue b/apps/finance/src/views/dashboard/analytics/analytics-visits-source.vue
new file mode 100644
index 0000000..0915c7a
--- /dev/null
+++ b/apps/finance/src/views/dashboard/analytics/analytics-visits-source.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/analytics/analytics-visits.vue b/apps/finance/src/views/dashboard/analytics/analytics-visits.vue
new file mode 100644
index 0000000..7e0f101
--- /dev/null
+++ b/apps/finance/src/views/dashboard/analytics/analytics-visits.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/analytics/index.vue b/apps/finance/src/views/dashboard/analytics/index.vue
new file mode 100644
index 0000000..5e3d6d2
--- /dev/null
+++ b/apps/finance/src/views/dashboard/analytics/index.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/apps/finance/src/views/dashboard/workspace/index.vue b/apps/finance/src/views/dashboard/workspace/index.vue
new file mode 100644
index 0000000..b95d613
--- /dev/null
+++ b/apps/finance/src/views/dashboard/workspace/index.vue
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+ 早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
+
+ 今日晴,20℃ - 32℃!
+
+
+
+
+
diff --git a/apps/finance/src/views/permission/users/index.vue b/apps/finance/src/views/permission/users/index.vue
new file mode 100644
index 0000000..9fe63ad
--- /dev/null
+++ b/apps/finance/src/views/permission/users/index.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/finance/src/views/permission/users/modules/form.vue b/apps/finance/src/views/permission/users/modules/form.vue
new file mode 100644
index 0000000..8d8ac1b
--- /dev/null
+++ b/apps/finance/src/views/permission/users/modules/form.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
diff --git a/apps/finance/tailwind.config.mjs b/apps/finance/tailwind.config.mjs
new file mode 100644
index 0000000..f17f556
--- /dev/null
+++ b/apps/finance/tailwind.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';
diff --git a/apps/finance/tsconfig.json b/apps/finance/tsconfig.json
new file mode 100644
index 0000000..02c287f
--- /dev/null
+++ b/apps/finance/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web-app.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "#/*": ["./src/*"]
+ }
+ },
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/apps/finance/tsconfig.node.json b/apps/finance/tsconfig.node.json
new file mode 100644
index 0000000..c2f0d86
--- /dev/null
+++ b/apps/finance/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/node.json",
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "noEmit": false
+ },
+ "include": ["vite.config.mts"]
+}
diff --git a/apps/finance/vite.config.mts b/apps/finance/vite.config.mts
new file mode 100644
index 0000000..b6360f1
--- /dev/null
+++ b/apps/finance/vite.config.mts
@@ -0,0 +1,20 @@
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+ return {
+ application: {},
+ vite: {
+ server: {
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ // mock代理目标地址
+ target: 'http://localhost:5320/api',
+ ws: true,
+ },
+ },
+ },
+ },
+ };
+});