3c9b65d982
- 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>
131 lines
5.3 KiB
Vue
131 lines
5.3 KiB
Vue
<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>
|