fix: 白板页签乐观显示 + 白板切换输入框 + /whiteboard/{id} 直链

- 存活逻辑改为乐观显示:探测中(pending)/存活(up) 都显示页签,仅明确
  不可达(down) 才隐藏。修复首屏看不到页签、需刷新才出现的问题
- 白板页加 ID 输入框与切换按钮,可切换不同白板,默认 share
- 路由支持 /whiteboard/:boardId 可选参数,直链访问自动填入输入框并切换
- 模块可声明 param 让路由表生成子路径,保持注册表驱动
This commit is contained in:
2026-07-22 02:43:26 +00:00
parent b5dcf924fb
commit 370388e729
7 changed files with 154 additions and 18 deletions

View File

@@ -1,23 +1,70 @@
<script setup>
import { ref, computed } from 'vue'
import { appConfig } from '../../config/app.config.js'
import { ref, computed, watch } from 'vue'
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'
const { t } = useLocale()
const route = useRoute()
const router = useRouter()
// 完整白板页 URL公开可被 iframe 嵌入)
const wbConfig = appConfig.whiteboard
const boardSrc = computed(
() => `${wbConfig.baseUrl}${wbConfig.pagePath(wbConfig.defaultBoardId)}`
// 当前白板 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
}
// iframe 加载失败(跨域无法读 error靠超时兜底
function scheduleFailCheck() {
setTimeout(() => {
if (!loaded.value) failed.value = true
@@ -25,7 +72,6 @@ function scheduleFailCheck() {
}
scheduleFailCheck()
// 外链打开(降级时提供)
function openExternal() {
window.open(boardSrc.value, '_blank', 'noopener')
}
@@ -34,10 +80,38 @@ function openExternal() {
<template>
<div class="wb-page">
<div class="wb-page__inner">
<h1 class="wb-page__title">{{ t('tabs.whiteboard') }}</h1>
<div class="wb-page__head">
<h1 class="wb-page__title">{{ t('tabs.whiteboard') }}</h1>
<form class="wb-page__switch" @submit.prevent="submitBoard">
<label class="wb-page__switch-label" for="boardInput">
{{ t('whiteboard.boardId') }}
</label>
<input
id="boardInput"
v-model="inputId"
class="wb-page__input"
type="text"
:placeholder="wbConfig.defaultBoardId"
spellcheck="false"
autocomplete="off"
autocapitalize="off"
/>
<button type="submit" class="wb-page__go">
{{ t('whiteboard.switch') }}
</button>
</form>
</div>
<!-- 降级iframe 长时间未加载完成 -->
<div v-if="failed" class="wb-page__fallback">
<!-- 服务不可达降级 -->
<div v-if="isDown" 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>
<!-- iframe 加载超时降级 -->
<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') }}
@@ -75,9 +149,53 @@ function openExternal() {
padding: 0 var(--space-lg);
box-sizing: border-box;
}
.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-bottom: var(--space-lg);
margin: 0;
}
.wb-page__switch {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
}
.wb-page__switch-label {
font-size: 0.85rem;
color: var(--text-secondary);
}
.wb-page__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-page__input:focus {
outline: none;
border-color: var(--accent);
}
.wb-page__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-page__go:hover {
background: var(--accent-hover);
}
.wb-page__frame {
position: relative;
@@ -89,6 +207,7 @@ function openExternal() {
/* 记事本以编辑为主,给足高度 */
height: 70vh;
min-height: 480px;
background: var(--surface);
}
.wb-page__iframe {
width: 100%;
@@ -144,6 +263,12 @@ function openExternal() {
}
@media (max-width: 768px) {
.wb-page__head {
align-items: flex-start;
}
.wb-page__input {
width: 120px;
}
.wb-page__frame {
height: 65vh;
min-height: 360px;