对接数据
This commit is contained in:
329
pages/ToolDetail/Comment/index.vue
Normal file
329
pages/ToolDetail/Comment/index.vue
Normal 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>
|
||||
267
pages/ToolDetail/Product/index.vue
Normal file
267
pages/ToolDetail/Product/index.vue
Normal 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>
|
||||
41
pages/ToolDetail/components/CommentBtn.vue
Normal file
41
pages/ToolDetail/components/CommentBtn.vue
Normal 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>
|
||||
80
pages/ToolDetail/components/CommentItem.vue
Normal file
80
pages/ToolDetail/components/CommentItem.vue
Normal 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>
|
||||
96
pages/ToolDetail/components/SimilarToolCard.vue
Normal file
96
pages/ToolDetail/components/SimilarToolCard.vue
Normal 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>
|
||||
97
pages/ToolDetail/components/ThumbBtn.vue
Normal file
97
pages/ToolDetail/components/ThumbBtn.vue
Normal 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
343
pages/ToolDetail/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user