|
|
|
<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,95 +185,219 @@ 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 items-center space-x-4">
|
|
|
|
<img
|
|
|
|
:src="config.public.apiUrl + DetailData.image"
|
|
|
|
:alt="DetailData.title"
|
|
|
|
class="w-16 h-16 object-contain"
|
|
|
|
loading="lazy"
|
|
|
|
/>
|
|
|
|
<div>
|
|
|
|
<h1 class="text-2xl font-bold text-[#5961f9]">
|
|
|
|
{{ 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
|
|
|
|
class="flex flex-col min-h-screen bg-white dark:bg-[#1a1b1d] transition-colors duration-300"
|
|
|
|
>
|
|
|
|
<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="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>
|
|
|
|
</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>
|
|
|
|
<span
|
|
|
|
v-if="DetailData.popupContent"
|
|
|
|
class="px-3 py-1 bg-gradient-to-r from-[#5961f9] to-[#a855f7] text-white text-xs rounded-full"
|
|
|
|
>
|
|
|
|
{{ 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-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>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<NuxtLink
|
|
|
|
:to="'/category/' + tag.alias"
|
|
|
|
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>
|
|
|
|
|
|
|
|
<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"
|
|
|
|
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 text-lg"></i>
|
|
|
|
<span>立即访问</span>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="lg:col-span-3">
|
|
|
|
<div
|
|
|
|
v-for="tag in DetailData.types"
|
|
|
|
class="flex items-center space-x-2"
|
|
|
|
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"
|
|
|
|
>
|
|
|
|
<template v-if="tag.children.length > 0">
|
|
|
|
<NuxtLink
|
|
|
|
v-for="child in tag.children"
|
|
|
|
:to="'/category/' + child.alias"
|
|
|
|
class="px-2 py-1 bg-blue-100 text-[#5961f9] rounded-full text-xs"
|
|
|
|
>
|
|
|
|
{{ child.label }}
|
|
|
|
</NuxtLink>
|
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<NuxtLink
|
|
|
|
:to="'/category/' + tag.alias"
|
|
|
|
class="px-2 py-1 bg-blue-100 text-[#5961f9] rounded-full text-xs"
|
|
|
|
>
|
|
|
|
{{ tag.label }}
|
|
|
|
</NuxtLink>
|
|
|
|
</template>
|
|
|
|
<div
|
|
|
|
class="text-center text-gray-400 dark:text-gray-500 text-sm"
|
|
|
|
>
|
|
|
|
<a
|
|
|
|
no_cache=""
|
|
|
|
href="https://www.coze.cn/?utm_medium=daohang&utm_source=aikit&utm_content=&utm_id=&utm_campaign=&utm_term=hw_coze_aikit&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 class="flex md:space-x-3 md:mt-0 mt-4">
|
|
|
|
<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"
|
|
|
|
>
|
|
|
|
<i class="iconfont icon-guide"></i>访问官网
|
|
|
|
</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="w-full h-full relative"
|
|
|
|
v-for="item in detailAd.frontAdVos"
|
|
|
|
<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
|
|
|
|
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"
|
|
|
|
>
|
|
|
|
<img
|
|
|
|
class="w-full h-full object-contain"
|
|
|
|
:src="config.public.baseUrl + item.image"
|
|
|
|
:alt="item.title"
|
|
|
|
/>
|
|
|
|
<div
|
|
|
|
class="absolute top-1 right-1 cursor-pointer bg-white w-4 h-4 text-center rounded-[50%] text-xs"
|
|
|
|
@click="showAd = false"
|
|
|
|
>
|
|
|
|
X
|
|
|
|
</div>
|
|
|
|
<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 class="md:max-w-5xl mx-auto md:p-8 p-2 w-full">
|
|
|
|
<article v-html="DetailData.content"></article>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
</div>
|
|
|
|
</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> |
...
|
...
|
|