作者 xiaoqiu

首次提交

# Nuxt dev/build outputs
.output
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
... ...
shamefully-hoist=true
strict-peer-dependencies=false
... ...
# nuxt3-template
#### Description
nuxt3的模版代码,因为nuxt3项目初始化失败所以保存此模版
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
... ...
# nuxt3-template
#### 介绍
nuxt3的模版代码,因为nuxt3项目初始化失败所以保存此模版
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
... ...
<template>
<NuxtLayout>
<NuxtPage></NuxtPage>
</NuxtLayout>
</template>
<script setup>
const webSite = useState("webSite", () => {});
const sortList = useState("sortTree", () => []);
const { data: webData } = await useFetch(
"http://aitoolht.crgx.net/dh/config/get"
);
const { data: treeData } = await useFetch(
"http://aitoolht.crgx.net/dh/type/typeTree"
);
webSite.value = webData.value.data;
sortList.value = treeData.value.data;
useHead({
title: webSite.value.webname,
meta: [
{ name: "description", content: webSite.value.webdescription },
{ name: "keywords", content: webSite.value.webkeywords },
],
});
</script>
<style>
.scroll-container {
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.scroll-container::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
</style>
... ...
@font-face {
font-family: "iconfont"; /* Project id 5094593 */
src: url('iconfont.woff2?t=1766651481158') format('woff2'),
url('iconfont.woff?t=1766651481158') format('woff'),
url('iconfont.ttf?t=1766651481158') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-tag:before {
content: "\e6af";
}
.icon-folder:before {
content: "\e631";
}
.icon-video:before {
content: "\e632";
}
.icon-image:before {
content: "\e704";
}
.icon-music:before {
content: "\e645";
}
.icon-study:before {
content: "\e634";
}
.icon-dev:before {
content: "\e635";
}
.icon-download:before {
content: "\e600";
}
.icon-upload:before {
content: "\e656";
}
.icon-fontsize:before {
content: "\e7a1";
}
.icon-money:before {
content: "\e663";
}
.icon-clipboard:before {
content: "\ea4c";
}
.icon-tool:before {
content: "\e678";
}
.icon-listnested:before {
content: "\e78c";
}
.icon-qq:before {
content: "\e65d";
}
.icon-select:before {
content: "\e60d";
}
.icon-fullscreen:before {
content: "\e620";
}
.icon-message:before {
content: "\e643";
}
.icon-riqi2:before {
content: "\e697";
}
.icon-user:before {
content: "\e752";
}
.icon-switch:before {
content: "\e601";
}
.icon-star:before {
content: "\e61d";
}
.icon-color:before {
content: "\e760";
}
.icon-password:before {
content: "\e82b";
}
.icon-system:before {
content: "\e60c";
}
.icon-GitHub:before {
content: "\ea0a";
}
.icon-redis:before {
content: "\e619";
}
.icon-build:before {
content: "\e7d5";
}
.icon-post:before {
content: "\e62d";
}
.icon-language:before {
content: "\e602";
}
.icon-people:before {
content: "\e603";
}
.icon-lock:before {
content: "\e604";
}
.icon-documentation:before {
content: "\e605";
}
.icon-email:before {
content: "\e606";
}
.icon-dashboard:before {
content: "\e607";
}
.icon-peoples:before {
content: "\e60a";
}
.icon-theme:before {
content: "\e60e";
}
.icon-eye-open:before {
content: "\e7ec";
}
.icon-list:before {
content: "\e62e";
}
.icon-question:before {
content: "\e81f";
}
.icon-shopping:before {
content: "\e865";
}
.icon-online:before {
content: "\e694";
}
.icon-bug:before {
content: "\e8e8";
}
.icon-chart:before {
content: "\e608";
}
.icon-drag:before {
content: "\e60f";
}
.icon-input:before {
content: "\e61a";
}
.icon-radio:before {
content: "\e627";
}
.icon-textarea:before {
content: "\e62f";
}
.icon-Excel:before {
content: "\edde";
}
.icon-Phone:before {
content: "\e660";
}
.icon-component:before {
content: "\e71a";
}
.icon-moon:before {
content: "\e6c3";
}
.icon-rows:before {
content: "\e940";
}
.icon-monitoring:before {
content: "\e88e";
}
.icon-wechat:before {
content: "\e610";
}
.icon-skill:before {
content: "\e61c";
}
.icon-time:before {
content: "\e621";
}
.icon-guide:before {
content: "\e611";
}
.icon-form:before {
content: "\e612";
}
.icon-international:before {
content: "\e613";
}
.icon-link:before {
content: "\e614";
}
.icon-table:before {
content: "\e618";
}
.icon-tab:before {
content: "\e61b";
}
.icon-zip:before {
content: "\e61e";
}
.icon-bg-pdf:before {
content: "\e639";
}
.icon-server:before {
content: "\e644";
}
.icon-number:before {
content: "\e63a";
}
.icon-enter:before {
content: "\e609";
}
.icon-example:before {
content: "\e60b";
}
.icon-education:before {
content: "\e615";
}
.icon-exit-fullscreen:before {
content: "\e616";
}
.icon-tree:before {
content: "\e61f";
}
.icon-slider:before {
content: "\e622";
}
.icon-search:before {
content: "\e747";
}
.icon-cascader:before {
content: "\e664";
}
.icon-tree-table:before {
content: "\e64e";
}
.icon-sunny:before {
content: "\e67d";
}
.icon-edit:before {
content: "\e653";
}
.icon-swagger:before {
content: "\e623";
}
.icon-more-up:before {
content: "\e667";
}
.icon-druid:before {
content: "\e657";
}
.icon-logininfor:before {
content: "\e74f";
}
.icon-time-range:before {
content: "\e762";
}
.icon-log:before {
content: "\e624";
}
.icon-job:before {
content: "\e633";
}
.icon-checkbox:before {
content: "\e625";
}
.icon-redis-list:before {
content: "\e626";
}
.icon-code:before {
content: "\e84f";
}
.icon-date-range:before {
content: "\e628";
}
.icon-eye:before {
content: "\e629";
}
.icon-a-404:before {
content: "\e62a";
}
.icon-icon:before {
content: "\e62b";
}
.icon-validCode:before {
content: "\e738";
}
.icon-rate:before {
content: "\e62c";
}
.icon-dict:before {
content: "\e630";
}
.icon-write:before {
content: "\e617";
}
... ...
此 diff 太大无法显示。
不能预览此文件类型
不能预览此文件类型
不能预览此文件类型
<template>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-12 px-8 mt-auto">
<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">
{{ webSite.webname }}
</h3>
<p class="text-gray-400">
提供安全、快速的网址跳转服务,保护您的隐私安全。
</p>
</div>
<div>
<h4 class="font-bold mb-4">联系我们</h4>
<ul class="space-y-2 text-gray-400">
<li>邮箱: contact@linkhub.com</li>
<li>电话: +86 400 123 4567</li>
</ul>
</div>
<div>
<h4 class="font-bold mb-4">关注我们</h4>
<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"
>
<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"
>
<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"
>
<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"
>
<el-icon :size="20"><Search /></el-icon>
</a>
</div>
</div>
</div>
<div
class="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-700 text-center text-gray-400"
>
<p>&copy; {{ webSite.bottomAnnouncement }}</p>
</div>
</footer>
</template>
<script setup>
import { Link, Search, Star } from "@element-plus/icons-vue";
const webSite = useState("webSite");
</script>
<style scoped lang="less"></style>
... ...
<template>
<!-- 顶部导航栏 -->
<header
class="fixed top-0 left-0 right-0 z-50 bg-gray-900 text-white shadow-md"
>
<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">
<el-icon :size="24"><Promotion /></el-icon>
<h1 class="md:text-xl text-base font-bold">
{{ webSite.webname }}
</h1>
</NuxtLink>
<div class="flex items-center gap-2">
<MySearch />
<MyMenu />
</div>
</div>
</header>
</template>
<script setup>
import { Promotion } from "@element-plus/icons-vue";
const webSite = useState("webSite");
</script>
<style scoped lang="less"></style>
... ...
<template>
<nav
class="max-[768px]:flex-[0] scroll-container w-56 bg-white shadow-lg h-[calc(100vh-4rem)] sticky top-16 overflow-y-auto"
>
<div class="md:p-4 p-2">
<h2 class="text-lg font-semibold mb-4 text-gray-700">工具分类</h2>
<ul class="space-y-1">
<li v-for="(category, index) in sortList" :key="index">
<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"
>
<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"
>
<ArrowRightBold />
</el-icon>
<el-icon
size="14px"
color="#515c6b"
v-show="activeCategory === index"
>
<ArrowDownBold />
</el-icon>
</div>
</a>
<transition name="slide">
<ul v-show="activeCategory === index" class="ml-4 space-y-0.5">
<li
v-for="(subItem, subIndex) in category.children"
:key="subItem.id"
>
<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=""
>
{{ subItem.label }}
</a>
</li>
</ul>
</transition>
</li>
</ul>
</div>
</nav>
</template>
<script setup lang="ts">
import { ArrowRightBold, ArrowDownBold } from "@element-plus/icons-vue";
const sortList = useState("sortTree");
// 激活的分类索引
const activeCategory = ref<number | null>(0);
const route = useRoute();
const router = useRouter();
// 切换分类展开状态
const toggleCategory = (event: any, id: number, index: number) => {
if (activeCategory.value === index) {
activeCategory.value = null;
} else {
activeCategory.value = index;
}
event?.preventDefault();
if (route.path === "/") {
document.getElementById(`term-${id}`)?.scrollIntoView({
behavior: "smooth",
block: "center",
});
} else {
router.push("/");
let timer = setTimeout(() => {
document.getElementById(`term-${id}`)?.scrollIntoView({
behavior: "smooth",
block: "center",
});
clearTimeout(timer);
}, 500);
}
};
</script>
... ...
<template>
<section
class="mb-12 rounded-2xl overflow-hidden relative h-80 bg-gradient-to-r from-blue-500 to-purple-600 text-white"
>
<div class="absolute inset-0 bg-black bg-opacity-30"></div>
<div class="relative z-10 h-full flex flex-col justify-center px-12">
<h2 class="text-4xl font-bold mb-4">发现强大的 AI 工具</h2>
<p class="text-xl max-w-2xl mb-6">
一站式获取各类 AI 解决方案,提升工作效率与创造力
</p>
<button
class="!rounded-button whitespace-nowrap self-start px-6 py-3 bg-white text-blue-600 font-medium hover:bg-gray-100 transition-colors"
>
开始探索
</button>
</div>
</section>
</template>
... ...
<template>
<div class="mb-6">
<div class="flex items-center mb-2">
<i
:id="`term-${childData.id}`"
class="iconfont text-lg mr-2"
:class="[`icon-${childData.icon}`]"
></i>
<h4 class="text-xl text-[#555]">
{{ 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"
slidertab="sliderTab"
>
<ul
class="relative whitespace-nowrap flex"
style="flex-wrap: inherit"
role="tablist"
>
<li
class="anchor md:w-[120.891px] w-[107.3px] cursor-pointer rounded-[100px] bg-[#5961f9]"
style="
position: absolute;
height: 28px;
opacity: 1;
transition: 0.35s;
"
:style="{ left: `${left}px` }"
></li>
<li
v-for="(child, index) in childData.children"
class="h-auto w-auto cursor-pointer"
>
<a
: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' : '']"
style="transition: 0.25s"
:href="`#tab-${childData.id}-${child.id}`"
@click.stop="onClick($event, child.alias, index)"
>{{ child.label }}</a
>
</li>
</ul>
</div>
<div class="flex-auto"></div>
<a
class="hidden md:block text-xs ml-2 text-[#282a2d] hover:text-[#5961f9]"
:href="`/category/${childAlias}`"
>查看更多 &gt;&gt;</a
>
<a
class="md:hidden text-xs ml-2 text-[#282a2d] hover:text-[#5961f9]"
:href="`/category/${childAlias}`"
>&gt;&gt;</a
>
</div>
</div>
<!-- 内容区域 -->
<div
v-for="(childContentItem, childContentIndex) in childData.children"
:key="childContentItem.id"
v-show="currentFilter === childContentIndex"
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"
>
<el-popconfirm
v-if="appItem.isPopup == '1'"
class="box-item"
:title="appItem.popupContent"
placement="top-start"
width="280"
:icon="Promotion"
confirm-button-text="确认前往"
cancel-button-text="取消"
@confirm="onConfirm(appItem.id)"
>
<template #reference>
<a
:href="'/details/' + appItem.id"
target="_blank"
@click.stop="onNuxtLink"
>
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
:src="'http://aitoolht.crgx.net' + appItem.image"
:alt="appItem.title"
class="w-10 h-10 md:w-14 md:h-14 object-cover rounded-lg"
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
>
{{ appItem.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ appItem.description }}
</p>
</div>
</div>
</div>
</a>
</template>
</el-popconfirm>
<a v-else :href="'/details/' + appItem.id" target="_blank">
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
:src="'http://aitoolht.crgx.net' + appItem.image"
:alt="appItem.title"
class="w-10 h-10 md:w-14 md:h-14 object-cover rounded-lg"
/>
<div>
<h3
class="font-bold md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
>
{{ appItem.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ appItem.description }}
</p>
</div>
</div>
</div>
</a>
</div>
</div>
</template>
<script setup lang="ts">
import { Promotion } from "@element-plus/icons-vue";
const props = defineProps<{
childData: any;
}>();
const childAlias = ref(props.childData.children[0].alias);
// 阻止默认行为
function onNuxtLink(event: any) {
event.preventDefault();
}
// 点击确认跳转
function onConfirm(id: number) {
window.open(`/details/${id}`);
}
// 导航样式内容
const currentFilter = ref(0);
const left = ref(0);
// 切换分类内容
function onClick(event: any, alias: string, index: number) {
let moveWidth = window.innerWidth > 768 ? 120.891 : 107.3;
event?.preventDefault();
childAlias.value = alias;
currentFilter.value = index;
left.value = index * moveWidth;
}
</script>
... ...
<template>
<!-- 分类导航 -->
<div class="mb-6">
<div class="flex items-center mb-2">
<h4 class="text-gray text-lg m-0">
<i
:id="`term-${childData.id}`"
class="iconfont text-lg mr-2"
:class="[`icon-${childData.icon}`]"
></i>
{{ childData.label }}
</h4>
<div class="flex-auto"></div>
<a
class="hidden md:block text-xs ml-2 text-[#282a2d] hover:text-[#5961f9]"
:href="`/category/${childData.alias}`"
>查看更多 &gt;&gt;</a
>
<a
class="md:hidden text-xs ml-2 text-[#282a2d] hover:text-[#5961f9]"
:href="`/category/${childData.alias}`"
>&gt;&gt;</a
>
</div>
</div>
<!-- 分类内容 -->
<div
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="(item, index) in childData.appVos"
:key="index"
class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow 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="'/details/' + item.id"
target="_blank"
@click.stop="onNuxtLink"
>
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
:src="'http://aitoolht.crgx.net' + 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 md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 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="'/details/' + item.id" target="_blank">
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
:src="'http://aitoolht.crgx.net' + 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 md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
</div>
</div>
</div>
</a>
</div>
</div>
</template>
<script setup lang="ts">
import { Promotion } from "@element-plus/icons-vue";
defineProps<{
childData: any;
}>();
// 阻止默认行为
function onNuxtLink(event: any) {
event.preventDefault();
}
// 点击确认跳转
function onConfirm(id: number) {
window.open(`/details/${id}`);
}
</script>
... ...
<template>
<LazyHomeContentHasChildren v-if="appData.children" :childData="appData" />
<LazyHomeContentNoChildren v-else :childData="appData" />
</template>
<script lang="ts" setup>
defineProps<{
appData: any;
}>();
</script>
... ...
<template>
<div class="md:mb-12 mb-6">
<div class="flex mb-6">
<div
class="relative bg-black/10 rounded-[50px] p-[3px] overflow-hidden"
slidertab="sliderTab"
>
<ul
class="flex relative whitespace-nowrap overflow-x-auto rounded-[100px] bg-[#5961f9]"
style="flex-wrap: inherit; overflow-y: unset"
role="tablist"
>
<li
class="w-auto h-auto cursor-pointer list-item whitespace-nowrap line-clamp-1"
>
<a
class="h-7 leading-7 px-3 block relative text-white text-center text-sm"
data-toggle="pill"
data-action="load_hot_post"
data-datas='{"data":{"title":"热门工具","type":"sites","order":"views","num":"12","mini":""}}'
>
<i
class="iconfont mr-2 font-bold text-xs"
:class="[`icon-${navIcon}`]"
></i>
{{ navTitle }}
</a>
</li>
</ul>
</div>
</div>
<div
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="(item, index) in recommendList"
:key="index"
class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow 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="'/details/' + item.id"
target="_blank"
@click.stop="onNuxtLink"
>
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
:src="'http://aitoolht.crgx.net' + 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 md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 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="'/details/' + item.id" target="_blank">
<div class="group p-3">
<div class="flex items-start space-x-4">
<img
:src="'http://aitoolht.crgx.net' + 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 md:text-base text-sm line-clamp-1 text-gray-800 transition group-hover:text-[#5961f9]"
>
{{ item.title }}
</h3>
<p
class="text-gray-600 text-xs mt-1 md:line-clamp-2 line-clamp-1"
>
{{ item.description }}
</p>
</div>
</div>
</div>
</a>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Promotion } from "@element-plus/icons-vue";
defineProps<{
recommendList: any[];
navTitle: string;
navIcon: string;
}>();
// 阻止默认行为
function onNuxtLink(event: any) {
event.preventDefault();
}
// 点击确认跳转
function onConfirm(id: number) {
window.open(`/details/${id}`);
}
</script>
... ...
<template>
<div class="md:hidden block">
<input ref="checkboxRef" id="checkbox" type="checkbox" />
<label class="toggle" for="checkbox" @click="drawer = !drawer">
<div id="bar1" class="bars"></div>
<div id="bar2" class="bars"></div>
<div id="bar3" class="bars"></div>
</label>
</div>
<el-drawer
v-model="drawer"
:with-header="false"
direction="ltr"
:append-to-body="true"
:lock-scroll="true"
size="266"
custom-class="drawer-sidebar"
@close="onClose"
>
<AppSidebar />
</el-drawer>
</template>
<script setup>
const drawer = ref(false);
const checkboxRef = ref(null);
// 关闭弹出框
function onClose() {
checkboxRef.value.checked = false;
drawer.value = false;
}
onMounted(() => {
// 获取复选框元素
checkboxRef.value = document.getElementById("checkbox");
});
</script>
<style scoped>
/* From Uiverse.io by Yaya12085 */
#checkbox {
display: none;
}
.toggle {
position: relative;
width: 35px;
height: 35px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 10px;
transition-duration: 0.5s;
}
.bars {
width: 100%;
height: 4px;
background-color: rgb(92, 130, 255);
border-radius: 4px;
}
#bar2 {
transition-duration: 0.8s;
}
#bar1 {
width: 50%;
}
#bar2 {
width: 75%;
}
#checkbox:checked + .toggle .bars {
position: absolute;
transition-duration: 0.5s;
}
#checkbox:checked + .toggle #bar2 {
transform: scaleX(0);
transition-duration: 0.1s;
}
#checkbox:checked + .toggle #bar1 {
width: 100%;
transform: rotate(45deg);
transition-duration: 0.5s;
}
#checkbox:checked + .toggle #bar3 {
width: 100%;
transform: rotate(-45deg);
transition-duration: 0.5s;
}
#checkbox:checked + .toggle {
transition-duration: 0.5s;
transform: rotate(180deg);
}
</style>
... ...
<template>
<!-- From Uiverse.io by ZAKARIAE48CHELLE -->
<div class="input-wrapper">
<button class="icon">
<svg
width="25px"
height="25px"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
stroke="#fff"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M22 22L20 20"
stroke="#fff"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>
</button>
<input
v-model="keyWord"
type="text"
name="searchKeyword"
class="input"
placeholder="输入应用名称回车查找"
@keyup.enter="onSearch"
/>
</div>
</template>
<script setup>
const keyWord = ref("");
function onSearch() {
if (keyWord.value) {
// 跳转到搜索页面
// this.$router.push({ path: "/search", query: { q: keyword.value } });
window.location.href = "/search?keyword=" + keyWord.value;
}
}
</script>
<style scoped>
/* From Uiverse.io by ZAKARIAE48CHELLE */
.input-wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 15px;
position: relative;
width: 250px;
}
.input {
border-style: none;
height: 50px;
width: 50px;
padding: 10px;
outline: none;
border-radius: 50%;
transition: 0.5s ease-in-out;
background-color: #5961f9;
box-shadow: 0px 0px 3px #5961f9;
padding-right: 40px;
color: #fff;
}
.input::placeholder,
.input {
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
"Lucida Sans", Arial, sans-serif;
font-size: 17px;
}
.input::placeholder {
color: #8f8f8f;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0px;
cursor: pointer;
width: 50px;
height: 50px;
outline: none;
border-style: none;
border-radius: 50%;
pointer-events: painted;
background-color: transparent;
transition: 0.2s linear;
}
.icon:focus ~ .input,
.input:focus {
box-shadow: none;
width: 250px;
border-radius: 0px;
background-color: transparent;
border-bottom: 3px solid #5961f9;
transition: all 500ms cubic-bezier(0, 0.11, 0.35, 2);
}
/* 当最大宽度为768pxs时 */
@media (max-width: 768px) {
.input-wrapper {
width: 200px;
gap: 10px;
}
.icon {
width: 40px;
height: 40px;
}
.input {
width: 40px;
height: 40px;
padding-right: 31px;
}
.input::placeholder,
.input {
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
"Lucida Sans", Arial, sans-serif;
font-size: 14px;
}
.icon:focus ~ .input,
.input:focus {
box-shadow: none;
width: 200px;
border-radius: 0px;
background-color: transparent;
border-bottom: 1.5px solid #5961f9;
transition: all 500ms cubic-bezier(0, 0.11, 0.35, 2);
}
}
</style>
... ...
<template>
<AppHeader />
<div class="flex md:pt-16 pt-[5rem] flex-grow">
<AppSidebar />
<div class="w-full flex-1">
<slot></slot>
<AppFooter />
</div>
</div>
</template>
<script setup></script>
<style></style>
... ...
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
runtimeConfig: {
public: {
baseUrl: 'http://aitoolht.crgx.net',
}
},
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@element-plus/nuxt'
],
devServer: {
host: 'localhost',
port: 3666
},
css: [
'~/assets/iconfonts/iconfont.css',
],
plugins: [
{ src: '~/assets/iconfonts/iconfont.js', ssr: false, mode: 'client' }
],
app: {
head: {
title: 'Annie网站',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '提供市面上最简洁的导航系统' },
{ name: 'format-detection', content: 'telephone=no' },
{ name: 'keywords', content: '提供市面上最简洁的导航系统,一个完全免费的导航站'}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
})
... ...
此 diff 太大无法显示。
{
"name": "nuxt-app",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@element-plus/nuxt": "^1.0.8",
"@nuxt/devtools": "latest",
"@nuxtjs/tailwindcss": "^6.11.4",
"@types/node": "^18",
"element-plus": "^2.6.3",
"nuxt": "^3.5.2"
},
"dependencies": {
"less": "^4.2.0"
}
}
... ...
<template>
<div class="md:p-10 p-4 pt-0" style="min-height: calc(100vh - 320px)">
<HomeRecommend
:recommendList="list"
:navTitle="findLabelByAlias(name as string, sortList as any)"
navIcon="tag"
/>
<el-pagination
background
layout="prev, pager, next"
:hide-on-single-page="true"
v-model:page-size="params.pageSize"
v-model:current-page="params.pageNum"
:total="total"
@current-change="onPageChange"
/>
</div>
</template>
<script lang="ts" setup>
const sortList = useState("sortTree");
const route = useRoute();
const router = useRouter();
const { name } = route.params;
const list = ref<any[]>([]);
const total = ref<number>(0);
const params = ref<any>({
pageNum: 1,
pageSize: 5,
typeAlias: name as string,
});
// 返回分类名称
function findLabelByAlias<
T extends { alias?: string; label?: string; children?: T[] }
>(alias: string, data: T[], childrenKey: string = "children"): string {
if (!data || data.length === 0) {
return "";
}
// 1. 首先在当前层级查找
for (const item of data) {
if (item.alias === alias) {
return item.label || ""; // 返回 label 或空字符串
}
}
// 2. 如果当前层级没找到,递归查找子节点
for (const item of data) {
const children = (item as any)[childrenKey] as T[];
if (children && children.length > 0) {
const foundLabel = findLabelByAlias(alias, children, childrenKey);
if (foundLabel !== "") {
return foundLabel;
}
}
}
return "";
}
function onPageChange(pageNum: number) {
router.push({
path: route.path + "/page/" + pageNum,
});
}
const { data } = await useFetch(
"http://aitoolht.crgx.net/dh/app/listFrontApp",
{
method: "get",
params: params.value,
}
);
list.value = data.value.rows;
total.value = data.value.total;
</script>
... ...
<template>
<div class="p-10">
<!-- 推荐工具区 -->
<HomeRecommend
:recommendList="list"
:navTitle="findLabelByAlias(name as string, sortList as any)"
navIcon="tag"
:navTitleWidth="120.5"
/>
<el-pagination
background
layout="prev, pager, next"
:hide-on-single-page="true"
v-model:page-size="params.pageSize"
v-model:current-page="params.pageNum"
:total="total"
@current-change="onPageChange"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const sortList = useState("sortTree");
const route = useRoute();
const router = useRouter();
const { pageNum, name } = route.params;
const list = ref<any[]>([]);
const total = ref<number>(0);
const params = ref<any>({
pageNum: Number(pageNum),
pageSize: 5,
typeAlias: name as string,
});
// 返回分类名称
function findLabelByAlias<
T extends { alias?: string; label?: string; children?: T[] }
>(alias: string, data: T[], childrenKey: string = "children"): string {
if (!data || data.length === 0) {
return "";
}
// 1. 首先在当前层级查找
for (const item of data) {
if (item.alias === alias) {
return item.label || ""; // 返回 label 或空字符串
}
}
// 2. 如果当前层级没找到,递归查找子节点
for (const item of data) {
const children = (item as any)[childrenKey] as T[];
if (children && children.length > 0) {
const foundLabel = findLabelByAlias(alias, children, childrenKey);
if (foundLabel !== "") {
return foundLabel;
}
}
}
return "";
}
function onPageChange(pageNum: number) {
if (pageNum === 1) {
router.push({
path: "/category/" + name,
});
} else if (pageNum > 1) {
router.push({
path: route.path + "/page/" + pageNum,
});
}
}
const { data } = await useFetch(
"http://aitoolht.crgx.net/dh/app/listFrontApp",
{
method: "get",
params: params.value,
}
);
list.value = data.value.rows;
total.value = data.value.total;
</script>
... ...
<script setup>
const route = useRoute();
const config = useRuntimeConfig();
const showAd = ref(true);
const appDetail = ref({
types: [],
});
const webSite = useState("webSite");
const detailAd = ref({
width: 300,
height: 177,
frontAdVos: [],
});
function mergeDuplicates(data) {
const map = new Map();
data.forEach((item) => {
if (!map.has(item.id)) {
// 如果是第一次遇到这个id,创建新对象
map.set(item.id, {
id: item.id,
label: item.label,
children: [...(item.children || [])],
});
} else {
// 如果已经存在,合并children
const existing = map.get(item.id);
// 避免重复的子项(基于子项id)
const existingChildIds = new Set(
existing.children.map((child) => child.id)
);
item.children.forEach((child) => {
if (!existingChildIds.has(child.id)) {
existing.children.push(child);
}
});
}
});
return Array.from(map.values());
}
const { data: detailData } = await useFetch(
`http://aitoolht.crgx.net/dh/app/${route.params.id}`
);
const { data: adData } = await useFetch(
"http://aitoolht.crgx.net/dh/ad/listFrontAd",
{
method: "get",
params: { pageSize: 10, pageNum: 1, code: "top" },
}
);
detailAd.value = adData.value.rows[0];
appDetail.value = detailData.value.data;
appDetail.value.types = mergeDuplicates(detailData.value.data.types);
useHead({
title: appDetail.value.popupContent
? `${appDetail.value.title} - ${appDetail.value.popupContent}`
: appDetail.value.title,
meta: [
{ name: "description", content: appDetail.value.description },
{
name: "og:title",
content: `${appDetail.value.title}-${appDetail.value.popupContent}`,
},
{ name: "og:description", content: appDetail.value.description },
{
name: "og:image",
content: config.public.baseUrl + appDetail.value.image,
},
{ name: "og:url", content: route.fullPath },
{ name: "og:site_name", content: webSite.value.webname },
],
});
</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="appDetail.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.baseUrl + appDetail.image"
alt="App Icon"
class="w-16 h-16 object-contain"
/>
<div>
<h1 class="text-2xl font-bold text-[#5961f9]">
{{ appDetail.title }}
</h1>
<p class="text-sm text-gray-600 mt-1">
{{ appDetail.description }}
</p>
<div class="mt-2 flex items-center space-x-2">
<div
v-for="tag in appDetail.types"
class="flex items-center space-x-2"
>
<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>
</div>
</div>
</div>
<div class="flex md:space-x-3 md:mt-0 mt-4">
<a
:href="appDetail.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>
<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"
>
<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>
</div>
</div>
<div class="md:max-w-5xl mx-auto md:p-8 p-2 w-full">
<div v-html="appDetail.content"></div>
</div>
</main>
</main>
</div>
</template>
... ...
<script setup>
const recommendList = ref([]);
const appList = ref([]);
const { data: RecommendData } = await useFetch(
"http://aitoolht.crgx.net/dh/app/listFrontApp",
{
method: "get",
params: { pageSize: 10, pageNum: 1, isRecommend: "1" },
}
);
const { data: allData } = await useFetch(
"http://aitoolht.crgx.net/dh/app/allFrontApp"
);
recommendList.value = RecommendData.value.rows;
appList.value = allData.value.data;
</script>
<template>
<div class="flex flex-col min-h-screen bg-white">
<main class="flex-grow md:p-6 p-2 bg-white">
<!-- Banner 区域 -->
<HomeBanner />
<!-- 广告区域 -->
<!-- 工具展示区 -->
<section class="md:mb-12 mb-6">
<!-- 推荐工具区 -->
<HomeRecommend
:recommendList="recommendList"
navTitle="推荐工具"
navIcon="star"
/>
<div v-for="appItem in appList" class="md:mb-12 mb-6">
<!-- 分类导航及内容 -->
<HomeContent :appData="appItem" />
</div>
</section>
</main>
</div>
</template>
... ...
<template>
<div class="p-10" style="min-height: calc(100vh - 320px)">
<HomeRecommend :recommendList="list" :navTitle="keyword" navIcon="tag" />
<el-pagination
background
layout="prev, pager, next"
:hide-on-single-page="true"
v-model:page-size="params.pageSize"
v-model:current-page="params.pageNum"
:total="total"
@current-change="onPageChange"
/>
</div>
</template>
<script lang="ts" setup>
const route = useRoute();
const router = useRouter();
const { keyword } = route.query as { keyword: string };
const list = ref<any[]>([]);
const total = ref<number>(0);
const params = ref<any>({
pageNum: 1,
pageSize: 10,
title: keyword,
});
function onPageChange(pageNum: number) {
router.push({
path: route.path + "/page/" + pageNum,
query: {
keyword: keyword,
},
});
}
const { data } = await useFetch(
"http://aitoolht.crgx.net/dh/app/listFrontApp",
{
method: "get",
params: params.value,
}
);
list.value = data.value.rows;
total.value = data.value.total;
</script>
... ...
<template>
<div class="p-10">
<!-- 推荐工具区 -->
<HomeRecommend
:recommendList="list"
:navTitle="keyword"
navIcon="tag"
:navTitleWidth="120.5"
/>
<el-pagination
background
layout="prev, pager, next"
:hide-on-single-page="true"
v-model:page-size="params.pageSize"
v-model:current-page="params.pageNum"
:total="total"
@current-change="onPageChange"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const route = useRoute();
const router = useRouter();
const { pageNum } = route.params;
const { keyword } = route.query as { keyword: string };
const list = ref<any[]>([]);
const total = ref<number>(0);
const params = ref<any>({
pageNum: Number(pageNum),
pageSize: 10,
title: keyword,
});
function onPageChange(pageNum: number) {
if (pageNum === 1) {
router.push({
path: "/search",
query: {
keyword: keyword,
},
});
} else if (pageNum > 1) {
router.push({
path: route.path + "/page/" + pageNum,
query: {
keyword: keyword,
},
});
}
}
const { data } = await useFetch(
"http://aitoolht.crgx.net/dh/app/listFrontApp",
{
method: "get",
params: params.value,
}
);
list.value = data.value.rows;
total.value = data.value.total;
</script>
... ...
不能预览此文件类型
{
"extends": "../.nuxt/tsconfig.server.json"
}
... ...
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}
... ...