提交 f4621cc6 编写于 作者: V vben

perf: perf loading logic

上级 0c2e72d2
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
- i18n 支持 vscode `i18n-ally`插件 - i18n 支持 vscode `i18n-ally`插件
- 新增多级路由缓存示例 - 新增多级路由缓存示例
### ⚡ Performance Improvements
- 页面切换 loading 逻辑修改。对于已经加载过的页面不管有没有关闭,再次打开不会在显示 loading(已经打开过的页面在此打开速度比较快),刷新后恢复。
### 🎫 Chores ### 🎫 Chores
- 首屏 loading 修改 - 首屏 loading 修改
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1; z-index: 300;
} }
} }
</style> </style>
...@@ -7,8 +7,6 @@ import { appStore } from '/@/store/modules/app'; ...@@ -7,8 +7,6 @@ import { appStore } from '/@/store/modules/app';
export function useMultipleTabSetting() { export function useMultipleTabSetting() {
const getMultipleTabSetting = computed(() => appStore.getProjectConfig.multiTabsSetting); const getMultipleTabSetting = computed(() => appStore.getProjectConfig.multiTabsSetting);
const getMax = computed(() => unref(getMultipleTabSetting).max);
const getShowMultipleTab = computed(() => unref(getMultipleTabSetting).show); const getShowMultipleTab = computed(() => unref(getMultipleTabSetting).show);
const getShowQuick = computed(() => unref(getMultipleTabSetting).showQuick); const getShowQuick = computed(() => unref(getMultipleTabSetting).showQuick);
...@@ -21,7 +19,6 @@ export function useMultipleTabSetting() { ...@@ -21,7 +19,6 @@ export function useMultipleTabSetting() {
setMultipleTabSetting, setMultipleTabSetting,
getMultipleTabSetting, getMultipleTabSetting,
getMax,
getShowMultipleTab, getShowMultipleTab,
getShowQuick, getShowQuick,
}; };
......
...@@ -11,8 +11,8 @@ export function useTransitionSetting() { ...@@ -11,8 +11,8 @@ export function useTransitionSetting() {
const getOpenNProgress = computed(() => unref(getTransitionSetting)?.openNProgress); const getOpenNProgress = computed(() => unref(getTransitionSetting)?.openNProgress);
const getOpenPageLoading = computed(() => { const getOpenPageLoading = computed((): boolean => {
return unref(getTransitionSetting)?.openPageLoading && unref(getEnableTransition); return !!unref(getTransitionSetting)?.openPageLoading;
}); });
const getBasicTransition = computed(() => unref(getTransitionSetting)?.basicTransition); const getBasicTransition = computed(() => unref(getTransitionSetting)?.basicTransition);
......
...@@ -11,10 +11,7 @@ export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEn ...@@ -11,10 +11,7 @@ export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEn
function handleError(e: Error) { function handleError(e: Error) {
console.error(e); console.error(e);
// 101是为了 大于 打开时候设置的100延时防止闪动 appStore.commitPageLoadingState(false);
setTimeout(() => {
appStore.commitPageLoadingState(false);
}, 101);
} }
// page switch // page switch
......
...@@ -12,10 +12,7 @@ ...@@ -12,10 +12,7 @@
&__loading { &__loading {
position: absolute; position: absolute;
top: 200px;
z-index: @page-loading-z-index; z-index: @page-loading-z-index;
> .basic-loading {
margin-bottom: 15%;
}
} }
} }
...@@ -5,7 +5,7 @@ import { Loading } from '/@/components/Loading'; ...@@ -5,7 +5,7 @@ import { Loading } from '/@/components/Loading';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import PageLayout from '/@/layouts/page/index.vue'; import PageLayout from '/@/layouts/page/index';
export default defineComponent({ export default defineComponent({
name: 'LayoutContent', name: 'LayoutContent',
setup() { setup() {
...@@ -16,7 +16,12 @@ export default defineComponent({ ...@@ -16,7 +16,12 @@ export default defineComponent({
return ( return (
<div class={['layout-content', unref(getLayoutContentMode)]}> <div class={['layout-content', unref(getLayoutContentMode)]}>
{unref(getOpenPageLoading) && ( {unref(getOpenPageLoading) && (
<Loading loading={unref(getPageLoading)} absolute class="layout-content__loading" /> <Loading
loading={unref(getPageLoading)}
background="rgba(240, 242, 245, 0.6)"
absolute
class="layout-content__loading"
/>
)} )}
<PageLayout /> <PageLayout />
</div> </div>
......
...@@ -465,7 +465,6 @@ export default defineComponent({ ...@@ -465,7 +465,6 @@ export default defineComponent({
baseHandler(HandlerEnum.OPEN_PAGE_LOADING, e); baseHandler(HandlerEnum.OPEN_PAGE_LOADING, e);
}, },
def: unref(getOpenPageLoading), def: unref(getOpenPageLoading),
disabled: !unref(getEnableTransition),
})} })}
{renderSwitchItem(t('layout.setting.switchAnimation'), { {renderSwitchItem(t('layout.setting.switchAnimation'), {
......
...@@ -4,23 +4,18 @@ ...@@ -4,23 +4,18 @@
<template> <template>
<div> <div>
<router-view> <router-view>
<template #default="{ Component, route }"> <template v-slot="{ Component, route }">
<transition v-bind="transitionEvent" :name="getName(route)" mode="out-in" appear> <keep-alive v-if="openCache" :include="getCaches">
<keep-alive v-if="openCache" :include="getCaches"> <component :is="Component" :key="route.fullPath" />
<component :max="getMax" :is="Component" :key="route.fullPath" /> </keep-alive>
</keep-alive> <component v-else :is="Component" :key="route.fullPath" />
<component v-else :max="getMax" :is="Component" :key="route.fullPath" />
</transition>
</template> </template>
</router-view> </router-view>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, unref } from 'vue'; import { computed, defineComponent, unref } from 'vue';
import { RouteLocationNormalized } from 'vue-router';
import { useTransition } from './useTransition';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
...@@ -28,43 +23,20 @@ ...@@ -28,43 +23,20 @@
import { useCache } from './useCache'; import { useCache } from './useCache';
export default defineComponent({ export default defineComponent({
props: { setup() {
isPage: { const { getCaches } = useCache(false);
type: Boolean,
},
},
setup(props) {
const { getCaches } = useCache(props.isPage);
const { getShowMenu } = useMenuSetting(); const { getShowMultipleTab } = useMultipleTabSetting();
const { getOpenKeepAlive } = useRootSetting(); const { getOpenKeepAlive } = useRootSetting();
const { getBasicTransition, getEnableTransition } = useTransitionSetting(); const { getBasicTransition, getEnableTransition } = useTransitionSetting();
const { getMax } = useMultipleTabSetting(); const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab));
const transitionEvent = useTransition();
const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMenu));
function getName(route: RouteLocationNormalized) {
if (!unref(getEnableTransition)) {
return null;
}
const cacheTabs = unref(getCaches);
const isInCache = cacheTabs.includes(route.name as string);
const name = isInCache && route.meta.inTab ? 'fade-slide' : null;
return name || route.meta.transitionName || unref(getBasicTransition);
}
return { return {
getCaches, getCaches,
getMax,
transitionEvent,
getBasicTransition, getBasicTransition,
getName,
openCache, openCache,
getEnableTransition, getEnableTransition,
}; };
......
import type { FunctionalComponent } from 'vue';
import { computed, defineComponent, unref, Transition, KeepAlive } from 'vue';
import { RouterView, RouteLocation } from 'vue-router';
import FrameLayout from '/@/layouts/iframe/index.vue';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import { useCache } from './useCache';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
interface DefaultContext {
Component: FunctionalComponent;
route: RouteLocation;
}
export default defineComponent({
name: 'PageLayout',
setup() {
const { getCaches } = useCache(true);
const { getShowMultipleTab } = useMultipleTabSetting();
const { getOpenKeepAlive, getCanEmbedIFramePage } = useRootSetting();
const { getBasicTransition, getEnableTransition } = useTransitionSetting();
const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab));
return () => {
return (
<>
<RouterView>
{{
default: ({ Component, route }: DefaultContext) => {
// No longer show animations that are already in the tab
const cacheTabs = unref(getCaches);
const isInCache = cacheTabs.includes(route.name as string);
const name =
isInCache && route.meta.loaded && unref(getEnableTransition)
? 'fade-slide'
: null;
const renderComp = () => <Component key={route.fullPath} />;
const PageContent = unref(openCache) ? (
<KeepAlive>{renderComp()}</KeepAlive>
) : (
renderComp()
);
return unref(getEnableTransition) ? (
<Transition
name={name || route.meta.transitionName || unref(getBasicTransition)}
mode="out-in"
appear={true}
>
{() => PageContent}
</Transition>
) : (
PageContent
);
},
}}
</RouterView>
{unref(getCanEmbedIFramePage) && <FrameLayout />}
</>
);
};
},
});
<template>
<ParentLayout :isPage="true" />
<FrameLayout v-if="getCanEmbedIFramePage" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FrameLayout from '/@/layouts/iframe/index.vue';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import ParentLayout from '/@/layouts/parent/index.vue';
export default defineComponent({
components: { ParentLayout, FrameLayout },
setup() {
const { getCanEmbedIFramePage } = useRootSetting();
return { getCanEmbedIFramePage };
},
});
</script>
...@@ -10,9 +10,8 @@ export function useCache(isPage: boolean) { ...@@ -10,9 +10,8 @@ export function useCache(isPage: boolean) {
const name = ref(''); const name = ref('');
const { currentRoute } = useRouter(); const { currentRoute } = useRouter();
tryTsxEmit((instance: any) => { tryTsxEmit((instance) => {
const routeName = instance.ctx.$options.name; const routeName = instance.type.name;
if (routeName && ![ParentLayoutName].includes(routeName)) { if (routeName && ![ParentLayoutName].includes(routeName)) {
name.value = routeName; name.value = routeName;
} else { } else {
...@@ -22,6 +21,7 @@ export function useCache(isPage: boolean) { ...@@ -22,6 +21,7 @@ export function useCache(isPage: boolean) {
name.value = matched[len - 2].name as string; name.value = matched[len - 2].name as string;
} }
}); });
const { getOpenKeepAlive } = useRootSetting(); const { getOpenKeepAlive } = useRootSetting();
const getCaches = computed((): string[] => { const getCaches = computed((): string[] => {
......
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import { appStore } from '/@/store/modules/app';
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
export function useTransition() {
function handleAfterEnter() {
const { getOpenPageLoading, getEnableTransition } = useTransitionSetting();
if (!getOpenPageLoading.value || !getEnableTransition.value) return;
// Close loading after the route switching animation ends
appStore.setPageLoadingAction(false);
}
tryOnUnmounted(() => {
handleAfterEnter();
stop();
});
return {
onAfterEnter: handleAfterEnter,
};
}
import type { AppRouteRecordRaw } from '/@/router/types'; import type { AppRouteRecordRaw } from '/@/router/types';
import ParentLayout from '/@/layouts/parent/index.vue'; import ParentLayout from '/@/layouts/page/ParentView.vue';
const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception'); const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception');
...@@ -8,11 +8,6 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception'); ...@@ -8,11 +8,6 @@ const EXCEPTION_COMPONENT = () => import('../views/sys/exception/Exception');
*/ */
export const LAYOUT = () => import('/@/layouts/default/index'); export const LAYOUT = () => import('/@/layouts/default/index');
/**
* @description: page-layout
*/
export const PAGE_LAYOUT_COMPONENT = () => import('/@/layouts/page/index.vue');
/** /**
* @description: page-layout * @description: page-layout
*/ */
......
import type { Router } from 'vue-router'; import { isNavigationFailure, Router } from 'vue-router';
import { Modal, notification } from 'ant-design-vue'; import { Modal, notification } from 'ant-design-vue';
...@@ -8,7 +8,7 @@ import { createPageLoadingGuard } from './pageLoadingGuard'; ...@@ -8,7 +8,7 @@ import { createPageLoadingGuard } from './pageLoadingGuard';
import { useGlobSetting, useProjectSetting } from '/@/hooks/setting'; import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
import { getIsOpenTab, getRoute } from '/@/router/helper/routeHelper'; import { getRoute } from '/@/router/helper/routeHelper';
import { setTitle } from '/@/utils/browser'; import { setTitle } from '/@/utils/browser';
import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel'; import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel';
...@@ -24,13 +24,10 @@ export function createGuard(router: Router) { ...@@ -24,13 +24,10 @@ export function createGuard(router: Router) {
if (removeAllHttpPending) { if (removeAllHttpPending) {
axiosCanceler = new AxiosCanceler(); axiosCanceler = new AxiosCanceler();
} }
const loadedPageMap = new Map<string, boolean>();
createPageLoadingGuard(router);
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
// Determine whether the tab has been opened to.meta.loaded = !!loadedPageMap.get(to.path);
const isOpen = getIsOpenTab(to.fullPath);
to.meta.inTab = isOpen;
// Notify routing changes // Notify routing changes
tabStore.commitLastChangeRouteState(getRoute(to)); tabStore.commitLastChangeRouteState(getRoute(to));
...@@ -47,11 +44,17 @@ export function createGuard(router: Router) { ...@@ -47,11 +44,17 @@ export function createGuard(router: Router) {
return true; return true;
}); });
router.afterEach((to) => { router.afterEach((to, from, failure) => {
loadedPageMap.set(to.path, true);
const { t } = useI18n(); const { t } = useI18n();
// change html title // change html title
to.name !== REDIRECT_NAME && setTitle(t(to.meta.title), globSetting.title); to.name !== REDIRECT_NAME && setTitle(t(to.meta.title), globSetting.title);
if (isNavigationFailure(failure)) {
console.error('router navigation failed:', failure);
}
}); });
createPageLoadingGuard(router);
createProgressGuard(router); createProgressGuard(router);
createPermissionGuard(router); createPermissionGuard(router);
} }
import type { Router } from 'vue-router'; import type { Router } from 'vue-router';
import { tabStore } from '/@/store/modules/tab';
import { appStore } from '/@/store/modules/app'; import { appStore } from '/@/store/modules/app';
import { userStore } from '/@/store/modules/user'; import { userStore } from '/@/store/modules/user';
import { getParams } from '/@/router/helper/routeHelper';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import { unref } from 'vue'; import { unref } from 'vue';
const { getOpenPageLoading, getEnableTransition } = useTransitionSetting(); const { getOpenPageLoading } = useTransitionSetting();
export function createPageLoadingGuard(router: Router) { export function createPageLoadingGuard(router: Router) {
let isFirstLoad = true;
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
const { openKeepAlive, multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
if (!userStore.getTokenState) { if (!userStore.getTokenState) {
return true; return true;
} }
if (to.meta.loaded) {
if (!unref(getEnableTransition) && unref(getOpenPageLoading)) {
appStore.commitPageLoadingState(true);
return true; return true;
} }
if (show && openKeepAlive && !isFirstLoad) { if (unref(getOpenPageLoading)) {
const tabList = tabStore.getTabsState;
const isOpen = tabList.some((tab) => tab.path === to.path);
appStore.setPageLoadingAction(!isOpen);
} else {
appStore.setPageLoadingAction(true); appStore.setPageLoadingAction(true);
return true;
} }
return true; return true;
}); });
router.afterEach(async (to, from) => { router.afterEach(async () => {
const realToPath = to.path.replace(getParams(to), ''); if (unref(getOpenPageLoading)) {
const realFormPath = from.path.replace(getParams(from), '');
if (
(!unref(getEnableTransition) && unref(getOpenPageLoading)) ||
isFirstLoad ||
to.meta.afterCloseLoading ||
realToPath === realFormPath
) {
setTimeout(() => { setTimeout(() => {
appStore.commitPageLoadingState(false); appStore.commitPageLoadingState(false);
}, 110); }, 300);
isFirstLoad = false;
} }
return true; return true;
}); });
} }
...@@ -10,12 +10,14 @@ const { getOpenNProgress } = useTransitionSetting(); ...@@ -10,12 +10,14 @@ const { getOpenNProgress } = useTransitionSetting();
export function createProgressGuard(router: Router) { export function createProgressGuard(router: Router) {
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
!to.meta.inTab && unref(getOpenNProgress) && NProgress.start(); if (to.meta.loaded) return true;
unref(getOpenNProgress) && NProgress.start();
return true; return true;
}); });
router.afterEach(async (to) => { router.afterEach(async (to) => {
!to.meta.inTab && unref(getOpenNProgress) && NProgress.done(); if (to.meta.loaded) return true;
unref(getOpenNProgress) && NProgress.done();
return true; return true;
}); });
} }
import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types'; import type { AppRouteModule, AppRouteRecordRaw } from '/@/router/types';
import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'; import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
import { appStore } from '/@/store/modules/app';
import { tabStore } from '/@/store/modules/tab';
import { getParentLayout, LAYOUT } from '/@/router/constant'; import { getParentLayout, LAYOUT } from '/@/router/constant';
import dynamicImport from './dynamicImport'; import dynamicImport from './dynamicImport';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
...@@ -48,20 +46,6 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul ...@@ -48,20 +46,6 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
return (routeList as unknown) as T[]; return (routeList as unknown) as T[];
} }
/**
* Determine whether the tab has been opened
* @param toPath
*/
export function getIsOpenTab(toPath: string) {
const { openKeepAlive, multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
if (show && openKeepAlive) {
const tabList = tabStore.getTabsState;
return tabList.some((tab) => tab.path === toPath);
}
return false;
}
export function getParams(data: any = {}) { export function getParams(data: any = {}) {
const { params = {} } = data; const { params = {} } = data;
let ret = ''; let ret = '';
......
...@@ -33,4 +33,8 @@ export function setupRouter(app: App<Element>) { ...@@ -33,4 +33,8 @@ export function setupRouter(app: App<Element>) {
createGuard(router); createGuard(router);
} }
router.onError((error) => {
console.error(error);
});
export default router; export default router;
...@@ -27,8 +27,6 @@ export interface RouteMeta { ...@@ -27,8 +27,6 @@ export interface RouteMeta {
// close loading // close loading
afterCloseLoading?: boolean; afterCloseLoading?: boolean;
// Is it in the tab
inTab?: boolean;
// Carrying parameters // Carrying parameters
carryParam?: boolean; carryParam?: boolean;
......
...@@ -119,8 +119,6 @@ const setting: ProjectConfig = { ...@@ -119,8 +119,6 @@ const setting: ProjectConfig = {
canDrag: true, canDrag: true,
// Turn on quick actions // Turn on quick actions
showQuick: true, showQuick: true,
// Maximum number of tab cache
max: 12,
}, },
// Transition Setting // Transition Setting
......
...@@ -105,7 +105,7 @@ class App extends VuexModule { ...@@ -105,7 +105,7 @@ class App extends VuexModule {
// Prevent flicker // Prevent flicker
timeId = setTimeout(() => { timeId = setTimeout(() => {
this.commitPageLoadingState(loading); this.commitPageLoadingState(loading);
}, 100); }, 50);
} else { } else {
this.commitPageLoadingState(loading); this.commitPageLoadingState(loading);
clearTimeout(timeId); clearTimeout(timeId);
......
import { appStore } from './app';
import type { import type {
LoginParams, LoginParams,
GetUserInfoByUserIdModel, GetUserInfoByUserIdModel,
...@@ -107,12 +106,7 @@ class User extends VuexModule { ...@@ -107,12 +106,7 @@ class User extends VuexModule {
// const name = FULL_PAGE_NOT_FOUND_ROUTE.name; // const name = FULL_PAGE_NOT_FOUND_ROUTE.name;
// name && router.removeRoute(name); // name && router.removeRoute(name);
goHome && goHome && router.push(PageEnum.BASE_HOME);
(await router.push(PageEnum.BASE_HOME).then(() => {
setTimeout(() => {
appStore.commitPageLoadingState(false);
}, 30);
}));
return userInfo; return userInfo;
} catch (error) { } catch (error) {
return null; return null;
......
...@@ -29,8 +29,6 @@ export interface MultiTabsSetting { ...@@ -29,8 +29,6 @@ export interface MultiTabsSetting {
// 开启快速操作 // 开启快速操作
showQuick: boolean; showQuick: boolean;
canDrag: boolean; canDrag: boolean;
// 缓存最大数量
max: number;
} }
export interface HeaderSetting { export interface HeaderSetting {
......
...@@ -4,15 +4,11 @@ ...@@ -4,15 +4,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, unref } from 'vue'; import { defineComponent, unref } from 'vue';
import { appStore } from '/@/store/modules/app';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
export default defineComponent({ export default defineComponent({
name: 'Redirect', name: 'Redirect',
setup() { setup() {
const { currentRoute, replace } = useRouter(); const { currentRoute, replace } = useRouter();
const { getOpenPageLoading, getEnableTransition } = useTransitionSetting();
const { params, query } = unref(currentRoute); const { params, query } = unref(currentRoute);
const { path } = params; const { path } = params;
...@@ -21,12 +17,7 @@ ...@@ -21,12 +17,7 @@
path: '/' + _path, path: '/' + _path,
query, query,
}); });
// close loading
if (unref(getEnableTransition) && unref(getOpenPageLoading)) {
setTimeout(() => {
appStore.setPageLoadingAction(false);
}, 0);
}
return {}; return {};
}, },
}); });
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册