写作台适配手机:列表与章节工作区分屏切换
- 手机上章节列表与工作区分两屏,点章节进入工作区,左上角返回 - 列表页顶部提供「继续下一章」与流程说明;工作区顶栏使用紧凑版控制按钮 - 在列表页启动写作后自动进入即将处理的章节 - 精简标签页文字、正文留白与版本信息,避免小屏截断 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,10 +143,10 @@ async function saveSummary() {
|
|||||||
|
|
||||||
const tab = ref('text')
|
const tab = ref('text')
|
||||||
const tabs = computed<TabsItem[]>(() => [
|
const tabs = computed<TabsItem[]>(() => [
|
||||||
{ label: '正文', value: 'text', icon: 'i-lucide-file-text' },
|
{ label: '正文', value: 'text' },
|
||||||
{ label: '简报', value: 'brief', icon: 'i-lucide-clipboard-list', badge: chapter.value?.brief ? undefined : { label: '空', color: 'neutral', variant: 'subtle' } },
|
{ label: '简报', value: 'brief', badge: chapter.value?.brief ? undefined : { label: '空', color: 'neutral', variant: 'subtle' } },
|
||||||
{ label: `版本 (${chapter.value?.versions.length ?? 0})`, value: 'versions', icon: 'i-lucide-history' },
|
{ label: `版本 ${chapter.value?.versions.length ?? 0}`, value: 'versions' },
|
||||||
{ label: '审稿与摘要', value: 'review', icon: 'i-lucide-message-square-text' }
|
{ label: '审稿·摘要', value: 'review' }
|
||||||
])
|
])
|
||||||
|
|
||||||
// 没有正文时默认显示简报
|
// 没有正文时默认显示简报
|
||||||
@@ -243,8 +243,8 @@ const VERSION_STATUS: Record<string, { label: string, color: 'success' | 'warnin
|
|||||||
<!-- 正文 -->
|
<!-- 正文 -->
|
||||||
<div v-if="tab === 'text'" class="flex-1 min-h-0 flex flex-col gap-2">
|
<div v-if="tab === 'text'" class="flex-1 min-h-0 flex flex-col gap-2">
|
||||||
<div v-if="activeVersion && !isWriting" class="flex items-center gap-2 text-xs text-muted">
|
<div v-if="activeVersion && !isWriting" class="flex items-center gap-2 text-xs text-muted">
|
||||||
<span>v{{ activeVersion.version }} · {{ activeVersion.writerName }} · {{ activeVersion.modelLabel }} · {{ activeVersion.wordCount }} 字</span>
|
<span>v{{ activeVersion.version }} · {{ activeVersion.writerName }}<span class="hidden sm:inline"> · {{ activeVersion.modelLabel }}</span> · {{ activeVersion.wordCount }} 字</span>
|
||||||
<code class="truncate">{{ activeVersion.filePath }}</code>
|
<code class="truncate hidden sm:inline">{{ activeVersion.filePath }}</code>
|
||||||
<div class="ms-auto flex gap-1">
|
<div class="ms-auto flex gap-1">
|
||||||
<template v-if="editing">
|
<template v-if="editing">
|
||||||
<UButton label="取消" size="xs" color="neutral" variant="ghost" @click="editing = false" />
|
<UButton label="取消" size="xs" color="neutral" variant="ghost" @click="editing = false" />
|
||||||
@@ -257,7 +257,7 @@ const VERSION_STATUS: Record<string, { label: string, color: 'success' | 'warnin
|
|||||||
<div
|
<div
|
||||||
v-else-if="displayText"
|
v-else-if="displayText"
|
||||||
ref="textBox"
|
ref="textBox"
|
||||||
class="overflow-y-auto rounded-lg border border-default bg-default px-8 py-6 max-h-[calc(100vh-18rem)]"
|
class="overflow-y-auto rounded-lg border border-default bg-default px-4 py-4 sm:px-8 sm:py-6 max-h-[calc(100vh-18rem)]"
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
>
|
>
|
||||||
<NovelText :text="displayText" :writing="isWriting" class="max-w-3xl mx-auto" />
|
<NovelText :text="displayText" :writing="isWriting" class="max-w-3xl mx-auto" />
|
||||||
@@ -276,7 +276,7 @@ const VERSION_STATUS: Record<string, { label: string, color: 'success' | 'warnin
|
|||||||
<h3 class="font-medium">
|
<h3 class="font-medium">
|
||||||
章节简报
|
章节简报
|
||||||
</h3>
|
</h3>
|
||||||
<span class="text-xs text-muted">主编写给作家的执行指令,作家会严格按此写作</span>
|
<span class="text-xs text-muted hidden sm:inline">主编写给作家的执行指令,作家会严格按此写作</span>
|
||||||
<div class="ms-auto flex gap-1">
|
<div class="ms-auto flex gap-1">
|
||||||
<template v-if="editingBrief">
|
<template v-if="editingBrief">
|
||||||
<UButton label="取消" size="xs" color="neutral" variant="ghost" @click="editingBrief = false" />
|
<UButton label="取消" size="xs" color="neutral" variant="ghost" @click="editingBrief = false" />
|
||||||
@@ -343,7 +343,7 @@ const VERSION_STATUS: Record<string, { label: string, color: 'success' | 'warnin
|
|||||||
<h3 class="font-medium">
|
<h3 class="font-medium">
|
||||||
本章摘要
|
本章摘要
|
||||||
</h3>
|
</h3>
|
||||||
<span class="text-xs text-muted">确认章节时自动生成;主编和后续作家只会看到摘要而不是全文</span>
|
<span class="text-xs text-muted hidden sm:inline">确认章节时自动生成;主编和后续作家只会看到摘要而不是全文</span>
|
||||||
<UButton class="ms-auto" label="保存" icon="i-lucide-save" size="xs" variant="soft" :disabled="summaryDraft === chapter.summary" @click="saveSummary" />
|
<UButton class="ms-auto" label="保存" icon="i-lucide-save" size="xs" variant="soft" :disabled="summaryDraft === chapter.summary" @click="saveSummary" />
|
||||||
</div>
|
</div>
|
||||||
<UTextarea v-model="summaryDraft" autoresize :rows="6" class="w-full" placeholder="确认章节后自动生成" />
|
<UTextarea v-model="summaryDraft" autoresize :rows="6" class="w-full" placeholder="确认章节后自动生成" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/** 流水线控制:继续写作 / 停止 / 模式切换 */
|
/** 流水线控制:继续写作 / 停止 / 模式切换。compact 用于手机端顶栏,只保留主按钮 */
|
||||||
|
defineProps<{ compact?: boolean }>()
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const { project, running, runningLabel, connected } = store
|
const { project, running, runningLabel, connected } = store
|
||||||
|
|
||||||
@@ -19,24 +20,26 @@ async function run() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<UTooltip :text="connected ? '实时连接正常' : '实时连接断开,正在重连…'">
|
<template v-if="!compact">
|
||||||
<span class="size-2 rounded-full" :class="connected ? 'bg-success' : 'bg-error animate-pulse'" />
|
<UTooltip :text="connected ? '实时连接正常' : '实时连接断开,正在重连…'">
|
||||||
</UTooltip>
|
<span class="size-2 rounded-full" :class="connected ? 'bg-success' : 'bg-error animate-pulse'" />
|
||||||
<USwitch
|
</UTooltip>
|
||||||
:model-value="settings.autoMode"
|
<USwitch
|
||||||
label="自动模式"
|
:model-value="settings.autoMode"
|
||||||
:disabled="running"
|
label="自动模式"
|
||||||
size="sm"
|
:disabled="running"
|
||||||
@update:model-value="toggleAuto"
|
size="sm"
|
||||||
/>
|
@update:model-value="toggleAuto"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template v-if="running">
|
<template v-if="running">
|
||||||
<UBadge color="warning" variant="subtle" icon="i-lucide-loader-circle" :ui="{ leadingIcon: 'animate-spin' }" class="max-w-64 truncate">
|
<UBadge v-if="!compact" color="warning" variant="subtle" icon="i-lucide-loader-circle" :ui="{ leadingIcon: 'animate-spin' }" class="max-w-56 truncate">
|
||||||
{{ runningLabel || '运行中' }}
|
{{ runningLabel || '运行中' }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
<UButton icon="i-lucide-square" label="停止" color="error" variant="soft" size="sm" @click="store.stop()" />
|
<UButton icon="i-lucide-square" label="停止" color="error" variant="soft" size="sm" @click="store.stop()" />
|
||||||
</template>
|
</template>
|
||||||
<UTooltip v-else :text="pendingChapter ? `第${pendingChapter.index}章待确认,请先在章节中确认` : (settings.autoMode ? `自动连续写作,最多 ${settings.autoChapterLimit} 章` : '主编规划/写简报 → 作家写下一章,写完后暂停等你确认')">
|
<UTooltip v-else :text="pendingChapter ? `第${pendingChapter.index}章待确认,请先在章节中确认` : (settings.autoMode ? `自动连续写作,最多 ${settings.autoChapterLimit} 章` : '主编写简报 → 作家写下一章,写完后暂停等你确认')">
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-lucide-play"
|
icon="i-lucide-play"
|
||||||
:label="settings.autoMode ? '开始自动写作' : '继续下一章'"
|
:label="settings.autoMode ? '开始自动写作' : '继续下一章'"
|
||||||
@@ -46,5 +49,8 @@ async function run() {
|
|||||||
@click="run"
|
@click="run"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
|
<p v-if="!compact && !running && pendingChapter && !settings.autoMode" class="basis-full text-xs text-primary lg:hidden">
|
||||||
|
第{{ pendingChapter.index }}章已写完,点进去阅读并确认后才能继续
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ watch(() => chapters.value.find(c => c.status === 'writing')?.id, (id) => {
|
|||||||
if (id) selectedId.value = id
|
if (id) selectedId.value = id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 手机上列表和工作区分两屏:URL 带 ?c= 时显示章节工作区
|
||||||
|
const showDetail = computed(() => Boolean(route.query.c))
|
||||||
|
function backToList() {
|
||||||
|
const { c: _c, ...rest } = route.query
|
||||||
|
router.replace({ query: rest })
|
||||||
|
}
|
||||||
|
// 在列表页点「继续下一章」后,直接进入即将处理的章节
|
||||||
|
watch(running, (v) => {
|
||||||
|
if (!v || route.query.c) return
|
||||||
|
const next = chapters.value.find(c => c.status !== 'approved')
|
||||||
|
if (next) selectedId.value = next.id
|
||||||
|
})
|
||||||
|
const nextChapter = computed(() => chapters.value.find(c => c.status !== 'approved'))
|
||||||
|
|
||||||
const totalWords = computed(() => chapters.value.reduce((s, c) => s + c.wordCount, 0))
|
const totalWords = computed(() => chapters.value.reduce((s, c) => s + c.wordCount, 0))
|
||||||
const approvedCount = computed(() => chapters.value.filter(c => c.status === 'approved').length)
|
const approvedCount = computed(() => chapters.value.filter(c => c.status === 'approved').length)
|
||||||
|
|
||||||
@@ -48,7 +62,7 @@ function liveWords(chapterId: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardPanel id="chapter-list" :default-size="22" :min-size="16" :max-size="35" resizable>
|
<UDashboardPanel id="chapter-list" :default-size="22" :min-size="16" :max-size="35" resizable :class="showDetail ? 'hidden lg:flex' : ''">
|
||||||
<template #header>
|
<template #header>
|
||||||
<UDashboardNavbar title="章节">
|
<UDashboardNavbar title="章节">
|
||||||
<template #right>
|
<template #right>
|
||||||
@@ -65,6 +79,18 @@ function liveWords(chapterId: string) {
|
|||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
|
<!-- 手机端:写作入口与说明(桌面端在右侧顶部) -->
|
||||||
|
<div class="lg:hidden rounded-lg border border-default bg-elevated/40 p-3 mb-3 space-y-2">
|
||||||
|
<RunControls />
|
||||||
|
<p class="text-xs text-muted leading-relaxed">
|
||||||
|
<template v-if="nextChapter">
|
||||||
|
点「继续下一章」:主编为第{{ nextChapter.index }}章写简报 → 作家写正文 → 写完暂停等你确认。也可以点下面任意一章,手动生成简报、指定作家后再写。
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
点「继续下一章」,主编会先规划后续章节。
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="text-xs text-muted flex gap-3 px-1 -mt-1 mb-2">
|
<div class="text-xs text-muted flex gap-3 px-1 -mt-1 mb-2">
|
||||||
<span>{{ approvedCount }}/{{ chapters.length }} 章已确认</span>
|
<span>{{ approvedCount }}/{{ chapters.length }} 章已确认</span>
|
||||||
<span>{{ formatWords(totalWords) }}</span>
|
<span>{{ formatWords(totalWords) }}</span>
|
||||||
@@ -88,6 +114,7 @@ function liveWords(chapterId: string) {
|
|||||||
<span class="text-xs text-muted tabular-nums w-10 shrink-0">第{{ c.index }}章</span>
|
<span class="text-xs text-muted tabular-nums w-10 shrink-0">第{{ c.index }}章</span>
|
||||||
<span class="truncate text-sm font-medium flex-1">{{ c.title || '(未命名)' }}</span>
|
<span class="truncate text-sm font-medium flex-1">{{ c.title || '(未命名)' }}</span>
|
||||||
<UIcon v-if="c.status === 'writing'" name="i-lucide-loader-circle" class="size-3.5 animate-spin text-warning" />
|
<UIcon v-if="c.status === 'writing'" name="i-lucide-loader-circle" class="size-3.5 animate-spin text-warning" />
|
||||||
|
<UIcon name="i-lucide-chevron-right" class="size-4 text-muted lg:hidden" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 mt-1 ps-12">
|
<div class="flex items-center gap-2 mt-1 ps-12">
|
||||||
<UBadge :color="CHAPTER_STATUS_COLOR[c.status]" variant="subtle" size="sm">
|
<UBadge :color="CHAPTER_STATUS_COLOR[c.status]" variant="subtle" size="sm">
|
||||||
@@ -101,11 +128,19 @@ function liveWords(chapterId: string) {
|
|||||||
</template>
|
</template>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
|
||||||
<UDashboardPanel id="chapter-main">
|
<UDashboardPanel id="chapter-main" :class="showDetail ? '' : 'hidden lg:flex'">
|
||||||
<template #header>
|
<template #header>
|
||||||
<UDashboardNavbar :title="store.project.value?.title">
|
<UDashboardNavbar :title="store.project.value?.title" :ui="{ title: 'truncate' }">
|
||||||
|
<template #leading>
|
||||||
|
<UButton icon="i-lucide-arrow-left" label="章节" color="neutral" variant="ghost" size="sm" class="lg:hidden -ms-1.5" @click="backToList" />
|
||||||
|
</template>
|
||||||
<template #right>
|
<template #right>
|
||||||
<RunControls />
|
<div class="hidden lg:block">
|
||||||
|
<RunControls />
|
||||||
|
</div>
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<RunControls compact />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user