feat: 共享记事本前端 zWhiteBoard
textarea + WebSocket 实时同步、心跳、断线重连的便携式 Vue 组件。 复用 zTools2 的 /api/wb/* 与 /api/ws/wb/* 接口,与 zPDF_package 集成范式一致。 可独立运行或经 mainPage 构建期组件 import 集成(:locale + :boardId props)。
This commit is contained in:
187
src/components/WbEditor.vue
Normal file
187
src/components/WbEditor.vue
Normal 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>
|
||||
Reference in New Issue
Block a user