对接数据

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_BASE_API = http://47.108.139.184:9062 #设置开发环境的api的基础路径
# NUXT_ENV_PUBLIC_PATH = / #设置资源路径前缀 # 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> <IntegratedLayout>
<div class="container"> <div class="container">
<div class="left-container"> <div class="left-container">
<img src="/logo/logo-rect.png" /> <img src="/logo/logo-rect.png" alt="" />
<span>AIProdLaunch</span> <span>AIProdLaunch</span>
</div> </div>
<div class="right-container"> <div class="right-container">
<div> <div>
<img src="/logo/bottom-logo.png" /> <img src="/logo/bottom-logo.png" alt="" />
</div> </div>
<div class="navigation-bottom"> <div class="navigation-bottom">
<span v-for="item in first"> <span v-for="item in first">
@ -25,7 +25,6 @@
</div> </div>
</div> </div>
</div> </div>
</IntegratedLayout> </IntegratedLayout>
</div> </div>
</template> </template>
@ -93,6 +92,7 @@
font-size: $larg-font-size; font-size: $larg-font-size;
color: $main-color; color: $main-color;
font-weight: bold; font-weight: bold;
font-family: 'Poppins-Bold', serif;
} }
} }
@ -102,12 +102,15 @@
.bottom-span { .bottom-span {
color: $grey-color; color: $grey-color;
font-family: 'Poppins-Regular', serif;
} }
.navigation-bottom { .navigation-bottom {
span { span {
display: inline-block; display: inline-block;
padding: 14px; padding: 14px;
font-family: 'Poppins-Medium', serif;
cursor: pointer;
} }
span:last-child { 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> <IntegratedLayout>
<div class="navigation-container"> <div class="navigation-container">
<div class="logo"> <div class="logo">
<img src="/logo/white-logo.png" /> <img src="/logo/white-logo.png" alt="" />
<span>AIProdLaunch</span> <span>AIProdLaunch</span>
</div> </div>
<div class="flex"> <div class="flex">
@ -17,23 +17,21 @@
<div v-if="activeMenu === item.path && item.meta.children" class="submenu"> <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)" <div v-for="sub in item.children" :key="sub.path" @click.stop="goto(sub.path)"
class="submenu-item pointer"> class="submenu-item pointer">
<img :src="`/logo/${sub.meta.icon}.png`" /> <img :src="`/logo/${sub.meta.icon}.png`" alt="" />
{{ sub.name }} {{ sub.name }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</IntegratedLayout> </IntegratedLayout>
</div> </div>
</template> </template>
<script> <script>
import { import {
routes routes
} from '../router'; } from '~/router';
export default { export default {
name: "Header", name: "Header",
@ -66,8 +64,8 @@ export default {
}, },
methods: { methods: {
handleParentClick(item) { handleParentClick(item) {
// 只有没有子菜单时才跳转 const hasVisibleChildren = item.meta && !item.meta.children;
if (!item.children || item.children.length === 0) { if (hasVisibleChildren) {
this.goto(item.path); this.goto(item.path);
} }
}, },
@ -76,7 +74,6 @@ export default {
* @param {String} path 导航的路径 * @param {String} path 导航的路径
*/ */
goto(path) { goto(path) {
console.log(path)
this.$router.push(path) this.$router.push(path)
}, },
showSubmenu(item) { 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; justify-content: flex-end;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin: 20px; margin: 20px 0;
} }
.pagination-btn, .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> <Footer></Footer>
<el-backtop target="#default-layout" :visibility-height="500" class="custom-backtop"> <el-backtop target="#default-layout" :visibility-height="500" class="custom-backtop">
<div class="backtop-content"> <div class="backtop-content" @mousedown="handleMouseDown" @mouseup="handleMouseUp">
<img src="/logo/back-top.png" /> <img src="/logo/back-top.png" alt="" />
</div> </div>
</el-backtop> </el-backtop>
</div> </div>
</template> </template>
<script> <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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
#default-layout { #default-layout {
position: relative; position: relative;
@include flex-column; @include flex-column;
overflow: auto; overflow: auto;
} }
.custom-backtop { .custom-backtop {
position: fixed; position: fixed;
right: 15% !important; right: 15% !important;
bottom: 30% !important; bottom: 30% !important;
z-index: 999; z-index: 999;
transition: transform 0.3s; transition: transform 0.3s;
}
} .backtop-content {
transition: opacity 0.2s ease-in-out;
}
#home-container { #home-container {
width: 100%; width: 100%;
// min-height: 100vh; // min-height: 100vh;
background-color: $background-color; background-color: $background-color;
} }
</style> </style>

View File

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

View File

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

View File

@ -1,125 +1,92 @@
<template> <template>
<div class="tools-container"> <div class="tools-container" v-loading.fullscreen.lock="fullscreenLoading">
<div class="card" v-for="item in 6"> <div class="input">
<div class="left"> <SearchInput v-model="searchText" placeholder="Please enter the key words" @search="handleTextSearch" />
<img src="" />
</div> </div>
<div class="right"> <div class="list">
<div> <ListCardItem v-for="it in articleList" :key="it.id" :item="it" type="tool" />
<div class="title-text">
AI Diagnostic Tool Analyzes 10M+ Medical Images with 99.5% Accuracy
</div> </div>
<div class="content-text"> <div class="pagination-wrapper">
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>
<div>
<Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" /> <Pagination :current-page="currentPage" :total-pages="totalPages" @page-change="handlePageChange" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default { export default {
components: {
ListCardItem,
},
data() { data() {
return { return {
praise_count: 12,
currentPage: 1, currentPage: 1,
totalPages: 10, // 假设总页数为 10 totalPages: 1,
pageSize: 10,
articleList: [],
searchText: '',
total: 0,
fullscreenLoading: false,
}
},
watch: {
total() {
this.calculateTotalPages();
},
pageSize() {
this.calculateTotalPages();
} }
}, },
methods: { 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) { handlePageChange(pageNumber) {
this.currentPage = 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.card { .input {
background: $white;
box-shadow: 0px 10px 30px 0px rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 40px 30px;
display: flex; display: flex;
margin-bottom: 30px; justify-content: flex-end;
gap: 30px; .input-container {
margin-top: 100px;
margin-bottom: 60px;
} }
}
.left { .list {
img {
width: 440px;
height: 220px;
border-radius: 6px;
}
}
.bottom-info,
.right {
display: flex; display: flex;
justify-content: space-between;
}
.right {
flex-direction: column; flex-direction: column;
gap: 30px;
.title-text { margin-bottom: 60px;
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;
}
}
</style> </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> <template>
<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> </div>
</template> </template>
<script> <script>
import ListCardItem from "@/pages/AIHub/components/ListCardItem.vue";
export default { export default {
components: {
ListCardItem,
},
data() { data() {
return { return {
// 数据项 currentPage: 1,
totalPages: 1,
pageSize: 10,
articleList: [],
total: 0,
searchText: '',
fullscreenLoading: false
}
},
watch: {
total() {
this.calculateTotalPages();
},
pageSize() {
this.calculateTotalPages();
} }
}, },
methods: { 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.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> </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,6 +1,7 @@
<template> <template>
<div id="normal-container"> <div id="normal-container">
<IntegratedLayout> <IntegratedLayout>
<div class="content">
<div class="bread-menu"> <div class="bread-menu">
<span>AI Hub</span> <span>AI Hub</span>
<i class="el-icon-arrow-right"></i> <i class="el-icon-arrow-right"></i>
@ -16,13 +17,8 @@
product trends and learn how each tool can be leveraged for work, research, or creative projects. product trends and learn how each tool can be leveraged for work, research, or creative projects.
</div> </div>
</div> </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 /> <nuxt-child />
</div>
</IntegratedLayout> </IntegratedLayout>
</div> </div>
</template> </template>
@ -45,9 +41,17 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.content {
padding-bottom: 100px;
.bread-menu { .bread-menu {
font-size: $mid-font-size; font-size: $mid-font-size;
margin: 100px 0; margin: 100px 0;
font-family: 'Poppins-Medium', serif;
.crumbs {
font-family: 'Poppins-SemiBold', serif;
font-weight: 600;
}
} }
.title { .title {
@ -59,14 +63,5 @@
font-size: $big-font-size; font-size: $big-font-size;
color: $grey-color; color: $grey-color;
} }
.input {
display: flex;
justify-content: flex-end;
.input-container {
margin-top: 100px;
margin-bottom:60px;
}
} }
</style> </style>

View File

@ -4,126 +4,69 @@
<div class="title"> <div class="title">
About AIToolsFinder About AIToolsFinder
</div> </div>
<div class="date"> <div class="little-title">
Last updated April 4, 2024 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> </div>
<div class="content">
<div> 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.
How do we process your information?We process your information to provide, improve, and administer our </div>
Services, communicate with you for security and fraud prevention, and comply with the law. We may also <div class="terms-title">
process your information for other purposes with your consent. We process your information only when we have Heres what youll find with us:
a valid legal reason to do so. Learn more about how we process your information.How do we process your </div>
information?We process your information to provide, improve, and administer our Services, communicate with <div class="terms-item">
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="dot"></div>
other purposes with your consent. We process your information only when we have a valid legal reason to do <div class="item-content">
so. Learn more about how we process your information.How do we process your information?We process your <span class="item-title">AI Tool Discovery </span>
information to provide, improve, and administer our Services, communicate with you for security and fraud <span class="item-text">
prevention, and comply with the law. We may also process your information for other purposes with your Thousands of carefully curated AI tools across writing, design, development, business, and entertainment.
consent. We process your information only when we have a valid legal reason to do so. Learn more about how </span>
we process your information.How do we process your information?We process your information to provide, </div>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply </div>
with the law. We may also process your information for other purposes with your consent. We process your <div class="terms-item">
information only when we have a valid legal reason to do so. Learn more about how we process your <div class="dot"></div>
information.How do we process your information?We process your information to provide, improve, and <div class="item-content">
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We <span class="item-title">Learning Resources </span>
may also process your information for other purposes with your consent. We process your information only <span class="item-text">
when we have a valid legal reason to do so. Learn more about how we process your information.How do we Tutorials, frameworks, and model recommendations to help you quickly level up your AI skills.
process your information?We process your information to provide, improve, and administer our Services, </span>
communicate with you for security and fraud prevention, and comply with the law. We may also process your </div>
information for other purposes with your consent. We process your information only when we have a valid </div>
legal reason to do so. Learn more about how we process your information.How do we process your <div class="terms-item">
information?We process your information to provide, improve, and administer our Services, communicate with <div class="dot"></div>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="item-content">
other purposes with your consent. We process your information only when we have a valid legal reason to do <span class="item-title">Industry Insights </span>
so. Learn more about how we process your information.How do we process your information?We process your <span class="item-text">
information to provide, improve, and administer our Services, communicate with you for security and fraud Stay updated with the latest trends, breakthroughs, and real-world applications in AI.
prevention, and comply with the law. We may also process your information for other purposes with your </span>
consent. We process your information only when we have a valid legal reason to do so. Learn more about how </div>
we process your information.How do we process your information?We process your information to provide, </div>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <div class="terms-item">
with the law. We may also process your information for other purposes with your consent. We process your <div class="dot"></div>
information only when we have a valid legal reason to do so. Learn more about how we process your <div class="item-content">
information.How do we process your information?We process your information to provide, improve, and <span class="item-title">Disclaimer </span>
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We <span class="item-text">
may also process your information for other purposes with your consent. We process your information only 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.
when we have a valid legal reason to do so. Learn more about how we process your information.How do we </span>
process your information?We process your information to provide, improve, and administer our Services, </div>
communicate with you for security and fraud prevention, and comply with the law. We may also process your </div>
information for other purposes with your consent. We process your information only when we have a valid <div class="terms-item">
legal reason to do so. Learn more about how we process your information.How do we process your <div class="dot"></div>
information?We process your information to provide, improve, and administer our Services, communicate with <div class="item-content">
you for security and fraud prevention, and comply with the law. We may also process your information for <span class="item-title">Get in Touch </span>
other purposes with your consent. We process your information only when we have a valid legal reason to do <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>
so. Learn more about how we process your information.How do we process your information?We process your <span class="item-link"> corina@inziqi.com.</span>
information to provide, improve, and administer our Services, communicate with you for security and fraud </div>
prevention, and comply with the law. We may also process your information for other purposes with your </div>
consent. We process your information only when we have a valid legal reason to do so. Learn more about how <div class="bottom-line">
we process your information.How do we process your information?We process your information to provide, <div class="bottom-line-left"></div>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <div class="bottom-text gradient-color">
with the law. We may also process your information for other purposes with your consent. We process your Lets explore the future of AItogether
information only when we have a valid legal reason to do so. Learn more about how we process your </div>
information.How do we process your information?We process your information to provide, improve, and <div class="bottom-line-right"></div>
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> </div>
</div> </div>
</template> </template>
@ -142,5 +85,88 @@
</script> </script>
<style lang="scss" scoped> <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> </style>

View File

@ -1,129 +1,66 @@
<template> <template>
<div class="text-container"> <div class="text-container terms">
<div class="top"> <div class="top">
<div class="title"> <div class="terms-title">
Privacy Policy Privacy Policy
</div> </div>
<div class="date"> <div class="terms-date">
Last updated April 4, 2024 Last updated April 4, 2024
</div> </div>
</div> </div>
<div> <div class="content">
How do we process your information?We process your information to provide, improve, and administer our <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>
Services, communicate with you for security and fraud prevention, and comply with the law. We may also <div class="text text-dot">
process your information for other purposes with your consent. We process your information only when we have Visit our website at <span class="text-link">http://www.futurepedia.io/</span> or any website of ours that links to this privacy notice
a valid legal reason to do so. Learn more about how we process your information.How do we process your </div>
information?We process your information to provide, improve, and administer our Services, communicate with <div class="text text-dot" style="margin-bottom: 30px">Engage with us in other related ways, including any sales, marketing, or events</div>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="title">Questions or concerns?</div>
other purposes with your consent. We process your information only when we have a valid legal reason to do <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>
so. Learn more about how we process your information.How do we process your information?We process your <div class="text-link" style="margin-bottom: 40px">contact@futurepedia.io.</div>
information to provide, improve, and administer our Services, communicate with you for security and fraud <div class="title-dark">SUMMARY OF KEY POINTS</div>
prevention, and comply with the law. We may also process your information for other purposes with your <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>
consent. We process your information only when we have a valid legal reason to do so. Learn more about how <div class="title">What personal information do we process?</div>
we process your information.How do we process your information?We process your information to provide, <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>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <div class="title">Do we process any sensitive personal information? </div>
with the law. We may also process your information for other purposes with your consent. We process your <div class="text" style="margin-bottom: 30px">We do not process sensitive personal information.</div>
information only when we have a valid legal reason to do so. Learn more about how we process your <div class="title">Do we receive any information from third parties?</div>
information.How do we process your information?We process your information to provide, improve, and <div class="text" style="margin-bottom: 30px">We do not receive any information from third parties.</div>
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We <div class="title">How do we process your information?</div>
may also process your information for other purposes with your consent. We process your information only <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>
when we have a valid legal reason to do so. Learn more about how we process your information.How do we <div class="title-dark">TABLE OF CONTENTS</div>
process your information?We process your information to provide, improve, and administer our Services, <div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
communicate with you for security and fraud prevention, and comply with the law. We may also process your <div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
information for other purposes with your consent. We process your information only when we have a valid <div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
legal reason to do so. Learn more about how we process your information.How do we process your <div class="title" style="margin-bottom: 25px">Personal information you disclose to us</div>
information?We process your information to provide, improve, and administer our Services, communicate with <div class="title">In Short:</div>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="text" style="margin-bottom: 30px">We collect personal information that you provide to us.
other purposes with your consent. We process your information only when we have a valid legal reason to do 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.
so. Learn more about how we process your information.How do we process your information?We process your </div>
information to provide, improve, and administer our Services, communicate with you for security and fraud <div class="title">Personal Information Provided by You. </div>
prevention, and comply with the law. We may also process your information for other purposes with your <div class="text" style="margin-bottom: 30px">
consent. We process your information only when we have a valid legal reason to do so. Learn more about how 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:
we process your information.How do we process your information?We process your information to provide, </div>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <div class="text text-dot">
with the law. We may also process your information for other purposes with your consent. We process your names
information only when we have a valid legal reason to do so. Learn more about how we process your </div>
information.How do we process your information?We process your information to provide, improve, and <div class="text text-dot">
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We email addresses
may also process your information for other purposes with your consent. We process your information only </div>
when we have a valid legal reason to do so. Learn more about how we process your information.How do we <div class="text text-dot" style="margin-bottom: 30px">
process your information?We process your information to provide, improve, and administer our Services, job titles
communicate with you for security and fraud prevention, and comply with the law. We may also process your </div>
information for other purposes with your consent. We process your information only when we have a valid <div class="title">Sensitive Information.</div>
legal reason to do so. Learn more about how we process your information.How do we process your <div class="text" style="margin-bottom: 25px">We do not process sensitive information.</div>
information?We process your information to provide, improve, and administer our Services, communicate with <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>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
other purposes with your consent. We process your information only when we have a valid legal reason to do <div class="title">In Short:</div>
so. Learn more about how we process your information.How do we process your information?We process your <div class="text" style="margin-bottom: 30px">
information to provide, improve, and administer our Services, communicate with you for security and fraud 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.
prevention, and comply with the law. We may also process your information for other purposes with your </div>
consent. We process your information only when we have a valid legal reason to do so. Learn more about how <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>
we process your information.How do we process your information?We process your information to provide, <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>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <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>
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> </div>
</div> </div>
</template> </template>
@ -142,5 +79,62 @@
</script> </script>
<style lang="scss" scoped> <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> </style>

View File

@ -1,129 +1,66 @@
<template> <template>
<div class="text-container"> <div class="text-container terms">
<div class="top"> <div class="top">
<div class="title"> <div class="title terms-title">
Terms Of Service Terms Of Service
</div> </div>
<div class="date"> <div class="date terms-date">
Last updated April 4, 2024 Last updated April 4, 2024
</div> </div>
</div> </div>
<div> <div class="content">
How do we process your information?We process your information to provide, improve, and administer our <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>
Services, communicate with you for security and fraud prevention, and comply with the law. We may also <div class="text text-dot">
process your information for other purposes with your consent. We process your information only when we have Visit our website at <span class="text-link">http://www.futurepedia.io/</span> or any website of ours that links to this privacy notice
a valid legal reason to do so. Learn more about how we process your information.How do we process your </div>
information?We process your information to provide, improve, and administer our Services, communicate with <div class="text text-dot" style="margin-bottom: 30px">Engage with us in other related ways, including any sales, marketing, or events</div>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="title">Questions or concerns?</div>
other purposes with your consent. We process your information only when we have a valid legal reason to do <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>
so. Learn more about how we process your information.How do we process your information?We process your <div class="text-link" style="margin-bottom: 40px">contact@futurepedia.io.</div>
information to provide, improve, and administer our Services, communicate with you for security and fraud <div class="title-dark">SUMMARY OF KEY POINTS</div>
prevention, and comply with the law. We may also process your information for other purposes with your <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>
consent. We process your information only when we have a valid legal reason to do so. Learn more about how <div class="title">What personal information do we process?</div>
we process your information.How do we process your information?We process your information to provide, <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>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <div class="title">Do we process any sensitive personal information? </div>
with the law. We may also process your information for other purposes with your consent. We process your <div class="text" style="margin-bottom: 30px">We do not process sensitive personal information.</div>
information only when we have a valid legal reason to do so. Learn more about how we process your <div class="title">Do we receive any information from third parties?</div>
information.How do we process your information?We process your information to provide, improve, and <div class="text" style="margin-bottom: 30px">We do not receive any information from third parties.</div>
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We <div class="title">How do we process your information?</div>
may also process your information for other purposes with your consent. We process your information only <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>
when we have a valid legal reason to do so. Learn more about how we process your information.How do we <div class="title-dark">TABLE OF CONTENTS</div>
process your information?We process your information to provide, improve, and administer our Services, <div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
communicate with you for security and fraud prevention, and comply with the law. We may also process your <div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
information for other purposes with your consent. We process your information only when we have a valid <div class="text-underline">1. WHAT INFORMATION DO WE COLLECT?</div>
legal reason to do so. Learn more about how we process your information.How do we process your <div class="title" style="margin-bottom: 25px">Personal information you disclose to us</div>
information?We process your information to provide, improve, and administer our Services, communicate with <div class="title">In Short:</div>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="text" style="margin-bottom: 30px">We collect personal information that you provide to us.
other purposes with your consent. We process your information only when we have a valid legal reason to do 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.
so. Learn more about how we process your information.How do we process your information?We process your </div>
information to provide, improve, and administer our Services, communicate with you for security and fraud <div class="title">Personal Information Provided by You. </div>
prevention, and comply with the law. We may also process your information for other purposes with your <div class="text" style="margin-bottom: 30px">
consent. We process your information only when we have a valid legal reason to do so. Learn more about how 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:
we process your information.How do we process your information?We process your information to provide, </div>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <div class="text text-dot">
with the law. We may also process your information for other purposes with your consent. We process your names
information only when we have a valid legal reason to do so. Learn more about how we process your </div>
information.How do we process your information?We process your information to provide, improve, and <div class="text text-dot">
administer our Services, communicate with you for security and fraud prevention, and comply with the law. We email addresses
may also process your information for other purposes with your consent. We process your information only </div>
when we have a valid legal reason to do so. Learn more about how we process your information.How do we <div class="text text-dot" style="margin-bottom: 30px">
process your information?We process your information to provide, improve, and administer our Services, job titles
communicate with you for security and fraud prevention, and comply with the law. We may also process your </div>
information for other purposes with your consent. We process your information only when we have a valid <div class="title">Sensitive Information.</div>
legal reason to do so. Learn more about how we process your information.How do we process your <div class="text" style="margin-bottom: 25px">We do not process sensitive information.</div>
information?We process your information to provide, improve, and administer our Services, communicate with <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>
you for security and fraud prevention, and comply with the law. We may also process your information for <div class="text-underline">2. HOW DO WE PROCESS YOUR INFORMATION?</div>
other purposes with your consent. We process your information only when we have a valid legal reason to do <div class="title">In Short:</div>
so. Learn more about how we process your information.How do we process your information?We process your <div class="text" style="margin-bottom: 30px">
information to provide, improve, and administer our Services, communicate with you for security and fraud 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.
prevention, and comply with the law. We may also process your information for other purposes with your </div>
consent. We process your information only when we have a valid legal reason to do so. Learn more about how <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>
we process your information.How do we process your information?We process your information to provide, <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>
improve, and administer our Services, communicate with you for security and fraud prevention, and comply <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>
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> </div>
</div> </div>
</template> </template>
@ -142,5 +79,62 @@
</script> </script>
<style lang="scss" scoped> <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> </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)"> <div v-for="item in list" class="tools" @click="checkTool(item)">
<span class="tool-card" :class="item.active?'checkedBg':''"> <span class="tool-card" :class="item.active?'checkedBg':''">
<span class="content"> <span class="content">
<img :src="`/logo/${item.img}_${item.active ? 'checkd' : 'un'}.png`" /> <img :src="''" alt="" />
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
</span> </span>
</span> </span>
</div> </div>
</div> </div>
</template> </template>
@ -28,9 +26,14 @@
}, },
methods: { methods: {
checkTool(item) { checkTool(item) {
this.list.forEach(i => this.$set(i, 'active', false)) // if (item.active) {
this.$set(item, 'active', true) this.$set(item, 'active', false);
this.$emit('tool-selected', item.name) // } 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 { .tool-card {
background: #FFFFFF; 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; border-radius: 12px;
padding: 10px 16px; padding: 10px 16px;
display: inline-block; display: inline-block;
font-weight: 600;
font-family: 'Poppins-SemiBold', serif;
.content { .content {
@include display-flex; @include display-flex;
img { img {
margin-right: 5px; 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,182 +9,64 @@
Get global AI tools in one stop Get global AI tools in one stop
</div> </div>
<div class="third-text"> <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, videos, audio, programming,
music, design, chatting, etc., and recommends learning platforms, frameworks and models music, design, chatting, etc., and recommends learning platforms, frameworks and models
</div> </div>
<div class="input-container"> <!-- 修改输入框容器 -->
<input type="text" placeholder="Please enter the key words"> <div style="margin: 68px auto 62px">
<i class="el-icon-search gradient-color search-icon pointer"></i> <SearchSelectInput />
</div> </div>
</div> </div>
<div class="card flex"> <div class="card flex">
<div class="left-card card-box"> <div class="left-card card-box">
<el-carousel indicator-position="outside"> <el-carousel :autoplay="false" height="354px" autoplay :interval="8000">
<el-carousel-item v-for="item in 4" :key="item"> <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-item>
</el-carousel> </el-carousel>
</div> </div>
<div class="right-card card-box"> <div class="right-card card-box">
<div class="clearfix"> <PopularToolList />
<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> </div>
</div> <NuxtChild />
</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>
</IntegratedLayout> </IntegratedLayout>
<!-- <el-backtop target=".scroll-container" :bottom="100">
<div>
<img src="/logo/back-top.png" />
</div>
</el-backtop> -->
</div> </div>
</template> </template>
<script> <script>
import ToolList from './ToolList.vue' import PopularToolList from "@/pages/Home/components/PopularToolList.vue";
import Toolbar from './Toolbar.vue' import GlobalLoading from "@/components/GlobalLoading.vue";
export default {
export default {
name: 'Home', name: 'Home',
components: { components: {
ToolList, PopularToolList,
Toolbar GlobalLoading,
},
computed: {
banner() {
const bannerConfig = this.$store.getters.bannerConfig;
if (bannerConfig.home && bannerConfig.home.length > 0) {
return bannerConfig.home;
}
return [];
}
}, },
data() { data() {
return { 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'
}
]
}
}, },
methods: { methods: {
scrollToTool(toolName) { },
const target = document.getElementById(`tool-${toolName}`) mounted() {
if (target) { this.$store.dispatch('getBannerConfig');
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
}
}
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped> #home-page {
#home-page {
flex: 1; flex: 1;
overflow-y: auto; // 必须启用滚动 overflow-y: auto; // 必须启用滚动
position: relative; // 添加相对定位 position: relative; // 添加相对定位
@ -192,114 +74,102 @@
background-size: contain; // 控制图片自适应 background-size: contain; // 控制图片自适应
background-position: top center; background-position: top center;
background-image: url('/logo/mask.png'); background-image: url('/logo/mask.png');
} }
.top-title {
.top-title {
@include flex-center; @include flex-center;
flex-direction: column; flex-direction: column;
font-weight: bold; font-weight: bold;
height: 400px; margin-top: 120px;
margin-top: 180px;
div { div {
text-align: center; text-align: center;
} }
.first-text { .first-text {
line-height: 90px; line-height: 90px;
font-size: $huge-font-size2; font-size: $huge-font-size3;
font-weight: 900;
font-family: 'Poppins-Bold', serif;
} }
.second-text { .second-text {
margin: 18px 0; margin: 18px 0;
font-size: $huge-font-size3; font-family: 'Poppins-Bold', serif;
font-size: $huge-font-size2;
font-weight: 900;
line-height: 75px;
} }
.third-text { .third-text {
width: 716px; width: 716px;
height: 81px;
font-weight: 500; font-weight: 500;
color: $grey-color; color: $grey-color;
font-size: $normal-font-size; font-size: $normal-font-size;
margin-top: 8px;
font-family: 'Poppins-Medium', serif;
.special { .special {
color: $main-color; color: $main-color;
} }
} }
} }
.card {
.card {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; // 4列布局 grid-template-columns: 2fr 1fr; // 4列布局
gap: 20px; // 网格间距 gap: 11px; // 网格间距
margin: 0 auto; margin: 0 auto;
height: 366px;
.card-box { .card-box {
background: $white; background: $white;
box-shadow: 0px 18px 33px 0px rgba(0, 0, 0, 0.05); box-shadow: 0 18px 33px 0 rgba(0, 0, 0, 0.05);
border-radius: 12px; border-radius: 12px;
border: 1px solid #FFFFFF; border: 1px solid #FFFFFF;
} }
.left-card { .left-card {
// min-width: 805px; // 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;
}
}
}
}
} }
.right-card { .right-card {
padding: 20px; padding: 20px;
// min-width: 372px; max-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;
} }
}
</style> </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,6 +1,8 @@
import homeApi from '~/api/home' import aboutApi from '~/api/about';
import aboutApi from '~/api/about' import toolApi from '@/api/tools';
import userApi from '~/api/user' import CommentApi from '~/api/comment';
import articleApi from '~/api/article';
import configApi from '~/api/config';
/** /**
* 将api注入到全局 * 将api注入到全局
@ -17,9 +19,11 @@ import userApi from '~/api/user'
*/ */
export default ({ $axios }, inject) => { export default ({ $axios }, inject) => {
const api = { const api = {
home: homeApi($axios), tool: toolApi($axios),
about: aboutApi($axios), about: aboutApi($axios),
user: userApi($axios), comment: CommentApi($axios),
article: articleApi($axios),
config: configApi($axios),
} }
inject('api', api) // 将api注入到全局 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 Home from '@/pages/Home/index.vue'
import AboutIndex from '@/pages/About/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 Privacy from '@/pages/About/Privacy.vue'
import About from '@/pages/About/About.vue' import About from '@/pages/About/About.vue'
import Service from '@/pages/About/Service.vue' import Service from '@/pages/About/Service.vue'
import AIHub from '@/pages/AIHub/index.vue' import AIHub from '@/pages/AIHub/index.vue'
import AITools from '@/pages/AIHub/AITools.vue' import AITools from '@/pages/AIHub/AITools.vue'
import Frameworks from '@/pages/AIHub/Frameworks.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) Vue.use(VueRouter)
export const routes = [{ export const routes = [
{
path: '/', path: '/',
name: 'home-redirect',
redirect: '/home'
},{
path: '/home',
name: 'home', name: 'home',
component: Home, component: Home,
redirect: '/home/list',
meta: { meta: {
navigationName: "Home", navigationName: "Home",
hidden: false hidden: false,
} //navigationName显示在header组件里面导航按钮的名字 hidden是否在导航栏里面显示 }, //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', path: '/launches',
name: 'AI Launches', name: 'AI Launches',
component: About, component: Launches,
meta: { meta: {
navigationName: "AI Launches", navigationName: "AI Launches",
hidden: false 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', path: '/hub',
name: 'AI Hub', name: 'AI Hub',
@ -58,22 +124,40 @@ export const routes = [{
icon: 'frameworks', icon: 'frameworks',
hidden: true hidden: true
} }
} },
] ]
}, },
{ {
path: '/login', path: '/tools-detail',
name: 'AI Daily News', name: 'Tool Detail',
component: Login, component: AIToolsDetail,
meta: { meta: {
navigationName: "AI Daily News", icon: 'tools',
hidden: false 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', path: '/learn',
name: 'Learn', name: 'Learn',
component: Login, component: Learn,
meta: { meta: {
navigationName: "Learn", navigationName: "Learn",
hidden: false, hidden: false,
@ -82,7 +166,7 @@ export const routes = [{
children: [{ children: [{
name: 'AI Observer', name: 'AI Observer',
path: '/observer', path: '/observer',
component: Privacy, component: Observer,
meta: { meta: {
icon: 'observer', icon: 'observer',
hidden: true hidden: true
@ -91,7 +175,7 @@ export const routes = [{
{ {
name: 'In-depth Analysis', name: 'In-depth Analysis',
path: '/analysis', path: '/analysis',
component: About, component: Depth,
meta: { meta: {
icon: 'analysis', icon: 'analysis',
hidden: true hidden: true
@ -100,7 +184,7 @@ export const routes = [{
{ {
name: 'Pioneer In The Field', name: 'Pioneer In The Field',
path: '/pioneer', path: '/pioneer',
component: Service, component: Pioneer,
meta: { meta: {
icon: 'pioneer', icon: 'pioneer',
hidden: true 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