对接数据

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

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

View File

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

View File

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

View File

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

View File

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

62
components/Rate.vue Normal file
View File

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

View File

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

View File

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