feat: 共享记事本前端 zWhiteBoard

textarea + WebSocket 实时同步、心跳、断线重连的便携式 Vue 组件。
复用 zTools2 的 /api/wb/* 与 /api/ws/wb/* 接口,与 zPDF_package 集成范式一致。
可独立运行或经 mainPage 构建期组件 import 集成(:locale + :boardId props)。
This commit is contained in:
2026-07-28 10:32:41 +08:00
commit d7d7450296
17 changed files with 4081 additions and 0 deletions

187
src/components/WbEditor.vue Normal file
View File

@@ -0,0 +1,187 @@
<script setup>
// ★ 记事本编辑器textarea + 工具栏 + 状态条。
// 与 WS 同步逻辑解耦 -- content/connected/statusText 由父组件App.vue经 props 传入,
// 编辑/clear/复制操作经 emit 上报,由父组件委托给 useWhiteboard 客户端。
// 这样 WS 逻辑可独立单测,组件仅负责 DOM 渲染与事件转发。
import { useLocale } from '../composables/useLocale.js'
const props = defineProps({
boardId: { type: String, default: '' },
modelValue: { type: String, default: '' }, // content
connected: { type: Boolean, default: false },
statusText: { type: String, default: '' },
})
const emit = defineEmits(['input', 'clear', 'copy-text', 'copy-link'])
const { t } = useLocale()
function onInput(e) {
emit('input', e.target.value)
}
</script>
<template>
<div class="zwb-editor">
<header class="zwb-bar">
<div class="zwb-bar-left">
<span class="zwb-title">{{ t('app.title') }}</span>
<code class="zwb-id" :title="t('board.idLabel')">{{ boardId }}</code>
<span class="zwb-online" :class="{ off: !connected }" title=""></span>
</div>
<div class="zwb-bar-right">
<button type="button" class="zwb-btn" :title="t('board.copyLink')" @click="emit('copy-link')">
{{ t('board.copyLink') }}
</button>
<button type="button" class="zwb-btn" :title="t('board.copyText')" @click="emit('copy-text')">
{{ t('board.copyText') }}
</button>
<button type="button" class="zwb-btn danger" :title="t('board.clear')" @click="emit('clear')">
{{ t('board.clear') }}
</button>
</div>
</header>
<main class="zwb-stage">
<textarea
class="zwb-textarea"
:value="modelValue"
:placeholder="t('board.placeholder')"
spellcheck="false"
autocomplete="off"
@input="onInput"
/>
<div v-if="statusText" class="zwb-status">{{ t(statusText) }}</div>
</main>
</div>
</template>
<style scoped>
/* scoped样式仅作用于本组件作为组件被嵌入时不污染宿主。
全局重置与设计令牌在 src/styles/global.css独立 app/ 宿主 :root嵌入时。 */
.zwb-editor {
display: flex;
flex-direction: column;
height: 100%;
background: var(--surface);
}
.zwb-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6em;
padding: 0.5em 0.9em;
background: var(--bg);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
flex-wrap: wrap;
flex-shrink: 0;
}
.zwb-bar-left,
.zwb-bar-right {
display: flex;
align-items: center;
gap: 0.6em;
}
.zwb-title {
font-weight: 700;
font-size: 1.05em;
color: var(--text-primary);
}
.zwb-id {
background: var(--surface-2);
padding: 0.15em 0.5em;
border-radius: 6px;
font-size: 0.82em;
color: var(--text-secondary);
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
}
.zwb-online {
color: var(--success);
font-size: 0.7em;
}
.zwb-online.off {
color: var(--text-tertiary);
}
.zwb-btn {
padding: 0.4em 0.9em;
font-size: 0.86em;
color: var(--text-primary);
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition), border-color var(--transition);
}
.zwb-btn:hover {
background: var(--surface-2);
}
.zwb-btn.danger {
color: var(--danger);
border-color: var(--danger-weak);
}
.zwb-btn.danger:hover {
background: var(--danger-weak);
}
.zwb-stage {
position: relative;
flex: 1;
overflow: hidden;
}
.zwb-textarea {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
resize: none;
border: none;
outline: none;
padding: 1em 1.2em;
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, "JetBrains Mono", monospace);
font-size: 14px;
line-height: 1.6;
background: var(--bg);
color: var(--text-primary);
touch-action: manipulation;
}
.zwb-textarea::placeholder {
color: var(--text-tertiary);
}
.zwb-status {
position: absolute;
bottom: 0.8em;
left: 50%;
transform: translateX(-50%);
background: var(--bg);
border: 1px solid var(--border);
padding: 0.3em 0.9em;
border-radius: 16px;
font-size: 0.8em;
color: var(--text-secondary);
box-shadow: var(--shadow);
pointer-events: none;
white-space: nowrap;
}
@media (max-width: 640px) {
.zwb-bar {
padding: 0.4em 0.5em;
gap: 0.4em;
}
.zwb-id {
max-width: 8em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.zwb-btn {
padding: 0.45em 0.7em;
}
.zwb-title {
display: none;
}
.zwb-textarea {
font-size: 15px;
padding: 0.8em;
}
}
</style>