对接数据
This commit is contained in:
71
pages/Launches/Detail/FinanceItem.vue
Normal file
71
pages/Launches/Detail/FinanceItem.vue
Normal 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>
|
||||
52
pages/Launches/Detail/ProjectItem.vue
Normal file
52
pages/Launches/Detail/ProjectItem.vue
Normal 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>
|
||||
63
pages/Launches/Detail/RelatedTool.vue
Normal file
63
pages/Launches/Detail/RelatedTool.vue
Normal 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>
|
||||
382
pages/Launches/Detail/index.vue
Normal file
382
pages/Launches/Detail/index.vue
Normal 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>
|
||||
173
pages/Launches/FinanceDetail/index.vue
Normal file
173
pages/Launches/FinanceDetail/index.vue
Normal 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>
|
||||
46
pages/Launches/components/CustomerPagination.vue
Normal file
46
pages/Launches/components/CustomerPagination.vue
Normal 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>
|
||||
188
pages/Launches/components/ListItem.vue
Normal file
188
pages/Launches/components/ListItem.vue
Normal 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>
|
||||
314
pages/Launches/components/OptionDates.vue
Normal file
314
pages/Launches/components/OptionDates.vue
Normal 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]);
|
||||
},
|
||||
|
||||
// 辅助:获取某月的天数(month:1-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}`;
|
||||
},
|
||||
|
||||
// 辅助:获取某月的首尾日期(month:1-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>
|
||||
59
pages/Launches/components/SwitchDate.vue
Normal file
59
pages/Launches/components/SwitchDate.vue
Normal 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>
|
||||
84
pages/Launches/components/SwitchMonth.vue
Normal file
84
pages/Launches/components/SwitchMonth.vue
Normal 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>
|
||||
47
pages/Launches/components/SwitchSort.vue
Normal file
47
pages/Launches/components/SwitchSort.vue
Normal 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>
|
||||
78
pages/Launches/components/SwitchYear.vue
Normal file
78
pages/Launches/components/SwitchYear.vue
Normal 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
220
pages/Launches/index.vue
Normal 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 what’s 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>
|
||||
Reference in New Issue
Block a user