You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
220 lines
4.5 KiB
220 lines
4.5 KiB
<script setup lang="ts">
|
|
import { useAuthSession } from "~/composables/useAuthSession";
|
|
|
|
definePageMeta({ layout: false });
|
|
|
|
interface CharacterItem {
|
|
id: number;
|
|
slug: string;
|
|
name: string;
|
|
avatar?: string | null;
|
|
description?: string | null;
|
|
tags?: string[];
|
|
}
|
|
|
|
const auth = useAuthSession();
|
|
const { data } = await useHttpFetch("/api/characters");
|
|
const characters = computed<CharacterItem[]>(() => {
|
|
const raw = data.value as any;
|
|
return raw?.data?.list ?? raw?.list ?? [];
|
|
});
|
|
|
|
interface CharacterDetail {
|
|
slug: string;
|
|
name: string;
|
|
avatar?: string | null;
|
|
description?: string | null;
|
|
personality?: string | null;
|
|
scenario?: string | null;
|
|
firstMessage?: string | null;
|
|
exampleDialogue?: string | null;
|
|
tags?: string[];
|
|
}
|
|
|
|
const selectedCard = ref<CharacterDetail | null>(null);
|
|
const detailShow = ref(false);
|
|
|
|
async function openDetail(c: CharacterItem) {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: CharacterDetail }>(
|
|
`/api/characters/${c.slug}/info`,
|
|
);
|
|
selectedCard.value = res.data ?? null;
|
|
} catch {
|
|
selectedCard.value = {
|
|
slug: c.slug,
|
|
name: c.name,
|
|
avatar: c.avatar,
|
|
description: c.description,
|
|
tags: c.tags,
|
|
};
|
|
}
|
|
detailShow.value = true;
|
|
}
|
|
|
|
function handleStart(slug: string) {
|
|
detailShow.value = false;
|
|
navigateTo(`/characters/${slug}`);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="character-gallery">
|
|
<header class="gallery-header">
|
|
<div class="header-inner">
|
|
<NuxtLink to="/" class="brand">
|
|
<Icon name="lucide:users" class="brand-icon" />
|
|
<span class="brand-name">角色卡</span>
|
|
</NuxtLink>
|
|
<div class="header-actions">
|
|
<NuxtLink v-if="auth.user.value?.role === 'admin'" to="/admin/characters" class="header-link">
|
|
管理
|
|
</NuxtLink>
|
|
<NuxtLink v-if="!auth.loggedIn.value" to="/auth/login" class="header-link">登录</NuxtLink>
|
|
<NuxtLink v-else to="/" class="header-link">首页</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="gallery-main">
|
|
<div class="gallery-title">
|
|
<h1 class="title">选择一个角色,开始对话</h1>
|
|
<p class="subtitle">每个角色都有独立的对话与世界观设定</p>
|
|
</div>
|
|
|
|
<div v-if="characters.length === 0" class="empty">暂无可用角色</div>
|
|
<div v-else class="card-grid">
|
|
<button
|
|
v-for="c in characters"
|
|
:key="c.id"
|
|
class="card-link"
|
|
@click="openDetail(c)"
|
|
>
|
|
<CharacterCard :name="c.name" :avatar="c.avatar" :description="c.description" :tags="c.tags" />
|
|
</button>
|
|
</div>
|
|
</main>
|
|
|
|
<CharacterDetailModal v-model:show="detailShow" :card="selectedCard" @start="handleStart" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.character-gallery {
|
|
min-height: 100vh;
|
|
background: var(--color-canvas);
|
|
}
|
|
|
|
.gallery-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
background: var(--color-canvas);
|
|
border-bottom: 1px solid var(--color-hairline);
|
|
}
|
|
|
|
.header-inner {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 0 24px;
|
|
height: 64px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.brand-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.brand-name {
|
|
font-family: var(--font-display);
|
|
font-size: 20px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.header-link {
|
|
font-size: 14px;
|
|
color: var(--color-muted);
|
|
text-decoration: none;
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.header-link:hover {
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.gallery-main {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 48px 24px 80px;
|
|
}
|
|
|
|
.gallery-title {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.title {
|
|
font-family: var(--font-display);
|
|
font-size: 32px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
margin: 0 0 8px;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 15px;
|
|
color: var(--color-muted);
|
|
margin: 0;
|
|
}
|
|
|
|
.empty {
|
|
padding: 64px 0;
|
|
text-align: center;
|
|
color: var(--color-muted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.card-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.card-link {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
font: inherit;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.gallery-main {
|
|
padding: 24px 16px 48px;
|
|
}
|
|
.title {
|
|
font-size: 24px;
|
|
}
|
|
}
|
|
</style>
|
|
|