作者 xiaoqiu

增加了回到顶部以及日间夜间模式

1 # API地址 1 # API地址
2 -NUXT_API_URL="http://htai.aiboxgo.com" 2 +NUXT_API_URL="https://htai.aiboxgo.com"
3 3
4 NUXT_BASE_URL="https://www.aiboxgo.com" 4 NUXT_BASE_URL="https://www.aiboxgo.com"
不能预览此文件类型
@@ -9,11 +9,42 @@ import { getWebSite } from "~/api/webSite"; @@ -9,11 +9,42 @@ import { getWebSite } from "~/api/webSite";
9 import { getClassifyList } from "~/api/classify"; 9 import { getClassifyList } from "~/api/classify";
10 import type { webSiteType } from "~/api/types/webSite"; 10 import type { webSiteType } from "~/api/types/webSite";
11 import type { classifyType } from "~/api/types/classify"; 11 import type { classifyType } from "~/api/types/classify";
  12 +
12 const webSite = useState<webSiteType>("webSite"); 13 const webSite = useState<webSiteType>("webSite");
13 const sortList = useState<classifyType[]>("sortTree"); 14 const sortList = useState<classifyType[]>("sortTree");
14 15
15 -webSite.value = await getWebSite();  
16 -sortList.value = await getClassifyList(); 16 +const { data: webSiteData } = await useAsyncData(
  17 + "webSite",
  18 + async () => {
  19 + const res = await getWebSite();
  20 + return res;
  21 + },
  22 + {
  23 + server: true,
  24 + lazy: false,
  25 + getCachedData: () => null,
  26 + }
  27 +);
  28 +
  29 +const { data: sortListData } = await useAsyncData(
  30 + "sortList",
  31 + async () => {
  32 + const res = await getClassifyList();
  33 + return res;
  34 + },
  35 + {
  36 + server: true,
  37 + lazy: false,
  38 + getCachedData: () => null,
  39 + }
  40 +);
  41 +
  42 +if (webSiteData.value) {
  43 + webSite.value = webSiteData.value;
  44 +}
  45 +if (sortListData.value) {
  46 + sortList.value = sortListData.value;
  47 +}
17 </script> 48 </script>
18 49
19 <style> 50 <style>
  1 +@tailwind base;
  2 +@tailwind components;
  3 +@tailwind utilities;
  4 +
  5 +@layer base {
  6 + html {
  7 + @apply transition-colors duration-300;
  8 + }
  9 +
  10 + body {
  11 + @apply bg-white dark:bg-[#1a1b1d] text-gray-900 dark:text-gray-100;
  12 + }
  13 +}
1 @font-face { 1 @font-face {
2 font-family: "iconfont"; /* Project id 5094593 */ 2 font-family: "iconfont"; /* Project id 5094593 */
3 - src: url('iconfont.woff2?t=1770621719751') format('woff2'),  
4 - url('iconfont.woff?t=1770621719751') format('woff'),  
5 - url('iconfont.ttf?t=1770621719751') format('truetype'); 3 + src: url('iconfont.woff2?t=1773713724403') format('woff2'),
  4 + url('iconfont.woff?t=1773713724403') format('woff'),
  5 + url('iconfont.ttf?t=1773713724403') format('truetype');
6 } 6 }
7 7
8 .iconfont { 8 .iconfont {
@@ -13,6 +13,10 @@ @@ -13,6 +13,10 @@
13 -moz-osx-font-smoothing: grayscale; 13 -moz-osx-font-smoothing: grayscale;
14 } 14 }
15 15
  16 +.icon-up_top:before {
  17 + content: "\e888";
  18 +}
  19 +
16 .icon-international:before { 20 .icon-international:before {
17 content: "\e638"; 21 content: "\e638";
18 } 22 }
  1 +<template>
  2 + <div class="fixed bottom-8 right-8 flex flex-col gap-3 z-50">
  3 + <Transition name="fade">
  4 + <button
  5 + v-show="showBackTop"
  6 + @click="scrollToTop"
  7 + class="w-12 h-12 bg-[#5961f9] hover:bg-[#4751e8] dark:bg-gray-700 dark:hover:bg-gray-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-300 hover:scale-110"
  8 + aria-label="回到顶部"
  9 + >
  10 + <i class="iconfont icon-up_top text-xl"></i>
  11 + </button>
  12 + </Transition>
  13 +
  14 + <button
  15 + @click="toggleDark"
  16 + class="w-12 h-12 bg-[#5961f9] hover:bg-[#4751e8] dark:bg-gray-700 dark:hover:bg-gray-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-300 hover:scale-110"
  17 + :aria-label="isDark ? '切换到日间模式' : '切换到夜间模式'"
  18 + >
  19 + <i v-if="isDark" class="iconfont icon-sunny text-xl"></i>
  20 + <i v-else class="iconfont icon-moon text-xl"></i>
  21 + </button>
  22 + </div>
  23 +</template>
  24 +
  25 +<script lang="ts" setup>
  26 +const { isDark, toggleDark } = useDarkMode();
  27 +
  28 +const showBackTop = ref(false);
  29 +
  30 +const handleScroll = () => {
  31 + showBackTop.value = window.scrollY > 300;
  32 +};
  33 +
  34 +const scrollToTop = () => {
  35 + window.scrollTo({
  36 + top: 0,
  37 + behavior: "smooth",
  38 + });
  39 +};
  40 +
  41 +onMounted(() => {
  42 + window.addEventListener("scroll", handleScroll);
  43 +});
  44 +
  45 +onUnmounted(() => {
  46 + window.removeEventListener("scroll", handleScroll);
  47 +});
  48 +</script>
  49 +
  50 +<style scoped>
  51 +.fade-enter-active,
  52 +.fade-leave-active {
  53 + transition: opacity 0.3s ease;
  54 +}
  55 +
  56 +.fade-enter-from,
  57 +.fade-leave-to {
  58 + opacity: 0;
  59 +}
  60 +</style>
1 <template> 1 <template>
2 - <!-- Footer -->  
3 - <footer class="bg-gray-800 text-white py-12 px-8 mt-auto"> 2 + <footer class="bg-gray-800 dark:bg-gray-950 text-white py-12 px-8 mt-auto transition-colors duration-300">
4 <div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8"> 3 <div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8">
5 <div> 4 <div>
6 <h3 class="text-xl font-bold mb-4"> 5 <h3 class="text-xl font-bold mb-4">
@@ -23,25 +22,25 @@ @@ -23,25 +22,25 @@
23 <div class="flex space-x-4"> 22 <div class="flex space-x-4">
24 <a 23 <a
25 href="#" 24 href="#"
26 - class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-blue-500 transition-colors" 25 + class="w-10 h-10 rounded-full bg-gray-700 dark:bg-gray-800 flex items-center justify-center hover:bg-blue-500 transition-colors"
27 > 26 >
28 <el-icon :size="20"><Star /></el-icon> 27 <el-icon :size="20"><Star /></el-icon>
29 </a> 28 </a>
30 <a 29 <a
31 href="#" 30 href="#"
32 - class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-blue-400 transition-colors" 31 + class="w-10 h-10 rounded-full bg-gray-700 dark:bg-gray-800 flex items-center justify-center hover:bg-blue-400 transition-colors"
33 > 32 >
34 <el-icon :size="20"><Link /></el-icon> 33 <el-icon :size="20"><Link /></el-icon>
35 </a> 34 </a>
36 <a 35 <a
37 href="#" 36 href="#"
38 - class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-pink-500 transition-colors" 37 + class="w-10 h-10 rounded-full bg-gray-700 dark:bg-gray-800 flex items-center justify-center hover:bg-pink-500 transition-colors"
39 > 38 >
40 <el-icon :size="20"><Star /></el-icon> 39 <el-icon :size="20"><Star /></el-icon>
41 </a> 40 </a>
42 <a 41 <a
43 href="#" 42 href="#"
44 - class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-red-500 transition-colors" 43 + class="w-10 h-10 rounded-full bg-gray-700 dark:bg-gray-800 flex items-center justify-center hover:bg-red-500 transition-colors"
45 > 44 >
46 <el-icon :size="20"><Search /></el-icon> 45 <el-icon :size="20"><Search /></el-icon>
47 </a> 46 </a>
@@ -49,7 +48,7 @@ @@ -49,7 +48,7 @@
49 </div> 48 </div>
50 </div> 49 </div>
51 <div 50 <div
52 - class="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-700 text-center text-gray-400" 51 + class="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-700 dark:border-gray-800 text-center text-gray-400"
53 > 52 >
54 <p>{{ webSite.bottomAnnouncement }}</p> 53 <p>{{ webSite.bottomAnnouncement }}</p>
55 </div> 54 </div>
1 <template> 1 <template>
2 - <!-- 顶部导航栏 -->  
3 <header 2 <header
4 - class="fixed top-0 left-0 right-0 z-50 bg-gray-900 text-white shadow-md" 3 + class="fixed top-0 left-0 right-0 z-50 bg-gray-900 dark:bg-[#272929] text-white shadow-md transition-colors duration-300"
5 > 4 >
6 <div class="mx-auto md:px-6 px-3 py-3 flex items-center justify-between"> 5 <div class="mx-auto md:px-6 px-3 py-3 flex items-center justify-between">
7 <NuxtLink to="/" class="flex items-center space-x-2"> 6 <NuxtLink to="/" class="flex items-center space-x-2">
1 <template> 1 <template>
2 <nav 2 <nav
3 - class="max-[768px]:flex-[0] flex-shrink-0 scroll-container w-56 bg-white shadow-lg h-[calc(100vh-4rem)] sticky top-16 overflow-y-auto" 3 + class="max-[768px]:flex-[0] flex-shrink-0 scroll-container w-56 bg-white dark:bg-[#272929] shadow-lg h-[calc(100vh-4rem)] sticky top-16 overflow-y-auto transition-colors duration-300"
4 > 4 >
5 <div class="md:p-4 p-2"> 5 <div class="md:p-4 p-2">
6 - <h2 class="text-lg font-semibold mb-4 text-gray-700">工具分类</h2> 6 + <h2 class="text-lg font-semibold mb-4 text-gray-700 dark:text-[#c6c9cf]">
  7 + 工具分类
  8 + </h2>
7 <ul id="menu" class="space-y-1"> 9 <ul id="menu" class="space-y-1">
8 <li 10 <li
9 :id="`menu-${category.id}`" 11 :id="`menu-${category.id}`"
@@ -11,35 +13,35 @@ @@ -11,35 +13,35 @@
11 :key="index" 13 :key="index"
12 class="menu-item" 14 class="menu-item"
13 > 15 >
  16 + <div
  17 + class="w-full flex items-center justify-between p-3 text-[#515c6b] dark:text-[#c6c9cf] hover:text-[#5961f9] dark:hover:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer"
  18 + >
14 <a 19 <a
15 :href="`#term-${category.id}`" 20 :href="`#term-${category.id}`"
16 - @click.stop="toggleCategory($event, category.id, index)"  
17 - class="w-full flex items-center justify-between p-3 text-[#515c6b] hover:text-[#5961f9] rounded-lg hover:bg-gray-100 transition-colors" 21 + @click="handleCategoryClick($event, category.id, category)"
  22 + class="flex items-center space-x-2 flex-1"
18 > 23 >
19 - <div class="flex items-center space-x-2">  
20 <i 24 <i
21 class="iconfont text-sm" 25 class="iconfont text-sm"
22 :class="[`icon-${category.icon}`]" 26 :class="[`icon-${category.icon}`]"
23 ></i> 27 ></i>
24 <span class="text-sm">{{ category.label }}</span> 28 <span class="text-sm">{{ category.label }}</span>
25 - </div>  
26 - <div v-if="category.children">  
27 - <el-icon  
28 - size="14px"  
29 - color="#515c6b"  
30 - v-show="activeCategory !== index" 29 + </a>
  30 + <button
  31 + v-if="category.children && category.children.length > 0"
  32 + @click.stop="toggleExpand(index)"
  33 + class="p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors"
  34 + :aria-label="activeCategory === index ? '收起分类' : '展开分类'"
31 > 35 >
32 - <ArrowRightBold />  
33 - </el-icon>  
34 <el-icon 36 <el-icon
35 size="14px" 37 size="14px"
36 - color="#515c6b"  
37 - v-show="activeCategory === index" 38 + class="text-[#515c6b] dark:text-[#c6d9df] transition-transform duration-300"
  39 + :class="{ 'rotate-90': activeCategory === index }"
38 > 40 >
39 - <ArrowDownBold /> 41 + <ArrowRightBold />
40 </el-icon> 42 </el-icon>
  43 + </button>
41 </div> 44 </div>
42 - </a>  
43 45
44 <transition name="slide"> 46 <transition name="slide">
45 <ul v-show="activeCategory === index" class="ml-4 space-y-0.5"> 47 <ul v-show="activeCategory === index" class="ml-4 space-y-0.5">
@@ -50,8 +52,10 @@ @@ -50,8 +52,10 @@
50 > 52 >
51 <a 53 <a
52 :href="`#term-${category.id}-${subItem.id}`" 54 :href="`#term-${category.id}-${subItem.id}`"
53 - class="block text-sm py-2 px-3 rounded hover:bg-gray-100 text-[#515c6b] hover:text-[#5961f9] transition-colors"  
54 - @click.stop="" 55 + class="block text-sm py-2 px-3 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-[#515c6b] dark:text-[#c6c9cf] hover:text-[#5961f9] dark:hover:text-white transition-colors"
  56 + @click.stop="
  57 + handleSubCategoryClick($event, category.id, subItem.id)
  58 + "
55 > 59 >
56 {{ subItem.label }} 60 {{ subItem.label }}
57 </a> 61 </a>
@@ -66,35 +70,68 @@ @@ -66,35 +70,68 @@
66 70
67 <script setup lang="ts"> 71 <script setup lang="ts">
68 import type { classifyType } from "~/api/types/classify"; 72 import type { classifyType } from "~/api/types/classify";
69 -import { ArrowRightBold, ArrowDownBold } from "@element-plus/icons-vue"; 73 +import { ArrowRightBold } from "@element-plus/icons-vue";
70 const sortList = useState<classifyType[]>("sortTree"); 74 const sortList = useState<classifyType[]>("sortTree");
71 -// 激活的分类索引  
72 const activeCategory = ref<number | null>(0); 75 const activeCategory = ref<number | null>(0);
73 const route = useRoute(); 76 const route = useRoute();
74 const router = useRouter(); 77 const router = useRouter();
  78 +const config = useRuntimeConfig();
  79 +const { setActiveSubCategory } = useActiveSubCategory();
75 80
76 -// 切换分类展开状态  
77 -const toggleCategory = (event: any, id: number, index: number) => { 81 +const toggleExpand = (index: number) => {
78 if (activeCategory.value === index) { 82 if (activeCategory.value === index) {
79 activeCategory.value = null; 83 activeCategory.value = null;
80 } else { 84 } else {
81 activeCategory.value = index; 85 activeCategory.value = index;
82 } 86 }
83 - event?.preventDefault();  
84 - if (route.path === "/") {  
85 - document.getElementById(`term-${id}`)?.scrollIntoView({ 87 +};
  88 +
  89 +const scrollToElement = (elementId: string) => {
  90 + const element = document.getElementById(elementId);
  91 + if (element) {
  92 + element.scrollIntoView({
86 behavior: "smooth", 93 behavior: "smooth",
87 block: "center", 94 block: "center",
88 }); 95 });
  96 + return true;
  97 + }
  98 + return false;
  99 +};
  100 +
  101 +const handleCategoryClick = (
  102 + event: Event,
  103 + id: number,
  104 + SubCategory: classifyType
  105 +) => {
  106 + event.preventDefault();
  107 + if (SubCategory.children && SubCategory.children.length > 0) {
  108 + const targetChildId = `term-${id}-${SubCategory.children[0].id}`;
  109 + setActiveSubCategory(targetChildId);
89 } else { 110 } else {
90 - router.push("/");  
91 - let timer = setTimeout(() => {  
92 - document.getElementById(`term-${id}`)?.scrollIntoView({  
93 - behavior: "smooth",  
94 - block: "center",  
95 - });  
96 - clearTimeout(timer);  
97 - }, 500); 111 + setActiveSubCategory(null);
  112 + }
  113 + const targetId = `term-${id}`;
  114 +
  115 + if (route.path === "/") {
  116 + scrollToElement(targetId);
  117 + } else {
  118 + router.push(`/#${targetId}`);
  119 + }
  120 +};
  121 +
  122 +const handleSubCategoryClick = (
  123 + event: Event,
  124 + parentId: number,
  125 + subId: number
  126 +) => {
  127 + event.preventDefault();
  128 + const targetId = `term-${parentId}-${subId}`;
  129 + setActiveSubCategory(targetId);
  130 +
  131 + if (route.path === "/") {
  132 + scrollToElement(targetId);
  133 + } else {
  134 + router.push(`/#${targetId}`);
98 } 135 }
99 }; 136 };
100 </script> 137 </script>
  1 +<template>
  2 + <canvas ref="canvasRef" class="absolute inset-0 w-full h-full"></canvas>
  3 +</template>
  4 +
  5 +<script lang="ts" setup>
  6 +const props = defineProps<{
  7 + particleCount?: number;
  8 + particleColor?: string;
  9 + lineColor?: string;
  10 + particleSize?: number;
  11 + lineDistance?: number;
  12 + speed?: number;
  13 +}>();
  14 +
  15 +const canvasRef = ref<HTMLCanvasElement | null>(null);
  16 +const { isDark } = useDarkMode();
  17 +
  18 +const config = {
  19 + particleCount: props.particleCount || 80,
  20 + particleColor: props.particleColor || "#5961f9",
  21 + lineColor: props.lineColor || "#5961f9",
  22 + particleSize: props.particleSize || 2,
  23 + lineDistance: props.lineDistance || 120,
  24 + speed: props.speed || 0.5,
  25 +};
  26 +
  27 +interface Particle {
  28 + x: number;
  29 + y: number;
  30 + vx: number;
  31 + vy: number;
  32 + size: number;
  33 +}
  34 +
  35 +let particles: Particle[] = [];
  36 +let animationId: number | null = null;
  37 +let ctx: CanvasRenderingContext2D | null = null;
  38 +let canvas: HTMLCanvasElement | null = null;
  39 +
  40 +const initParticles = () => {
  41 + particles = [];
  42 + if (!canvas) return;
  43 +
  44 + for (let i = 0; i < config.particleCount; i++) {
  45 + particles.push({
  46 + x: Math.random() * canvas.width,
  47 + y: Math.random() * canvas.height,
  48 + vx: (Math.random() - 0.5) * config.speed,
  49 + vy: (Math.random() - 0.5) * config.speed,
  50 + size: Math.random() * config.particleSize + 1,
  51 + });
  52 + }
  53 +};
  54 +
  55 +const drawParticles = () => {
  56 + if (!ctx || !canvas) return;
  57 +
  58 + ctx.clearRect(0, 0, canvas.width, canvas.height);
  59 +
  60 + const particleAlpha = isDark.value ? 0.6 : 0.8;
  61 + const lineAlpha = isDark.value ? 0.15 : 0.2;
  62 +
  63 + particles.forEach((particle, i) => {
  64 + particle.x += particle.vx;
  65 + particle.y += particle.vy;
  66 +
  67 + if (particle.x < 0 || particle.x > canvas!.width) particle.vx *= -1;
  68 + if (particle.y < 0 || particle.y > canvas!.height) particle.vy *= -1;
  69 +
  70 + ctx!.beginPath();
  71 + ctx!.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
  72 + ctx!.fillStyle = `${config.particleColor}${Math.round(particleAlpha * 255)
  73 + .toString(16)
  74 + .padStart(2, "0")}`;
  75 + ctx!.fill();
  76 +
  77 + for (let j = i + 1; j < particles.length; j++) {
  78 + const dx = particles[j].x - particle.x;
  79 + const dy = particles[j].y - particle.y;
  80 + const distance = Math.sqrt(dx * dx + dy * dy);
  81 +
  82 + if (distance < config.lineDistance) {
  83 + const opacity = (1 - distance / config.lineDistance) * lineAlpha;
  84 + ctx!.beginPath();
  85 + ctx!.moveTo(particle.x, particle.y);
  86 + ctx!.lineTo(particles[j].x, particles[j].y);
  87 + ctx!.strokeStyle = `${config.lineColor}${Math.round(opacity * 255)
  88 + .toString(16)
  89 + .padStart(2, "0")}`;
  90 + ctx!.lineWidth = 1;
  91 + ctx!.stroke();
  92 + }
  93 + }
  94 + });
  95 +
  96 + animationId = requestAnimationFrame(drawParticles);
  97 +};
  98 +
  99 +const handleResize = () => {
  100 + if (!canvas || !ctx) return;
  101 +
  102 + const parent = canvas.parentElement;
  103 + if (parent) {
  104 + canvas.width = parent.offsetWidth;
  105 + canvas.height = parent.offsetHeight;
  106 + }
  107 +
  108 + initParticles();
  109 +};
  110 +
  111 +const handleMouse = (e: MouseEvent) => {
  112 + if (!canvas) return;
  113 +
  114 + const rect = canvas.getBoundingClientRect();
  115 + const mouseX = e.clientX - rect.left;
  116 + const mouseY = e.clientY - rect.top;
  117 +
  118 + particles.forEach((particle) => {
  119 + const dx = mouseX - particle.x;
  120 + const dy = mouseY - particle.y;
  121 + const distance = Math.sqrt(dx * dx + dy * dy);
  122 +
  123 + if (distance < 100) {
  124 + const force = (100 - distance) / 100;
  125 + particle.vx -= (dx / distance) * force * 0.5;
  126 + particle.vy -= (dy / distance) * force * 0.5;
  127 + }
  128 + });
  129 +};
  130 +
  131 +onMounted(() => {
  132 + canvas = canvasRef.value;
  133 + if (!canvas) return;
  134 +
  135 + ctx = canvas.getContext("2d");
  136 + if (!ctx) return;
  137 +
  138 + handleResize();
  139 + drawParticles();
  140 +
  141 + window.addEventListener("resize", handleResize);
  142 + canvas.addEventListener("mousemove", handleMouse);
  143 +});
  144 +
  145 +onUnmounted(() => {
  146 + if (animationId) {
  147 + cancelAnimationFrame(animationId);
  148 + }
  149 + window.removeEventListener("resize", handleResize);
  150 + if (canvas) {
  151 + canvas.removeEventListener("mousemove", handleMouse);
  152 + }
  153 +});
  154 +
  155 +watch(isDark, () => {
  156 + drawParticles();
  157 +});
  158 +</script>
  1 +<template>
  2 + <div
  3 + v-for="(item, index) in cardList"
  4 + :key="index"
  5 + class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
  6 + >
  7 + <el-popconfirm
  8 + v-if="item.isPopup == '1'"
  9 + class="box-item"
  10 + :title="item.popupContent"
  11 + placement="top-start"
  12 + icon-color="#5961f9"
  13 + width="280"
  14 + :icon="Promotion"
  15 + confirm-button-text="确认前往"
  16 + cancel-button-text="取消"
  17 + @confirm="onConfirm(item.id)"
  18 + >
  19 + <template #reference>
  20 + <a
  21 + :href="config.public.baseUrl + '/site-details/' + item.id"
  22 + target="_blank"
  23 + @click.stop="onNuxtLink"
  24 + >
  25 + <div class="group p-3">
  26 + <div class="flex items-start space-x-4">
  27 + <img
  28 + loading="lazy"
  29 + :src="config.public.apiUrl + item.image"
  30 + :alt="item.title"
  31 + class="w-10 h-10 md:w-14 md:h-14 object-cover rounded-lg"
  32 + />
  33 + <div>
  34 + <h3
  35 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
  36 + >
  37 + {{ item.title }}
  38 + </h3>
  39 + <p
  40 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
  41 + >
  42 + {{ item.description }}
  43 + </p>
  44 + </div>
  45 + </div>
  46 + </div>
  47 + </a>
  48 + </template>
  49 + </el-popconfirm>
  50 + <a
  51 + v-else
  52 + :href="config.public.baseUrl + '/site-details/' + item.id"
  53 + target="_blank"
  54 + >
  55 + <div class="group p-3">
  56 + <div class="flex items-start space-x-4">
  57 + <img
  58 + loading="lazy"
  59 + :src="config.public.apiUrl + item.image"
  60 + :alt="item.title"
  61 + class="w-10 h-10 md:w-14 md:h-14 object-cover rounded-lg"
  62 + />
  63 + <div>
  64 + <h3
  65 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
  66 + >
  67 + {{ item.title }}
  68 + </h3>
  69 + <p
  70 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
  71 + >
  72 + {{ item.description }}
  73 + </p>
  74 + </div>
  75 + </div>
  76 + </div>
  77 + </a>
  78 + </div>
  79 +</template>
  80 +
  81 +<script lang="ts" setup>
  82 +import { Promotion } from "@element-plus/icons-vue";
  83 +defineProps<{
  84 + cardList: any[];
  85 +}>();
  86 +
  87 +const config = useRuntimeConfig();
  88 +// 阻止默认行为
  89 +function onNuxtLink(event: any) {
  90 + event.preventDefault();
  91 +}
  92 +// 点击确认跳转
  93 +function onConfirm(id: number) {
  94 + window.open(`/site-details/${id}`);
  95 +}
  96 +</script>
@@ -6,13 +6,13 @@ @@ -6,13 +6,13 @@
6 class="iconfont text-lg mr-2" 6 class="iconfont text-lg mr-2"
7 :class="[`icon-${childData.icon}`]" 7 :class="[`icon-${childData.icon}`]"
8 ></i> 8 ></i>
9 - <h4 class="text-xl text-[#555]"> 9 + <h4 class="text-xl text-[#555] dark:text-[#888]">
10 {{ childData.label }} 10 {{ childData.label }}
11 </h4> 11 </h4>
12 </div> 12 </div>
13 <div class="flex items-center flex-auto"> 13 <div class="flex items-center flex-auto">
14 <div 14 <div
15 - class="scroll-container relative bg-black/10 rounded-[50px] md:overflow-hidden p-[3px] overflow-y-auto" 15 + class="scroll-container relative bg-black/10 dark:bg-[#17181a] rounded-[50px] md:overflow-hidden p-[3px] overflow-y-auto"
16 slidertab="sliderTab" 16 slidertab="sliderTab"
17 > 17 >
18 <ul 18 <ul
@@ -22,13 +22,24 @@ @@ -22,13 +22,24 @@
22 > 22 >
23 <li 23 <li
24 v-for="(child, index) in childData.children" 24 v-for="(child, index) in childData.children"
  25 + :key="child.id"
25 class="h-auto w-auto cursor-pointer rounded-[100px] transition-all duration-350" 26 class="h-auto w-auto cursor-pointer rounded-[100px] transition-all duration-350"
26 - :class="[index === currentFilter ? 'bg-[#5961f9]' : '']" 27 + :class="[
  28 + activeSubCategoryId === `term-${childData.id}-${child.id}` ||
  29 + index === currentFilter
  30 + ? 'bg-[#5961f9]'
  31 + : '',
  32 + ]"
27 > 33 >
28 <a 34 <a
29 - :id="`#term-${childData.id}-${child.id}`" 35 + :id="`term-${childData.id}-${child.id}`"
30 class="h-7 leading-7 px-3 block relative text-[#888] text-center md:text-sm text-xs md:leading-7" 36 class="h-7 leading-7 px-3 block relative text-[#888] text-center md:text-sm text-xs md:leading-7"
31 - :class="[index === currentFilter ? 'text-white' : '']" 37 + :class="[
  38 + activeSubCategoryId === `term-${childData.id}-${child.id}` ||
  39 + index === currentFilter
  40 + ? 'text-white'
  41 + : '',
  42 + ]"
32 style="transition: 0.25s" 43 style="transition: 0.25s"
33 :href="`#tab-${childData.id}-${child.id}`" 44 :href="`#tab-${childData.id}-${child.id}`"
34 @click.stop="onClick($event, child.alias, index)" 45 @click.stop="onClick($event, child.alias, index)"
@@ -55,13 +66,17 @@ @@ -55,13 +66,17 @@
55 <div 66 <div
56 v-for="(childContentItem, childContentIndex) in childData.children" 67 v-for="(childContentItem, childContentIndex) in childData.children"
57 :key="childContentItem.id" 68 :key="childContentItem.id"
58 - v-show="currentFilter === childContentIndex" 69 + v-show="
  70 + activeSubCategoryId === `term-${childData.id}-${childContentItem.id}` ||
  71 + childContentIndex === currentFilter
  72 + "
  73 + :id="`tab-${childData.id}-${childContentItem.id}`"
59 class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 md:gap-6 gap-4" 74 class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 md:gap-6 gap-4"
60 > 75 >
61 <div 76 <div
62 v-for="appItem in childContentItem.appVos" 77 v-for="appItem in childContentItem.appVos"
63 :key="appItem.id" 78 :key="appItem.id"
64 - class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300" 79 + class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
65 > 80 >
66 <el-popconfirm 81 <el-popconfirm
67 v-if="appItem.isPopup == '1'" 82 v-if="appItem.isPopup == '1'"
@@ -90,12 +105,12 @@ @@ -90,12 +105,12 @@
90 /> 105 />
91 <div> 106 <div>
92 <h3 107 <h3
93 - class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]" 108 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
94 > 109 >
95 {{ appItem.title }} 110 {{ appItem.title }}
96 </h3> 111 </h3>
97 <p 112 <p
98 - class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1" 113 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
99 > 114 >
100 {{ appItem.description }} 115 {{ appItem.description }}
101 </p> 116 </p>
@@ -120,12 +135,12 @@ @@ -120,12 +135,12 @@
120 /> 135 />
121 <div> 136 <div>
122 <h3 137 <h3
123 - class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]" 138 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
124 > 139 >
125 {{ appItem.title }} 140 {{ appItem.title }}
126 </h3> 141 </h3>
127 <p 142 <p
128 - class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1" 143 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
129 > 144 >
130 {{ appItem.description }} 145 {{ appItem.description }}
131 </p> 146 </p>
@@ -143,24 +158,90 @@ import { Promotion } from "@element-plus/icons-vue"; @@ -143,24 +158,90 @@ import { Promotion } from "@element-plus/icons-vue";
143 const props = defineProps<{ 158 const props = defineProps<{
144 childData: any; 159 childData: any;
145 }>(); 160 }>();
146 - 161 +const route = useRoute();
147 const childAlias = ref(props.childData.children[0].alias); 162 const childAlias = ref(props.childData.children[0].alias);
148 const config = useRuntimeConfig(); 163 const config = useRuntimeConfig();
149 -// 阻止默认行为 164 +const { activeSubCategoryId, setActiveSubCategory } = useActiveSubCategory();
  165 +
  166 +// 监听activeSubCategoryId的变化,当变化是把currentFilter更新为-1
  167 +watch(activeSubCategoryId, (newValue, oldValue) => {
  168 + if (newValue !== oldValue) {
  169 + currentFilter.value = -1;
  170 + }
  171 +});
  172 +
150 function onNuxtLink(event: any) { 173 function onNuxtLink(event: any) {
151 event.preventDefault(); 174 event.preventDefault();
152 } 175 }
153 -// 点击确认跳转 176 +
154 function onConfirm(id: number) { 177 function onConfirm(id: number) {
155 window.open(`/site-details/${id}`); 178 window.open(`/site-details/${id}`);
156 } 179 }
157 -// 导航样式内容 180 +
158 const currentFilter = ref(0); 181 const currentFilter = ref(0);
159 182
160 -// 切换分类内容  
161 function onClick(event: any, alias: string, index: number) { 183 function onClick(event: any, alias: string, index: number) {
162 event?.preventDefault(); 184 event?.preventDefault();
163 childAlias.value = alias; 185 childAlias.value = alias;
164 currentFilter.value = index; 186 currentFilter.value = index;
  187 + const targetId = `term-${props.childData.id}-${props.childData.children[index].id}`;
  188 + setActiveSubCategory(targetId);
165 } 189 }
  190 +
  191 +const scrollToSubCategory = () => {
  192 + if (import.meta.client) {
  193 + const hash = window.location.hash;
  194 + if (hash && hash.startsWith("#term-")) {
  195 + const parts = hash.replace("#term-", "").split("-");
  196 + if (parts.length === 2) {
  197 + const parentId = parts[0];
  198 + const childId = parts[1];
  199 +
  200 + if (parentId === String(props.childData.id)) {
  201 + const index = props.childData.children.findIndex(
  202 + (child: any) => String(child.id) === childId
  203 + );
  204 + if (index !== -1) {
  205 + currentFilter.value = index;
  206 + childAlias.value = props.childData.children[index].alias;
  207 + const targetId = `term-${parentId}-${childId}`;
  208 + setActiveSubCategory(targetId);
  209 +
  210 + nextTick(() => {
  211 + const element = document.getElementById(targetId);
  212 + if (element) {
  213 + element.scrollIntoView({
  214 + behavior: "smooth",
  215 + block: "center",
  216 + });
  217 + }
  218 + });
  219 + }
  220 + }
  221 + } else if (parts.length === 1) {
  222 + const parentId = parts[0];
  223 + if (parentId === String(props.childData.id)) {
  224 + nextTick(() => {
  225 + const element = document.getElementById(`term-${parentId}`);
  226 + if (element) {
  227 + element.scrollIntoView({
  228 + behavior: "smooth",
  229 + block: "center",
  230 + });
  231 + }
  232 + });
  233 + }
  234 + }
  235 + }
  236 + }
  237 +};
  238 +
  239 +onMounted(() => {
  240 + scrollToSubCategory();
  241 + window.addEventListener("hashchange", scrollToSubCategory);
  242 +});
  243 +
  244 +onUnmounted(() => {
  245 + window.removeEventListener("hashchange", scrollToSubCategory);
  246 +});
166 </script> 247 </script>
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <!-- 分类导航 --> 2 <!-- 分类导航 -->
3 <div class="mb-6"> 3 <div class="mb-6">
4 <div class="flex items-center mb-2"> 4 <div class="flex items-center mb-2">
5 - <h4 class="text-gray text-lg m-0"> 5 + <h4 class="text-gray dark:text-[#888] text-lg m-0">
6 <i 6 <i
7 :id="`term-${childData.id}`" 7 :id="`term-${childData.id}`"
8 class="iconfont text-lg mr-2" 8 class="iconfont text-lg mr-2"
@@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
12 </h4> 12 </h4>
13 <div class="flex-auto"></div> 13 <div class="flex-auto"></div>
14 <a 14 <a
15 - class="hidden md:block text-xs ml-2 text-[#282a2d] hover:text-[#5961f9]" 15 + class="hidden md:block text-xs ml-2 text-[#282a2d] hover:text-[#5961f9] dark:text-[#989da1]"
16 :href="`${config.public.baseUrl}/category/${childData.alias}`" 16 :href="`${config.public.baseUrl}/category/${childData.alias}`"
17 >查看更多 &gt;&gt;</a 17 >查看更多 &gt;&gt;</a
18 > 18 >
@@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
30 <div 30 <div
31 v-for="(item, index) in childData.appVos" 31 v-for="(item, index) in childData.appVos"
32 :key="index" 32 :key="index"
33 - class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300" 33 + class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
34 > 34 >
35 <el-popconfirm 35 <el-popconfirm
36 v-if="item.isPopup == '1'" 36 v-if="item.isPopup == '1'"
@@ -60,12 +60,12 @@ @@ -60,12 +60,12 @@
60 /> 60 />
61 <div> 61 <div>
62 <h3 62 <h3
63 - class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]" 63 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
64 > 64 >
65 {{ item.title }} 65 {{ item.title }}
66 </h3> 66 </h3>
67 <p 67 <p
68 - class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1" 68 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
69 > 69 >
70 {{ item.description }} 70 {{ item.description }}
71 </p> 71 </p>
@@ -86,12 +86,12 @@ @@ -86,12 +86,12 @@
86 /> 86 />
87 <div> 87 <div>
88 <h3 88 <h3
89 - class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]" 89 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
90 > 90 >
91 {{ item.title }} 91 {{ item.title }}
92 </h3> 92 </h3>
93 <p 93 <p
94 - class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1" 94 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
95 > 95 >
96 {{ item.description }} 96 {{ item.description }}
97 </p> 97 </p>
@@ -36,7 +36,7 @@ @@ -36,7 +36,7 @@
36 <div 36 <div
37 v-for="(item, index) in recommendList" 37 v-for="(item, index) in recommendList"
38 :key="index" 38 :key="index"
39 - class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300" 39 + class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
40 > 40 >
41 <el-popconfirm 41 <el-popconfirm
42 v-if="item.isPopup == '1'" 42 v-if="item.isPopup == '1'"
@@ -66,12 +66,12 @@ @@ -66,12 +66,12 @@
66 /> 66 />
67 <div> 67 <div>
68 <h3 68 <h3
69 - class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]" 69 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
70 > 70 >
71 {{ item.title }} 71 {{ item.title }}
72 </h3> 72 </h3>
73 <p 73 <p
74 - class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1" 74 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
75 > 75 >
76 {{ item.description }} 76 {{ item.description }}
77 </p> 77 </p>
@@ -96,12 +96,12 @@ @@ -96,12 +96,12 @@
96 /> 96 />
97 <div> 97 <div>
98 <h3 98 <h3
99 - class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]" 99 + class="font-bold dark:text-[#c6c9cf] md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9] dark:group-hover:text-white"
100 > 100 >
101 {{ item.title }} 101 {{ item.title }}
102 </h3> 102 </h3>
103 <p 103 <p
104 - class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1" 104 + class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
105 > 105 >
106 {{ item.description }} 106 {{ item.description }}
107 </p> 107 </p>
  1 +export const useActiveSubCategory = () => {
  2 + const activeSubCategoryId = useState<string | null>("activeSubCategoryId", () => null);
  3 +
  4 + const setActiveSubCategory = (id: string | null) => {
  5 + activeSubCategoryId.value = id;
  6 + };
  7 +
  8 + const clearActiveSubCategory = () => {
  9 + activeSubCategoryId.value = null;
  10 + };
  11 +
  12 + return {
  13 + activeSubCategoryId,
  14 + setActiveSubCategory,
  15 + clearActiveSubCategory,
  16 + };
  17 +};
  1 +export const useDarkMode = () => {
  2 + const isDark = useState<boolean>("darkMode", () => false);
  3 +
  4 + const toggleDark = () => {
  5 + isDark.value = !isDark.value;
  6 + updateTheme();
  7 + };
  8 +
  9 + const updateTheme = () => {
  10 + if (import.meta.client) {
  11 + if (isDark.value) {
  12 + document.documentElement.classList.add("dark");
  13 + localStorage.setItem("theme", "dark");
  14 + } else {
  15 + document.documentElement.classList.remove("dark");
  16 + localStorage.setItem("theme", "light");
  17 + }
  18 + }
  19 + };
  20 +
  21 + const initTheme = () => {
  22 + if (import.meta.client) {
  23 + const savedTheme = localStorage.getItem("theme");
  24 + const prefersDark = window.matchMedia(
  25 + "(prefers-color-scheme: dark)"
  26 + ).matches;
  27 +
  28 + if (savedTheme === "dark" || (!savedTheme && prefersDark)) {
  29 + isDark.value = true;
  30 + document.documentElement.classList.add("dark");
  31 + } else {
  32 + isDark.value = false;
  33 + document.documentElement.classList.remove("dark");
  34 + }
  35 + }
  36 + };
  37 +
  38 + return {
  39 + isDark,
  40 + toggleDark,
  41 + initTheme,
  42 + };
  43 +};
@@ -10,8 +10,15 @@ @@ -10,8 +10,15 @@
10 <AppFooter /> 10 <AppFooter />
11 </div> 11 </div>
12 </div> 12 </div>
  13 + <AppFloatingButtons />
13 </template> 14 </template>
14 15
15 -<script setup></script> 16 +<script lang="ts" setup>
  17 +const { initTheme } = useDarkMode();
  18 +
  19 +onMounted(() => {
  20 + initTheme();
  21 +});
  22 +</script>
16 23
17 <style></style> 24 <style></style>
@@ -12,6 +12,22 @@ export default defineNuxtConfig({ @@ -12,6 +12,22 @@ export default defineNuxtConfig({
12 '@nuxtjs/tailwindcss', 12 '@nuxtjs/tailwindcss',
13 '@element-plus/nuxt' 13 '@element-plus/nuxt'
14 ], 14 ],
  15 + tailwindcss: {
  16 + cssPath: '~/assets/css/tailwind.css',
  17 + config: {
  18 + darkMode: 'class',
  19 + content: [
  20 + './components/**/*.{js,vue,ts}',
  21 + './layouts/**/*.vue',
  22 + './pages/**/*.vue',
  23 + './plugins/**/*.{js,ts}',
  24 + './app.vue',
  25 + ],
  26 + theme: {
  27 + extend: {},
  28 + },
  29 + }
  30 + },
15 features: { 31 features: {
16 inlineStyles: false, 32 inlineStyles: false,
17 devLogs: false, 33 devLogs: false,
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -import type { appListType, appType } from "~/api/types/app";  
3 -import type { adListType } from "~/api/types/ad";  
4 import { getAppList, getAllApp } from "~/api/app"; 2 import { getAppList, getAllApp } from "~/api/app";
5 import type { webSiteType } from "~/api/types/webSite"; 3 import type { webSiteType } from "~/api/types/webSite";
6 import { getAdList } from "~/api/ad"; 4 import { getAdList } from "~/api/ad";
7 -const recommendList = ref<appType[]>([]);  
8 -const appList = ref<appListType[]>([]);  
9 -const adList = ref<adListType | null>(null);  
10 -// 获取轮播广告  
11 -const adRes = await getAdList();  
12 -adList.value = adRes[0];  
13 -// 获取推荐应用  
14 -const recommendRes = await getAppList({ 5 +
  6 +const webSite = useState<webSiteType>("webSite");
  7 +const route = useRoute();
  8 +console.log(route);
  9 +const { data: adList } = await useAsyncData(
  10 + "adList",
  11 + async () => {
  12 + const res = await getAdList();
  13 + return res[0] || null;
  14 + },
  15 + {
  16 + server: true,
  17 + lazy: false,
  18 + }
  19 +);
  20 +
  21 +const { data: recommendList } = await useAsyncData(
  22 + "recommendList",
  23 + async () => {
  24 + const res = await getAppList({
15 pageSize: 10, 25 pageSize: 10,
16 pageNum: 1, 26 pageNum: 1,
17 isRecommend: "1", 27 isRecommend: "1",
18 -});  
19 -recommendList.value = recommendRes.rows;  
20 -// 获取全部应用  
21 -const allRes = await getAllApp();  
22 -appList.value = allRes.data;  
23 -const webSite = useState<webSiteType>("webSite"); 28 + });
  29 + return res.rows || [];
  30 + },
  31 + {
  32 + server: true,
  33 + lazy: false,
  34 + }
  35 +);
  36 +
  37 +const { data: appList } = await useAsyncData(
  38 + "appList",
  39 + async () => {
  40 + const res = await getAllApp();
  41 + return res.data || [];
  42 + },
  43 + {
  44 + server: true,
  45 + lazy: false,
  46 + }
  47 +);
24 48
25 useHead({ 49 useHead({
26 title: webSite.value.webname, 50 title: webSite.value.webname,
@@ -67,69 +91,73 @@ useHead({ @@ -67,69 +91,73 @@ useHead({
67 { 91 {
68 "@type": "WebSite", 92 "@type": "WebSite",
69 "@id": "https://aiboxgo.com/#website", 93 "@id": "https://aiboxgo.com/#website",
70 - "url": "https://aiboxgo.com/",  
71 - "name": webSite.value.webname,  
72 - "description": webSite.value.webdescription,  
73 - "inLanguage": "zh-CN",  
74 - "potentialAction": { 94 + url: "https://aiboxgo.com/",
  95 + name: webSite.value.webname,
  96 + description: webSite.value.webdescription,
  97 + inLanguage: "zh-CN",
  98 + potentialAction: {
75 "@type": "SearchAction", 99 "@type": "SearchAction",
76 - "target": { 100 + target: {
77 "@type": "EntryPoint", 101 "@type": "EntryPoint",
78 - "urlTemplate": "https://aiboxgo.com/search?keyword={search_term_string}" 102 + urlTemplate:
  103 + "https://aiboxgo.com/search?keyword={search_term_string}",
  104 + },
  105 + "query-input": "required name=search_term_string",
79 }, 106 },
80 - "query-input": "required name=search_term_string"  
81 - }  
82 }, 107 },
83 { 108 {
84 "@type": "Organization", 109 "@type": "Organization",
85 "@id": "https://aiboxgo.com/#organization", 110 "@id": "https://aiboxgo.com/#organization",
86 - "name": webSite.value.webname,  
87 - "url": "https://aiboxgo.com/",  
88 - "logo": { 111 + name: webSite.value.webname,
  112 + url: "https://aiboxgo.com/",
  113 + logo: {
89 "@type": "ImageObject", 114 "@type": "ImageObject",
90 - "url": "https://aiboxgo.com/favicon.ico" 115 + url: "https://aiboxgo.com/favicon.ico",
91 }, 116 },
92 - "sameAs": [] 117 + sameAs: [],
93 }, 118 },
94 { 119 {
95 "@type": "CollectionPage", 120 "@type": "CollectionPage",
96 "@id": "https://aiboxgo.com/", 121 "@id": "https://aiboxgo.com/",
97 - "url": "https://aiboxgo.com/",  
98 - "name": webSite.value.webname,  
99 - "description": webSite.value.webdescription,  
100 - "isPartOf": {  
101 - "@id": "https://aiboxgo.com/#website"  
102 - },  
103 - "about": {  
104 - "@id": "https://aiboxgo.com/#organization"  
105 - }  
106 - }  
107 - ]  
108 - })  
109 - }  
110 - ] 122 + url: "https://aiboxgo.com/",
  123 + name: webSite.value.webname,
  124 + description: webSite.value.webdescription,
  125 + isPartOf: {
  126 + "@id": "https://aiboxgo.com/#website",
  127 + },
  128 + about: {
  129 + "@id": "https://aiboxgo.com/#organization",
  130 + },
  131 + },
  132 + ],
  133 + }),
  134 + },
  135 + ],
111 }); 136 });
112 </script> 137 </script>
113 138
114 <template> 139 <template>
115 - <div class="flex flex-col min-h-screen bg-white">  
116 - <main class="flex-grow md:p-6 p-2 bg-white">  
117 - <!-- 轮播广告区域 -->  
118 - <ADSwiperCarousel :adSwiperList="adList" />  
119 - <!-- Banner 区域 --> 140 + <div
  141 + class="flex flex-col min-h-screen bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
  142 + >
  143 + <main
  144 + class="flex-grow md:p-6 p-2 bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
  145 + >
  146 + <ADSwiperCarousel v-if="adList" :adSwiperList="adList" />
120 <HomeBanner /> 147 <HomeBanner />
121 - <!-- 广告区域 -->  
122 - <!-- 工具展示区 -->  
123 <section class="md:mb-12 mb-6"> 148 <section class="md:mb-12 mb-6">
124 - <!-- 推荐工具区 -->  
125 <HomeRecommend 149 <HomeRecommend
  150 + v-if="recommendList && recommendList.length > 0"
126 :recommendList="recommendList" 151 :recommendList="recommendList"
127 navTitle="推荐工具" 152 navTitle="推荐工具"
128 navIcon="star" 153 navIcon="star"
129 /> 154 />
130 155
131 - <div v-for="appItem in appList" class="md:mb-12 mb-6">  
132 - <!-- 分类导航及内容 --> 156 + <div
  157 + v-for="appItem in appList"
  158 + :key="appItem?.id"
  159 + class="md:mb-12 mb-6"
  160 + >
133 <HomeContent :appData="appItem" /> 161 <HomeContent :appData="appItem" />
134 </div> 162 </div>
135 </section> 163 </section>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 -import { getAppDetail } from "~/api/app";  
3 -import type { appDetail, Types } from "~/api/types/app"; 2 +import { getAppDetail, getAppList } from "~/api/app";
  3 +import type { appDetail, Types, appType } from "~/api/types/app";
4 import type { webSiteType } from "~/api/types/webSite"; 4 import type { webSiteType } from "~/api/types/webSite";
5 const route = useRoute(); 5 const route = useRoute();
6 const config = useRuntimeConfig(); 6 const config = useRuntimeConfig();
@@ -14,12 +14,12 @@ const DetailData = ref<appDetail>({ @@ -14,12 +14,12 @@ const DetailData = ref<appDetail>({
14 types: [], 14 types: [],
15 }); 15 });
16 const webSite = useState<webSiteType>("webSite"); 16 const webSite = useState<webSiteType>("webSite");
  17 +const relatedApps = ref<appType[]>([]);
  18 +
17 function mergeDuplicates(data: Types[]) { 19 function mergeDuplicates(data: Types[]) {
18 const map = new Map(); 20 const map = new Map();
19 -  
20 data.forEach((item) => { 21 data.forEach((item) => {
21 if (!map.has(item.id)) { 22 if (!map.has(item.id)) {
22 - // 如果是第一次遇到这个id,创建新对象  
23 map.set(item.id, { 23 map.set(item.id, {
24 id: item.id, 24 id: item.id,
25 label: item.label, 25 label: item.label,
@@ -27,9 +27,7 @@ function mergeDuplicates(data: Types[]) { @@ -27,9 +27,7 @@ function mergeDuplicates(data: Types[]) {
27 children: [...(item.children || [])], 27 children: [...(item.children || [])],
28 }); 28 });
29 } else { 29 } else {
30 - // 如果已经存在,合并children  
31 const existing = map.get(item.id); 30 const existing = map.get(item.id);
32 - // 避免重复的子项(基于子项id)  
33 const existingChildIds = new Set( 31 const existingChildIds = new Set(
34 existing.children.map((child: any) => child.id) 32 existing.children.map((child: any) => child.id)
35 ); 33 );
@@ -40,16 +38,26 @@ function mergeDuplicates(data: Types[]) { @@ -40,16 +38,26 @@ function mergeDuplicates(data: Types[]) {
40 }); 38 });
41 } 39 }
42 }); 40 });
43 -  
44 return Array.from(map.values()); 41 return Array.from(map.values());
45 } 42 }
46 43
47 -// 获取详情数据  
48 const detailRes = await getAppDetail(Number(route.params.id)); 44 const detailRes = await getAppDetail(Number(route.params.id));
49 DetailData.value = detailRes.data; 45 DetailData.value = detailRes.data;
50 DetailData.value.types = mergeDuplicates(detailRes.data.types); 46 DetailData.value.types = mergeDuplicates(detailRes.data.types);
51 -  
52 -console.log("详情数据", DetailData.value); 47 +if (DetailData.value.types?.length > 0) {
  48 + const firstType = DetailData.value.types[0];
  49 + const typeAlias = firstType.alias || firstType.children?.[0]?.alias;
  50 + if (typeAlias) {
  51 + const relatedRes = await getAppList({
  52 + pageNum: 1,
  53 + pageSize: 8,
  54 + typeAlias: typeAlias,
  55 + });
  56 + relatedApps.value = relatedRes.rows
  57 + .filter((app: appType) => app.id !== DetailData.value.id)
  58 + .slice(0, 6);
  59 + }
  60 +}
53 61
54 useHead({ 62 useHead({
55 title: DetailData.value.popupContent 63 title: DetailData.value.popupContent
@@ -177,37 +185,62 @@ useHead({ @@ -177,37 +185,62 @@ useHead({
177 </script> 185 </script>
178 186
179 <template> 187 <template>
180 - <div class="flex flex-col min-h-screen bg-white">  
181 - <main class="flex-grow md:p-6 bg-white p-1">  
182 - <!-- Top Application Info Bar -->  
183 - <header  
184 - v-show="DetailData.types.length > 0"  
185 - class="bg-white shadow-sm md:py-4 md:px-8 py-2 px-4 flex md:items-center md:justify-between flex-col md:flex-row" 188 + <div
  189 + class="flex flex-col min-h-screen bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
186 > 190 >
187 - <div class="flex items-center space-x-4"> 191 + <main class="flex-grow bg-white dark:bg-[#1a1b1d]">
  192 + <div class="relative overflow-hidden">
  193 + <CommonParticleBackground
  194 + :particleCount="60"
  195 + particleColor="#5961f9"
  196 + lineColor="#7c3aed"
  197 + :particleSize="2"
  198 + :lineDistance="100"
  199 + :speed="0.3"
  200 + />
  201 + <div
  202 + class="absolute inset-0 bg-gradient-to-r from-[#5961f9]/10 via-[#7c3aed]/10 to-[#a855f7]/10 dark:from-[#5961f9]/5 dark:via-[#7c3aed]/5 dark:to-[#a855f7]/5"
  203 + ></div>
  204 +
  205 + <div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  206 + <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
  207 + <div class="lg:col-span-2 flex justify-center lg:justify-end">
  208 + <div class="relative group">
  209 + <div
  210 + class="absolute -inset-1 bg-gradient-to-r from-[#5961f9] to-[#a855f7] rounded-2xl blur opacity-30 group-hover:opacity-50 transition duration-300"
  211 + ></div>
188 <img 212 <img
189 :src="config.public.apiUrl + DetailData.image" 213 :src="config.public.apiUrl + DetailData.image"
190 :alt="DetailData.title" 214 :alt="DetailData.title"
191 - class="w-16 h-16 object-contain" 215 + class="relative w-24 h-24 md:w-32 md:h-32 object-contain bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-3"
192 loading="lazy" 216 loading="lazy"
193 /> 217 />
194 - <div>  
195 - <h1 class="text-2xl font-bold text-[#5961f9]"> 218 + </div>
  219 + </div>
  220 +
  221 + <div class="lg:col-span-7 space-y-4">
  222 + <div class="flex items-center gap-3 flex-wrap">
  223 + <h1
  224 + class="text-2xl md:text-3xl font-bold text-[#282a2d] dark:text-[#c6c9cf]"
  225 + >
196 {{ DetailData.title }} 226 {{ DetailData.title }}
197 </h1> 227 </h1>
198 - <p class="text-sm text-gray-600 mt-1">  
199 - {{ DetailData.description }}  
200 - </p>  
201 - <div class="mt-2 flex items-center space-x-2">  
202 - <div  
203 - v-for="tag in DetailData.types"  
204 - class="flex items-center space-x-2" 228 + <span
  229 + v-if="DetailData.popupContent"
  230 + class="px-3 py-1 bg-gradient-to-r from-[#5961f9] to-[#a855f7] text-white text-xs rounded-full"
205 > 231 >
206 - <template v-if="tag.children.length > 0"> 232 + {{ DetailData.popupContent }}
  233 + </span>
  234 + </div>
  235 +
  236 + <div class="flex flex-wrap gap-2">
  237 + <template v-for="tag in DetailData.types" :key="tag.id">
  238 + <template v-if="tag.children && tag.children.length > 0">
207 <NuxtLink 239 <NuxtLink
208 v-for="child in tag.children" 240 v-for="child in tag.children"
  241 + :key="child.id"
209 :to="'/category/' + child.alias" 242 :to="'/category/' + child.alias"
210 - class="px-2 py-1 bg-blue-100 text-[#5961f9] rounded-full text-xs" 243 + class="px-3 py-1.5 bg-white/80 dark:bg-gray-800/80 text-[#5961f9] dark:text-[#8b92f9] rounded-full text-sm hover:bg-[#5961f9] hover:text-white dark:hover:bg-[#5961f9] transition-all duration-300 shadow-sm"
211 > 244 >
212 {{ child.label }} 245 {{ child.label }}
213 </NuxtLink> 246 </NuxtLink>
@@ -215,57 +248,156 @@ useHead({ @@ -215,57 +248,156 @@ useHead({
215 <template v-else> 248 <template v-else>
216 <NuxtLink 249 <NuxtLink
217 :to="'/category/' + tag.alias" 250 :to="'/category/' + tag.alias"
218 - class="px-2 py-1 bg-blue-100 text-[#5961f9] rounded-full text-xs" 251 + class="px-3 py-1.5 bg-white/80 dark:bg-gray-800/80 text-[#5961f9] dark:text-[#8b92f9] rounded-full text-sm hover:bg-[#5961f9] hover:text-white dark:hover:bg-[#5961f9] transition-all duration-300 shadow-sm"
219 > 252 >
220 {{ tag.label }} 253 {{ tag.label }}
221 </NuxtLink> 254 </NuxtLink>
222 </template> 255 </template>
  256 + </template>
223 </div> 257 </div>
224 - </div>  
225 - </div>  
226 - </div>  
227 - <div class="flex md:space-x-3 md:mt-0 mt-4"> 258 +
  259 + <p
  260 + class="text-gray-600 dark:text-gray-300 text-base leading-relaxed line-clamp-3"
  261 + >
  262 + {{ DetailData.description }}
  263 + </p>
  264 +
  265 + <div class="flex flex-wrap gap-3 pt-2">
228 <a 266 <a
229 :href="DetailData.link" 267 :href="DetailData.link"
230 target="_blank" 268 target="_blank"
231 - class="!rounded-button whitespace-nowrap px-4 py-2 bg-[#5961f9] max-[768px]:text-xs text-white hover:bg-blue-600 transition-colors" 269 + rel="noopener noreferrer"
  270 + class="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-[#5961f9] to-[#7c3aed] hover:from-[#4751e8] hover:to-[#6d28d9] text-white font-medium rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-300"
232 > 271 >
233 - <i class="iconfont icon-guide"></i>访问官网 272 + <i class="iconfont icon-guide text-lg"></i>
  273 + <span>立即访问</span>
234 </a> 274 </a>
235 </div> 275 </div>
236 - </header> 276 + </div>
237 277
238 - <main class="relative w-full">  
239 - <!-- 悬浮广告弹窗 -->  
240 - <!-- <div  
241 - class="md:absolute top-0 right-0 md:m-4 z-50 relative max-[768px]:m-auto"  
242 - v-show="showAd"  
243 - :style="{  
244 - width: `${detailAd.width}px`,  
245 - height: `${detailAd.height}px`,  
246 - }" 278 + <div class="lg:col-span-3">
  279 + <div
  280 + class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-2xl shadow-lg p-4 border border-gray-100 dark:border-gray-700"
247 > 281 >
248 <div 282 <div
249 - class="w-full h-full relative"  
250 - v-for="item in detailAd.frontAdVos" 283 + class="text-center text-gray-400 dark:text-gray-500 text-sm"
251 > 284 >
252 - <img  
253 - class="w-full h-full object-contain"  
254 - :src="config.public.baseUrl + item.image"  
255 - :alt="item.title"  
256 - /> 285 + <a
  286 + no_cache=""
  287 + href="https://www.coze.cn/?utm_medium=daohang&amp;utm_source=aikit&amp;utm_content=&amp;utm_id=&amp;utm_campaign=&amp;utm_term=hw_coze_aikit&amp;utm_source_platform="
  288 + rel="external nofollow"
  289 + target="_blank"
  290 + ><img
  291 + src="https://ai-kit.cn/wp-content/uploads/2026/01/kouzi_gd.jpg"
  292 + alt="扣子"
  293 + /></a>
  294 + </div>
  295 + </div>
  296 + </div>
  297 + </div>
  298 + </div>
  299 + </div>
  300 +
  301 + <div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  302 + <article class="prose prose-lg dark:prose-invert max-w-none p-6 md:p-8">
  303 + <div v-html="DetailData.content" class="detail-content"></div>
  304 + </article>
  305 + </div>
  306 +
257 <div 307 <div
258 - class="absolute top-1 right-1 cursor-pointer bg-white w-4 h-4 text-center rounded-[50%] text-xs"  
259 - @click="showAd = false" 308 + v-if="relatedApps.length > 0"
  309 + class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"
  310 + >
  311 + <div class="rounded-2xl p-6">
  312 + <h2
  313 + class="text-xl font-bold text-[#555] dark:text-[#888] mb-6 flex items-center gap-2"
260 > 314 >
261 - X 315 + <i class="iconfont icon-tag" style="font-size: 1.2rem"></i>
  316 + 相关推荐
  317 + </h2>
  318 + <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-4">
  319 + <CommonCard :cardList="relatedApps" />
262 </div> 320 </div>
263 </div> 321 </div>
264 - </div> -->  
265 - <div class="md:max-w-5xl mx-auto md:p-8 p-2 w-full">  
266 - <article v-html="DetailData.content"></article>  
267 </div> 322 </div>
268 </main> 323 </main>
269 - </main>  
270 </div> 324 </div>
271 </template> 325 </template>
  326 +
  327 +<style scoped lang="less">
  328 +.line-clamp-3 {
  329 + display: -webkit-box;
  330 + -webkit-line-clamp: 3;
  331 + -webkit-box-orient: vertical;
  332 + overflow: hidden;
  333 +}
  334 +
  335 +.detail-content {
  336 + @apply text-[#282a2d] dark:text-[#c6c9cf];
  337 +}
  338 +
  339 +.detail-content :deep(h1),
  340 +.detail-content :deep(h2),
  341 +.detail-content :deep(h3),
  342 +.detail-content :deep(h4) {
  343 + @apply text-[#282a2d] my-5 py-1 pl-5 border-l-4 border-[#5961f9] dark:text-[#c6c9cf];
  344 +}
  345 +
  346 +.detail-content :deep(h1) {
  347 + @apply text-2xl;
  348 +}
  349 +
  350 +.detail-content :deep(h2) {
  351 + @apply text-xl;
  352 +}
  353 +
  354 +.detail-content :deep(h3) {
  355 + @apply text-2xl;
  356 +}
  357 +
  358 +.detail-content :deep(p) {
  359 + @apply mb-4 leading-relaxed;
  360 +}
  361 +
  362 +.detail-content :deep(img) {
  363 + @apply rounded-lg max-w-full h-auto my-4;
  364 +}
  365 +
  366 +.detail-content :deep(a) {
  367 + @apply text-[#5961f9] hover:underline;
  368 +}
  369 +
  370 +.detail-content :deep(ul),
  371 +.detail-content :deep(ol) {
  372 + @apply pl-6 mb-4 text-sm list-disc;
  373 +}
  374 +
  375 +.detail-content :deep(li) {
  376 + @apply mb-2;
  377 +}
  378 +
  379 +.detail-content :deep(blockquote) {
  380 + @apply border-l-4 border-[#5961f9] pl-4 italic my-4;
  381 +}
  382 +
  383 +.detail-content :deep(code) {
  384 + @apply bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-sm;
  385 +}
  386 +
  387 +.detail-content :deep(pre) {
  388 + @apply bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto my-4;
  389 +}
  390 +
  391 +.detail-content :deep(table) {
  392 + @apply w-full border-collapse my-4;
  393 +}
  394 +
  395 +.detail-content :deep(th),
  396 +.detail-content :deep(td) {
  397 + @apply border border-gray-200 dark:border-gray-600 px-4 py-2;
  398 +}
  399 +
  400 +.detail-content :deep(th) {
  401 + @apply bg-gray-100 dark:bg-gray-700 font-semibold;
  402 +}
  403 +</style>
1 import { ElLoading } from 'element-plus' 1 import { ElLoading } from 'element-plus'
2 2
3 -const useMyfetch = async (url: any, options?: any, headers?: any) => {  
4 - let loadingInstance 3 +let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
  4 +let requestCount = 0
  5 +
  6 +const showLoading = () => {
  7 + if (requestCount === 0) {
  8 + loadingInstance = ElLoading.service({
  9 + lock: true,
  10 + text: '加载中...',
  11 + background: 'rgba(255, 255, 255, 0.7)',
  12 + })
  13 + }
  14 + requestCount++
  15 +}
  16 +
  17 +const hideLoading = () => {
  18 + requestCount--
  19 + if (requestCount <= 0) {
  20 + requestCount = 0
  21 + if (loadingInstance) {
  22 + loadingInstance.close()
  23 + loadingInstance = null
  24 + }
  25 + }
  26 +}
5 27
  28 +const useMyfetch = async (url: any, options?: any, headers?: any) => {
6 try { 29 try {
7 - loadingInstance = ElLoading.service()  
8 - const config = useRuntimeConfig() // 3.0正式版环境变量要从useRuntimeConfig里的public拿  
9 - const reqUrl = config.public.apiUrl + url // 你的接口地址  
10 - // 不设置key,始终拿到的都是第一个请求的值,参数一样则不会进行第二次请求 30 + showLoading()
  31 + const config = useRuntimeConfig()
  32 + const reqUrl = config.public.apiUrl + url
11 33
12 - // 可以设置默认headers例如  
13 const customHeaders = { 34 const customHeaders = {
14 Authorization: useCookie('accessToken').value, 35 Authorization: useCookie('accessToken').value,
15 ...headers 36 ...headers
@@ -23,17 +44,11 @@ const useMyfetch = async (url: any, options?: any, headers?: any) => { @@ -23,17 +44,11 @@ const useMyfetch = async (url: any, options?: any, headers?: any) => {
23 44
24 const result = data.value as any 45 const result = data.value as any
25 46
26 - if (pending && loadingInstance) {  
27 - loadingInstance.close()  
28 - }  
29 -  
30 return result 47 return result
31 } catch (err) { 48 } catch (err) {
32 return Promise.reject(err) 49 return Promise.reject(err)
33 } finally { 50 } finally {
34 - if (loadingInstance) {  
35 - loadingInstance.close()  
36 - } 51 + hideLoading()
37 } 52 }
38 } 53 }
39 54