🆕 渲染icon、完善动态路由和资源、封装request

This commit is contained in:
乾乾
2025-11-14 19:12:57 +08:00
committed by Dawn
parent f6edb09a20
commit cf6b949050
32 changed files with 3135 additions and 3110 deletions

View File

@@ -1,10 +1,17 @@
# 后端服务地址
VITE_SERVICE_URL="http://localhost:9095/api/manage"
# 项目标题
VITE_APP_TITLE="HuLa—校园平台"
# 标签后缀
VITE_TITLE_SUFFIX=" | HuLa"
# 项目备案号
VITE_APP_ICP="桂ICP备2021000000号"
# 项目名称
VITE_APP_NAME="HuLa-vue3"
# 生产环境配置
# 应用标题
VITE_APP_TITLE=HuLa Admin
# Gateway 地址
VITE_API_BASE_URL=https://hulaspark.com
# 是否启用代理Y/N
VITE_HTTP_PROXY=N
# Authorization 密钥
VITE_SECRET_KEY=luohuo_web_pro:luohuo_web_pro_secret
# 应用环境
VITE_APP_ENV=production

View File

@@ -1,7 +1,7 @@
{
"name": "hula-vue3",
"private": true,
"version": "v1.1.5-beta",
"version": "v1.2.0-beta",
"packageManager": "pnpm@8.14.1",
"type": "module",
"engines": {
@@ -41,82 +41,82 @@
"dependencies": {
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@intlify/unplugin-vue-i18n": "^0.11.0",
"@types/crypto-js": "^4.2.1",
"@types/crypto-js": "^4.2.2",
"@vicons/ionicons5": "^0.13.0",
"@vicons/tabler": "^0.12.0",
"@vueuse/core": "^10.7.0",
"@vueuse/core": "^10.11.1",
"animate.css": "^4.1.1",
"axios": "^1.6.2",
"axios": "^1.13.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"dayjs": "^1.11.19",
"echarts": "5.4.1",
"encryptlong": "^3.1.4",
"hotkeys-js": "^3.13.1",
"jsencrypt": "^3.3.2",
"hotkeys-js": "^3.13.15",
"jsencrypt": "^3.5.4",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"naive-ui": "^2.37.3",
"notiflix": "^3.2.6",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"naive-ui": "^2.43.1",
"notiflix": "^3.2.8",
"pinia": "^2.3.1",
"pinia-plugin-persistedstate": "^3.2.3",
"screenfull": "^6.0.2",
"vue": "^3.4.15",
"vue": "^3.5.24",
"vue-drag-resize": "^1.5.4",
"vue-i18n": "^9.8.0",
"vue-router": "^4.2.5",
"vue-i18n": "^9.14.5",
"vue-router": "^4.6.3",
"vue3-count-to": "^1.1.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.7.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.23.3",
"@babel/eslint-parser": "^7.28.5",
"@rollup/plugin-terser": "^0.4.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@vitejs/plugin-vue": "^5.0.0",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@types/node": "^20.19.25",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"@vitest/ui": "^0.32.4",
"@vue/test-utils": "^2.4.3",
"autoprefixer": "^10.4.16",
"commitizen": "^4.3.0",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.22",
"commitizen": "^4.3.1",
"conventional-changelog": "^5.1.0",
"conventional-changelog-cli": "^4.1.0",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.0",
"cz-customizable": "^7.5.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.19.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^9.33.0",
"lint-staged": "^14.0.1",
"only-allow": "^1.2.1",
"oxlint": "^0.2.4",
"prettier": "^3.1.1",
"rollup-plugin-visualizer": "^5.11.0",
"sass": "^1.69.5",
"sass-loader": "^14.0.0",
"oxlint": "^0.2.18",
"prettier": "^3.6.2",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.94.0",
"sass-loader": "^14.2.1",
"stylelint": "^15.11.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.4.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-recommended-scss": "^12.0.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-order": "^6.0.4",
"stylelint-prettier": "^3.0.0",
"stylelint-scss": "^5.3.1",
"terser": "^5.26.0",
"typescript": "5.3.3",
"unplugin-auto-import": "^0.17.2",
"stylelint-scss": "^5.3.2",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.26.0",
"vite": "5.0.10",
"vite": "7.2.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.0.11",
"vite-plugin-vue-devtools": "^7.7.8",
"vitest": "^0.32.4",
"vue-tsc": "^1.8.27"
},

5535
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ const Content = defineComponent({
</script>
<style lang="scss">
@import '@/styles/scss/global-app';
@use '@/styles/scss/global-app';
#app {
background-color: v-bind(LOGIN_BGC);
}

View File

@@ -1,13 +1,13 @@
import { request } from '@/utils/http'
import { RequestModule } from '@/enums/request'
import type { LoginParams, LoginResponse, CaptchaResponse, UserInfo, ApiResponse } from '@/types/api'
import type { LoginParams, LoginResponse, CaptchaResponse, UserInfo } from '@/types/api'
/**
* 登录
* @param data 登录参数
*/
export function loginApi(data: LoginParams): Promise<ApiResponse<LoginResponse>> {
return request({
export function loginApi(data: LoginParams): Promise<LoginResponse> {
return request<LoginResponse>({
url: '/anyTenant/login',
method: 'post',
data,
@@ -19,8 +19,8 @@ export function loginApi(data: LoginParams): Promise<ApiResponse<LoginResponse>>
/**
* 获取验证码
*/
export function getCaptchaApi(): Promise<ApiResponse<CaptchaResponse>> {
return request({
export function getCaptchaApi(): Promise<CaptchaResponse> {
return request<CaptchaResponse>({
url: '/anyTenant/captcha',
method: 'get',
module: RequestModule.OAUTH,
@@ -32,8 +32,8 @@ export function getCaptchaApi(): Promise<ApiResponse<CaptchaResponse>> {
* 获取用户信息
* 后端从 token 中获取当前登录用户的 userId
*/
export function getUserInfoApi(): Promise<ApiResponse<UserInfo>> {
return request({
export function getUserInfoApi(): Promise<UserInfo> {
return request<UserInfo>({
url: '/anyone/getUserInfo',
method: 'get',
module: RequestModule.OAUTH
@@ -44,8 +44,8 @@ export function getUserInfoApi(): Promise<ApiResponse<UserInfo>> {
* 刷新 Token
* @param refreshToken 刷新令牌
*/
export function refreshTokenApi(refreshToken: string): Promise<ApiResponse<LoginResponse>> {
return request({
export function refreshTokenApi(refreshToken: string): Promise<LoginResponse> {
return request<LoginResponse>({
url: '/anyTenant/login',
method: 'post',
data: {
@@ -61,8 +61,8 @@ export function refreshTokenApi(refreshToken: string): Promise<ApiResponse<Login
* 退出登录
* @param data 退出参数
*/
export function logoutApi(data: { token: string; refreshToken?: string }): Promise<ApiResponse> {
return request({
export function logoutApi(data: { token: string; refreshToken?: string }): Promise<void> {
return request<void>({
url: '/anyUser/logout',
method: 'post',
data,
@@ -78,8 +78,8 @@ export function updatePasswordApi(data: {
oldPassword: string
newPassword: string
confirmPassword: string
}): Promise<ApiResponse> {
return request({
}): Promise<void> {
return request<void>({
url: '/anyUser/updatePassword',
method: 'post',
data,
@@ -94,8 +94,8 @@ export function updatePasswordApi(data: {
export function switchTenantAndOrgApi(data: {
orgId?: string
clientId: string
}): Promise<ApiResponse<LoginResponse>> {
return request({
}): Promise<LoginResponse> {
return request<LoginResponse>({
url: '/anyone/switchTenantAndOrg',
method: 'put',
params: data,

View File

@@ -1,13 +1,13 @@
import { request } from '@/utils/http'
import { RequestModule } from '@/enums/request'
import type { RouteItem, UserRoutesResponse, ResourceTreeNode, ApiResponse } from '@/types/api'
import type { RouteItem, UserRoutesResponse, ResourceTreeNode } from '@/types/api'
/**
* 获取公共路由
* @param applicationId 应用 ID
*/
export function getConstantRoutesApi(applicationId: number = 1): Promise<ApiResponse<RouteItem[]>> {
return request({
export function getConstantRoutesApi(applicationId: number = 1): Promise<RouteItem[]> {
return request<RouteItem[]>({
url: '/anyTenant/menu/initRoute',
method: 'get',
params: { applicationId },
@@ -20,8 +20,8 @@ export function getConstantRoutesApi(applicationId: number = 1): Promise<ApiResp
* 获取用户路由(需要登录)
* @param applicationId 应用 ID
*/
export function getUserRoutesApi(applicationId: number = 1): Promise<ApiResponse<UserRoutesResponse>> {
return request({
export function getUserRoutesApi(applicationId: number = 1): Promise<UserRoutesResponse> {
return request<UserRoutesResponse>({
url: '/anyone/visible/resource',
method: 'get',
params: { applicationId },
@@ -33,8 +33,8 @@ export function getUserRoutesApi(applicationId: number = 1): Promise<ApiResponse
* 检查路由是否存在
* @param routeName 路由名称
*/
export function checkRouteExistApi(routeName: string): Promise<ApiResponse<boolean>> {
return request({
export function checkRouteExistApi(routeName: string): Promise<boolean> {
return request<boolean>({
url: '/defResource/isRouteExist',
method: 'get',
params: { routeName },
@@ -46,8 +46,8 @@ export function checkRouteExistApi(routeName: string): Promise<ApiResponse<boole
* 获取资源树(用于菜单管理)
* @param applicationId 应用 ID
*/
export function getResourceTreeApi(applicationId: number = 1): Promise<ApiResponse<ResourceTreeNode[]>> {
return request({
export function getResourceTreeApi(applicationId: number = 1): Promise<ResourceTreeNode[]> {
return request<ResourceTreeNode[]>({
url: '/defResource/tree',
method: 'post',
params: { applicationId },
@@ -59,8 +59,8 @@ export function getResourceTreeApi(applicationId: number = 1): Promise<ApiRespon
* 获取可见资源(菜单 + 视图)
* @param applicationId 应用 ID
*/
export function getVisibleResourceApi(applicationId: number = 1): Promise<ApiResponse<RouteItem[]>> {
return request({
export function getVisibleResourceApi(applicationId: number = 1): Promise<RouteItem[]> {
return request<RouteItem[]>({
url: '/defResource/visible',
method: 'get',
params: { applicationId },
@@ -72,8 +72,8 @@ export function getVisibleResourceApi(applicationId: number = 1): Promise<ApiRes
* 新增资源
* @param data 资源数据
*/
export function addResourceApi(data: Partial<ResourceTreeNode>): Promise<ApiResponse> {
return request({
export function addResourceApi(data: Partial<ResourceTreeNode>): Promise<void> {
return request<void>({
url: '/defResource/add',
method: 'post',
data,
@@ -85,8 +85,8 @@ export function addResourceApi(data: Partial<ResourceTreeNode>): Promise<ApiResp
* 编辑资源
* @param data 资源数据
*/
export function editResourceApi(data: Partial<ResourceTreeNode>): Promise<ApiResponse> {
return request({
export function editResourceApi(data: Partial<ResourceTreeNode>): Promise<void> {
return request<void>({
url: '/defResource/edit',
method: 'post',
data,
@@ -98,8 +98,8 @@ export function editResourceApi(data: Partial<ResourceTreeNode>): Promise<ApiRes
* 删除资源
* @param data 删除参数
*/
export function deleteResourceApi(data: { id?: string; ids?: string[] }): Promise<ApiResponse> {
return request({
export function deleteResourceApi(data: { id?: string; ids?: string[] }): Promise<void> {
return request<void>({
url: '/defResource/del',
method: 'post',
data,
@@ -111,8 +111,8 @@ export function deleteResourceApi(data: { id?: string; ids?: string[] }): Promis
* 获取资源详情
* @param id 资源 ID
*/
export function getResourceDetailApi(id: string): Promise<ApiResponse<ResourceTreeNode>> {
return request({
export function getResourceDetailApi(id: string): Promise<ResourceTreeNode> {
return request<ResourceTreeNode>({
url: '/defResource/detail',
method: 'get',
params: { id },

View File

@@ -1,13 +1,13 @@
import { request } from '@/utils/http'
import { RequestModule } from '@/enums/request'
import type { TenantInfo, ApiResponse } from '@/types/api'
import type { TenantInfo } from '@/types/api'
/**
* 获取所有正常状态的租户列表
* 需要登录后调用
*/
export function getTenantListApi(): Promise<ApiResponse<TenantInfo[]>> {
return request({
export function getTenantListApi(): Promise<TenantInfo[]> {
return request<TenantInfo[]>({
url: '/defTenant/all',
method: 'get',
module: RequestModule.BASE
@@ -19,8 +19,8 @@ export function getTenantListApi(): Promise<ApiResponse<TenantInfo[]>> {
* 需要登录后调用
* 后端会从 token 中解析 userId无需前端传递
*/
export function getUserTenantListApi(): Promise<ApiResponse<TenantInfo[]>> {
return request({
export function getUserTenantListApi(): Promise<TenantInfo[]> {
return request<TenantInfo[]>({
url: `/defTenant/listTenantByUserId`,
method: 'get',
module: RequestModule.BASE
@@ -31,8 +31,8 @@ export function getUserTenantListApi(): Promise<ApiResponse<TenantInfo[]>> {
* 检测租户编码是否存在
* @param code 租户编码
*/
export function checkTenantCodeApi(code: string): Promise<ApiResponse<boolean>> {
return request({
export function checkTenantCodeApi(code: string): Promise<boolean> {
return request<boolean>({
url: `/defTenant/check/${code}`,
method: 'get',
module: RequestModule.BASE
@@ -43,8 +43,8 @@ export function checkTenantCodeApi(code: string): Promise<ApiResponse<boolean>>
* 获取租户详情
* @param id 租户 ID
*/
export function getTenantDetailApi(id: string): Promise<ApiResponse<TenantInfo>> {
return request({
export function getTenantDetailApi(id: string): Promise<TenantInfo> {
return request<TenantInfo>({
url: `/defTenant/detail`,
method: 'get',
params: { id },

View File

@@ -1,13 +1,15 @@
import { request } from '@/utils/http'
import { RequestModule } from '@/enums/request'
import type { UserInfo, PageParams, PageResponse, ApiResponse } from '@/types/api'
import type { UserInfo, PageParams, PageResponse } from '@/types/api'
/**
* 获取用户列表(分页)
* @param params 分页参数
*/
export function getUserListApi(params: PageParams & Record<string, any>): Promise<ApiResponse<PageResponse<UserInfo>>> {
return request({
export function getUserListApi(
params: PageParams & Record<string, any>
): Promise<PageResponse<UserInfo>> {
return request<PageResponse<UserInfo>>({
url: '/user/page',
method: 'get',
params,
@@ -19,8 +21,8 @@ export function getUserListApi(params: PageParams & Record<string, any>): Promis
* 获取用户详情
* @param id 用户 ID
*/
export function getUserDetailApi(id: number | string): Promise<ApiResponse<UserInfo>> {
return request({
export function getUserDetailApi(id: number | string): Promise<UserInfo> {
return request<UserInfo>({
url: '/user/detail',
method: 'get',
params: { id },
@@ -32,8 +34,8 @@ export function getUserDetailApi(id: number | string): Promise<ApiResponse<UserI
* 新增用户
* @param data 用户数据
*/
export function addUserApi(data: Partial<UserInfo>): Promise<ApiResponse> {
return request({
export function addUserApi(data: Partial<UserInfo>): Promise<void> {
return request<void>({
url: '/user/add',
method: 'post',
data,
@@ -45,8 +47,8 @@ export function addUserApi(data: Partial<UserInfo>): Promise<ApiResponse> {
* 编辑用户
* @param data 用户数据
*/
export function editUserApi(data: Partial<UserInfo>): Promise<ApiResponse> {
return request({
export function editUserApi(data: Partial<UserInfo>): Promise<void> {
return request<void>({
url: '/user/edit',
method: 'post',
data,
@@ -58,8 +60,8 @@ export function editUserApi(data: Partial<UserInfo>): Promise<ApiResponse> {
* 删除用户
* @param data 删除参数
*/
export function deleteUserApi(data: { id?: number; ids?: number[] }): Promise<ApiResponse> {
return request({
export function deleteUserApi(data: { id?: number; ids?: number[] }): Promise<void> {
return request<void>({
url: '/user/del',
method: 'post',
data,
@@ -71,8 +73,8 @@ export function deleteUserApi(data: { id?: number; ids?: number[] }): Promise<Ap
* 重置用户密码
* @param data 重置密码参数
*/
export function resetPasswordApi(data: { id: number; password: string }): Promise<ApiResponse> {
return request({
export function resetPasswordApi(data: { id: number; password: string }): Promise<void> {
return request<void>({
url: '/user/resetPassword',
method: 'post',
data,
@@ -84,8 +86,8 @@ export function resetPasswordApi(data: { id: number; password: string }): Promis
* 修改用户状态
* @param data 状态参数
*/
export function updateUserStateApi(data: { id: number; state: boolean }): Promise<ApiResponse> {
return request({
export function updateUserStateApi(data: { id: number; state: boolean }): Promise<void> {
return request<void>({
url: '/user/updateState',
method: 'post',
data,
@@ -96,8 +98,8 @@ export function updateUserStateApi(data: { id: number; state: boolean }): Promis
/**
* 获取当前用户信息(扩展)
*/
export function getCurrentUserInfoApi(): Promise<ApiResponse<UserInfo>> {
return request({
export function getCurrentUserInfoApi(): Promise<UserInfo> {
return request<UserInfo>({
url: '/user/current',
method: 'get',
module: RequestModule.BASE
@@ -108,8 +110,8 @@ export function getCurrentUserInfoApi(): Promise<ApiResponse<UserInfo>> {
* 更新当前用户信息
* @param data 用户数据
*/
export function updateCurrentUserApi(data: Partial<UserInfo>): Promise<ApiResponse> {
return request({
export function updateCurrentUserApi(data: Partial<UserInfo>): Promise<void> {
return request<void>({
url: '/user/updateCurrent',
method: 'post',
data,
@@ -117,15 +119,16 @@ export function updateCurrentUserApi(data: Partial<UserInfo>): Promise<ApiRespon
})
}
/**
* 上传用户头像
* @param file 头像文件
*/
export function uploadAvatarApi(file: File): Promise<ApiResponse<{ url: string }>> {
export function uploadAvatarApi(file: File): Promise<{ url: string }> {
const formData = new FormData()
formData.append('file', file)
return request({
return request<{ url: string }>({
url: '/user/uploadAvatar',
method: 'post',
data: formData,
@@ -136,3 +139,4 @@ export function uploadAvatarApi(file: File): Promise<ApiResponse<{ url: string }
})
}

View File

@@ -72,7 +72,7 @@ const englishSwitch = () => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/layout-header.scss';
@use '@/styles/scss/layout-header.scss';
.n-button-hover {
font-weight: bold;
.n-button:hover {

View File

@@ -16,7 +16,11 @@
@contextmenu="handleContextMenu($event, item.path)"
@click.stop="router.push(item.path)">
<div class="tabs-left" />
<n-icon class="tab-icon" size="18" :component="(vicons as any)[item.icon]" />
<n-icon
class="tab-icon"
size="18"
:component="(vicons as any)[item.icon] || (vicons as any)[DEFAULT_TAB_ICON]"
/>
{{ item.title }}
<n-icon class="del" size="14" :component="X" @click.stop="removeTabs(item.path)" />
</div>
@@ -52,6 +56,8 @@
<script setup lang="ts">
import * as vicons from '@vicons/tabler'
import { BrowserX, DotsVertical, LetterA, LetterO, SmartHome, X } from '@vicons/tabler'
const DEFAULT_TAB_ICON = 'LayoutGrid'
import { mainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
import { tabs } from '@/stores/tabs'

View File

@@ -87,5 +87,5 @@ const handleDel = async (item: any) => {
}
</script>
<style lang="scss" scoped>
@import '@/styles/scss/global-search';
@use '@/styles/scss/global-search';
</style>

View File

@@ -75,5 +75,5 @@ const handleTo = () => {
}
</script>
<style lang="scss" scoped>
@import '@/styles/scss/global-search';
@use '@/styles/scss/global-search';
</style>

View File

@@ -104,7 +104,7 @@ document.addEventListener('keydown', (event) => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/layout-header.scss';
@use '@/styles/scss/layout-header.scss';
.search-input {
display: flex;
align-items: center;

View File

@@ -162,6 +162,6 @@ const save = (val: globalSetting, event: MouseEvent) => {
</script>
<style lang="scss">
@import '@/styles/scss/layout-header.scss';
@import '@/styles/scss/toggle-theme.scss';
@use '@/styles/scss/layout-header';
@use '@/styles/scss/toggle-theme';
</style>

View File

@@ -3,8 +3,6 @@ import useState from '@/hooks/useState.ts'
import { i18n } from '@/i18n'
import { NForm } from 'naive-ui'
import { remember } from '@/stores/remember'
import apis from '@/services/apis'
import { RCodeEnum } from '@/enums'
import { Loading } from 'notiflix'
import { tabs } from '@/stores/tabs.ts'
import { getEnhancedFingerprint } from '@/utils/fingerprint'
@@ -135,25 +133,26 @@ export const useLogin = () => {
* @param notifi 是否显示提示
*/
const exit = async (notifi = true) => {
await apis.logout().then((res) => {
if (res.code !== RCodeEnum.OK) {
window.$notification.error({
title: res.msg ? res.msg : t('logout_error'),
duration: 1500,
keepAliveOnHover: true
})
return false
}
userInfoStore.logout()
try {
await userInfoStore.logout()
tabsStore.resetState()
if (notifi) {
window.$notification.success({
title: res.msg,
title: t('logout'),
duration: 1500,
keepAliveOnHover: true
})
}
})
} catch (error) {
console.error('退出登录失败:', error)
if (notifi) {
window.$notification.error({
title: t('logout_error'),
duration: 1500,
keepAliveOnHover: true
})
}
}
}
/*弹出验证码输入框*/
const handleCodeInput = async (formInstance: any) => {

View File

@@ -101,10 +101,12 @@ const handleCollapsed = () => {
emit('collapsed', collapsed.value)
}
/* 渲染菜单图标(兼容后端返回的任意 icon 字符串,不存在的直接不渲染) */
const DEFAULT_MENU_ICON = 'LayoutGrid'
/* 渲染菜单图标(兼容后端返回的任意 icon 字符串,找不到则使用默认图标) */
const renderIcon = (icon?: string) => {
if (!icon) return undefined
const Comp = (vicons as any)[icon]
const iconName = icon && (vicons as any)[icon] ? icon : DEFAULT_MENU_ICON
const Comp = (vicons as any)[iconName]
if (!Comp) return undefined
return () => <NIcon component={Comp} />
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="footer">Copyright © 2023-2024 HuLa.All Rights Reserved.</div>
<div class="footer">Copyright © 2023-2026 HuLa.All Rights Reserved.</div>
</template>
<script setup lang="ts">

View File

@@ -271,7 +271,7 @@ const userExit = () => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/layout-header.scss';
@use '@/styles/scss/layout-header.scss';
:deep(.n-tabs-capsule) {
width: 122px !important;
height: 33px !important;

View File

@@ -32,9 +32,19 @@ const routes: Array<RouteRecordRaw> = [
path: '/',
name: 'page',
component: () => import('@/layout/index.vue'),
//斜杠重定向路由到/home
// 斜杠重定向路由到 /home
redirect: '/home',
children: []
children: [
{
path: '/home',
name: 'Home',
meta: {
title: '主页',
requiresAuth: true
},
component: () => import('@/views/page/Home.vue')
}
]
}
]

View File

@@ -53,37 +53,35 @@ export const userStore = defineStore('localUserInfo', {
*/
async login(loginParams: LoginParams) {
try {
const res = await loginApi(loginParams)
if (res.success && res.data) {
this.loginInfo.token = res.data.token
this.loginInfo.refreshToken = res.data.refreshToken
const uid = res.data.uid
const data = await loginApi(loginParams)
if (!this.loginInfo.sysUser) {
this.loginInfo.sysUser = {
id: '',
uid: uid,
tenantId: '',
role: ''
} as any
} else {
this.loginInfo.sysUser.uid = uid
}
this.loginInfo.token = data.token
this.loginInfo.refreshToken = data.refreshToken
const uid = data.uid
console.log('✅ 登录成功token 和 uid 已保存:', {
token: res.data.token?.substring(0, 20) + '...',
if (!this.loginInfo.sysUser) {
this.loginInfo.sysUser = {
id: '',
uid,
userId: this.loginInfo.sysUser?.id
})
// 等待租户选择后再获取用户信息和初始化路由
return { success: true, needSelectTenant: true, uid: res.data.uid }
tenantId: '',
role: ''
} as any
} else {
return { success: false, message: res.msg || '登录失败' }
this.loginInfo.sysUser.uid = uid
}
console.log('✅ 登录成功token 和 uid 已保存:', {
token: data.token?.substring(0, 20) + '...',
uid,
userId: this.loginInfo.sysUser?.id
})
// 等待租户选择后再获取用户信息和初始化路由
return { success: true, needSelectTenant: true, uid: data.uid }
} catch (error: any) {
console.error('❌ 登录失败:', error)
return { success: false, message: error.message || '登录失败' }
const message = (error && (error.msg || error.message)) || '登录失败'
return { success: false, message }
}
},
@@ -92,13 +90,8 @@ export const userStore = defineStore('localUserInfo', {
*/
async getUserTenantList(): Promise<TenantInfo[]> {
try {
const res = await getUserTenantListApi()
if (res.success && res.data) {
return res.data
} else {
console.error('获取租户列表失败:', res.msg)
return []
}
const list = await getUserTenantListApi()
return list || []
} catch (error) {
console.error('获取租户列表失败:', error)
return []
@@ -126,43 +119,33 @@ export const userStore = defineStore('localUserInfo', {
}
// 调用切换租户 API
const switchRes = await switchTenantAndOrgApi({
const switchData = await switchTenantAndOrgApi({
clientId: 'luohuo_web_pro' // 使用配置的客户端 ID
})
if (switchRes.success && switchRes.data) {
// 更新 token
if (switchRes.data.token) {
this.loginInfo.token = switchRes.data.token
console.log('✅ 切换租户成功token 已更新')
}
} else {
console.error('❌ 切换租户失败:', switchRes.msg)
return { success: false, message: switchRes.msg || '切换租户失败' }
// 更新 token
if (switchData.token) {
this.loginInfo.token = switchData.token
console.log('✅ 切换租户成功token 已更新')
}
// 获取用户信息
const userRes = await getUserInfoApi()
if (userRes.success && userRes.data) {
this.loginInfo.sysUser = {
...userRes.data,
id: userRes.data.id || '',
uid: userRes.data.uid || '',
tenantId,
role:
Array.isArray(userRes.data.roles) && userRes.data.roles.length > 0
? userRes.data.roles[0].code || ''
: ''
}
console.log('✅ 用户信息获取成功:', {
id: userRes.data.id,
userName: userRes.data.userName,
tenantId: String(tenantId)
})
} else {
console.error('❌ 获取用户信息失败:', userRes.msg)
return { success: false, message: userRes.msg || '获取用户信息失败' }
const userData = await getUserInfoApi()
this.loginInfo.sysUser = {
...userData,
id: userData.id || '',
uid: userData.uid || '',
tenantId,
role:
Array.isArray(userData.roles) && userData.roles.length > 0
? userData.roles[0].code || ''
: ''
}
console.log('✅ 用户信息获取成功:', {
id: userData.id,
userName: userData.userName,
tenantId: String(tenantId)
})
// 初始化动态路由
await this.initUserInfo()
@@ -170,7 +153,8 @@ export const userStore = defineStore('localUserInfo', {
return { success: true }
} catch (error: any) {
console.error('❌ 设置租户失败:', error)
return { success: false, message: error.message || '设置租户失败' }
const message = (error && (error.msg || error.message)) || '设置租户失败'
return { success: false, message }
}
},
@@ -179,134 +163,132 @@ export const userStore = defineStore('localUserInfo', {
*/
async initUserInfo() {
try {
const routeRes = await getUserRoutesApi(1) // applicationId = 1
if (routeRes.success && routeRes.data) {
const rawRoutes = routeRes.data.routerList || []
const routeData = await getUserRoutesApi(1) // applicationId = 1
const rawRoutes = routeData.routerList || []
// 将后端返回的 VueRouter 数据转换为前端菜单结构
const transformRoutesToMenus = (routes: any[]): any[] => {
/**
* 从路由记录解析出前端使用的 page 名称
* - 优先使用 route.page
* - 其次使用 route.component / route.meta.component
* - 对于后端返回的 "/basic/.../index"、"/basic/.../Edit" 等,取最后一段作为 page
* - 对于 "LAYOUT",不生成实际页面,仅作为分组存在
*/
const getPageFromRoute = (route: any): string | undefined => {
if (route.page) return route.page
// 将后端返回的 VueRouter 数据转换为前端菜单结构
const transformRoutesToMenus = (routes: any[]): any[] => {
/**
* 从路由记录解析出前端使用的 page 名称
* - 优先使用 route.page
* - 其次使用 route.component / route.meta.component
* - 对于后端返回的 "/basic/.../index"、"/basic/.../Edit" 等,取最后一段作为 page
* - 对于 "LAYOUT",不生成实际页面,仅作为分组存在
*/
const getPageFromRoute = (route: any): string | undefined => {
if (route.page) return route.page
const rawComponent: string | undefined = route.component || route.meta?.component
if (!rawComponent || rawComponent === 'LAYOUT') return undefined
const rawComponent: string | undefined = route.component || route.meta?.component
if (!rawComponent || rawComponent === 'LAYOUT') return undefined
if (rawComponent.includes('/basic/user/')) {
return 'User'
}
if (rawComponent.includes('/basic/system/baseRole/')) {
return 'Role'
}
if (rawComponent.includes('/basic/system/') || rawComponent.includes('/basic/msg/')) {
return 'Home'
}
let comp = rawComponent
// 去掉可能的前缀,例如 src/views/、views/、page/、/basic/
comp = comp.replace(/^\/?src\/views\//, '')
comp = comp.replace(/^views\//, '')
comp = comp.replace(/^page\//, '')
comp = comp.replace(/^\/?basic\//, '')
const segments = comp.split('/')
let last = segments[segments.length - 1] || ''
// 去掉 .vue 后缀
if (last.endsWith('.vue')) {
last = last.slice(0, -4)
}
return last || undefined
if (rawComponent.includes('/basic/user/')) {
return 'User'
}
if (rawComponent.includes('/basic/system/baseRole/')) {
return 'Role'
}
if (rawComponent.includes('/basic/system/') || rawComponent.includes('/basic/msg/')) {
return 'Home'
}
const normalizePath = (path: string | undefined, page: string): string => {
// Home 页特殊处理:保持为 "home"
if (page === 'Home') {
if (!path || path === '/' || path === '/home' || path === 'home') {
return 'home'
}
}
let comp = rawComponent
// 去掉可能的前缀,例如 src/views/、views/、page/、/basic/
comp = comp.replace(/^\/?src\/views\//, '')
comp = comp.replace(/^views\//, '')
comp = comp.replace(/^page\//, '')
comp = comp.replace(/^\/?basic\//, '')
const p = path || `/${page}`
return p.startsWith('/') ? p : `/${p}`
const segments = comp.split('/')
let last = segments[segments.length - 1] || ''
// 去掉 .vue 后缀
if (last.endsWith('.vue')) {
last = last.slice(0, -4)
}
const loop = (list: any[]): any[] => {
const result: any[] = []
list.forEach((route) => {
const meta = route.meta || {}
const hideMenu = meta.hideMenu === true
const hideChildrenInMenu = meta.hideChildrenInMenu === true
const page = getPageFromRoute(route)
const path = page ? normalizePath(route.path, page) : route.path
let children: any[] | undefined
if (Array.isArray(route.children) && route.children.length && !hideChildrenInMenu) {
children = loop(route.children)
if (!children.length) children = undefined
}
// 当前节点隐藏菜单,仅提升子节点
if (hideMenu) {
if (children) {
result.push(...children)
}
return
}
// LAYOUT 等仅作为分组存在且没有子节点时,直接忽略
if (!page && (!children || !children.length)) {
return
}
const menu: any = {
id: route.id ? String(route.id) : undefined,
path: page ? path : undefined, // 分组节点不需要 path
// 菜单显示名称优先使用 meta.title
name: meta.title || route.name || page,
page,
icon: route.icon || meta.icon,
hideMenu,
hideChildrenInMenu
}
if (children) {
menu.children = children
}
result.push(menu)
})
return result
}
return loop(routes)
return last || undefined
}
this.loginInfo.menus = transformRoutesToMenus(rawRoutes)
// 同步权限资源与角色信息
this.loginInfo.auths = (routeRes.data.resourceList || []).map((code: string) => ({ auth: code }))
this.loginInfo.roles = (routeRes.data.roleList || []).map((code: string) => ({ name: code, code }))
const normalizePath = (path: string | undefined, page: string): string => {
// Home 页特殊处理:保持为 "home"
if (page === 'Home') {
if (!path || path === '/' || path === '/home' || path === 'home') {
return 'home'
}
}
console.log('✅ 动态路由初始化完成:', {
rawRoutes,
menus: this.loginInfo.menus,
resourceList: routeRes.data.resourceList,
roleList: routeRes.data.roleList
})
const p = path || `/${page}`
return p.startsWith('/') ? p : `/${p}`
}
// 设置动态路由
setRoutes(this.loginInfo.menus)
const loop = (list: any[]): any[] => {
const result: any[] = []
list.forEach((route) => {
const meta = route.meta || {}
const hideMenu = meta.hideMenu === true
const hideChildrenInMenu = meta.hideChildrenInMenu === true
const page = getPageFromRoute(route)
const path = page ? normalizePath(route.path, page) : route.path
let children: any[] | undefined
if (Array.isArray(route.children) && route.children.length && !hideChildrenInMenu) {
children = loop(route.children)
if (!children.length) children = undefined
}
// 当前节点隐藏菜单,仅提升子节点
if (hideMenu) {
if (children) {
result.push(...children)
}
return
}
// LAYOUT 等仅作为分组存在且没有子节点时,直接忽略
if (!page && (!children || !children.length)) {
return
}
const menu: any = {
id: route.id ? String(route.id) : undefined,
path: page ? path : undefined, // 分组节点不需要 path
// 菜单显示名称优先使用 meta.title
name: meta.title || route.name || page,
page,
icon: route.icon || meta.icon,
hideMenu,
hideChildrenInMenu
}
if (children) {
menu.children = children
}
result.push(menu)
})
return result
}
return loop(routes)
}
this.loginInfo.menus = transformRoutesToMenus(rawRoutes)
// 同步权限资源与角色信息
this.loginInfo.auths = (routeData.resourceList || []).map((code: string) => ({ auth: code }))
this.loginInfo.roles = (routeData.roleList || []).map((code: string) => ({ name: code, code }))
console.log('✅ 动态路由初始化完成:', {
rawRoutes,
menus: this.loginInfo.menus,
resourceList: routeData.resourceList,
roleList: routeData.roleList
})
// 设置动态路由
setRoutes(this.loginInfo.menus)
return true
} catch (error) {
console.error('初始化用户信息失败:', error)

View File

@@ -115,9 +115,7 @@ const createAxiosInstance = (): AxiosInstance => {
console.log('✅ Response:', config.url, data)
// 判断响应是否成功
const isSuccess = data.code === 200 || data.code === '200' || data.code === '00000' || data.success === true
if (isSuccess) {
if (data.code === 200 || data.success) {
return response
}
@@ -283,8 +281,9 @@ const http = createAxiosInstance()
* @param url 请求地址
* @param config 请求配置
*/
export function get<T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return http.get(url, config).then((res) => res.data)
export async function get<T = any>(url: string, config?: RequestConfig): Promise<T> {
const res = await http.get<ApiResponse<T>>(url, config)
return res.data.data
}
/**
@@ -293,8 +292,9 @@ export function get<T = any>(url: string, config?: RequestConfig): Promise<ApiRe
* @param data 请求数据
* @param config 请求配置
*/
export function post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> {
return http.post(url, data, config).then((res) => res.data)
export async function post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
const res = await http.post<ApiResponse<T>>(url, data, config)
return res.data.data
}
/**
@@ -303,8 +303,9 @@ export function post<T = any>(url: string, data?: any, config?: RequestConfig):
* @param data 请求数据
* @param config 请求配置
*/
export function put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> {
return http.put(url, data, config).then((res) => res.data)
export async function put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
const res = await http.put<ApiResponse<T>>(url, data, config)
return res.data.data
}
/**
@@ -312,16 +313,18 @@ export function put<T = any>(url: string, data?: any, config?: RequestConfig): P
* @param url 请求地址
* @param config 请求配置
*/
export function del<T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return http.delete(url, config).then((res) => res.data)
export async function del<T = any>(url: string, config?: RequestConfig): Promise<T> {
const res = await http.delete<ApiResponse<T>>(url, config)
return res.data.data
}
/**
* 通用请求方法
* @param config 请求配置
*/
export function request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
return http.request(config).then((res) => res.data)
export async function request<T = any>(config: RequestConfig): Promise<T> {
const res = await http.request<ApiResponse<T>>(config)
return res.data.data
}
export default http

View File

@@ -249,7 +249,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/login';
@use '@/styles/scss/login';
.login h1 {
color: v-bind(TEXT_COLOR);

View File

@@ -108,6 +108,6 @@ const linkOpen = (val: any) => {
</script>
<style lang="scss">
@import '@/styles/scss/login';
@import '@/styles/scss/toggle-theme';
@use '@/styles/scss/login';
@use '@/styles/scss/toggle-theme';
</style>

View File

@@ -219,5 +219,5 @@ provide('onLoginSuccess', async () => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/login';
@use '@/styles/scss/login';
</style>

View File

@@ -128,5 +128,5 @@ const handleUpdateFilter = (filters: DataTableFilterState, sourceColumn: DataTab
</script>
<style lang="scss" scoped>
@import '@/styles/scss/user';
@use '@/styles/scss/user';
</style>

View File

@@ -116,7 +116,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/barchar.scss';
@use '@/styles/scss/barchar.scss';
.barchart {
background: v-bind(BGC);
}

View File

@@ -116,7 +116,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/barchar.scss';
@use '@/styles/scss/barchar.scss';
.barchart {
background: v-bind(BGC);
}

View File

@@ -116,7 +116,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/barchar.scss';
@use '@/styles/scss/barchar.scss';
.barchart {
background: v-bind(BGC);

View File

@@ -155,7 +155,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/cardchart.scss';
@use '@/styles/scss/cardchart.scss';
.weekData {
background: v-bind(BGC);
span {

View File

@@ -145,7 +145,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/cardchart.scss';
@use '@/styles/scss/cardchart.scss';
.weekData {
background: v-bind(BGC);
span {

View File

@@ -94,7 +94,7 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '@/styles/scss/cardchart.scss';
@use '@/styles/scss/cardchart.scss';
.weekData {
background: v-bind(BGC);
span {

View File

@@ -75,6 +75,11 @@ export default defineConfig(({ mode }: ConfigEnv) => {
}
})
],
css: {
preprocessorOptions: {
scss: {}
}
},
build: {
cssCodeSplit: true, // 启用 CSS 代码拆分
minify: 'terser', // 指定使用哪种混淆器