Merge pull request #32 from Moe-Sakura/dev

feat: 添加友情链接功能与更新组件样式
This commit is contained in:
Asuna
2025-12-25 15:19:41 +08:00
committed by GitHub
7 changed files with 327 additions and 213 deletions

34
public/data/friends.json Normal file
View File

@@ -0,0 +1,34 @@
{
"friends": [
{
"name": "Jurangren",
"desc": "本站后端大手子!",
"url": "https://github.com/Jurangren",
"logo": "https://avatars.githubusercontent.com/u/111159360"
},
{
"name": "Asuna",
"desc": "LINK START!",
"url": "https://saop.cc/",
"logo": "https://saop.cc/avatar.webp"
},
{
"name": "VNDB",
"desc": "Visual Novel Database",
"url": "https://vndb.org/",
"logo": "https://vndb.org/favicon.ico"
},
{
"name": "梓澪",
"desc": "梓澪の妙妙屋",
"url": "https://zi0.cc/",
"logo": "https://img.mjj.today/2023/01/23/2b6331a29bf32d2af96a2537e10a5ee8.webp"
},
{
"name": "VNS",
"desc": "Visual Novel",
"url": "https://gal.saop.cc",
"logo": "https://gal.saop.cc/images/logo.svg"
}
]
}

View File

@@ -1,43 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd
http://www.google.com/schemas/sitemap-image/1.1
http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd">
<!-- 主页 -->
<url>
<loc>https://searchgal.homes/</loc>
<lastmod>2025-12-21</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
<image:image>
<image:loc>https://searchgal.homes/logo.svg</image:loc>
<image:title>SearchGal - Galgame 聚合搜索</image:title>
<image:caption>Galgame 资源聚合搜索引擎,支持多站点搜索</image:caption>
</image:image>
<image:image>
<image:loc>https://searchgal.homes/og-image.png</image:loc>
<image:title>SearchGal 社交分享图</image:title>
</image:image>
</url>
<!-- AI/LLM 信息页 -->
<url>
<loc>https://searchgal.homes/llms.txt</loc>
<lastmod>2025-12-21</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<!-- PWA Manifest -->
<url>
<loc>https://searchgal.homes/manifest.json</loc>
<lastmod>2025-12-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
</urlset>
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd
http://www.google.com/schemas/sitemap-image/1.1
http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd">
<!-- 主页 -->
<url>
<loc>https://searchgal.homes/</loc>
<lastmod>2025-12-21</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
<image:image>
<image:loc>https://searchgal.homes/logo.svg</image:loc>
<image:title>SearchGal - Galgame 聚合搜索</image:title>
<image:caption>Galgame 资源聚合搜索引擎,支持多站点搜索</image:caption>
</image:image>
<image:image>
<image:loc>https://searchgal.homes/og-image.png</image:loc>
<image:title>SearchGal 社交分享图</image:title>
</image:image>
</url>
<!-- AI/LLM 信息页 -->
<url>
<loc>https://searchgal.homes/llms.txt</loc>
<lastmod>2025-12-21</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<!-- PWA Manifest -->
<url>
<loc>https://searchgal.homes/manifest.json</loc>
<lastmod>2025-12-21</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -390,6 +390,72 @@
</ul>
</div>
</div>
<!-- 友情链接 -->
<div
v-if="friendLinks.length > 0"
class="w-full max-w-5xl mx-auto mt-6 sm:mt-8 px-2 sm:px-0 animate-fade-in animation-delay-1000"
>
<div
class="glassmorphism-card rounded-2xl sm:rounded-3xl
shadow-xl shadow-theme-primary/10 dark:shadow-theme-accent/20
p-4 sm:p-6"
>
<div class="flex items-center justify-between mb-4">
<h2
class="text-lg sm:text-xl font-bold
text-theme-primary dark:text-theme-accent
flex items-center gap-2"
>
<Link2 :size="18" />
友情链接
</h2>
<a
href="https://github.com/Moe-Sakura/frontend/edit/dev/public/data/friends.json"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium
text-white bg-gradient-to-r from-[#ff1493] to-[#d946ef]
shadow-md shadow-pink-500/20 hover:shadow-lg hover:shadow-pink-500/30
transition-all"
>
<GitPullRequestArrow :size="14" />
<span>交换友链</span>
</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<a
v-for="friend in friendLinks"
:key="friend.url"
:href="friend.url"
target="_blank"
rel="noopener noreferrer"
class="friend-card group flex items-center gap-3 p-3 rounded-xl
bg-white/50 dark:bg-slate-800/50
border border-gray-200/50 dark:border-slate-700/50
hover:border-[#ff1493]/30 dark:hover:border-[#ff69b4]/30
hover:shadow-lg hover:shadow-pink-500/10
transition-all duration-300"
>
<img
:src="friend.logo"
:alt="friend.name"
class="w-10 h-10 rounded-lg object-cover bg-gray-100 dark:bg-slate-700 flex-shrink-0"
loading="lazy"
@error="handleFriendLogoError"
/>
<div class="flex-1 min-w-0">
<h3 class="font-bold text-gray-800 dark:text-white text-sm group-hover:text-[#ff1493] dark:group-hover:text-[#ff69b4] transition-colors truncate">
{{ friend.name }}
</h3>
<p class="text-xs text-gray-500 dark:text-slate-400 truncate">
{{ friend.desc }}
</p>
</div>
</a>
</div>
</div>
</div>
</div>
</template>
@@ -418,6 +484,8 @@ import {
Loader2,
CornerDownLeft,
XCircle,
Link2,
GitPullRequestArrow,
} from 'lucide-vue-next'
import { getSearchParamsFromURL, updateURLParams, onURLParamsChange } from '@/utils/urlParams'
import { saveSearchHistory } from '@/utils/persistence'
@@ -428,6 +496,32 @@ const customApi = ref('')
const searchMode = ref<'game' | 'patch'>('game')
let cleanupURLListener: (() => void) | null = null
// 友情链接
interface FriendLink {
name: string
desc: string
url: string
logo: string
}
const friendLinks = ref<FriendLink[]>([])
// 获取友情链接数据
async function loadFriendLinks() {
try {
const res = await fetch('/data/friends.json')
const data = await res.json()
friendLinks.value = data.friends || []
} catch {
// 静默失败
}
}
// 友链 logo 加载失败时的处理
function handleFriendLogoError(e: Event) {
const img = e.target as HTMLImageElement
img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ff1493"><circle cx="12" cy="12" r="10"/></svg>'
}
// 搜索防抖 - 防止 800ms 内重复触发
const { isLocked: isSearchLocked, click: debouncedSearchTrigger } = useDebouncedClick(800)
@@ -472,6 +566,9 @@ onMounted(() => {
isUpdatingFromURL = false
}, 200)
})
// 加载友情链接
loadFriendLinks()
})
onUnmounted(() => {

View File

@@ -23,39 +23,23 @@
{{ countdown > 0 ? `${countdown} 秒后自动更新...` : '正在更新...' }}
</p>
</div>
<!-- 立即更新按钮 -->
<button
v-if="countdown > 0"
class="flex-shrink-0 px-3 py-1.5 rounded-xl bg-white/20 hover:bg-white/30 text-xs font-medium transition-colors"
@click="updateNow"
>
立即更新
</button>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { RefreshCw } from 'lucide-vue-next'
import { playNotification, playTap, playSwipe } from '@/composables/useSound'
import { playNotification } from '@/composables/useSound'
const props = defineProps<{
isVisible: boolean
onUpdate: () => void
}>()
const countdown = ref(3)
const countdown = ref(5)
let timer: number | null = null
function updateNow() {
playTap()
countdown.value = 0
playSwipe()
props.onUpdate()
}
function startCountdown() {
// 先清除可能存在的旧定时器,避免创建多个并发定时器
if (timer) {
@@ -64,7 +48,7 @@ function startCountdown() {
}
playNotification()
countdown.value = 3
countdown.value = 5
timer = window.setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
@@ -90,7 +74,6 @@ onUnmounted(() => {
})
// 监听 isVisible 变化
import { watch } from 'vue'
watch(() => props.isVisible, (visible) => {
if (visible) {
startCountdown()

View File

@@ -43,7 +43,7 @@ async function getSnd(): Promise<Snd | null> {
try {
isLoading = true
sndInstance = new Snd()
// 加载音效套件
const kit = currentKit.value === 'SND01' ? Snd.KITS.SND01 : Snd.KITS.SND02
await sndInstance.load(kit)

View File

@@ -221,7 +221,7 @@ export const useUIStore = defineStore('ui', () => {
// 解析失败,使用默认值
}
}
// 从 sessionStorage 加载会话状态(刷新恢复)
function loadSessionState() {
try {
@@ -259,7 +259,7 @@ export const useUIStore = defineStore('ui', () => {
// 保存失败,静默处理
}
}
// 保存会话状态到 sessionStorage刷新恢复
function saveSessionState() {
try {

View File

@@ -1,146 +1,146 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";
import { fileURLToPath, URL } from 'node:url';
import { swVersionPlugin } from './scripts/sw-version-plugin';
export default defineConfig({
server: {
host: "localhost",
port: 5500,
// 预热常用文件
warmup: {
clientFiles: [
'./src/App.vue',
'./src/components/*.vue',
'./src/stores/*.ts',
],
},
},
plugins: [
vue({
script: {
// 响应式语法糖(如需要可启用)
defineModel: true,
},
}),
tailwindcss(),
// 构建时自动注入 SW 版本号
swVersionPlugin({
swPath: 'sw.js',
includeGitHash: true,
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// 构建优化
build: {
// 使用现代浏览器目标
target: 'esnext',
// 启用 CSS 代码分割
cssCodeSplit: true,
// 压缩选项
minify: 'esbuild',
// 源码映射(生产环境关闭)
sourcemap: false,
// Chunk 大小警告阈值 (KB)
chunkSizeWarningLimit: 600,
// Rollup 配置
rollupOptions: {
output: {
// 资源文件名
assetFileNames: (assetInfo) => {
const name = assetInfo.name || '';
// 字体文件
if (/\.(woff2?|eot|ttf|otf)$/i.test(name)) {
return 'fonts/[name]-[hash][extname]';
}
// 图片文件
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(name)) {
return 'images/[name]-[hash][extname]';
}
// CSS 文件
if (/\.css$/i.test(name)) {
return 'css/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
// JS 入口文件名
entryFileNames: 'js/[name]-[hash].js',
// JS Chunk 文件名
chunkFileNames: 'js/[name]-[hash].js',
// 手动分包
manualChunks: (id) => {
if (id.includes('node_modules')) {
// Vue 核心
if (id.includes('/vue/') || id.includes('/@vue/')) {
return 'vue-core';
}
// Pinia 状态管理
if (id.includes('/pinia/')) {
return 'pinia';
}
// UI 库
if (id.includes('/lucide-vue-next/')) {
return 'ui-libs';
}
// 动画库
if (id.includes('/animejs/')) {
return 'anime';
}
// Artalk 评论
if (id.includes('/artalk/')) {
return 'artalk';
}
// 代码编辑器
if (id.includes('/prismjs/') || id.includes('/vue-prism-editor/')) {
return 'editor';
}
// 音效库
if (id.includes('/snd-lib/')) {
return 'sound';
}
// Fancybox
if (id.includes('/@fancyapps/')) {
return 'fancybox';
}
// 其他第三方库
return 'vendor';
}
},
},
},
},
// 依赖优化
optimizeDeps: {
// 预构建的依赖
include: [
'vue',
'pinia',
'lucide-vue-next',
'animejs',
],
// 排除不需要预构建的
exclude: ['artalk'],
},
// esbuild 配置
esbuild: {
// 生产环境移除 console 和 debugger
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
// 压缩选项
legalComments: 'none',
},
// CSS 配置
css: {
devSourcemap: true,
},
});
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";
import { fileURLToPath, URL } from 'node:url';
import { swVersionPlugin } from './scripts/sw-version-plugin';
export default defineConfig({
server: {
host: "localhost",
port: 5500,
// 预热常用文件
warmup: {
clientFiles: [
'./src/App.vue',
'./src/components/*.vue',
'./src/stores/*.ts',
],
},
},
plugins: [
vue({
script: {
// 响应式语法糖(如需要可启用)
defineModel: true,
},
}),
tailwindcss(),
// 构建时自动注入 SW 版本号
swVersionPlugin({
swPath: 'sw.js',
includeGitHash: true,
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
// 构建优化
build: {
// 使用现代浏览器目标
target: 'esnext',
// 启用 CSS 代码分割
cssCodeSplit: true,
// 压缩选项
minify: 'esbuild',
// 源码映射(生产环境关闭)
sourcemap: false,
// Chunk 大小警告阈值 (KB)
chunkSizeWarningLimit: 600,
// Rollup 配置
rollupOptions: {
output: {
// 资源文件名
assetFileNames: (assetInfo) => {
const name = assetInfo.name || '';
// 字体文件
if (/\.(woff2?|eot|ttf|otf)$/i.test(name)) {
return 'fonts/[name]-[hash][extname]';
}
// 图片文件
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(name)) {
return 'images/[name]-[hash][extname]';
}
// CSS 文件
if (/\.css$/i.test(name)) {
return 'css/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
// JS 入口文件名
entryFileNames: 'js/[name]-[hash].js',
// JS Chunk 文件名
chunkFileNames: 'js/[name]-[hash].js',
// 手动分包
manualChunks: (id) => {
if (id.includes('node_modules')) {
// Vue 核心
if (id.includes('/vue/') || id.includes('/@vue/')) {
return 'vue-core';
}
// Pinia 状态管理
if (id.includes('/pinia/')) {
return 'pinia';
}
// UI 库
if (id.includes('/lucide-vue-next/')) {
return 'ui-libs';
}
// 动画库
if (id.includes('/animejs/')) {
return 'anime';
}
// Artalk 评论
if (id.includes('/artalk/')) {
return 'artalk';
}
// 代码编辑器
if (id.includes('/prismjs/') || id.includes('/vue-prism-editor/')) {
return 'editor';
}
// 音效库
if (id.includes('/snd-lib/')) {
return 'sound';
}
// Fancybox
if (id.includes('/@fancyapps/')) {
return 'fancybox';
}
// 其他第三方库
return 'vendor';
}
},
},
},
},
// 依赖优化
optimizeDeps: {
// 预构建的依赖
include: [
'vue',
'pinia',
'lucide-vue-next',
'animejs',
],
// 排除不需要预构建的
exclude: ['artalk'],
},
// esbuild 配置
esbuild: {
// 生产环境移除 console 和 debugger
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
// 压缩选项
legalComments: 'none',
},
// CSS 配置
css: {
devSourcemap: true,
},
});