作者 xiaoqiu

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

# API地址
NUXT_API_URL="http://htai.aiboxgo.com"
NUXT_API_URL="https://htai.aiboxgo.com"
NUXT_BASE_URL="https://www.aiboxgo.com"
\ No newline at end of file
... ...
不能预览此文件类型
... ... @@ -9,11 +9,42 @@ import { getWebSite } from "~/api/webSite";
import { getClassifyList } from "~/api/classify";
import type { webSiteType } from "~/api/types/webSite";
import type { classifyType } from "~/api/types/classify";
const webSite = useState<webSiteType>("webSite");
const sortList = useState<classifyType[]>("sortTree");
webSite.value = await getWebSite();
sortList.value = await getClassifyList();
const { data: webSiteData } = await useAsyncData(
"webSite",
async () => {
const res = await getWebSite();
return res;
},
{
server: true,
lazy: false,
getCachedData: () => null,
}
);
const { data: sortListData } = await useAsyncData(
"sortList",
async () => {
const res = await getClassifyList();
return res;
},
{
server: true,
lazy: false,
getCachedData: () => null,
}
);
if (webSiteData.value) {
webSite.value = webSiteData.value;
}
if (sortListData.value) {
sortList.value = sortListData.value;
}
</script>
<style>
... ...
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply transition-colors duration-300;
}
body {
@apply bg-white dark:bg-[#1a1b1d] text-gray-900 dark:text-gray-100;
}
}
... ...
@font-face {
font-family: "iconfont"; /* Project id 5094593 */
src: url('iconfont.woff2?t=1770621719751') format('woff2'),
url('iconfont.woff?t=1770621719751') format('woff'),
url('iconfont.ttf?t=1770621719751') format('truetype');
src: url('iconfont.woff2?t=1773713724403') format('woff2'),
url('iconfont.woff?t=1773713724403') format('woff'),
url('iconfont.ttf?t=1773713724403') format('truetype');
}
.iconfont {
... ... @@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-up_top:before {
content: "\e888";
}
.icon-international:before {
content: "\e638";
}
... ...
<template>
<div class="fixed bottom-8 right-8 flex flex-col gap-3 z-50">
<Transition name="fade">
<button
v-show="showBackTop"
@click="scrollToTop"
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"
aria-label="回到顶部"
>
<i class="iconfont icon-up_top text-xl"></i>
</button>
</Transition>
<button
@click="toggleDark"
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"
:aria-label="isDark ? '切换到日间模式' : '切换到夜间模式'"
>
<i v-if="isDark" class="iconfont icon-sunny text-xl"></i>
<i v-else class="iconfont icon-moon text-xl"></i>
</button>
</div>
</template>
<script lang="ts" setup>
const { isDark, toggleDark } = useDarkMode();
const showBackTop = ref(false);
const handleScroll = () => {
showBackTop.value = window.scrollY > 300;
};
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
onMounted(() => {
window.addEventListener("scroll", handleScroll);
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
... ...
<template>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-12 px-8 mt-auto">
<footer class="bg-gray-800 dark:bg-gray-950 text-white py-12 px-8 mt-auto transition-colors duration-300">
<div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 class="text-xl font-bold mb-4">
... ... @@ -23,25 +22,25 @@
<div class="flex space-x-4">
<a
href="#"
class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-blue-500 transition-colors"
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"
>
<el-icon :size="20"><Star /></el-icon>
</a>
<a
href="#"
class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-blue-400 transition-colors"
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"
>
<el-icon :size="20"><Link /></el-icon>
</a>
<a
href="#"
class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-pink-500 transition-colors"
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"
>
<el-icon :size="20"><Star /></el-icon>
</a>
<a
href="#"
class="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center hover:bg-red-500 transition-colors"
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"
>
<el-icon :size="20"><Search /></el-icon>
</a>
... ... @@ -49,7 +48,7 @@
</div>
</div>
<div
class="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-700 text-center text-gray-400"
class="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-700 dark:border-gray-800 text-center text-gray-400"
>
<p>{{ webSite.bottomAnnouncement }}</p>
</div>
... ...
<template>
<!-- 顶部导航栏 -->
<header
class="fixed top-0 left-0 right-0 z-50 bg-gray-900 text-white shadow-md"
class="fixed top-0 left-0 right-0 z-50 bg-gray-900 dark:bg-[#272929] text-white shadow-md transition-colors duration-300"
>
<div class="mx-auto md:px-6 px-3 py-3 flex items-center justify-between">
<NuxtLink to="/" class="flex items-center space-x-2">
... ...
<template>
<nav
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"
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"
>
<div class="md:p-4 p-2">
<h2 class="text-lg font-semibold mb-4 text-gray-700">工具分类</h2>
<h2 class="text-lg font-semibold mb-4 text-gray-700 dark:text-[#c6c9cf]">
工具分类
</h2>
<ul id="menu" class="space-y-1">
<li
:id="`menu-${category.id}`"
... ... @@ -11,35 +13,35 @@
:key="index"
class="menu-item"
>
<div
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"
>
<a
:href="`#term-${category.id}`"
@click.stop="toggleCategory($event, category.id, index)"
class="w-full flex items-center justify-between p-3 text-[#515c6b] hover:text-[#5961f9] rounded-lg hover:bg-gray-100 transition-colors"
@click="handleCategoryClick($event, category.id, category)"
class="flex items-center space-x-2 flex-1"
>
<div class="flex items-center space-x-2">
<i
class="iconfont text-sm"
:class="[`icon-${category.icon}`]"
></i>
<span class="text-sm">{{ category.label }}</span>
</div>
<div v-if="category.children">
<el-icon
size="14px"
color="#515c6b"
v-show="activeCategory !== index"
</a>
<button
v-if="category.children && category.children.length > 0"
@click.stop="toggleExpand(index)"
class="p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors"
:aria-label="activeCategory === index ? '收起分类' : '展开分类'"
>
<ArrowRightBold />
</el-icon>
<el-icon
size="14px"
color="#515c6b"
v-show="activeCategory === index"
class="text-[#515c6b] dark:text-[#c6d9df] transition-transform duration-300"
:class="{ 'rotate-90': activeCategory === index }"
>
<ArrowDownBold />
<ArrowRightBold />
</el-icon>
</button>
</div>
</a>
<transition name="slide">
<ul v-show="activeCategory === index" class="ml-4 space-y-0.5">
... ... @@ -50,8 +52,10 @@
>
<a
:href="`#term-${category.id}-${subItem.id}`"
class="block text-sm py-2 px-3 rounded hover:bg-gray-100 text-[#515c6b] hover:text-[#5961f9] transition-colors"
@click.stop=""
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"
@click.stop="
handleSubCategoryClick($event, category.id, subItem.id)
"
>
{{ subItem.label }}
</a>
... ... @@ -66,35 +70,68 @@
<script setup lang="ts">
import type { classifyType } from "~/api/types/classify";
import { ArrowRightBold, ArrowDownBold } from "@element-plus/icons-vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
const sortList = useState<classifyType[]>("sortTree");
// 激活的分类索引
const activeCategory = ref<number | null>(0);
const route = useRoute();
const router = useRouter();
const config = useRuntimeConfig();
const { setActiveSubCategory } = useActiveSubCategory();
// 切换分类展开状态
const toggleCategory = (event: any, id: number, index: number) => {
const toggleExpand = (index: number) => {
if (activeCategory.value === index) {
activeCategory.value = null;
} else {
activeCategory.value = index;
}
event?.preventDefault();
if (route.path === "/") {
document.getElementById(`term-${id}`)?.scrollIntoView({
};
const scrollToElement = (elementId: string) => {
const element = document.getElementById(elementId);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
return true;
}
return false;
};
const handleCategoryClick = (
event: Event,
id: number,
SubCategory: classifyType
) => {
event.preventDefault();
if (SubCategory.children && SubCategory.children.length > 0) {
const targetChildId = `term-${id}-${SubCategory.children[0].id}`;
setActiveSubCategory(targetChildId);
} else {
router.push("/");
let timer = setTimeout(() => {
document.getElementById(`term-${id}`)?.scrollIntoView({
behavior: "smooth",
block: "center",
});
clearTimeout(timer);
}, 500);
setActiveSubCategory(null);
}
const targetId = `term-${id}`;
if (route.path === "/") {
scrollToElement(targetId);
} else {
router.push(`/#${targetId}`);
}
};
const handleSubCategoryClick = (
event: Event,
parentId: number,
subId: number
) => {
event.preventDefault();
const targetId = `term-${parentId}-${subId}`;
setActiveSubCategory(targetId);
if (route.path === "/") {
scrollToElement(targetId);
} else {
router.push(`/#${targetId}`);
}
};
</script>
... ...
<template>
<canvas ref="canvasRef" class="absolute inset-0 w-full h-full"></canvas>
</template>
<script lang="ts" setup>
const props = defineProps<{
particleCount?: number;
particleColor?: string;
lineColor?: string;
particleSize?: number;
lineDistance?: number;
speed?: number;
}>();
const canvasRef = ref<HTMLCanvasElement | null>(null);
const { isDark } = useDarkMode();
const config = {
particleCount: props.particleCount || 80,
particleColor: props.particleColor || "#5961f9",
lineColor: props.lineColor || "#5961f9",
particleSize: props.particleSize || 2,
lineDistance: props.lineDistance || 120,
speed: props.speed || 0.5,
};
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
size: number;
}
let particles: Particle[] = [];
let animationId: number | null = null;
let ctx: CanvasRenderingContext2D | null = null;
let canvas: HTMLCanvasElement | null = null;
const initParticles = () => {
particles = [];
if (!canvas) return;
for (let i = 0; i < config.particleCount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * config.speed,
vy: (Math.random() - 0.5) * config.speed,
size: Math.random() * config.particleSize + 1,
});
}
};
const drawParticles = () => {
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const particleAlpha = isDark.value ? 0.6 : 0.8;
const lineAlpha = isDark.value ? 0.15 : 0.2;
particles.forEach((particle, i) => {
particle.x += particle.vx;
particle.y += particle.vy;
if (particle.x < 0 || particle.x > canvas!.width) particle.vx *= -1;
if (particle.y < 0 || particle.y > canvas!.height) particle.vy *= -1;
ctx!.beginPath();
ctx!.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx!.fillStyle = `${config.particleColor}${Math.round(particleAlpha * 255)
.toString(16)
.padStart(2, "0")}`;
ctx!.fill();
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[j].x - particle.x;
const dy = particles[j].y - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < config.lineDistance) {
const opacity = (1 - distance / config.lineDistance) * lineAlpha;
ctx!.beginPath();
ctx!.moveTo(particle.x, particle.y);
ctx!.lineTo(particles[j].x, particles[j].y);
ctx!.strokeStyle = `${config.lineColor}${Math.round(opacity * 255)
.toString(16)
.padStart(2, "0")}`;
ctx!.lineWidth = 1;
ctx!.stroke();
}
}
});
animationId = requestAnimationFrame(drawParticles);
};
const handleResize = () => {
if (!canvas || !ctx) return;
const parent = canvas.parentElement;
if (parent) {
canvas.width = parent.offsetWidth;
canvas.height = parent.offsetHeight;
}
initParticles();
};
const handleMouse = (e: MouseEvent) => {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
particles.forEach((particle) => {
const dx = mouseX - particle.x;
const dy = mouseY - particle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
const force = (100 - distance) / 100;
particle.vx -= (dx / distance) * force * 0.5;
particle.vy -= (dy / distance) * force * 0.5;
}
});
};
onMounted(() => {
canvas = canvasRef.value;
if (!canvas) return;
ctx = canvas.getContext("2d");
if (!ctx) return;
handleResize();
drawParticles();
window.addEventListener("resize", handleResize);
canvas.addEventListener("mousemove", handleMouse);
});
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId);
}
window.removeEventListener("resize", handleResize);
if (canvas) {
canvas.removeEventListener("mousemove", handleMouse);
}
});
watch(isDark, () => {
drawParticles();
});
</script>
... ...
<template>
<div
v-for="(item, index) in cardList"
:key="index"
class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
>
<el-popconfirm
v-if="item.isPopup == '1'"
class="box-item"
:title="item.popupContent"
placement="top-start"
icon-color="#5961f9"
width="280"
:icon="Promotion"
confirm-button-text="确认前往"
cancel-button-text="取消"
@confirm="onConfirm(item.id)"
>
<template #reference>
<a
:href="config.public.baseUrl + '/site-details/' + item.id"
target="_blank"
@click.stop="onNuxtLink"
>
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
loading="lazy"
:src="config.public.apiUrl + item.image"
:alt="item.title"
class="w-10 h-10 md:w-14 md:h-14 object-cover rounded-lg"
/>
<div>
<h3
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"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
</div>
</div>
</div>
</a>
</template>
</el-popconfirm>
<a
v-else
:href="config.public.baseUrl + '/site-details/' + item.id"
target="_blank"
>
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
loading="lazy"
:src="config.public.apiUrl + item.image"
:alt="item.title"
class="w-10 h-10 md:w-14 md:h-14 object-cover rounded-lg"
/>
<div>
<h3
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"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
</div>
</div>
</div>
</a>
</div>
</template>
<script lang="ts" setup>
import { Promotion } from "@element-plus/icons-vue";
defineProps<{
cardList: any[];
}>();
const config = useRuntimeConfig();
// 阻止默认行为
function onNuxtLink(event: any) {
event.preventDefault();
}
// 点击确认跳转
function onConfirm(id: number) {
window.open(`/site-details/${id}`);
}
</script>
... ...
... ... @@ -6,13 +6,13 @@
class="iconfont text-lg mr-2"
:class="[`icon-${childData.icon}`]"
></i>
<h4 class="text-xl text-[#555]">
<h4 class="text-xl text-[#555] dark:text-[#888]">
{{ childData.label }}
</h4>
</div>
<div class="flex items-center flex-auto">
<div
class="scroll-container relative bg-black/10 rounded-[50px] md:overflow-hidden p-[3px] overflow-y-auto"
class="scroll-container relative bg-black/10 dark:bg-[#17181a] rounded-[50px] md:overflow-hidden p-[3px] overflow-y-auto"
slidertab="sliderTab"
>
<ul
... ... @@ -22,13 +22,24 @@
>
<li
v-for="(child, index) in childData.children"
:key="child.id"
class="h-auto w-auto cursor-pointer rounded-[100px] transition-all duration-350"
:class="[index === currentFilter ? 'bg-[#5961f9]' : '']"
:class="[
activeSubCategoryId === `term-${childData.id}-${child.id}` ||
index === currentFilter
? 'bg-[#5961f9]'
: '',
]"
>
<a
:id="`#term-${childData.id}-${child.id}`"
:id="`term-${childData.id}-${child.id}`"
class="h-7 leading-7 px-3 block relative text-[#888] text-center md:text-sm text-xs md:leading-7"
:class="[index === currentFilter ? 'text-white' : '']"
:class="[
activeSubCategoryId === `term-${childData.id}-${child.id}` ||
index === currentFilter
? 'text-white'
: '',
]"
style="transition: 0.25s"
:href="`#tab-${childData.id}-${child.id}`"
@click.stop="onClick($event, child.alias, index)"
... ... @@ -55,13 +66,17 @@
<div
v-for="(childContentItem, childContentIndex) in childData.children"
:key="childContentItem.id"
v-show="currentFilter === childContentIndex"
v-show="
activeSubCategoryId === `term-${childData.id}-${childContentItem.id}` ||
childContentIndex === currentFilter
"
:id="`tab-${childData.id}-${childContentItem.id}`"
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 md:gap-6 gap-4"
>
<div
v-for="appItem in childContentItem.appVos"
:key="appItem.id"
class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
>
<el-popconfirm
v-if="appItem.isPopup == '1'"
... ... @@ -90,12 +105,12 @@
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
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"
>
{{ appItem.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ appItem.description }}
</p>
... ... @@ -120,12 +135,12 @@
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
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"
>
{{ appItem.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ appItem.description }}
</p>
... ... @@ -143,24 +158,90 @@ import { Promotion } from "@element-plus/icons-vue";
const props = defineProps<{
childData: any;
}>();
const route = useRoute();
const childAlias = ref(props.childData.children[0].alias);
const config = useRuntimeConfig();
// 阻止默认行为
const { activeSubCategoryId, setActiveSubCategory } = useActiveSubCategory();
// 监听activeSubCategoryId的变化,当变化是把currentFilter更新为-1
watch(activeSubCategoryId, (newValue, oldValue) => {
if (newValue !== oldValue) {
currentFilter.value = -1;
}
});
function onNuxtLink(event: any) {
event.preventDefault();
}
// 点击确认跳转
function onConfirm(id: number) {
window.open(`/site-details/${id}`);
}
// 导航样式内容
const currentFilter = ref(0);
// 切换分类内容
function onClick(event: any, alias: string, index: number) {
event?.preventDefault();
childAlias.value = alias;
currentFilter.value = index;
const targetId = `term-${props.childData.id}-${props.childData.children[index].id}`;
setActiveSubCategory(targetId);
}
const scrollToSubCategory = () => {
if (import.meta.client) {
const hash = window.location.hash;
if (hash && hash.startsWith("#term-")) {
const parts = hash.replace("#term-", "").split("-");
if (parts.length === 2) {
const parentId = parts[0];
const childId = parts[1];
if (parentId === String(props.childData.id)) {
const index = props.childData.children.findIndex(
(child: any) => String(child.id) === childId
);
if (index !== -1) {
currentFilter.value = index;
childAlias.value = props.childData.children[index].alias;
const targetId = `term-${parentId}-${childId}`;
setActiveSubCategory(targetId);
nextTick(() => {
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
});
}
}
} else if (parts.length === 1) {
const parentId = parts[0];
if (parentId === String(props.childData.id)) {
nextTick(() => {
const element = document.getElementById(`term-${parentId}`);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
});
}
}
}
}
};
onMounted(() => {
scrollToSubCategory();
window.addEventListener("hashchange", scrollToSubCategory);
});
onUnmounted(() => {
window.removeEventListener("hashchange", scrollToSubCategory);
});
</script>
... ...
... ... @@ -2,7 +2,7 @@
<!-- 分类导航 -->
<div class="mb-6">
<div class="flex items-center mb-2">
<h4 class="text-gray text-lg m-0">
<h4 class="text-gray dark:text-[#888] text-lg m-0">
<i
:id="`term-${childData.id}`"
class="iconfont text-lg mr-2"
... ... @@ -12,7 +12,7 @@
</h4>
<div class="flex-auto"></div>
<a
class="hidden md:block text-xs ml-2 text-[#282a2d] hover:text-[#5961f9]"
class="hidden md:block text-xs ml-2 text-[#282a2d] hover:text-[#5961f9] dark:text-[#989da1]"
:href="`${config.public.baseUrl}/category/${childData.alias}`"
>查看更多 &gt;&gt;</a
>
... ... @@ -30,7 +30,7 @@
<div
v-for="(item, index) in childData.appVos"
:key="index"
class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
>
<el-popconfirm
v-if="item.isPopup == '1'"
... ... @@ -60,12 +60,12 @@
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
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"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
... ... @@ -86,12 +86,12 @@
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
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"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
... ...
... ... @@ -36,7 +36,7 @@
<div
v-for="(item, index) in recommendList"
:key="index"
class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
class="bg-white dark:bg-[#272929] rounded-xl shadow-md overflow-hidden hover:shadow-lg hover:translate-y-[-6px] transition-all duration-300"
>
<el-popconfirm
v-if="item.isPopup == '1'"
... ... @@ -66,12 +66,12 @@
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
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"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
... ... @@ -96,12 +96,12 @@
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
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"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
class="text-gray-600 dark:text-[#6c757d] text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
... ...
export const useActiveSubCategory = () => {
const activeSubCategoryId = useState<string | null>("activeSubCategoryId", () => null);
const setActiveSubCategory = (id: string | null) => {
activeSubCategoryId.value = id;
};
const clearActiveSubCategory = () => {
activeSubCategoryId.value = null;
};
return {
activeSubCategoryId,
setActiveSubCategory,
clearActiveSubCategory,
};
};
... ...
export const useDarkMode = () => {
const isDark = useState<boolean>("darkMode", () => false);
const toggleDark = () => {
isDark.value = !isDark.value;
updateTheme();
};
const updateTheme = () => {
if (import.meta.client) {
if (isDark.value) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}
};
const initTheme = () => {
if (import.meta.client) {
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (savedTheme === "dark" || (!savedTheme && prefersDark)) {
isDark.value = true;
document.documentElement.classList.add("dark");
} else {
isDark.value = false;
document.documentElement.classList.remove("dark");
}
}
};
return {
isDark,
toggleDark,
initTheme,
};
};
... ...
... ... @@ -10,8 +10,15 @@
<AppFooter />
</div>
</div>
<AppFloatingButtons />
</template>
<script setup></script>
<script lang="ts" setup>
const { initTheme } = useDarkMode();
onMounted(() => {
initTheme();
});
</script>
<style></style>
... ...
... ... @@ -12,6 +12,22 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss',
'@element-plus/nuxt'
],
tailwindcss: {
cssPath: '~/assets/css/tailwind.css',
config: {
darkMode: 'class',
content: [
'./components/**/*.{js,vue,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./plugins/**/*.{js,ts}',
'./app.vue',
],
theme: {
extend: {},
},
}
},
features: {
inlineStyles: false,
devLogs: false,
... ...
<script lang="ts" setup>
import type { appListType, appType } from "~/api/types/app";
import type { adListType } from "~/api/types/ad";
import { getAppList, getAllApp } from "~/api/app";
import type { webSiteType } from "~/api/types/webSite";
import { getAdList } from "~/api/ad";
const recommendList = ref<appType[]>([]);
const appList = ref<appListType[]>([]);
const adList = ref<adListType | null>(null);
// 获取轮播广告
const adRes = await getAdList();
adList.value = adRes[0];
// 获取推荐应用
const recommendRes = await getAppList({
const webSite = useState<webSiteType>("webSite");
const route = useRoute();
console.log(route);
const { data: adList } = await useAsyncData(
"adList",
async () => {
const res = await getAdList();
return res[0] || null;
},
{
server: true,
lazy: false,
}
);
const { data: recommendList } = await useAsyncData(
"recommendList",
async () => {
const res = await getAppList({
pageSize: 10,
pageNum: 1,
isRecommend: "1",
});
recommendList.value = recommendRes.rows;
// 获取全部应用
const allRes = await getAllApp();
appList.value = allRes.data;
const webSite = useState<webSiteType>("webSite");
});
return res.rows || [];
},
{
server: true,
lazy: false,
}
);
const { data: appList } = await useAsyncData(
"appList",
async () => {
const res = await getAllApp();
return res.data || [];
},
{
server: true,
lazy: false,
}
);
useHead({
title: webSite.value.webname,
... ... @@ -67,69 +91,73 @@ useHead({
{
"@type": "WebSite",
"@id": "https://aiboxgo.com/#website",
"url": "https://aiboxgo.com/",
"name": webSite.value.webname,
"description": webSite.value.webdescription,
"inLanguage": "zh-CN",
"potentialAction": {
url: "https://aiboxgo.com/",
name: webSite.value.webname,
description: webSite.value.webdescription,
inLanguage: "zh-CN",
potentialAction: {
"@type": "SearchAction",
"target": {
target: {
"@type": "EntryPoint",
"urlTemplate": "https://aiboxgo.com/search?keyword={search_term_string}"
urlTemplate:
"https://aiboxgo.com/search?keyword={search_term_string}",
},
"query-input": "required name=search_term_string",
},
"query-input": "required name=search_term_string"
}
},
{
"@type": "Organization",
"@id": "https://aiboxgo.com/#organization",
"name": webSite.value.webname,
"url": "https://aiboxgo.com/",
"logo": {
name: webSite.value.webname,
url: "https://aiboxgo.com/",
logo: {
"@type": "ImageObject",
"url": "https://aiboxgo.com/favicon.ico"
url: "https://aiboxgo.com/favicon.ico",
},
"sameAs": []
sameAs: [],
},
{
"@type": "CollectionPage",
"@id": "https://aiboxgo.com/",
"url": "https://aiboxgo.com/",
"name": webSite.value.webname,
"description": webSite.value.webdescription,
"isPartOf": {
"@id": "https://aiboxgo.com/#website"
},
"about": {
"@id": "https://aiboxgo.com/#organization"
}
}
]
})
}
]
url: "https://aiboxgo.com/",
name: webSite.value.webname,
description: webSite.value.webdescription,
isPartOf: {
"@id": "https://aiboxgo.com/#website",
},
about: {
"@id": "https://aiboxgo.com/#organization",
},
},
],
}),
},
],
});
</script>
<template>
<div class="flex flex-col min-h-screen bg-white">
<main class="flex-grow md:p-6 p-2 bg-white">
<!-- 轮播广告区域 -->
<ADSwiperCarousel :adSwiperList="adList" />
<!-- Banner 区域 -->
<div
class="flex flex-col min-h-screen bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
>
<main
class="flex-grow md:p-6 p-2 bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
>
<ADSwiperCarousel v-if="adList" :adSwiperList="adList" />
<HomeBanner />
<!-- 广告区域 -->
<!-- 工具展示区 -->
<section class="md:mb-12 mb-6">
<!-- 推荐工具区 -->
<HomeRecommend
v-if="recommendList && recommendList.length > 0"
:recommendList="recommendList"
navTitle="推荐工具"
navIcon="star"
/>
<div v-for="appItem in appList" class="md:mb-12 mb-6">
<!-- 分类导航及内容 -->
<div
v-for="appItem in appList"
:key="appItem?.id"
class="md:mb-12 mb-6"
>
<HomeContent :appData="appItem" />
</div>
</section>
... ...
<script lang="ts" setup>
import { getAppDetail } from "~/api/app";
import type { appDetail, Types } from "~/api/types/app";
import { getAppDetail, getAppList } from "~/api/app";
import type { appDetail, Types, appType } from "~/api/types/app";
import type { webSiteType } from "~/api/types/webSite";
const route = useRoute();
const config = useRuntimeConfig();
... ... @@ -14,12 +14,12 @@ const DetailData = ref<appDetail>({
types: [],
});
const webSite = useState<webSiteType>("webSite");
const relatedApps = ref<appType[]>([]);
function mergeDuplicates(data: Types[]) {
const map = new Map();
data.forEach((item) => {
if (!map.has(item.id)) {
// 如果是第一次遇到这个id,创建新对象
map.set(item.id, {
id: item.id,
label: item.label,
... ... @@ -27,9 +27,7 @@ function mergeDuplicates(data: Types[]) {
children: [...(item.children || [])],
});
} else {
// 如果已经存在,合并children
const existing = map.get(item.id);
// 避免重复的子项(基于子项id)
const existingChildIds = new Set(
existing.children.map((child: any) => child.id)
);
... ... @@ -40,16 +38,26 @@ function mergeDuplicates(data: Types[]) {
});
}
});
return Array.from(map.values());
}
// 获取详情数据
const detailRes = await getAppDetail(Number(route.params.id));
DetailData.value = detailRes.data;
DetailData.value.types = mergeDuplicates(detailRes.data.types);
console.log("详情数据", DetailData.value);
if (DetailData.value.types?.length > 0) {
const firstType = DetailData.value.types[0];
const typeAlias = firstType.alias || firstType.children?.[0]?.alias;
if (typeAlias) {
const relatedRes = await getAppList({
pageNum: 1,
pageSize: 8,
typeAlias: typeAlias,
});
relatedApps.value = relatedRes.rows
.filter((app: appType) => app.id !== DetailData.value.id)
.slice(0, 6);
}
}
useHead({
title: DetailData.value.popupContent
... ... @@ -177,37 +185,62 @@ useHead({
</script>
<template>
<div class="flex flex-col min-h-screen bg-white">
<main class="flex-grow md:p-6 bg-white p-1">
<!-- Top Application Info Bar -->
<header
v-show="DetailData.types.length > 0"
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"
<div
class="flex flex-col min-h-screen bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
>
<div class="flex items-center space-x-4">
<main class="flex-grow bg-white dark:bg-[#1a1b1d]">
<div class="relative overflow-hidden">
<CommonParticleBackground
:particleCount="60"
particleColor="#5961f9"
lineColor="#7c3aed"
:particleSize="2"
:lineDistance="100"
:speed="0.3"
/>
<div
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"
></div>
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<div class="lg:col-span-2 flex justify-center lg:justify-end">
<div class="relative group">
<div
class="absolute -inset-1 bg-gradient-to-r from-[#5961f9] to-[#a855f7] rounded-2xl blur opacity-30 group-hover:opacity-50 transition duration-300"
></div>
<img
:src="config.public.apiUrl + DetailData.image"
:alt="DetailData.title"
class="w-16 h-16 object-contain"
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"
loading="lazy"
/>
<div>
<h1 class="text-2xl font-bold text-[#5961f9]">
</div>
</div>
<div class="lg:col-span-7 space-y-4">
<div class="flex items-center gap-3 flex-wrap">
<h1
class="text-2xl md:text-3xl font-bold text-[#282a2d] dark:text-[#c6c9cf]"
>
{{ DetailData.title }}
</h1>
<p class="text-sm text-gray-600 mt-1">
{{ DetailData.description }}
</p>
<div class="mt-2 flex items-center space-x-2">
<div
v-for="tag in DetailData.types"
class="flex items-center space-x-2"
<span
v-if="DetailData.popupContent"
class="px-3 py-1 bg-gradient-to-r from-[#5961f9] to-[#a855f7] text-white text-xs rounded-full"
>
<template v-if="tag.children.length > 0">
{{ DetailData.popupContent }}
</span>
</div>
<div class="flex flex-wrap gap-2">
<template v-for="tag in DetailData.types" :key="tag.id">
<template v-if="tag.children && tag.children.length > 0">
<NuxtLink
v-for="child in tag.children"
:key="child.id"
:to="'/category/' + child.alias"
class="px-2 py-1 bg-blue-100 text-[#5961f9] rounded-full text-xs"
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"
>
{{ child.label }}
</NuxtLink>
... ... @@ -215,57 +248,156 @@ useHead({
<template v-else>
<NuxtLink
:to="'/category/' + tag.alias"
class="px-2 py-1 bg-blue-100 text-[#5961f9] rounded-full text-xs"
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"
>
{{ tag.label }}
</NuxtLink>
</template>
</template>
</div>
</div>
</div>
</div>
<div class="flex md:space-x-3 md:mt-0 mt-4">
<p
class="text-gray-600 dark:text-gray-300 text-base leading-relaxed line-clamp-3"
>
{{ DetailData.description }}
</p>
<div class="flex flex-wrap gap-3 pt-2">
<a
:href="DetailData.link"
target="_blank"
class="!rounded-button whitespace-nowrap px-4 py-2 bg-[#5961f9] max-[768px]:text-xs text-white hover:bg-blue-600 transition-colors"
rel="noopener noreferrer"
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"
>
<i class="iconfont icon-guide"></i>访问官网
<i class="iconfont icon-guide text-lg"></i>
<span>立即访问</span>
</a>
</div>
</header>
</div>
<main class="relative w-full">
<!-- 悬浮广告弹窗 -->
<!-- <div
class="md:absolute top-0 right-0 md:m-4 z-50 relative max-[768px]:m-auto"
v-show="showAd"
:style="{
width: `${detailAd.width}px`,
height: `${detailAd.height}px`,
}"
<div class="lg:col-span-3">
<div
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"
>
<div
class="w-full h-full relative"
v-for="item in detailAd.frontAdVos"
class="text-center text-gray-400 dark:text-gray-500 text-sm"
>
<img
class="w-full h-full object-contain"
:src="config.public.baseUrl + item.image"
:alt="item.title"
/>
<a
no_cache=""
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="
rel="external nofollow"
target="_blank"
><img
src="https://ai-kit.cn/wp-content/uploads/2026/01/kouzi_gd.jpg"
alt="扣子"
/></a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<article class="prose prose-lg dark:prose-invert max-w-none p-6 md:p-8">
<div v-html="DetailData.content" class="detail-content"></div>
</article>
</div>
<div
class="absolute top-1 right-1 cursor-pointer bg-white w-4 h-4 text-center rounded-[50%] text-xs"
@click="showAd = false"
v-if="relatedApps.length > 0"
class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"
>
<div class="rounded-2xl p-6">
<h2
class="text-xl font-bold text-[#555] dark:text-[#888] mb-6 flex items-center gap-2"
>
X
<i class="iconfont icon-tag" style="font-size: 1.2rem"></i>
相关推荐
</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-4">
<CommonCard :cardList="relatedApps" />
</div>
</div>
</div> -->
<div class="md:max-w-5xl mx-auto md:p-8 p-2 w-full">
<article v-html="DetailData.content"></article>
</div>
</main>
</main>
</div>
</template>
<style scoped lang="less">
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.detail-content {
@apply text-[#282a2d] dark:text-[#c6c9cf];
}
.detail-content :deep(h1),
.detail-content :deep(h2),
.detail-content :deep(h3),
.detail-content :deep(h4) {
@apply text-[#282a2d] my-5 py-1 pl-5 border-l-4 border-[#5961f9] dark:text-[#c6c9cf];
}
.detail-content :deep(h1) {
@apply text-2xl;
}
.detail-content :deep(h2) {
@apply text-xl;
}
.detail-content :deep(h3) {
@apply text-2xl;
}
.detail-content :deep(p) {
@apply mb-4 leading-relaxed;
}
.detail-content :deep(img) {
@apply rounded-lg max-w-full h-auto my-4;
}
.detail-content :deep(a) {
@apply text-[#5961f9] hover:underline;
}
.detail-content :deep(ul),
.detail-content :deep(ol) {
@apply pl-6 mb-4 text-sm list-disc;
}
.detail-content :deep(li) {
@apply mb-2;
}
.detail-content :deep(blockquote) {
@apply border-l-4 border-[#5961f9] pl-4 italic my-4;
}
.detail-content :deep(code) {
@apply bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-sm;
}
.detail-content :deep(pre) {
@apply bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto my-4;
}
.detail-content :deep(table) {
@apply w-full border-collapse my-4;
}
.detail-content :deep(th),
.detail-content :deep(td) {
@apply border border-gray-200 dark:border-gray-600 px-4 py-2;
}
.detail-content :deep(th) {
@apply bg-gray-100 dark:bg-gray-700 font-semibold;
}
</style>
... ...
import { ElLoading } from 'element-plus'
const useMyfetch = async (url: any, options?: any, headers?: any) => {
let loadingInstance
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
let requestCount = 0
const showLoading = () => {
if (requestCount === 0) {
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(255, 255, 255, 0.7)',
})
}
requestCount++
}
const hideLoading = () => {
requestCount--
if (requestCount <= 0) {
requestCount = 0
if (loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
}
const useMyfetch = async (url: any, options?: any, headers?: any) => {
try {
loadingInstance = ElLoading.service()
const config = useRuntimeConfig() // 3.0正式版环境变量要从useRuntimeConfig里的public拿
const reqUrl = config.public.apiUrl + url // 你的接口地址
// 不设置key,始终拿到的都是第一个请求的值,参数一样则不会进行第二次请求
showLoading()
const config = useRuntimeConfig()
const reqUrl = config.public.apiUrl + url
// 可以设置默认headers例如
const customHeaders = {
Authorization: useCookie('accessToken').value,
...headers
... ... @@ -23,17 +44,11 @@ const useMyfetch = async (url: any, options?: any, headers?: any) => {
const result = data.value as any
if (pending && loadingInstance) {
loadingInstance.close()
}
return result
} catch (err) {
return Promise.reject(err)
} finally {
if (loadingInstance) {
loadingInstance.close()
}
hideLoading()
}
}
... ...