后期修改完善,上线版本

This commit is contained in:
2025-11-12 18:11:11 +08:00
parent c54f9c9976
commit 8f57683dd5
98 changed files with 2110 additions and 867 deletions

View File

@ -7,7 +7,7 @@ export default {
},
methods: {
async getToolsAsyncData() {
const {data: res} = await this.$api.tool.getToolsList({isHot: 1, page: 1, limit: 6});
const {data: res} = await this.$api.tool.getToolsList({isRecommend: 1, page: 1, limit: 6});
const {code, data} = res;
if (code === 0 && data.list) {
this.pop_tools = data.list;
@ -15,9 +15,9 @@ export default {
},
// 跳转工具详情页
goToToolDetail(item) {
if (item.slug && item.categorySlug) {
if (item.slug && item.categoryName && item.categorySlug) {
this.recordToolClick(item);
this.$router.push(`/detail?tool_slug=${item.slug}&category_slug=${item.categorySlug}`);
this.$router.push(`/detail?tool_slug=${item.slug}&category_slug=${item.categorySlug}&category_name=${item.categoryName}`);
}
},
// 记录工具点击次数
@ -36,7 +36,7 @@ export default {
<template>
<div>
<div class="clearfix">
<img src="/logo/hot.png" :style="{marginRight: '6px'}" alt=""/>
<img src="/logo/hot.png" class="mr-6" alt=""/>
Popular Tools
</div>
<div class="line" />
@ -54,13 +54,16 @@ export default {
</template>
<style scoped lang="scss">
.mr-6 {
margin-right: 6px;
}
.clearfix {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $larg-font-size;
font-weight: bold;
font-family: 'Poppins-SemiBold', serif;
font-family: 'Poppins-SemiBold';
}
.img-box {
@ -103,7 +106,7 @@ export default {
}
.tool-name {
font-family: 'Poppins-Medium', serif;
font-family: 'Poppins-Medium';
color: #64748B;
text-align: center;
white-space: nowrap;

View File

@ -0,0 +1,189 @@
<template>
<div class="tool-card" :class="{ 'checkedBg': item.active, 'hovered': isHover && !item.active }"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave">
<div class="content">
<div class="icon-container">
<img :src="iconBase64 || item.icon || ''" alt="" class="icon-base" />
<img :src="iconSelectedBase64 || item.iconSelected || ''" alt="" class="icon-selected" />
</div>
<span class="text">{{ item.name || '' }}</span>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true
}
},
data() {
return {
iconBase64: '',
iconSelectedBase64: '',
isHover: false
}
},
created() {
// 组件创建时预加载图标
this.preloadIcons();
},
methods: {
// 处理点击事件
handleClick() {
if (this.item.active) {
this.$set(this.item, 'active', false);
} else {
// 先重置所有项,再激活当前项
// 这里我们需要通知父组件来完成全局重置
this.$emit('tool-selected', this.item);
}
},
// 处理鼠标进入事件
handleMouseEnter() {
// 当项已激活时不触发hover效果
if (this.item.active) {
return;
}
this.isHover = true;
},
// 处理鼠标离开事件
handleMouseLeave() {
// 当项已激活时不触发hover效果
if (this.item.active) {
return;
}
this.isHover = false;
},
// 预加载图标并转换为base64
async preloadIcons() {
// 预加载普通图标
if (this.item.icon && this.item.icon !== '') {
try {
this.iconBase64 = await this.convertToBase64(this.item.icon);
} catch (error) {
// console.warn('Failed to load icon:', this.item.icon, error);
}
}
// 预加载选中图标
if (this.item.iconSelected && this.item.iconSelected !== '') {
try {
this.iconSelectedBase64 = await this.convertToBase64(this.item.iconSelected);
} catch (error) {
// console.warn('Failed to load iconSelected:', this.item.iconSelected, error);
}
}
},
// 将图片URL转换为base64
convertToBase64(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous'; // 处理跨域问题
img.onload = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL);
} catch (error) {
reject(error);
}
};
img.onerror = (error) => {
reject(error);
};
img.src = url;
});
}
}
}
</script>
<style lang="scss" scoped>
.tool-card {
background: #FFFFFF;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 10px 16px;
font-weight: 600;
font-family: 'Poppins-SemiBold';
cursor: pointer;
width: fit-content;
.content {
@include display-flex;
white-space: nowrap;
max-width: 220px;
.icon-container {
position: relative;
width: 24px;
height: 24px;
margin-right: 5px;
}
.icon-base,
.icon-selected {
position: absolute;
top: 0;
left: 0;
width: 24px;
height: 24px;
transition: opacity 0.2s ease;
}
// 基础状态:显示普通图标,隐藏选中图标
.icon-base {
opacity: 1;
}
.icon-selected {
opacity: 0;
}
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
// 激活状态样式
.checkedBg {
color: $white;
background: linear-gradient(90deg, $linear-gradient-start 22%, $linear-gradient-end 73%);
.icon-container {
.icon-base {
opacity: 0 !important;
}
.icon-selected {
opacity: 1 !important;
}
}
}
// 悬停状态样式(非激活时)
.hovered {
color: $white;
background: linear-gradient(90deg, $linear-gradient-start 22%, $linear-gradient-end 73%);
// 悬停状态(非激活时):显示选中图标,隐藏普通图标
.icon-container {
.icon-base {
opacity: 0 !important;
}
.icon-selected {
opacity: 1 !important;
}
}
}
</style>

View File

@ -15,9 +15,9 @@ export default {
},
methods: {
goToToolDetail() {
if (this.config.slug && this.categorySlug) {
if (this.config.slug && this.config.categoryName && this.categorySlug) {
this.recordToolClick(this.config);
this.$router.push(`/detail?tool_slug=${this.config.slug}&category_slug=${this.categorySlug}`);
this.$router.push(`/detail?tool_slug=${this.config.slug}&category_slug=${this.categorySlug}&category_name=${this.config.categoryName}`);
}
},
// 记录点击次数
@ -109,7 +109,7 @@ export default {
color: $main-font-color;
font-size: $big-font-size;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
font-family: 'Poppins-SemiBold';
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
@ -119,7 +119,7 @@ export default {
.text {
color: $grey-color;
font-family: 'Poppins-Regular', serif;
font-family: 'Poppins-Regular';
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -1,83 +1,47 @@
<template>
<div class="list">
<div v-for="item in list" class="tools" @click="checkTool(item)">
<span class="tool-card" :class="item.active?'checkedBg':''">
<span class="content">
<img :src="item.icon || ''" alt="" />
<span>{{ item.name || '' }}</span>
</span>
</span>
<div v-for="(item, index) in list" class="tools">
<ToolItem :item="item" @tool-selected="handleToolSelected" />
</div>
</div>
</template>
<script>
export default {
props: ['list'],
data() {
return {
ischeck: 'check',
}
},
created() {
import ToolItem from './ToolItem.vue'
export default {
props: ['list'],
components: {
ToolItem
},
created() {
this.list.forEach(item => {
this.$set(item, 'active', false)
})
},
methods: {
handleToolSelected(selectedItem) {
// 重置所有项
this.list.forEach(item => {
this.$set(item, 'active', false)
})
},
methods: {
checkTool(item) {
if (item.active) {
if (item !== selectedItem) {
this.$set(item, 'active', false);
} else {
// 否则,先重置所有项,再激活当前项
this.list.forEach(i => this.$set(i, 'active', false));
this.$set(item, 'active', true);
this.$emit('tool-selected', item.name);
}
}
});
// 激活当前选中项
this.$set(selectedItem, 'active', true);
this.$emit('tool-selected', selectedItem.name);
}
}
}
</script>
<style lang="scss" scoped>
.list {
display: grid;
grid-template-columns: repeat(4, 1fr); // 4列布局
gap: 20px; // 网格间距
.list {
display: grid;
grid-template-columns: repeat(4, 1fr); // 4列布局
gap: 20px; // 网格间距
.tools {
max-width: 300px;
}
.tool-card {
background: #FFFFFF;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 10px 16px;
display: inline-block;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
cursor: pointer;
&:hover {
color: $white;
background: linear-gradient(90deg, $linear-gradient-start 22%, $linear-gradient-end 73%);
}
.content {
@include display-flex;
img {
margin-right: 5px;
width: 24px;
height: 24px;
}
}
}
.tools .checkedBg {
color: $white;
background: linear-gradient(90deg, $linear-gradient-start 22%, $linear-gradient-end 73%);
}
.tools {
display: inline-block;
}
}
</style>

View File

@ -3,7 +3,7 @@
<div class="bar-list">
<div class="top-box">
<div class="title-wrap">
<img :src="category_icon || ''" alt="" />
<img :src="tool.blueIcon || ''" alt="" />
<span class="title-text gradient-color">
{{tool.categoryName}}
</span>
@ -15,14 +15,24 @@
<div v-if="tool.tagList && tool.tagList.length">
<ScrollList>
<div class="tags">
<div class="tag-item" v-for="(item,index) in tool.tagList" :key="index">
{{ item }}
<div class="tag-item" @click="handleTagClick(item)" :class="{active: item.categorySlug === activeCategorySlug}" v-for="(item,index) in tool.tagList" :key="index">
{{ item.categoryName }}
</div>
</div>
</ScrollList>
<div class="line"></div>
<div v-if="activeSubCategories.length">
<div class="more pointer" @click="tagGoToViewMore">
View more<i class="el-icon-arrow-right"></i>
</div>
<div class="item-card">
<div v-for="(item, index) in activeSubCategories" :key="index" class="item">
<ToolItemCard :config="item" :categorySlug="item.categorySlug || ''" />
</div>
</div>
</div>
</div>
<div>
<div v-else>
<div @click="goToViewMore" class="more pointer" v-if="tool.tagList && tool.tagList.length">
View more<i class="el-icon-arrow-right"></i>
</div>
@ -57,23 +67,49 @@
type: String,
default: '',
},
category_icon: {
type: String,
default: '',
}
},
data() {
return {
activeCategorySlug: '',
activeCategoryName: '',
activeSubCategories: [],
}
},
methods: {
// 查看更多
goToViewMore() {
if (this.categorySlug) {
this.$router.push('/home/more?category_slug=' + this.categorySlug)
if (this.categorySlug && this.tool.categoryName) {
this.$router.push('/home/more?category_slug=' + this.categorySlug + '&tag_name=' + this.tool.categoryName);
}
},
handleTagClick(item) {
this.activeCategorySlug = item.categorySlug;
// 设置二级分类列表
this.activeSubCategories = item.tools || [];
this.activeCategoryName = item.categoryName;
},
tagGoToViewMore() {
if (this.activeCategorySlug && this.activeCategoryName) {
this.$router.push('/home/more?category_slug=' + this.activeCategorySlug + '&tag_name=' + this.activeCategoryName);
}
},
},
watch: {
tool: {
handler(newTool) {
// 检查 tool 中 tagList 是否存在且为数组长度不为0
if (newTool &&
newTool.tagList &&
Array.isArray(newTool.tagList) &&
newTool.tagList.length > 0) {
this.activeCategorySlug = newTool.tagList[0].categorySlug || '';
this.activeCategoryName = newTool.tagList[0].categoryName || '';
this.activeSubCategories = newTool.tagList[0].tools || [];
}
},
immediate: true // 立即执行,确保组件初始化时也会执行
}
}
},
}
</script>
@ -100,7 +136,7 @@
.title-text {
font-weight: 600;
font-size: $larg-font-size;
font-family: 'Poppins-SemiBold', serif;
font-family: 'Poppins-SemiBold';
}
}
@ -120,10 +156,16 @@
padding: 10px;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
/* 显示抓取手势 */
font-family: 'Poppins-SemiBold', sans-serif;
font-family: 'Poppins-SemiBold';
color: #64748B;
font-weight: 600;
cursor: pointer;
&.active {
color: #fff;
background: $header-backgroungd;
border: none;
}
}
.more {
@ -131,7 +173,7 @@
text-align: right;
color: $grey-color;
font-size: $mid-font-size;
font-family: 'Poppins-Regular', serif;
font-family: 'Poppins-Regular';
&:active {
opacity: 0.8;

View File

@ -11,19 +11,21 @@
<div class="third-text">
It includes over a <a href="/" class="special">thousand global AI tools</a>, covering writing, images,
videos, audio, programming,
music, design, chatting, etc., and recommends learning platforms, frameworks and models
music, design, chatting, etc, and recommends learning platforms, frameworks and models
</div>
<!-- 修改输入框容器 -->
<div style="margin: 68px auto 62px">
<div class="margin-62">
<SearchSelectInput />
</div>
</div>
<div class="card flex">
<div class="left-card card-box">
<el-carousel :autoplay="false" height="354px" autoplay :interval="8000">
<el-carousel :autoplay="false" autoplay :interval="8000">
<el-carousel-item v-for="(item, i) in banner" :key="i">
<img :src="item.imageUrl || ''" alt="" style="height: 354px; width: 100%; border-radius: 12px" />
<div class="img-cover">
<img :src="item.imageUrl || ''" alt="" class="img" />
</div>
</el-carousel-item>
</el-carousel>
</div>
@ -60,13 +62,31 @@ export default {
},
methods: {
},
watch: {
'$route'() {
// 当路由变化时滚动到顶部
window.scrollTo(0, 0);
}
},
mounted() {
this.$store.dispatch('getBannerConfig');
}
}
</script>
<style lang="scss" scoped> #home-page {
<style lang="scss" scoped>
.img-cover {
height: 100%; width: 100%; padding: 24px 20px 30px;
}
.img {
height: 100%;
width: 100%;
border-radius: 12px;
}
.margin-62 {
margin: 40px auto 40px;
}
#home-page {
flex: 1;
overflow-y: auto; // 必须启用滚动
position: relative; // 添加相对定位
@ -89,26 +109,23 @@ export default {
.first-text {
line-height: 90px;
font-size: $huge-font-size3;
font-weight: 900;
font-family: 'Poppins-Bold', serif;
font-family: 'Poppins-ExtraBold';
@include text-gradient(90deg, #2563eb, 22%, #7B61FF, 73%);
}
.second-text {
margin: 18px 0;
font-family: 'Poppins-Bold', serif;
font-family: 'Poppins-Bold';
font-size: $huge-font-size2;
font-weight: 900;
line-height: 75px;
}
.third-text {
width: 716px;
font-weight: 500;
color: $grey-color;
font-size: $normal-font-size;
margin-top: 8px;
font-family: 'Poppins-Medium', serif;
font-family: 'Poppins-Medium';
.special {
color: $main-color;
@ -134,6 +151,9 @@ export default {
overflow: hidden;
box-sizing: border-box;
::v-deep .el-carousel {
.el-carousel__container {
height: 354px;
}
.el-carousel__arrow {
opacity: 0 !important;
transition: none !important;

View File

@ -5,7 +5,7 @@
</div>
<div class="line">
</div>
<div class="toolbar" v-for="tool in toolsGroup">
<div class="toolbar" v-for="tool in processedToolsGroup" :key="tool.categoryId">
<Toolbar :tool="tool" :id="`tool-${tool.categoryName}`" :category-slug="tool.categorySlug || ''"></Toolbar>
</div>
</div>
@ -22,6 +22,14 @@ export default {
fullscreenLoading: false,
categoryList: [],
toolsGroup: [],
processedToolsGroup: [] // 处理后的工具列表
}
},
computed: {
// 计算属性,用于获取处理后的工具列表
getProcessedTools() {
this.processData();
return this.processedToolsGroup;
}
},
methods: {
@ -48,6 +56,8 @@ export default {
const {code, data} = res;
if (code === 0 && data.list) {
this.toolsGroup = data.list;
// 数据获取完成后处理数据
this.processData();
}
},
async onLoad() {
@ -55,6 +65,38 @@ export default {
await this.getCategoryAsyncData();
await this.getToolsGroupAsyncData();
this.fullscreenLoading = false;
},
// 处理数据的核心逻辑
processData() {
// 确保工具列表已加载
if (!this.toolsGroup.length) {
return;
}
// 1. 分离一级分类parentId === 0和二级分类parentId !== 0
const mainTools = this.toolsGroup.filter(tool => tool.parentId === 0);
const subTools = this.toolsGroup.filter(tool => tool.parentId !== 0);
// 2. 创建一个映射,方便快速查找一级分类
const mainToolMap = {};
mainTools.forEach(tool => {
// 去除tools属性添加tagList属性
mainToolMap[tool.categoryId] = {
...tool,
tagList: [] // 初始化空的tagList
};
});
// 3. 将二级分类添加到对应的一级分类的tagList中
subTools.forEach(subTool => {
const parentTool = mainToolMap[subTool.parentId];
if (parentTool) {
parentTool.tagList.push(subTool);
}
});
// 4. 获取处理后的工具列表(值的数组)
this.processedToolsGroup = Object.values(mainToolMap);
}
},
created() {

View File

@ -47,6 +47,7 @@ export default {
const slug = to.query.category_slug;
if (slug) {
this.category_slug = slug;
this.tagName = to.query.tag_name;
this.getToolsByTag(slug);
}
}
@ -54,9 +55,11 @@ export default {
mounted() {
// 组件挂载时也检查一次路由参数
const slug = this.$route.query.category_slug;
if (slug) {
const tagName = this.$route.query.tag_name;
if (slug && tagName) {
this.category_slug = slug;
this.getToolsByTag(slug);
this.tagName = tagName;
}
},
}
@ -64,13 +67,13 @@ export default {
<style scoped lang="scss">
.content {
padding-top: 60px;
padding-top: 50px;
padding-bottom: 100px;
.tag-item {
display: inline-block;
padding: 10px;
border-radius: 12px;
font-family: 'Poppins-SemiBold', sans-serif;
font-family: 'Poppins-SemiBold';
color: #fff;
font-weight: 600;
background: $header-backgroungd;