对接数据

This commit is contained in:
2025-10-24 15:45:38 +08:00
parent 672a2f4c90
commit d3375a347f
138 changed files with 16904 additions and 1026 deletions

View File

@ -1,2 +1,2 @@
NUXT_ENV_BASE_API = https://prod.jysd.tech #设置开发环境的api的基础路径
# NUXT_ENV_PUBLIC_PATH = / #设置资源路径前缀
NUXT_ENV_BASE_API = http://47.108.139.184:9062 #设置开发环境的api的基础路径
# NUXT_ENV_PUBLIC_PATH = / #设置资源路径前缀

18
api/article.js Normal file
View File

@ -0,0 +1,18 @@
export default ($axios) => ({
// 获取文章详情
getArticleDetail(slug) {
return $axios.get('/articles/slug/' + slug)
},
// 获取文章列表
getArticleList(params) {
return $axios.get('/articles', { params })
},
// 记录文章点击
recordArticleClick(id) {
return $axios.post('/articles/click', {id})
},
// 文章点赞
clickArticleLike(id) {
return $axios.post('/articles/like', {id})
}
})

8
api/comment.js Normal file
View File

@ -0,0 +1,8 @@
export default ($axios) => ({
getToolCommentList(params) {
return $axios.get('/comment', { params })
},
addToolComment(data) {
return $axios.post('/comment', data)
},
})

8
api/config.js Normal file
View File

@ -0,0 +1,8 @@
export default ($axios) => ({
getModuleConfigKey(Key) {
return $axios.get('/setting', { params: { Key }});
},
getModuleConfigs() {
return $axios.get('/settings');
}
})

View File

@ -1,5 +0,0 @@
export default ($axios) => ({
// getGameList(params) {
// return $axios.get('/x/game', { params })
// },
})

23
api/tools.js Normal file
View File

@ -0,0 +1,23 @@
export default ($axios) => ({
getToolsList(params) {
return $axios.get('/tool', { params })
},
getCategoryList(params) {
return $axios.get('/category', { params })
},
getToolByCategory(params) {
return $axios.get('/tool/group', { params })
},
getToolDetailBySlug(slug) {
return $axios.get('/tool/' + slug)
},
clickToolLike(id) {
return $axios.post('/tool/like', {id})
},
recordToolClickNum(id) {
return $axios.post('/tool/click', {id})
},
searchToolByWord(word) {
return $axios.get('/tool/search', { params:{q: word, limit:50}})
}
})

View File

@ -1,3 +0,0 @@
export default ($axios) => ({
})

View File

@ -3,12 +3,12 @@
<IntegratedLayout>
<div class="container">
<div class="left-container">
<img src="/logo/logo-rect.png" />
<img src="/logo/logo-rect.png" alt="" />
<span>AIProdLaunch</span>
</div>
<div class="right-container">
<div>
<img src="/logo/bottom-logo.png" />
<img src="/logo/bottom-logo.png" alt="" />
</div>
<div class="navigation-bottom">
<span v-for="item in first">
@ -25,7 +25,6 @@
</div>
</div>
</div>
</IntegratedLayout>
</div>
</template>
@ -93,6 +92,7 @@
font-size: $larg-font-size;
color: $main-color;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
}
}
@ -102,12 +102,15 @@
.bottom-span {
color: $grey-color;
font-family: 'Poppins-Regular', serif;
}
.navigation-bottom {
span {
display: inline-block;
padding: 14px;
font-family: 'Poppins-Medium', serif;
cursor: pointer;
}
span:last-child {

View File

@ -0,0 +1,72 @@
<!-- /components/GlobalLoading.vue -->
<template>
<div v-if="isLoading" class="global-loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<p class="loading-text">Loading...</p>
</div>
</div>
</template>
<script>
export default {
name: 'GlobalLoading',
data() {
return {
isLoading: false
}
},
methods: {
show() {
this.isLoading = true;
},
hide() {
this.isLoading = false;
}
}
}
</script>
<style scoped lang="scss">
.global-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px;
background: white;
border-radius: 10px;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #7B61FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
margin-top: 15px;
font-family: 'Poppins-Medium', serif;
color: #7B61FF;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@ -3,7 +3,7 @@
<IntegratedLayout>
<div class="navigation-container">
<div class="logo">
<img src="/logo/white-logo.png" />
<img src="/logo/white-logo.png" alt="" />
<span>AIProdLaunch</span>
</div>
<div class="flex">
@ -17,23 +17,21 @@
<div v-if="activeMenu === item.path && item.meta.children" class="submenu">
<div v-for="sub in item.children" :key="sub.path" @click.stop="goto(sub.path)"
class="submenu-item pointer">
<img :src="`/logo/${sub.meta.icon}.png`" />
<img :src="`/logo/${sub.meta.icon}.png`" alt="" />
{{ sub.name }}
</div>
</div>
</div>
</div>
</div>
</IntegratedLayout>
</div>
</template>
<script>
import {
routes
} from '../router';
} from '~/router';
export default {
name: "Header",
@ -66,8 +64,8 @@ export default {
},
methods: {
handleParentClick(item) {
// 只有没有子菜单时才跳转
if (!item.children || item.children.length === 0) {
const hasVisibleChildren = item.meta && !item.meta.children;
if (hasVisibleChildren) {
this.goto(item.path);
}
},
@ -76,7 +74,6 @@ export default {
* @param {String} path 导航的路径
*/
goto(path) {
console.log(path)
this.$router.push(path)
},
showSubmenu(item) {

View File

@ -0,0 +1,200 @@
<!-- HorizontalDateList.vue -->
<template>
<div class="horizontal-date-wrapper">
<div
class="nav-button prev-button"
:class="{ disabled: !canScrollPrev }"
@click="scrollPrev"
>
<img :src="prevIcon" alt="Previous" />
</div>
<div ref="scrollWrapper" class="scroll-wrapper">
<div ref="scrollContent" class="scroll-content">
<slot></slot>
</div>
</div>
<div
class="nav-button next-button"
:class="{ disabled: !canScrollNext }"
@click="scrollNext"
>
<img :src="nextIcon" alt="Next" />
</div>
</div>
</template>
<script>
import IconPrev from '@/static/launches/icon_prev.png';
import IconNext from '@/static/launches/icon_next.png';
import IconPrevDisabled from '@/static/launches/icon_prev_disabled.png';
import IconNextDisabled from '@/static/launches/icon_next_disabled.png';
export default {
data() {
return {
scrollPosition: 0,
maxScroll: 0,
canScrollPrev: false,
canScrollNext: true
}
},
computed: {
prevIcon() {
return this.canScrollPrev ? IconPrev : IconPrevDisabled;
},
nextIcon() {
return this.canScrollNext ? IconNext : IconNextDisabled;
}
},
mounted() {
this.initScroll();
window.addEventListener('resize', this.updateScrollState);
},
beforeDestroy() {
window.removeEventListener('resize', this.updateScrollState);
},
methods: {
initScroll() {
this.updateScrollState();
},
forceUpdate() {
this.updateScrollState();
},
updateScrollState() {
this.$nextTick(() => {
const wrapper = this.$refs.scrollWrapper;
const content = this.$refs.scrollContent;
if (wrapper && content) {
// 重置滚动位置
wrapper.scrollLeft = 0;
this.scrollPosition = 0;
// 重新计算最大滚动距离
this.maxScroll = Math.max(0, content.offsetWidth - wrapper.offsetWidth);
// 更新按钮状态
this.updateButtonStates();
}
});
},
updateButtonStates() {
const wrapper = this.$refs.scrollWrapper;
if (wrapper) {
this.scrollPosition = wrapper.scrollLeft;
this.canScrollPrev = this.scrollPosition > 0;
this.canScrollNext = this.scrollPosition < this.maxScroll;
}
},
scrollPrev() {
const wrapper = this.$refs.scrollWrapper;
if (!wrapper || wrapper.scrollLeft <= 0) return;
const scrollAmount = wrapper.offsetWidth * 0.8;
const newPosition = Math.max(0, wrapper.scrollLeft - scrollAmount);
wrapper.scrollTo({
left: newPosition,
behavior: 'smooth'
});
// 更新状态
setTimeout(() => {
this.updateButtonStates();
}, 300);
},
scrollNext() {
const wrapper = this.$refs.scrollWrapper;
if (!wrapper) return;
const scrollAmount = wrapper.offsetWidth * 0.8;
const newPosition = Math.min(this.maxScroll, wrapper.scrollLeft + scrollAmount);
wrapper.scrollTo({
left: newPosition,
behavior: 'smooth'
}
);
// 更新状态
setTimeout(() => {
this.updateButtonStates();
}, 300);
}
},
watch: {
// 监听插槽内容变化
'$slots.default'() {
this.updateScrollState();
}
}
}
</script>
<!-- HorizontalDateList.vue -->
<style scoped lang="scss">
.horizontal-date-wrapper {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.nav-button {
width: 44px;
height: 44px;
cursor: pointer;
background-color: #FFFFFF;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.8;
}
img {
width: 44px;
height: 44px;
}
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.scroll-wrapper {
flex: 1; /* 使用flex而不是固定宽度 */
min-width: 0; /* 允许flex项目收缩 */
overflow-x: auto;
overflow-y: hidden;
position: relative;
/* 隐藏滚动条 */
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.scroll-content {
display: flex; /* 使用flex布局 */
gap: 12px; /* 设置元素间距 */
white-space: nowrap;
width: max-content; /* 确保内容宽度正确 */
}
}
</style>

View File

@ -72,7 +72,7 @@
justify-content: flex-end;
align-items: center;
gap: 8px;
margin: 20px;
margin: 20px 0;
}
.pagination-btn,

62
components/Rate.vue Normal file
View File

@ -0,0 +1,62 @@
<script>
export default {
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Number,
default: 0,
},
readonly: {
type: Boolean,
default: false
}
},
methods: {
handleChange(val) {
if (!this.readonly) {
this.$emit('change', val);
}
}
}
}
</script>
<template>
<div class="rate flex items-center">
<el-rate
:value="value"
@input="handleChange"
:colors="['#7B61FF', '#7B61FF', '#7B61FF']"
void-color="#E2E8F0"
:size="24"
:disabled="readonly">
</el-rate>
<div class="rate-num" :class="value ? 'gradient-box' : ''">{{ value }}</div>
</div>
</template>
<style scoped lang="scss">
::v-deep .el-rate {
height: 24px;
.el-rate__icon {
font-size: 24px;
}
}
.rate-num {
width: 28px;
height: 28px;
line-height: 28px;
text-align: center;
background: #E2E8F0;
color: #fff;
border-radius: 12px;
margin-left: 8px;
font-family: 'Poppins-Regular', serif;
}
.gradient-box {
background: $header-backgroungd;
}
</style>

View File

@ -0,0 +1,97 @@
<script>
export default {
props: {
value: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
}
},
data() {
return {
localValue: this.value, // 创建本地副本
isInputFocused: false,
};
},
watch: {
value(newVal) {
this.localValue = newVal; // 监听外部 value 变化并同步到本地
}
},
methods: {
handleInput(event) {
const newValue = event.target.value;
this.localValue = newValue;
this.$emit('input', newValue); // 触发 input 事件以支持 v-model
},
// 新增:处理查询事件
handleSearch() {
this.$emit('search', this.localValue);
}
}
}
</script>
<template>
<div class="input-container" :class="{ focused: isInputFocused || localValue }">
<input
type="text"
:placeholder="placeholder"
:value="localValue"
@input="handleInput"
@focus="isInputFocused = true"
@blur="isInputFocused = false"
@keydown.enter="handleSearch"
>
<img
alt=""
:src="isInputFocused || localValue ? '/search/home_search_s.png' : '/search/home_search.png'"
@click="handleSearch"
/>
</div>
</template>
<style scoped lang="scss">
/* 添加输入框容器聚焦状态样式 */
.input-container {
position: relative;
input {
width: 100%;
height: 60px;
padding: 0 20px;
border-radius: 12px;
border: 2px solid transparent;
outline: none;
font-size: $normal-font-size;
transition: border-color 0.3s ease;
&::placeholder {
color: #ccc;
}
}
img {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
width: 54px;
height: 40px;
pointer-events: none;
cursor: pointer;
}
&.focused {
background:
linear-gradient(white, white) padding-box,
linear-gradient(90deg, $linear-gradient-start 22%, $linear-gradient-end 73%) border-box;
border-radius: 12px;
border: 1px solid transparent;
}
}
</style>

View File

@ -0,0 +1,163 @@
<template>
<div class="search-select-container">
<!-- 使用现有的 SearchInput 组件 -->
<SearchInput
ref="searchInput"
v-model="searchValue"
:placeholder="placeholder"
@search="handleSearch"
/>
<!-- 搜索结果展示区域 -->
<div v-if="searched && hasResults" class="results-list">
<div
v-for="(item, index) in searchResults"
:key="index"
class="result-item flex items-center"
@click="selectItem(item)"
>
<img alt="" :src="item.icon_url || ''" />
<div>{{ item.name || '' }}</div>
</div>
</div>
<!-- 无结果提示 -->
<div v-else-if="searched && !hasResults" class="no-results flex items-center">
<img src="/search/icon_alarm.png" alt="" />
<div>No relevant content was found</div>
</div>
</div>
</template>
<script>
import SearchInput from './SearchInput.vue';
export default {
name: 'SearchSelectInput',
components: {
SearchInput
},
data() {
return {
searchValue: '',
searched: false,
searchResults: [],
loading: false,
placeholder: 'Please enter the key words'
};
},
computed: {
hasResults() {
return this.searchResults && this.searchResults.length > 0;
}
},
methods: {
async handleSearch(value) {
if (!value) {
this.searched = false;
this.searchResults = [];
return;
}
this.loading = true;
try {
await this.searchTools(value);
this.searched = true;
} catch (error) {
this.searchResults = [];
this.searched = true;
} finally {
this.loading = false;
}
},
selectItem(item) {
this.searched = false;
this.searchResults = [];
this.jumpToToolDetail(item);
},
// 搜索相关工具
async searchTools(keywords) {
const {data: res} = await this.$api.tool.searchToolByWord(keywords);
const {code, data} = res;
if (code === 0 && data.list) {
this.searchResults = data.list;
}
},
// 跳转对应工具详情
jumpToToolDetail(item) {
if (item.slug && item.category_slug) {
this.$router.push('/detail?tool_slug=' + item.slug + '&category_slug=' + item.category_slug);
this.recordToolClick(item);
}
},
// 记录工具点击次数
async recordToolClick(item) {
if (item.id) {
await this.$api.tool.recordToolClickNum(item.id);
}
}
}
};
</script>
<style scoped lang="scss">
.search-select-container {
position: relative;
width: 100%;
}
.results-list {
padding: 30px 20px;
margin-top: 10px;
border-radius: 12px;
max-height: 350px;
overflow-y: auto;
background: white;
box-shadow: 0 10px 30px #0000000d;
.result-item {
padding: 20px 30px;
cursor: pointer;
border-radius: 6px;
gap: 12px;
color: #1E293B;
img {
width: 28px;
height: 28px;
}
&:hover {
background-color: #F5F6F9;
color: #7B61FF;
}
&:active {
background-color: #F5F6F9;
color: #7B61FF;
}
}
}
.no-results {
gap: 30px;
padding: 10px 16px;
text-align: center;
border-radius: 8px;
margin: 16px auto 0;
color: #1E293B;
font-family: 'Poppins-SemiBold', serif;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
width: fit-content; // 改为自适应内容宽度
img {
width: 24px;
height: 24px;
}
}
</style>

View File

@ -8,39 +8,59 @@
<Footer></Footer>
<el-backtop target="#default-layout" :visibility-height="500" class="custom-backtop">
<div class="backtop-content">
<img src="/logo/back-top.png" />
<div class="backtop-content" @mousedown="handleMouseDown" @mouseup="handleMouseUp">
<img src="/logo/back-top.png" alt="" />
</div>
</el-backtop>
</div>
</template>
<script>
export default {
export default {
data() {
return {
}
},
methods: {
handleMouseDown() {
const backtopContent = document.querySelector('.backtop-content');
if (backtopContent) {
backtopContent.style.opacity = '0.8';
}
},
handleMouseUp() {
const backtopContent = document.querySelector('.backtop-content');
if (backtopContent) {
backtopContent.style.opacity = '1';
}
}
}
}
</script>
<style lang="scss" scoped>
#default-layout {
position: relative;
@include flex-column;
overflow: auto;
}
#default-layout {
position: relative;
@include flex-column;
overflow: auto;
}
.custom-backtop {
position: fixed;
right: 15% !important;
bottom: 30% !important;
z-index: 999;
transition: transform 0.3s;
.custom-backtop {
position: fixed;
right: 15% !important;
bottom: 30% !important;
z-index: 999;
transition: transform 0.3s;
}
}
.backtop-content {
transition: opacity 0.2s ease-in-out;
}
#home-container {
width: 100%;
// min-height: 100vh;
#home-container {
width: 100%;
// min-height: 100vh;
background-color: $background-color;
}
background-color: $background-color;
}
</style>

View File

@ -8,6 +8,11 @@ export default {
ssr: true,
target: 'server',
server: {
host: '0.0.0.0', // 允许外部访问,默认是 localhost (127.0.0.1)
port: 3000 // 可选:指定端口号,默认是 3000
},
// publicPath: process.env.NUXT_ENV_PUBLIC_PATH || '/', //服务器资源路径是否有前缀
loading: false, //是否在路由切换或者asyncData/Fetch异步请求期间出现进度条 可自定义详情见https://v2.nuxt.com/docs/features/loading#loading
@ -51,7 +56,10 @@ export default {
css: [
'normalize.css/normalize.css', // 引入
'@/styles/index.scss', //引入全局样式
'@/styles/text.scss'
'@/styles/text.scss',
'@/styles/font.scss',
'@/styles/flex.scss',
'@/styles/article.scss',
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
@ -95,7 +103,7 @@ export default {
},
axios: {
baseURL: process.env.NUXT_ENV_BASE_API || 'https://api.example.com', // API 根地址
baseURL: process.env.NUXT_ENV_BASE_API || 'http://47.108.139.184:9062', // API 根地址
credentials: true, // 是否跨域请求时携带 cookie
timeout: 30000, // 请求超时时间ms
// proxy:true, //开启代理
@ -104,7 +112,7 @@ export default {
//代理
// proxy: {
// '/api': { // 需要转发代理的请求前缀
// target: 'http://localhost:3000', // 代理的目标地址
// target: 'http://app.topv100.com:10041', // 代理的目标地址
// pathRewrite: { '^/api/': '' }, // 重写路径:去掉 /api 前缀
// changeOrigin: true // 是否改变请求源
// },

View File

@ -9,17 +9,19 @@
"generate": "nuxt generate"
},
"dependencies": {
"@better-scroll/core": "^2.5.1",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/dotenv": "^1.4.2",
"@nuxtjs/proxy": "^2.1.0",
"@nuxtjs/router": "^1.7.0",
"@nuxtjs/style-resources": "^1.2.2",
"axios": "^1.12.2",
"cookie-universal-nuxt": "^2.2.2",
"core-js": "^3.25.3",
"element-ui": "^2.15.14",
"normalize.css": "^8.0.1",
"nuxt": "^2.15.8",
"vue": "^2.7.10",
"element-ui": "^2.15.14",
"vue-server-renderer": "^2.7.10",
"vue-template-compiler": "^2.7.10"
},

View File

@ -1,125 +1,92 @@
<template>
<div class="tools-container">
<div class="card" v-for="item in 6">
<div class="left">
<img src="" />
</div>
<div class="right">
<div>
<div class="title-text">
AI Diagnostic Tool Analyzes 10M+ Medical Images with 99.5% Accuracy
</div>
<div class="content-text">
Revolutionary AI system detects diseases from X-rays, MRIs, and CT scans using a database of 10
million annotated images, supporting early diagnosis in 150 countries.
</div>
</div>
<div class="bottom-info">
<div class="first">
<div>
<img src="/logo/logo_xs.png" />
<span>AI toolkit</span>
</div>
<div class="praise">
<img src="/logo/praise.png" />
<span>{{praise_count}}</span>
</div>
</div>
<div class="time">
<img src="/logo/clock.png" />
<span>2025-07-12</span>
</div>
</div>
</div>
<div class="tools-container" v-loading.fullscreen.lock="fullscreenLoading">
<div class="input">
<SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
</div>
<div>
<div class="list">
<ListCardItem v-for="it in articleList" :key="it.id" :item="it" type="tool" />
</div>
<div class="pagination-wrapper">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</template>
<script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default {
components: {
ListCardItem,
},
data() {
return {
praise_count: 12,
currentPage: 1,
totalPages: 10, // 假设总页数为 10
totalPages: 1,
pageSize: 10,
articleList: [],
searchText: '',
total: 0,
fullscreenLoading: false,
}
},
watch: {
total() {
this.calculateTotalPages();
},
pageSize() {
this.calculateTotalPages();
}
},
methods: {
// 模糊搜索
async handleTextSearch() {
await this.getArticleListData(this.currentPage, this.pageSize, this.searchText);
},
calculateTotalPages() {
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
this.getArticleListData(pageNumber, this.pageSize);
},
// 获取文章列表
async getArticleListData(page = 1, limit = 10, searchText) {
const params = {page, limit, articleType: 'tool', title: searchText};
if (!searchText) {
delete params.title;
}
this.fullscreenLoading = true;
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
this.total = data.total;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
},
},
mounted() {
this.getArticleListData(this.currentPage, this.pageSize);
}
}
</script>
<style lang="scss" scoped>
.card {
background: $white;
box-shadow: 0px 10px 30px 0px rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 40px 30px;
display: flex;
margin-bottom: 30px;
gap: 30px;
}
.left {
img {
width: 440px;
height: 220px;
border-radius: 6px;
}
}
.bottom-info,
.right {
display: flex;
justify-content: space-between;
}
.right {
flex-direction: column;
.title-text {
margin-bottom: 14px;
font-weight: 600;
font-size: $normal-font-size;
color: #3A4A65;
}
.content-text {
font-weight: 400;
font-size: $mid-font-size;
color: $grey-color;
}
}
.bottom-info {
color: #C8CFD7;
font-size: 15px;
.first {
width: 30%;
display: flex;
justify-content: space-between
}
img {
margin-right: 8px
}
div {
@include display-flex;
}
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom: 60px;
}
}
.list {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 60px;
}
</style>

View File

@ -0,0 +1,382 @@
<template>
<div id="normal-container" v-loading.fullscreen.lock="fullscreenLoading">
<IntegratedLayout>
<div class="content">
<div class="views-title">{{ newsDetail.title || '' }}</div>
<div class="views-header flex-between-center">
<div class="description">
{{ newsDetail.summary || '' }}
</div>
<div class="flex flex-col" style="gap: 16px">
<div class="flex" style="gap: 20px">
<ThumbBtn :like-count="newsDetail.likeCount || 0" :id="newsDetail.id || 0" type="article" @like-success="refreshToolDetail" />
<CommentBtn :count="commentCount" />
</div>
<div class="flex items-center justify-center" style="padding: 0 7px; gap: 8px">
<img src="/ToolDetail/icon_clock1.png" alt="" style="width: 16px; height: 16px" />
<div style="font-size: 14px; color: #869EC2; font-family: 'Poppins-Regular', serif; line-height: 18px">
{{ formatDate(newsDetail.publishTime) }}
</div>
</div>
</div>
</div>
<div class="container flex justify-between">
<div class="left-content flex flex-col">
<div class="card preview-wrapper">
<img src="/" alt="" style="width: 100%; height: 100%" />
</div>
<div class="flex-1">
<NewsDetail :article="newsDetail" />
</div>
</div>
<div class="right-content">
<!--轮播图-->
<div class="card swiper-box">
<el-carousel :autoplay="false" height="140px">
<el-carousel-item v-for="(item, i) in banner" :key="i">
<img :src="item.imageUrl || ''" alt="" style="height: 140px; width: 100%;" />
</el-carousel-item>
</el-carousel>
</div>
<!--网站导航-->
<div class="card pop-list">
<div style="padding: 24px 4px">
<PopularToolList />
</div>
</div>
<!--文章列表-->
<div class="card">
<div style="padding: 24px 4px">
<div class="clearfix">
<img src="/logo/hot.png" :style="{marginRight: '6px'}" alt=""/>
Latest Article
</div>
<div class="list-scroll">
<div class="list">
<ArticleCardItem v-for="it in latestNewsList" :key="it.id" :item="it" @refresh="goToRelatedNewsDetail" />
</div>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 44px">
<div class="comment-title">Related news</div>
<div class="flex" style="gap: 20px">
<NewsCardItem v-for="it in relatedNewsList" :key="it.id" :item="it" @refresh="goToToolDetail" />
</div>
</div>
<Comment comment-type="article" :id="newsDetail.id" @update:commentCount="handleCommentCountUpdate" />
</div>
</IntegratedLayout>
</div>
</template>
<script>
import Comment from "@/pages/ToolDetail/Comment/index.vue";
import CommentBtn from "@/pages/ToolDetail/components/CommentBtn.vue";
import ThumbBtn from "@/pages/ToolDetail/components/ThumbBtn.vue";
import NewsCardItem from "@/pages/DailyNews/components/NewsCardItem.vue";
import ArticleCardItem from "@/pages/DailyNews/components/ArticleCardItem.vue";
import PopularToolList from "@/pages/Home/components/PopularToolList.vue";
import NewsDetail from "@/pages/DailyNews/NewsDetailIndex/NewsDetail.vue";
export default {
components: {
NewsDetail,
ArticleCardItem,
NewsCardItem,
ThumbBtn, CommentBtn,
Comment,
PopularToolList,
},
data() {
return {
newsDetail: {},
news_slug: '',
articleList: [],
latestNewsList: [],
type: '',
commentCount: 0,
fullscreenLoading: false,
}
},
methods: {
// 刷新工具详情数据
refreshToolDetail() {
if (this.news_slug) {
this.getNewsDetail(this.news_slug);
}
},
// 重新跳转路由
goToToolDetail(item) {
if (item.slug) {
this.$router.push('/tools-detail?news_slug=' + item.slug + '&type=' + this.type)
}
},
// 侧边新闻重新跳转路由
goToRelatedNewsDetail(item) {
if (item.slug && item.articleType) {
this.$router.push('/tools-detail?news_slug=' + item.slug + '&type=' + item.articleType)
}
},
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 获取新闻详情
async getNewsDetail(slug) {
const {data: res} = await this.$api.article.getArticleDetail(slug);
const {code, data} = res;
if (code === 0) {
this.newsDetail = {...data};
}
},
// 获取相关文章列表
async getArticleListData(page = 1, limit = 4, type) {
if (!type) {
return false;
}
const params = {page, limit, articleType: type};
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
}
},
handleCommentCountUpdate(count) {
this.commentCount = count;
},
// 获取最新文章列表
async getLatestArticleListData() {
const params = {
page: 1,
limit: 21,
excludeTypes: ['launches'],
sortField: 'publish_time',
sortOrder: 'desc',
};
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.latestNewsList = data.list.filter(e => e.id !== this.newsDetail.id);
}
},
async onLoad() {
this.fullscreenLoading = true;
this.news_slug = this.$route.query.news_slug;
this.type = this.$route.query.type;
if (this.news_slug && this.type) {
await this.$store.dispatch('getBannerConfig');
await this.getNewsDetail(this.news_slug);
await this.getArticleListData(1, 4, this.type);
await this.getLatestArticleListData();
}
this.fullscreenLoading = false;
},
},
watch: {
// 监听路由变化
'$route'(to, from) {
this.news_slug = to.query.news_slug;
this.type = to.query.type;
if (this.news_slug && this.type) {
this.$router.go(0);
}
}
},
mounted() {
this.onLoad();
},
computed: {
relatedNewsList() {
if (!this.newsDetail.id) {
return [];
}
return this.articleList.filter(e => e.id !== this.newsDetail.id);
},
banner() {
const bannerConfig = this.$store.getters.bannerConfig;
if (bannerConfig.news && bannerConfig.news.length > 0) {
return bannerConfig.news;
}
return [];
},
},
}
</script>
<style scoped lang="scss">
.card {
padding: 16px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.content {
padding-top: 192px;
padding-bottom: 100px;
.views-title {
font-family: 'Poppins-Bold', serif;
font-size: 40px;
font-weight: 700;
margin-top: 78px;
}
.views-header {
margin-top: 10px;
.description {
margin-right: 350px;
font-family: 'Poppins-Medium', serif;
font-size: 18px;
color: #64748B;
}
}
.container {
gap: 20px;
margin-top: 114px;
margin-bottom: 40px;
.right-content {
width: 372px;
display: flex;
flex-direction: column;
gap: 20px;
.list-scroll {
overflow-y: auto;
overflow-x: hidden;
height: 1350px;
}
.swiper-box {
height: 174px;
::v-deep .el-carousel {
.el-carousel__arrow {
opacity: 0 !important;
transition: none !important;
}
.el-carousel__indicators {
.el-carousel__indicator {
height: 4px !important;
width: 12px !important;
border-radius: 2.66px !important;
border: 0.66px solid #2563eb !important;
background: transparent !important;
margin: 0 3px 0 3px !important;
padding: 0 !important;
.el-carousel__button {
background-color: transparent;
}
&.is-active {
border: none !important;
width: 20px !important;
background: linear-gradient(0deg, #2563EB 22%, #7B61FF 73%) !important;
}
// 重置其他可能的默认样式
&:before,
&:after {
display: none !important;
}
}
}
}
}
.clearfix {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $larg-font-size;
font-weight: bold;
font-family: 'Poppins-SemiBold', serif;
}
.pop-list {
.pop-item {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, auto);
justify-content: space-between;
gap: 32px;
.box {
display: flex;
flex-direction: column;
align-items: center;
.img-box {
width: 50px;
height: 50px;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #E2E8F0;
@include flex-center;
margin-bottom: 12px;
&:hover {
transform: scale(1.15);
cursor: pointer;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
box-shadow: 0 5px 7px 0 #00000014;
}
}
.tool-name {
font-family: 'Poppins-Medium', serif;
color: #64748B;
}
}
}
}
.article-list {
flex: 1;
}
}
.left-content {
flex: 1;
gap: 20px;
.preview-wrapper {
height: 320px;
}
.comment-title {
font-size: 30px;
font-weight: 600;
margin-bottom: 30px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
}
}
.comment-title {
font-size: 30px;
font-weight: 600;
margin-bottom: 30px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
}
</style>

View File

@ -1,22 +1,92 @@
<template>
<div>
<!-- 组件内容 -->
</div>
<div class="tools-container" v-loading.fullscreen.lock="fullscreenLoading">
<div class="input">
<SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
</div>
<div class="list">
<ListCardItem v-for="it in articleList" :key="it.id" :item="it" type="framework" />
</div>
<div class="pagination-wrapper">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</template>
<script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default {
data() {
return {
// 数据项
}
},
methods: {
// 方法
}
components: {
ListCardItem,
},
data() {
return {
currentPage: 1,
totalPages: 1,
pageSize: 10,
articleList: [],
total: 0,
searchText: '',
fullscreenLoading: false
}
},
watch: {
total() {
this.calculateTotalPages();
},
pageSize() {
this.calculateTotalPages();
}
},
methods: {
// 模糊搜索
async handleTextSearch() {
await this.getArticleListData(this.currentPage, this.pageSize, this.searchText);
},
calculateTotalPages() {
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
this.getArticleListData(pageNumber, 10);
},
// 获取文章列表
async getArticleListData(page = 1, limit = 10, searchText) {
const params = {page, limit, articleType: 'framework', title: searchText};
if (!searchText) {
delete params.title;
}
this.fullscreenLoading = true;
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
this.total = data.total;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
}
},
mounted() {
this.getArticleListData(this.currentPage, this.pageSize);
}
}
</script>
<style lang="scss" scoped>
</style>
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom: 60px;
}
}
.list {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 60px;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="card" @click="handleClick">
<div class="left">
<img :src="item.coverImage || ''" alt="" />
</div>
<div class="right flex-1">
<div>
<div class="title-text">
{{ item.title || '' }}
</div>
<div class="content-text">
{{ item.summary || '' }}
</div>
</div>
<div class="bottom-info">
<div class="first" style="gap: 50px">
<div>
<img src="/logo/logo_xs.png" alt="" />
<span>{{ item.slug || ''}}</span>
</div>
<div class="praise flex items-center">
<img src="/logo/praise.png" alt="" style="width: 16px; height: 16px" />
<span>{{ item.likeCount || 0 }}</span>
</div>
</div>
<div class="time">
<img src="/logo/clock.png" alt="" />
<span>{{ item.publishTime || '' }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: '',
}
},
data() {
return {
}
},
methods: {
handleClick() {
if (!this.item.slug || !this.type){
return false;
}
this.articleClick(this.item.id);
this.$router.push('/tools-detail?news_slug=' + this.item.slug + '&type=' + this.type)
},
// 记录文章点击次数
async articleClick(id) {
if (id) {
await this.$api.article.recordArticleClick(id);
}
}
},
}
</script>
<style scoped lang="scss">
.card {
background: $white;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 40px 30px;
display: flex;
gap: 30px;
border: 1px solid transparent;
transition: border 0.3s ease; // 添加过渡效果
// hover 时添加渐变边框
&:hover {
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
&:active {
opacity: 0.8;
}
.left {
img {
width: 440px;
height: 220px;
border-radius: 6px;
}
}
.bottom-info,
.right {
display: flex;
justify-content: space-between;
}
.right {
flex-direction: column;
.title-text {
margin-bottom: 14px;
font-weight: 600;
font-size: $normal-font-size;
color: #3A4A65;
transition: color 0.3s ease; // 添加颜色过渡效果
// hover 时改变标题颜色
.card:hover & {
color: #1890ff;
}
}
.content-text {
font-weight: 400;
font-size: $mid-font-size;
color: $grey-color;
}
}
.bottom-info {
color: #C8CFD7;
font-size: 15px;
.first {
display: flex;
justify-content: space-between
}
img {
margin-right: 8px
}
div {
@include display-flex;
}
}
}
</style>

View File

@ -1,28 +1,24 @@
<template>
<div id="normal-container">
<IntegratedLayout>
<div class="bread-menu">
<span>AI Hub</span>
<i class="el-icon-arrow-right"></i>
<span class="crumbs gradient-color">{{$route.name}}</span>
</div>
<div class="top-text">
<div class="title">
{{$route.name}}
<div class="content">
<div class="bread-menu">
<span>AI Hub</span>
<i class="el-icon-arrow-right"></i>
<span class="crumbs gradient-color">{{$route.name}}</span>
</div>
<div class="description">
A comprehensive collection of cutting-edge AI tools, featuring detailed explanations of their
functionalities, practical usage, and real-world application scenarios. Quickly grasp emerging
product trends and learn how each tool can be leveraged for work, research, or creative projects.
<div class="top-text">
<div class="title">
{{$route.name}}
</div>
<div class="description">
A comprehensive collection of cutting-edge AI tools, featuring detailed explanations of their
functionalities, practical usage, and real-world application scenarios. Quickly grasp emerging
product trends and learn how each tool can be leveraged for work, research, or creative projects.
</div>
</div>
<nuxt-child />
</div>
<div class="input">
<div class="input-container">
<input type="text" placeholder="Please enter the key words">
<i class="el-icon-search gradient-color search-icon pointer"></i>
</div>
</div>
<nuxt-child />
</IntegratedLayout>
</div>
</template>
@ -45,28 +41,27 @@
</script>
<style lang="scss" scoped>
.bread-menu {
font-size: $mid-font-size;
margin: 100px 0;
}
.content {
padding-bottom: 100px;
.bread-menu {
font-size: $mid-font-size;
margin: 100px 0;
font-family: 'Poppins-Medium', serif;
.title {
font-weight: bold;
font-size: $huge-font-size1;
}
.crumbs {
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
}
}
.description {
font-size: $big-font-size;
color: $grey-color;
}
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom:60px;
.title {
font-weight: bold;
font-size: $huge-font-size1;
}
.description {
font-size: $big-font-size;
color: $grey-color;
}
}
</style>

View File

@ -4,126 +4,69 @@
<div class="title">
About AIToolsFinder
</div>
<div class="date">
Last updated April 4, 2024
<div class="little-title">
The Ultimate Global AI Tools Directory, Discover the Best New AI Products
</div>
<div class="sub-title">
Welcome to AIToolsFinder your all-in-one gateway to the world of artificial intelligence.
</div>
</div>
<div>
How do we process your information?We process your information to provide, improve, and administer our
Services, communicate with you for security and fraud prevention, and comply with the law. We may also
process your information for other purposes with your consent. We process your information only when we have
a valid legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.
How do we process your information?We process your information to provide, improve, and administer our
Services, communicate with you for security and fraud prevention, and comply with the law. We may also
process your information for other purposes with your consent. We process your information only when we have
a valid legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.
<div class="content">
Were more than just a directory. AIToolsFinder is designed to help you discover, learn, and apply AI in ways that truly make a difference in your personal and professional life. Whether youre a student exploring new technologies, a designer looking for creative tools, a developer in need of coding assistance, or a business team seeking automation solutionsAIToolsFinder is here to guide you.
</div>
<div class="terms-title">
Heres what youll find with us:
</div>
<div class="terms-item">
<div class="dot"></div>
<div class="item-content">
<span class="item-title">AI Tool Discovery </span>
<span class="item-text">
Thousands of carefully curated AI tools across writing, design, development, business, and entertainment.
</span>
</div>
</div>
<div class="terms-item">
<div class="dot"></div>
<div class="item-content">
<span class="item-title">Learning Resources </span>
<span class="item-text">
Tutorials, frameworks, and model recommendations to help you quickly level up your AI skills.
</span>
</div>
</div>
<div class="terms-item">
<div class="dot"></div>
<div class="item-content">
<span class="item-title">Industry Insights </span>
<span class="item-text">
Stay updated with the latest trends, breakthroughs, and real-world applications in AI.
</span>
</div>
</div>
<div class="terms-item">
<div class="dot"></div>
<div class="item-content">
<span class="item-title">Disclaimer </span>
<span class="item-text">
AIToolsFinder is the official and only website under this name. We do not operate paid groups, courses, or third-party affiliates. Please always verify youre on the official site.
</span>
</div>
</div>
<div class="terms-item">
<div class="dot"></div>
<div class="item-content">
<span class="item-title">Get in Touch </span>
<span class="item-text">Got a new AI tool to share, want to collaborate, or have ideas for improving AIToolsFinder? Reach us anytime at </span>
<span class="item-link"> corina@inziqi.com.</span>
</div>
</div>
<div class="bottom-line">
<div class="bottom-line-left"></div>
<div class="bottom-text gradient-color">
Lets explore the future of AItogether
</div>
<div class="bottom-line-right"></div>
</div>
</div>
</template>
@ -142,5 +85,88 @@
</script>
<style lang="scss" scoped>
.sub-title {
color: #3a4a65;
font-size: 20px;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
margin-top: 30px;
text-align: center;
}
.content {
color: #64748B;
font-size: 18px;
font-family: 'Poppins-Medium', serif;
}
.terms-title {
color: #64748B;
font-size: 18px;
font-family: 'Poppins-Bold', serif;
margin-top: 30px;
margin-bottom: 20px;
font-weight: bold;
}
.little-title {
font-family: 'Poppins-Medium', serif;
font-size: 18px;
color: #64748B;
margin-top: 20px;
text-align: center;
}
.terms-item {
display: flex;
align-items: flex-start;
margin-bottom: 25px;
.dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: $header-backgroungd;
margin-right: 14px;
margin-top: 7px;
flex-shrink: 0;
}
.item-content {
flex: 1;
}
.item-title {
color: #64748B;
font-size: 18px;
font-family: 'Poppins-Bold', serif;
font-weight: bold;
}
.item-text {
color: #64748B;
font-size: 18px;
font-family: 'Poppins-Medium', serif;
}
.item-link {
color: #7B61FF;
font-size: 18px;
font-family: 'Poppins-Medium', serif;
}
}
.bottom-line {
margin-top: 75px;
display: flex;
justify-content: center;
align-items: center;
gap: 23px;
.bottom-text {
font-weight: 600;
font-size: 28px;
font-family: 'Poppins-SemiBold', serif;
}
.bottom-line-left {
width: 120px;
height: 8px;
background: url("/about/linear-line-left.png") no-repeat center/contain;
}
.bottom-line-right {
width: 120px;
height: 8px;
background: url("/about/linear-line-right.png") no-repeat center/contain;
}
}
</style>

View File

@ -1,129 +1,66 @@
<template>
<div class="text-container">
<div class="text-container terms">
<div class="top">
<div class="title">
<div class="terms-title">
Privacy Policy
</div>
<div class="date">
<div class="terms-date">
Last updated April 4, 2024
</div>
</div>
<div>
How do we process your information?We process your information to provide, improve, and administer our
Services, communicate with you for security and fraud prevention, and comply with the law. We may also
process your information for other purposes with your consent. We process your information only when we have
a valid legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.
How do we process your information?We process your information to provide, improve, and administer our
Services, communicate with you for security and fraud prevention, and comply with the law. We may also
process your information for other purposes with your consent. We process your information only when we have
a valid legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.
<div class="content">
<div class="title" style="margin-bottom: 30px">This privacy notice for Futurepedia LLC (doing business as Futurepedia) ("we," "us," or "our") describes how and why we might collect, store, use, and/or share ("process") your information when you use our services ("Services"), such as when you:</div>
<div class="text text-dot">
Visit our website at <span class="text-link">http://www.futurepedia.io/</span> or any website of ours that links to this privacy notice
</div>
<div class="text text-dot" style="margin-bottom: 30px">Engage with us in other related ways, including any sales, marketing, or events</div>
<div class="title">Questions or concerns?</div>
<div class="text text-dot">Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with our policies and practices, please do not use our Services. If you still have questions or concerns, please contact us at </div>
<div class="text-link" style="margin-bottom: 40px">contact@futurepedia.io.</div>
<div class="title-dark">SUMMARY OF KEY POINTS</div>
<div class="title" style="margin-bottom: 30px">This summary provides key points from our privacy notice, but you can find out more details about any of these topics by clicking the link following each key point or by using our table of contents below to find the section you are looking for.</div>
<div class="title">What personal information do we process?</div>
<div class="text" style="margin-bottom: 30px">When you visit, use, or navigate our Services, we may process personal information depending on how you interact with us and the Services, the choices you make, and the products and features you use.</div>
<div class="title">Do we process any sensitive personal information? </div>
<div class="text" style="margin-bottom: 30px">We do not process sensitive personal information.</div>
<div class="title">Do we receive any information from third parties?</div>
<div class="text" style="margin-bottom: 30px">We do not receive any information from third parties.</div>
<div class="title">How do we process your information?</div>
<div class="text" style="margin-bottom: 30px">We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent. about how we process your information.We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent. about how we process your information.</div>
<div class="title-dark">TABLE OF CONTENTS</div>
<div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
<div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
<div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
<div class="title" style="margin-bottom: 25px">Personal information you disclose to us</div>
<div class="title">In Short:</div>
<div class="text" style="margin-bottom: 30px">We collect personal information that you provide to us.
We collect personal information that you voluntarily provide us when you register on the Services, express an interest in obtaining information about us or our products and Services when participating in activities on the Services, or otherwise when you contact us.
</div>
<div class="title">Personal Information Provided by You. </div>
<div class="text" style="margin-bottom: 30px">
The personal information we collect depends on the context of your interactions with us and the Services, your choices, and the products and features you use. The personal information we collect may include the following:
</div>
<div class="text text-dot">
names
</div>
<div class="text text-dot">
email addresses
</div>
<div class="text text-dot" style="margin-bottom: 30px">
job titles
</div>
<div class="title">Sensitive Information.</div>
<div class="text" style="margin-bottom: 25px">We do not process sensitive information.</div>
<div class="text">Social Media Login Data. We may provide you with the option to register with us using your existing social media account details, like your Facebook, Twitter, or other social media accounts. If you choose to register in this way, we will collect the information described in the section called "HOW DO WE HANDLE YOUR SOCIAL LOGINS?" below.</div>
<div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
<div class="title">In Short:</div>
<div class="text" style="margin-bottom: 30px">
We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent.
</div>
<div class="title" style="margin-bottom: 25px">We process your personal information for a variety of reasons, depending on how you interact with our Services, including:</div>
<div class="text text-dot">To facilitate account creation and authentication and otherwise manage user accounts. We may process your information so you can create and log in to your account, as well as keep your account in working order.</div>
<div class="text text-dot">To save or protect an individual's vital interest. We may process your information when necessary to save or protect an individuals vital interest, such as to prevent harm.</div>
</div>
</div>
</template>
@ -142,5 +79,62 @@
</script>
<style lang="scss" scoped>
.terms {
.terms-title {
font-size: 40px;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
margin-top: 20px;
text-align: center;
}
.terms-date {
font-size: 16px;
color: #7B61FF;
text-align: center;
}
.content {
margin-top: 75px;
padding-bottom: 50px;
.text {
color: #64748B;
font-family: 'Poppins-Regular', serif;
}
.title {
font-size: 18px;
color: #64748B;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
}
.title-dark {
font-size: 18px;
color: #1E293B;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
margin-bottom: 16px;
}
.text-link {
color: #7B61FF;
font-family: 'Poppins-Regular', serif;
}
.text-underline {
text-decoration: underline;
color: #3A4A65;
font-family: 'Poppins-Medium', serif;
margin-bottom: 20px;
}
.text-dot {
&::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: $header-backgroungd;
margin-right: 14px;
margin-top: 7px;
flex-shrink: 0;
}
}
}
}
</style>

View File

@ -1,129 +1,66 @@
<template>
<div class="text-container">
<div class="text-container terms">
<div class="top">
<div class="title">
<div class="title terms-title">
Terms Of Service
</div>
<div class="date">
<div class="date terms-date">
Last updated April 4, 2024
</div>
</div>
<div>
How do we process your information?We process your information to provide, improve, and administer our
Services, communicate with you for security and fraud prevention, and comply with the law. We may also
process your information for other purposes with your consent. We process your information only when we have
a valid legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.
How do we process your information?We process your information to provide, improve, and administer our
Services, communicate with you for security and fraud prevention, and comply with the law. We may also
process your information for other purposes with your consent. We process your information only when we have
a valid legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.How do we
process your information?We process your information to provide, improve, and administer our Services,
communicate with you for security and fraud prevention, and comply with the law. We may also process your
information for other purposes with your consent. We process your information only when we have a valid
legal reason to do so. Learn more about how we process your information.How do we process your
information?We process your information to provide, improve, and administer our Services, communicate with
you for security and fraud prevention, and comply with the law. We may also process your information for
other purposes with your consent. We process your information only when we have a valid legal reason to do
so. Learn more about how we process your information.How do we process your information?We process your
information to provide, improve, and administer our Services, communicate with you for security and fraud
prevention, and comply with the law. We may also process your information for other purposes with your
consent. We process your information only when we have a valid legal reason to do so. Learn more about how
we process your information.How do we process your information?We process your information to provide,
improve, and administer our Services, communicate with you for security and fraud prevention, and comply
with the law. We may also process your information for other purposes with your consent. We process your
information only when we have a valid legal reason to do so. Learn more about how we process your
information.How do we process your information?We process your information to provide, improve, and
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We
may also process your information for other purposes with your consent. We process your information only
when we have a valid legal reason to do so. Learn more about how we process your information.
<div class="content">
<div class="title" style="margin-bottom: 30px">This privacy notice for Futurepedia LLC (doing business as Futurepedia) ("we," "us," or "our") describes how and why we might collect, store, use, and/or share ("process") your information when you use our services ("Services"), such as when you:</div>
<div class="text text-dot">
Visit our website at <span class="text-link">http://www.futurepedia.io/</span> or any website of ours that links to this privacy notice
</div>
<div class="text text-dot" style="margin-bottom: 30px">Engage with us in other related ways, including any sales, marketing, or events</div>
<div class="title">Questions or concerns?</div>
<div class="text text-dot">Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with our policies and practices, please do not use our Services. If you still have questions or concerns, please contact us at </div>
<div class="text-link" style="margin-bottom: 40px">contact@futurepedia.io.</div>
<div class="title-dark">SUMMARY OF KEY POINTS</div>
<div class="title" style="margin-bottom: 30px">This summary provides key points from our privacy notice, but you can find out more details about any of these topics by clicking the link following each key point or by using our table of contents below to find the section you are looking for.</div>
<div class="title">What personal information do we process?</div>
<div class="text" style="margin-bottom: 30px">When you visit, use, or navigate our Services, we may process personal information depending on how you interact with us and the Services, the choices you make, and the products and features you use.</div>
<div class="title">Do we process any sensitive personal information? </div>
<div class="text" style="margin-bottom: 30px">We do not process sensitive personal information.</div>
<div class="title">Do we receive any information from third parties?</div>
<div class="text" style="margin-bottom: 30px">We do not receive any information from third parties.</div>
<div class="title">How do we process your information?</div>
<div class="text" style="margin-bottom: 30px">We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent. about how we process your information.We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent. about how we process your information.</div>
<div class="title-dark">TABLE OF CONTENTS</div>
<div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
<div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
<div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
<div class="title" style="margin-bottom: 25px">Personal information you disclose to us</div>
<div class="title">In Short:</div>
<div class="text" style="margin-bottom: 30px">We collect personal information that you provide to us.
We collect personal information that you voluntarily provide us when you register on the Services, express an interest in obtaining information about us or our products and Services when participating in activities on the Services, or otherwise when you contact us.
</div>
<div class="title">Personal Information Provided by You. </div>
<div class="text" style="margin-bottom: 30px">
The personal information we collect depends on the context of your interactions with us and the Services, your choices, and the products and features you use. The personal information we collect may include the following:
</div>
<div class="text text-dot">
names
</div>
<div class="text text-dot">
email addresses
</div>
<div class="text text-dot" style="margin-bottom: 30px">
job titles
</div>
<div class="title">Sensitive Information.</div>
<div class="text" style="margin-bottom: 25px">We do not process sensitive information.</div>
<div class="text">Social Media Login Data. We may provide you with the option to register with us using your existing social media account details, like your Facebook, Twitter, or other social media accounts. If you choose to register in this way, we will collect the information described in the section called "HOW DO WE HANDLE YOUR SOCIAL LOGINS?" below.</div>
<div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
<div class="title">In Short:</div>
<div class="text" style="margin-bottom: 30px">
We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent.
</div>
<div class="title" style="margin-bottom: 25px">We process your personal information for a variety of reasons, depending on how you interact with our Services, including:</div>
<div class="text text-dot">To facilitate account creation and authentication and otherwise manage user accounts. We may process your information so you can create and log in to your account, as well as keep your account in working order.</div>
<div class="text text-dot">To save or protect an individual's vital interest. We may process your information when necessary to save or protect an individuals vital interest, such as to prevent harm.</div>
</div>
</div>
</template>
@ -142,5 +79,62 @@
</script>
<style lang="scss" scoped>
.terms {
.terms-title {
font-size: 40px;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
margin-top: 20px;
text-align: center;
}
.terms-date {
font-size: 16px;
color: #7B61FF;
text-align: center;
}
.content {
margin-top: 75px;
padding-bottom: 50px;
.text {
color: #64748B;
font-family: 'Poppins-Regular', serif;
}
.title {
font-size: 18px;
color: #64748B;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
}
.title-dark {
font-size: 18px;
color: #1E293B;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
margin-bottom: 16px;
}
.text-link {
color: #7B61FF;
font-family: 'Poppins-Regular', serif;
}
.text-underline {
text-decoration: underline;
color: #3A4A65;
font-family: 'Poppins-Medium', serif;
margin-bottom: 20px;
}
.text-dot {
&::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: $header-backgroungd;
margin-right: 14px;
margin-top: 7px;
flex-shrink: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<div class="news-text-content flex flex-col">
<div class="tag-list flex flex-wrap">
<div v-for="(tag, i) in parsedTags" :key="i" class="tag-item">{{ tag }}</div>
</div>
<div class="card flex-1 article-content">
<div v-html="article.content || ''"></div>
</div>
</div>
</template>
<script>
export default {
props: {
article: {
type: Object,
default: () => {
return {}
}
},
},
data() {
return {
}
},
methods: {
safeJsonParse(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
return null;
}
}
},
computed: {
parsedTags() {
if (!this.article || !this.article.tags) {
return [];
}
// 如果tags已经是数组直接返回
if (Array.isArray(this.article.tags)) {
return this.article.tags;
}
// 如果tags是对象但不是数组包装成数组返回
if (typeof this.article.tags === 'object') {
return [this.article.tags];
}
// 如果是字符串尝试解析为JSON
if (typeof this.article.tags === 'string') {
// 处理空字符串情况
if (this.article.tags.trim() === '') {
return [];
}
const parsed = this.safeJsonParse(this.article.tags);
// 如果解析结果是数组,直接返回
if (Array.isArray(parsed)) {
return parsed;
}
// 如果解析结果是对象,包装成数组返回
if (parsed && typeof parsed === 'object') {
return [parsed];
}
// 其他情况返回空数组
return [];
}
return [];
}
},
}
</script>
<style scoped lang="scss">
.card {
padding: 20px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.news-text-content {
gap: 16px;
height: 100%;
.tag-list {
padding: 0 10px;
margin: 16px 0;
gap: 20px;
.tag-item {
font-family: 'Poppins-Regular', serif;
color: #1E293B;
padding: 4px 12px !important;
@include gradient-border($linear-gradient-start, $linear-gradient-end)
}
}
.article-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
margin-top: 10px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
.article-text {
color: #64748B;
font-family: 'Poppins-Regular', serif;
}
.article-graph {
padding-left: 20px;
gap: 10px;
margin-bottom: 20px;
.dot-container {
padding-top: 8px;
}
.dot {
background: $header-backgroungd;
width: 10px;
height: 10px;
border-radius: 50%;
}
.container {
flex: 1;
}
.title {
font-size: 18px;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
color: #506179;
}
.content {
font-family: 'Poppins-Regular', serif;
color: #64748B;
margin-top: 18px;
}
}
}
</style>

View File

@ -0,0 +1,340 @@
<template>
<div id="normal-container" class="daily-news-content" v-loading.fullscreen.lock="fullscreenLoading">
<IntegratedLayout>
<div class="content">
<div class="views-title">{{ newsDetail.title || '' }}</div>
<div class="views-header flex-between-center">
<div class="description">
{{ newsDetail.summary || '' }}
</div>
<div class="flex flex-col" style="gap: 16px">
<div class="flex" style="gap: 20px">
<ThumbBtn :like-count="newsDetail.likeCount || 0" :id="newsDetail.id || 0" type="article" @like-success="refreshToolDetail" />
<CommentBtn :comment-count="commentCount" />
</div>
<div class="flex items-center justify-center" style="padding: 0 7px; gap: 8px">
<img src="/ToolDetail/icon_clock1.png" alt="" style="width: 16px; height: 16px" />
<div style="font-size: 14px; color: #869EC2; font-family: 'Poppins-Regular', serif; line-height: 18px">
{{ formatDate(newsDetail.publishTime) }}
</div>
</div>
</div>
</div>
<div class="container flex justify-between">
<div class="left-content flex flex-col">
<div class="card preview-wrapper">
<img :src="newsDetail.coverImage || ''" alt="" />
</div>
<div class="flex-1">
<NewsDetail :article="newsDetail" />
</div>
<div>
<div class="comment-title">Related news</div>
<div class="flex-between-center" style="gap: 20px">
<NewsCardItem v-for="it in relatedNewsList" :key="it.id" :item="it" @refresh="goToRefreshPage" />
</div>
</div>
</div>
<div class="right-content">
<!--轮播图-->
<div class="card swiper-box">
<el-carousel :autoplay="false" height="140px">
<el-carousel-item v-for="item in 4" :key="item">
<img style="width: 100%; height: 140px" alt="" src="/" />
</el-carousel-item>
</el-carousel>
</div>
<!--网站导航-->
<div class="card pop-list">
<div style="padding: 24px 4px">
<PopularToolList />
</div>
</div>
<!--文章列表-->
<div class="card">
<div style="padding: 24px 4px">
<div class="clearfix">
<img src="/logo/hot.png" :style="{marginRight: '6px'}" alt=""/>
Latest Article
</div>
<div class="list-scroll">
<div class="list">
<ArticleCardItem v-for="it in 16" :key="it" />
</div>
</div>
</div>
</div>
</div>
</div>
<Comment comment-type="article" :id="newsDetail.id" @update:commentCount="handleCommentCountUpdate" />
</div>
</IntegratedLayout>
</div>
</template>
<script>
import ThumbBtn from "@/pages/ToolDetail/components/ThumbBtn.vue";
import CommentBtn from "@/pages/ToolDetail/components/CommentBtn.vue";
import Comment from "@/pages/ToolDetail/Comment/index.vue";
import ArticleCardItem from "@/pages/DailyNews/components/ArticleCardItem.vue";
import NewsCardItem from "@/pages/DailyNews/components/NewsCardItem.vue";
import NewsDetail from "@/pages/DailyNews/NewsDetailIndex/NewsDetail.vue";
import PopularToolList from "@/pages/Home/components/PopularToolList.vue";
export default {
components: {
ThumbBtn,
CommentBtn,
Comment,
ArticleCardItem,
NewsCardItem,
NewsDetail,
PopularToolList,
},
data() {
return {
news_slug: '',
newsDetail: {},
articleList: [],
commentCount: 0,
fullscreenLoading: false,
}
},
methods: {
// 刷新工具详情数据
async refreshToolDetail() {
if (this.news_slug) {
await this.getNewsDetail(this.news_slug);
}
},
// 重置页面
goToRefreshPage(item) {
if (item.slug) {
this.$router.push('/news-detail?news_slug=' + item.slug);
}
},
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 获取新闻详情
async getNewsDetail(slug) {
const {data: res} = await this.$api.article.getArticleDetail(slug);
const {code, data} = res;
if (code === 0) {
this.newsDetail = {...data};
}
},
// 获取相关文章列表
async getArticleListData(page = 1, limit = 4) {
const params = {page, limit, articleType: 'news'};
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
}
},
handleCommentCountUpdate(count) {
this.commentCount = count;
},
async onLoad() {
this.fullscreenLoading = true;
this.news_slug = this.$route.query.news_slug;
if (this.news_slug) {
await this.getNewsDetail(this.news_slug);
await this.getArticleListData();
}
this.fullscreenLoading = false;
}
},
mounted() {
this.onLoad();
},
watch: {
$route(to, from) {
this.news_slug = to.query.news_slug;
if (this.news_slug) {
this.$router.go(0);
}
}
},
computed: {
relatedNewsList() {
if (!this.newsDetail.id) {
return [];
}
return this.articleList.filter(e => e.id !== this.newsDetail.id).slice(0, 3);
}
},
}
</script>
<style scoped lang="scss">
.card {
padding: 16px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.daily-news-content {
flex: 1;
overflow-y: auto;
position: relative;
.content {
padding-top: 87px;
padding-bottom: 120px;
.gradient-title {
font-family: 'Poppins-SemiBold', serif;
font-size: 18px;
font-weight: 600;
}
.views-title {
font-family: 'Poppins-Bold', serif;
font-size: 40px;
font-weight: 700;
margin-top: 78px;
}
.views-header {
margin-top: 10px;
.description {
margin-right: 350px;
font-family: 'Poppins-Medium', serif;
font-size: 18px;
color: #64748B;
}
}
.container {
gap: 20px;
margin-top: 114px;
.right-content {
width: 372px;
display: flex;
flex-direction: column;
gap: 20px;
.list-scroll {
overflow-y: auto;
overflow-x: hidden;
height: 1350px;
}
.swiper-box {
height: 174px;
::v-deep .el-carousel {
.el-carousel__arrow {
opacity: 0 !important;
transition: none !important;
}
.el-carousel__indicators {
.el-carousel__indicator {
height: 4px !important;
width: 12px !important;
border-radius: 2.66px !important;
border: 0.66px solid #2563eb !important;
background: transparent !important;
margin: 0 3px 0 3px !important;
padding: 0 !important;
.el-carousel__button {
background-color: transparent;
}
&.is-active {
border: none !important;
width: 20px !important;
background: linear-gradient(0deg, #2563EB 22%, #7B61FF 73%) !important;
}
// 重置其他可能的默认样式
&:before,
&:after {
display: none !important;
}
}
}
}
}
.clearfix {
display: flex;
align-items: center;
font-size: $larg-font-size;
font-weight: bold;
font-family: 'Poppins-SemiBold', serif;
}
.pop-list {
.pop-item {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, auto);
justify-content: space-between;
gap: 32px;
.box {
display: flex;
flex-direction: column;
align-items: center;
.img-box {
width: 50px;
height: 50px;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #E2E8F0;
@include flex-center;
margin-bottom: 12px;
&:hover {
transform: scale(1.15);
cursor: pointer;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
box-shadow: 0 5px 7px 0 #00000014;
}
}
.tool-name {
font-family: 'Poppins-Medium', serif;
color: #64748B;
}
}
}
}
.article-list {
flex: 1;
}
}
.left-content {
flex: 1;
gap: 20px;
.preview-wrapper {
height: 320px;
img {
width: 100%;
height: 100%;
}
}
.comment-title {
font-size: 30px;
font-weight: 600;
margin-bottom: 30px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,166 @@
<template>
<div class="news-list-wrap flex flex-col" v-loading.fullscreen.lock="fullscreenLoading">
<div class="input">
<SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
</div>
<div class="card flex-1 flex flex-col">
<div>
<div style="padding: 30px 10px">
<div class="daily-content" v-for="(it, i) in groupedArticles" :key="i">
<div class="date-title flex items-center">
<img src="/about/icon_title_date.png" alt="Daily News" />
<div class="gradient-color">{{ it.date }}</div>
</div>
<div class="flex-col flex" style="gap: 40px">
<ArticleTextListItem v-for="item in it.list" :key="item.id" :item="item" />
</div>
<div class="diver"></div>
</div>
</div>
</div>
<div class="flex-middle-left">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</div>
</template>
<script>
import ArticleTextListItem from "@/pages/DailyNews/components/ArticleTextListItem.vue";
export default {
components: {
ArticleTextListItem,
},
data() {
return {
currentPage: 1,
totalPages: 1,
pageSize: 10,
articleList: [],
searchText: '',
total: 0,
fullscreenLoading: false,
}
},
methods: {
// 模糊搜索
async handleTextSearch() {
await this.getArticleListData(this.currentPage, this.pageSize, this.searchText);
},
calculateTotalPages() {
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
// 获取文章数据列表
async getArticleListData(page = 1, limit = 10, searchText) {
const params = {page, limit, title: searchText, articleType: 'news', sortField: 'publish_time', sortOrder: 'desc'};
if (!searchText) {
delete params.title;
}
this.fullscreenLoading = true;
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.total = data.total;
this.articleList = data.list;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
this.getArticleListData(pageNumber, 10);
},
// 添加日期格式化方法
formatDate(date) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[date.getMonth()];
const day = date.getDate().toString().padStart(2, '0');
const year = date.getFullYear();
return `${month} ${day} ${year}`;
},
},
computed: {
// 添加计算属性,将文章列表按日期分组
groupedArticles() {
if (!this.articleList || this.articleList.length === 0) {
return [];
}
// 创建一个对象来存储按日期分组的文章
const groups = {};
// 遍历文章列表,按日期分组
this.articleList.forEach(article => {
// 假设 publishTime 是时间戳或可被解析的时间字符串
const date = new Date(article.publishTime);
// 格式化日期为 YYYY-MM-DD 格式作为键
const dateKey = date.toISOString().split('T')[0];
// 如果该日期还没有分组,则初始化
if (!groups[dateKey]) {
groups[dateKey] = {
date: this.formatDate(date), // 格式化为 Sep 04 2025 这样的格式
list: []
};
}
// 将文章添加到对应日期的分组中
groups[dateKey].list.push(article);
});
// 将分组对象转换为数组并按日期降序排列
return Object.values(groups).sort((a, b) => {
// 解析日期字符串进行比较
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB - dateA;
});
}
},
mounted() {
this.getArticleListData(this.currentPage, this.pageSize);
}
}
</script>
<style scoped lang="scss">
.card {
padding: 20px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.news-list-wrap {
height: 100%;
gap: 16px;
.input {
display: flex;
margin-top: 20px;
}
.daily-content {
.date-title {
gap: 8px;
font-family: 'Poppins-SemiBold', serif;
font-size: 24px;
font-weight: 600;
margin-bottom: 40px;
img {
width: 28px;
height: 28px;
}
}
}
.diver {
width: 100%;
height: 2px;
border-top-color: #f3f8fe;
border-top-style: solid;
border-top-width: 2px;
margin: 40px 0;
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="article-box" @click="handleClick">
<div class="line"></div>
<div style="gap: 20px" class="flex">
<div class="preview-box">
<img :src="item.coverImage || ''" alt="" >
</div>
<div class="content flex flex-col justify-between">
<div class="description">{{ item.summary || '' }}</div>
<div class="flex items-center" style="gap: 6px">
<img src="/ToolDetail/icon_clock1.png" alt="" />
<div class="time">{{ item.publishTime || '' }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
props: {
item: {
type: Object,
default: () => {
return {}
}
}
},
methods: {
handleClick() {
this.$emit('refresh', this.item);
}
},
}
</script>
<style scoped lang="scss">
.article-box {
cursor: pointer;
&:active {
opacity: 0.8;
}
.preview-box {
width: 104px;
height: 104px;
border-radius: 6px;
img {
width: 100%;
height: 100%;
}
}
.content {
height: 104px;
flex: 1;
.description {
color: #1E293B;
font-family: 'Poppins-Medium', serif;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
img {
width: 16px;
height: 16px;
}
.time {
color: #869EC2;
font-family: 'Poppins-Regular', serif;
font-size: 14px;
line-height: 16px;
}
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div class="article-item-box flex" @click="goToNewsDetail">
<div class="dot-container">
<div class="dot"></div>
</div>
<div class="container">
<div class="title">
{{ item.title || '' }}
</div>
<div class="content">
{{ item.summary || '' }}
</div>
<div class="source">
Source: TechCrunch
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
props: {
item: {
type: Object,
default: () => {
return {}
}
}
},
methods: {
goToNewsDetail() {
if (this.item && this.item.slug) {
this.articleClick(this.item.id);
this.$router.push('/tools-detail?news_slug=' + this.item.slug + '&type=news')
}
},
// 记录文章点击次数
async articleClick(id) {
if (id) {
await this.$api.article.recordArticleClick(id);
}
}
}
}
</script>
<style scoped lang="scss">
.article-item-box {
gap: 10px;
&:active {
opacity: 0.8;
}
.dot-container {
padding-top: 8px;
}
.dot {
background: $header-backgroungd;
width: 10px;
height: 10px;
border-radius: 50%;
}
.container {
flex: 1;
}
.title {
font-size: 18px;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
color: #3A4A65;
}
.content {
font-family: 'Poppins-Regular', serif;
color: #64748B;
margin-top: 20px;
}
.source {
font-family: 'Poppins-Regular', serif;
color: #C8CFD7;
margin-top: 10px;
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<div class="card card-content" @click="handleClick">
<div class="preview">
<img :src="item.coverImage || ''" alt="" />
</div>
<div class="flex flex-col" style="gap: 14px">
<div class="title" style="height: 60px">{{ item.title || '' }}</div>
<div class="description" style="height: 72px">
{{ item.summary || '' }}
</div>
</div>
<div class="bottom">
<div class="flex items-center" style="gap: 12px">
<div class="circle"></div>
<div class="text">{{ item.slug || '' }}</div>
</div>
<div class="flex items-center" style="gap: 8px">
<img src="/ToolDetail/icon_thumb.png" alt="" style="width: 14px; height: 14px" />
<div class="text">{{ item.likeCount || 0 }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => ({})
},
},
data() {
return {
}
},
methods: {
handleClick() {
this.$emit('refresh', this.item);
this.$api.article.recordArticleClick(this.id);
}
},
}
</script>
<style scoped lang="scss">
.card {
padding: 20px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.card-content {
width: 256px;
gap: 20px;
display: flex;
flex-direction: column;
cursor: pointer;
&:active {
opacity: 0.8;
}
.preview {
height: 100px;
border-radius: 6px;
img {
width: 100%;
height: 100%;
}
}
.bottom {
display: flex;
justify-content: space-between;
align-items: center;
.circle {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #64748B;
}
.text {
font-size: 14px;
color: #C8CFD7;
font-family: 'Poppins-Regular', serif;
line-height: 18px;
}
}
.title {
font-size: 20px;
color: #3A4A65;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.description {
color: #64748B;
font-family: 'Poppins-Regular', serif;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>

291
pages/DailyNews/index.vue Normal file
View File

@ -0,0 +1,291 @@
<template>
<div id="normal-container" class="daily-news-content" v-loading.fullscreen.lock="fullscreenLoading">
<IntegratedLayout>
<div class="content">
<div class="gradient-color gradient-title">AI Daily News</div>
<div class="views-title">AI Daily News</div>
<div class="views-header flex-between-center">
<div class="description">Keep up-to-date with the latest AI industry developments. This section provides daily news coverage, focusing on global breakthroughs, frontier research, market movements, and emerging trends across AI sectors.</div>
</div>
<div class="container flex justify-between">
<div class="left-content flex flex-col">
<div class="card preview-wrapper">
<img :src="newConfig.imageUrl || ''" alt="" />
</div>
<div class="flex-1">
<NewsList />
</div>
</div>
<div class="right-content">
<!--轮播图-->
<div class="card swiper-box">
<el-carousel :autoplay="false" height="140px">
<el-carousel-item v-for="(item, i) in banner" :key="i">
<img :src="item.imageUrl || ''" alt="" style="height: 140px; width: 100%;" />
</el-carousel-item>
</el-carousel>
</div>
<!--网站导航-->
<div class="card pop-list">
<div style="padding: 24px 4px">
<PopularToolList />
</div>
</div>
<!--文章列表-->
<div class="card">
<div style="padding: 24px 4px">
<div class="clearfix">
<img src="/logo/hot.png" :style="{marginRight: '6px'}" alt=""/>
Latest Article
</div>
<div class="list-scroll">
<div class="list">
<ArticleCardItem v-for="it in latestNewsList" :key="it.id" :item="it" @refresh="goToRelatedNewsDetail" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</IntegratedLayout>
</div>
</template>
<script>
import ArticleCardItem from "@/pages/DailyNews/components/ArticleCardItem.vue";
import NewsList from "@/pages/DailyNews/NewsList.vue";
import PopularToolList from "@/pages/Home/components/PopularToolList.vue";
export default {
components: {
NewsList,
ArticleCardItem,
PopularToolList,
},
data() {
return {
latestNewsList: [],
fullscreenLoading: false,
newConfig: {},
}
},
computed: {
banner() {
const bannerConfig = this.$store.getters.bannerConfig;
if (bannerConfig.news && bannerConfig.news.length > 0) {
return bannerConfig.news;
}
return [];
},
},
methods: {
// 获取最新文章列表
async getLatestArticleListData() {
const params = {
page: 1,
limit: 20,
excludeTypes: ['launches'],
sortField: 'publish_time',
sortOrder: 'desc',
};
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.latestNewsList = data.list;
}
},
// 侧边新闻重新跳转路由
goToRelatedNewsDetail(item) {
if (item.slug && item.articleType) {
this.$router.push('/tools-detail?news_slug=' + item.slug + '&type=' + item.articleType)
}
},
// 获取当前模块信息
async getModuleConfig() {
const {data: res} = await this.$api.config.getModuleConfigKey('module');
const {data, code} = res;
if (code === 0 && data.module && data.module.news) {
this.newConfig = data.module.news.length > 0 ? data.module.news[0] : {};
}
},
async onLoad() {
this.fullscreenLoading = true;
await this.$store.dispatch('getBannerConfig');
await this.getModuleConfig();
await this.getLatestArticleListData();
this.fullscreenLoading = false;
}
},
mounted() {
this.onLoad();
}
}
</script>
<style scoped lang="scss">
.card {
padding: 16px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.daily-news-content {
flex: 1;
overflow-y: auto;
position: relative;
.content {
padding-top: 87px;
padding-bottom: 120px;
.gradient-title {
font-family: 'Poppins-SemiBold', serif;
font-size: 18px;
font-weight: 600;
}
.views-title {
font-family: 'Poppins-Bold', serif;
font-size: 40px;
font-weight: 700;
margin-top: 78px;
}
.views-header {
margin-top: 10px;
.description {
margin-right: 350px;
font-family: 'Poppins-Medium', serif;
font-size: 18px;
color: #64748B;
}
}
.container {
gap: 20px;
margin-top: 114px;
.right-content {
width: 372px;
display: flex;
flex-direction: column;
gap: 20px;
.list-scroll {
overflow-y: auto;
overflow-x: hidden;
height: 1350px;
}
.swiper-box {
height: 174px;
::v-deep .el-carousel {
.el-carousel__arrow {
opacity: 0 !important;
transition: none !important;
}
.el-carousel__indicators {
.el-carousel__indicator {
height: 4px !important;
width: 12px !important;
border-radius: 2.66px !important;
border: 0.66px solid #2563eb !important;
background: transparent !important;
margin: 0 3px 0 3px !important;
padding: 0 !important;
.el-carousel__button {
background-color: transparent;
}
&.is-active {
border: none !important;
width: 20px !important;
background: linear-gradient(0deg, #2563EB 22%, #7B61FF 73%) !important;
}
// 重置其他可能的默认样式
&:before,
&:after {
display: none !important;
}
}
}
}
}
.clearfix {
display: flex;
align-items: center;
font-size: $larg-font-size;
font-weight: bold;
font-family: 'Poppins-SemiBold', serif;
}
.pop-list {
.pop-item {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, auto);
justify-content: space-between;
gap: 32px;
.box {
display: flex;
flex-direction: column;
align-items: center;
.img-box {
width: 50px;
height: 50px;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #E2E8F0;
@include flex-center;
margin-bottom: 12px;
&:hover {
transform: scale(1.15);
cursor: pointer;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
box-shadow: 0 5px 7px 0 #00000014;
}
}
.tool-name {
font-family: 'Poppins-Medium', serif;
color: #64748B;
}
}
}
}
.article-list {
flex: 1;
}
}
.left-content {
flex: 1;
gap: 20px;
.preview-wrapper {
height: 320px;
img {
width: 100%;
height: 100%;
border-radius: 12px;
}
}
.comment-title {
font-size: 30px;
font-weight: 600;
margin-bottom: 30px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
}
}
}
}
</style>

View File

@ -1,153 +0,0 @@
<template>
<div :id="id">
<div class="bar-list">
<div class="top-box">
<div>
<img :src="`/logo/${tool.img}_check.png`" />
<span class="title-text .gradient-color">
{{tool.name}}
</span>
</div>
<div class="more pointer" v-if="!tagList.length">
View more<i class="el-icon-arrow-right"></i>
</div>
</div>
<div v-if="tagList.length">
<div class="tags" ref="tagsContainer" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag"
@mouseleave="stopDrag">
<div class="tag-item" v-for="(item,index) in 10">
AI Image & illustration Generation
</div>
</div>
<div class="line"></div>
</div>
<div>
<div class="more pointer" v-if="tagList.length">
View more<i class="el-icon-arrow-right"></i>
</div>
<div class="item-card">
<div class="card-caontainer" v-for="(item,index) in 8">
<div class="title">
<img src="/logo/collect.png" />
<span>
Skywork
</span>
</div>
<div class="content">
Latest ToolsLatest ToolsLatest ToolsLatest Tools
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['tool','id'],
data() {
return {
isDragging: false,
startX: 0,
scrollLeft: 0,
tagList: [],
}
},
methods: {
startDrag(e) {
this.isDragging = true;
this.startX = e.pageX - this.$refs.tagsContainer.offsetLeft;
this.scrollLeft = this.$refs.tagsContainer.scrollLeft;
},
onDrag(e) {
if (!this.isDragging) return;
e.preventDefault();
const x = e.pageX - this.$refs.tagsContainer.offsetLeft;
const walk = (x - this.startX) * 2; // 调整滑动速度
this.$refs.tagsContainer.scrollLeft = this.scrollLeft - walk;
},
stopDrag() {
this.isDragging = false;
}
}
}
</script>
<style lang="scss" scoped>
.bar-list {
margin: 50px 0;
}
.top-box {
@include display-flex;
justify-content: space-between;
.title-text {
font-weight: 600;
font-size: $larg-font-size;
}
}
.tags {
display: flex;
overflow-x: auto;
gap: 12px;
scrollbar-width: none;
-ms-overflow-style: none;
// cursor: grab;
user-select: none;
}
.tag-item {
flex-shrink: 0;
padding: 10px;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
// cursor: grab;
/* 显示抓取手势 */
&:active {
// cursor: grabbing;
/* 抓取中状态 */
}
}
.more {
text-align: right;
color: $grey-color;
font-size: $mid-font-size;
}
.item-card {
display: grid;
gap: 20px;
grid-template-columns: repeat(4, 1fr);
margin-top: 30px;
}
.card-caontainer {
background: #FFFFFF;
box-shadow: 0px 10px 30px 0px rgba(0, 0, 0, 0.05);
border-radius: 8px;
padding: 16px;
border: 1px solid #E2E8F0;
.title {
display: flex;
align-items: center;
span {
color: $main-font-color;
font-size: $big-font-size;
font-weight: 600;
}
}
.content {
color: $grey-color;
// line-height: 30px;
}
}
</style>

View File

@ -0,0 +1,110 @@
<script>
export default {
data() {
return {
pop_tools: [],
}
},
methods: {
async getToolsAsyncData() {
const {data: res} = await this.$api.tool.getToolsList({isHot: 1, page: 1, limit: 6});
const {code, data} = res;
if (code === 0 && data.list) {
this.pop_tools = data.list;
}
},
// 跳转工具详情页
goToToolDetail(item) {
if (item.slug && item.categorySlug) {
this.recordToolClick(item);
this.$router.push(`/detail?tool_slug=${item.slug}&category_slug=${item.categorySlug}`);
}
},
// 记录工具点击次数
async recordToolClick(item) {
if (item.id) {
await this.$api.tool.recordToolClickNum(item.id);
}
}
},
created() {
this.getToolsAsyncData();
}
}
</script>
<template>
<div>
<div class="clearfix">
<img src="/logo/hot.png" :style="{marginRight: '6px'}" alt=""/>
Popular Tools
</div>
<div class="line" />
<div class="pop-item">
<div v-for="item in pop_tools" class="box" @click="goToToolDetail(item)">
<div class="img-box">
<img :src="item.iconUrl || ''" alt=""/>
</div>
<div class="tool-name">
{{ item.name }}
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.clearfix {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $larg-font-size;
font-weight: bold;
font-family: 'Poppins-SemiBold', serif;
}
.img-box {
width: 50px;
height: 50px;
margin: 15px 0 12px;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #E2E8F0;
@include flex-center;
&:hover {
transform: scale(1.15);
cursor: pointer;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
box-shadow: 0 5px 7px 0 #00000014;
}
}
.pop-item {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, 1fr);
justify-content: space-between;
gap: 12px;
.box {
display: flex;
flex-direction: column;
align-items: center;
&:active {
opacity: 0.8;
}
.tool-name {
font-family: 'Poppins-Medium', serif;
color: #64748B;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100px;
}
}
}
</style>

View File

@ -0,0 +1,40 @@
<script>
import BScroll from '@better-scroll/core';
export default {
mounted() {
this.$nextTick(() => {
this.bs = new BScroll(this.$refs.wrapper, {
scrollX: true,
scrollY: false,
click: true,
bounce: false
})
})
},
beforeDestroy() {
this.bs && this.bs.destroy()
}
}
</script>
<template>
<div ref="wrapper" class="btn-wrapper">
<div class="btn-content">
<slot></slot>
</div>
</div>
</template>
<style scoped>
.btn-wrapper {
overflow: hidden;
position: relative;
width: 100%
}
.btn-content {
white-space: nowrap;
display: inline-block;
width: max-content;
}
</style>

View File

@ -0,0 +1,113 @@
<script>
export default {
props: {
config: {
type: Object,
default: () => ({})
},
categorySlug: {
type: String,
default: '',
}
},
data() {
return {}
},
methods: {
goToToolDetail() {
if (this.config.slug && this.categorySlug) {
this.recordToolClick(this.config);
this.$router.push(`/detail?tool_slug=${this.config.slug}&category_slug=${this.categorySlug}`);
}
},
// 记录点击次数
async recordToolClick(item) {
if (item.id) {
await this.$api.tool.recordToolClickNum(item.id);
}
},
/**
* 检测文本是否包含有效的HTML标签
* @param {string} text - 要检测的文本
* @returns {boolean} - 是否包含有效的HTML标签
*/
containsHtml(text) {
if (!text || typeof text !== 'string') {
return false;
}
// 简单检测常见HTML标签格式实际项目中可能需要更复杂的验证
const htmlRegex = /<[^>]*>/;
return htmlRegex.test(text);
}
}
}
</script>
<template>
<div class="card-caontainer" @click="goToToolDetail">
<div class="title">
<img :src="config.iconUrl || '/'" alt="" />
<span style="font-size: 18px">
{{ config.name || '' }}
</span>
</div>
<!--<div class="text"-->
<!-- v-if="config.memo && containsHtml(config.memo + '</p>')"-->
<!-- v-html="config.memo"></div>-->
<div class="text">
{{ config.memo ? config.memo : '' }}
</div>
</div>
</template>
<style scoped lang="scss">
.card-caontainer {
width: 100%;
height: 100%;
background: #FFFFFF;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.05);
border-radius: 8px;
padding: 16px;
border: 1px solid #E2E8F0;
&:hover {
cursor: pointer;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
&:active {
opacity: 0.8;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
.title {
display: flex;
align-items: center;
gap: 10px;
img {
width: 40px;
height: 40px;
}
span {
color: $main-font-color;
font-size: $big-font-size;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
}
}
.text {
color: $grey-color;
font-family: 'Poppins-Regular', serif;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
</style>

View File

@ -3,12 +3,10 @@
<div v-for="item in list" class="tools" @click="checkTool(item)">
<span class="tool-card" :class="item.active?'checkedBg':''">
<span class="content">
<img :src="`/logo/${item.img}_${item.active ? 'checkd' : 'un'}.png`" />
<img :src="''" alt="" />
<span>{{ item.name }}</span>
</span>
</span>
</div>
</div>
</template>
@ -28,9 +26,14 @@
},
methods: {
checkTool(item) {
this.list.forEach(i => this.$set(i, 'active', false)) //
this.$set(item, 'active', true)
this.$emit('tool-selected', item.name) //
if (item.active) {
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);
}
}
}
}
@ -48,16 +51,20 @@
.tool-card {
background: #FFFFFF;
box-shadow: 0px 10px 30px 0px rgba(0, 0, 0, 0.05);
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;
.content {
@include display-flex;
img {
margin-right: 5px;
width: 24px;
height: 24px;
}
}

View File

@ -0,0 +1,142 @@
<template>
<div :id="id">
<div class="bar-list">
<div class="top-box">
<div class="title-wrap">
<img :src="`/logo/${tool.img}_check.png`" alt="" />
<span class="title-text gradient-color">
{{tool.categoryName}}
</span>
</div>
<div @click="goToViewMore" class="more pointer" v-if="!tool.tagList">
View more<i class="el-icon-arrow-right"></i>
</div>
</div>
<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>
</div>
</ScrollList>
<div class="line"></div>
</div>
<div>
<div @click="goToViewMore" class="more pointer" v-if="tool.tagList && tool.tagList.length">
View more<i class="el-icon-arrow-right"></i>
</div>
<div class="item-card" v-if="tool.tools && tool.tools.length">
<div v-for="(item, index) in tool.tools" :key="index" style="min-height: 110px">
<ToolItemCard :config="item" :categorySlug="categorySlug" />
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ScrollList from "~/pages/Home/components/ScrollList.vue";
import ToolItemCard from "~/pages/Home/components/ToolItemCard.vue";
export default {
components: {ToolItemCard, ScrollList},
props: {
tool: {
type: Object,
default: () => {
return {}
}
},
id: {
type: String,
default: '',
},
categorySlug: {
type: String,
default: '',
}
},
data() {
return {
}
},
methods: {
// 查看更多
goToViewMore() {
if (this.categorySlug) {
this.$router.push('/home/more?category_slug=' + this.categorySlug)
}
}
}
}
</script>
<style lang="scss" scoped>
.title-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.bar-list {
margin: 50px 0;
}
.top-box {
@include display-flex;
justify-content: space-between;
.title-text {
font-weight: 600;
font-size: $larg-font-size;
font-family: 'Poppins-SemiBold', serif;
}
}
.tags {
margin-top: 27px;
display: flex;
overflow-x: auto;
gap: 12px;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
width: max-content;
}
.tag-item {
flex-shrink: 0;
padding: 10px;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
/* 显示抓取手势 */
font-family: 'Poppins-SemiBold', sans-serif;
color: #64748B;
font-weight: 600;
&:active {
// cursor: grabbing;
/* 抓取中状态 */
}
}
.more {
display: block;
text-align: right;
color: $grey-color;
font-size: $mid-font-size;
font-family: 'Poppins-Regular', serif;
&:active {
opacity: 0.8;
}
}
.item-card {
display: grid;
gap: 20px;
grid-template-columns: repeat(4, 1fr);
margin-top: 30px;
}
</style>

View File

@ -9,297 +9,167 @@
Get global AI tools in one stop
</div>
<div class="third-text">
It includes over a <span class="special">thousand global AI tools</span>, covering writing, images,
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
</div>
<div class="input-container">
<input type="text" placeholder="Please enter the key words">
<i class="el-icon-search gradient-color search-icon pointer"></i>
<!-- 修改输入框容器 -->
<div style="margin: 68px auto 62px">
<SearchSelectInput />
</div>
</div>
<div class="card flex">
<div class="left-card card-box">
<el-carousel indicator-position="outside">
<el-carousel-item v-for="item in 4" :key="item">
<el-carousel :autoplay="false" height="354px" 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" />
</el-carousel-item>
</el-carousel>
</div>
<div class="right-card card-box">
<div class="clearfix">
<img src="/logo/hot.png" />
Popular Tools
</div>
<div class="line">
</div>
<div class="pop-item">
<div v-for="item in pop_tools" class="box">
<div class="img-box">
<img src="/logo/bottom-logo.png" />
</div>
<div>
{{ item.name }}
</div>
</div>
</div>
<PopularToolList />
</div>
</div>
<div class="tool-list">
<ToolList :list="list" @tool-selected="scrollToTool"></ToolList>
</div>
<div class="line">
</div>
<div class="toolbar" v-for="tool in list">
<Toolbar :tool="tool" :id="`tool-${tool.name}`"></Toolbar>
</div>
<NuxtChild />
</IntegratedLayout>
<!-- <el-backtop target=".scroll-container" :bottom="100">
<div>
<img src="/logo/back-top.png" />
</div>
</el-backtop> -->
</div>
</template>
<script>
import ToolList from './ToolList.vue'
import Toolbar from './Toolbar.vue'
export default {
name: 'Home',
components: {
ToolList,
Toolbar
},
data() {
return {
pop_tools: [{
name: 'Gemini',
src: ''
},
{
name: 'Gemini',
src: ''
},
{
name: 'Gemini',
src: ''
},
{
name: 'Gemini',
src: ''
},
{
name: 'Gemini',
src: ''
},
{
name: 'Gemini',
src: ''
},
],
list: [{
name: 'AI Image Tool',
img: 'image'
},
{
name: 'AI Office Tools',
img: 'office'
},
{
name: 'AI Video Tool',
img: 'Video'
},
{
name: 'AI Programming Tools',
img: 'program'
},
{
name: 'AI Chat Assistant',
img: 'chat'
},
{
name: 'AI Writing Tool',
img: 'write'
},
{
name: 'AI learning Website',
img: 'learn'
},
{
name: 'AI Design Tool',
img: 'design'
},
{
name: 'AI Search Engine',
img: 'search'
},
{
name: 'AI Development Platform',
img: 'develop'
},
{
name: 'AI Audio Tool',
img: 'audio'
},
{
name: 'AI Model Evaluation',
img: 'model'
},
{
name: 'AI Prompt Command',
img: 'prompt'
},
{
name: 'AI Content Detection',
img: 'content'
},
{
name: 'AI Agent',
img: 'agent'
},
{
name: 'Model Training',
img: 'train'
}
]
import PopularToolList from "@/pages/Home/components/PopularToolList.vue";
import GlobalLoading from "@/components/GlobalLoading.vue";
export default {
name: 'Home',
components: {
PopularToolList,
GlobalLoading,
},
computed: {
banner() {
const bannerConfig = this.$store.getters.bannerConfig;
if (bannerConfig.home && bannerConfig.home.length > 0) {
return bannerConfig.home;
}
},
methods: {
scrollToTool(toolName) {
const target = document.getElementById(`tool-${toolName}`)
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
return [];
}
},
data() {
return {}
},
methods: {
},
mounted() {
this.$store.dispatch('getBannerConfig');
}
}
</script>
<style lang="scss" scoped> #home-page {
flex: 1;
overflow-y: auto; // 必须启用滚动
position: relative; // 添加相对定位
background-repeat: no-repeat;
background-size: contain; // 控制图片自适应
background-position: top center;
background-image: url('/logo/mask.png');
}
.top-title {
@include flex-center;
flex-direction: column;
font-weight: bold;
margin-top: 120px;
div {
text-align: center;
}
.first-text {
line-height: 90px;
font-size: $huge-font-size3;
font-weight: 900;
font-family: 'Poppins-Bold', serif;
}
.second-text {
margin: 18px 0;
font-family: 'Poppins-Bold', serif;
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;
.special {
color: $main-color;
}
}
}
.card {
display: grid;
grid-template-columns: 2fr 1fr; // 4列布局
gap: 11px; // 网格间距
margin: 0 auto;
.card-box {
background: $white;
box-shadow: 0 18px 33px 0 rgba(0, 0, 0, 0.05);
border-radius: 12px;
border: 1px solid #FFFFFF;
}
.left-card {
// min-width: 805px;
overflow: hidden;
box-sizing: border-box;
::v-deep .el-carousel {
.el-carousel__arrow {
opacity: 0 !important;
transition: none !important;
}
.el-carousel__indicators {
.el-carousel__indicator {
height: 4px !important;
width: 12px !important;
border-radius: 2.66px !important;
border: 0.66px solid #2563eb !important;
background: transparent !important;
margin: 0 3px 0 3px !important;
padding: 0 !important;
.el-carousel__button {
background-color: transparent;
}
&.is-active {
border: none !important;
width: 20px !important;
background: linear-gradient(0deg, #2563EB 22%, #7B61FF 73%) !important;
}
// 重置其他可能的默认样式
&:before,
&:after {
display: none !important;
}
}
}
}
}
</script>
<style lang="scss" scoped>
#home-page {
flex: 1;
overflow-y: auto; // 必须启用滚动
position: relative; // 添加相对定位
background-repeat: no-repeat;
background-size: contain; // 控制图片自适应
background-position: top center;
background-image: url('/logo/mask.png');
}
.top-title {
@include flex-center;
flex-direction: column;
font-weight: bold;
height: 400px;
margin-top: 180px;
div {
text-align: center;
}
.first-text {
line-height: 90px;
font-size: $huge-font-size2;
}
.second-text {
margin: 18px 0;
font-size: $huge-font-size3;
}
.third-text {
width: 716px;
height: 81px;
font-weight: 500;
color: $grey-color;
font-size: $normal-font-size;
.special {
color: $main-color;
}
}
}
.card {
display: grid;
grid-template-columns: 2fr 1fr; // 4列布局
gap: 20px; // 网格间距
margin: 0 auto;
height: 366px;
.card-box {
background: $white;
box-shadow: 0px 18px 33px 0px rgba(0, 0, 0, 0.05);
border-radius: 12px;
border: 1px solid #FFFFFF;
}
.left-card {
// min-width: 805px;
}
.right-card {
padding: 20px;
// min-width: 372px;
}
.clearfix {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $larg-font-size;
}
.img-box {
width: 50px;
height: 50px;
margin: 15px 0;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #E2E8F0;
@include flex-center;
&:hover {
transform: scale(1.15);
cursor: pointer;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
}
.pop-item {
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 10px; // 新增间距控制
.box {
display: flex;
flex-direction: column;
align-items: center;
}
}
}
.tool-list {
margin: 40px 0;
}
.input-container {
margin: 60px auto;
.right-card {
padding: 20px;
max-width: 372px;
}
}
</style>

70
pages/Home/views/List.vue Normal file
View File

@ -0,0 +1,70 @@
<template>
<div v-loading.fullscreen.lock="fullscreenLoading">
<div class="tool-list">
<ToolList :list="categoryList" @tool-selected="scrollToTool"></ToolList>
</div>
<div class="line">
</div>
<div class="toolbar" v-for="tool in toolsGroup">
<Toolbar :tool="tool" :id="`tool-${tool.categoryName}`" :category-slug="tool.categorySlug || ''"></Toolbar>
</div>
</div>
</template>
<script>
import Toolbar from "../components/Toolbar.vue";
import ToolList from "../components/ToolList.vue";
export default {
components: {ToolList, Toolbar},
data() {
return {
fullscreenLoading: false,
categoryList: [],
toolsGroup: [],
}
},
methods: {
scrollToTool(toolName) {
const target = document.getElementById(`tool-${toolName}`)
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
},
// 获取分类列表
async getCategoryAsyncData() {
const {data: res} = await this.$api.tool.getCategoryList();
const {code, data} = res;
if (code === 0 && data.list) {
this.categoryList = data.list;
}
},
// 获取分类分组的工具列表
async getToolsGroupAsyncData() {
const {data: res} = await this.$api.tool.getToolByCategory({limit: 8});
const {code, data} = res;
if (code === 0 && data.list) {
this.toolsGroup = data.list;
}
},
async onLoad() {
this.fullscreenLoading = true;
await this.getCategoryAsyncData();
await this.getToolsGroupAsyncData();
this.fullscreenLoading = false;
}
},
created() {
this.onLoad();
}
}
</script>
<style scoped lang="scss">
.tool-list {
margin: 60px 0;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="content" v-loading.fullscreen.lock="fullscreenLoading">
<div class="tag-item" v-if="tagName">
{{tagName}}
</div>
<div class="item-card">
<ToolItemCard v-for="(item, index) in toolList" :key="index" :config="item" :category-slug="category_slug" />
</div>
</div>
</template>
<script>
import ToolItemCard from "~/pages/Home/components/ToolItemCard.vue";
export default {
components: {
ToolItemCard,
},
data() {
return {
tagName: '',
toolList: [],
category_slug: '',
fullscreenLoading: false,
}
},
methods: {
// 按分类获取工具列表
async getToolsByTag(slug) {
if (slug) {
this.fullscreenLoading = true;
const params = {categorySlug: slug};
const {data: res} = await this.$api.tool.getToolsList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.toolList = data.list;
}
this.fullscreenLoading = false;
}
}
},
watch: {
// 监听路由变化
'$route'(to) {
const slug = to.query.category_slug;
if (slug) {
this.category_slug = slug;
this.getToolsByTag(slug);
}
}
},
mounted() {
// 组件挂载时也检查一次路由参数
const slug = this.$route.query.category_slug;
if (slug) {
this.category_slug = slug;
this.getToolsByTag(slug);
}
},
}
</script>
<style scoped lang="scss">
.content {
padding-top: 60px;
padding-bottom: 100px;
.tag-item {
display: inline-block;
padding: 10px;
border-radius: 12px;
font-family: 'Poppins-SemiBold', sans-serif;
color: #fff;
font-weight: 600;
background: $header-backgroungd;
}
.item-card {
display: grid;
gap: 20px;
grid-template-columns: repeat(4, 1fr);
margin-top: 30px;
.card-caontainer {
background: #FFFFFF;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.05);
border-radius: 8px;
padding: 16px;
border: 1px solid #E2E8F0;
.title {
display: flex;
align-items: center;
span {
color: $main-font-color;
font-size: $big-font-size;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
}
}
.text {
color: $grey-color;
font-family: 'Poppins-Regular', serif;
// line-height: 30px;
}
}
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div @click="goToFinanceDetail">
<div class="finance-item flex">
<div class="dot-container">
<div class="dot"></div>
</div>
<div>
<div class="title">2025-2-01</div>
<div class="content-text">Koah参与$ 500万美元种子轮融资领投Forerunner 参与投资South Park Commons </div>
</div>
</div>
<div class="diver-line"></div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => {
return {}
}
},
},
data() {
return {
}
},
methods: {
goToFinanceDetail() {
this.$router.push({
path: '/finance-detail',
})
}
}
}
</script>
<style scoped lang="scss">
.diver-line {
border-top: 2px solid #E2E8F0;
}
.finance-item {
padding-top: 35px;
padding-bottom: 35px;
cursor: pointer;
&:active {
opacity: 0.8;
}
.dot-container {
padding-right: 20px;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #7B61FF;
margin-top: 8px;
}
}
.title {
color: #aebadee6;
font-family: 'Poppins-Medium', serif;
}
.content-text {
color: #64748B;
font-family: 'Poppins-Medium', serif;
font-size: 20px;
}
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<div class="item flex items-center">
<div class="icon flex items-center justify-center">
<img :src="item.iconUrl || ''" alt="" />
</div>
<div class="text">{{ item.name || '' }}</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => {
return {}
}
},
},
data() {
return {
}
},
methods: {}
}
</script>
<style scoped lang="scss">
.item {
border-radius: 12px;
padding: 14px 10px;
gap: 16px;
margin-bottom: 6px;
color: #64748B;
font-size: 18px;
font-family: 'Poppins-Medium', serif;
.icon {
width: 24px;
height: 24px;
border-radius: 4px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.08);
img {
width: 14px;
height: 14px;
}
}
&:hover {
background-color: #f5f5f5;
color: #7B61FF;
}
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<div class="item">
<div class="flex items-center">
<img class="icon" :src="item.iconUrl || ''" alt="" />
<div class="flex-1 flex flex-col justify-between">
<div class="title">{{ item.title || '' }}</div>
<div class="flex items-center" style="gap: 30px">
<div class="flex items-center data" style="gap: 12px">
<img alt="" src="/launches/detail/icon_thumb.png" style="width: 24px; height: 24px;" />
<div>{{ item.likeCount || 0 }}</div>
</div>
<div class="flex items-center data" style="gap: 12px">
<img alt="" src="/launches/detail/icon_star.png" style="width: 24px; height: 24px;" />
<div>{{ item.rating || 0 }}</div>
</div>
</div>
</div>
</div>
<div class="summary">{{ item.summary || '' }}</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => ({})
}
},
data() {
return {
}
},
methods: {},
}
</script>
<style scoped lang="scss">
.item {
.icon {
width: 70px;
height: 70px;
border-radius: 6px;
margin-right: 20px;
}
.title {
font-family: 'Poppins-SemiBold', serif;
font-size: 24px;
color: #1E293B;
font-weight: 600;
}
.data {
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
.summary {
font-family: 'Poppins-Regular', serif;
color: #64748B;
margin-top: 12px;
}
}
</style>

View File

@ -0,0 +1,382 @@
<template>
<div id="normal-container">
<IntegratedLayout>
<div class="content">
<div class="title-text">{{ news_detail.title || '' }}</div>
<div class="rate-box">
<Rate v-model="news_detail.rating" readonly />
<div class="flex" style="gap: 20px">
<CommentBtn :comment-count="commentCount" />
</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/icon_note.png" alt="">
<span>Introduction: </span>
</div>
<div class="item-content">
{{ news_detail.summary || '' }}
</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/icon_clock.png" alt="">
<span>Data update: </span>
</div>
<div class="item-content">{{ news_detail.publishTime || '' }}</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/like_icon.png" alt="">
<span>Like: </span>
</div>
<div class="item-content">{{ news_detail.likeCount || 0 }}</div>
</div>
<div class="tags">
<div class="tag-item" v-for="(it, index) in tagList" :key="index">{{ it }}</div>
</div>
<div class="diver"></div>
<div class="container flex">
<div class="left-content flex-1 flex flex-col">
<!--<div class="sketch flex justify-between">-->
<!-- <div class="flex items-center" style="gap: 8px">-->
<!-- <img style="width: 18px; height: 18px" src="/launches/detail/icon_fly.png" alt="" />-->
<!-- <div class="text">This is the <span style="color: #7B61FF">6</span> release of Sketch</div>-->
<!-- </div>-->
<!-- <div class="flex items-center more">-->
<!-- <div>View more</div>-->
<!-- <img src="/launches/detail/icon_arrow.png" alt="" />-->
<!-- </div>-->
<!--</div>-->
<div class="card flex-1">
<div class="article-content">
<div v-html="news_detail.content || ''"></div>
</div>
</div>
</div>
<div class="right-content">
<div class="card">
<div class="flex items-center" style="margin-top: 20px">
<img alt="" src="/launches/detail/icon_hourse.png" class="icon" />
<div class="content-title">Company Information</div>
</div>
<div class="diver-line"></div>
<ProjectItem :item="{name: 'Sketch.com', iconUrl: '/launches/web/sketch.png'}" />
<ProjectItem :item="{name: 'Github.com', iconUrl: '/launches/web/github.png'}" />
<ProjectItem :item="{name: 'Facebook.com', iconUrl: '/launches/web/facebook.png'}" />
<ProjectItem :item="{name: 'Instagram.com', iconUrl: '/launches/web/instagram.png'}" />
<ProjectItem :item="{name: 'Twitter.com', iconUrl: '/launches/web/twitter.png'}" />
<!--<div class="more flex items-center justify-between" style="margin-top: 20px">-->
<!-- <div>A total of 8 projects were released</div>-->
<!-- <img src="/launches/detail/icon_arrow.png" alt="" />-->
<!--</div>-->
</div>
<div class="card" style="margin-top: 20px">
<div class="flex items-center" style="margin-top: 20px">
<img alt="" src="/launches/detail/icon_finance.png" class="icon" />
<div class="content-title">Special Financing</div>
</div>
<div class="diver-line"></div>
<FinanceItem />
<FinanceItem />
<FinanceItem />
<!--<div class="flex justify-end" style="margin-top: 30px">-->
<!-- <div class="more flex items-center justify-between" style="width: 50%">-->
<!-- <div>View more</div>-->
<!-- <img src="/launches/detail/icon_arrow.png" alt="" />-->
<!-- </div>-->
<!--</div>-->
</div>
</div>
</div>
<div class="related">
<div class="related-title flex justify-between items-center">
<div class="title">Related Products</div>
<div @click="goToViewMore" class="more pointer">
View more<i class="el-icon-arrow-right"></i>
</div>
</div>
<div class="card">
<div class="flex-between-center list">
<RelatedTool :item="{
title: 'Figma',
summary: 'Easily create highly interactive prototypes',
likeCount: 123,
rating: 4.5
}" />
<RelatedTool :item="{
title: 'Figma',
summary: 'Easily create highly interactive prototypes',
likeCount: 123,
rating: 4.5
}" />
<RelatedTool :item="{
title: 'Figma',
summary: 'Easily create highly interactive prototypes',
likeCount: 123,
rating: 4.5
}" />
</div>
</div>
</div>
<Comment comment-type="article" :id="news_detail.id" @update:commentCount="handleCommentCountUpdate" />
</div>
</IntegratedLayout>
</div>
</template>
<script>
import CommentBtn from "@/pages/ToolDetail/components/CommentBtn.vue";
import Comment from "@/pages/ToolDetail/Comment/index.vue";
import RelatedTool from "@/pages/Launches/Detail/RelatedTool.vue";
import ProjectItem from "@/pages/Launches/Detail/ProjectItem.vue";
import FinanceItem from "@/pages/Launches/Detail/FinanceItem.vue";
export default {
components: {FinanceItem, ProjectItem, RelatedTool, CommentBtn, Comment},
data() {
return {
news_detail: {},
commentCount: 0,
news_slug: '',
}
},
methods: {
// 获取新闻详情
async getNewsDetail(newsSlug) {
const {data: res} = await this.$api.article.getArticleDetail(newsSlug);
const {code, data} = res;
if (code === 0 && data) {
this.news_detail = {...data};
}
},
stringJsonToObject(str) {
// 将json字符串转为对象捕获错误当str为空或者转对象失败时默认返回空数组
try {
return JSON.parse(str);
} catch (e) {
console.error('Error parsing JSON string:', e);
return [];
}
},
goToViewMore() {
// 返回上一页
this.$router.go(-1);
},
handleCommentCountUpdate(count) {
this.commentCount = count;
},
},
watch: {
'$route'(to, from) {
// 当路由参数发生变化时重新加载数据
if (to.query.news_slug !== from.query.news_slug) {
this.news_slug = to.query.news_slug;
if (this.news_slug) {
this.getNewsDetail(this.news_slug);
}
}
}
},
mounted() {
this.news_slug = this.$route.query.news_slug;
if (this.news_slug) {
this.getNewsDetail(this.news_slug);
}
},
computed: {
tagList() {
if (!this.news_detail.tags) {
return [];
}
return this.stringJsonToObject(this.news_detail.tags || '[]');
}
},
}
</script>
<style scoped lang="scss">
.card {
padding: 20px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.content {
padding-top: 100px;
padding-bottom: 100px;
.diver {
margin-top: 20px;
border-top: 4px solid #E2E8F0;
margin-bottom: 20px;
}
.title-text {
font-size: 34px;
color: #1E293B;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
margin-top: 80px;
}
.rate-box {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: space-between;
.btn-wrapper {
display: flex;
align-items: center;
gap: 20px;
}
}
.terms-item {
margin-bottom: 30px;
display: flex;
align-items: flex-start;
gap: 7px;
.item-title {
display: flex;
align-items: center;
gap: 8px;
color: #1E293B;
font-family: 'Poppins-Medium', serif;
img {
width: 24px;
height: 24px;
}
}
.item-content {
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
}
.tags {
display: flex;
overflow-x: auto;
gap: 12px;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
flex-wrap: wrap;
width: 80%;
.tag-item {
font-family: 'Poppins-Medium', serif;
flex-shrink: 0;
padding: 4px 12px;
border-radius: 12px;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
}
.container {
gap: 20px;
.right-content {
width: 372px;
.diver-line {
margin-top: 20px;
border-top: 2px solid #E2E8F0;
margin-bottom: 20px;
}
.content-title {
margin-left: 6px;
font-family: 'Poppins-SemiBold', serif;
font-size: 24px;
color: #1E293B;
font-weight: 600;
}
.icon {
width: 24px;
height: 24px;
}
.more {
cursor: pointer;
border-radius: 6px;
padding: 8px 15px;
border: 1px solid #e2e8f0;
gap: 16px;
font-family: 'Poppins-Regular', serif;
color: #64748B;
img {
width: 16px;
height: 16px;
}
}
}
.left-content {
.sketch {
padding: 22px;
background-color: #F5F4FF;
border-radius: 12px;
.text {
font-family: 'Poppins-Regular', serif;
color: #64748B;
font-size: 18px;
}
.more {
cursor: pointer;
border-radius: 6px;
padding: 8px 15px;
border: 1px solid #e2e8f0;
gap: 16px;
font-family: 'Poppins-Regular', serif;
color: #64748B;
img {
width: 16px;
height: 16px;
}
}
}
}
}
.related {
margin-top: 60px;
.related-title {
.title {
margin-bottom: 20px;
font-size: 30px;
color: #1E293B;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
.more {
display: block;
text-align: right;
color: $grey-color;
font-size: $mid-font-size;
font-family: 'Poppins-Regular', serif;
}
}
.list {
gap: 60px;
margin: 40px 10px 40px 10px;
}
}
}
</style>

View File

@ -0,0 +1,173 @@
<template>
<div id="normal-container">
<IntegratedLayout>
<div class="content">
<div class="title-text">{{ financeDetail.title || '' }}</div>
<div class="rate-box">
<Rate v-model="financeDetail.rating" readonly />
<div class="flex" style="gap: 20px">
<CommentBtn :comment-count="commentCount" />
</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/icon_note.png" alt="">
<span>Introduction: </span>
</div>
<div class="item-content">
{{ financeDetail.summary || '' }}
</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/icon_clock.png" alt="">
<span>Data update: </span>
</div>
<div class="item-content">{{ financeDetail.publishTime || '' }}</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/like_icon.png" alt="">
<span>Like: </span>
</div>
<div class="item-content">{{ financeDetail.likeCount || 0 }}</div>
</div>
<div class="tags">
<div class="tag-item" v-for="(it, index) in tagList" :key="index">{{ it }}</div>
</div>
<div class="diver"></div>
<div class="title">Special Financing</div>
<div class="card"></div>
</div>
</IntegratedLayout>
</div>
</template>
<script>
import CommentBtn from "@/pages/ToolDetail/components/CommentBtn.vue";
export default {
components: {CommentBtn},
data() {
return {
commentCount: 0,
financeDetail: {
title: 'The Timeline Of Sketch',
rating: 1,
summary: 'The newly upgraded Sketch of Athens has optimized the loading speed of offline maps, added an AR real-scene navigation function, and expanded the hidden scenic spots recommended by local creators, making the exploration of Athens smoother, smarter and more in-depth',
publishTime: 'Sep 03 2025',
likeCount: 100,
},
tagList: [
'Sketch',
'Athens',
'Offline Maps',
'AR',
'Navigation',
'Scenic Spots',
'Exploration',
],
}
},
methods: {},
}
</script>
<style scoped lang="scss">
.card {
padding: 50px 30px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.title {
margin-bottom: 20px;
font-size: 30px;
color: #1E293B;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
.content {
padding-top: 180px;
padding-bottom: 100px;
.diver {
margin-top: 20px;
border-top: 4px solid #E2E8F0;
margin-bottom: 20px;
}
.title-text {
font-size: 34px;
color: #1E293B;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
margin-top: 80px;
}
.rate-box {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: space-between;
.btn-wrapper {
display: flex;
align-items: center;
gap: 20px;
}
}
.terms-item {
margin-bottom: 30px;
display: flex;
align-items: flex-start;
gap: 7px;
.item-title {
display: flex;
align-items: center;
gap: 8px;
color: #1E293B;
font-family: 'Poppins-Medium', serif;
img {
width: 24px;
height: 24px;
}
}
.item-content {
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
}
.tags {
display: flex;
overflow-x: auto;
gap: 12px;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
flex-wrap: wrap;
width: 80%;
.tag-item {
font-family: 'Poppins-Medium', serif;
flex-shrink: 0;
padding: 4px 12px;
border-radius: 12px;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
}
}
</style>

View File

@ -0,0 +1,46 @@
<script>
export default {
data() {
return {
currentPage: 1,
}
},
methods: {
}
}
</script>
<template>
<div>
<el-pagination
background
:page-size="10"
:current-page="currentPage"
layout="prev, pager, next"
:total="100">
</el-pagination>
</div>
</template>
<style scoped lang="scss">
::v-deep .el-pagination {
.btn-prev, .btn-next {
background-color: #FFFFFF !important;
border: 1px solid #E2E8F0 !important;
border-radius: 6px !important;
}
.el-pager {
.number, .more {
background-color: #FFFFFF !important;
border: 1px solid #E2E8F0 !important;
border-radius: 6px !important;
}
.active {
color: #7B61FF !important;
border: 1px solid #7B61FF !important;
background-color: #FFFFFF !important;
}
}
}
</style>

View File

@ -0,0 +1,188 @@
<template>
<div class="item-content flex justify-between" @click="handleClick">
<div class="left-content flex flex-1 items-center">
<div class="order-num">{{ sortIndex }}</div>
<div class="preview-box">
<img :src="config.coverImage || ''" alt="" />
</div>
<div class="tool-content flex flex-col justify-between">
<div class="content-top flex items-start">
<div class="icon">
<img src="/" alt="" />
</div>
<div class="flex-1">
<div class="title">{{ config.title || '' }}</div>
<div class="sub-title" style="padding-right: 30px">{{ config.summary || '' }}</div>
</div>
</div>
<div class="content-bottom flex items-center">
<img src="/launches/item/icon_clock.png" alt="" />
<div class="time-style">{{ config.publishTime || '' }}</div>
</div>
</div>
</div>
<div class="right-content flex">
<div class="flex items-center">
<img :src="hovered ? '/launches/item/icon_thumb_grey.png' : '/launches/item/icon_thumb.png'" alt="" />
<div :class="{ 'hover-text': hovered }">{{ config.likeCount || 0 }}</div>
</div>
<div class="flex items-center">
<img :src="hovered ? '/launches/item/icon_star_grey.png' : '/launches/item/icon_star.png'" alt="" />
<div :class="{ 'hover-text': hovered }">{{ config.rating || 0 }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
hovered: false
}
},
props: {
sortIndex: {
type: Number,
default: 1,
},
config: {
type: Object,
default: () => {
return {}
},
},
},
methods: {
handleMouseEnter() {
this.hovered = true;
},
handleMouseLeave() {
this.hovered = false;
},
// 跳转详情页
handleClick() {
if (!this.config.slug) {
return false;
}
this.articleClick(this.config.id);
this.$router.push({
path: '/launches/detail',
query: {
news_slug: this.config.slug || '',
}
});
},
// 记录文章点击数
async articleClick(id) {
if (id) {
await this.$api.article.recordArticleClick(id);
}
}
},
mounted() {
const itemContent = this.$el;
itemContent.addEventListener('mouseenter', this.handleMouseEnter);
itemContent.addEventListener('mouseleave', this.handleMouseLeave);
},
beforeDestroy() {
const itemContent = this.$el;
itemContent.removeEventListener('mouseenter', this.handleMouseEnter);
itemContent.removeEventListener('mouseleave', this.handleMouseLeave);
}
}
</script>
<style scoped lang="scss">
.item-content {
border-radius: 12px;
padding: 30px 20px;
transition: background-color 0.3s ease;
&:hover {
background-color: #F5F4FF;
.order-num {
background: #fff;
}
}
.left-content {
gap: 30px;
.order-num {
width: 36px;
height: 36px;
border-radius: 12px;
background: #F5F4FF;
margin-right: 10px;
text-align: center;
line-height: 36px;
font-size: 18px;
font-family: 'Poppins-Regular', serif;
transition: background-color 0.3s ease;
}
.preview-box {
width: 228px;
height: 133px;
border-radius: 6px;
background-color: #FFFFFF;
img {
width: 100%;
height: 100%;
}
}
.tool-content {
height: 100%;
.content-top {
.icon {
width: 46px;
height: 46px;
margin-right: 10px;
img {
width: 100%;
height: 100%;
}
}
.title {
font-size: 20px;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
line-height: 24px;
color: #3A4A65;
}
.sub-title {
color: #64748B;
font-family: 'Poppins-Regular', serif;
margin-top: 10px;
}
}
.content-bottom {
gap: 12px;
img {
width: 24px;
height: 24px;
}
.time-style {
color: #64748B;
font-family: 'Poppins-Regular', serif;
}
}
}
}
.right-content {
padding-right: 23px;
gap: 33px;
font-family: 'Poppins-Regular', serif;
color: #E2E8F0;
height: 24px;
img {
width: 24px;
height: 24px;
margin-right: 12px;
}
.hover-text {
color: #64748B;
}
}
}
</style>

View File

@ -0,0 +1,314 @@
<template>
<div class="date-picker-core">
<!-- 日期// 选择区 -->
<div class="picker-container">
<!-- 日模式 -->
<div v-if="mode === 'Daily'" class="daily-picker">
<HorizontalDateList>
<button
v-for="day in dailyDays"
:key="day.dateStr"
:class="{ 'selected': day.dateStr === selectedDate }"
@click="handleSelect(day.dateStr)"
>{{ day.day }}</button>
</HorizontalDateList>
</div>
<!-- 周模式 -->
<div v-else-if="mode === 'Weekly'" class="weekly-picker">
<HorizontalDateList>
<button
v-for="week in weeklyRanges"
:key="week.start"
:class="{ 'selected': week.start === selectedDate[0] && week.end === selectedDate[1] }"
@click="handleSelect([week.start, week.end])"
>{{ week.label }}</button>
</HorizontalDateList>
</div>
<!-- 月模式 -->
<div v-else class="monthly-picker">
<HorizontalDateList>
<button
style="margin: 0 15px"
v-for="(monthAbbr, index) in monthAbbrs"
:key="index"
:class="{ 'selected': (index + 1) === selectedMonth }"
@click="handleMonthSelect(index + 1)"
>{{ monthAbbr }}</button>
</HorizontalDateList>
</div>
</div>
</div>
</template>
<script>
import HorizontalDateList from '@/components/HorizontalDateList.vue';
export default {
name: 'OptionDates',
components: {
HorizontalDateList
},
props: {
// 当前年份,由外部传入
year: {
type: Number,
required: true
},
// 当前模式,由外部传入 (Daily, Weekly, Monthly)
mode: {
type: String,
required: true,
validator: (val) => ['Daily', 'Weekly', 'Monthly'].includes(val)
},
// 日模式下的当前月份,由外部传入
month: {
type: Number,
default: () => new Date().getMonth() + 1,
validator: (val) => val >= 1 && val <= 12
}
},
data() {
return {
selectedDate: this.mode === 'Daily' ? '' : ['', ''], // 选中的日期/日期范围
selectedMonth: this.month // 月模式下选中的月份
};
},
computed: {
// 日模式:生成"当前年月"的所有日期
dailyDays() {
const daysInMonth = this.getDaysInMonth(this.year, this.month);
return Array.from({ length: daysInMonth }, (_, i) => {
const day = i + 1;
const date = new Date(this.year, this.month - 1, day);
return {
day,
dateStr: this.formatDate(date)
};
});
},
// 周模式:生成"当前年份"的所有完整周区间
weeklyRanges() {
return this.getWeekRanges(this.year);
},
// 月模式12个月份的英文简写
monthAbbrs() {
return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
}
},
created() {
// 组件创建时自动选中当前日期或周
this.autoSelectCurrentDate();
},
methods: {
// 自动选中当前日期或周
autoSelectCurrentDate(shouldEmit = true) {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
const currentDateStr = this.formatDate(now);
// 只有当年份和月份匹配当前日期时才自动选中
if (this.year === currentYear) {
if (this.mode === 'Daily' && this.month === currentMonth) {
this.selectedDate = currentDateStr;
if (shouldEmit) {
this.$emit('select', currentDateStr);
}
} else if (this.mode === 'Weekly') {
// 找到当前日期所在的周
const currentWeek = this.getWeekForDate(now);
if (currentWeek) {
this.selectedDate = [currentWeek.start, currentWeek.end];
if (shouldEmit) {
this.$emit('select', [currentWeek.start, currentWeek.end]);
}
}
} else if (this.mode === 'Monthly') {
// 月模式下自动选中当前月并导出日期范围
this.selectedMonth = currentMonth;
const [start, end] = this.getMonthRange(this.year, currentMonth);
if (shouldEmit) {
this.$emit('select', [start, end]);
}
}
}
},
// 获取指定日期所在的周
getWeekForDate(date) {
const weeks = this.getWeekRanges(this.year);
const dateStr = this.formatDate(date);
for (const week of weeks) {
// 检查日期是否在该周范围内
if (dateStr >= week.start && dateStr <= week.end) {
return week;
}
}
return null;
},
// 处理日/周模式的选择
handleSelect(value) {
this.selectedDate = value;
this.$emit('select', value);
},
// 处理月模式的选择
handleMonthSelect(month) {
this.selectedMonth = month;
const [start, end] = this.getMonthRange(this.year, month);
this.$emit('select', [start, end]);
},
// 辅助获取某月的天数month1-12
getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate();
},
// 辅助:格式化日期为 yyyy-mm-dd
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 辅助获取某月的首尾日期month1-12
getMonthRange(year, month) {
const start = new Date(year, month - 1, 1);
const end = new Date(year, month, 0);
return [this.formatDate(start), this.formatDate(end)];
},
// 辅助:获取某年的所有完整周区间(假设周从周日开始,到周六结束)
getWeekRanges(year) {
const ranges = [];
let currentDate = new Date(year, 0, 1); // 当年1月1日
// 找到第一个周日(作为周起始)
while (currentDate.getDay() !== 0) {
currentDate.setDate(currentDate.getDate() + 1);
}
// 生成所有"结束日期在当年"的完整周
while (currentDate.getFullYear() === year) {
const start = new Date(currentDate);
const end = new Date(currentDate);
end.setDate(end.getDate() + 6); // 周日到周六共7天
if (end.getFullYear() === year) { // 确保周结束在当年
ranges.push({
label: `${this.monthAbbrs[start.getMonth()]} ${start.getDate()}-${end.getDate()}`,
start: this.formatDate(start),
end: this.formatDate(end)
});
}
currentDate.setDate(currentDate.getDate() + 7); // 下一周
}
return ranges;
},
// 重置选择状态
resetSelection() {
if (this.mode === 'Daily') {
this.selectedDate = '';
} else if (this.mode === 'Weekly') {
this.selectedDate = ['', ''];
} else {
this.selectedMonth = this.month;
}
}
},
watch: {
// 监听年份变化,重置选择
year() {
this.resetSelection();
this.autoSelectCurrentDate();
},
mode: {
handler(newMode, oldMode) {
if (newMode !== oldMode) {
this.resetSelection();
// 切换模式时不立即触发 emit让组件自行处理
this.autoSelectCurrentDate(false);
// 通知子组件重新计算滚动状态
this.$nextTick(() => {
this.$children.forEach(child => {
if (child.$options.name === 'HorizontalDateList') {
child.updateScrollState();
}
});
});
}
},
immediate: true
},
month() {
if (this.mode === 'Daily') {
this.selectedDate = '';
}
this.autoSelectCurrentDate();
}
},
};
</script>
<style scoped>
.date-picker-core {
padding: 10px;
}
.picker-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
background-color: #ffffff;
}
.daily-picker,
.weekly-picker,
.monthly-picker {
display: flex;
flex-wrap: wrap;
gap: 6px;
width: 100%;
}
.daily-picker button,
.weekly-picker button,
.monthly-picker button {
padding: 6px 10px;
border: 1px solid #e5e7eb;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
&:active {
opacity: 0.8;
}
}
.daily-picker button:hover,
.weekly-picker button:hover,
.monthly-picker button:hover {
border-color: #9ca3af;
}
.daily-picker button.selected,
.weekly-picker button.selected,
.monthly-picker button.selected {
background: #4f46e5;
color: #fff;
border-color: #4f46e5;
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="switch-box flex-between-center">
<div class="item-text" :class="value === 'Daily' && 'active'" @click="handleClick('Daily')">Daily</div>
<div class="diver"></div>
<div class="item-text" :class="value === 'Weekly' && 'active'" @click="handleClick('Weekly')">Weekly</div>
<div class="diver"></div>
<div class="item-text" :class="value === 'Monthly' && 'active'" @click="handleClick('Monthly')">Monthly</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: 'Daily'
}
},
data() {
return {}
},
methods: {
handleClick(type) {
this.$emit('input', type);
}
}
}
</script>
<style scoped lang="scss">
.switch-box {
padding: 22px 40px;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 10px 30px 0 #0000000d;
.diver {
width: 1px;
height: 36px;
margin-left: 70px;
margin-right: 70px;
border-left-width: 1px;
border-left-style: solid;
border-left-color: #E2E8F0;
}
.item-text {
font-size: 24px;
font-family: 'Poppins-Regular', serif;
color: #3A4A65;
&:active {
opacity: 0.8;
}
}
.active {
color: #7B61FF;
font-weight: 900 !important;
font-family: 'Poppins-Bold', serif;
}
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="flex-center box">
<div class="btn" @click="prevYear">
<img :src="prevIcon" alt="Previous Year" />
</div>
<div style="width: 60px;text-align: center">{{ monthName }}</div>
<div class="btn" @click="nextYear">
<img :src="nextIcon" :alt="isNextDisabled ? 'Next Year Disabled' : 'Next Year'" />
</div>
</div>
</template>
<script>
import IconPrev from '@/static/launches/icon_prev.png';
import IconNext from '@/static/launches/icon_next.png';
import IconNextDisabled from '@/static/launches/icon_next_disabled.png';
import IconPrevDisabled from '@/static/launches/icon_prev_disabled.png';
export default {
data() {
return {
}
},
props: {
value: {
type: Number,
default: () => new Date().getMonth() + 1
}
},
methods: {
prevYear() {
if (this.value > 1) {
this.$emit('input', this.value - 1);
}
},
nextYear() {
if (this.value < 12) {
this.$emit('input', this.value + 1);
}
}
},
computed: {
monthName() {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return months[this.value - 1];
},
prevIcon() {
return this.value > 1 ? IconPrev : IconPrevDisabled;
},
nextIcon() {
return this.value < 12 ? IconNext : IconNextDisabled;
},
isNextDisabled() {
return this.value >= 12;
}
},
}
</script>
<style scoped lang="scss">
.box {
gap: 16px;
color: #3A4A65;
font-size: 24px;
font-family: 'Poppins-SemiBold', serif;
.btn {
width: 48px;
height: 48px;
cursor: pointer;
background-color: #FFFFFF;
border-radius: 4px;
&:active {
opacity: 0.8;
}
img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,47 @@
<script>
export default {
props: {
value: {
type: String,
default: 'popular'
}
},
methods: {
handleClick(type) {
this.$emit('input', type);
}
}
}
</script>
<template>
<div class="switch-sort-box flex-center">
<div class="btn-item" @click="handleClick('popular')">
<img :src="value === 'popular' ? '/launches/icon_popular_checked.png' : '/launches/icon_popular.png'" alt="" />
</div>
<div class="btn-item" @click="handleClick('newest')">
<img :src="value === 'newest' ? '/launches/icon_newest_checked.png' : '/launches/icon_newest.png'" alt="" />
</div>
</div>
</template>
<style scoped lang="scss">
.switch-sort-box {
width: 294px;
height: 52px;
background-color: #FFFFFF;
border-radius: 12px;
.btn-item {
width: 147px;
height: 52px;
cursor: pointer;
&:active {
opacity: 0.8;
}
img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,78 @@
<script>
import IconPrev from '@/static/launches/icon_prev.png';
import IconNext from '@/static/launches/icon_next.png';
import IconNextDisabled from '@/static/launches/icon_next_disabled.png';
export default {
props: {
value: {
type: Number,
default: () => new Date().getFullYear()
}
},
data() {
return {}
},
computed: {
year() {
return this.value;
},
prevIcon() {
return IconPrev;
},
nextIcon() {
return this.year === new Date().getFullYear() ? IconNextDisabled : IconNext;
},
isNextDisabled() {
return this.year === new Date().getFullYear();
}
},
methods: {
prevYear() {
this.$emit('input', this.year - 1);
},
nextYear() {
if (!this.isNextDisabled) {
this.$emit('input', this.year + 1);
}
}
}
}
</script>
<template>
<div class="flex-center box">
<div class="btn" @click="prevYear">
<img :src="prevIcon" alt="Previous Year" />
</div>
<div style="width: 60px;text-align: center">{{ year }}</div>
<div class="btn" @click="nextYear">
<img :src="nextIcon" :alt="isNextDisabled ? 'Next Year Disabled' : 'Next Year'" />
</div>
</div>
</template>
<style scoped lang="scss">
.box {
gap: 16px;
color: #3A4A65;
font-size: 24px;
font-family: 'Poppins-SemiBold', serif;
.btn {
width: 48px;
height: 48px;
cursor: pointer;
background-color: #FFFFFF;
border-radius: 4px;
&:active {
opacity: 0.8;
}
img {
width: 100%;
height: 100%;
}
}
}
</style>

220
pages/Launches/index.vue Normal file
View File

@ -0,0 +1,220 @@
<template>
<div id="normal-container" class="launches-content" v-loading.fullscreen.lock="fullscreenLoading">
<IntegratedLayout>
<div class="content">
<div class="launches-header">
<h1 class="launches-title-text">
AI Launches
</h1>
<p class="launches-subtitle-text">A daily curated selection of the newest AI applications and products. Explore the most talked-about innovations, trending technologies, and hot new releases shaping the AI landscape. Stay ahead by discovering whats capturing attention in the AI community.</p>
</div>
<div class="flex justify-center">
<SwitchDate v-model="currentMode" />
</div>
<div class="list-header flex-between-center">
<SwitchSort v-model="sortType" />
<div class="flex items-center" style="gap: 60px">
<SwitchMonth v-show="currentMode === 'Daily'" v-model="currentMonth" />
<SwitchYear v-model="currentYear" />
</div>
</div>
<div class="card list-container">
<OptionDates :year="currentYear" :mode="currentMode" :month="currentMonth" @select="handleDateSelect" />
<div class="diver"></div>
<div class="list">
<ListItem v-for="(it, i) in articleList" :key="it.id" :config="it" :sort-index="i + 1" />
</div>
</div>
<div class="flex-bottom-right">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</IntegratedLayout>
</div>
</template>
<script>
import SwitchDate from "@/pages/Launches/components/SwitchDate.vue";
import SwitchSort from "@/pages/Launches/components/SwitchSort.vue";
import SwitchYear from "@/pages/Launches/components/SwitchYear.vue";
import SwitchMonth from "@/pages/Launches/components/SwitchMonth.vue";
import ListItem from "@/pages/Launches/components/ListItem.vue";
import OptionDates from "@/pages/Launches/components/OptionDates.vue";
export default {
components: {
SwitchDate,
SwitchSort,
SwitchYear,
ListItem,
OptionDates,
SwitchMonth,
},
data() {
return {
lastSelectedValue: null,
currentPage: 1,
totalPages: 1,
pageSize: 10,
total: 0,
currentYear: new Date().getFullYear(),
currentMode: 'Daily',
currentMonth: new Date().getMonth() + 1,
articleList: [],
sortType: 'popular',
fullscreenLoading: false,
}
},
watch: {
total() {
this.calculateTotalPages();
},
pageSize() {
this.calculateTotalPages();
},
sortType(newVal, oldVal) {
if (oldVal !== null) { // 避免初始化时触发
let startTime = '';
let endTime = '';
if (this.lastSelectedValue) {
try {
const selectedValue = JSON.parse(this.lastSelectedValue);
if (selectedValue instanceof Array) {
startTime = selectedValue[0] + ' 00:00:00';
endTime = selectedValue[1] + ' 23:59:59';
} else {
startTime = selectedValue + ' 00:00:00';
endTime = selectedValue + ' 23:59:59';
}
} catch (e) {
console.error('解析时间参数失败', e);
}
}
// 重新获取文章列表,传入新的排序类型
this.getArticleListData(this.currentPage, this.pageSize, startTime, endTime, newVal);
}
},
},
methods: {
calculateTotalPages() {
// 当 total 为 0 时 totalPages 为 1
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
handleDateSelect(selectedValue) {
const stringValue = JSON.stringify(selectedValue);
if (this.lastSelectedValue === stringValue) {
return;
}
this.lastSelectedValue = stringValue;
let startTime = '';
let endTime = '';
if (selectedValue instanceof Array) {
startTime = selectedValue[0] + ' 00:00:00';
endTime = selectedValue[1] + ' 23:59:59';
} else {
startTime = selectedValue + ' 00:00:00';
endTime = selectedValue + ' 23:59:59';
}
this.getArticleListData(this.currentPage, this.pageSize, startTime, endTime, this.sortType);
},
// 获取文章列表
async getArticleListData(page = 1, limit = 10, startTime, endTime, sortType = 'popular') {
const params = {page, limit, startTime, endTime, articleType: 'launches'};
if (sortType === 'popular') {
params.isHot = 1;
} else {
params.sortField = 'publish_time';
params.sortOrder = 'desc';
}
this.fullscreenLoading = true;
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
this.total = data.total;
this.calculateTotalPages();
} else {
this.articleList = [];
this.total = 0;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
// 从 lastSelectedValue 中提取时间参数
let startTime = '';
let endTime = '';
if (this.lastSelectedValue) {
try {
const selectedValue = JSON.parse(this.lastSelectedValue);
if (selectedValue instanceof Array) {
startTime = selectedValue[0] + ' 00:00:00';
endTime = selectedValue[1] + ' 23:59:59';
} else {
startTime = selectedValue + ' 00:00:00';
endTime = selectedValue + ' 23:59:59';
}
} catch (e) {
console.error('解析时间参数失败', e);
}
}
this.getArticleListData(pageNumber, this.pageSize, startTime, endTime, this.sortType);
},
},
}
</script>
<style scoped lang="scss">
.card {
padding: 60px 30px;
background-color: #FFFFFF;
border-radius: 12px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.08);
}
.launches-content {
flex: 1;
overflow-y: auto;
position: relative;
.content {
padding-top: 190px;
padding-bottom: 100px;
.launches-header {
margin-bottom: 114px;
.launches-title-text {
margin: 0;
font-size: 40px;
font-weight: bold;
font-family: 'Poppins-Bold', serif;
}
.launches-subtitle-text {
font-family: 'Poppins-Medium', serif;
color: #64748B;
margin-top: 10px;
}
}
.list-header {
margin-top: 56px;
}
.list-container {
margin-top: 60px;
margin-bottom: 50px;
.diver {
height: 1px;
border-top-color: #E2E8F0;
border-top-style: solid;
border-top-width: 2px;
margin-top: 40px;
margin-bottom: 40px;
}
.list {
gap: 30px;
display: flex;
flex-direction: column;
}
}
}
}
</style>

103
pages/Learn/Depth.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<div class="container" v-loading.fullscreen.lock="fullscreenLoading">
<div class="top-text">
<div class="title">In-Depth Analysis</div>
<div class="description">
Provides thorough articles, case studies, and technical breakdowns. Analyze AI topics from both theoretical and practical perspectives, offering valuable guidance for professionals, researchers, and enthusiasts looking to deepen their understanding.
</div>
</div>
<div class="input">
<SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
</div>
<div class="list">
<ListCardItem v-for="it in articleList" :key="it.id" :item="it" type="analysis" />
</div>
<div class="pagination-wrapper">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</template>
<script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default {
components: {
ListCardItem,
},
data() {
return {
currentPage: 1,
totalPages: 1,
pageSize: 10,
articleList: [],
searchText: '',
total: 0,
fullscreenLoading: false,
}
},
methods: {
// 模糊搜索
async handleTextSearch() {
await this.getArticleListData(this.currentPage, this.pageSize, this.searchText);
},
calculateTotalPages() {
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
this.getArticleListData(pageNumber, 10);
},
// 获取文章列表
async getArticleListData(page = 1, limit = 10, searchText) {
this.fullscreenLoading = true;
const params = {page, limit, articleType: 'analysis', title: searchText};
if (!searchText) {
delete params.title;
}
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
this.total = data.total;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
}
},
mounted() {
this.getArticleListData(this.currentPage, this.pageSize);
}
}
</script>
<style scoped lang="scss">
.container {
.title {
font-weight: bold;
font-size: $huge-font-size1;
}
.description {
font-size: $big-font-size;
color: $grey-color;
}
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom: 60px;
}
}
.list {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 60px;
}
}
</style>

103
pages/Learn/Observer.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<div class="container" v-loading.fullscreen.lock="fullscreenLoading">
<div class="top-text">
<div class="title">AI Observer</div>
<div class="description">
An organized hub of leading AI research projects and frameworks. Dive into the methodologies of major academic papers, understand the underlying technical principles, and gain insights that support both learning and hands-on AI development.
</div>
</div>
<div class="input">
<SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
</div>
<div class="list">
<ListCardItem v-for="it in articleList" :key="it.id" :item="it" type="observer" />
</div>
<div class="pagination-wrapper">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</template>
<script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default {
components: {
ListCardItem,
},
data() {
return {
currentPage: 1,
totalPages: 1,
pageSize: 10,
articleList: [],
searchText: '',
total: 0,
fullscreenLoading: false,
}
},
methods: {
// 模糊搜索
async handleTextSearch() {
await this.getArticleListData(this.currentPage, this.pageSize, this.searchText);
},
calculateTotalPages() {
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
this.getArticleListData(pageNumber, 10);
},
// 获取文章列表
async getArticleListData(page = 1, limit = 10, searchText) {
this.fullscreenLoading = true;
const params = {page, limit, articleType: 'observer', title: searchText};
if (!searchText) {
delete params.title;
}
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
this.total = data.total;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
}
},
mounted() {
this.getArticleListData(this.currentPage, this.pageSize);
}
}
</script>
<style scoped lang="scss">
.container {
.title {
font-weight: bold;
font-size: $huge-font-size1;
}
.description {
font-size: $big-font-size;
color: $grey-color;
}
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom: 60px;
}
}
.list {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 60px;
}
}
</style>

103
pages/Learn/Pioneer.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<div class="container" v-loading.fullscreen.lock="fullscreenLoading">
<div class="top-text">
<div class="title">Pioneer In the Field</div>
<div class="description">
Spotlights leading figures, research teams, and innovators in AI. Discover their philosophies, key achievements, and the impact they are making, offering inspiration and context for understanding the forefront of AI development.
</div>
</div>
<div class="input">
<SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
</div>
<div class="list">
<ListCardItem v-for="it in articleList" :key="it.id" :item="it" type="pioneer" />
</div>
<div class="pagination-wrapper">
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div>
</div>
</template>
<script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default {
components: {
ListCardItem,
},
data() {
return {
currentPage: 1,
totalPages: 1,
pageSize: 10,
articleList: [],
searchText: '',
total: 0,
fullscreenLoading: false,
}
},
methods: {
// 模糊搜索
async handleTextSearch() {
await this.getArticleListData(this.currentPage, this.pageSize, this.searchText);
},
calculateTotalPages() {
// 否则向上取整计算总页数
this.totalPages = this.total === 0 ? 1 : Math.ceil(this.total / this.pageSize);
},
handlePageChange(pageNumber) {
this.currentPage = pageNumber;
this.getArticleListData(pageNumber, 10);
},
// 获取文章列表
async getArticleListData(page = 1, limit = 10, searchText) {
this.fullscreenLoading = true;
const params = {page, limit, articleType: 'pioneer', title: searchText};
if (!searchText) {
delete params.title;
}
const {data: res} = await this.$api.article.getArticleList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.articleList = data.list;
this.total = data.total;
this.calculateTotalPages();
}
this.fullscreenLoading = false;
}
},
mounted() {
this.getArticleListData(this.currentPage, this.pageSize);
}
}
</script>
<style scoped lang="scss">
.container {
.title {
font-weight: bold;
font-size: $huge-font-size1;
}
.description {
font-size: $big-font-size;
color: $grey-color;
}
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom: 60px;
}
}
.list {
display: flex;
flex-direction: column;
gap: 30px;
margin-bottom: 60px;
}
}
</style>

40
pages/Learn/index.vue Normal file
View File

@ -0,0 +1,40 @@
<template>
<div id="normal-container">
<IntegratedLayout>
<div class="content">
<div class="bread-menu">
<span>AI Hub</span>
<i class="el-icon-arrow-right"></i>
<span class="crumbs gradient-color">{{$route.name}}</span>
</div>
<nuxt-child />
</div>
</IntegratedLayout>
</div>
</template>
<script>
export default {
data() {
return {
}
},
methods: {}
}
</script>
<style scoped lang="scss">
.content {
padding-bottom: 100px;
.bread-menu {
font-size: $mid-font-size;
margin: 100px 0;
font-family: 'Poppins-Medium', serif;
.crumbs {
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
}
}
}
</style>

View File

@ -0,0 +1,329 @@
<template>
<div class="comment-content">
<div class="comment-title">
Comment
<span class="little-text">{{commentList.length}} comments</span>
</div>
<!--写评论-->
<div class="write-comment-box card">
<div class="flex-between-center" style="margin-bottom: 30px">
<div class="flex-1">
<h2 class="content-title">Would you recommend Skywork?</h2>
<div class="rate-box flex-between-center">
<div class="flex items-center">
<Rate v-model="rate" />
<span style="margin-left: 30px; display: block">Full score: 5 points</span>
</div>
<div class="flex items-center" style="gap: 16px" v-if="alarmText">
<img src="/logo/icon_alarm.png" alt="" style="width: 16px; height: 16px;" />
<p style="color: #CD0E4AFF">{{ alarmText }}</p>
</div>
</div>
</div>
<div class="comment-avatar flex-center">
<img src="/logo/logo-rect.png" alt="" />
</div>
</div>
<!--评论输入框-->
<el-input
type="textarea"
placeholder="Please express your opinions"
v-model="commentText"
:maxlength="1000"
>
</el-input>
<div class="information-container flex-between-center">
<el-input v-model="nickName" placeholder="NickName" :maxlength="50" />
<el-input v-model="email" placeholder="Email" :maxlength="50" />
</div>
<div class="submit-button flex-center" @click="submitComment">
<span>Submit</span>
<img src="/ToolDetail/icon_arrow.png" alt="" />
</div>
</div>
<!--评论列表-->
<div class="comment-list card">
<div class="list-header flex-between-center">
<h2 class="content-title">All comments <span class="little-text">{{commentList.length}} comments</span></h2>
<div class="more pointer" v-if="!checkMore" @click="checkMore = true">
View more<i class="el-icon-arrow-right"></i>
</div>
</div>
<div class="comment-list-wrap">
<CommentItem v-for="(it, index) in filterList" :key="index" :item="it" />
</div>
</div>
</div>
</template>
<script>
import CommentItem from "@/pages/ToolDetail/components/CommentItem.vue";
import Rate from "@/components/Rate.vue";
export default {
components: {
CommentItem,
Rate,
},
props: {
commentType: {
type: String,
default: 'tool',
},
id: {
type: Number,
default: 0,
},
},
data() {
return {
commentText: '',
nickName: '',
email: '',
rate: 0,
commentList: [],
checkMore: false,
alarmText: '',
}
},
methods: {
// 获取评论列表
async getCommentListData() {
if (this.id) {
const params = this.commentType === 'tool' ? {toolId: this.id} : {articleId: this.id};
const {data: res} = await this.$api.comment.getToolCommentList(params);
const {code, data} = res;
if (code === 0 && data.list && data.list instanceof Array) {
this.commentList = [...data.list];
// 向父组件传递评论数量
this.$emit('update:commentCount', this.commentList.length);
} else {
// 即使没有数据也更新评论数量为0
this.$emit('update:commentCount', 0);
}
}
},
// 验证昵称
validateNickname() {
if (!this.nickName) {
this.alarmText = 'Please enter your nickname';
return false;
} else if (this.nickName.length >= 50) {
this.alarmText = 'Nickname should not exceed 50 characters';
return false;
}
return true;
},
// 验证邮箱
validateEmail() {
if (!this.email) {
this.alarmText = 'Please enter your email';
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(this.email)) {
this.alarmText = 'Please fill in the correct email address';
return false;
}
if (this.email.length >= 50) {
this.alarmText = 'Email should not exceed 50 characters';
return false;
}
return true;
},
// 验证评论内容词数
validateComment() {
if (!this.commentText) {
this.alarmText = 'Please enter your comment';
return false;
}
const wordCount = this.commentText.length;
if (wordCount > 1000) {
this.alarmText = 'Comment should not exceed 1000 words';
return false;
}
return true;
},
// 提交评论
async submitComment() {
if (!this.id) {
return false;
}
// 清除之前的警告信息
this.alarmText = '';
// 验证各项输入
const isCommentValid = this.validateComment();
const isNicknameValid = this.validateNickname();
const isEmailValid = this.validateEmail();
if (!isCommentValid || !isNicknameValid || !isEmailValid) {
return;
}
const params = {
content: this.commentText,
username: this.nickName,
email: this.email,
rating: this.rate,
};
this.commentType === 'tool' ? params.toolId = this.id : params.articleId = this.id;
const {data: res} = await this.$api.comment.addToolComment(params);
const {code} = res;
if (code === 0) {
this.$message.success('Comment submitted successfully');
this.rate = null;
this.commentText = '';
this.nickName = '';
this.email = '';
await this.getCommentListData();
} else {
this.$message.error('Comment submission failed');
}
}
},
watch: {
id: {
handler(newVal) {
if (newVal) {
this.getCommentListData();
}
},
immediate: true
}
},
computed: {
filterList() {
if (!this.checkMore) {
return this.commentList.slice(0, 5);
} else {
return this.commentList;
}
}
},
}
</script>
<style scoped lang="scss">
.card {
background-color: #fff;
border-radius: 12px;
padding: 30px;
box-shadow: 0 10px 30px 0 #0000000d;
}
.comment-content {
padding-top: 50px;
.comment-title {
font-size: 30px;
font-weight: 600;
margin-bottom: 30px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
.write-comment-box {
.rate-box {
margin-top: 14px;
font-family: 'Poppins-Regular', serif;
color: #E2E8F0;
}
::v-deep .el-textarea {
.el-textarea__inner {
border-color: #E2E8F0 !important;
padding: 30px !important;
height: 320px !important;
}
}
.information-container {
margin-top: 20px;
gap: 20px;
::v-deep .el-input {
.el-input__inner {
border-color: #E2E8F0 !important;
}
}
}
.submit-button {
padding: 8px 38px;
background: $header-backgroungd;
border-radius: 12px;
gap: 4px;
margin-top: 30px;
color: #fff;
font-size: 18px;
font-family: 'Poppins-Medium', serif;
width: fit-content;
margin-left: auto;
cursor: pointer;
img {
width: 24px;
height: 24px;
}
&:active {
opacity: 0.8;
}
}
.comment-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid #E2E8F0;
margin-left: 24px;
img {
width: 24px;
height: 24px;
}
}
}
.comment-list {
margin-top: 20px;
.list-header {
margin-top: 30px;
margin-bottom: 20px;
}
.comment-list-wrap {
padding-right: 36px;
}
}
}
.little-text {
font-size: 14px;
font-family: 'Poppins-Regular', serif;
color: #64748B;
vertical-align: middle;
margin-left: 8px;
}
.content-title {
font-size: 24px;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
line-height: 24px;
margin: 0;
}
.more {
display: block;
text-align: right;
color: $grey-color;
font-size: $mid-font-size;
font-family: 'Poppins-Regular', serif;
}
</style>

View File

@ -0,0 +1,267 @@
<template>
<div class="product-content">
<h1 class="product-title">Product information</h1>
<div class="content-card article-content">
<div v-html="tool_content || ''"></div>
</div>
<div class="law-text-box">
<div class="law-title"> Special Announcement </div>
<p class="law-text">Without the explicit written permission of this platform, no unit or individual may copy, reprint, quote, modify, disseminate or use all or part of the content of this website in any way. It is strictly prohibited to establish a mirror image or conduct illegal collection on any unofficially authorized server. For any infringement, this platform will hold the offender legally responsible in accordance with the law.</p>
</div>
<!--其他相似导航-->
<div v-if="otherTools.length" class="similar-nav-box">
<!--表头-->
<div class="flex-between-center">
<div class="flex-center similar-nav-title-box">
<img src="/ToolDetail/icon_pack.png" alt="" style="width: 24px; height: 24px" />
<span class="similar-nav-title">Similar Tools</span>
</div>
<NuxtLink :to="'/home/more?category_slug=' + categorySlug" class="more pointer">
View more<i class="el-icon-arrow-right"></i>
</NuxtLink>
</div>
<!--列表-->
<div class="similar-nav-list">
<SimilarToolCard v-for="it in filteredOtherTools" :key="it.id" :config="it" />
</div>
</div>
<!--滚动预览图-->
<div class="tools-preview-box">
<el-carousel autoplay height="300px">
<el-carousel-item v-for="(item, i) in banner" :key="i">
<img :src="item.imageUrl || ''" alt="" style="height: 300px; width: 100%; border-radius: 12px" />
</el-carousel-item>
</el-carousel>
</div>
</div>
</template>
<script>
import SimilarToolCard from "@/pages/ToolDetail/components/SimilarToolCard.vue";
export default {
components: {
SimilarToolCard
},
props: {
otherTools: {
type: Array,
default: () => []
},
toolId: {
type: Number,
default: 0,
},
toolSlug: {
type: String,
default: "",
},
categorySlug: {
type: String,
default: "",
},
tool_content: {
type: String,
default: "",
},
},
data() {
return {}
},
methods: {},
computed: {
filteredOtherTools() {
// 检查传入的列表中是否有id等于toolId的项
const toolIndex = this.otherTools.findIndex(tool => tool.id === this.toolId);
if (toolIndex !== -1) {
// 如果有则返回去掉该id的列表
return this.otherTools.filter((tool, index) => index !== toolIndex);
} else {
// 如果没有,则截取传入列表的前八个选项
return this.otherTools.slice(0, 8);
}
},
banner() {
const bannerConfig = this.$store.getters.bannerConfig;
if (bannerConfig.tools && bannerConfig.tools.length > 0) {
return bannerConfig.tools;
}
return [];
},
// 查看更多
goToViewMore() {
if (this.categorySlug) {
this.$router.push('/home/more?category_slug=' + this.categorySlug)
}
}
},
mounted() {
this.$store.dispatch('getBannerConfig');
}
}
</script>
<style scoped lang="scss">
.product-content {
padding-top: 50px;
.product-title {
font-size: 30px;
font-weight: 600;
margin-bottom: 30px;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 6px;
height: 26px;
background: $header-backgroungd;
margin-right: 8px;
border-radius: 0 6px 6px 0;
}
}
.content-card {
background-color: #fff;
padding: 60px 30px;
box-shadow: 0 10px 30px 0 #0000000d;
.content-title {
color: #1E293B;
font-size: 24px;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
display: flex;
align-items: center;
margin-bottom: 12px;
&::before {
content: '';
display: inline-block;
width: 10px;
height: 10px;
background: $header-backgroungd;
margin-right: 13px;
border-radius: 50%;
}
}
.diver {
height: 1px;
border-top: 2px solid #f3f8fe;
margin-bottom: 20px;
margin-top: 20px;
}
.content-text {
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
.content-text-semi-bold {
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
color: #64748B;
}
.content-text-url {
font-family: 'Poppins-Regular', serif;
color: #7B61FF;
}
}
.law-text-box {
background-color: #FFF8F1;
padding: 50px;
margin-top: 60px;
.law-title {
font-size: 24px;
color: #1E293B;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
text-align: center;
margin-bottom: 22px;
}
.law-text {
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
}
.similar-nav-box {
margin-top: 60px;
.similar-nav-title-box {
padding: 10px 16px;
border-radius: 12px;
background: $header-backgroungd;
gap: 8px;
.similar-nav-title {
color: #fff;
font-size: 24px;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
line-height: 24px;
}
}
.more {
display: block;
text-align: right;
color: $grey-color;
font-size: $mid-font-size;
font-family: 'Poppins-Regular', serif;
&:active {
opacity: 0.8;
}
}
.similar-nav-list {
display: grid;
//grid-auto-rows: 1fr;
grid-template-columns: repeat(4, 1fr);
justify-content: space-between;
gap: 20px;
margin-top: 30px;
}
}
.tools-preview-box {
background-color: #ffffff4d;
border-radius: 16px;
box-shadow: 0 18px 33px 0 #0000000d;
padding: 35px 20px;
margin-top: 60px;
::v-deep .el-carousel {
.el-carousel__arrow {
opacity: 0 !important;
transition: none !important;
}
.el-carousel__indicators {
.el-carousel__indicator {
height: 4px !important;
width: 12px !important;
border-radius: 2.66px !important;
border: 0.66px solid #2563eb !important;
background: transparent !important;
margin: 0 3px 0 3px !important;
padding: 0 !important;
.el-carousel__button {
background-color: transparent;
}
&.is-active {
border: none !important;
width: 20px !important;
background: linear-gradient(0deg, #2563EB 22%, #7B61FF 73%) !important;
}
// 重置其他可能的默认样式
&:before,
&:after {
display: none !important;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,41 @@
<script>
export default {
props: {
commentCount: {
type: Number,
default: 0,
}
},
data() {
return {
isActive: false,
}
},
methods: {
}
}
</script>
<template>
<div class="box flex flex-col items-center" :style="{background: !isActive && '#FFFFFF'}">
<img :src="isActive ? '/ToolDetail/icon_comment_selected.png' : '/ToolDetail/icon_comment.png'" alt="" />
<span :style="{color: isActive ? '#ffffffcc' : '#64748b'}">{{ commentCount }}</span>
</div>
</template>
<style scoped lang="scss">
.box {
width: 48px;
height: 48px;
border-radius: 12px;
box-shadow: 0 4px 6px 0 #0000000d;
background: $header-backgroungd;
padding: 5px;
font-family: 'Poppins-Regular', serif;
font-size: 12px;
img {
width: 20px;
height: 20px;
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="comment-item-box flex border">
<div class="comment-avatar flex-center">
<img src="/logo/logo-rect.png" alt="" />
</div>
<div class="comment-content">
<div class="comment-author">
{{item.username || ''}}
</div>
<div class="date-wrap flex-top-left">
<img src="/ToolDetail/icon_clock1.png" alt="" style="width: 16px; height: 16px;" />
<span style="line-height: 18px">{{ item.createdAt || '' }}</span>
</div>
<p class="comment-text">
{{ item.content || '' }}
</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
props: {
item: {
type: Object,
default: () => {
return {}
}
}
},
methods: {
}
}
</script>
<style scoped lang="scss">
.comment-item-box {
gap: 15px;
padding: 30px 0;
.comment-avatar {
width: 36px;
height: 36px;
@include gradient-circle-border($linear-gradient-start, $linear-gradient-end);
img {
width: 24px;
height: 24px;
}
}
.comment-content {
.comment-author {
font-size: 18px;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
color: #1E293B;
line-height: 27px;
height: 27px;
}
.date-wrap {
gap: 8px;
font-family: 'Poppins-Regular', serif;
font-size: 14px;
color: #869EC2;
margin-top: 3px;
}
.comment-text {
margin-bottom: 0;
margin-top: 13px;
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
}
}
.border {
border-bottom: 2px solid #f3f8fe;
}
</style>

View File

@ -0,0 +1,96 @@
<script>
export default {
data() {
return {
category_slug: '',
};
},
props: {
config: {
type: Object,
default: () => {
return {}
}
},
},
methods: {
goToToolDetail() {
if (this.config.slug && this.category_slug) {
this.$router.push('/detail?tool_slug=' + this.config.slug + '&category_slug=' + this.category_slug);
}
}
},
mounted() {
this.category_slug = this.$route.query.category_slug;
},
watch: {
'$route': function (newVal) {
this.category_slug = newVal.query.category_slug;
},
}
}
</script>
<template>
<div class="similar-card-container" @click="goToToolDetail">
<div class="title">
<img :src="config.iconUrl || ''" alt="" />
<span style="font-size: 18px">
{{ config.name || '' }}
</span>
</div>
<div class="text">
{{ config.memo || '' }}
</div>
</div>
</template>
<style scoped lang="scss">
.similar-card-container {
background: #FFFFFF;
box-shadow: 0 10px 30px 0 #0000000d;
border-radius: 8px;
padding: 16px 16px 10px;
border: 1px solid #BACFFF;
max-width: 305px;
cursor: pointer;
&:hover {
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
&:active {
opacity: 0.8;
}
.title {
display: flex;
align-items: center;
img {
width: 40px;
height: 40px;
margin-right: 4px;
}
span {
color: $main-font-color;
font-size: $big-font-size;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.text {
color: $grey-color;
font-family: 'Poppins-Regular', serif;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,97 @@
<script>
export default {
props: {
likeCount: {
type: Number,
default: 0
},
id: {
type: Number,
default: 0,
},
type: {
type: String,
default: 'tool',
},
},
data() {
return {
isActive: false,
throttleTimer: null,
}
},
methods: {
// 添加点赞
async addLike() {
// 节流控制 - 如果已有定时器则阻止执行
if (this.throttleTimer) {
return false;
}
// 设置节流定时器
this.throttleTimer = setTimeout(() => {
this.throttleTimer = null;
}, 3000);
if (!this.id) {
// 清除定时器
clearTimeout(this.throttleTimer);
this.throttleTimer = null;
return false;
}
if (this.type === 'tool') {
const {data: res} = await this.$api.tool.clickToolLike(this.id);
const {code} = res;
if (code === 0) {
this.$message.success('Like successful');
this.$emit('like-success');
} else {
// 请求失败时清除定时器,允许重新点击
clearTimeout(this.throttleTimer);
this.throttleTimer = null;
}
} else {
const {data: res} = await this.$api.article.clickArticleLike(this.id);
const {code} = res;
if (code === 0) {
this.$message.success('Like successful');
this.$emit('like-success');
} else {
// 请求失败时清除定时器,允许重新点击
clearTimeout(this.throttleTimer);
this.throttleTimer = null;
}
}
}
}
}
</script>
<template>
<div class="box flex flex-col items-center" :style="{background: !isActive && '#FFFFFF'}" @click="addLike">
<img :src="isActive ? '/ToolDetail/icon_thumb_selected.png' : '/ToolDetail/icon_thumb.png'" alt="" />
<span :style="{color: isActive ? '#ffffffcc' : '#64748b'}">{{ likeCount }}</span>
</div>
</template>
<style scoped lang="scss">
.box {
width: 48px;
height: 48px;
border-radius: 12px;
box-shadow: 0 4px 6px 0 #0000000d;
background: $header-backgroungd;
padding: 5px;
font-family: 'Poppins-Regular', serif;
font-size: 12px;
cursor: pointer;
img {
width: 20px;
height: 20px;
}
&:active {
opacity: 0.8;
}
}
</style>

343
pages/ToolDetail/index.vue Normal file
View File

@ -0,0 +1,343 @@
<template>
<div id="normal-container" class="tool-detail">
<IntegratedLayout>
<div class="content">
<!--面包屑-->
<div class="bread-menu">
<span>Home</span>
<i class="el-icon-arrow-right"></i>
<span>{{ category_slug }}</span>
<i class="el-icon-arrow-right"></i>
<span class="crumbs gradient-color">{{ tool_detail.name || '' }}</span>
</div>
<!--标题-->
<p class="title-text">{{ tool_detail.name || '' }}</p>
<!--评分-->
<div class="rate-box">
<Rate v-model="tool_detail.rating" readonly />
<div class="flex" style="gap: 20px">
<ThumbBtn
:like-count="tool_detail.likeCount || 0"
:id="tool_detail.id"
type="tool"
@like-success="refreshToolDetail"
/>
<CommentBtn :comment-count="commentCount" />
</div>
</div>
<!--工具内容-->
<div class="tool-content">
<div class="left-content flex flex-col">
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/icon_note.png" alt="">
<span>Introduction: </span>
</div>
<div class="item-content">
{{ tool_detail.memo || '' }}
</div>
</div>
<div class="terms-item">
<div class="item-title">
<img src="/ToolDetail/icon_clock.png" alt="">
<span>Data update: </span>
</div>
<div class="item-content">{{ tool_detail.updatedAt || '' }}</div>
</div>
<div class="tags">
<div class="tag-item" v-for="(it, index) in tagList" :key="index">{{ it }}</div>
</div>
</div>
<div class="right-content">
<div class="card-website-view">
<iframe :src="tool_detail.url || ''" style="width: 100%; height: 100%; pointer-events: none"></iframe>
</div>
<a :href="tool_detail.url || ''" class="link-button">
<img src="/ToolDetail/icon_link.png" alt="" style="width: 16px; height: 16px" />
<span>Visit website</span>
</a>
</div>
</div>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="Product information" name="product">
<Product
:other-tools="other_tools"
:tool-id="tool_detail.id || 0"
:tool-slug="tool_slug || ''"
:category-slug="category_slug || ''"
:tool_content="tool_detail.description || ''"
/>
</el-tab-pane>
<el-tab-pane label="Comment" name="comment"></el-tab-pane>
</el-tabs>
<Comment comment-type="tool" :id="tool_detail.id" @update:commentCount="handleCommentCountUpdate" />
</div>
</IntegratedLayout>
</div>
</template>
<script>
import Product from "@/pages/ToolDetail/Product/index.vue";
import Comment from "@/pages/ToolDetail/Comment/index.vue";
import ThumbBtn from "@/pages/ToolDetail/components/ThumbBtn.vue";
import CommentBtn from "@/pages/ToolDetail/components/CommentBtn.vue";
import Rate from "@/components/Rate.vue";
export default {
components: {
Product,
Comment,
ThumbBtn,
CommentBtn,
Rate,
},
data() {
return {
activeName: 'product',
tool_slug: null,
tool_detail: {},
commentCount: 0,
other_tools: [],
category_slug: null,
}
},
mounted() {
this.tool_slug = this.$route.query.tool_slug;
this.category_slug = this.$route.query.category_slug;
this.getAsyncToolDetailData();
this.getAsyncOtherTools();
},
watch: {
'$route'(to, from) {
// 当路由参数发生变化时重新加载数据
if (to.query.tool_slug !== from.query.tool_slug) {
this.tool_slug = to.query.tool_slug;
this.category_slug = to.query.category_slug;
this.resetAndReloadData();
}
}
},
methods: {
// 刷新工具详情数据
async refreshToolDetail() {
await this.getAsyncToolDetailData();
},
// 获取详情数据
async getAsyncToolDetailData() {
if (this.tool_slug) {
const {data: res} = await this.$api.tool.getToolDetailBySlug(this.tool_slug);
const {code, data} = res;
if (code === 0 && data) {
this.tool_detail = {...data};
}
}
},
stringJsonToObject(str) {
// 将json字符串转为对象捕获错误当str为空或者转对象失败时默认返回空数组
try {
return JSON.parse(str);
} catch (e) {
console.error('Error parsing JSON string:', e);
return [];
}
},
handleCommentCountUpdate(count) {
this.commentCount = count;
},
handleClick() {},
// 添加重置和重新加载数据的方法
resetAndReloadData() {
// 重置数据
this.tool_detail = {};
this.commentCount = 0;
this.other_tools = [];
// 重新加载数据
this.getAsyncToolDetailData();
this.getAsyncOtherTools();
},
// 获取其他工具
async getAsyncOtherTools() {
if (!this.category_slug) {
return false;
}
const params = {categorySlug: this.category_slug, page: 1, limit: 9};
const {data: res} = await this.$api.tool.getToolsList(params);
const {code, data} = res;
if (code === 0 && data.list) {
this.other_tools = data.list;
}
}
},
computed: {
tagList() {
return this.stringJsonToObject(this.tool_detail.tags || '[]');
}
},
}
</script>
<style scoped lang="scss">
.tool-detail {
flex: 1;
overflow-y: auto;
position: relative;
.content {
padding-top: 100px;
padding-bottom: 100px;
min-height: 100vh;
.bread-menu {
font-size: $mid-font-size;
font-family: 'Poppins-Medium', serif;
font-weight: 600;
.crumbs {
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
}
}
.title-text {
font-size: 34px;
color: #1E293B;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
margin-top: 55px;
}
.rate-box {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: space-between;
.btn-wrapper {
display: flex;
align-items: center;
gap: 20px;
}
}
.tool-content {
display: flex;
margin-top: 20px;
margin-bottom: 20px;
gap: 50px;
.left-content {
padding-top: 12px;
gap: 50px;
flex: 1;
.terms-item {
margin-bottom: 30px;
display: flex;
align-items: flex-start;
gap: 7px;
.item-title {
display: flex;
align-items: center;
gap: 8px;
color: #1E293B;
font-family: 'Poppins-Medium', serif;
img {
width: 24px;
height: 24px;
}
}
.item-content {
font-family: 'Poppins-Regular', serif;
color: #64748B;
}
}
.tags {
display: flex;
overflow-x: auto;
gap: 12px;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
flex-wrap: wrap;
width: 80%;
.tag-item {
flex-shrink: 0;
padding: 4px 12px;
border-radius: 12px;
@include gradient-border($linear-gradient-start, $linear-gradient-end);
}
}
}
.right-content {
width: 450px;
.card-website-view {
width: 100%;
height: 268px;
background-color: #FFFFFF;
border-radius: 16px;
box-shadow: 0 10px 30px 0 #0000000d;
padding: 19px 25px;
margin-bottom: 28px;
}
.link-button {
width: 280px;
height: 44px;
background: $header-backgroungd;
border-radius: 12px;
margin: auto;
color: #fff;
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.8;
}
}
}
}
}
:deep(.el-tabs) {
.el-tabs__header {
.el-tabs__nav-wrap::after {
height: 4px !important;
background-color: #E2E8F0 !important;
}
.el-tabs__item {
font-weight: 700 !important;
font-size: 20px !important;
margin-bottom: 14px;
&.is-active {
background: linear-gradient(90deg, $linear-gradient-start, $linear-gradient-end);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
color: transparent;
}
}
.el-tabs__active-bar {
height: 4px !important;
background: linear-gradient(90deg, $linear-gradient-start, $linear-gradient-end) !important;
}
}
}
}
</style>

View File

@ -1,10 +1,12 @@
import homeApi from '~/api/home'
import aboutApi from '~/api/about'
import userApi from '~/api/user'
import aboutApi from '~/api/about';
import toolApi from '@/api/tools';
import CommentApi from '~/api/comment';
import articleApi from '~/api/article';
import configApi from '~/api/config';
/**
* 将api注入到全局
*
*
* 在页面或组件里使用
* async mounted() {
const games = await this.$api.game.getGameList({ page: 1 })
@ -17,9 +19,11 @@ import userApi from '~/api/user'
*/
export default ({ $axios }, inject) => {
const api = {
home: homeApi($axios),
tool: toolApi($axios),
about: aboutApi($axios),
user: userApi($axios),
comment: CommentApi($axios),
article: articleApi($axios),
config: configApi($axios),
}
inject('api', api) // 将api注入到全局

116
router.js
View File

@ -4,34 +4,100 @@ import VueRouter from 'vue-router'
// 引入页面组件
import Home from '@/pages/Home/index.vue'
import AboutIndex from '@/pages/About/index.vue'
import User from '@/pages/User/index.vue'
import Login from '@/pages/Login/index.vue'
// import Login from '@/pages/Login/index.vue'
import Privacy from '@/pages/About/Privacy.vue'
import About from '@/pages/About/About.vue'
import Service from '@/pages/About/Service.vue'
import AIHub from '@/pages/AIHub/index.vue'
import AITools from '@/pages/AIHub/AITools.vue'
import Frameworks from '@/pages/AIHub/Frameworks.vue'
import ViewMore from "@/pages/Home/views/ViewMore.vue";
import List from "@/pages/Home/views/List.vue";
import Detail from "@/pages/ToolDetail/index.vue";
import Launches from "@/pages/Launches/index.vue";
import DailyNews from "@/pages/DailyNews/index.vue";
import AIToolsDetail from "@/pages/AIHub/AIToolsDetail.vue";
import Learn from "@/pages/Learn/index.vue";
import Observer from "@/pages/Learn/Observer.vue";
import Depth from "@/pages/Learn/Depth.vue";
import Pioneer from "@/pages/Learn/Pioneer.vue";
// import NewsDetail from "@/pages/DailyNews/NewsDetailIndex/index.vue";
import LaunchesDetail from "@/pages/Launches/Detail/index.vue";
import FinanceDetail from "@/pages/Launches/FinanceDetail/index.vue";
Vue.use(VueRouter)
export const routes = [{
export const routes = [
{
path: '/',
name: 'home-redirect',
redirect: '/home'
},{
path: '/home',
name: 'home',
component: Home,
redirect: '/home/list',
meta: {
navigationName: "Home",
hidden: false
} //navigationName显示在header组件里面导航按钮的名字 hidden是否在导航栏里面显示
hidden: false,
}, //navigationName显示在header组件里面导航按钮的名字 hidden是否在导航栏里面显示
children: [
{
path: 'list',
name: 'list',
component: List,
meta: {
navigationName: 'List',
hidden: true,
},
},
{
path: 'more',
name: 'more',
component: ViewMore,
meta: {
navigationName: 'View More',
hidden: true,
}
}
]
},
{
path: '/detail',
name: 'Tool Detail',
component: Detail,
meta: {
navigationName: "Tool ToolDetail",
hidden: true,
}
},
{
path: '/launches',
name: 'AI Launches',
component: About,
component: Launches,
meta: {
navigationName: "AI Launches",
hidden: false
}
},
{
path: '/launches/detail',
name: 'AI Launches Detail',
component: LaunchesDetail,
meta: {
navigationName: "AI Launches Detail",
hidden: true,
},
},
{
path: '/finance-detail',
name: 'Finance',
component: FinanceDetail,
meta: {
navigationName: "Finance",
hidden: true,
}
},
{
path: '/hub',
name: 'AI Hub',
@ -58,22 +124,40 @@ export const routes = [{
icon: 'frameworks',
hidden: true
}
}
},
]
},
{
path: '/login',
name: 'AI Daily News',
component: Login,
path: '/tools-detail',
name: 'Tool Detail',
component: AIToolsDetail,
meta: {
navigationName: "AI Daily News",
hidden: false
icon: 'tools',
hidden: true,
}
},
{
path: '/dailyNews',
name: 'AI Daily News',
component: DailyNews,
meta: {
navigationName: "AI Daily News",
hidden: false
},
},
// {
// path: '/news-detail',
// name: 'News Detail',
// component: NewsDetail,
// meta: {
// navigationName: 'News ToolDetail',
// hidden: true
// }
// },
{
path: '/learn',
name: 'Learn',
component: Login,
component: Learn,
meta: {
navigationName: "Learn",
hidden: false,
@ -82,7 +166,7 @@ export const routes = [{
children: [{
name: 'AI Observer',
path: '/observer',
component: Privacy,
component: Observer,
meta: {
icon: 'observer',
hidden: true
@ -91,7 +175,7 @@ export const routes = [{
{
name: 'In-depth Analysis',
path: '/analysis',
component: About,
component: Depth,
meta: {
icon: 'analysis',
hidden: true
@ -100,7 +184,7 @@ export const routes = [{
{
name: 'Pioneer In The Field',
path: '/pioneer',
component: Service,
component: Pioneer,
meta: {
icon: 'pioneer',
hidden: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More