Files
zMainPage/src/modules/whiteboard/Whiteboard.vue
zikai a844f364dc 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,注册方式一致
2026-07-23 04:04:40 +00:00

245 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, computed, watch } from 'vue'
// 组件名与模块 id 一致,供 <KeepAlive :include> 按 id 精确匹配;
// 白板后端失活时把它从 include 移除即可卸载组件
defineOptions({ name: 'whiteboard' })
import { useRoute, useRouter } from 'vue-router'
import { appConfig, healthUrl } from '../../config/app.config.js'
import { useLocale } from '../../composables/useLocale.js'
import { getLiveness } from '../../composables/useLiveness.js'
import PageShell from '../../components/ui/PageShell.vue'
const { t } = useLocale()
const route = useRoute()
const router = useRouter()
const wbConfig = appConfig.whiteboard
// 当前白板 id优先取路由参数 /whiteboard/:boardId否则用配置默认值
function resolveBoardId(val) {
const id = String(val ?? '').trim()
// 仅允许白板服务接受的字符(与后端正则一致:字母数字 _ -),否则回退默认
return /^[a-zA-Z0-9_-]+$/.test(id) ? id : wbConfig.defaultBoardId
}
const currentBoardId = ref(resolveBoardId(route.params.boardId))
const inputId = ref(currentBoardId.value)
// 路由参数变化时(如浏览器前进/后退、直链进入)同步到当前白板与输入框
watch(
() => route.params.boardId,
(val) => {
const id = resolveBoardId(val)
if (id !== currentBoardId.value) {
currentBoardId.value = id
inputId.value = id
}
}
)
// 提交输入框:切换白板并更新 URLreplace避免历史栈堆积
function submitBoard() {
const id = resolveBoardId(inputId.value)
inputId.value = id
if (id !== currentBoardId.value) {
currentBoardId.value = id
router.replace(`/whiteboard/${encodeURIComponent(id)}`)
}
}
// 完整白板页 URL公开可被 iframe 嵌入);随 currentBoardId 响应式变化
const boardSrc = computed(
() => `${wbConfig.baseUrl}${wbConfig.pagePath(currentBoardId.value)}`
)
// 存活态:服务挂掉时显示降级提示(页签本身也已隐藏,但用户若直链进入仍可见)
const aliveState = getLiveness(healthUrl('whiteboard'))
const isDown = computed(() => aliveState.value === 'down')
const loaded = ref(false)
const failed = ref(false)
// 切换白板时重置加载态
watch(boardSrc, () => {
loaded.value = false
failed.value = false
scheduleFailCheck()
})
function onLoad() {
loaded.value = true
}
function scheduleFailCheck() {
setTimeout(() => {
if (!loaded.value) failed.value = true
}, 6000)
}
scheduleFailCheck()
function openExternal() {
window.open(boardSrc.value, '_blank', 'noopener')
}
</script>
<template>
<PageShell title-key="tabs.whiteboard">
<template #actions>
<form class="wb-switch" @submit.prevent="submitBoard">
<label class="wb-switch__label" for="boardInput">
{{ t('whiteboard.boardId') }}
</label>
<input
id="boardInput"
v-model="inputId"
class="wb-switch__input"
type="text"
:placeholder="wbConfig.defaultBoardId"
spellcheck="false"
autocomplete="off"
autocapitalize="off"
/>
<button type="submit" class="wb-switch__go">
{{ t('whiteboard.switch') }}
</button>
</form>
</template>
<!-- 服务不可达 / iframe 加载超时降级 -->
<div v-if="isDown || failed" class="wb-fallback">
<p class="wb-fallback__text">{{ t('common.serviceUnavailable') }}</p>
<button type="button" class="wb-fallback__open" @click="openExternal">
{{ t('whiteboard.openExternal') }}
</button>
</div>
<div v-else class="wb-frame">
<!-- 加载骨架iframe 加载完成后淡出 -->
<Transition name="fade">
<div v-if="!loaded" class="wb-frame__skeleton">
<div class="wb-frame__skeleton-text">{{ t('common.loading') }}</div>
</div>
</Transition>
<iframe
:src="boardSrc"
class="wb-frame__iframe"
frameborder="0"
:title="t('tabs.whiteboard')"
@load="onLoad"
/>
</div>
</PageShell>
</template>
<style scoped>
.wb-switch {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
}
.wb-switch__label {
font-size: 0.85rem;
color: var(--text-secondary);
}
.wb-switch__input {
width: 160px;
padding: 6px 10px;
font-size: 0.9rem;
font-family: var(--font-mono);
color: var(--text-primary);
background: var(--bg);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
transition: border-color var(--transition);
}
.wb-switch__input:focus {
outline: none;
border-color: var(--accent);
}
.wb-switch__go {
padding: 7px 14px;
font-size: 0.85rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: var(--radius-sm);
transition: background var(--transition);
}
.wb-switch__go:hover {
background: var(--accent-hover);
}
.wb-frame {
position: relative;
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid var(--border);
width: 100%;
/* 记事本以编辑为主,给足高度 */
height: 70vh;
min-height: 480px;
background: var(--surface);
}
.wb-frame__iframe {
width: 100%;
height: 100%;
display: block;
border: 0;
}
.wb-frame__skeleton {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface);
}
.wb-frame__skeleton-text {
color: var(--text-tertiary);
font-size: 0.95rem;
}
.wb-fallback {
text-align: center;
padding: var(--space-2xl) var(--space-lg);
border: 1px dashed var(--border-strong);
border-radius: var(--radius);
background: var(--surface);
}
.wb-fallback__text {
color: var(--text-secondary);
margin-bottom: var(--space-md);
}
.wb-fallback__open {
display: inline-flex;
align-items: center;
padding: 8px 18px;
font-size: 0.9rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: var(--radius-sm);
transition: background var(--transition);
}
.wb-fallback__open:hover {
background: var(--accent-hover);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@media (max-width: 768px) {
.wb-switch__input {
width: 120px;
}
.wb-frame {
height: 65vh;
min-height: 360px;
}
}
</style>