refactor: 引入 PageShell 统一页面外壳,高内聚低耦合
- 新增可复用 PageShell 组件(titleKey/fluid/padded + actions 插槽), 统一容器宽度(1080px)、垂直节奏与标题,消除各模块重复包裹 - whiteboard/calendar/mobile-game 全部改用 PageShell,去掉各自手写的 .container/.__inner/.__title 重复样式;calendar 用 fluid 全宽变体 - timetable 改用 mainPage 侧包装器 Timetable.vue:import 子模块 App.vue 套 PageShell(对齐其他页宽度),并持有 KeepAlive 匹配名 name:'timetable' - 子模块 timeTableFix 的 App.vue 去掉 defineOptions(保持纯净),name 约定 由 mainPage 包装器持有,消除跨仓库脆弱约定;submodule 升级到 17aeea8 - 注册表清理:去掉无效 try/catch,order 缺省改 999(避免与 hidden 页 99 冲突), 注释文档化全部元数据字段与 KeepAlive 约定 - 新增页签照 timetable 模式复用 PageShell,注册方式一致
This commit is contained in:
75
src/components/ui/PageShell.vue
Normal file
75
src/components/ui/PageShell.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useLocale } from '../../composables/useLocale.js'
|
||||||
|
|
||||||
|
// ★ 可复用页面外壳 -- 统一页面的容器宽度、垂直节奏与标题,消除各模块重复包裹。
|
||||||
|
// 高内聚:所有页面布局令牌集中于此;低耦合:模块只声明 titleKey/fluid/padded,
|
||||||
|
// 不关心 .container 实现。新增页签照此复用,无需各写一套 wrapper。
|
||||||
|
//
|
||||||
|
// Props:
|
||||||
|
// titleKey: i18n 键(如 'tabs.whiteboard'),渲染为页面主标题;不传则无标题
|
||||||
|
// fluid: true 时突破 1080px 限制(全宽 + 左右 padding),如日历页
|
||||||
|
// padded: false 时去掉默认上下间距(页面完全自管布局时用)
|
||||||
|
const props = defineProps({
|
||||||
|
titleKey: { type: String, default: '' },
|
||||||
|
fluid: { type: Boolean, default: false },
|
||||||
|
padded: { type: Boolean, default: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useLocale()
|
||||||
|
const title = computed(() => (props.titleKey ? t(props.titleKey) : ''))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-shell" :class="{ 'page-shell--padded': padded }">
|
||||||
|
<div :class="fluid ? 'page-shell__fluid' : 'container page-shell__inner'">
|
||||||
|
<div v-if="title || $slots.actions" class="page-shell__head">
|
||||||
|
<h1 v-if="title" class="page-shell__title">{{ title }}</h1>
|
||||||
|
<div v-if="$slots.actions" class="page-shell__actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-shell--padded {
|
||||||
|
padding: var(--space-xl) 0 var(--space-2xl);
|
||||||
|
}
|
||||||
|
/* 标准容器宽度复用全局 .container(max-width 1080px,居中,左右 padding) */
|
||||||
|
.page-shell__inner {
|
||||||
|
/* 宽度由 .container 控制,此处仅作语义占位 */
|
||||||
|
}
|
||||||
|
/* 全宽变体:突破 1080px,仅留左右内边距(如日历页) */
|
||||||
|
.page-shell__fluid {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--space-lg);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.page-shell__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
.page-shell__title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.page-shell__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-shell__head {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
|
|||||||
// 组件名与模块 id 一致,供 <KeepAlive :include> 按 id 精确匹配
|
// 组件名与模块 id 一致,供 <KeepAlive :include> 按 id 精确匹配
|
||||||
defineOptions({ name: 'cqy' })
|
defineOptions({ name: 'cqy' })
|
||||||
import { useLocale } from '../../composables/useLocale.js'
|
import { useLocale } from '../../composables/useLocale.js'
|
||||||
|
import PageShell from '../../components/ui/PageShell.vue'
|
||||||
|
|
||||||
const { t } = useLocale()
|
const { t } = useLocale()
|
||||||
|
|
||||||
@@ -18,46 +19,29 @@ const loaded = ref(false)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="calendar-page">
|
<!-- 日历用全宽变体(fluid):突破 1080px,仅留左右内边距 -->
|
||||||
<div class="calendar-page__inner">
|
<PageShell title-key="tabs.calendar" fluid>
|
||||||
<h1 class="calendar-page__title">{{ t('tabs.calendar') }}</h1>
|
<div class="cal-frame">
|
||||||
<div class="calendar-page__frame">
|
|
||||||
<!-- 加载占位骨架(覆盖层,iframe 加载完成后淡出) -->
|
<!-- 加载占位骨架(覆盖层,iframe 加载完成后淡出) -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="!loaded" class="calendar-page__skeleton">
|
<div v-if="!loaded" class="cal-frame__skeleton">
|
||||||
<div class="calendar-page__skeleton-text">{{ t('common.loading') }}</div>
|
<div class="cal-frame__skeleton-text">{{ t('common.loading') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<iframe
|
<iframe
|
||||||
:src="calendarSrc"
|
:src="calendarSrc"
|
||||||
class="calendar-page__iframe"
|
class="cal-frame__iframe"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
:title="t('tabs.calendar')"
|
:title="t('tabs.calendar')"
|
||||||
@load="loaded = true"
|
@load="loaded = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageShell>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-page {
|
.cal-frame {
|
||||||
padding: var(--space-2xl) 0;
|
|
||||||
}
|
|
||||||
/* 日历页用全宽容器(不受全局 .container 的 1080px 限制),仅留左右内边距 */
|
|
||||||
.calendar-page__inner {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--space-lg);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.calendar-page__title {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
.calendar-page__frame {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -68,13 +52,13 @@ const loaded = ref(false)
|
|||||||
aspect-ratio: 16 / 10;
|
aspect-ratio: 16 / 10;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
.calendar-page__iframe {
|
.cal-frame__iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
.calendar-page__skeleton {
|
.cal-frame__skeleton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -83,7 +67,7 @@ const loaded = ref(false)
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
.calendar-page__skeleton-text {
|
.cal-frame__skeleton-text {
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
@@ -97,7 +81,7 @@ const loaded = ref(false)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.calendar-page__frame {
|
.cal-frame {
|
||||||
aspect-ratio: 4 / 3;
|
aspect-ratio: 4 / 3;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,28 @@
|
|||||||
// 自动生成统一的项目列表。新增项目页签 = 新建 modules/<新项目>/ 文件夹,
|
// 自动生成统一的项目列表。新增项目页签 = 新建 modules/<新项目>/ 文件夹,
|
||||||
// 无需改动导航/路由代码。
|
// 无需改动导航/路由代码。
|
||||||
//
|
//
|
||||||
// 每个模块 index.js 默认导出形如:
|
// 每个模块 index.js 默认导出元数据:
|
||||||
// {
|
// {
|
||||||
// id: 'mobile-game', // 唯一标识,同时用作路由 path
|
// id: 'mobile-game', // 必填:唯一标识,同时用作路由 path / KeepAlive 匹配名
|
||||||
// tabKey: 'tabs.mobileGame', // i18n 键,用于页签标题
|
// tabKey: 'tabs.mobileGame', // 必填:i18n 键,页签标题与 PageShell 标题均用此
|
||||||
// order: 1, // 页签排序
|
// component: () => import(...), // 必填:懒加载组件(函数形式,独立 chunk)
|
||||||
// component: () => import('./MobileGame.vue') // 懒加载组件
|
// order: 1, // 可选:页签排序,缺省 999(排在显式页签之后)
|
||||||
|
// hidden: true, // 可选:不在导航显示,但 URL 访问后保持可见(如 cqy)
|
||||||
|
// requiresAlive: 'url', // 可选:存活探针 URL,服务失活则隐藏页签并卸载组件
|
||||||
|
// param: 'boardId', // 可选:路由参数名,生成 /<id>/:<param>? 子路径
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// 容错:单个模块解析/导出异常时跳过该模块并告警,不影响其余页签加载。
|
// 容错:字段不完整的模块跳过并告警,单个模块异常不影响其余页签加载。
|
||||||
|
// KeepAlive 约定:组件需 defineOptions({ name: id }) 以匹配缓存名单(见 App.vue)。
|
||||||
|
|
||||||
const moduleFiles = import.meta.glob('./*/index.js', { eager: true })
|
const moduleFiles = import.meta.glob('./*/index.js', { eager: true })
|
||||||
|
|
||||||
const modules = Object.values(moduleFiles)
|
const modules = Object.values(moduleFiles)
|
||||||
.map((mod) => {
|
.map((mod) => mod?.default)
|
||||||
try {
|
|
||||||
return mod?.default
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[modules] 模块导出解析失败,已跳过:', e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
// 字段完整性兜底:缺 id/tabKey/component 的模块无法生成页签与路由,跳过
|
// 字段完整性兜底:缺 id/tabKey/component 的模块无法生成页签与路由,跳过
|
||||||
.filter((m) => m && m.id && m.tabKey && typeof m.component === 'function')
|
.filter((m) => m && m.id && m.tabKey && typeof m.component === 'function')
|
||||||
.sort((a, b) => (a.order ?? 99) - (b.order ?? 99))
|
// order 缺省 999:排在所有显式 order 页签之后(hidden 页常用 99 之类显式值)
|
||||||
|
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
||||||
|
|
||||||
export default modules
|
export default modules
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ defineOptions({ name: 'mobile-game' })
|
|||||||
import HeroSection from './components/HeroSection.vue'
|
import HeroSection from './components/HeroSection.vue'
|
||||||
import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
|
import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
|
||||||
import SubTabs from './components/SubTabs.vue'
|
import SubTabs from './components/SubTabs.vue'
|
||||||
|
import PageShell from '../../components/ui/PageShell.vue'
|
||||||
import { useLocale } from '../../composables/useLocale.js'
|
import { useLocale } from '../../composables/useLocale.js'
|
||||||
import { subTabs, defaultSubTab } from './config.js'
|
import { subTabs, defaultSubTab } from './config.js'
|
||||||
|
|
||||||
@@ -26,7 +27,9 @@ const ExperimentTab = defineAsyncComponent(() => import('./components/Experiment
|
|||||||
<div class="mobile-game">
|
<div class="mobile-game">
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
|
||||||
<div class="container">
|
<!-- 内容区复用 PageShell 统一容器宽度(无标题,Hero 即页头) -->
|
||||||
|
<PageShell :padded="false">
|
||||||
|
<div class="mg-blocks">
|
||||||
<!-- 概览 + 截图轮播(始终显示) -->
|
<!-- 概览 + 截图轮播(始终显示) -->
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<div class="block__grid">
|
<div class="block__grid">
|
||||||
@@ -58,6 +61,7 @@ const ExperimentTab = defineAsyncComponent(() => import('./components/Experiment
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</PageShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -65,6 +69,9 @@ const ExperimentTab = defineAsyncComponent(() => import('./components/Experiment
|
|||||||
.mobile-game {
|
.mobile-game {
|
||||||
padding-bottom: var(--space-2xl);
|
padding-bottom: var(--space-2xl);
|
||||||
}
|
}
|
||||||
|
.mg-blocks {
|
||||||
|
padding: var(--space-xl) 0;
|
||||||
|
}
|
||||||
.block {
|
.block {
|
||||||
padding: var(--space-xl) 0;
|
padding: var(--space-xl) 0;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|||||||
39
src/modules/timetable/Timetable.vue
Normal file
39
src/modules/timetable/Timetable.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
// ★ timetable 页签的 mainPage 侧包装器。
|
||||||
|
// 子模块 timeTableFix 的 App.vue 作为纯功能组件被 import,本包装器负责:
|
||||||
|
// 1. 套 PageShell 统一容器宽度与标题(与其他页签对齐)
|
||||||
|
// 2. 持有 KeepAlive 匹配用的组件名(与模块 id 一致),子模块自身不持 name
|
||||||
|
// 子模块独立运行时不经过本包装器,保持纯净。
|
||||||
|
defineOptions({ name: 'timetable' })
|
||||||
|
|
||||||
|
import PageShell from '../../components/ui/PageShell.vue'
|
||||||
|
import TimeTableFixApp from '../../../third_party/timeTableFix/src/App.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageShell title-key="tabs.timetable">
|
||||||
|
<div class="timetable__frame">
|
||||||
|
<TimeTableFixApp />
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 让子应用填满外壳容器,保留其内部 flex 布局自洽 */
|
||||||
|
.timetable__frame {
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
height: 75vh;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.timetable__frame {
|
||||||
|
height: 70vh;
|
||||||
|
min-height: 380px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
// 模块元数据 -- 日程整理(timeTableFix)页签
|
// 模块元数据 -- 日程整理(timeTableFix)页签
|
||||||
// 构建期组件集成:直接 import 子模块 timeTableFix 的 App.vue 作为懒加载路由组件。
|
// 构建期组件集成:mainPage 侧包装器 Timetable.vue import 子模块 App.vue,
|
||||||
// timeTableFix 作为 git submodule 位于 third_party/timeTableFix,独立维护、独立仓库。
|
// 套 PageShell 统一容器与标题。timeTableFix 作为 git submodule 独立维护。
|
||||||
// 升级:cd third_party/timeTableFix && git pull,回 mainPage 根 git add 该子模块并重新 build。
|
// 升级:cd third_party/timeTableFix && git pull,回 mainPage 根 git add 该子模块并重新 build。
|
||||||
export default {
|
export default {
|
||||||
id: 'timetable',
|
id: 'timetable',
|
||||||
tabKey: 'tabs.timetable',
|
tabKey: 'tabs.timetable',
|
||||||
order: 3,
|
order: 3,
|
||||||
// 懒加载子模块组件 -> 独立 chunk(含 ical.js,仅访问该页签时下载)
|
component: () => import('./Timetable.vue')
|
||||||
component: () => import('../../../third_party/timeTableFix/src/App.vue')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { appConfig, healthUrl } from '../../config/app.config.js'
|
import { appConfig, healthUrl } from '../../config/app.config.js'
|
||||||
import { useLocale } from '../../composables/useLocale.js'
|
import { useLocale } from '../../composables/useLocale.js'
|
||||||
import { getLiveness } from '../../composables/useLiveness.js'
|
import { getLiveness } from '../../composables/useLiveness.js'
|
||||||
|
import PageShell from '../../components/ui/PageShell.vue'
|
||||||
|
|
||||||
const { t } = useLocale()
|
const { t } = useLocale()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -81,96 +82,65 @@ function openExternal() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wb-page">
|
<PageShell title-key="tabs.whiteboard">
|
||||||
<div class="container wb-page__inner">
|
<template #actions>
|
||||||
<div class="wb-page__head">
|
<form class="wb-switch" @submit.prevent="submitBoard">
|
||||||
<h1 class="wb-page__title">{{ t('tabs.whiteboard') }}</h1>
|
<label class="wb-switch__label" for="boardInput">
|
||||||
<form class="wb-page__switch" @submit.prevent="submitBoard">
|
|
||||||
<label class="wb-page__switch-label" for="boardInput">
|
|
||||||
{{ t('whiteboard.boardId') }}
|
{{ t('whiteboard.boardId') }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="boardInput"
|
id="boardInput"
|
||||||
v-model="inputId"
|
v-model="inputId"
|
||||||
class="wb-page__input"
|
class="wb-switch__input"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="wbConfig.defaultBoardId"
|
:placeholder="wbConfig.defaultBoardId"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="wb-page__go">
|
<button type="submit" class="wb-switch__go">
|
||||||
{{ t('whiteboard.switch') }}
|
{{ t('whiteboard.switch') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- 服务不可达降级 -->
|
<!-- 服务不可达 / iframe 加载超时降级 -->
|
||||||
<div v-if="isDown" class="wb-page__fallback">
|
<div v-if="isDown || failed" class="wb-fallback">
|
||||||
<p class="wb-page__fallback-text">{{ t('common.serviceUnavailable') }}</p>
|
<p class="wb-fallback__text">{{ t('common.serviceUnavailable') }}</p>
|
||||||
<button type="button" class="wb-page__open" @click="openExternal">
|
<button type="button" class="wb-fallback__open" @click="openExternal">
|
||||||
{{ t('whiteboard.openExternal') }}
|
{{ t('whiteboard.openExternal') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- iframe 加载超时降级 -->
|
<div v-else class="wb-frame">
|
||||||
<div v-else-if="failed" class="wb-page__fallback">
|
|
||||||
<p class="wb-page__fallback-text">{{ t('common.serviceUnavailable') }}</p>
|
|
||||||
<button type="button" class="wb-page__open" @click="openExternal">
|
|
||||||
{{ t('whiteboard.openExternal') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="wb-page__frame">
|
|
||||||
<!-- 加载骨架(iframe 加载完成后淡出) -->
|
<!-- 加载骨架(iframe 加载完成后淡出) -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="!loaded" class="wb-page__skeleton">
|
<div v-if="!loaded" class="wb-frame__skeleton">
|
||||||
<div class="wb-page__skeleton-text">{{ t('common.loading') }}</div>
|
<div class="wb-frame__skeleton-text">{{ t('common.loading') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<iframe
|
<iframe
|
||||||
:src="boardSrc"
|
:src="boardSrc"
|
||||||
class="wb-page__iframe"
|
class="wb-frame__iframe"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
:title="t('tabs.whiteboard')"
|
:title="t('tabs.whiteboard')"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageShell>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.wb-page {
|
.wb-switch {
|
||||||
padding: var(--space-xl) 0 var(--space-2xl);
|
|
||||||
}
|
|
||||||
/* 内容区用全局 .container(max-width 1080px,左右 padding),
|
|
||||||
与移动游戏项目页保持一致的左右留白。 */
|
|
||||||
.wb-page__inner {
|
|
||||||
/* 仅补充容器内部纵向间距,宽度由 .container 控制 */
|
|
||||||
}
|
|
||||||
.wb-page__head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-md);
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
.wb-page__title {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.wb-page__switch {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
.wb-page__switch-label {
|
.wb-switch__label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
.wb-page__input {
|
.wb-switch__input {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -181,11 +151,11 @@ function openExternal() {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
transition: border-color var(--transition);
|
transition: border-color var(--transition);
|
||||||
}
|
}
|
||||||
.wb-page__input:focus {
|
.wb-switch__input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
.wb-page__go {
|
.wb-switch__go {
|
||||||
padding: 7px 14px;
|
padding: 7px 14px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -194,10 +164,10 @@ function openExternal() {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
transition: background var(--transition);
|
transition: background var(--transition);
|
||||||
}
|
}
|
||||||
.wb-page__go:hover {
|
.wb-switch__go:hover {
|
||||||
background: var(--accent-hover);
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
.wb-page__frame {
|
.wb-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -209,13 +179,13 @@ function openExternal() {
|
|||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
.wb-page__iframe {
|
.wb-frame__iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
.wb-page__skeleton {
|
.wb-frame__skeleton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -224,22 +194,22 @@ function openExternal() {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
.wb-page__skeleton-text {
|
.wb-frame__skeleton-text {
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
.wb-page__fallback {
|
.wb-fallback {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: var(--space-2xl) var(--space-lg);
|
padding: var(--space-2xl) var(--space-lg);
|
||||||
border: 1px dashed var(--border-strong);
|
border: 1px dashed var(--border-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
.wb-page__fallback-text {
|
.wb-fallback__text {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
.wb-page__open {
|
.wb-fallback__open {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
@@ -250,7 +220,7 @@ function openExternal() {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
transition: background var(--transition);
|
transition: background var(--transition);
|
||||||
}
|
}
|
||||||
.wb-page__open:hover {
|
.wb-fallback__open:hover {
|
||||||
background: var(--accent-hover);
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
@@ -263,13 +233,10 @@ function openExternal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.wb-page__head {
|
.wb-switch__input {
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.wb-page__input {
|
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
.wb-page__frame {
|
.wb-frame {
|
||||||
height: 65vh;
|
height: 65vh;
|
||||||
min-height: 360px;
|
min-height: 360px;
|
||||||
}
|
}
|
||||||
|
|||||||
2
third_party/timeTableFix
vendored
2
third_party/timeTableFix
vendored
Submodule third_party/timeTableFix updated: a49a725950...17aeea8359
Reference in New Issue
Block a user