Files
short-drama-agent-front/src/features/projects/ProjectsPage.vue
T

163 lines
7.6 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">
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ArrowUpRight, Search, RefreshCw, Clapperboard } from '@lucide/vue'
import { usePolling } from '../../composables/usePolling'
import { EmptyState, StatusBadge } from '../../components/ui'
import { formatDate } from '../../lib/format'
import { projectsApi } from './api'
import CreateProjectDialog from './components/CreateProjectDialog.vue'
/** 项目索引:真实查询、客户端筛选,以及进入两条 graph 的入口。 */
const router = useRouter()
const query = usePolling(ref('projects'), (_, signal) => projectsApi.list(signal), 12_000)
const search = ref('')
const filter = ref('all')
const projects = computed(() => query.data.value ?? [])
const filtered = computed(() =>
projects.value.filter(item => {
const matches = `${item.title ?? ''} ${item.topic} ${item.style ?? ''}`
.toLowerCase()
.includes(search.value.toLowerCase().trim())
return matches && (filter.value === 'all' || item.status === filter.value)
})
)
/** 202 后直接打开工作流,后续刷新由项目布局负责。 */
function openProject(id: string) {
void router.push(`/projects/${encodeURIComponent(id)}/create-drama`)
}
/** 多项筛选的重置放在函数中,避免格式化后产生无效模板表达式。 */
function clearFilters() {
search.value = ''
filter.value = 'all'
}
</script>
<template>
<section class="page-container">
<div class="page-heading">
<div>
<p class="eyebrow">工作空间 / 项目</p>
<h1>我的剧本</h1>
<p class="page-description">从故事到镜头在这里继续你的创作</p>
</div>
<CreateProjectDialog @created="openProject" />
</div>
<div class="workspace-note">
<Clapperboard :size="20" :stroke-width="1.5" /><span
>剧本创作 <span class="mx-3 text-faint">/</span> 主体拆解
<span class="mx-3 text-faint">/</span> 分镜规划</span
><span class="ml-auto hidden text-xs text-muted sm:block">两个工作流一个项目</span>
</div>
<div class="toolbar mt-7">
<div class="flex flex-wrap gap-1">
<button
v-for="item in [
{ value: 'all', label: '全部项目' },
{ value: 'generating', label: '生成中' },
{ value: 'completed', label: '已完成' },
{ value: 'need_review', label: '待审核' },
{ value: 'failed', label: '失败' }
]"
:key="item.value"
class="filter-button"
:class="{ active: filter === item.value }"
:aria-pressed="filter === item.value"
@click="filter = item.value"
>
{{ item.label
}}<span v-if="item.value === 'all'" class="ml-2 text-muted">{{ projects.length }}</span>
</button>
</div>
<div class="flex items-center gap-2">
<div class="search-field">
<Search :size="15" /><input v-model="search" aria-label="搜索项目" placeholder="搜索剧本" />
</div>
<button
class="icon-button"
aria-label="刷新项目"
:disabled="query.loading.value"
@click="query.refresh"
>
<RefreshCw :size="16" :class="{ 'animate-spin': query.loading.value }" />
</button>
</div>
</div>
<div v-if="query.error.value" class="alert alert-error mt-4" role="alert">
{{ query.error.value }}<button class="ml-3 underline" @click="query.refresh">重新连接</button>
</div>
<div class="panel mt-4 overflow-hidden" :aria-busy="query.loading.value">
<div v-if="!query.data.value && query.loading.value" class="p-10 text-sm text-muted" role="status">
正在读取项目……
</div>
<div v-else-if="filtered.length" class="table-scroll">
<table class="project-table">
<thead>
<tr>
<th>剧本名称</th>
<th>风格</th>
<th>创作状态</th>
<th>最近更新</th>
<th><span class="sr-only">打开项目</span></th>
</tr>
</thead>
<tbody>
<tr v-for="project in filtered" :key="project.id">
<td>
<RouterLink :to="`/projects/${project.id}/create-drama`" class="project-name"
><span class="project-monogram">{{
(project.title || project.topic).slice(0, 1)
}}</span
><span class="min-w-0"
><strong class="block truncate font-medium">{{
project.title || project.topic
}}</strong
><span class="mt-1 block max-w-md truncate text-xs text-muted">{{
project.topic
}}</span></span
></RouterLink
>
</td>
<td class="text-muted">{{ project.style || '未设置' }}</td>
<td><StatusBadge :status="project.status" /></td>
<td class="whitespace-nowrap text-xs text-muted">{{ formatDate(project.updatedAt) }}</td>
<td>
<RouterLink
:to="`/projects/${project.id}/create-drama`"
class="icon-button"
:aria-label="`打开 ${project.title || project.topic}`"
><ArrowUpRight :size="17"
/></RouterLink>
</td>
</tr>
</tbody>
</table>
</div>
<EmptyState
v-else-if="query.data.value"
:title="projects.length ? '没有匹配的剧本' : '第一部故事,从这里开始'"
:description="
projects.length
? '试试其他关键词,或切换项目状态。'
: '新建一个剧本,生成角色与剧集;完成后,再将故事拆解为主体和分镜。'
"
><button v-if="projects.length" class="button button-secondary" @click="clearFilters">
清除筛选
</button></EmptyState
>
<EmptyState
v-else
title="等待连接后端"
description="启动后端服务并检查 API_PROXY_TARGET,连接成功后会显示你已有的项目。"
/>
</div>
<p class="mt-4 text-xs text-muted">
项目数据来自后端数据库<span v-if="query.updatedAt.value">
· 更新于 {{ formatDate(query.updatedAt.value) }}</span
>
</p>
</section>
</template>