\`。
- \`\`: 翻译后的游戏介绍,游戏介绍的每个段落用\`
\`分段,但禁止其他复杂HTML/样式标签。
- \`\`:包含翻译后的模板文本。模板如下:
- \`{book_length}, 平均游玩时长为 {play_hours}小时{length_minute}分钟, 共{length_votes}名玩家参与投票\`
- 其中的标签与代码严禁翻译或改动。
- 以中文为例, {book_length}应该被根据响应的值翻译为: "短篇", "中篇", "长篇", "超长篇"。
- 如果length_minute=0, 模板则无需输出游玩分钟。
-
-3.Tag列表:
- \`\`:包含\`\`~\`\`子元素。
- \`\`: 翻译用户输入的tags1里面的所有tag,tag之间必须使用\`, \`分隔。若tags1为空,则该标签为空标签。
- \`\`~\`\`:同上,翻译输入的tags2~tags4里面的所有tag。
-
-4.人物信息列表:
- \`\`:包含所有\`\`子元素。
- 每个\`\`包含以下子元素(严格按顺序):
- \`\`:若有则包含URL,否则输出\`\`空标签。
- \`\`:人物翻译后的姓名。
- \`\`:原始值(main/primary/side/appears),勿翻译。
- \`\`:原始姓名(优先中文/日文名), 如无, 则输出未翻译的主姓名。
- \`\`:人物描述。结合游戏介绍与角色'tag'('tag'本身勿输出),灵活撰写以突出特点,严禁输出不雅内容。
-
-5.总结与思考:
- \`\`:包含\`\`子元素。
- \`\`:基于翻译后的故事与人物,提出一个引人深思/好奇的问题。勿用总结性开场白(如“总体来说”)。
-
-最终输出约束:
- 纯XML内容,严格遵循XML标准及结构,所有标签正确嵌套/闭合。勿含任何额外文字/评论。使用\`\`\`xml \`\`\`的代码块来包裹。`,
- },
- {
- role: "user",
- content: `Game Description:\n${description}\n\nPlay Time:\nplay_hours: ${play_hours}\nlength_minute: ${length_minute}\nlength_votes: ${length_votes}\nlength_color: ${length_color}\nbook_length: ${book_length}\n\nGame Tags:\n${(
- vntags || []
- )
- .map((tags, index) => {
- const tagContent =
- tags && tags.length > 0 ? tags.join(", ") : "";
- return `Tag${index + 1}: ${tagContent}`;
- })
- .join("\n")}\n\nCharacter Details:\n${JSON.stringify(
- characters,
- null,
- 2
- )}`,
- },
- ];
-
- console.log("Sending AI request with messages:", messages);
-
- const response = await fetch(AI_TRANSLATE_API_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${AI_TRANSLATE_API_KEY}`,
- },
- body: JSON.stringify({
- model: AI_TRANSLATE_MODEL,
- messages: messages,
- stream: true,
- }),
- });
-
- if (response.ok) {
- const reader = response.body.getReader();
- const decoder = new TextDecoder("utf-8");
- let buffer = "";
- let isFirstChunk = true;
- vndbInfo.aiRawResponse = ""; // Reset before streaming
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) {
- console.log("Stream finished.");
- console.log("AI响应原文:", vndbInfo.aiRawResponse);
- break;
- }
-
- buffer += decoder.decode(value, { stream: true });
- let boundary = buffer.indexOf("\n");
- while (boundary !== -1) {
- const line = buffer.substring(0, boundary).trim();
- buffer = buffer.substring(boundary + 1);
-
- if (line.startsWith("data: ")) {
- const jsonStr = line.substring(6);
- if (jsonStr !== "[DONE]") {
- try {
- const chunk = JSON.parse(jsonStr);
- if (
- chunk.choices &&
- chunk.choices[0].delta &&
- chunk.choices[0].delta.content
- ) {
- if (isFirstChunk) {
- if (lockViewBtn) lockViewBtn.disabled = false;
- isFirstChunk = false;
- }
- vndbInfo.aiRawResponse += chunk.choices[0].delta.content;
- renderAiView(vndbInfo.aiRawResponse);
-
- const startIndexDesc = vndbInfo.aiRawResponse.indexOf(
- ""
- );
- if (startIndexDesc !== -1) {
- const endIndexDesc = vndbInfo.aiRawResponse.indexOf(
- ""
- );
- let contentToRenderDesc =
- endIndexDesc !== -1
- ? vndbInfo.aiRawResponse.substring(
- startIndexDesc +
- "".length,
- endIndexDesc
- )
- : vndbInfo.aiRawResponse.substring(
- startIndexDesc +
- "".length
- );
- // Remove tag and its content from description
- const playTimeRegex = /[\s\S]*?<\/play_time>/;
- contentToRenderDesc = contentToRenderDesc.replace(playTimeRegex, "");
- contentToRenderDesc = contentToRenderDesc.replace(
- //g,
- "
"
- );
- vndbDescription.innerHTML = contentToRenderDesc;
- // Show the one-time toast notification here
- if (!hasShownViewToggleToast && !isMobileView) {
- const toastMessage = "按下空格键进入游戏详情视图";
- showToast(toastMessage);
- hasShownViewToggleToast = true;
- }
- }
- }
- } catch (e) {
- // Ignore JSON parsing errors
- }
- }
- }
- boundary = buffer.indexOf("\n");
- }
- }
- } else {
- // Fallback for non-200 responses
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- } catch (error) {
- console.error("Error or fallback during AI translation:", error);
- vndbDescription.innerHTML = description.replace(/\n/g, "
");
-
- // Construct fallback XML for AI view
- let fallbackXml = "";
- fallbackXml += `${description.replace(
- /\n/g,
- "
"
- )}
`;
-
- // Construct fallback play_time content
- let fallbackBookLength = "Overlength";
- if (play_hours < 10) {
- fallbackBookLength = "Short";
- } else if (play_hours < 30) {
- fallbackBookLength = "Medium";
- } else if (play_hours < 40) {
- fallbackBookLength = "Long";
- }
-
- let fallbackPlayTimeContent = `${fallbackBookLength}, Average play time is ${play_hours} hours`;
- if (length_minute !== 0) {
- fallbackPlayTimeContent += ` ${length_minute} minutes`;
- }
- fallbackPlayTimeContent += `, with ${length_votes} players voting`;
-
- fallbackXml += `${fallbackPlayTimeContent}`;
- fallbackXml += ``;
-
- // Add tags to fallback XML
- if (vntags) {
- fallbackXml += "";
- const tagsXml = Object.entries(vntags)
- .map(([key, value]) => `<${key}>${value}${key}>`)
- .join("");
- fallbackXml += tagsXml;
- fallbackXml += "";
- }
-
- // Add tags to fallback XML
- if (vntags && vntags.length > 0) {
- fallbackXml += "";
- vntags.forEach((tagArray, index) => {
- if (index < 4) { // Only process up to tags4
- const tagName = `tags${index + 1}`;
- const tagValue = tagArray.join(", ");
- fallbackXml += `<${tagName}>${tagValue}${tagName}>`;
- }
- });
- fallbackXml += "";
- }
-
- if (characters && characters.length > 0) {
- fallbackXml += "";
- characters.forEach((c) => {
- fallbackXml += "";
- fallbackXml += `${c.image || ""}`;
- fallbackXml += `${c.name}`;
- fallbackXml += `${c.role}`;
- fallbackXml += `${
- c.originalName || c.name
- }`;
- fallbackXml += `${
- c.description || "无可用描述"
- }`;
- fallbackXml += "";
- });
- fallbackXml += "";
- }
- fallbackXml +=
- "AI翻译服务当前不可用,以上为原始信息。";
- fallbackXml += "";
-
- vndbInfo.aiRawResponse = fallbackXml;
- renderAiView(vndbInfo.aiRawResponse);
- if (lockViewBtn) lockViewBtn.disabled = false;
- }
-}
-
-async function checkLlmStatus() {
- const statusBtn = document.getElementById("llm-status-btn");
- const statusText = document.getElementById("llm-status-text");
-
- if (!statusBtn || !statusText) {
- return;
- }
-
- try {
- const response = await fetch(
- "https://api.pulsetic.com/public/status/status.searchgal.homes",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ password: null }),
- }
- );
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const data = await response.json();
- const llmMonitor = data.data.monitors.find(
- (monitor) => monitor.name === "后端搜索 API (无实际搜索)"
- );
-
- if (llmMonitor) {
- statusBtn.classList.remove("bg-white", "text-gray-500");
- statusBtn.classList.add("text-white");
- if (llmMonitor.status === "online") {
- statusBtn.classList.add("bg-green-500");
- statusText.textContent = "正常";
- } else {
- statusBtn.classList.add("bg-red-700");
- statusText.textContent = "异常";
- }
- } else {
- throw new Error("LLM monitor not found");
- }
- } catch (error) {
- console.error("Error fetching LLM status:", error);
- statusBtn.classList.remove("bg-white", "text-gray-500");
- statusBtn.classList.add("bg-gray-500", "text-white");
- statusText.textContent = "未知";
- }
-}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..3527715
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,98 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import App from './App.vue'
+import router from './router'
+
+// GSAP 插件
+import gsap from 'gsap'
+import { ScrollToPlugin } from 'gsap/ScrollToPlugin'
+
+// 注册 GSAP 插件
+gsap.registerPlugin(ScrollToPlugin)
+
+// Font Awesome
+import '@fortawesome/fontawesome-free/css/all.min.css'
+
+// Material 3 Web Components
+import '@material/web/button/filled-button.js'
+import '@material/web/button/outlined-button.js'
+import '@material/web/button/text-button.js'
+import '@material/web/textfield/filled-text-field.js'
+import '@material/web/textfield/outlined-text-field.js'
+import '@material/web/fab/fab.js'
+import '@material/web/dialog/dialog.js'
+import '@material/web/list/list.js'
+import '@material/web/list/list-item.js'
+import '@material/web/chips/chip-set.js'
+import '@material/web/chips/assist-chip.js'
+import '@material/web/chips/filter-chip.js'
+import '@material/web/progress/linear-progress.js'
+import '@material/web/progress/circular-progress.js'
+import '@material/web/icon/icon.js'
+import '@material/web/iconbutton/icon-button.js'
+// Card 组件在 labs 目录(实验性功能)
+import '@material/web/labs/card/elevated-card.js'
+import '@material/web/labs/card/filled-card.js'
+import '@material/web/labs/card/outlined-card.js'
+import '@material/web/divider/divider.js'
+import '@material/web/ripple/ripple.js'
+
+// Pace.js 页面加载进度条
+import Pace from 'pace-js'
+import 'pace-js/themes/blue/pace-theme-flash.css'
+
+// 配置 Pace.js:禁用自动启动
+Pace.options = {
+ ajax: false, // 不监听 AJAX 请求
+ document: false, // 不监听文档加载
+ eventLag: false, // 不监听事件延迟
+ elements: false, // 不监听元素
+ restartOnPushState: false,
+ restartOnRequestAfter: false
+}
+
+// Artalk 评论系统
+import 'artalk/dist/Artalk.css'
+
+// LightGallery
+import 'lightgallery/css/lightgallery.css'
+
+// Fancybox
+import "@fancyapps/ui/dist/fancybox/fancybox.css"
+
+// Instant.page 预加载
+import 'instant.page'
+
+const app = createApp(App)
+const pinia = createPinia()
+
+app.use(pinia)
+app.use(router)
+
+app.mount('#app')
+
+// 注册 Service Worker (PWA)
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/sw.js', { scope: '/' })
+ .then(registration => {
+ console.log('[SW] Service Worker registered:', registration.scope)
+
+ // 检查更新
+ registration.addEventListener('updatefound', () => {
+ const newWorker = registration.installing
+ console.log('[SW] New Service Worker found, installing...')
+
+ newWorker?.addEventListener('statechange', () => {
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+ console.log('[SW] New content available, please refresh')
+ // 可以在这里显示更新提示
+ }
+ })
+ })
+ })
+ .catch(error => {
+ console.error('[SW] Service Worker registration failed:', error)
+ })
+ })
+}
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..0a5140f
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,16 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import HomeView from '@/views/HomeView.vue'
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: HomeView
+ }
+ ]
+})
+
+export default router
+
diff --git a/src/stores/search.ts b/src/stores/search.ts
new file mode 100644
index 0000000..3e39178
--- /dev/null
+++ b/src/stores/search.ts
@@ -0,0 +1,123 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+export interface VndbInfo {
+ names: string[]
+ mainName: string
+ originalTitle: string
+ mainImageUrl: string | null
+ screenshotUrl: string | null
+ description: string | null
+ va: any[]
+ vntags: any[]
+ play_hours: number
+ length_minute: number
+ length_votes: number
+ length_color: string
+ book_length: string
+}
+
+export interface SearchResult {
+ platform: string
+ title: string
+ url: string
+ tags?: string[]
+}
+
+export interface PlatformData {
+ name: string
+ color: 'lime' | 'white' | 'gold' | 'red'
+ items: SearchResult[]
+ error: string
+ currentPage: number
+ itemsPerPage: number
+}
+
+export const useSearchStore = defineStore('search', () => {
+ // 状态
+ const searchQuery = ref('')
+ const searchMode = ref<'game' | 'patch'>('game')
+ const customApi = ref('')
+ const platformResults = ref