Files
ai-novel-studio/app/pages/index.vue
T
jocay 3c9b65d982 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>
2026-10-18 02:04:40 +08:00

131 lines
5.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>