172 lines
6.8 KiB
Vue
172 lines
6.8 KiB
Vue
<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>
|