Files
short-drama-agent-front/src/features/breakdown/components/SubjectList.vue
T

172 lines
6.8 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 { NButton, NCollapse, NCollapseItem, NInput, NTag } from 'naive-ui'
import { computed, ref } from 'vue'
import { Search } from '@lucide/vue'
import { EmptyState } from '../../../components/ui'
import type { SubjectCandidate, SubjectForm } from '../types'
/** 主体列表按稳定 ref 展示;形态以 profileId 关联。 */
const props = defineProps<{ subjects: SubjectCandidate[]; forms: SubjectForm[]; projectId?: string }>()
const search = ref('')
/** 长篇主体经历默认展示摘要,展开不丢失原始正文。 */
const expanded = ref(new Set<string>())
function toggleDescription(id: string) {
if (expanded.value.has(id)) expanded.value.delete(id)
else expanded.value.add(id)
}
const filtered = computed(() =>
props.subjects.filter(item =>
`${item.name} ${item.ref} ${item.aliases?.join(' ') ?? ''}`
.toLowerCase()
.includes(search.value.trim().toLowerCase())
)
)
</script>
<template>
<div class="subject-list">
<!-- 以下是主体搜索 -->
<div class="subject-list-toolbar">
<NInput
class="subject-list-search"
placeholder="搜索名称或引用"
aria-label="搜索主体"
:input-props="{ 'aria-label': '搜索主体' }"
v-model:value="search"
clearable
><template #prefix><Search :size="14" aria-hidden="true" /></template
></NInput>
<p class="subject-list-count" role="status">
<template v-if="search.trim()">匹配 {{ filtered.length }} / {{ subjects.length }} 个主体</template>
<template v-else> {{ subjects.length }} 个主体</template>
</p>
</div>
<div v-if="filtered.length" class="record-list subject-record-list">
<article v-for="subject in filtered" :key="subject.profileId">
<div class="mb-3 flex flex-wrap items-center gap-3">
<h3 class="font-semibold">{{ subject.name }}</h3>
<code class="subject-ref">{{ subject.ref }}</code
><span v-if="subject.aliases?.length" class="text-xs text-muted"
>别名{{ subject.aliases.join('、') }}</span
>
</div>
<p
class="text-sm leading-7"
:class="{
'line-clamp-3': (subject.description?.length ?? 0) > 160 && !expanded.has(subject.profileId)
}"
>
{{ subject.description }}
</p>
<NButton
v-if="(subject.description?.length ?? 0) > 160"
text
size="small"
class="mt-2"
:aria-expanded="expanded.has(subject.profileId)"
@click="toggleDescription(subject.profileId)"
>{{ expanded.has(subject.profileId) ? '收起介绍' : '展开完整介绍' }}</NButton
>
<RouterLink
v-if="projectId"
:to="{ path: `/projects/${projectId}/subject-identity`, query: { subjectRef: subject.ref } }"
class="text-button mt-3 mr-4"
>管理稳定身份母版</RouterLink
>
<RouterLink
v-if="projectId"
:to="{ path: `/projects/${projectId}/subject-images`, query: { subjectRef: subject.ref } }"
class="text-button mt-3"
>查看形态图片生图</RouterLink
>
<NCollapse class="subject-details mt-4"
><NCollapseItem name="details"
><template #header
>外观描述与形态
<span class="ml-1 text-muted">{{
forms.filter(form => form.profileId === subject.profileId).length
}}</span></template
>
<p class="mt-3 whitespace-pre-wrap text-sm leading-7 text-muted">
{{ subject.appearance_prompt || '暂无外观描述' }}
</p>
<div
v-for="form in forms.filter(item => item.profileId === subject.profileId)"
:key="form.formId"
class="subject-form-card"
>
<p class="flex flex-wrap items-center gap-2 text-sm font-medium">
{{ form.formName || form.name
}}<NTag v-if="form.isDefault" size="small" :bordered="false">默认</NTag>
</p>
<p class="mt-2 text-sm leading-6 text-muted">{{ form.description }}</p>
<p
v-if="form.appearancePrompt?.trim() !== form.description?.trim()"
class="mt-2 text-xs leading-6 text-muted"
>
{{ form.appearancePrompt }}
</p>
</div></NCollapseItem
></NCollapse
>
</article>
</div>
<EmptyState
v-else
:title="subjects.length ? '没有匹配的主体' : '尚无主体结果'"
description="主体会在抽取、合并与档案整理完成后出现在这里。若未启用该模块,则不会生成此类主体。"
/>
</div>
</template>
<style>
/* 本组件专属布局与 Naive 内部结构覆盖。 */
@reference "../../../styles/styles.css";
.subject-record-list > article:nth-child(odd) {
@apply bg-(--app-control);
}
.subject-form-card {
@apply mt-3 p-4 min-w-0 wrap-anywhere bg-(--app-subtle);
}
.subject-record-list > article:nth-child(even) .subject-form-card {
@apply bg-(--app-control);
}
.subject-details summary {
@apply inline-flex items-center gap-[5px] text-ink text-[11px] list-none;
}
.subject-details summary::-webkit-details-marker {
@apply hidden;
}
.subject-details[open] summary svg {
@apply rotate-180;
}
.subject-list {
@apply p-5;
}
@media (min-width: 1024px) {
.subject-list {
@apply p-7;
}
}
.subject-list-toolbar {
@apply flex flex-wrap items-center justify-start gap-y-2.5 gap-x-3 mb-5;
}
.subject-list-search.n-input {
@apply flex-[0_1_260px] min-w-0 max-w-full;
}
.subject-list-count {
@apply shrink-0 text-muted text-xs whitespace-nowrap;
}
@media (max-width: 800px) {
.subject-list {
@apply p-3;
}
.subject-list-toolbar {
@apply flex-nowrap gap-2 mb-3;
}
.subject-list-search.n-input {
@apply flex-1 max-w-none;
}
}
</style>