refactor: migrate Vue components and stores to Composition API

This commit is contained in:
2026-09-28 15:37:26 +08:00
parent 28fda43317
commit 1f016bb8a2
9 changed files with 268 additions and 231 deletions
+2 -4
View File
@@ -1,10 +1,8 @@
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
<script lang="ts" setup>
defineOptions({
name: 'FrameBlank',
});
</script>
+7 -11
View File
@@ -9,21 +9,17 @@
</template>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, unref } from 'vue';
<script lang="ts" setup>
import { computed, unref } from 'vue';
import FrameContent from '../components/FrameContent.vue';
import { useFrameKeepAlive } from './useFrameKeepAlive';
export default defineComponent({
defineOptions({
name: 'FrameLayout',
components: { FrameContent },
setup() {
const { getFramePages, hasRenderFrame, showIframe } = useFrameKeepAlive();
const showFrame = computed(() => unref(getFramePages).length > 0);
return { getFramePages, hasRenderFrame, showIframe, showFrame };
},
});
const { getFramePages, hasRenderFrame, showIframe } = useFrameKeepAlive();
const showFrame = computed(() => unref(getFramePages).length > 0);
</script>
@@ -37,16 +37,12 @@
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
data: {
type: Object,
default: () => {
return {};
},
<script lang="ts" setup>
const { data } = defineProps({
data: {
type: Object,
default: () => {
return {};
},
},
});
+19 -17
View File
@@ -1,4 +1,5 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { t } from '@/locales';
import type { NotificationItem } from '@/types/interface';
@@ -44,21 +45,22 @@ const getMsgData = () => [
type MsgDataType = ReturnType<typeof getMsgData>;
export const useNotificationStore = defineStore('notification', {
state: () => ({
msgData: getMsgData(),
}),
getters: {
unreadMsg: (state) => state.msgData.filter((item: NotificationItem) => item.status),
readMsg: (state) => state.msgData.filter((item: NotificationItem) => !item.status),
export const useNotificationStore = defineStore(
'notification',
() => {
const msgData = ref<MsgDataType>(getMsgData());
const unreadMsg = computed(() => msgData.value.filter((item: NotificationItem) => item.status));
const readMsg = computed(() => msgData.value.filter((item: NotificationItem) => !item.status));
function setMsgData(data: MsgDataType) {
msgData.value = data;
}
function refreshMsgData() {
msgData.value = getMsgData();
}
return { msgData, unreadMsg, readMsg, setMsgData, refreshMsgData };
},
actions: {
setMsgData(data: MsgDataType) {
this.msgData = data;
},
refreshMsgData() {
this.msgData = getMsgData();
},
},
persist: false,
});
{ persist: false },
);
+32 -31
View File
@@ -3,6 +3,7 @@
import cloneDeep from 'lodash/cloneDeep';
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import router, { allRoutes } from '@/router';
@@ -29,41 +30,41 @@ function filterPermissionsRouters(routes: Array<RouteRecordRaw>, roles: Array<un
return { accessedRouters: res, removeRoutes };
}
export const usePermissionStore = defineStore('permission', {
state: () => ({
whiteListRouters: ['/login'],
routers: [] as Array<RouteRecordRaw>,
removeRoutes: [] as Array<RouteRecordRaw>,
}),
actions: {
async initRoutes(roles: Array<unknown>) {
let accessedRouters = [];
export const usePermissionStore = defineStore('permission', () => {
const whiteListRouters = ref(['/login']);
const routers = ref<Array<RouteRecordRaw>>([]);
const removeRoutes = ref<Array<RouteRecordRaw>>([]);
let removeRoutes: Array<RouteRecordRaw> = [];
// special token
if (roles.includes('all')) {
accessedRouters = cloneDeep(allRoutes);
} else {
const res = filterPermissionsRouters(allRoutes, roles);
accessedRouters = res.accessedRouters;
removeRoutes = res.removeRoutes;
async function initRoutes(roles: Array<unknown>) {
let accessedRouters = [];
let routesToRemove: Array<RouteRecordRaw> = [];
// special token
if (roles.includes('all')) {
accessedRouters = cloneDeep(allRoutes);
} else {
const res = filterPermissionsRouters(allRoutes, roles);
accessedRouters = res.accessedRouters;
routesToRemove = res.removeRoutes;
}
routers.value = accessedRouters;
removeRoutes.value = routesToRemove;
routesToRemove.forEach((item: RouteRecordRaw) => {
if (router.hasRoute(item.name!)) {
router.removeRoute(item.name!);
}
});
}
this.routers = accessedRouters;
this.removeRoutes = removeRoutes;
async function restore() {
removeRoutes.value.forEach((item: RouteRecordRaw) => {
router.addRoute(item);
});
}
removeRoutes.forEach((item: RouteRecordRaw) => {
if (router.hasRoute(item.name!)) {
router.removeRoute(item.name!);
}
});
},
async restore() {
this.removeRoutes.forEach((item: RouteRecordRaw) => {
router.addRoute(item);
});
},
},
return { whiteListRouters, routers, removeRoutes, initRoutes, restore };
});
export function getPermissionStore() {
+39 -37
View File
@@ -1,5 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import type { RouteItem } from '@/api/model/permissionModel';
@@ -8,45 +9,46 @@ import router, { fixedRouterList, homepageRouterList } from '@/router';
import { store } from '@/store';
import { transformObjectToRoute } from '@/utils/route';
export const usePermissionStore = defineStore('permission', {
state: () => ({
whiteListRouters: ['/login'],
routers: [] as Array<RouteRecordRaw>,
removeRoutes: [] as Array<RouteRecordRaw>,
asyncRoutes: [] as Array<RouteRecordRaw>,
}),
actions: {
async initRoutes() {
const accessedRouters = this.asyncRoutes;
export const usePermissionStore = defineStore('permission', () => {
const whiteListRouters = ref(['/login']);
const routers = ref<Array<RouteRecordRaw>>([]);
const removeRoutes = ref<Array<RouteRecordRaw>>([]);
const asyncRoutes = ref<Array<RouteRecordRaw>>([]);
// 在菜单展示全部路由
this.routers = cloneDeep([...homepageRouterList, ...accessedRouters, ...fixedRouterList]);
// 在菜单只展示动态路由和首页
// this.routers = [...homepageRouterList, ...accessedRouters];
// 在菜单只展示动态路由
// this.routers = [...accessedRouters];
},
async buildAsyncRoutes() {
try {
// 发起菜单权限请求 获取菜单列表
const asyncRoutes: Array<RouteItem> = (await getMenuList()).list;
this.asyncRoutes = transformObjectToRoute(asyncRoutes);
await this.initRoutes();
return this.asyncRoutes;
} catch (error) {
throw new Error("Can't build routes", error as ErrorOptions);
async function initRoutes() {
const accessedRouters = asyncRoutes.value;
// 在菜单展示全部路由
routers.value = cloneDeep([...homepageRouterList, ...accessedRouters, ...fixedRouterList]);
// 在菜单只展示动态路由和首页
// routers.value = [...homepageRouterList, ...accessedRouters];
// 在菜单只展示动态路由
// routers.value = [...accessedRouters];
}
async function buildAsyncRoutes() {
try {
// 发起菜单权限请求 获取菜单列表
const routeItems: Array<RouteItem> = (await getMenuList()).list;
asyncRoutes.value = transformObjectToRoute(routeItems);
await initRoutes();
return asyncRoutes.value;
} catch (error) {
throw new Error("Can't build routes", error as ErrorOptions);
}
}
async function restoreRoutes() {
// 不需要在此额外调用initRoutes更新侧边导肮内容,在登录后asyncRoutes为空会调用
asyncRoutes.value.forEach((item: RouteRecordRaw) => {
if (item.name) {
router.removeRoute(item.name);
}
},
async restoreRoutes() {
// 不需要在此额外调用initRoutes更新侧边导肮内容,在登录后asyncRoutes为空会调用
this.asyncRoutes.forEach((item: RouteRecordRaw) => {
if (item.name) {
router.removeRoute(item.name);
}
});
this.asyncRoutes = [];
},
},
});
asyncRoutes.value = [];
}
return { whiteListRouters, routers, removeRoutes, asyncRoutes, initRoutes, buildAsyncRoutes, restoreRoutes };
});
export function getPermissionStore() {
+64 -49
View File
@@ -1,6 +1,7 @@
import keys from 'lodash/keys';
import { defineStore } from 'pinia';
import { Color } from 'tvision-color';
import { computed, reactive, toRefs } from 'vue';
import type { TColorSeries } from '@/config/color';
import { DARK_CHART_COLORS, LIGHT_CHART_COLORS } from '@/config/color';
@@ -9,7 +10,7 @@ import { store } from '@/store';
import type { ModeType } from '@/types/interface';
import { generateColorMap, insertThemeStylesheet } from '@/utils/color';
const state: Record<string, any> = {
const state = {
...STYLE_CONFIG,
showSettingPanel: false,
colorList: {} as TColorSeries,
@@ -19,57 +20,53 @@ const state: Record<string, any> = {
export type TState = typeof state;
export type TStateKey = keyof typeof state;
export const useSettingStore = defineStore('setting', {
state: () => state,
getters: {
showSidebar: (state) => state.layout !== 'top',
showSidebarLogo: (state) => state.layout === 'side',
showHeaderLogo: (state) => state.layout !== 'side',
displayMode: (state): ModeType => {
if (state.mode === 'auto') {
export const useSettingStore = defineStore(
'setting',
() => {
const settingState = reactive({ ...state });
const showSidebar = computed(() => settingState.layout !== 'top');
const showSidebarLogo = computed(() => settingState.layout === 'side');
const showHeaderLogo = computed(() => settingState.layout !== 'side');
const displayMode = computed((): ModeType => {
if (settingState.mode === 'auto') {
const media = window.matchMedia('(prefers-color-scheme:dark)');
if (media.matches) {
return 'dark';
}
return 'light';
}
return state.mode as ModeType;
},
displaySideMode: (state): ModeType => {
return state.sideMode as ModeType;
},
},
actions: {
async changeMode(mode: ModeType | 'auto') {
let theme = mode;
return settingState.mode as ModeType;
});
const displaySideMode = computed((): ModeType => settingState.sideMode as ModeType);
if (mode === 'auto') {
theme = this.getMediaColor();
}
const isDarkMode = theme === 'dark';
document.documentElement.setAttribute('theme-mode', isDarkMode ? 'dark' : '');
this.chartColors = isDarkMode ? DARK_CHART_COLORS : LIGHT_CHART_COLORS;
},
async changeSideMode(mode: ModeType) {
const isDarkMode = mode === 'dark';
document.documentElement.setAttribute('side-mode', isDarkMode ? 'dark' : '');
},
getMediaColor() {
function getMediaColor() {
const media = window.matchMedia('(prefers-color-scheme:dark)');
if (media.matches) {
return 'dark';
}
return 'light';
},
changeBrandTheme(brandTheme: string) {
const mode = this.displayMode;
}
async function changeMode(mode: ModeType | 'auto') {
const theme = mode === 'auto' ? getMediaColor() : mode;
const isDarkMode = theme === 'dark';
document.documentElement.setAttribute('theme-mode', isDarkMode ? 'dark' : '');
settingState.chartColors = isDarkMode ? DARK_CHART_COLORS : LIGHT_CHART_COLORS;
}
async function changeSideMode(mode: ModeType) {
const isDarkMode = mode === 'dark';
document.documentElement.setAttribute('side-mode', isDarkMode ? 'dark' : '');
}
function changeBrandTheme(brandTheme: string) {
const mode = displayMode.value;
// 以主题色加显示模式作为键
const colorKey = `${brandTheme}[${mode}]`;
let colorMap = this.colorList[colorKey];
let colorMap = settingState.colorList[colorKey];
// 如果不存在色阶,就需要计算
if (colorMap === undefined) {
const [{ colors: newPalette, primary: brandColorIndex }] = Color.getColorGradations({
@@ -78,33 +75,51 @@ export const useSettingStore = defineStore('setting', {
remainInput: false, // 是否保留输入 不保留会矫正不合适的主题色
});
colorMap = generateColorMap(brandTheme, newPalette, mode, brandColorIndex);
this.colorList[colorKey] = colorMap;
settingState.colorList[colorKey] = colorMap;
}
// TODO 需要解决不停切换时有反复插入 style 的问题
insertThemeStylesheet(brandTheme, colorMap, mode);
document.documentElement.setAttribute('theme-color', brandTheme);
},
updateConfig(payload: Partial<TState>) {
}
function updateConfig(payload: Partial<TState>) {
for (const key in payload) {
if (payload[key as TStateKey] !== undefined) {
this[key as TStateKey] = payload[key as TStateKey];
const value = payload[key as TStateKey];
if (value !== undefined) {
Reflect.set(settingState, key, value);
}
if (key === 'mode') {
this.changeMode(payload[key] as ModeType);
changeMode(value as ModeType);
}
if (key === 'sideMode') {
this.changeSideMode(payload[key] as ModeType);
changeSideMode(value as ModeType);
}
if (key === 'brandTheme') {
this.changeBrandTheme(payload[key]);
changeBrandTheme(value as string);
}
}
}
return {
...toRefs(settingState),
showSidebar,
showSidebarLogo,
showHeaderLogo,
displayMode,
displaySideMode,
changeMode,
changeSideMode,
getMediaColor,
changeBrandTheme,
updateConfig,
};
},
{
persist: {
pick: [...keys(STYLE_CONFIG), 'colorList', 'chartColors'],
},
},
persist: {
pick: [...keys(STYLE_CONFIG), 'colorList', 'chartColors'],
},
});
);
export function getSettingStore() {
return useSettingStore(store);
+66 -42
View File
@@ -1,4 +1,5 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { store } from '@/store';
import type { TRouterInfo, TTabRouterType } from '@/types/interface';
@@ -23,75 +24,98 @@ const state = {
// const ignoreCacheRoutes = ['DashboardDetail'];
const ignoreCacheRoutes = ['login'];
export const useTabsRouterStore = defineStore('tabsRouter', {
state: () => state,
getters: {
tabRouters: (state: TTabRouterType) => state.tabRouterList,
refreshing: (state: TTabRouterType) => state.isRefreshing,
},
actions: {
export const useTabsRouterStore = defineStore(
'tabsRouter',
() => {
const tabRouterList = ref<TTabRouterType['tabRouterList']>(state.tabRouterList);
const isRefreshing = ref(state.isRefreshing);
const tabRouters = computed(() => tabRouterList.value);
const refreshing = computed(() => isRefreshing.value);
// 处理刷新
toggleTabRouterAlive(routeIdx: number) {
this.isRefreshing = !this.isRefreshing;
this.tabRouters[routeIdx].isAlive = !this.tabRouters[routeIdx].isAlive;
},
function toggleTabRouterAlive(routeIdx: number) {
isRefreshing.value = !isRefreshing.value;
tabRouters.value[routeIdx].isAlive = !tabRouters.value[routeIdx].isAlive;
}
// 处理新增
appendTabRouterList(newRoute: TRouterInfo) {
function appendTabRouterList(newRoute: TRouterInfo) {
// 不要将判断条件newRoute.meta.keepAlive !== false修改为newRoute.meta.keepAlive,starter默认开启保活,所以meta.keepAlive未定义时也需要进行保活,只有显式说明false才禁用保活。
const needAlive = !ignoreCacheRoutes.includes(newRoute.name as string) && newRoute.meta?.keepAlive !== false;
const normalizedPath = normalizePath(newRoute.path);
const existed = this.tabRouters.some((route: TRouterInfo) => {
const existed = tabRouters.value.some((route: TRouterInfo) => {
return route.path === normalizedPath || normalizePath(route.path) === normalizedPath;
});
if (!existed) {
this.tabRouterList = this.tabRouterList.concat({ ...newRoute, path: normalizedPath, isAlive: needAlive });
tabRouterList.value = tabRouterList.value.concat({ ...newRoute, path: normalizedPath, isAlive: needAlive });
}
},
}
// 处理关闭当前
subtractCurrentTabRouter(newRoute: TRouterInfo) {
function subtractCurrentTabRouter(newRoute: TRouterInfo) {
const { routeIdx } = newRoute;
if (routeIdx === undefined) return;
this.tabRouterList = this.tabRouterList.slice(0, routeIdx).concat(this.tabRouterList.slice(routeIdx + 1));
},
tabRouterList.value = tabRouterList.value.slice(0, routeIdx).concat(tabRouterList.value.slice(routeIdx + 1));
}
// 处理关闭右侧
subtractTabRouterBehind(newRoute: TRouterInfo) {
function subtractTabRouterBehind(newRoute: TRouterInfo) {
const { routeIdx } = newRoute;
if (routeIdx === undefined) return;
const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
let tabRouterList: Array<TRouterInfo> = this.tabRouterList.slice(0, routeIdx + 1);
const homeIdx: number = tabRouters.value.findIndex((route: TRouterInfo) => route.isHome);
let updatedTabRouterList: Array<TRouterInfo> = tabRouterList.value.slice(0, routeIdx + 1);
if (routeIdx < homeIdx) {
tabRouterList = tabRouterList.concat(homeRoute);
updatedTabRouterList = updatedTabRouterList.concat(homeRoute);
}
this.tabRouterList = tabRouterList;
},
tabRouterList.value = updatedTabRouterList;
}
// 处理关闭左侧
subtractTabRouterAhead(newRoute: TRouterInfo) {
function subtractTabRouterAhead(newRoute: TRouterInfo) {
const { routeIdx } = newRoute;
if (routeIdx === undefined) return;
const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
let tabRouterList: Array<TRouterInfo> = this.tabRouterList.slice(routeIdx);
const homeIdx: number = tabRouters.value.findIndex((route: TRouterInfo) => route.isHome);
let updatedTabRouterList: Array<TRouterInfo> = tabRouterList.value.slice(routeIdx);
if (routeIdx > homeIdx) {
tabRouterList = homeRoute.concat(tabRouterList);
updatedTabRouterList = homeRoute.concat(updatedTabRouterList);
}
this.tabRouterList = tabRouterList;
},
tabRouterList.value = updatedTabRouterList;
}
// 处理关闭其他
subtractTabRouterOther(newRoute: TRouterInfo) {
function subtractTabRouterOther(newRoute: TRouterInfo) {
const { routeIdx } = newRoute;
if (routeIdx === undefined) return;
const homeIdx: number = this.tabRouters.findIndex((route: TRouterInfo) => route.isHome);
this.tabRouterList = routeIdx === homeIdx ? homeRoute : homeRoute.concat([this.tabRouterList?.[routeIdx]]);
},
removeTabRouterList() {
this.tabRouterList = [];
},
initTabRouterList(newRoutes: TRouterInfo[]) {
newRoutes?.forEach((route: TRouterInfo) => this.appendTabRouterList(route));
},
const homeIdx: number = tabRouters.value.findIndex((route: TRouterInfo) => route.isHome);
tabRouterList.value = routeIdx === homeIdx ? homeRoute : homeRoute.concat([tabRouterList.value?.[routeIdx]]);
}
function removeTabRouterList() {
tabRouterList.value = [];
}
function initTabRouterList(newRoutes: TRouterInfo[]) {
newRoutes?.forEach(appendTabRouterList);
}
return {
tabRouterList,
isRefreshing,
tabRouters,
refreshing,
toggleTabRouterAlive,
appendTabRouterList,
subtractCurrentTabRouter,
subtractTabRouterBehind,
subtractTabRouterAhead,
subtractTabRouterOther,
removeTabRouterList,
initTabRouterList,
};
},
persist: true,
});
{ persist: true },
);
export function getTabsRouterStore() {
return useTabsRouterStore(store);
+33 -30
View File
@@ -1,4 +1,5 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { t } from '@/locales';
import { usePermissionStore } from '@/store';
@@ -9,18 +10,14 @@ const InitUserInfo: UserInfo = {
roles: [], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
};
export const useUserStore = defineStore('user', {
state: () => ({
token: 'main_token', // 默认token不走权限
userInfo: { ...InitUserInfo },
}),
getters: {
roles: (state) => {
return state.userInfo?.roles;
},
},
actions: {
async login(userInfo: Record<string, unknown>) {
export const useUserStore = defineStore(
'user',
() => {
const token = ref('main_token'); // 默认token不走权限
const userInfo = ref<UserInfo>({ ...InitUserInfo });
const roles = computed(() => userInfo.value?.roles);
async function login(userInfo: Record<string, unknown>) {
const mockLogin = async (userInfo: Record<string, unknown>) => {
// 登录请求流程
console.log(`用户信息:`, userInfo);
@@ -50,12 +47,13 @@ export const useUserStore = defineStore('user', {
const res = await mockLogin(userInfo);
if (res.code === 200) {
this.token = res.data;
token.value = res.data;
} else {
throw res;
}
},
async getUserInfo() {
}
async function getUserInfo() {
const mockRemoteUserInfo = async (token: string) => {
if (token === 'main_token') {
return {
@@ -68,21 +66,26 @@ export const useUserStore = defineStore('user', {
roles: ['UserIndex', 'DashboardBase', 'login'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
};
};
const res = await mockRemoteUserInfo(this.token);
const res = await mockRemoteUserInfo(token.value);
this.userInfo = res;
},
async logout() {
this.token = '';
this.userInfo = { ...InitUserInfo };
userInfo.value = res;
}
async function logout() {
token.value = '';
userInfo.value = { ...InitUserInfo };
}
return { token, userInfo, roles, login, getUserInfo, logout };
},
{
persist: {
afterHydrate: () => {
const permissionStore = usePermissionStore();
permissionStore.initRoutes();
},
key: 'user',
pick: ['token'],
},
},
persist: {
afterHydrate: () => {
const permissionStore = usePermissionStore();
permissionStore.initRoutes();
},
key: 'user',
pick: ['token'],
},
});
);