AI 小说工坊:主编 + 作家多模型协作写作应用
- Nuxt 4 + Nuxt UI 4 + AI SDK 7 + Drizzle/SQLite - 主编规划大纲与章节简报、选派作家;作家流式写入章节文件并实时推送前端 - 逐章确认模式(默认)与自动模式;摘要与故事记忆 - 酒馆角色卡(PNG/JSON,V1-V3)与世界书导入 - 自定义最高优先级系统提示词、审稿/修改/多版本、任务日志与提示词检视 - 模拟 LLM 服务与端到端测试脚本 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
const api = useApi()
|
||||
const { data: projects, refresh } = await useAsyncData('projects', () => api.get<ProjectListItem[]>('/api/projects'), { default: () => [] })
|
||||
const { data: models } = await useAsyncData('models-count', () => api.get<ModelItem[]>('/api/models'), { default: () => [] })
|
||||
|
||||
const createOpen = ref(false)
|
||||
const form = reactive({ title: '', description: '' })
|
||||
const creating = ref(false)
|
||||
|
||||
async function create() {
|
||||
if (!form.title.trim()) return
|
||||
creating.value = true
|
||||
try {
|
||||
const { id } = await api.post<{ id: string }>('/api/projects', form)
|
||||
createOpen.value = false
|
||||
form.title = ''
|
||||
form.description = ''
|
||||
await navigateTo(`/projects/${id}/setting`)
|
||||
}
|
||||
finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleting = ref<ProjectListItem | null>(null)
|
||||
async function confirmDelete() {
|
||||
if (!deleting.value) return
|
||||
await api.del(`/api/projects/${deleting.value.id}`)
|
||||
deleting.value = null
|
||||
await refresh()
|
||||
}
|
||||
|
||||
function formatTime(t: string | number | Date) {
|
||||
return new Date(t).toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel id="projects">
|
||||
<template #header>
|
||||
<UDashboardNavbar title="我的作品">
|
||||
<template #right>
|
||||
<UButton icon="i-lucide-plus" label="新建作品" @click="createOpen = true" />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UAlert
|
||||
v-if="!models.length"
|
||||
color="warning"
|
||||
variant="subtle"
|
||||
icon="i-lucide-plug-zap"
|
||||
title="还没有配置模型"
|
||||
description="先去「模型与 API」添加一个 API 提供方和至少一个模型,主编和作家才能开始工作。"
|
||||
:actions="[{ label: '去配置', to: '/settings', color: 'warning', variant: 'solid' }]"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<div v-if="!projects.length" class="flex flex-col items-center justify-center py-24 text-center gap-3">
|
||||
<UIcon name="i-lucide-book-plus" class="size-12 text-muted" />
|
||||
<p class="text-muted">还没有作品,开始创作你的第一部小说吧</p>
|
||||
<UButton icon="i-lucide-plus" label="新建作品" @click="createOpen = true" />
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<UCard v-for="p in projects" :key="p.id" class="hover:ring-primary/50 transition" :ui="{ body: 'space-y-3' }">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<NuxtLink :to="`/projects/${p.id}`" class="font-semibold text-lg hover:text-primary line-clamp-1">
|
||||
{{ p.title }}
|
||||
</NuxtLink>
|
||||
<UDropdownMenu :items="[[{ label: '删除作品', icon: 'i-lucide-trash-2', color: 'error', onSelect: () => (deleting = p) }]]">
|
||||
<UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" size="sm" />
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
<p class="text-sm text-muted line-clamp-2 min-h-10">
|
||||
{{ p.description || '暂无简介' }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<UBadge variant="subtle" color="neutral">
|
||||
{{ p.stats.approved }}/{{ p.stats.total }} 章
|
||||
</UBadge>
|
||||
<UBadge variant="subtle" color="neutral">
|
||||
{{ (p.stats.words / 10000).toFixed(1) }} 万字
|
||||
</UBadge>
|
||||
<UBadge v-if="p.running" variant="subtle" color="warning" icon="i-lucide-loader-circle">
|
||||
{{ p.running.label }}
|
||||
</UBadge>
|
||||
<span class="ms-auto text-muted">{{ formatTime(p.updatedAt) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<UButton :to="`/projects/${p.id}`" label="写作台" icon="i-lucide-pen-line" size="sm" />
|
||||
<UButton :to="`/projects/${p.id}/setting`" label="设定" icon="i-lucide-book-open" size="sm" color="neutral" variant="outline" />
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
|
||||
<UModal v-model:open="createOpen" title="新建作品">
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<UFormField label="作品名" required>
|
||||
<UInput v-model="form.title" placeholder="例如:雨季来临之前" class="w-full" autofocus @keydown.enter="create" />
|
||||
</UFormField>
|
||||
<UFormField label="简介" hint="可选">
|
||||
<UTextarea v-model="form.description" :rows="3" autoresize class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer="{ close }">
|
||||
<div class="flex justify-end gap-2 w-full">
|
||||
<UButton label="取消" color="neutral" variant="outline" @click="close" />
|
||||
<UButton label="创建" :loading="creating" :disabled="!form.title.trim()" @click="create" />
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UModal :open="!!deleting" title="删除作品" @update:open="v => !v && (deleting = null)">
|
||||
<template #body>
|
||||
<p>确定要删除《{{ deleting?.title }}》吗?所有章节、设定和生成的文件都会被永久删除,无法恢复。</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2 w-full">
|
||||
<UButton label="取消" color="neutral" variant="outline" @click="deleting = null" />
|
||||
<UButton label="永久删除" color="error" @click="confirmDelete" />
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user