mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-03-15 04:53:18 +08:00
Enhance VndbPanel and related components with character and quote features
- Updated VndbPanel.vue to include new sections for displaying characters and quotes, enhancing the overall functionality. - Added support for fetching and displaying character details and quotes from the VNDB API. - Improved the UI with loading indicators and toggle buttons for expanded sections. - Refactored translation handling to support quotes and added a one-click translation feature for all content. - Cleaned up the codebase by optimizing imports and restructuring state management for better clarity.
This commit is contained in:
@@ -43,8 +43,11 @@ export interface VndbTag {
|
||||
export interface VndbTitleEntry {
|
||||
lang: string
|
||||
title: string
|
||||
official?: boolean
|
||||
main?: boolean
|
||||
}
|
||||
|
||||
|
||||
export interface VndbScreenshot {
|
||||
url: string
|
||||
sexual: number
|
||||
@@ -80,8 +83,10 @@ export interface VndbApiItem {
|
||||
export interface VndbInfo {
|
||||
id?: string
|
||||
names: string[]
|
||||
aliases?: string[]
|
||||
mainName: string
|
||||
originalTitle: string
|
||||
alttitle?: string
|
||||
mainImageUrl: string | null
|
||||
screenshotUrl: string | null
|
||||
screenshots: string[]
|
||||
@@ -105,6 +110,28 @@ export interface VndbInfo {
|
||||
languages?: string[]
|
||||
olang?: string
|
||||
devstatus?: number
|
||||
characters?: VndbCharacter[]
|
||||
quotes?: VndbQuote[]
|
||||
}
|
||||
|
||||
export interface VndbCharacter {
|
||||
id: string
|
||||
name: string
|
||||
original?: string
|
||||
image?: string
|
||||
sex?: string
|
||||
description?: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
export interface VndbQuote {
|
||||
id: string
|
||||
quote: string
|
||||
character?: {
|
||||
id: string
|
||||
name: string
|
||||
original?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,7 +318,7 @@ export async function fetchVndbData(gameName: string): Promise<VndbInfo | null>
|
||||
body: JSON.stringify({
|
||||
filters: ['search', '=', gameName],
|
||||
fields:
|
||||
'id, title, titles.lang, titles.title, description, image.url, image.sexual, image.violence, screenshots.url, screenshots.sexual, screenshots.violence, screenshots.votecount, length_minutes, length_votes, rating, average, votecount, released, developers.id, developers.name, developers.original, platforms, languages, olang, devstatus, tags.id, tags.name, tags.rating, tags.spoiler, tags.category, relations.id, relations.title, relations.relation, relations.relation_official, extlinks.url, extlinks.label, extlinks.name',
|
||||
'id, title, alttitle, aliases, titles{lang, title, official, main}, description, image{url, sexual, violence}, screenshots{url, sexual, violence, votecount}, length_minutes, length_votes, rating, average, votecount, released, developers{id, name, original}, platforms, languages, olang, devstatus, tags{id, name, rating, spoiler, category}, relations{id, title, relation, relation_official}, extlinks{url, label, name}, va{note, character{id, name, original}, staff{id, name, original}}',
|
||||
results: 1,
|
||||
}),
|
||||
})
|
||||
@@ -407,8 +434,20 @@ export async function fetchVndbData(gameName: string): Promise<VndbInfo | null>
|
||||
}))
|
||||
: []
|
||||
|
||||
// 声优信息暂时不从 API 获取(需要单独查询 POST /character)
|
||||
const va: VndbVoiceActor[] = []
|
||||
// 提取声优信息
|
||||
const va: VndbVoiceActor[] = result.va
|
||||
? result.va.map(
|
||||
(v: {
|
||||
note: string | null
|
||||
character: { id: string; name: string; original?: string }
|
||||
staff: { id: string; name: string; original?: string }
|
||||
}) => ({
|
||||
note: v.note,
|
||||
character: v.character,
|
||||
staff: v.staff,
|
||||
}),
|
||||
)
|
||||
: []
|
||||
|
||||
// 提取相关作品
|
||||
const relations: VndbRelation[] = result.relations
|
||||
@@ -429,11 +468,16 @@ export async function fetchVndbData(gameName: string): Promise<VndbInfo | null>
|
||||
}))
|
||||
: []
|
||||
|
||||
// 提取别名
|
||||
const aliases: string[] = result.aliases || []
|
||||
|
||||
const finalResult: VndbInfo = {
|
||||
id: result.id || undefined,
|
||||
names: [...new Set(names)],
|
||||
aliases: aliases.length > 0 ? aliases : undefined,
|
||||
mainName,
|
||||
originalTitle: result.title,
|
||||
alttitle: result.alttitle || undefined,
|
||||
mainImageUrl,
|
||||
screenshotUrl,
|
||||
screenshots,
|
||||
@@ -473,19 +517,201 @@ export async function fetchVndbData(gameName: string): Promise<VndbInfo | null>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 VNDB 角色列表
|
||||
* @param vnId - 游戏 ID(如 "v19073")
|
||||
* @returns 角色列表
|
||||
*/
|
||||
export async function fetchVndbCharacters(vnId: string): Promise<VndbCharacter[]> {
|
||||
try {
|
||||
const response = await fetch(`${VNDB_API_BASE_URL}/character`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
filters: ['vn', '=', ['id', '=', vnId]],
|
||||
fields: 'id, name, original, image{url, sexual, violence}, sex, description, age',
|
||||
results: 15,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`VNDB Character API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.results || data.results.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 转换角色数据,过滤不安全图片
|
||||
const characters: VndbCharacter[] = data.results.map(
|
||||
(c: {
|
||||
id: string
|
||||
name: string
|
||||
original?: string
|
||||
image?: { url: string; sexual: number; violence: number }
|
||||
sex?: [string, string]
|
||||
description?: string
|
||||
age?: number
|
||||
}) => {
|
||||
let imageUrl: string | undefined
|
||||
if (c.image && c.image.url && c.image.sexual <= 1 && c.image.violence === 0) {
|
||||
imageUrl = c.image.url
|
||||
}
|
||||
|
||||
return {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
original: c.original,
|
||||
image: imageUrl,
|
||||
sex: c.sex?.[0],
|
||||
description: c.description,
|
||||
age: c.age,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 使用代理替换 URL
|
||||
if (ENABLE_VNDB_IMAGE_PROXY && isProxyAvailable) {
|
||||
characters.forEach((char) => {
|
||||
if (char.image) {
|
||||
char.image = char.image.replace('https://t.vndb.org/', VNDB_IMAGE_PROXY)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return characters
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 VNDB 名言列表
|
||||
* @param vnId - 游戏 ID(如 "v19073")
|
||||
* @returns 名言列表
|
||||
*/
|
||||
export async function fetchVndbQuotes(vnId: string): Promise<VndbQuote[]> {
|
||||
try {
|
||||
const response = await fetch(`${VNDB_API_BASE_URL}/quote`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
filters: ['vn', '=', ['id', '=', vnId]],
|
||||
fields: 'id, quote, character{id, name, original}',
|
||||
results: 10,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`VNDB Quote API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.results || data.results.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 转换名言数据
|
||||
const quotes: VndbQuote[] = data.results.map(
|
||||
(q: {
|
||||
id: string
|
||||
quote: string
|
||||
character?: { id: string; name: string; original?: string }
|
||||
}) => ({
|
||||
id: q.id,
|
||||
quote: q.quote,
|
||||
character: q.character,
|
||||
}),
|
||||
)
|
||||
|
||||
return quotes
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 翻译模式类型
|
||||
export type TranslateMode = 'description' | 'tags' | 'quotes'
|
||||
|
||||
// 简介翻译提示词
|
||||
const DESCRIPTION_PROMPT = `你是一名专业的视觉小说(Galgame/AVG)本地化专家。请将游戏简介精准翻译为简体中文。
|
||||
|
||||
【翻译规范】
|
||||
1. 格式净化:清除所有 HTML、Markdown、BBCode 等标记,仅保留纯文本
|
||||
2. 结构保留:保持原文段落划分,段落间用换行分隔
|
||||
3. 术语处理:使用视觉小说领域通用的中文术语
|
||||
4. 人名处理:优先使用中文圈广泛接受的译名,无通用译名时保留原名
|
||||
5. 内容控制:禁止添加剧透、解释性文字或主观评价
|
||||
|
||||
【输出要求】
|
||||
仅输出翻译后的纯文本,无需任何说明`
|
||||
|
||||
// 标签翻译提示词
|
||||
const TAGS_PROMPT = `你是一名视觉小说(Galgame/AVG)专家,精通 VNDB 标签体系。请将以下游戏标签翻译为简体中文。
|
||||
|
||||
【输入格式】
|
||||
每行一个英文标签
|
||||
|
||||
【翻译规范】
|
||||
1. 使用 VNDB 中文社区或 Bangumi 等平台的通用译法
|
||||
2. 游戏机制类标签直译(如 Multiple Endings → 多结局)
|
||||
3. 角色属性类标签使用二次元圈常用说法(如 Tsundere → 傲娇)
|
||||
4. 无通用译法的专有名词可保留英文或音译
|
||||
5. 保持简洁,每个标签译文不超过 10 个字
|
||||
|
||||
【输出要求】
|
||||
每行输出一个翻译结果,与输入行数严格一一对应
|
||||
仅输出译文,无需编号、原文或解释`
|
||||
|
||||
// 名言翻译提示词
|
||||
const QUOTES_PROMPT = `你是一名专业的视觉小说(Galgame/AVG)本地化专家。请将游戏中的经典台词/名言翻译为简体中文。
|
||||
|
||||
【输入格式】
|
||||
每行一条英文/日文台词
|
||||
|
||||
【翻译规范】
|
||||
1. 保留原文的情感色彩和语气特点
|
||||
2. 台词中的人名保留原文或使用通用译名
|
||||
3. 注意口语化表达,符合角色说话习惯
|
||||
4. 保持原文的文学性和感染力
|
||||
5. 不要添加引号或其他标点修饰
|
||||
|
||||
【输出要求】
|
||||
每行输出一条翻译结果,与输入行数严格一一对应
|
||||
仅输出译文,无需编号、原文或解释`
|
||||
|
||||
/**
|
||||
* AI 翻译文本
|
||||
* @param text - 要翻译的文本
|
||||
* @param mode - 翻译模式:description(简介)、tags(标签)或 quotes(名言)
|
||||
* @param maxRetries - 最大重试次数
|
||||
* @returns 翻译后的文本,失败返回 null
|
||||
*/
|
||||
export async function translateText(text: string, maxRetries: number = 2): Promise<string | null> {
|
||||
export async function translateText(
|
||||
text: string,
|
||||
mode: TranslateMode = 'description',
|
||||
maxRetries: number = 2,
|
||||
): Promise<string | null> {
|
||||
if (!text || text.trim().length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 限制文本长度,避免超出 API 限制
|
||||
const maxLength = 3000
|
||||
// 根据模式选择提示词和参数
|
||||
const modeConfig = {
|
||||
description: { prompt: DESCRIPTION_PROMPT, maxLength: 3000, maxTokens: 2000, temperature: 0.3 },
|
||||
tags: { prompt: TAGS_PROMPT, maxLength: 1500, maxTokens: 1000, temperature: 0.2 },
|
||||
quotes: { prompt: QUOTES_PROMPT, maxLength: 2000, maxTokens: 1500, temperature: 0.4 },
|
||||
}
|
||||
|
||||
const config = modeConfig[mode]
|
||||
const systemPrompt = config.prompt
|
||||
const maxLength = config.maxLength
|
||||
const maxTokens = config.maxTokens
|
||||
const temperature = config.temperature
|
||||
|
||||
const textToTranslate = text.length > maxLength ? text.substring(0, maxLength) + '...' : text
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
@@ -501,35 +727,15 @@ export async function translateText(text: string, maxRetries: number = 2): Promi
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `你是一名专业的视觉小说游戏本地化专家,请将提供的游戏简介精准翻译为简体中文。
|
||||
|
||||
【翻译规范】
|
||||
|
||||
1. 格式净化:翻译前完全清除所有HTML、Markdown、XML等非内容格式标记,仅保留原始文本内容
|
||||
|
||||
2. 结构保留:完整保持原文段落划分,段落间以自然换行分隔
|
||||
|
||||
3. 术语统一:准确处理视觉小说领域的专业术语,确保表述一致
|
||||
|
||||
4. 人名规范:
|
||||
- 保留原名拼写
|
||||
- 或采用中文圈广泛接受的译名
|
||||
- 同一作品内译名保持统一
|
||||
|
||||
5. 内容控制:
|
||||
- 严禁添加原文未包含的剧透信息
|
||||
- 禁止插入解释性文字或主观评价
|
||||
- 杜绝文化偏见性表述
|
||||
|
||||
6. 输出要求:仅输出翻译后的纯文本内容,无需任何说明性文字`,
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: textToTranslate,
|
||||
},
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 2000,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
stream: false,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -379,6 +379,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 右侧:操作按钮 -->
|
||||
<div class="toolbar-right">
|
||||
<!-- 缩放按钮 -->
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
title="放大 (+)"
|
||||
@@ -394,7 +395,7 @@ onUnmounted(() => {
|
||||
<ZoomOut :size="20" />
|
||||
</button>
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
class="toolbar-btn hidden-mobile"
|
||||
title="1:1"
|
||||
@click="handleToggleZoom"
|
||||
>
|
||||
@@ -403,29 +404,30 @@ onUnmounted(() => {
|
||||
|
||||
<div class="toolbar-divider" />
|
||||
|
||||
<!-- 旋转翻转按钮 - 仅桌面端显示 -->
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
class="toolbar-btn hidden-mobile"
|
||||
title="逆时针旋转 (Shift+R)"
|
||||
@click="handleRotateCCW"
|
||||
>
|
||||
<RotateCcw :size="20" />
|
||||
</button>
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
class="toolbar-btn hidden-mobile"
|
||||
title="顺时针旋转 (R)"
|
||||
@click="handleRotateCW"
|
||||
>
|
||||
<RotateCw :size="20" />
|
||||
</button>
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
class="toolbar-btn hidden-mobile"
|
||||
title="水平翻转"
|
||||
@click="handleFlipH"
|
||||
>
|
||||
<FlipHorizontal :size="20" />
|
||||
</button>
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
class="toolbar-btn hidden-mobile"
|
||||
title="垂直翻转"
|
||||
@click="handleFlipV"
|
||||
>
|
||||
@@ -434,8 +436,9 @@ onUnmounted(() => {
|
||||
|
||||
<div class="toolbar-divider" />
|
||||
|
||||
<!-- 下载和关闭 -->
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
class="toolbar-btn hidden-mobile"
|
||||
title="下载"
|
||||
@click="handleDownload"
|
||||
>
|
||||
@@ -570,6 +573,7 @@ onUnmounted(() => {
|
||||
|
||||
.image-viewer-toolbar.top {
|
||||
top: 0;
|
||||
padding-top: max(12px, env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.image-viewer-toolbar.bottom {
|
||||
@@ -777,15 +781,36 @@ onUnmounted(() => {
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.image-viewer-toolbar {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.image-viewer-toolbar.top {
|
||||
padding-top: max(8px, env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.toolbar-btn.hidden-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.image-counter {
|
||||
font-size: 12px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 60px;
|
||||
@@ -798,6 +823,11 @@ onUnmounted(() => {
|
||||
|
||||
.image-caption {
|
||||
font-size: 13px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.image-viewer-content {
|
||||
padding: 50px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,30 @@
|
||||
|
||||
<!-- 右侧按钮组 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 一键翻译按钮 -->
|
||||
<button
|
||||
v-if="!hasAnyTranslation"
|
||||
class="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium transition-all"
|
||||
:class="isTranslatingAll
|
||||
? 'bg-gray-200 dark:bg-slate-700 text-gray-500 dark:text-slate-400 cursor-wait'
|
||||
: 'text-white bg-gradient-to-r from-violet-500 to-purple-600 shadow-lg shadow-violet-500/25 hover:shadow-xl'"
|
||||
:disabled="isTranslatingAll"
|
||||
@click="handleTranslateAll"
|
||||
>
|
||||
<Loader v-if="isTranslatingAll" :size="14" class="animate-spin" />
|
||||
<Bot v-else :size="14" />
|
||||
<span class="hidden sm:inline">{{ isTranslatingAll ? '翻译中...' : 'AI 翻译' }}</span>
|
||||
</button>
|
||||
<!-- 翻译完成后的切换按钮 -->
|
||||
<button
|
||||
v-else
|
||||
class="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-violet-600 dark:text-violet-400 bg-violet-100 dark:bg-violet-900/30 hover:bg-violet-200 dark:hover:bg-violet-900/50 transition-colors"
|
||||
@click="toggleAllTranslations"
|
||||
>
|
||||
<ArrowLeftRight :size="14" />
|
||||
<span class="hidden sm:inline">{{ showOriginal ? '译文' : '原文' }}</span>
|
||||
</button>
|
||||
|
||||
<!-- VNDB 链接按钮 -->
|
||||
<a
|
||||
:href="vndbUrl"
|
||||
@@ -108,19 +132,32 @@
|
||||
</h2>
|
||||
|
||||
<!-- 原名 -->
|
||||
<p v-if="searchStore.vndbInfo.originalTitle" class="text-sm text-gray-500 dark:text-slate-400 text-center mb-4">
|
||||
<p v-if="searchStore.vndbInfo.originalTitle" class="text-sm text-gray-500 dark:text-slate-400 text-center mb-1">
|
||||
{{ searchStore.vndbInfo.originalTitle }}
|
||||
</p>
|
||||
|
||||
<!-- 罗马音 -->
|
||||
<p v-if="searchStore.vndbInfo.alttitle && searchStore.vndbInfo.alttitle !== searchStore.vndbInfo.originalTitle" class="text-xs text-gray-400 dark:text-slate-500 text-center mb-4 italic">
|
||||
{{ searchStore.vndbInfo.alttitle }}
|
||||
</p>
|
||||
<div v-else class="mb-3" />
|
||||
|
||||
<!-- 别名标签 -->
|
||||
<div v-if="searchStore.vndbInfo.names.length > 1" class="flex flex-wrap justify-center gap-2">
|
||||
<span
|
||||
v-for="(name, index) in searchStore.vndbInfo.names.slice(0, 5)"
|
||||
v-for="(name, index) in (expandedSections.names ? searchStore.vndbInfo.names : searchStore.vndbInfo.names.slice(0, 5))"
|
||||
:key="index"
|
||||
class="px-3 py-1 bg-pink-100 dark:bg-pink-900/30 text-[#ff1493] dark:text-[#ff69b4] text-xs rounded-full"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
<button
|
||||
v-if="searchStore.vndbInfo.names.length > 5"
|
||||
class="px-3 py-1 bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-slate-300 text-xs rounded-full hover:bg-gray-200 dark:hover:bg-slate-600 transition-colors"
|
||||
@click="toggleSection('names')"
|
||||
>
|
||||
{{ expandedSections.names ? '收起' : `+${searchStore.vndbInfo.names.length - 5}` }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -254,38 +291,8 @@
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">标签</h3>
|
||||
<span class="text-xs text-gray-400 dark:text-slate-500">(按相关性排序)</span>
|
||||
</div>
|
||||
<!-- 翻译按钮 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 切换原文/翻译 -->
|
||||
<button
|
||||
v-if="translatedTags.size > 0"
|
||||
class="flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-lg transition-colors"
|
||||
:class="showOriginalTags
|
||||
? 'bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-slate-300'
|
||||
: 'bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400'"
|
||||
@click="toggleTagsLanguage"
|
||||
>
|
||||
<Languages :size="14" />
|
||||
{{ showOriginalTags ? '原文' : '中文' }}
|
||||
</button>
|
||||
<!-- 翻译按钮 -->
|
||||
<button
|
||||
v-if="translatedTags.size === 0"
|
||||
class="flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-lg transition-colors"
|
||||
:class="isTranslatingTags
|
||||
? 'bg-gray-100 dark:bg-slate-700 text-gray-400 dark:text-slate-500 cursor-wait'
|
||||
: translateTagsError
|
||||
? 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400'
|
||||
: 'bg-gradient-to-r from-violet-500 to-purple-500 text-white shadow-sm hover:shadow-md'"
|
||||
:disabled="isTranslatingTags"
|
||||
@click="handleTranslateTags"
|
||||
>
|
||||
<Loader v-if="isTranslatingTags" :size="14" class="animate-spin" />
|
||||
<AlertTriangle v-else-if="translateTagsError" :size="14" />
|
||||
<Bot v-else :size="14" />
|
||||
{{ isTranslatingTags ? '翻译中...' : translateTagsError ? '重试' : 'AI 翻译' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 翻译中指示器 -->
|
||||
<Loader v-if="isTranslatingTags" :size="14" class="animate-spin text-violet-500" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
@@ -321,27 +328,31 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<a
|
||||
v-for="(voiceActor, index) in searchStore.vndbInfo.va.slice(0, 10)"
|
||||
v-for="(voiceActor, index) in (expandedSections.va ? searchStore.vndbInfo.va : searchStore.vndbInfo.va.slice(0, 10))"
|
||||
:key="index"
|
||||
:href="`https://vndb.org/${voiceActor.id}`"
|
||||
:href="`https://vndb.org/${voiceActor.staff?.id}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-2 p-2 rounded-xl bg-cyan-50 dark:bg-cyan-900/20 hover:bg-cyan-100 dark:hover:bg-cyan-900/30 transition-colors group"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-cyan-700 dark:text-cyan-400 truncate group-hover:underline">
|
||||
{{ voiceActor.name }}
|
||||
{{ voiceActor.staff?.original || voiceActor.staff?.name }}
|
||||
</p>
|
||||
<p v-if="voiceActor.character?.name" class="text-xs text-gray-500 dark:text-slate-400 truncate">
|
||||
饰 {{ voiceActor.character.name }}
|
||||
饰 {{ voiceActor.character.original || voiceActor.character.name }}
|
||||
</p>
|
||||
</div>
|
||||
<ExternalLink :size="12" class="text-cyan-400 dark:text-cyan-600 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="searchStore.vndbInfo.va.length > 10" class="text-xs text-gray-400 dark:text-slate-500 mt-2 text-center">
|
||||
还有 {{ searchStore.vndbInfo.va.length - 10 }} 位声优...
|
||||
</p>
|
||||
<button
|
||||
v-if="searchStore.vndbInfo.va.length > 10"
|
||||
class="w-full mt-2 py-1.5 text-xs font-medium text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-900/20 hover:bg-cyan-100 dark:hover:bg-cyan-900/30 rounded-lg transition-colors"
|
||||
@click="toggleSection('va')"
|
||||
>
|
||||
{{ expandedSections.va ? '收起' : `显示全部 ${searchStore.vndbInfo.va.length} 位声优` }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 相关作品 -->
|
||||
@@ -352,7 +363,7 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
v-for="(relation, index) in searchStore.vndbInfo.relations.slice(0, 8)"
|
||||
v-for="(relation, index) in (expandedSections.relations ? searchStore.vndbInfo.relations : searchStore.vndbInfo.relations.slice(0, 8))"
|
||||
:key="index"
|
||||
:href="`https://vndb.org/${relation.id}`"
|
||||
target="_blank"
|
||||
@@ -368,9 +379,13 @@
|
||||
<ExternalLink :size="12" class="text-amber-400 dark:text-amber-600 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="searchStore.vndbInfo.relations.length > 8" class="text-xs text-gray-400 dark:text-slate-500 mt-2 text-center">
|
||||
还有 {{ searchStore.vndbInfo.relations.length - 8 }} 个相关作品...
|
||||
</p>
|
||||
<button
|
||||
v-if="searchStore.vndbInfo.relations.length > 8"
|
||||
class="w-full mt-2 py-1.5 text-xs font-medium text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 hover:bg-amber-100 dark:hover:bg-amber-900/30 rounded-lg transition-colors"
|
||||
@click="toggleSection('relations')"
|
||||
>
|
||||
{{ expandedSections.relations ? '收起' : `显示全部 ${searchStore.vndbInfo.relations.length} 个相关作品` }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 外部链接 -->
|
||||
@@ -394,6 +409,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色 -->
|
||||
<div v-if="characters.length > 0" class="vndb-card">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<Users :size="18" class="text-rose-500" />
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">角色</h3>
|
||||
<span class="text-xs text-gray-400 dark:text-slate-500">({{ characters.length }})</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3">
|
||||
<a
|
||||
v-for="(char, index) in (expandedSections.characters ? characters : characters.slice(0, 10))"
|
||||
:key="index"
|
||||
:href="`https://vndb.org/${char.id}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex flex-col items-center p-2 rounded-xl bg-rose-50 dark:bg-rose-900/20 hover:bg-rose-100 dark:hover:bg-rose-900/30 transition-all hover:scale-105 group"
|
||||
>
|
||||
<div class="relative w-16 h-20 sm:w-20 sm:h-24 mb-2 rounded-lg overflow-hidden shadow-md">
|
||||
<img
|
||||
v-if="char.image"
|
||||
:src="char.image"
|
||||
:alt="char.name"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="w-full h-full bg-gradient-to-br from-rose-200 to-rose-300 dark:from-rose-800 dark:to-rose-900 flex items-center justify-center">
|
||||
<Users :size="24" class="text-rose-400 dark:text-rose-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-rose-700 dark:text-rose-400 text-center truncate w-full group-hover:underline">
|
||||
{{ char.original || char.name }}
|
||||
</p>
|
||||
<p v-if="char.sex" class="text-[10px] text-gray-500 dark:text-slate-400 text-center">
|
||||
{{ formatSex(char.sex) }}{{ char.age ? ` · ${char.age}岁` : '' }}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
v-if="characters.length > 10"
|
||||
class="w-full mt-2 py-1.5 text-xs font-medium text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20 hover:bg-rose-100 dark:hover:bg-rose-900/30 rounded-lg transition-colors"
|
||||
@click="toggleSection('characters')"
|
||||
>
|
||||
{{ expandedSections.characters ? '收起' : `显示全部 ${characters.length} 个角色` }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="isLoadingCharacters" class="vndb-card">
|
||||
<div class="flex items-center gap-2">
|
||||
<Loader :size="18" class="animate-spin text-rose-500" />
|
||||
<span class="text-sm text-gray-500 dark:text-slate-400">加载角色中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 名言 -->
|
||||
<div v-if="quotes.length > 0" class="vndb-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Quote :size="18" class="text-indigo-500" />
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">名言</h3>
|
||||
<span class="text-xs text-gray-400 dark:text-slate-500">({{ quotes.length }})</span>
|
||||
</div>
|
||||
<!-- 翻译中指示器 -->
|
||||
<Loader v-if="isTranslatingQuotes" :size="14" class="animate-spin text-indigo-500" />
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(q, index) in (expandedSections.quotes ? quotes : quotes.slice(0, 5))"
|
||||
:key="index"
|
||||
class="relative pl-4 border-l-2 border-indigo-300 dark:border-indigo-600"
|
||||
>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 italic leading-relaxed">
|
||||
"{{ getQuoteDisplayText(q.quote) }}"
|
||||
</p>
|
||||
<p v-if="q.character" class="text-xs text-indigo-500 dark:text-indigo-400 mt-1 font-medium">
|
||||
— {{ q.character.original || q.character.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="quotes.length > 5"
|
||||
class="w-full mt-2 py-1.5 text-xs font-medium text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-900/20 hover:bg-indigo-100 dark:hover:bg-indigo-900/30 rounded-lg transition-colors"
|
||||
@click="toggleSection('quotes')"
|
||||
>
|
||||
{{ expandedSections.quotes ? '收起' : `显示全部 ${quotes.length} 条名言` }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="isLoadingQuotes" class="vndb-card">
|
||||
<div class="flex items-center gap-2">
|
||||
<Loader :size="18" class="animate-spin text-indigo-500" />
|
||||
<span class="text-sm text-gray-500 dark:text-slate-400">加载名言中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 简介 -->
|
||||
<div v-if="searchStore.vndbInfo.description" class="vndb-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -401,44 +507,12 @@
|
||||
<AlignLeft :size="18" class="text-[#ff1493]" />
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">简介</h3>
|
||||
</div>
|
||||
<!-- 翻译按钮 -->
|
||||
<button
|
||||
v-if="!isTranslating && !translatedDescription"
|
||||
class="px-3 py-1.5 text-xs font-medium text-white bg-gradient-to-r from-[#ff1493] to-[#d946ef] rounded-full shadow-md hover:shadow-lg transition-all flex items-center gap-1"
|
||||
@click="handleTranslate"
|
||||
>
|
||||
<Languages :size="14" />
|
||||
<span>AI 翻译</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="translatedDescription && !isTranslating"
|
||||
class="px-3 py-1.5 text-xs font-medium text-[#ff1493] dark:text-[#ff69b4] bg-pink-100 dark:bg-pink-900/30 rounded-full hover:bg-pink-200 dark:hover:bg-pink-900/50 transition-colors flex items-center gap-1"
|
||||
@click="showOriginal = !showOriginal; playToggle()"
|
||||
>
|
||||
<ArrowLeftRight :size="14" />
|
||||
<span>{{ showOriginal ? '显示译文' : '显示原文' }}</span>
|
||||
</button>
|
||||
<!-- 翻译中指示器 -->
|
||||
<Loader v-if="isTranslating" :size="14" class="animate-spin text-[#ff1493]" />
|
||||
</div>
|
||||
|
||||
<!-- 翻译中 -->
|
||||
<div v-if="isTranslating" class="flex flex-col items-center justify-center gap-2 text-[#ff1493] py-8">
|
||||
<Loader :size="24" class="animate-spin" />
|
||||
<span>AI 翻译中,请稍候...</span>
|
||||
</div>
|
||||
<!-- 翻译失败 -->
|
||||
<div v-else-if="translateError" class="flex flex-col items-center justify-center gap-2 text-red-500 py-8">
|
||||
<AlertTriangle :size="24" />
|
||||
<span>翻译服务暂时不可用</span>
|
||||
<button
|
||||
class="mt-2 px-3 py-1 text-xs bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center gap-1"
|
||||
@click="handleTranslate"
|
||||
>
|
||||
<RotateCcw :size="12" />
|
||||
<span>重试</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 显示内容 -->
|
||||
<div v-else class="text-sm text-gray-700 dark:text-slate-300 leading-relaxed whitespace-pre-line">
|
||||
<div class="text-sm text-gray-700 dark:text-slate-300 leading-relaxed whitespace-pre-line">
|
||||
<template v-if="showOriginal || !translatedDescription">
|
||||
{{ searchStore.vndbInfo.description }}
|
||||
</template>
|
||||
@@ -484,9 +558,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useSearchStore } from '@/stores/search'
|
||||
import { useSearchStore, type VndbCharacter, type VndbQuote } from '@/stores/search'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { translateText } from '@/api/search'
|
||||
import { translateText, fetchVndbCharacters, fetchVndbQuotes } from '@/api/search'
|
||||
import { playClick, playSuccess, playError, playToggle, playTransitionUp, playTransitionDown, playSwipe } from '@/composables/useSound'
|
||||
import { animate } from '@/composables/useAnime'
|
||||
import { useImageViewer } from '@/composables/useImageViewer'
|
||||
@@ -503,8 +577,6 @@ import {
|
||||
Languages,
|
||||
ArrowLeftRight,
|
||||
Loader,
|
||||
AlertTriangle,
|
||||
RotateCcw,
|
||||
Bot,
|
||||
Image,
|
||||
Maximize2,
|
||||
@@ -516,6 +588,8 @@ import {
|
||||
GitBranch,
|
||||
Globe,
|
||||
Gamepad2,
|
||||
Users,
|
||||
Quote,
|
||||
} from 'lucide-vue-next'
|
||||
import { useWindowManager, type ResizeDirection } from '@/composables/useWindowManager'
|
||||
import WindowResizeHandles from '@/components/WindowResizeHandles.vue'
|
||||
@@ -559,6 +633,40 @@ const translatedTags = ref<Map<string, string>>(new Map())
|
||||
const showOriginalTags = ref(false)
|
||||
const translateTagsError = ref(false)
|
||||
|
||||
// 角色和名言
|
||||
const characters = ref<VndbCharacter[]>([])
|
||||
const quotes = ref<VndbQuote[]>([])
|
||||
const isLoadingCharacters = ref(false)
|
||||
const isLoadingQuotes = ref(false)
|
||||
|
||||
// 名言翻译状态
|
||||
const translatedQuotes = ref<Map<string, string>>(new Map())
|
||||
const isTranslatingQuotes = ref(false)
|
||||
const translateQuotesError = ref(false)
|
||||
const showOriginalQuotes = ref(false)
|
||||
|
||||
// 一键翻译状态
|
||||
const isTranslatingAll = computed(() =>
|
||||
isTranslating.value || isTranslatingTags.value || isTranslatingQuotes.value,
|
||||
)
|
||||
const hasAnyTranslation = computed(() =>
|
||||
translatedDescription.value || translatedTags.value.size > 0 || translatedQuotes.value.size > 0,
|
||||
)
|
||||
|
||||
// 展开/收起状态
|
||||
const expandedSections = ref({
|
||||
names: false,
|
||||
va: false,
|
||||
relations: false,
|
||||
characters: false,
|
||||
quotes: false,
|
||||
})
|
||||
|
||||
function toggleSection(section: keyof typeof expandedSections.value) {
|
||||
playClick()
|
||||
expandedSections.value[section] = !expandedSections.value[section]
|
||||
}
|
||||
|
||||
// 窗口管理
|
||||
const modalRef = ref<HTMLElement | null>(null)
|
||||
const { isFullscreen, windowStyle, startDrag, startResize, toggleFullscreen, reset } = useWindowManager({
|
||||
@@ -593,8 +701,8 @@ const vndbUrl = computed(() => {
|
||||
return 'https://vndb.org/'
|
||||
})
|
||||
|
||||
// 监听 vndbInfo 变化,重置翻译状态
|
||||
watch(() => searchStore.vndbInfo, () => {
|
||||
// 监听 vndbInfo 变化,重置翻译状态并加载角色和名言
|
||||
watch(() => searchStore.vndbInfo, async (newInfo) => {
|
||||
translatedDescription.value = null
|
||||
showOriginal.value = false
|
||||
isTranslating.value = false
|
||||
@@ -604,8 +712,46 @@ watch(() => searchStore.vndbInfo, () => {
|
||||
showOriginalTags.value = false
|
||||
isTranslatingTags.value = false
|
||||
translateTagsError.value = false
|
||||
// 重置名言翻译状态
|
||||
translatedQuotes.value = new Map()
|
||||
showOriginalQuotes.value = false
|
||||
isTranslatingQuotes.value = false
|
||||
translateQuotesError.value = false
|
||||
// 重置角色和名言
|
||||
characters.value = []
|
||||
quotes.value = []
|
||||
// 重置展开状态
|
||||
expandedSections.value = {
|
||||
names: false,
|
||||
va: false,
|
||||
relations: false,
|
||||
characters: false,
|
||||
quotes: false,
|
||||
}
|
||||
|
||||
// 如果有游戏 ID,加载角色和名言
|
||||
if (newInfo?.id) {
|
||||
loadCharactersAndQuotes(newInfo.id)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载角色和名言
|
||||
async function loadCharactersAndQuotes(vnId: string) {
|
||||
// 并行加载角色和名言
|
||||
isLoadingCharacters.value = true
|
||||
isLoadingQuotes.value = true
|
||||
|
||||
const [chars, quoteList] = await Promise.all([
|
||||
fetchVndbCharacters(vnId),
|
||||
fetchVndbQuotes(vnId),
|
||||
])
|
||||
|
||||
characters.value = chars
|
||||
quotes.value = quoteList
|
||||
isLoadingCharacters.value = false
|
||||
isLoadingQuotes.value = false
|
||||
}
|
||||
|
||||
// 监听打开状态
|
||||
watch(() => uiStore.isVndbPanelOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
@@ -625,7 +771,7 @@ async function handleTranslate() {
|
||||
translateError.value = false
|
||||
|
||||
try {
|
||||
const translated = await translateText(searchStore.vndbInfo.description)
|
||||
const translated = await translateText(searchStore.vndbInfo.description, 'description')
|
||||
if (translated) {
|
||||
translatedDescription.value = translated
|
||||
showOriginal.value = false
|
||||
@@ -658,7 +804,7 @@ async function handleTranslateTags() {
|
||||
const tagNames = searchStore.vndbInfo.tags.map(tag => tag.name)
|
||||
const textToTranslate = tagNames.join('\n')
|
||||
|
||||
const translated = await translateText(textToTranslate)
|
||||
const translated = await translateText(textToTranslate, 'tags')
|
||||
if (translated) {
|
||||
// 解析翻译结果,按换行符分割
|
||||
const translatedNames = translated.split('\n').map(s => s.trim()).filter(s => s)
|
||||
@@ -687,10 +833,95 @@ async function handleTranslateTags() {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换显示原始/翻译标签
|
||||
function toggleTagsLanguage() {
|
||||
// 翻译名言
|
||||
async function handleTranslateQuotes() {
|
||||
if (quotes.value.length === 0 || isTranslatingQuotes.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isTranslatingQuotes.value = true
|
||||
translateQuotesError.value = false
|
||||
|
||||
try {
|
||||
// 收集所有名言,用换行符分隔
|
||||
const quoteTexts = quotes.value.map(q => q.quote)
|
||||
const textToTranslate = quoteTexts.join('\n')
|
||||
|
||||
const translated = await translateText(textToTranslate, 'quotes')
|
||||
if (translated) {
|
||||
// 解析翻译结果,按换行符分割
|
||||
const translatedTexts = translated.split('\n').map(s => s.trim()).filter(s => s)
|
||||
|
||||
// 创建映射
|
||||
const newMap = new Map<string, string>()
|
||||
quoteTexts.forEach((original, index) => {
|
||||
if (translatedTexts[index]) {
|
||||
newMap.set(original, translatedTexts[index])
|
||||
}
|
||||
})
|
||||
|
||||
translatedQuotes.value = newMap
|
||||
showOriginalQuotes.value = false
|
||||
translateQuotesError.value = false
|
||||
} else {
|
||||
translateQuotesError.value = true
|
||||
}
|
||||
} catch {
|
||||
translateQuotesError.value = true
|
||||
} finally {
|
||||
isTranslatingQuotes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取名言显示文本
|
||||
function getQuoteDisplayText(quote: string): string {
|
||||
if (showOriginalQuotes.value || translatedQuotes.value.size === 0) {
|
||||
return quote
|
||||
}
|
||||
return translatedQuotes.value.get(quote) || quote
|
||||
}
|
||||
|
||||
// 一键翻译全部
|
||||
async function handleTranslateAll() {
|
||||
if (isTranslatingAll.value) {
|
||||
return
|
||||
}
|
||||
|
||||
playClick()
|
||||
|
||||
// 并行执行所有翻译任务
|
||||
const tasks: Promise<void>[] = []
|
||||
|
||||
// 翻译简介
|
||||
if (searchStore.vndbInfo?.description && !translatedDescription.value) {
|
||||
tasks.push(handleTranslate())
|
||||
}
|
||||
|
||||
// 翻译标签
|
||||
if (searchStore.vndbInfo?.tags && searchStore.vndbInfo.tags.length > 0 && translatedTags.value.size === 0) {
|
||||
tasks.push(handleTranslateTags())
|
||||
}
|
||||
|
||||
// 翻译名言
|
||||
if (quotes.value.length > 0 && translatedQuotes.value.size === 0) {
|
||||
tasks.push(handleTranslateQuotes())
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
|
||||
// 如果有任何翻译成功,播放成功音效
|
||||
if (translatedDescription.value || translatedTags.value.size > 0 || translatedQuotes.value.size > 0) {
|
||||
playSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换所有翻译的显示状态
|
||||
function toggleAllTranslations() {
|
||||
playToggle()
|
||||
showOriginalTags.value = !showOriginalTags.value
|
||||
const newState = !showOriginal.value
|
||||
showOriginal.value = newState
|
||||
showOriginalTags.value = newState
|
||||
showOriginalQuotes.value = newState
|
||||
}
|
||||
|
||||
// 获取标签显示名称
|
||||
@@ -744,6 +975,17 @@ function openGallery(startIndex: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化性别
|
||||
function formatSex(sex: string): string {
|
||||
const sexMap: Record<string, string> = {
|
||||
'm': '男性',
|
||||
'f': '女性',
|
||||
'b': '双性',
|
||||
'n': '无性',
|
||||
}
|
||||
return sexMap[sex] || sex
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) {return '未知'}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useHistoryStore } from './history'
|
||||
import { useCacheStore } from './cache'
|
||||
|
||||
export interface VndbVoiceActor {
|
||||
id: string
|
||||
name: string
|
||||
character?: { id: string; name: string }
|
||||
note: string | null
|
||||
character: { id: string; name: string; original?: string }
|
||||
staff: { id: string; name: string; original?: string }
|
||||
}
|
||||
|
||||
export interface VndbTag {
|
||||
@@ -37,11 +37,33 @@ export interface VndbDeveloper {
|
||||
original?: string
|
||||
}
|
||||
|
||||
export interface VndbCharacter {
|
||||
id: string
|
||||
name: string
|
||||
original?: string
|
||||
image?: string
|
||||
sex?: string
|
||||
description?: string
|
||||
age?: number
|
||||
}
|
||||
|
||||
export interface VndbQuote {
|
||||
id: string
|
||||
quote: string
|
||||
character?: {
|
||||
id: string
|
||||
name: string
|
||||
original?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface VndbInfo {
|
||||
id?: string
|
||||
names: string[]
|
||||
aliases?: string[]
|
||||
mainName: string
|
||||
originalTitle: string
|
||||
alttitle?: string
|
||||
mainImageUrl: string | null
|
||||
screenshotUrl: string | null
|
||||
screenshots: string[]
|
||||
@@ -65,6 +87,8 @@ export interface VndbInfo {
|
||||
languages?: string[]
|
||||
olang?: string
|
||||
devstatus?: number
|
||||
characters?: VndbCharacter[]
|
||||
quotes?: VndbQuote[]
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
|
||||
Reference in New Issue
Block a user