对接数据

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

@ -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>