Refactor code structure and remove redundant changes

This commit is contained in:
wangqiao
2025-08-15 16:45:15 +08:00
commit 99df1d1f81
220 changed files with 33086 additions and 0 deletions

View File

@ -0,0 +1,201 @@
<template>
<div class="channel-header">
<div class="header-container">
<!-- Logo and Title Section -->
<div class="logo-title-section">
<div class="logo">
<img :src="lunTanRes.channelIcon" alt="JRS Logo" />
</div>
<div class="title-section">
<h1 class="main-title">#{{ lunTanRes.channelTitle }}</h1>
<div class="action-buttons">
<el-button v-if="!lunTanRes.isFollow" type="danger" class="subscribe-btn" @click="handleFollow"
><el-icon class="mr-4px color-#fff!"><Plus /></el-icon> 关注
</el-button>
<el-button v-else type="danger" class="subscribe-btn" @click="handleUnfollow"> 取消关注 </el-button>
<el-button type="danger" class="post-btn" @click="handleClick">
<el-icon class="mr-4px color-#fff!"><EditPen /></el-icon> 发帖
</el-button>
</div>
<!-- Channel Info -->
<div class="channel-info">
<span class="info-item">话题介绍</span>
<span class="info-item">{{ lunTanRes.channelProfile }}</span>
</div>
<!-- Stats -->
<div class="channel-stats">
<div class="stats-item">
<span class="stats-label">话题热度</span>
<span class="stats-value"><i class="el-icon-arrow-up"></i> 220.4w</span>
</div>
<div class="stats-item">
<span class="stats-label">关注人数</span>
<span class="stats-value"><i class="el-icon-arrow-up"></i> {{ lunTanRes.followCount }}</span>
</div>
<div class="stats-item">
<span class="stats-label">当前有</span>
<span class="stats-value"><i class="el-icon-arrow-up"></i> {{ lunTanRes.chatUserCount }}人聊天</span>
<span class="stats-value ml-2px cursor-pointer color-#1a65ff!" @click="handleChat">立即加入</span>
</div>
</div>
<!-- Tags -->
<div class="channel-tags">
<span class="tag-label">标签:</span>
<span v-for="(item, index) in lunTanRes.hotTags" :key="index" class="tag-item"
>{{ item }}{{ index === lunTanRes.hotTags.length - 1 ? '' : '、' }}</span
>
</div>
</div>
</div>
</div>
<!-- 打开群聊窗口 -->
<ChatPage
v-if="isChat"
v-model:is-chat="isChat"
:chat-title="lunTanRes.channelTitle"
:chat-description="lunTanRes.channelProfile"
:channel-id="lunTanRes.channelId"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Plus, EditPen } from '@element-plus/icons-vue'
import { ChannelRespVO } from '@/api/channel/types'
import { createChannelFollow, deleteChannelFollow } from '@/api/channel/index'
import ChatPage from '@/pages/chat-page/index.vue'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const lunTanRes = defineModel<ChannelRespVO>('modelValue', {
required: true,
})
const handleClick = () => {
window.open('/channel/create?channelId=' + lunTanRes.value.channelId, '_blank')
}
const handleFollow = () => {
createChannelFollow({ channelId: lunTanRes.value.channelId }).then((res) => {
if (res.code === 0) {
lunTanRes.value.isFollow = true
ElMessage.success('关注成功')
}
})
}
const handleUnfollow = () => {
deleteChannelFollow({ channelId: lunTanRes.value.channelId }).then((res) => {
if (res.code === 0) {
lunTanRes.value.isFollow = false
ElMessage.success('取消关注成功')
}
})
}
// 订阅群聊
const isChat = ref(false)
const handleChat = async () => {
// 登录判断
if (!userStore.token) {
ElMessage.warning('请先登录')
return
}
await userStore.mqttClient?.subscribe(`zbjk_message_group/${lunTanRes.value.channelId}`)
isChat.value = true
}
</script>
<style scoped>
.channel-header {
background-color: #fff;
border-radius: 8px;
padding: 16px;
/* box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); */
border: 1px solid #eeeeee;
margin-bottom: 16px;
}
.header-container {
display: flex;
}
.logo-title-section {
display: flex;
align-items: flex-start;
}
.logo {
width: 80px;
height: 80px;
margin-right: 16px;
}
.logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.title-section {
flex: 1;
}
.main-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin: 0 0 12px 0;
}
.action-buttons {
display: flex;
gap: 4px;
margin-bottom: 12px;
}
.subscribe-btn,
.post-btn {
font-size: 14px;
padding: 0px 12px !important;
}
.channel-info {
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.channel-stats {
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.stats-value {
color: #333;
font-weight: 500;
}
.channel-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
font-size: 14px;
color: #666;
}
.tag-label {
font-weight: 500;
}
.tag-item {
color: #1a65ff;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<div class="box-border w-320px border border-[#EEEEEE] rounded-8px border-solid bg-[#FFFFFF] px-23px py-25px">
<div class="text-16px text-[#333333] font-normal">频道列表</div>
<div class="mt-30px">
<el-row>
<el-col v-for="(item, index) in channelIdList" :key="index" :span="8" class="mb-10px">
<span
class="cursor-pointer text-14px text-[#999999] font-normal"
:class="{ active: channelId === item.channelId }"
@click="handleClick(item.channelId)"
>{{ item.channelTitle }}</span
>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { list } from '@/api/channel/index'
const channelId = defineModel('modelValue', {
required: true,
})
/** 获取频道列表 */
const channelIdList = ref<any>([])
const getChannelIdList = () => {
list().then((res) => {
channelIdList.value = res.data
if (channelIdList.value.length > 0) {
channelId.value = channelIdList.value[0].channelId
}
})
}
getChannelIdList()
const handleClick = (id: number) => {
console.log(id)
channelId.value = id
}
</script>
<style scoped lang="scss">
.active {
color: #1a65ff !important;
}
</style>

View File

@ -0,0 +1,18 @@
<template>
<!-- 用户信息 -->
<div class="flex flex-col">
<UserInfo></UserInfo>
<HotLlabel v-model="channelId" class="mt-18px"></HotLlabel>
</div>
</template>
<script setup lang="ts">
import UserInfo from './UserInfo.vue'
import HotLlabel from './HotLlabel.vue'
const channelId = defineModel('modelValue', {
required: true,
})
</script>
<style scoped></style>

View File

@ -0,0 +1,80 @@
<template>
<div class="ml-19px w-100%">
<ChannelHeader v-if="Object.keys(lunTanRes).length" v-model="lunTanRes"></ChannelHeader>
<div class="mb-13px box-border flex flex-1 flex-col cursor-pointer gap-12px border border-[#EEEEEE] rounded-8px border-solid bg-[#FFFFFF] px-20px py-16px">
<div
v-for="(item, index) in pageRes.list"
:key="index"
class="flex justify-between border-b-1px border-b-[#eee] border-b-solid pb-8px"
:class="{ 'border-b-0! pb-0px!': index === pageRes.list.length - 1 }"
@click="handleClick(item.postsId)"
>
<div class="flex flex-1 items-center">
<div class="ellipsis max-w-70% text-15px text-[#2d3137] font-normal">{{ item.postsTitle }}</div>
<span class="ml-10px flex-shrink-0 text-13px color-#96999f">{{ item.likeNum || 0 }}人赞过</span>
<span class="ml-10px flex-shrink-0 text-13px color-#96999f">{{ item.commentNum || 0 }}评论</span>
<span class="ml-10px flex-shrink-0 text-13px color-#96999f">{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}发布</span>
</div>
<div class="w-100px flex flex-shrink-0 items-center justify-end">
<span class="ellipsis text-13px color-#96999f">{{ item.creatorName }}</span>
<!-- 删除 -->
<el-button v-if="false" type="danger" size="small" @click="handleDelete(item.postsId)">删除</el-button>
</div>
</div>
</div>
<el-pagination
v-if="pageRes.list.length > 0"
v-model:current-page="pageNo"
:page-size="10"
layout="prev, pager, next"
:total="pageRes.total"
@current-change="handleCurrentChange"
/>
<!-- 暂无数据 -->
<el-empty v-if="!pageRes.list.length" description="暂无数据"></el-empty>
</div>
</template>
<script lang="ts" setup>
import { TpageRes, ChannelRespVO } from '@/api/channel/types'
import { postsDelete } from '@/api/channel/index'
import ChannelHeader from './ChannelHeader.vue'
import dayjs from 'dayjs'
const emit = defineEmits(['updatePageNo'])
const pageRes = defineModel<TpageRes>('modelValue', {
required: true,
})
const lunTanRes = defineModel<ChannelRespVO>('lunTanRes', {
required: true,
})
const pageNo = defineModel<number>('pageNo', {
required: true,
})
// const handleTags = (tags: string) => {
// if (!tags) return []
// return tags.split(',')
// }
const handleCurrentChange = (current: number) => {
emit('updatePageNo', current)
}
const handleDelete = (channelId: number) => {
console.log(channelId)
postsDelete({
id: channelId,
}).then((res) => {
console.log(res)
})
}
const handleClick = (channelId: number) => {
// 新开窗口
window.open(`/chat-detail?channelId=${channelId}`, '_blank')
}
</script>
<style scoped></style>

View File

@ -0,0 +1,49 @@
<template>
<div class="box-border h-240px w-320px border border-[#EEEEEE] rounded-8px border-solid bg-[#FFFFFF] px-31px py-25px">
<div class="flex">
<div>
<img src="@/assets/images/user2.png" alt="" srcset="" class="h-47px w-48px rounded-full" />
</div>
<div class="ml-13px">
<div class="text-16px text-[#333333] font-normal">你好</div>
<div class="mt-6px text-14px text-[#999999] font-normal">欢迎使用多多图纸~</div>
</div>
</div>
<div class="mt-26px flex justify-between text-14px text-[#999999] font-normal">
<div class="flex flex-col items-center">
<div>下载</div>
<div class="mt-4px">-</div>
</div>
<div class="flex flex-col items-center">
<div>查看</div>
<div class="mt-4px">-</div>
</div>
<div class="flex flex-col items-center">
<div>点赞</div>
<div class="mt-4px">-</div>
</div>
<div class="flex flex-col items-center">
<div>评论</div>
<div class="mt-4px">-</div>
</div>
</div>
<div
class="mt-25px h-36px w-260px cursor-pointer rounded-4px bg-[#1A65FF] text-center text-15px text-[#FFFFFF] font-normal line-height-36px"
@click="handleClick"
>发帖子</div
>
</div>
</template>
<script setup lang="ts">
import useUserStore from '@/store/user'
const userStore = useUserStore()
const handleClick = () => {
// 判断是否登录
if (!userStore.token) {
ElMessage.warning('请先登录')
return
}
window.open('/channel/create', '_blank')
}
</script>

View File

@ -0,0 +1,367 @@
<template>
<KlNavTab></KlNavTab>
<div class="mx-auto w-1440px">
<!-- 使用 el-form 重构表单区域 -->
<el-form ref="formRef" inline :model="formData" label-width="110px" class="custom-form mb-20px mt-20px border rounded p-20px!">
<el-form-item label="标题:" prop="postsTitle" :rules="{ required: true, message: '请输入标题', trigger: 'blur' }">
<el-input v-model="formData.postsTitle" placeholder="请输入标题" class="w-300px!" minlength="4" maxlength="40"></el-input>
</el-form-item>
<el-form-item label="分类:" class="mb-10px" prop="projectDicId" :rules="{ required: true, message: '请选择分类', trigger: ['blur', 'change'] }">
<el-select v-model="formData.projectDicId" placeholder="请选择分类" class="w-300px!">
<el-option v-for="item in projectTypeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="标签:" class="mb-10px" prop="postsTags" :rules="{ required: true, message: '请输入标签', trigger: ['blur', 'change'] }">
<el-select
v-model="formData.postsTags"
:remote-method="remoteMethod"
:loading="loading"
filterable
remote
multiple
placeholder="请输入搜索标签"
class="w-300px!"
>
<el-option v-for="(item, index) in labelsList" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="频道列表:" class="mb-10px" prop="channelId" :rules="{ required: true, message: '请选择频道', trigger: ['blur', 'change'] }">
<el-select v-model="formData.channelId" placeholder="请选择频道" class="w-300px!">
<el-option v-for="item in channelIdList" :key="item.channelId" :label="item.channelTitle" :value="item.channelId" />
</el-select>
</el-form-item>
<!-- <el-form-item label="上传封面:" prop="postsCover" :rules="{ required: true, message: '请上传封面', trigger: ['blur', 'change'] }">
<KlUploader v-model:file-list="formData.postsCover" :limit="1" :size="1" tips="上传图片支持jpg/gif/png格式"> </KlUploader>
</el-form-item> -->
</el-form>
<Editor :id="tinymceId" v-model="myValue" :init="init" :disabled="disabled" :placeholder="placeholder" />
<!-- 按钮区域 -->
<div class="mt-20px flex justify-end">
<el-button :loading="post_loading" class="mr-10px" @click="previewContent">预览</el-button>
<el-button :loading="post_loading" type="primary" @click="saveContent">发表</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { keywords } from '@/api/upnew/index'
import { reactive, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
import { create, list } from '@/api/channel/index'
import { parent } from '@/api/upnew/index'
import { upload } from '@/api/common/index' // 自定义上传方法
import Editor from '@tinymce/tinymce-vue'
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/themes/silver/theme'
import 'tinymce/models/dom'
import 'tinymce/icons/default'
import 'tinymce/icons/default/icons'
// 引入编辑器插件
import 'tinymce/plugins/code' //编辑源码
import 'tinymce/plugins/image' //插入编辑图片
import 'tinymce/plugins/media' //插入视频
import 'tinymce/plugins/link' //超链接
import 'tinymce/plugins/preview' //预览
// import 'tinymce/plugins/template' //模板
import 'tinymce/plugins/table' //表格
import 'tinymce/plugins/pagebreak' //分页
import 'tinymce/plugins/lists' //列
import 'tinymce/plugins/advlist' //列
import 'tinymce/plugins/quickbars' //快速工具条
import 'tinymce/plugins/wordcount' // 字数统计插件
// import '@/assets/tinymce/langs/zh-Hans.js' //下载后的语言包
// import 'tinymce/skins/content/default/content.css'
// 获取从其他地方传过来的参数
const channelId = route.query.channelId as string
const props = defineProps({
value: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '请输入帖子内容',
},
height: {
type: Number,
default: 500,
},
disabled: {
type: Boolean,
default: false,
},
plugins: {
type: [String, Array],
default: 'code image link preview table quickbars pagebreak lists advlist',
},
toolbar: {
type: [String, Array],
default:
'undo redo codesample bold italic underline strikethrough link alignleft aligncenter alignright alignjustify \
bullist numlist outdent indent removeformat forecolor backcolor |formatselect fontselect fontsizeselect | \
blocks fontfamily fontsize pagebreak lists image customvideoupload table preview | code selectall',
},
templates: {
type: Array,
default: () => [],
},
options: {
type: Object,
default: () => ({}),
},
})
//用于接收外部传递进来的富文本
const myValue = ref(props.value)
const tinymceId = ref('vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + ''))
const init = reactive({
selector: '#' + tinymceId.value, //富文本编辑器的id,
language_url: '/tinymce/langs/zh_CN.js', // 语言包的路径具体路径看自己的项目文档后面附上中文js文件
language: 'zh-Hans', //语言
skin_url: '/tinymce/skins/ui/oxide', // skin路径具体路径看自己的项目
content_css: '/tinymce/skins/content/default/content.css',
menubar: true, //顶部菜单栏显示
statusbar: true, // 底部的状态栏
plugins: props.plugins,
toolbar: props.toolbar,
toolbar_mode: 'sliding',
font_formats: 'Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;', //字体
paste_convert_word_fake_lists: false, // 插入word文档需要该属性
font_size_formats: '12px 14px 16px 18px 22px 24px 36px 72px', //文字大小
height: props.height, //编辑器高度
placeholder: props.placeholder,
branding: false, //是否禁用"Powered by TinyMCE"
promotion: false, //禁用升级按钮
image_dimensions: false, //去除宽高属性
paste_webkit_styles: 'all',
paste_merge_formats: true,
nonbreaking_force_tab: false,
paste_auto_cleanup_on_paste: false,
file_picker_types: 'file',
resize: true,
elementpath: true,
content_style: `img {max-width:100%;} body{background-color: #fff;}`, // 直接自定义可编辑区域的css样式
templates: props.templates,
quickbars_selection_toolbar: 'forecolor backcolor bold italic underline strikethrough link',
quickbars_image_toolbar: 'alignleft aligncenter alignright',
quickbars_insert_toolbar: false,
image_caption: true,
image_advtab: true,
convert_urls: false,
images_upload_url: import.meta.env.VITE_BASE_API,
images_upload_handler: function (blobInfo: any, progress: any) {
console.log(blobInfo, progress)
return new Promise((resolve, reject) => {
const data = new FormData()
data.append('file', blobInfo.blob())
data.append('fieldName', blobInfo.filename())
upload('/prod-api/app-api/infra/file/upload', data)
.then((res) => {
resolve(res.data)
})
.catch(() => {
reject('Image upload failed')
})
})
},
// 添加自定义按钮
setup: function (editor: any) {
// 注册一个新的视频上传按钮
editor.ui.registry.addButton('customvideoupload', {
icon: 'embed', // 使用嵌入媒体图标
tooltip: '上传视频',
onAction: function () {
// 创建文件输入元素
const input = document.createElement('input')
input.setAttribute('type', 'file')
input.setAttribute('accept', 'video/*')
// 处理文件选择事件
input.onchange = function () {
if (input.files && input.files[0]) {
const file = input.files[0]
const data = new FormData()
data.append('file', file)
data.append('fieldName', file.name)
// 可以在这里添加上传进度显示
upload('/prod-api/app-api/infra/file/upload', data)
.then((res) => {
// 插入视频到编辑器
editor.insertContent(`
<video controls width="400">
<source src="${res.data}" type="video/${file.name.split('.').pop()}">
您的浏览器不支持视频标签
</video>
`)
})
.catch((error) => {
console.error('视频上传失败:', error)
// 可以添加错误提示
})
}
}
// 触发文件选择
input.click()
},
})
},
preview_styles: true,
...props.options,
})
// 表单数据
const formData = ref({
projectDicId: undefined,
postsTags: [],
postsCover: [] as any,
postsTitle: '',
channelId: channelId || undefined,
})
const formRef = ref()
const post_loading = ref(false)
const saveContent = () => {
formRef.value.validate().then(() => {
if (!myValue.value) {
ElMessage.error('请输入帖子内容')
return
}
post_loading.value = true
create({
postsTitle: formData.value.postsTitle,
// postsCover: formData.value.postsCover[0].url,
postsTags: formData.value.postsTags.join(','),
postsContent: myValue.value,
projectDicId: formData.value.projectDicId,
channelId: formData.value.channelId,
})
.then((res) => {
console.log(res)
if (res.code === 0) {
ElMessage.success('发表成功')
router.push('/communication/channel')
}
})
.catch((err) => {
console.log(err)
})
.finally(() => {
post_loading.value = false
})
})
}
const previewContent = () => {
// 获取编辑器实例
const editor = tinymce.get(tinymceId.value)
// 调用编辑器的预览命令
if (editor) {
editor.execCommand('mcePreview')
}
}
//在onMounted中初始化编辑器
onMounted(() => {
tinymce.init({})
})
/** 获取频道列表 */
const channelIdList = ref<any>([])
const getChannelIdList = () => {
list().then((res) => {
channelIdList.value = res.data
})
}
getChannelIdList()
const projectTypeList = ref<any>([])
/** 获取分类下拉框 */
const getParent = () => {
parent({
type: 1,
parentId: 0,
}).then((res) => {
projectTypeList.value = res.data
})
}
getParent()
const loading = ref(false)
/** 获取标签 */
const labelsList = ref<any>([])
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
keywords({
type: 1,
keywords: query,
})
.then((res) => {
labelsList.value = res.data
})
.finally(() => {
loading.value = false
})
} else {
labelsList.value = []
}
}
</script>
<style scoped>
.custom-form {
border: 2px solid #eeeeee;
border-radius: 4px;
}
.upload-container {
display: flex;
flex-direction: column;
}
.cover-uploader {
:deep(.el-upload) {
width: fit-content;
}
}
.image-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #909399;
height: 100%;
}
.image-actions {
display: flex;
justify-content: center;
}
:deep(.el-form-item__label) {
font-weight: normal;
}
:deep(.el-button--primary.is-link) {
padding: 0;
height: auto;
font-size: 14px;
}
.text-gray-500 {
color: #999;
}
.text-12px {
font-size: 12px;
}
</style>

62
pages/channel/index.vue Normal file
View File

@ -0,0 +1,62 @@
<template>
<!-- 导航 -->
<KlNavTab active="交流频道" />
<div class="ma-auto mt-30px w-1440px flex">
<LeftContent v-model="pageReq.channelId"></LeftContent>
<RightContent v-model="pageRes" v-model:lun-tan-res="lunTanRes" v-model:page-no="pageReq.pageNo" @update-page-no="handleUpdatePageNo"></RightContent>
</div>
</template>
<script setup lang="ts">
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import LeftContent from './components/LeftContent.vue'
import RightContent from './components/RightContent.vue'
import { page, getChannelLunTanDetail } from '@/api/channel/index.ts'
import { reactive, watch, ref } from 'vue'
import { TpageRes, ChannelRespVO } from '@/api/channel/types'
const pageReq = reactive({
pageNo: 1,
pageSize: 10,
channelId: '', // 频道ID
})
const pageRes = reactive<TpageRes>({
list: [],
total: 0,
})
// 获得频道帖子分页
const getPage = () => {
page(pageReq).then((res) => {
pageRes.list = res.data.list
pageRes.total = res.data.total
})
}
getPage()
const handleUpdatePageNo = (pageNo: number) => {
pageReq.pageNo = pageNo
getPage()
}
// 获取论坛详情
const lunTanRes = ref({} as ChannelRespVO)
const getLunTanDetaiil = (val: string) => {
getChannelLunTanDetail({
id: val,
}).then((res) => {
lunTanRes.value = res.data
})
}
watch(
() => pageReq.channelId,
(val) => {
if (val) {
getPage()
getLunTanDetaiil(val)
}
}
)
</script>
<style lang="scss" scoped></style>

169
pages/chat-detail/index.vue Normal file
View File

@ -0,0 +1,169 @@
<template>
<KlNavTab />
<div class="ml-auto mr-auto mt-20px w1440 flex">
<div class="left box-border w-1019px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-42px py-30px">
<div class="title text-24px text-[#333333] font-bold">{{ channelDetail?.postsTitle }}</div>
<div class="mt-20px flex items-center justify-between border-b-1px border-b-[#eee] border-b-solid pb-14px">
<div class="flex items-center">
<img :src="channelDetail?.creatorAvatar" alt="" srcset="" class="h-50px w-49px rounded-full" />
<div class="ml-10px">
<div class="text-16px text-[#333333] font-normal">{{ channelDetail?.creatorName }}</div>
<div class="text-12px text-[#999999] font-normal">发表时间{{ dayjs(channelDetail?.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
</div>
<div class="mt-19px flex items-center justify-between">
<div class="flex items-center">
<img src="@/assets/images/look.png" alt="" srcset="" class="mr-4px h-17px" />
<span class="text-[#666666]">{{ channelDetail?.likeNum || 0 }}人赞过</span>
<div class="ml-16px flex items-center">
<img src="@/assets/images/add.png" alt="" class="mr-4px h-23px" />
<span class="text-[#666666]">{{ channelDetail?.commentNum || 0 }}评论</span>
</div>
</div>
<div class="ml-16px flex items-center">
<img src="@/assets/images/chat.png" alt="" srcset="" class="mr-4px h-17px" />
<span class="text-[#666666]">{{ channelDetail?.browseNum || 0 }}人看过</span>
</div>
</div>
</div>
<div>
<img :src="channelDetail?.postsCover" alt="" srcset="" class="h-396px w-auto" />
<div class="mt-20px text-14px text-[#333333] font-normal" v-html="channelDetail?.postsContent"></div>
</div>
<div class="mt-30px">
<div class="h-48px w-938px rounded-1px bg-[#F8F8F8] pl-10px text-16px text-[#333333] font-normal line-height-50px"
>共有{{ commentList.total || 0 }}条评论</div
>
<div v-for="item in commentList.list" :key="item.commentId" class="mt-10px border-b-1px border-b-[#eee] border-b-solid pb-14px">
<div class="flex items-center justify-between">
<div class="flex items-center">
<img :src="item.creatorAvatar" alt="" srcset="" class="relative top-12px h-50px w-49px rounded-full" />
<div class="ml-10px">
<span>{{ item.creatorName }}</span>
</div>
</div>
<div class="text-12px text-[#999999] font-normal">发表时间{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
<div class="ml-60px mt--10px box-border rd-4px bg-[#F8F8F8] pa-6px px-8px text-14px text-[#999999] font-normal">{{ item.content }}</div>
</div>
<!-- 添加element-plus分页 -->
<el-pagination
v-model:current-page="query.pageNo"
:page-size="query.pageSize"
layout="prev, pager, next"
:total="commentList.total"
class="mt-10px"
@current-change="handleCurrentChange"
/>
<el-input v-model="commentContent" type="textarea" :rows="6" placeholder="请输入内容" class="mt-20px w-100%"></el-input>
</div>
<div>
<el-button type="primary" class="mt-10px h-40px w-101px rounded-4px text-16px text-[#FFFFFF] font-bold" @click="handleCreateComment"
>发表评论</el-button
>
</div>
</div>
<div class="right ml-23px w-100%">
<div class="mt-20px w-398px border border-[#EEEEEE] border-rd-[10px_10px_0px_0px] border-solid bg-[#FFFFFF]">
<img src="@/assets/images/sign.png" alt="" srcset="" class="h-206px w-100%" />
<div class="box-border border border-[#EEEEEE] border-rd-[10px_10px_0px_0px] border-solid border-t-none bg-[#FFFFFF] pa-18px">
<div class="mt-10px flex items-center">
<div class="text-18px text-[#333333] font-bold">王琦</div>
<div class="ml-10px box-border border border-[#1A65FF] rounded-13px border-solid px-8px py-3px color-#1a65ff">五金工具</div>
<div class="ml-10px box-border border border-[#1A65FF] rounded-13px border-solid px-8px py-3px color-#1a65ff">电子产品</div>
</div>
<div class="mt-10px text-14px text-[#333333] font-normal"
>你好我是专注于工业设计和机械设计领域的专业设计师 擅长进行外观设计结构设计钣金设计等各类工程设计 工作我拥有丰富的经</div
>
<div class="mt-20px h-1px w-336px rounded-1px bg-[#EEEEEE]"></div>
<div class="mt-20px flex items-center">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div>
<span class="ml-10px text-16px">相关内容</span>
</div>
<div class="mt-10px">
<div v-for="item in 10" :key="item" class="flex items-center justify-between py-10px">
<div class="text-16px text-[#333333] font-normal">Microsoft .NET Framework</div>
<span class="color-##999999 ml-10px">11-12</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { watch, ref, reactive } from 'vue'
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import { useRoute } from 'vue-router'
import { getChannelDetail, postscommentpage, createPostsComment } from '@/api/channel'
import type { TGetChannelPostsRes, PageResultPostsCommentRespVO } from '@/api/channel/types'
const route = useRoute()
const channelId = route.query.channelId as string
const channelDetail = ref<TGetChannelPostsRes>()
const commentList = reactive<PageResultPostsCommentRespVO>({
list: [],
total: 0,
})
const getChannel = () => {
getChannelDetail({
id: channelId,
}).then((res) => {
if (res.code === 0) {
channelDetail.value = res.data
}
})
}
const query = reactive({
pageNo: 1,
pageSize: 4,
})
const getComment = () => {
postscommentpage({
postsId: channelId,
pageNo: query.pageNo,
pageSize: query.pageSize,
}).then((res) => {
if (res.code === 0) {
commentList.list = res.data.list
commentList.total = res.data.total
}
})
}
watch(
() => channelId,
(val) => {
if (val) {
getChannel()
getComment()
}
},
{
immediate: true,
}
)
const commentContent = ref('')
const handleCreateComment = () => {
if (!commentContent.value) {
ElMessage.error('请输入评论内容')
return
}
createPostsComment({
postsId: channelId,
content: commentContent.value,
}).then((res) => {
if (res.code === 0) {
getComment()
commentContent.value = ''
}
})
}
const handleCurrentChange = (pageNo: number) => {
query.pageNo = pageNo
getComment()
}
</script>

657
pages/chat-page/index.vue Normal file
View File

@ -0,0 +1,657 @@
<template>
<div>
<el-dialog v-model="dialogVisible" lock-scroll :title="chatTitle" width="900px" :before-close="handleClose" destroy-on-close>
<div class="chat-container">
<!-- Messages area -->
<div ref="messageContainer" class="chat-messages">
<div
v-for="(message, index) in messages"
:key="index"
:class="['message-item', message.fromId === userStore.userInfoRes.id ? 'message-right' : 'message-left']"
>
<!-- 左侧消息 -->
<template v-if="message.fromId !== userStore.userInfoRes.id">
<div class="avatar"><el-avatar :size="40" :src="message.fromId === userStore.userInfoRes.id ? userStore.userInfoRes.avatar : userAvatar" /></div>
<div class="message-content left">
<div v-if="message.msgType === 0" class="text whitespace-pre-wrap">{{ message.content }}</div>
<div v-else-if="message.msgType === 1" class="image-container">
<img :src="message.content" alt="Shared image" class="message-image" />
</div>
<div v-else>{{ message.content.split('/').pop() }}</div>
<!-- <div class="time text-left!">{{ message.createTime && formatTime(message.createTime) }}</div> -->
</div>
</template>
<!-- 右侧消息 -->
<template v-else>
<div class="message-content right">
<div v-if="message.msgType === 0" class="text whitespace-pre-wrap">{{ message.content }}</div>
<div v-else-if="message.msgType === 1" class="image-container">
<img :src="message.content" alt="Shared image" class="message-image" />
</div>
<!-- <div class="time color-#fff!">{{ message.createTime && formatTime(message.createTime) }}</div> -->
</div>
<div class="avatar"><el-avatar :size="40" :src="message.fromId === userStore.userInfoRes.id ? userStore.userInfoRes.avatar : userAvatar" /></div>
</template>
</div>
</div>
<!-- Chat sidebar/info panel -->
<div class="chat-sidebar">
<div class="info-section">
<h3>概述</h3>
<p>{{ chatTitle }}</p>
</div>
<div class="info-section">
<h3>主要内容</h3>
<p>{{ chatDescription }}</p>
</div>
<!-- Participants/emoji section -->
<div class="participants-section">
<div class="emoji-grid">
<div v-for="(emoji, index) in emojis" :key="index" class="emoji-item">
<div class="emoji-avatar">
<img :src="emoji.avatar" alt="emoji" />
</div>
<span>{{ emoji.nickname }}</span>
</div>
</div>
<!-- <div class="view-more" @click="showMoreEmojis"> 查看更多 >> </div> -->
</div>
</div>
<!-- Input area -->
<div class="input-container">
<div class="toolbar">
<div ref="emojiButtonRef" class="emoji-button-container">
<button class="emoji-button" @click="toggleEmojiPanel"
><el-icon class="tool-icon emoji-trigger"><Sunrise /></el-icon
></button>
<div v-if="showEmojiPanel" class="emoji-panel">
<div class="emoji-grid-popup">
<div v-for="emoji in commonEmojis" :key="emoji" class="emoji-item-popup" @click="sendEmojiMessage(emoji)">
{{ emoji }}
</div>
</div>
</div>
</div>
<button class="image-button" @click="triggerImageUpload"
><el-icon class="tool-icon"><Picture /></el-icon
></button>
<input ref="imageInputRef" type="file" accept="image/*" style="display: none" @change="handleImageUpload" />
</div>
<div class="input-wrapper">
<textarea
v-model="newMessage"
type="textarea"
:rows="2"
placeholder="输入消息..."
resize="none"
maxlength="255"
@keydown.enter.prevent="(e: any) => (e.shiftKey ? (newMessage += '\n') : sendMessage(0))"
></textarea>
</div>
<button class="send-button" @click="sendMessage(0)">发送</button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { upload } from '@/api/common/index'
import { Picture, Sunrise } from '@element-plus/icons-vue'
import { getGroupMembers } from '@/api/channel/index'
import { msgType, PageResultMessageRespVO, MemberUserRespDTO } from '@/api/channel/types'
import { ref, onMounted, nextTick, watch, onUnmounted } from 'vue'
// import dayjs from 'dayjs'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const props = defineProps({
channelId: {
type: String,
required: true,
},
chatTitle: {
type: String,
required: true,
},
chatDescription: {
type: String,
required: true,
},
})
// 模拟数据
const userAvatar = 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
// 添加图片上传相关的ref
const imageInputRef = ref<HTMLInputElement | null>(null)
// Chat data with types
const chatTitle = ref<string>(props.chatTitle)
const chatDescription = ref<string>(props.chatDescription)
// Messages data with type
const messages = ref<PageResultMessageRespVO['list']>([])
// Emojis/participants with type
const emojis = ref<MemberUserRespDTO[]>([])
// Refs with types
const newMessage = ref<string>('')
const messageContainer = ref()
const dialogVisible = defineModel<boolean>('isChat', { required: true })
// 表情面板状态
const showEmojiPanel = ref<boolean>(false)
// 常用表情列表
const commonEmojis = ref<string[]>([
'😀',
'😃',
'😄',
'😁',
'😆',
'😅',
'😂',
'🤣',
'😊',
'😇',
'🙂',
'🙃',
'😉',
'😌',
'😍',
'🥰',
'😘',
'😗',
'😙',
'😚',
'😋',
'😛',
'😝',
'😜',
'🤪',
'🤨',
'🧐',
'🤓',
'😎',
'🤩',
'🥳',
'😏',
'😒',
'😞',
'😔',
'😟',
'😕',
'🙁',
'☹️',
'😣',
])
// 添加 ref 用于表情按钮容器
const emojiButtonRef = ref<HTMLElement | null>(null)
// Methods with type annotations
const sendMessage = (msgType: msgType = 0): void => {
if (!newMessage.value.trim()) return
const message = {
msgType: msgType,
content: newMessage.value,
toId: props.channelId, // 对方userId
userId: userStore.userInfoRes.id, // 当前用户id
fromId: userStore.userInfoRes.id, // 当前用户id
createTime: new Date().toISOString(), // 当前时间戳
}
userStore.mqttClient?.publish(`zbjk_message_group/${props.channelId}`, JSON.stringify(message))
messages.value.push(message)
newMessage.value = ''
nextTick(() => {
scrollToBottom()
})
}
const scrollToBottom = (): void => {
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
}
}
const handleClose = (done: () => void): void => {
// 取消订阅
userStore.mqttClient?.unsubscribe(`zbjk_message_group/${props.channelId}`)
done()
}
// const formatTime = (time: string | number): string => {
// return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
// }
// 修改点击处理函数
const handleClickOutside = (e: MouseEvent): void => {
if (!emojiButtonRef.value) return
// 如果面板没显示,不需要处理
if (!showEmojiPanel.value) return
// 检查点击是否在表情按钮容器外
if (!emojiButtonRef.value.contains(e.target as Node)) {
showEmojiPanel.value = false
}
}
// 修改切换面板函数
const toggleEmojiPanel = (e: MouseEvent): void => {
e.stopPropagation() // 阻止事件冒泡
showEmojiPanel.value = !showEmojiPanel.value
}
// 修改发送表情消息函数
const sendEmojiMessage = (emoji: string): void => {
newMessage.value += emoji
showEmojiPanel.value = false
}
// 触发图片上传
const triggerImageUpload = (): void => {
if (imageInputRef.value) {
imageInputRef.value.click()
}
}
// 处理图片上传
const handleImageUpload = async (event: Event): Promise<void> => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
const file = files[0]
// 检查文件类型 0:文本 1:图片 2:视频 3:文件
const msgType = file.type.startsWith('image/') ? 1 : file.type.startsWith('video/') ? 2 : 3
const formData = new FormData()
formData.append('fieldName', file?.name)
formData.append('file', file as Blob)
const res = await upload('/prod-api/app-api/infra/file/upload', formData)
if (res.code === 0) {
const imageUrl = res.data
if (msgType === 1) {
// 预加载图片
const img = new Image()
img.src = imageUrl
img.onload = () => {
newMessage.value = imageUrl
sendMessage(msgType)
}
} else {
newMessage.value = imageUrl
sendMessage(msgType)
}
}
// 重置文件输入以允许重复选择同一文件
if (imageInputRef.value) {
imageInputRef.value.value = ''
}
}
}
// 获取群组成员
const getGroupMemberlist = async () => {
const res = await getGroupMembers({ channelId: props.channelId })
if (res.code === 0) {
emojis.value = res.data
}
}
// Lifecycle hooks
onMounted(() => {
scrollToBottom()
document.addEventListener('click', handleClickOutside)
userStore.mqttClient?.onMessage((topic: string, message: any) => {
if (topic.indexOf(`zbjk_message_group`) === -1) return
const msg = JSON.parse(message)
if (msg.userId === userStore.userInfoRes.id) return
console.log('接收消息---------', topic, message)
msg.msgType = Number(msg.msgType)
messages.value.push(msg)
})
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
// Watchers
watch(
() => messages.value,
() => {
nextTick(() => {
scrollToBottom()
})
},
{ deep: true }
)
watch(
() => props.channelId,
(val) => {
if (val) {
getGroupMemberlist()
}
},
{ immediate: true }
)
</script>
<style scoped>
::v-deep(.el-dialog__body) {
padding: 0px !important;
overflow-y: hidden;
}
.chat-container {
display: grid;
grid-template-columns: 1fr 300px;
grid-template-rows: 0px 1fr 160px;
grid-template-areas:
'header header'
'messages sidebar'
'input sidebar';
height: 64vh;
background-color: #f0f2f5;
font-family: Arial, sans-serif;
width: 100%;
margin: 0 auto;
}
.chat-messages {
grid-area: messages;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
border-left: 1px solid #eee;
background-color: #fff;
}
.message-item {
display: flex;
margin-bottom: 15px;
align-items: flex-start;
width: 100%;
}
.message-left {
justify-content: flex-start;
}
.message-right {
justify-content: flex-end;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #eee;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-weight: bold;
flex-shrink: 0;
}
.message-content {
max-width: 60%;
padding: 10px;
margin: 0 10px;
position: relative;
}
.message-content.left {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.message-content.right {
background-color: #1a65ff;
border-radius: 12px;
color: #fff;
}
.nickname {
font-size: 12px;
color: #999;
margin-bottom: 5px;
}
.text {
word-break: break-word;
line-height: 1.4;
font-size: 14px;
}
.time {
font-size: 12px;
color: #999;
margin-top: 5px;
text-align: right;
}
/* 文件消息样式 */
.message-content.file {
background-color: #f8f9fa;
padding: 12px;
border: 1px solid #e9ecef;
}
.chat-sidebar {
grid-area: sidebar;
padding: 20px;
background-color: #f8fafc;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
overflow-y: auto;
}
.info-section {
margin-bottom: 20px;
}
.info-section h3 {
font-size: 16px;
color: #666;
margin-bottom: 10px;
}
.participants-section {
margin-top: 30px;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.emoji-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s;
}
.emoji-item:hover {
transform: scale(1.1);
}
.emoji-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
transition: opacity 0.2s;
}
.emoji-item:hover .emoji-avatar {
opacity: 0.8;
}
.emoji-item span {
font-size: 12px;
color: #888;
margin-top: 5px;
}
.view-more {
text-align: right;
margin-top: 10px;
color: #1976d2;
cursor: pointer;
transition: color 0.2s;
}
.view-more:hover {
color: #1565c0;
text-decoration: underline;
}
.input-container {
grid-area: input;
display: flex;
flex-direction: column;
border-top: 1px solid #eee;
background-color: white;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.toolbar {
display: flex;
padding: 10px;
gap: 10px;
}
.toolbar button {
background: none;
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
}
.input-wrapper {
flex: 1;
display: flex;
padding: 0 10px;
}
textarea {
flex: 1;
border: none;
resize: none;
padding: 10px;
font-size: 14px;
outline: none;
}
.send-button {
margin: 10px;
padding: 5px 20px;
background-color: #1a65ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
align-self: flex-end;
}
.emoji-button-container {
position: relative;
}
.emoji-panel {
position: absolute;
bottom: 100%;
left: 0;
background: white;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 10px;
z-index: 1000;
margin-bottom: 5px;
}
.emoji-grid-popup {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 5px;
width: 320px;
}
.emoji-item-popup {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 20px;
transition: transform 0.2s;
}
.emoji-item-popup:hover {
transform: scale(1.2);
background-color: #f5f5f5;
border-radius: 4px;
}
.toolbar button {
background: none;
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
padding: 5px 10px;
border-radius: 4px;
transition: background-color 0.2s;
}
.toolbar button:hover {
background-color: #f5f5f5;
}
.message-content.file {
background-color: #f8f9fa;
padding: 12px;
border: 1px solid #e9ecef;
}
.message-image {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
cursor: pointer;
}
.image-container {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="teacher-card">
<div class="avatar">
<!-- 这里可以放置教师头像 -->
</div>
<h3>{{ teacher.name }}</h3>
<p class="title">{{ teacher.title }}</p>
<div class="tags">
<span v-for="tag in teacher.tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
teacher: {
type: Object,
required: true,
},
})
</script>
<style scoped>
.teacher-card {
background: #fff;
border-radius: 8px;
padding: 15px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.avatar {
width: 100px;
height: 100px;
background: #eee;
border-radius: 50%;
margin: 0 auto 10px;
}
.tag {
background: #e8f3ff;
color: #4080ff;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin: 0 4px;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="teacher-detail-card">
<div class="header">
<div class="avatar">
<!-- 这里可以放置教师头像 -->
</div>
<div class="info">
<h3>{{ teacher.name }}</h3>
<p class="university">{{ teacher.university }}</p>
</div>
</div>
<div class="stats">
<div class="stat">
<span class="number">{{ teacher.works }}</span>
<span class="label">作品</span>
</div>
<div class="stat">
<span class="number">{{ teacher.students }}</span>
<span class="label">粉丝</span>
</div>
</div>
<div class="tags">
<span v-for="tag in teacher.tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
<p class="description">{{ teacher.description }}</p>
</div>
</template>
<script setup lang="ts">
defineProps({
teacher: {
type: Object,
required: true,
},
})
</script>
<style scoped>
.teacher-detail-card {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.avatar {
width: 60px;
height: 60px;
background: #eee;
border-radius: 50%;
margin-right: 15px;
}
.stats {
display: flex;
gap: 20px;
margin: 15px 0;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
}
.number {
font-weight: bold;
color: #333;
}
.label {
font-size: 12px;
color: #666;
}
.tag {
background: #e8f3ff;
color: #4080ff;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin: 0 4px;
}
.description {
margin-top: 15px;
font-size: 14px;
color: #666;
line-height: 1.5;
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<div class="timeline-section">
<h2 class="section-title">
<i class="timeline-icon"></i>
个人履历
</h2>
<div class="timeline">
<!-- 时间节点 -->
<div class="timeline-items">
<div v-for="(item, index) in timelineData" :key="index" :class="['timeline-item', index % 2 === 0 ? 'top' : 'bottom']">
<div class="content">
<div class="year">{{ item.year }}</div>
<div class="description">{{ item.description }}</div>
</div>
<div class="dot" :class="{ active: index === 0 }"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const timelineData = ref([
{
year: '2020年',
description: '新创设计21号项目设计~某月某空中机',
},
{
year: '2021年',
description: '南京工业大学机器人实验室项目',
},
{
year: '2022年',
description: '新创设计21号项目设计~某月某空中机',
},
{
year: '2023年',
description: '南京工业大学机器人实验室项目',
},
{
year: '2023年',
description: '南京工业大学机器人实验室项目',
},
{
year: '2023年',
description: '南京工业大学机器人实验室项目',
},
// 可以继续添加更多数据...
])
</script>
<style scoped>
.timeline-section {
margin: 30px 0;
padding: 20px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 30px;
font-size: 18px;
color: #333;
}
.timeline {
position: relative;
padding: 40px 0;
}
.timeline-items {
position: relative;
display: grid; /* 改用grid布局 */
grid-template-columns: repeat(4, 1fr); /* 4列 */
gap: 60px 20px; /* 行间距60px列间距20px */
z-index: 2;
}
.timeline-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
min-width: 200px;
}
/* 时间轴横线 - 为每一行创建独立的线 */
.timeline-item::before {
content: '';
position: absolute;
left: -10px;
right: -10px;
top: 50%;
height: 2px;
background-color: #e8f3ff;
z-index: 1;
}
/* 连接相邻项目的横线 */
.timeline-item::after {
content: '';
position: absolute;
left: 50%;
right: -50%;
top: 50%;
height: 2px;
background-color: #e8f3ff;
z-index: 1;
}
/* 最后一个项目不需要连接线 */
.timeline-item:last-child::after {
display: none;
}
.timeline-item.top {
flex-direction: column;
}
.timeline-item.bottom {
flex-direction: column-reverse;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #fff;
border: 2px solid #005be5;
margin: 15px 0;
z-index: 2;
}
.dot.active {
background-color: #005be5;
}
.content {
text-align: center;
background-color: #fff;
padding: 0 10px;
width: 100%;
}
.top .content {
padding-bottom: 20px;
}
.bottom .content {
padding-top: 20px;
}
.year {
font-size: 16px;
color: #005be5;
font-weight: 500;
margin-bottom: 8px;
}
.description {
font-size: 14px;
color: #666;
line-height: 1.5;
}
/* 响应式布局 */
@media screen and (max-width: 1200px) {
.timeline-items {
grid-template-columns: repeat(3, 1fr); /* 3列 */
}
}
@media screen and (max-width: 900px) {
.timeline-items {
grid-template-columns: repeat(2, 1fr); /* 2列 */
}
}
@media screen and (max-width: 600px) {
.timeline-items {
grid-template-columns: 1fr; /* 1列 */
}
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class="work-card">
<div class="work-image">
<img :src="work.image" :alt="work.title" />
</div>
<div class="work-info">
<h3>{{ work.title }}</h3>
<div class="work-stats">
<span>
<i class="icon-view"></i>
{{ work.views }}
</span>
<span>
<i class="icon-like"></i>
{{ work.likes }}
</span>
<span>
<i class="icon-comment"></i>
{{ work.comments }}
</span>
</div>
<div class="work-author">
<img :src="work.authorAvatar" :alt="work.author" />
<span>by {{ work.author }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
work: {
type: Object,
required: true,
},
})
</script>
<style scoped>
.work-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.work-image {
height: 200px;
overflow: hidden;
}
.work-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.work-info {
padding: 15px;
}
.work-stats {
display: flex;
gap: 15px;
color: #666;
font-size: 14px;
margin: 10px 0;
}
.work-author {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.work-author img {
width: 24px;
height: 24px;
border-radius: 50%;
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<KlNavTab active="牛人社区" />
<div class="expert-detail">
<!-- 头部信息区域 -->
<div class="header-section">
<div class="expert-basic-info">
<div class="avatar">
<img :src="expert.avatar" alt="专家头像" />
</div>
<div class="info">
<h1>{{ expert.name }}</h1>
<p class="university">毕业院校{{ expert.university }}</p>
<div class="stats">
<div class="stat-item">
<i class="icon-works"></i>
<span>作品{{ expert.works }}</span>
</div>
<div class="stat-item">
<i class="icon-followers"></i>
<span>粉丝{{ expert.followers }}</span>
</div>
</div>
<div class="tags">
<span v-for="tag in expert.tags" :key="tag" class="tag">{{ tag }}</span>
</div>
</div>
</div>
</div>
<!-- 技术证书区域 -->
<div class="certificate-section">
<h2>技术证书</h2>
<div class="certificate-list">
<div v-for="(cert, index) in certificates" :key="index" class="certificate-item">
<img :src="cert.image" :alt="'证书 ' + (index + 1)" />
</div>
</div>
</div>
<!-- 个人履历 -->
<Timeline />
<!-- 工程师简介 -->
<div class="introduction-section">
<h2>工程师简介</h2>
<p>{{ expert.introduction }}</p>
</div>
<!-- 作品展示 -->
<div class="works-section">
<h2>作品展示</h2>
<div class="works-grid">
<WorkCard v-for="work in works" :key="work.id" :work="work" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import WorkCard from './WorkCard.vue'
import Timeline from './Timeline.vue'
const expert = ref({
name: '王刚',
avatar: '/path/to/avatar.jpg',
university: '重庆工商大学',
works: 568,
followers: 378,
tags: ['五金工具', '电子产品'],
introduction: '你好!我是专注于工业设计和机械设计领域的专业设计师,擅长进行方案设计、结构设计、钣金设计等各类工程设计工作。具有丰富经验...',
})
const certificates = ref([
{ image: '/path/to/cert1.jpg' },
{ image: '/path/to/cert2.jpg' },
{ image: '/path/to/cert3.jpg' },
{ image: '/path/to/cert4.jpg' },
{ image: '/path/to/cert5.jpg' },
{ image: '/path/to/cert6.jpg' },
])
const works = ref([
{
id: 1,
title: '高压细水资灭火推车组建',
image: '/path/to/work1.jpg',
views: 28,
likes: 15,
comments: 5,
author: '王刚',
},
// ... 更多作品
])
</script>
<style scoped>
.expert-detail {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header-section {
background: linear-gradient(to right, #002b6f, #005be5);
color: white;
padding: 40px;
border-radius: 8px;
}
.expert-basic-info {
display: flex;
gap: 30px;
}
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 4px solid rgba(255, 255, 255, 0.2);
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.stats {
display: flex;
gap: 20px;
margin: 15px 0;
}
.tag {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 15px;
margin-right: 10px;
font-size: 14px;
}
.certificate-list {
display: flex;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
}
.certificate-item {
width: 180px;
height: 120px;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
}
.timeline {
position: relative;
margin: 40px 0;
padding-left: 30px;
}
.timeline-item {
position: relative;
padding-bottom: 30px;
}
.time-point {
position: absolute;
left: -30px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #005be5;
border: 2px solid #fff;
}
.works-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
h2 {
margin: 40px 0 20px;
font-size: 24px;
color: #333;
}
</style>

172
pages/community/index.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<KlNavTab active="牛人社区" />
<div class="app-container">
<!-- 顶部 Banner -->
<div class="banner"> </div>
<!-- 生人推荐部分 -->
<section class="teacher-recommend text-center">
<h2>牛人推荐</h2>
<div class="teacher-cards">
<TeacherCard v-for="teacher in teachers" :key="teacher.id" :teacher="teacher" />
</div>
</section>
<!-- 生人列表部分 -->
<section class="teacher-list mt-30px text-center">
<h2>牛人列表</h2>
<div class="teacher-detail-cards">
<TeacherDetailCard v-for="teacher in teacherDetails" :key="teacher.id" :teacher="teacher" />
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import TeacherCard from './components/TeacherCard.vue'
import TeacherDetailCard from './components/TeacherDetailCard.vue'
const teachers = ref([
{
id: 1,
name: '王刚',
title: 'CAD建模技术、非标设备设计',
tags: ['在线工程', '电子产品'],
},
{
id: 1,
name: '王刚',
title: 'CAD建模技术、非标设备设计',
tags: ['在线工程', '电子产品'],
},
{
id: 1,
name: '王刚',
title: 'CAD建模技术、非标设备设计',
tags: ['在线工程', '电子产品'],
},
{
id: 1,
name: '王刚',
title: 'CAD建模技术、非标设备设计',
tags: ['在线工程', '电子产品'],
},
{
id: 1,
name: '王刚',
title: 'CAD建模技术、非标设备设计',
tags: ['在线工程', '电子产品'],
},
// ... 其他教师数据
])
const teacherDetails = ref([
{
id: 1,
name: '王刚',
university: '重庆工商大学',
works: 568,
students: 378,
tags: ['在线工程', '电子产品', '设计方案'],
description: '国家注册分析师设计,结构设计,电气设计等资深工程设计工作,具有丰富的经验...',
},
{
id: 1,
name: '王刚',
university: '重庆工商大学',
works: 568,
students: 378,
tags: ['在线工程', '电子产品', '设计方案'],
description: '国家注册分析师设计,结构设计,电气设计等资深工程设计工作,具有丰富的经验...',
},
{
id: 1,
name: '王刚',
university: '重庆工商大学',
works: 568,
students: 378,
tags: ['在线工程', '电子产品', '设计方案'],
description: '国家注册分析师设计,结构设计,电气设计等资深工程设计工作,具有丰富的经验...',
},
{
id: 1,
name: '王刚',
university: '重庆工商大学',
works: 568,
students: 378,
tags: ['在线工程', '电子产品', '设计方案'],
description: '国家注册分析师设计,结构设计,电气设计等资深工程设计工作,具有丰富的经验...',
},
{
id: 1,
name: '王刚',
university: '重庆工商大学',
works: 568,
students: 378,
tags: ['在线工程', '电子产品', '设计方案'],
description: '国家注册分析师设计,结构设计,电气设计等资深工程设计工作,具有丰富的经验...',
},
// ... 其他详细教师数据
])
</script>
<style scoped>
.app-container {
width: 1440px;
margin: 0 auto;
padding: 20px;
}
.banner {
background-color: #4080ff;
color: white;
padding: 40px;
box-sizing: border-box;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
background-image: url('@/assets/images/community-banner.png');
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
height: 410px;
}
.primary-btn {
background-color: #ffb800;
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
}
.teacher-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
margin-top: 20px;
}
.teacher-detail-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
h2 {
color: #333;
margin: 40px 0 20px;
background-image: url('@/assets/images/community-bg.png');
background-size: 120px 16px;
background-position: 14px 22px;
background-repeat: no-repeat;
height: 50px;
width: 120px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,212 @@
<template>
<div class="box-border h-631px w-1019px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] pa-30px">
<swiper
:style="{
'--swiper-navigation-color': '#666',
'--swiper-pagination-color': '#666',
}"
:space-between="10"
disabled-class=""
:thumbs="{ swiper: thumbsSwiper }"
:modules="modules"
class="mySwiper2"
@swiper="onSwiper"
@slide-change="changeSwiper"
>
<swiper-slide v-for="(item, index) in props.data" :key="index"><el-image fit="cover" :src="item.url" /></swiper-slide>
<div class="swiper-button-prev color-#fff" @click="handlePrev"></div
><!--左箭头如果放置在swiper外面需要自定义样式-->
<div class="swiper-button-next color-#fff" @click="handleNext"></div
><!--右箭头如果放置在swiper外面需要自定义样式-->
</swiper>
</div>
<div class="mb-20px mt-34px flex items-center text-16px text-[#333333] font-normal">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div
><span class="ml-10px">{{ props.type === 1 ? '图纸' : props.type === 2 ? '文本' : '模型' }}</span></div
>
<div class="box-border h-126px w-1019px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] pa-23px">
<swiper
:space-between="10"
:slides-per-view="4"
:free-mode="true"
:watch-slides-progress="true"
:modules="modules"
class="mySwiper"
@swiper="setThumbsSwiper"
>
<swiper-slide v-for="(item, index) in props.data" :key="index">
<div class="Thumbs">
<el-image fit="cover" :src="item.url" />
</div>
</swiper-slide>
</swiper>
</div>
</template>
<script setup lang="ts">
import { ref, PropType, watch, nextTick } from 'vue'
// @ts-ignore
import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/css'
import 'swiper/css/free-mode'
import 'swiper/css/navigation'
import 'swiper/css/thumbs'
// import required modules
// @ts-ignore
import { FreeMode, Navigation, Thumbs } from 'swiper/modules'
const modules = [FreeMode, Navigation, Thumbs]
const props = defineProps({
data: {
type: Array as PropType<any[]>,
default: () => [],
},
type: {
type: Number as PropType<number>,
default: 1,
},
})
const emit = defineEmits(['changeSwiper', 'nextSwiper'])
const thumbsSwiper = ref<any>(null)
const useSwiper = ref<any>(null)
const activeIndex = ref<number>(0)
watch(
() => props.data,
(value) => {
if (value.length) {
nextTick(() => {
const thumb_active = document.querySelector(`.mySwiper .swiper-slide`)
thumb_active?.classList.add('swiper-slide-thumb-active')
})
}
}
)
const setThumbsSwiper = (swiper: any) => {
thumbsSwiper.value = swiper
}
const handlePrev = () => {
if (activeIndex.value === 0 || props.data.length === 0) {
emit('nextSwiper', false)
}
useSwiper.value.slidePrev()
}
const handleNext = () => {
if (activeIndex.value === props.data.length - 1 || props.data.length === 0) {
emit('nextSwiper', true)
}
useSwiper.value.slideNext()
}
const onSwiper = (swiper: any) => {
useSwiper.value = swiper
}
const changeSwiper = (e: any) => {
activeIndex.value = e.activeIndex
emit('changeSwiper', e.activeIndex)
}
</script>
<style scoped>
.swiper-button-disabled {
cursor: pointer !important;
}
.swiper {
width: 100%;
height: 100%;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
/* Center slide text vertically */
display: flex;
justify-content: center;
align-items: center;
}
.swiper-slide img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
}
.swiper {
width: 100%;
height: 100%;
margin-left: auto;
margin-right: auto;
}
.swiper-slide {
background-size: cover;
background-position: center;
}
.mySwiper2 {
height: 100%;
/* height: calc(100vh - 207px); */
/* width: 100%; */
}
.mySwiper {
height: 90px;
box-sizing: border-box;
padding: 6px 6px;
/* position: absolute;
bottom: 0;
left: 0px;
right: 0px !important; */
background: rgba(0, 0, 0, 0.65);
/* z-index: 1000; */
/* width: 100%; */
}
.mySwiper .swiper-slide {
width: 25%;
height: 100%;
opacity: 0.4;
background: rgba(0, 0, 0, 0.65);
cursor: pointer;
}
.mySwiper .swiper-slide-thumb-active {
opacity: 1;
}
.swiper-slide img {
display: block;
/* width: 100%; */
height: 100%;
object-fit: contain;
pointer-events: none;
}
.Thumbs {
position: relative;
height: 100%;
width: auto;
background: rgba(0, 0, 0, 0.65);
}
.Thumbs img {
height: 100%;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,350 @@
<template>
<KlNavTab />
<div class="ml-auto mr-auto mt-20px w1440">
<div class="flex items-center">
<div class="box-border h-60px w-1019px flex items-center justify-between border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-27px py-24px">
<div class="text-20px text-[#333333] font-normal"> {{ detail.title }}</div>
<div class="flex items-center">
<img :src="detail.ownedUserIdInfo?.avatar" alt="" srcset="" class="h-30px w-30px rd-50%" />
<span class="ml-12px color-#999999">by {{ detail.ownedUserIdInfo?.nickName }}</span>
</div>
</div>
<div class="ml-23px flex flex-1 text-18px text-[#FFFFFF] font-normal">
<div class="h-60px w-160px flex cursor-pointer items-center justify-center rounded-8px bg-[#1A65FF]" @click="handleDownload">
<img src="@/assets/images/download.png" alt="" srcset="" class="mr-4px h-22px w-27px" />
{{ detail.points === 0 ? '免费下载' : '立即下载' }}
</div>
<div
v-if="!detail.favoriteId"
class="ml-11px h-60px flex flex-1 cursor-pointer items-center justify-center rounded-8px bg-[#E7B03B]"
@click="handleCollect"
><img src="@/assets/images/collect.png" alt="" srcset="" class="mr-4px h-24px w-24px" /> 收藏</div
>
<div v-else class="ml-11px h-60px flex flex-1 cursor-pointer items-center justify-center rounded-8px bg-[#E7B03B]" @click="handleCollect"
><img src="@/assets/images/wjx2.png" alt="" srcset="" class="mr-4px h-18px w-18px" /> 已收藏</div
>
<div class="ml-11px h-60px flex flex-1 cursor-pointer items-center justify-center rounded-8px bg-[#F56C6C]" @click="handleReport"
><el-icon class="mr-4px mt-4px"><Warning /></el-icon> 举报</div
>
</div>
</div>
</div>
<!-- -->
<div class="ma-auto mt-21px flex">
<div class="w-1019px">
<div>
<ThumBnail :data="detail.coverImages" :type="detail.type" @change-swiper="changeSwiper" @next-swiper="nextSwiper"></ThumBnail>
</div>
<div class="mb-20px mt-34px flex items-center text-16px text-[#333333] font-normal">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div><span class="ml-10px">{{ detail.title }}描述</span></div
>
<div class="box-border min-h-90px w-1019px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] pa-24px text-14px text-[#333333] font-normal">
{{ detail.description }}
</div>
<div id="section1" class="mb-20px mt-34px flex items-center text-16px text-[#333333] font-normal">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div><span class="ml-10px">{{ detail.title }}附件</span></div
>
<div class="box-border w-1019px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] pa-24px">
<div class="border-b-1px border-b-[#eee] border-b-solid p-b-10px">
{{ detail.type === 1 ? '图纸' : detail.type === 2 ? '文本' : '模型' }}中包含的文件
</div>
<div>
<div v-for="item in detail.files" :key="item.id" class="flex items-center justify-between border-b-1px border-b-[#eee] border-b-solid py-10px">
<!-- <img src="@/assets/images/avater.png" alt="" srcset="" class="h-30px w-30px" /> -->
<div>
<span class="ml-10px cursor-pointer" @click="handleDownloadPreview(item)">{{ item.title }}</span>
<span v-if="item.size" class="ml-200px color-#999">{{ item.size || '-' }}</span>
</div>
<el-button
v-if="detail.downloadId || detail.points === 0"
type="text"
tag="a"
target="download"
:href="item.url"
@click="handleDownloadFile(item.url, item.title)"
>
下载
</el-button>
</div>
</div>
</div>
<!-- 关联项目 -->
<div class="mb-20px mt-34px flex items-center text-16px text-[#333333] font-normal">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div
><span class="ml-10px">关联{{ detail.type === 1 ? '图纸' : detail.type === 2 ? '文本' : '模型' }}</span></div
>
<el-row :gutter="20">
<el-col v-for="(item, index) in detail.relationDraws" :key="index" :span="12">
<CardPicture :item-info="item" />
</el-col>
</el-row>
<el-empty v-if="!detail.relationDraws?.length" description="暂无数据"></el-empty>
<!-- 关联模型 -->
<div class="mb-20px mt-34px flex items-center text-16px text-[#333333] font-normal">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div
><span class="ml-10px">相关{{ detail.type === 1 ? '图纸' : detail.type === 2 ? '文本' : '模型' }}推荐</span></div
>
<el-row :gutter="20">
<el-col v-for="(item, index) in relationRecommend" :key="index" :span="12">
<CardPicture :item-info="item" />
</el-col>
</el-row>
<!-- 评论 -->
<CommentSection :relation-id="detail.id" :project-id="detail.projectId" />
</div>
<div class="ml-22px">
<div class="box-border h-269px w-397px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] pa-22px">
<div class="mb-10px">图纸ID: {{ detail.id }}</div>
<div class="mb-10px">文件大小{{ detail.filesInfo?.fileSize || 0 }} </div>
<!-- <div class="mb-10px">图纸版本{{ detail.editionsName }} </div> -->
<div class="mb-10px">图纸格式{{ detail.formatType?.toString() }}</div>
<div class="mb-10px">所需金币{{ detail.points }}金币</div>
<div class="mb-10px">发布时间{{ dayjs(detail.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div class="mb-10px">图纸参数{{ detail.editTypeName }}</div>
<div class="mb-10px">图纸分类{{ detail.projectTypeName }}</div>
<div class="mb-10px">软件分类{{ detail.editionsName }}</div>
</div>
<div class="mt-20px w-398px border border-[#EEEEEE] border-rd-[10px_10px_0px_0px] border-solid bg-[#FFFFFF]">
<img src="@/assets/images/banner.png" alt="" srcset="" class="w-100%" />
<div class="box-border border border-[#EEEEEE] border-rd-[10px_10px_0px_0px] border-solid border-t-none bg-[#FFFFFF] pa-18px">
<div class="flex flex-wrap items-start">
<div v-if="userInfo.nickname" class="mt-10px text-18px text-[#333333] font-bold">{{ userInfo.nickname }}</div>
<div
v-for="item in userInfo.labels"
:key="item"
class="mb-10px ml-10px box-border border border-[#1A65FF] rounded-13px border-solid px-8px py-3px color-#1a65ff"
>{{ item }}</div
>
</div>
<div v-if="userInfo.description" class="mb-20px text-14px text-[#333333] font-normal">{{ userInfo.description }}</div>
<!-- 显示作品 粉丝 荣誉证书 -->
<div class="flex items-center gap-40px">
<div class="flex items-center">
<div class="h-20px">
<img src="@/assets/images/folder.png" alt="works" class="w-80%" />
</div>
<div class="ml-8px mt--4px text-14px text-[#666] font-normal">作品: {{ userInfo.projectCount || 0 }}</div>
</div>
<div class="flex items-center">
<div class="h-20px">
<img src="@/assets/images/user4.png" alt="fans" class="w-80% rounded-full vertical-top" />
</div>
<div class="relative top--3px ml-8px text-14px text-[#666] font-normal">粉丝: {{ userInfo.fansCount || 0 }}</div>
</div>
</div>
<!-- 3个图片一排 超过换下一行 -->
<div v-if="userInfo.files?.length" class="mt-20px flex flex-wrap gap-16px">
<div v-for="i in userInfo.files" :key="i" class="flex-1">
<div
class="box-border h-200px w-full overflow-hidden border border-[#E5E7EB] rounded-8px border-solid from-[#FFFFFF] to-[#F5F7FA] bg-gradient-to-b p-16px"
>
<el-image :src="i.url" fit="cover" alt="" srcset="" class="h-full object-cover" />
</div>
</div>
</div>
<div class="mt-20px h-1px w-336px rounded-1px bg-[#EEEEEE]"></div>
<div class="mt-20px flex items-center">
<div class="h-24px w-4px rounded-1px bg-[#1A65FF]"></div>
<span class="ml-10px text-16px">最新发布</span>
</div>
<div class="mt-10px">
<div
v-for="item in mainWork"
:key="item.id"
class="flex cursor-pointer items-center justify-between px-10px py-10px hover:bg-#f5f5f5"
@click="handleClick(item.id)"
>
<div class="ellipsis text-15px text-[#333333] font-normal">{{ item.title }}</div>
<span class="ml-10px flex-shrink-0 color-#999999">{{ dayjs(item.createTime).format('MM-DD') }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { downloadFile } from '@/utils/utils'
import { useMessage } from '@/utils/useMessage'
import { Warning } from '@element-plus/icons-vue'
import CardPicture from '@/components/kl-card-picture/index.vue'
import { getDetail, getRelationRecommend, report, getUserInfo, getMainWork, createContent, createUserProject, deleteProject } from '@/api/drawe-detail/index'
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import ThumBnail from './components/swiper.vue'
import CommentSection from '@/components/comment-section/index.vue'
import { useRoute } from 'vue-router'
import { ref } from 'vue'
import { ProjectRespVO, ProjectDrawPageRespVO, UserExtendSimpleRespDTO, ProjectDrawMemberRespVO } from '@/api/drawe-detail/types'
import useUserStore from '@/store/user'
const message = useMessage()
const userStore = useUserStore()
// 获取路由参数
const route = useRoute()
const id = route.query.id as string
// 获取详情
const detail = ref<ProjectRespVO>({} as ProjectRespVO)
const init = () => {
getDetail({ id }).then((res) => {
if (res.code === 0) {
detail.value = res.data
// 获取推荐信息
getRelationRecommendList()
// 获取用户信息
handleGetUserInfo()
// 最新发布
handleGetMainWork()
}
})
}
init()
// 获取最新发布
const mainWork = ref<ProjectDrawMemberRespVO[]>([])
const handleGetMainWork = () => {
getMainWork({ id: detail.value.id, limit: 10, memberId: detail.value.ownedUserId }).then((res) => {
if (res.code === 0) {
mainWork.value = res.data
}
})
}
// 获取用户信息
const userInfo = ref<UserExtendSimpleRespDTO>({} as UserExtendSimpleRespDTO)
const handleGetUserInfo = () => {
getUserInfo({ id: detail.value.id }).then((res) => {
if (res.code === 0) {
userInfo.value = res.data
}
})
}
// 获取关联推荐
const relationRecommend = ref<ProjectDrawPageRespVO[]>([])
const getRelationRecommendList = () => {
getRelationRecommend({ type: detail.value.type, projectType: detail.value.projectType[0] }).then((res) => {
if (res.code === 0) {
relationRecommend.value = res.data
}
})
}
const changeSwiper = (index: number) => {
console.log(index)
}
const nextSwiper = (val: any) => {
console.log(val)
}
const handleDownloadPreview = (item: any) => {
// 预览pdf
window.open(`/pdf-preview?url=${item.url}`)
}
/** 获取下载类型 */
const getType = (type: number) => {
if (type === 1 || type === 2 || type === 3) {
// 图纸 文本 模型都传1
return 1
}
// 工具箱传
return 2
}
const handleDownload = async () => {
if (!userStore.token) {
ElMessage.error('请先登录')
return
}
if (detail.value.points === 0) {
scrollTo()
return
}
if (detail.value.downloadId) {
ElMessage.success('您已获取下载权限')
scrollTo()
return
}
const res = await message.confirm(`是否花费${detail.value.points}金币下载此资源,是否继续?`, '提示')
if (res) {
createUserProject({ relationId: detail.value.id, type: getType(detail.value.type) }).then((res) => {
if (res.code === 0) {
ElMessage.success('获取下载权限成功')
detail.value.downloadId = res.data
scrollTo()
}
})
}
}
const scrollTo = () => {
const element = document.getElementById('section1')
if (element) {
element.scrollIntoView({
behavior: 'smooth',
})
}
}
const handleReport = () => {
if (!userStore.token) {
ElMessage.error('请先登录')
return
}
console.log('举报')
ElMessageBox.prompt('说明内容', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入举报内容',
inputErrorMessage: '请输入举报内容',
}).then(({ value }) => {
report({
id: detail.value.id,
title: detail.value.title,
files: detail.value.files,
comments: value,
projectId: detail.value.projectId,
drawId: detail.value.id,
}).then((res) => {
if (res.code === 0) {
ElMessage.success('举报成功')
}
})
})
}
const handleCollect = async () => {
if (!userStore.token) {
ElMessage.error('请先登录')
return
}
const res = detail.value.favoriteId
? await deleteProject({ id: detail.value.favoriteId })
: await createContent({
projectId: detail.value.projectId,
drawId: detail.value.id,
})
if (res.code === 0) {
ElMessage.success(`${detail.value.favoriteId ? '取消' : '收藏'}成功`)
init()
}
}
const handleClick = (id: string | number) => {
window.open(`/down-drawe-detail?id=${id}`, '_blank') // 修改为在新窗口打开
}
const handleDownloadFile = (url: string, name: string) => {
downloadFile(url, name)
}
</script>

View File

@ -0,0 +1,48 @@
<template>
<div class="box-border w-100% border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-26px py-30px">
<div class="flex items-center">
<div class="text-28px text-[#333333] font-normal">精选专题</div>
<div class="ml-50px text-21px text-[#999999] font-normal">了解最新趋势发展</div>
</div>
<div class="mt-36px flex justify-between">
<div v-for="item in 4" :key="item" class="flex flex-col items-center">
<img :src="`https://picsum.photos/320/190?_t${new Date().getTime()}`" alt="" srcset="" class="h-190px w320 rounded-4px" />
<div class="mt-10px text-18px text-[#333333] font-normal">机器人</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { PropType, ref, watch } from 'vue'
import { recommendTop } from '@/api/upnew/index'
import { recommendTopRes } from '@/api/upnew/types'
const props = defineProps({
type: {
type: Number as PropType<1 | 2 | 3>,
default: 1,
},
})
/**
* 这个接口调用错了 后续修改
*/
const list = ref<recommendTopRes[]>([])
watch(
() => props.type,
(newVal) => {
if (newVal) {
recommendTop({
type: newVal,
projectType: 0,
isDomestic: 1,
}).then((res) => {
list.value = res.data
})
}
},
{
immediate: true,
}
)
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="relative mt-34px w-100%">
<KlTabBar v-model="query.source" :data="tabBar" />
<div class="absolute right-0px top-10px text-16px text-[#999999] font-normal"
><span class="color-#1A65FF">{{ result.total }}</span
>个筛选结果</div
>
<div class="content mt-10px">
<el-row :gutter="20">
<el-col v-for="(item, index) in result.list" :key="index" :span="6">
<CardPicture :item-info="item" />
</el-col>
</el-row>
<el-empty v-if="!result.list.length" :image="emptyImg"></el-empty>
</div>
</div>
</template>
<script lang="ts" setup>
import KlTabBar from '@/components/kl-tab-bar/index.vue'
import CardPicture from '@/components/kl-card-picture/index.vue'
import { ref } from 'vue'
import { pageRes, pageReq } from '@/api/upnew/types'
import emptyImg from '@/assets/images/empty.png'
const query = defineModel<pageReq>('modelValue', {
required: true,
})
const result = defineModel<pageRes>('result', {
required: true,
})
const tabBar = ref([
{
label: '图纸推荐',
value: '',
},
{
label: '原创图纸',
value: 1,
},
{
label: '最新上传',
value: 2,
},
])
</script>
<style lang="scss" scoped></style>

101
pages/drawe/index.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<!-- 导航 -->
<KlNavTab active="图纸" :type="1" />
<div class="ma-auto w-1440px">
<!-- 图纸分类 -->
<KlWallpaperCategory v-model="query" v-model:level="level" :type="1" />
<!-- 推荐栏目 -->
<RecommendedColumnsV2 v-model="query" v-model:result="result"></RecommendedColumnsV2>
<!-- 精选专题 -->
<!-- <FeaturedSpecials></FeaturedSpecials> -->
<!-- 分页 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="query.pageNo"
v-model:page-size="query.pageSize"
:page-sizes="[12, 24, 48]"
:total="result.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleClickSize"
@current-change="handeClickCurrent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import KlWallpaperCategory from '@/components/kl-wallpaper-category/index.vue'
import RecommendedColumnsV2 from './components/RecommendedColumnsV2.vue'
// import FeaturedSpecials from './components/FeaturedSpecials.vue'
import { useRoute } from 'vue-router'
import { reactive, watch, ref } from 'vue'
import { page } from '@/api/upnew/index'
import { pageRes, pageReq } from '@/api/upnew/types'
const route = useRoute()
const level = ref(
route.query.level
? JSON.parse(route.query.level as string)
: [
{
id: '0',
name: '图纸库',
isChildren: false,
},
]
)
const keywords = ref(route.query.keywords as string)
const query = reactive<pageReq>({
pageNo: 1,
pageSize: 12,
projectType: '',
editions: '',
source: '',
type: 1,
title: keywords.value,
})
const result = reactive<pageRes>({
list: [],
total: 0,
})
// 如果id存在则设置projectType
if (level.value.length) {
query.projectType = level.value[level.value.length - 1].id || ''
}
const handleClickSize = (val: number) => {
query.pageSize = val
getPage()
}
const handeClickCurrent = (val: number) => {
query.pageNo = val
getPage()
}
const getPage = () => {
page(query).then((res) => {
const { data, code } = res
if (code === 0) {
result.list = data.list
result.total = data.total
}
})
}
getPage()
watch([() => query.projectType, () => query.editions, () => query.source], (val) => {
if (val) {
getPage()
}
})
</script>
<style lang="scss" scoped>
:deep(.el-pagination) {
.el-input__inner {
text-align: center !important;
}
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<KlNavTab active="" :type="1" />
<div class="editor-view">
<div class="rich-content" v-html="decodedContent"></div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { computed } from 'vue'
// 获取路由参数
const route = useRoute()
// 假设内容通过query参数传递如 /editor-view?content=xxx
const content = computed(() => (route.query.content as string) || '')
// 解码(如果需要)
const decodedContent = computed(() => decodeURIComponent(content.value))
</script>
<style scoped>
.rich-content {
/* 可根据需要自定义样式 */
padding: 16px;
background: #fff;
border-radius: 8px;
min-height: 200px;
}
</style>

214
pages/error/404.vue Normal file
View File

@ -0,0 +1,214 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/images/404.png" alt="404" />
<img class="left pic-404__child" src="@/assets/images/404_cloud.png" alt="404" />
<img class="pic-404__child mid" src="@/assets/images/404_cloud.png" alt="404" />
<img class="pic-404__child right" src="@/assets/images/404_cloud.png" alt="404" />
</div>
<div class="bullshit">
<div class="bullshit__oops"> 404错误! </div>
<div class="bullshit__headline"> 找不到网页 </div>
<div class="bullshit__info"> 对不起您正在寻找的页面不存在尝试检查URL的错误然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容 </div>
<router-link to="/" class="bullshit__return-home"> 返回首页 </router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.wscn-http404-container {
transform: translate(-50%, -50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<div class="banner">
<div class="banner-content">
<div class="banner-text">
<h1 class="title">开启 CAD 学习之旅</h1>
<p class="subtitle">为你的创意引擎注入强劲动力驱动设计梦想在市场中乘风破浪</p>
<button class="join-button">快来加入</button>
</div>
<div class="banner-image">
<img src="@/assets/images/foreign_banner.png" alt="CAD工作环境" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 组件逻辑可以在这里添加
</script>
<style scoped>
.banner {
background: #f4f6f6;
padding: 60px 40px;
min-height: 400px;
}
.banner-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
/* gap: 40px; */
}
.banner-text {
flex: 1;
}
.title {
font-size: 2.5rem;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.subtitle {
font-size: 1.2rem;
color: #666;
line-height: 1.6;
margin-bottom: 30px;
}
.join-button {
background-color: #1a73e8;
color: white;
border: none;
padding: 12px 32px;
border-radius: 4px;
font-size: 1.1rem;
cursor: pointer;
transition: background-color 0.3s;
}
.join-button:hover {
background-color: #1557b0;
}
.banner-image {
flex: 1;
display: flex;
justify-content: flex-end;
}
.banner-image img {
width: 100%;
max-width: 600px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.banner {
padding: 20px;
}
.banner-content {
flex-direction: column;
text-align: center;
}
.title {
font-size: 2rem;
}
.subtitle {
font-size: 1rem;
}
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="box-border w-100% border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-26px py-30px">
<div class="flex items-center">
<div class="text-28px text-[#333333] font-normal">精选专题</div>
<div class="ml-50px text-21px text-[#999999] font-normal">了解最新趋势发展</div>
</div>
<div class="mt-36px flex justify-between">
<div v-for="item in 4" :key="item" class="flex flex-col items-center">
<img :src="`https://picsum.photos/320/190?_t${new Date().getTime()}`" alt="" srcset="" class="h-190px w320 rounded-4px" />
<div class="mt-10px text-18px text-[#333333] font-normal">机器人</div>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,251 @@
<template>
<div class="tech-showcase">
<div class="showcase-grid">
<!-- 五金工具卡片 -->
<div class="showcase-card large-card">
<div class="card-content">
<h2 class="card-title">五金工具</h2>
<div class="card-divider"></div>
<p class="card-description">小五金模具电动工具手动工具</p>
<button class="learn-button">Learn more</button>
</div>
</div>
<!-- 上排卡片: CAD设计工作站和笔记本工作站 -->
<div class="top-row">
<!-- CAD设计工作站卡片 - 较宽 -->
<div class="showcase-card card-wide medium-card">
<div class="card-overlay"></div>
</div>
<!-- 工作站卡片 - 较窄 -->
<div class="showcase-card medium-card card-narrow">
<div class="card-overlay"></div>
</div>
</div>
<!-- 下排卡片: 工业机器人和CAD工业设计 -->
<div class="bottom-row">
<!-- 工业机器人卡片 - 较窄 -->
<div class="showcase-card medium-card card-narrow">
<div class="card-overlay"></div>
</div>
<!-- CAD工业设计卡片 - 较宽 -->
<div class="showcase-card medium-card card-wide">
<div class="card-overlay"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 组件逻辑可以在此添加
</script>
<style scoped>
.tech-showcase {
width: 100%;
max-width: 1440px;
margin: 0 auto;
padding: 80px 0px;
background-color: #fff;
}
.showcase-grid {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: repeat(2, 1fr);
gap: 20px;
height: 600px;
}
.top-row,
.bottom-row {
display: flex;
gap: 20px;
}
.top-row {
grid-column: 2;
grid-row: 1;
}
.bottom-row {
grid-column: 2;
grid-row: 2;
}
.showcase-card {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background-size: cover;
background-position: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.showcase-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
/* 五金工具卡片 */
.large-card {
grid-column: 1;
grid-row: 1 / span 2;
background-image: url('@/assets/images/hardware-tools.png');
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
/* 宽卡片占70% */
.card-wide {
flex: 7;
}
/* 窄卡片占30% */
.card-narrow {
flex: 3;
}
/* CAD设计工作站卡片 - 较宽 */
.top-row .card-wide {
background-image: url('@/assets/images/cad-workstation.png');
}
/* 工作站卡片 - 较窄 */
.top-row .card-narrow {
background-image: url('@/assets/images/laptop-workspace.png');
}
/* 工业机器人卡片 - 较窄 */
.bottom-row .card-narrow {
background-image: url('@/assets/images/industrial-robots.png');
}
/* CAD工业设计卡片 - 较宽 */
.bottom-row .card-wide {
background-image: url('@/assets/images/cad-industrial-design.png');
}
.card-content {
position: relative;
z-index: 2;
padding: 30px;
color: white;
text-align: center;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.large-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.card-title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 15px;
}
.card-divider {
width: 40px;
height: 3px;
background-color: white;
margin: 0 auto 15px;
}
.card-description {
font-size: 1rem;
margin-bottom: 20px;
}
.learn-button {
background-color: #1a73e8;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
align-self: center;
transition: background-color 0.3s;
margin-top: 60px;
}
.learn-button:hover {
background-color: #1557b0;
}
.card-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
transition: background 0.3s;
}
.medium-card:hover .card-overlay {
background: rgba(0, 0, 0, 0.4);
}
@media (max-width: 992px) {
.showcase-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
height: auto;
}
.large-card {
height: 300px;
grid-row: 1;
}
.top-row,
.bottom-row {
grid-column: 1;
}
.top-row {
grid-row: 2;
}
.bottom-row {
grid-row: 3;
}
.card-wide,
.card-narrow {
height: 250px;
}
}
@media (max-width: 576px) {
.top-row,
.bottom-row {
flex-direction: column;
gap: 20px;
}
.card-wide,
.card-narrow {
height: 200px;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div class="relative mt-34px w-100%">
<KlTabBar v-model="tabIndex" :data="tabBar" />
<KlWallpaperCategory v-model="query" v-model:level="level" :type="1" />
<div class="absolute right-0px top-10px text-16px text-[#999999] font-normal"
><span class="color-#1A65FF">{{ result.total }}</span
>个筛选结果</div
>
<div class="content mt-10px">
<el-row :gutter="20">
<el-col v-for="(item, index) in result.list" :key="index" :span="6">
<CardPicture :item-info="item" />
</el-col>
</el-row>
<el-empty v-if="!result.list.length" description="暂无数据"></el-empty>
</div>
</div>
</template>
<script lang="ts" setup>
import KlTabBar from '@/components/kl-tab-bar/index.vue'
import CardPicture from '@/components/kl-card-picture/index.vue'
import { ref } from 'vue'
import { pageRes } from '@/api/upnew/types'
const level = ref([
{
id: '0',
name: '图纸库',
isChildren: false,
},
])
const result = defineModel<pageRes>('modelValue', {
required: true,
})
const query = defineModel<any>('query', {
required: true,
})
const tabIndex = ref(1)
const tabBar = ref([
{
label: '图纸推荐',
value: 1,
},
{
label: '原创图纸',
value: 2,
},
{
label: '最新上传',
value: 3,
},
])
</script>
<style lang="scss" scoped></style>

86
pages/foreign/index.vue Normal file
View File

@ -0,0 +1,86 @@
<template>
<!-- 导航 -->
<KlNavTab active="国外专区" />
<!-- banneer提示 -->
<BannerTips />
<div class="ma-auto w-1440px">
<!-- 图片展示鼠标移上去展示提示语 -->
<!-- <ImageTips /> -->
<!-- 推荐栏目 -->
<RecommendedColumnsV2 v-model:query="query" v-model="result"></RecommendedColumnsV2>
<!-- 精选专题 -->
<!-- <FeaturedSpecials></FeaturedSpecials> -->
<!-- 分页 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="query.pageNo"
v-model:page-size="query.pageSize"
:page-sizes="[10, 20, 30]"
:total="result.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleChangeSize"
@current-change="handleChangeCurrent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import RecommendedColumnsV2 from './components/RecommendedColumnsV2.vue'
// import FeaturedSpecials from './components/FeaturedSpecials.vue'
import BannerTips from './components/BannerTips.vue'
// import ImageTips from './components/ImageTips.vue'
import { reactive, watch } from 'vue'
import { page } from '@/api/upnew/index'
import { pageRes, pageReq } from '@/api/upnew/types'
const query = reactive<pageReq>({
pageNo: 1,
pageSize: 10,
projectType: '',
editions: '',
source: '',
type: 1,
})
const result = reactive<pageRes>({
list: [],
total: 0,
})
const getPage = () => {
page(query).then((res) => {
const { data, code } = res
if (code === 0) {
result.list = data.list
result.total = data.total
}
})
}
getPage()
const handleChangeSize = (val: number) => {
query.pageSize = val
query.pageNo = 1
getPage()
}
const handleChangeCurrent = (val: number) => {
query.pageNo = val
getPage()
}
watch([() => query.projectType, () => query.editions, () => query.source], (val) => {
if (val) {
getPage()
}
})
</script>
<style lang="scss" scoped>
:deep(.el-pagination) {
.el-input__inner {
text-align: center !important;
}
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<div class="flex">
<div>
<div class="my-32px mb-20px text-18px text-[#333333] font-normal"><img src="@/assets/images/2.png" alt="" srcset="" /> 多多排行榜</div>
<div class="flex">
<div class="ma-auto box-border h-470px w-460px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-28px">
<div class="title-bg ma-auto mb-40px mt-20px">一周图纸作者排行</div>
<div v-for="(item, index) in topList" :key="item.ownUserId" class="mb-23px flex items-center">
<div class="w-30px text-center"
><img
v-if="index === 0 || index === 1 || index === 2"
:src="imagesUrl(index)"
alt=""
srcset=""
:class="index === 0 ? 'w-20px h-22px' : 'w-28px h-29px'"
/>
<span v-else class="LiHei (Noncommercial) font-MF text-16px text-[#999999] font-normal">{{ index }}</span>
</div>
<div class="ml-20px w-120px flex items-center text-16px text-[#333333] font-normal">
<img :src="item.avatar" alt="" srcset="" class="h-36px w-36px rd-50%" />
<span class="ellipsis1 ml-10px">{{ item.nickname }}</span>
</div>
<div class="ml-20px flex text-14px text-[#666666] font-normal">
<!-- <el-icon class="text-17px color-#a8abb2!"><Folder /></el-icon> -->
<img src="@/assets/images/file.png" alt="" srcset="" class="h-18px" />
<div class="ellipsis1 ml-10px">作品{{ item.projectCount || 0 }}</div>
</div>
<div class="ml-20px flex text-14px text-[#666666] font-normal">
<!-- <el-icon class="text-17px color-[#e4e7ed!]"><User /></el-icon> -->
<img src="@/assets/images/user4.png" alt="" srcset="" class="h-17px" />
<div class="ellipsis1 ml-10px">粉丝{{ item.fansCount || 0 }}</div>
</div>
</div>
<!-- 暂无数据 -->
<el-empty v-if="!topList.length" description="暂无数据"></el-empty>
</div>
<div class="ma-auto ml-18px box-border h-470px w-460px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-28px">
<div class="title-bg ma-auto mb-40px mt-20px">优质上传作者图纸</div>
<div v-for="(item, index) in userTopList" :key="index" class="mb-23px flex items-center">
<div class="w-30px text-center"
><img
v-if="index === 0 || index === 1 || index === 2"
:src="imagesUrl(index)"
alt=""
srcset=""
:class="index === 0 ? 'w-20px h-22px' : 'w-28px h-29px'"
/>
<span v-else class="font-MF LiHei (Noncommercial) text-16px text-[#999999] font-normal">{{ index }}</span>
</div>
<div class="ml-20px w-120px flex items-center text-16px text-[#333333] font-normal">
<img :src="item.avatar" alt="" srcset="" class="h-36px w-36px rd-50%" />
<span class="ellipsis1 ml-10px">{{ item.nickname }}</span>
</div>
<div class="ml-20px flex text-14px text-[#666666] font-normal">
<img src="@/assets/images/file.png" alt="" srcset="" class="h-18px" />
<div class="ellipsis1 ml-10px">作品{{ item.projectCount || 0 }}</div>
</div>
<div class="ml-20px flex text-14px text-[#666666] font-normal">
<img src="@/assets/images/user4.png" alt="" srcset="" class="h-17px" />
<div class="ellipsis1 ml-10px">粉丝{{ item.fansCount || 0 }}</div>
</div>
</div>
<!-- 暂无数据 -->
<el-empty v-if="!userTopList.length" description="暂无数据"></el-empty>
</div>
</div>
</div>
<div class="ml-63px">
<div class="my-32px mb-20px text-18px text-[#333333] font-normal"><img src="@/assets/images/2.png" alt="" srcset="" /> 发布动态</div>
<div class="box-border h-470px w-437px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] p-15px">
<div
v-for="item in newDrawList"
:key="item.id"
class="flex cursor-pointer items-center justify-between px-10px py-10px text-15px text-[#333333] font-normal hover:bg-[#f5f5f5]"
@click="handleClick(item)"
>
<div class="ellipsis1 w-70%">{{ item.title }}</div>
<div class="text-15px color-#999">{{ dayjs(item.createTime).format('MM-DD') }}</div>
</div>
<!-- 暂无数据 -->
<el-empty v-if="!newDrawList.length" description="暂无数据"></el-empty>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
// import { Folder, User } from '@element-plus/icons-vue'
import { newDraw, userTop } from '@/api/home/index'
import { ProjectDrawPageRespVO, ProjectTrendingScoreUserInfoVO } from '@/api/home/type'
import dayjs from 'dayjs'
const Url = Object.values(
import.meta.glob('@/assets/images/no*.png', {
eager: true,
query: 'url',
})
).map((module: any) => module.default)
const imagesUrl = computed(() => {
return (index: number) => {
return `${Url[index]}`
}
})
// 最新动态
const newDrawList = ref<ProjectDrawPageRespVO[]>([])
const handlenewDraw = async () => {
const res = await newDraw({
type: 1,
limit: 11,
})
newDrawList.value = res.data
}
handlenewDraw()
const topList = ref<ProjectTrendingScoreUserInfoVO[]>([]) // 最新动态
const userTopList = ref<ProjectTrendingScoreUserInfoVO[]>([]) // 最新动态
const handleuserTop = () => {
Promise.all([userTop({ type: 1 }), userTop({})]).then((res) => {
topList.value = res[0].data
userTopList.value = res[1].data
})
}
handleuserTop()
const handleClick = (item: ProjectDrawPageRespVO) => {
window.open(`/down-drawe-detail?id=${item.id}`, '_blank') // 修改为在新窗口打开
}
</script>
<style scoped lang="scss">
.title-bg {
width: 224px;
height: 52px;
background-image: url('@/assets/images/name_bg.png');
background-size: 100% 100%;
text-align: center;
line-height: 48px;
font-weight: bold;
font-size: 18px;
color: #a44120;
}
.ellipsis1 {
@include ellipsis(1);
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div v-if="bannerList?.length" class="mt-34px w-100% flex items-center justify-between">
<el-image
v-for="(item, index) in bannerList"
:key="index"
:src="item.content"
alt=""
srcset=""
fit="cover"
class="h-180px w-460px"
:class="{
'cursor-pointer': item.url,
}"
@click="item.url && handleClick(item.url)"
>
<template #placeholder>
<div class="image-slot">Loading<span class="dot">...</span></div>
</template>
</el-image>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { getSettingPage } from '@/api/home/index'
import { PageResultIndexSettingRespVO } from '@/api/home/type'
const pageReq = reactive({
type: 2,
status: 0,
})
const bannerList = ref<PageResultIndexSettingRespVO[]>([])
const getBanner = async () => {
const res = await getSettingPage(pageReq)
if (res.code === 0) {
bannerList.value = res.data
}
}
getBanner()
const handleClick = (url: string) => {
window.open(url, '_blank')
}
</script>
<style scoped></style>

View File

@ -0,0 +1,200 @@
<template>
<div class="login-container flex flex-col justify-between">
<div class="ma-auto mt-25px w-100% flex flex-col items-center">
<el-image
:src="userStore.userInfoRes.avatar || 'https://tuxixi.oss-cn-chengdu.aliyuncs.com/avater.png'"
alt=""
srcset=""
class="h-69px w-69px rd-50%"
:class="{ 'cursor-pointer': isLogin }"
fit="cover"
@click="handleUserInfo"
/>
<div class="mt-10px text-16px text-[#333333] font-normal">
<img v-if="userStore.userInfoRes.vipLevel === 1" src="@/assets/svg/vip.svg" alt="" class="relative top-12px" />
<img v-if="userStore.userInfoRes.vipLevel === 2" src="@/assets/svg/svip.svg" alt="" class="relative top-12px" />
Hi{{ userStore.userInfoRes.nickname || '欢迎访问~' }}
</div>
</div>
<div v-if="isLogin" class="mt-20px flex flex-col gap-20px px-20px text-14px text-[#333333] font-normal">
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="@/assets/images/cad_0 (1).png" alt="" srcset="" />
<span class="title ml-4px" :title="`${userStaticInfo?.pointCount}`">我的积分: {{ userStaticInfo?.pointCount || 0 }}</span>
</div>
<div class="flex items-center">
<img src="@/assets/images/cad_0 (2).png" alt="" srcset="" />
<span class="title ml-4px" :title="`${userStaticInfo?.followCount}`">我的收藏: {{ userStaticInfo?.followCount || 0 }}</span>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="@/assets/images/cad_0 (3).png" alt="" srcset="" />
<span class="title ml-4px" :title="`${userStaticInfo?.projectCount}`">我的发布: {{ userStaticInfo?.projectCount || 0 }}</span>
</div>
<div class="flex items-center">
<img src="@/assets/images/cad_0 (4).png" alt="" srcset="" />
<span class="title ml-4px" :title="`${userStaticInfo?.downloadCount}`">我的下载: {{ userStaticInfo?.downloadCount || 0 }}</span>
</div>
</div>
</div>
<div v-if="!isLogin" class="mt-48px box-border flex justify-between px-18px">
<div
class="h-37px w-101px cursor-pointer border border-[#1A65FF] rounded-2px border-solid bg-[#1A65FF] text-center text-14px text-[#FFFFFF] font-normal line-height-37px"
@click="handleLogin"
>立即登录</div
>
<div
class="h-37px w-101px cursor-pointer border border-[#DDDDDD] rounded-2px border-solid text-center text-14px text-[#333333] font-normal line-height-37px"
@click="handleRegister"
>免费注册</div
>
</div>
<div v-else class="mt-30px box-border flex justify-between px-18px">
<div
class="h-37px w-101px cursor-pointer border border-[#1A65FF] rounded-2px border-solid bg-[#1A65FF] text-center text-14px text-[#FFFFFF] font-normal line-height-37px"
@click="handleDrawe"
>发布图纸</div
>
<div
class="h-37px w-101px cursor-pointer border border-[#DDDDDD] rounded-2px border-solid text-center text-14px text-[#333333] font-normal line-height-37px"
@click="handleLoginOut"
>退出登录</div
>
</div>
<div v-if="!isLogin" class="mt-30px flex justify-between px-20px">
<img src="@/assets/images/qq-v2.png" alt="QQ登录" class="social-icon" @click="handleLoginQQ" />
<img src="@/assets/images/weixin-v2.png" alt="微信登录" class="social-icon" @click="handleLoginWechat" />
<img src="@/assets/images/email-v2.png" alt="邮箱登录" class="social-icon" @click="handleLoginEmail" />
<img src="@/assets/images/phone-v2.png" alt="手机登录" class="social-icon" @click="handleLoginPhone" />
</div>
<div class="sign-bonus mt-18px" @click="handleSign">
<img src="@/assets/images/sign.png" alt="签到奖励" class="bonus-image" />
</div>
</div>
</template>
<script setup lang="ts">
import { refreshToken as REFRESHTOKEN } from '@/utils/axios'
import { getCurrentInstance, computed, watchEffect, ref } from 'vue'
import { handleLoginQQ, handleLoginWechat } from '@/utils/login'
import { UserStatisticsCountRespVO } from '@/api/personal-center/types'
import { getUserStatistics } from '@/api/personal-center/index'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const instance = getCurrentInstance()
// 是否登录
const isLogin = computed(() => {
return !!userStore.token
})
// 获取用户统计信息
const userStaticInfo = ref<UserStatisticsCountRespVO>()
const fetchUserStatistics = async () => {
try {
const res = await getUserStatistics()
userStaticInfo.value = res.data
} catch (error) {
console.log(error)
}
}
const handleLogin = () => {
if (instance) {
instance.appContext.config.globalProperties.$openLogin()
}
}
const handleRegister = () => {
if (instance) {
instance.appContext.config.globalProperties.$openRegister()
}
}
const handleLoginPhone = () => {
if (instance) {
instance.appContext.config.globalProperties.$openLogin('verify')
}
}
const handleLoginEmail = () => {
if (instance) {
instance.appContext.config.globalProperties.$openLoginEmail()
}
}
const handleUserInfo = () => {
if (!isLogin.value) return
window.open('/personal-detail', '_blank') // 修改为在新窗口打开
}
// 发布图纸
const handleDrawe = () => {
window.open('/upnew/drawe', '_blank') // 修改为在新窗口打开
}
// 推出登录
const handleLoginOut = () => {
REFRESHTOKEN.removeToken()
userStore.setToken('')
userStore.setUserId('')
userStore.setRefreshToken('')
window.location.reload()
}
const handleSign = () => {
window.open('/sign-page', '_blank') // 修改为在新窗口打开
}
watchEffect(() => {
if (isLogin.value) {
fetchUserStatistics()
}
})
</script>
<style scoped lang="scss">
.login-container {
width: 260px;
background: white;
/* border-radius: 8px; */
box-sizing: border-box;
}
.login-button:hover {
background-color: #40a9ff;
cursor: pointer;
}
.social-login {
display: flex;
justify-content: space-between;
gap: 10px;
margin-top: 16px;
}
.social-icon {
width: 35px;
height: 36px;
cursor: pointer;
}
.sign-bonus {
background: #f5f5f5;
width: 100%;
height: 120px;
overflow: hidden;
}
.bonus-image {
width: 100%;
height: auto;
object-fit: contain;
}
.title {
@include ellipsis(1);
}
</style>

View File

@ -0,0 +1,52 @@
`<template>
<div class="main-content">
<div class="flex">
<div class="h-424px w-957px">
<el-carousel height="424px" indicator-position="none">
<el-carousel-item v-for="(item, index) in bannerList" :key="index">
<el-image :src="item.content" class="w-100%" :class="{ 'cursor-pointer': item.url }" fit="cover" @click="handleClick(item.url)" />
</el-carousel-item>
</el-carousel>
</div>
<LoginForm />
</div>
<div class="box-border h-56px w-1219px flex items-center border border-[#EEEEEE] border-solid border-t-none bg-[#FFFFFF] pl-10px line-height-46px">
<img src="@/assets/images/voice.png" alt="" srcset="" class="mr-10px h-15px w-16px" />
<Vue3Marquee :duration="10" direction="normal" pause-on-hover>· 经典来袭SolidWorks装配经典案例之气动发动机 </Vue3Marquee>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import LoginForm from './LoginForm.vue'
import { getSettingPage } from '@/api/home/index'
import { PageResultIndexSettingRespVO } from '@/api/home/type'
const pageReq = reactive({
type: 1,
status: 0,
})
const bannerList = ref<PageResultIndexSettingRespVO[]>([])
const getBanner = async () => {
const res = await getSettingPage(pageReq)
if (res.code === 0) {
bannerList.value = res.data
}
}
getBanner()
const handleClick = (url: string) => {
if (url) {
window.open(url, '_blank')
}
}
</script>
<style scoped lang="scss">
.main-content {
flex: 1;
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<div class="mt-34px w-100%">
<div class="flex items-center justify-between">
<KlTabBar v-model="query.type" :data="tabBar" :show-icon="true" />
<div class="flex gap-15px">
<span
v-for="(item, index) in projectTypeList"
:key="index"
:class="{ 'color-#1A65FF! border-b-2px border-b-solid border-b-[#1A65FF] rounded-1px pb-3px': query.projectTypeDay === item.id }"
class="cursor-pointer text-14px color-#333333"
@mouseenter="handleHover(item)"
@click="handleClickType(item)"
>
{{ item.name }}
</span>
</div>
</div>
<div class="content mt-10px flex">
<!-- <div class="sider">
<div class="box-border h-100% h-55px w-221px flex items-center rounded-lg bg-[#1A65FF] pl-24px text-white">
<img src="@/assets/images/1.png" alt="" srcset="" />
<span class="ml-12px text-16px">全部资源分类</span>
</div>
<div class="side-menu border border-[#EEEEEE] border-solid">
<div
v-for="(item, index) in projectTypeListChildren"
:key="index"
class="menu-item"
:class="{ active: query.projectType === item.id }"
@mouseenter="handleClick(item)"
@click="
handleClickType(
projectTypeList?.find((c: any) => c.id === query.projectTypeDay),
item
)
"
>
{{ item.name }}
</div>
<el-empty v-if="projectTypeList.length === 0" description="暂无数据"></el-empty>
</div>
</div> -->
<div class="box-border w-100%">
<div class="grid grid-cols-5 gap-17px">
<div
v-for="item in hotTopList"
:key="item.id"
class="cursor-pointer border border-[#EEEEEE] rounded-1px border-solid bg-[#FFFFFF]"
@click="handleCardClick(item)"
>
<div>
<div class="h-212px w-100%">
<el-image :src="item.iconUrl" alt="" srcset="" fit="cover" class="block h-100% w-100%" />
</div>
<div class="ellipsis mx-20px my-11px text-14px color-#333333 font-normal">{{ item.title }}</div>
</div>
</div>
</div>
<el-empty v-if="hotTopList.length === 0" description="暂无数据"></el-empty>
</div>
</div>
<!-- <div class="morefont-400 text-16px text-[#1A65FF] text-right cursor-pointer"> 查看更多 >> </div> -->
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue'
import KlTabBar from '@/components/kl-tab-bar/index.vue'
// import KlCardDetail from '@/components/kl-card-detail/index.vue'
import { hotTop, hotTag } from '@/api/home/index'
import { ProjectDrawPageRespVO, ProjectDictNodeVO } from '@/api/home/type'
/** 请求参数 */
const query = reactive({
type: 1,
projectType: '',
projectTypeDay: '', // 自定义的
isDomestic: 1,
limit: 6,
})
const tabBar = ref([
{
label: '热门图纸',
value: 1,
},
{
label: '热门模型',
value: 3,
},
{
label: '热门文本',
value: 2,
},
])
// const hotTopListTitle = computed(() => {
// if (projectTypeList.value.length > 0) {
// const item = projectTypeList.value.find((item) => item.id === query.projectType)
// return item?.name
// }
// return ''
// })
/** 点击卡片 */
const handleCardClick = (item: ProjectDrawPageRespVO) => {
// 跳转到下载详情页 并且是单独开标签
window.open(`/down-drawe-detail?id=${item.id}`, '_blank') // 修改为在新窗口打开
}
const handleClickType = (primary?: ProjectDictNodeVO, secondary?: ProjectDictNodeVO) => {
if (primary?.name !== '全部分类') return
const normal = { id: '0', name: query.type === 1 ? '图纸库' : query.type === 2 ? '文本库' : '模型库', isChildren: false }
const level = [primary, secondary].filter((item) => item).map((item) => ({ id: item?.id, name: item?.name, isChildren: item?.children ? false : true }))
if (primary?.id === '0') {
level[0].name = '图纸库'
} else {
level.unshift(normal)
}
if (query.type === 1) {
window.open(`/drawe?level=${JSON.stringify(level)}`)
} else if (query.type === 2) {
window.open(`/text?level=${JSON.stringify(level)}`)
} else if (query.type === 3) {
window.open(`/model?level=${JSON.stringify(level)}`)
}
}
/** 热门数据 */
const hotTopList = ref<ProjectDrawPageRespVO[]>([])
const getHotTop = () => {
hotTop({
type: query.type,
projectType: query.projectTypeDay,
isDomestic: query.isDomestic,
projectTypeTop: query.projectTypeDay,
}).then((res) => {
if (res.code === 0) {
hotTopList.value = res.data || []
}
})
}
/** 获取分类下拉框 */
const projectTypeList = ref<ProjectDictNodeVO[]>([])
const projectTypeListChildren = ref<ProjectDictNodeVO[]>([])
const getParent = () => {
hotTag({
type: query.type,
limit: 6,
size: 1000,
}).then((res) => {
if (res.code === 0) {
if (Array.isArray(res.data) && res.data.length > 0) {
projectTypeList.value = [...res.data, { id: '0', name: '全部分类', children: [] }]
projectTypeListChildren.value = res.data[0].children || []
query.projectTypeDay = res.data[0].id || ''
query.projectType = res.data[0]!.children?.[0]!.id || ''
// 热门数据
getHotTop()
} else {
hotTopList.value = []
}
}
})
}
const handleHover = (item: ProjectDictNodeVO) => {
query.projectTypeDay = item.id || ''
if (item.name === '全部分类') return
projectTypeListChildren.value = item.children || []
query.projectType = item.children?.[0].id || ''
// 热门数据
getHotTop()
}
/** 点击分类 */
// const handleClick = (item: ProjectDictNodeVO) => {
// query.projectType = item.id || ''
// getHotTop()
// }
watch(
() => query.type,
() => {
getParent()
},
{
immediate: true,
}
)
</script>
<style lang="scss" scoped>
.side-menu {
width: 221px;
height: 470px;
background-color: #fff;
// padding: 10px 0;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
overflow-y: auto;
box-sizing: border-box;
}
.menu-item {
padding: 10px 24px;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
font-size: 14px;
&.active {
background-color: #f0f7ff;
color: #1a65ff !important;
}
}
.menu-item:hover {
background-color: #f0f7ff;
color: #1a65ff;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="mt-34px w-100%">
<KlTabBar v-model="query.type" :data="tabBar" :show-icon="true" />
<div class="content mt-10px">
<el-row :gutter="20">
<el-col v-for="i in recommendTopList" :key="i.id" :span="6">
<CardPicture :item-info="i" />
</el-col>
</el-row>
</div>
<!-- 暂无数据 -->
<el-empty v-if="recommendTopList.length === 0" description="暂无数据"></el-empty>
<div v-if="recommendTopList.length > 0" class="cursor-pointer text-right text-16px text-[#1A65FF] font-normal" @click="handleClick"> 查看更多 >> </div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue'
import KlTabBar from '@/components/kl-tab-bar/index.vue'
import CardPicture from '@/components/kl-card-picture/index.vue'
import { recommendTop } from '@/api/home/index'
import { ProjectDrawPageRespVO } from '@/api/home/type'
const query = reactive({
type: 1,
projectType: 0,
isDomestic: 1,
limit: 8,
})
const tabBar = ref([
{
label: '推荐图纸',
value: 1,
},
{
label: '推荐模型',
value: 3,
},
{
label: '推荐文本',
value: 2,
},
])
const recommendTopList = ref<ProjectDrawPageRespVO[]>([])
const getRecommendTop = () => {
// @ts-ignore
recommendTop(query).then((res) => {
if (Array.isArray(res.data) && res.data.length > 0) {
recommendTopList.value = res.data
}
})
}
const handleClick = () => {
if (query.type === 1) {
window.open('/drawe', '_blank')
} else if (query.type === 2) {
window.open('/text', '_blank')
} else {
window.open('/model', '_blank')
}
}
watch(
() => query.type,
(val) => {
if (val) {
getRecommendTop()
}
},
{
immediate: true,
}
)
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,262 @@
<template>
<div ref="sideMenu" class="side-menu">
<div
v-for="(item, index) in menuItems"
:key="index"
:ref="(el) => setMenuItemRef(el, index)"
class="menu-item"
@mouseenter="showSubMenu(index)"
@mouseleave="hideSubMenu"
>
<!-- @click="handleSubmenuClick(item)" -->
{{ item.name }}
<!-- 悬浮浮窗 -->
<div v-if="activeIndex === index" class="submenu-panel" :style="{ top: submenuTop + 'px' }" @mouseenter="keepSubmenuVisible" @mouseleave="hideSubMenu">
<div class="submenu-content">
<!-- <div class="text-right">更多</div> -->
<div class="submenu-group">
<template v-for="group in item.children" :key="group.id">
<div class="submenu-group-title" @click.stop="handleSubmenuClick(group)">{{ group.name }}</div>
<div class="submenu-group-items">
<div v-for="sub in group.children" :key="sub.id" class="submenu-item" @click.stop="handleSubmenuClick(group, sub)">
{{ sub.name }}
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { tab2 } from '@/api/home/index'
import { ProjectDictNodeVO } from '@/api/home/type'
const activeIndex = ref(-1)
const submenuTop = ref(0)
// const submenuLeft = ref(0)
const sideMenu = ref()
const menuItemRefs = ref<HTMLElement[]>([])
const showSubMenu = (index: number) => {
// if (menuItems.value.length === index + 1) {
// return
// }
activeIndex.value = index
const dom = menuItemRefs.value[index].getBoundingClientRect()
console.log(dom)
// submenuTop.value = window.scrollY
}
const hideSubMenu = () => {
activeIndex.value = -1
}
const handleSubmenuClick = (primary?: ProjectDictNodeVO, secondary?: ProjectDictNodeVO, tertiary?: ProjectDictNodeVO) => {
const normal = { id: '0', name: '图纸库', isChildren: false }
const level = [primary, secondary, tertiary]
.filter(Boolean)
.map((item) => ({ id: item?.id, name: item?.name, isChildren: item?.children?.length ? false : true }))
if (primary?.id === '0') {
level[0].name = '图纸库'
} else {
level.unshift(normal)
}
window.open(`/drawe?level=${JSON.stringify(level)}`, '_blank')
}
const keepSubmenuVisible = () => {
// activeIndex.value = activeIndex.value // 保持当前索引
}
const setMenuItemRef = (el: Element | ComponentPublicInstance | null, index: number) => {
if (el && 'offsetTop' in el) {
menuItemRefs.value[index] = el as HTMLElement
}
}
const menuItems = ref<ProjectDictNodeVO[]>([])
const getLabel = () => {
tab2().then((res) => {
const arr = []
for (let i = 0; i < res.data.length; i += 2) {
arr.push({
children: res.data.slice(i, i + 2),
name: getName(res.data.slice(i, i + 2)),
})
}
menuItems.value = arr
})
}
const getName = (arr: any[]) => {
if (arr.length === 1) {
return arr[0].name
} else {
return arr[0].name + ' / ' + arr[1].name
}
}
const init = () => {
// 获取标签
getLabel()
}
onMounted(() => {
init()
})
</script>
<style scoped>
.side-menu {
width: 221px;
background-color: #fff;
/* padding: 10px 0; */
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
/* overflow: visible; */
box-sizing: border-box;
position: relative; /* 添加定位上下文 */
/* overflow-y: auto; */
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
z-index: 9;
/* padding-top: 10px; */
}
/* 隐藏 Webkit 浏览器的滚动条 */
.side-menu::-webkit-scrollbar {
display: none;
}
/* 鼠标悬停时显示滚动条 */
.side-menu:hover {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* .side-menu:hover::-webkit-scrollbar {
display: block;
width: 8px !important;
}
.side-menu:hover::-webkit-scrollbar-thumb {
background-color: #e6e8eb;
border-radius: 6px !important;
}
.side-menu:hover::-webkit-scrollbar-track {
background-color: #fff;
} */
.menu-item {
/* position: relative; */
text-align: center;
padding: 4px 24px;
cursor: pointer;
/* transition: all 0.3s ease; */
color: #333;
font-size: 14px;
border: 1px solid transparent;
z-index: 9;
}
.menu-item:hover {
background-color: #fff;
color: #1890ff;
border: 1px solid #1890ff;
border-right: transparent !important;
}
.submenu-panel {
position: absolute;
left: 220px;
top: 0;
width: 957px;
background: #fff;
/* box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); */
border: 1px solid #1890ff;
/* border-radius: 4px; */
display: flex;
padding: 20px;
z-index: -1;
box-sizing: border-box;
min-height: 480px;
overflow-y: auto;
}
.submenu-panel::before {
/* content: '';
position: absolute;
left: -6px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid #fff;
filter: drop-shadow(-2px 0 2px rgba(0, 0, 0, 0.1)); */
}
.submenu-content {
/* flex: 1; */
/* display: grid; */
/* grid-template-columns: repeat(3, 1fr); */
/* gap: 15px; */
width: 100%;
display: flex;
flex-direction: column;
.submenu-group {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-wrap: wrap;
-webkit-column-break-inside: avoid; /* 兼容webkit内核 */
/* border: 1px solid #e6e8eb; */
.submenu-group-title {
font-size: 15px;
/* font-weight: 700; */
padding: 10px 16px;
color: #000;
font-weight: 600;
transition: all 0.2s;
/* border-bottom: 1px solid #e6e8eb; */
}
.submenu-group-title:hover {
color: #1890ff;
/* border-bottom: 1px solid #e6e8eb; */
}
.submenu-group-items {
display: flex !important;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
color: #666;
padding: 0px 16px;
gap: 10px;
.submenu-item {
cursor: pointer;
}
}
}
}
.submenu-item {
cursor: pointer;
transition: all 0.2s;
display: inline-block; /* 改为行内块元素 */
/* padding: 8px 12px; */
padding-right: 12px;
/* padding-bottom: 8px; */
}
.submenu-item:hover {
color: #1890ff;
}
</style>

49
pages/home/index.vue Normal file
View File

@ -0,0 +1,49 @@
<template>
<div class="ma-auto w-1440px">
<!-- 搜索 -->
<KlSearch></KlSearch>
<!-- 菜单 -->
<KlMenuV2 />
<!-- banner -->
<div class="banner-container">
<SideMenu class="side-menu" />
<MainContent class="main-content" />
</div>
<!-- 学习方法推荐 -->
<LearningRecommendations></LearningRecommendations>
<!-- 推荐栏目 -->
<RecommendedColumns></RecommendedColumns>
<!-- 热门图纸 -->
<PopularDrawings></PopularDrawings>
<!-- 排行榜 -->
<Leaderboard></Leaderboard>
</div>
</template>
<script setup lang="ts">
import KlSearch from '@/layout/kl-search/index.vue'
import KlMenuV2 from '@/layout/kl-menus-v2/index.vue'
import SideMenu from './components/SideMenu.vue'
import MainContent from './components/MainContent.vue'
import RecommendedColumns from './components/RecommendedColumns.vue'
import PopularDrawings from './components/PopularDrawings.vue'
import Leaderboard from './components/Leaderboard.vue'
import LearningRecommendations from './components/LearningRecommendations.vue'
</script>
<style scoped>
.banner-container {
display: flex;
height: 480px;
background-color: #f0f2f5;
border: 1px solid #eeeeee;
}
.side-menu {
flex-shrink: 0;
}
.main-content {
flex: 1;
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="box-border w-100% border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-26px py-30px">
<div class="flex items-center">
<div class="text-28px text-[#333333] font-normal">精选专题</div>
<div class="ml-50px text-21px text-[#999999] font-normal">了解最新趋势发展</div>
</div>
<div class="mt-36px flex justify-between">
<div v-for="item in 4" :key="item" class="flex flex-col items-center">
<img :src="`https://picsum.photos/320/190?_t${new Date().getTime()}`" alt="" srcset="" class="h-190px w320 rounded-4px" />
<div class="mt-10px text-18px text-[#333333] font-normal">机器人</div>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,49 @@
<template>
<div class="relative mt-34px w-100%">
<KlTabBar v-model="query.source" :data="tabBar" />
<div class="absolute right-0px top-10px text-16px text-[#999999] font-normal"
><span class="color-#1A65FF">{{ result.total }}</span
>个筛选结果</div
>
<div class="content mt-10px">
<el-row :gutter="20">
<el-col v-for="(item, index) in result.list" :key="index" :span="6">
<CardPicture :item-info="item" />
</el-col>
</el-row>
<el-empty v-if="!result.list.length" :image="emptyImg"></el-empty>
</div>
</div>
</template>
<script lang="ts" setup>
import KlTabBar from '@/components/kl-tab-bar/index.vue'
import CardPicture from '@/components/kl-card-picture/index.vue'
import { ref } from 'vue'
import { pageRes, pageReq } from '@/api/upnew/types'
import emptyImg from '@/assets/images/empty.png'
const query = defineModel<pageReq>('modelValue', {
required: true,
})
const result = defineModel<pageRes>('result', {
required: true,
})
const tabBar = ref([
{
label: '模型推荐',
value: '',
},
{
label: '原创模型',
value: 1,
},
{
label: '转载分享',
value: 2,
},
])
</script>
<style lang="scss" scoped></style>

101
pages/model/index.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<!-- 导航 -->
<KlNavTab active="模型" :type="3" />
<div class="ma-auto w-1440px">
<!-- 图纸分类 -->
<KlWallpaperCategory v-model="query" v-model:level="level" :type="3" />
<!-- 推荐栏目 -->
<RecommendedColumnsV2 v-model="query" v-model:result="result"></RecommendedColumnsV2>
<!-- 精选专题 -->
<!-- <FeaturedSpecials></FeaturedSpecials> -->
<!-- 分页 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="query.pageNo"
v-model:page-size="query.pageSize"
:page-sizes="[12, 24, 48]"
:total="result.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleChangeSize"
@current-change="handleChangeCurrent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import KlWallpaperCategory from '@/components/kl-wallpaper-category/index.vue'
import RecommendedColumnsV2 from './components/RecommendedColumnsV2.vue'
// import FeaturedSpecials from './components/FeaturedSpecials.vue'
import { reactive, watch, ref } from 'vue'
import { page } from '@/api/upnew/index'
import { pageRes, pageReq } from '@/api/upnew/types'
import { useRoute } from 'vue-router'
const route = useRoute()
const level = ref(
route.query.level
? JSON.parse(route.query.level as string)
: [
{
id: '0',
name: '模型库',
isChildren: false,
},
]
)
const query = reactive<pageReq>({
pageNo: 1,
pageSize: 12,
projectType: '',
editions: '',
source: '',
type: 3,
})
const result = reactive<pageRes>({
list: [],
total: 0,
})
// 如果id存在则设置projectType
if (level.value.length) {
query.projectType = level.value[level.value.length - 1].id || ''
}
const getPage = () => {
page(query).then((res) => {
const { data, code } = res
if (code === 0) {
result.list = data.list
result.total = data.total
}
})
}
getPage()
const handleChangeSize = (size: number) => {
query.pageSize = size
query.pageNo = 1
getPage()
}
const handleChangeCurrent = (current: number) => {
query.pageNo = current
getPage()
}
watch([() => query.projectType, () => query.editions, () => query.source], (val) => {
if (val) {
getPage()
}
})
</script>
<style lang="scss" scoped>
:deep(.el-pagination) {
.el-input__inner {
text-align: center !important;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<el-empty v-if="isError" description="加载中..." class="h-100vh"></el-empty>
<div class="w-100%">
<VuePdfEmbed :source="preview.url" class="w-full" @loaded="handlePdfLoaded" @error="handlePdfError" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import VuePdfEmbed from 'vue-pdf-embed'
const isError = ref(true)
const url = new URL(window.location.href)
const preview = {
url: url.searchParams.get('url') || '',
}
// PDF加载成功回调
const handlePdfLoaded = (pdfInfo: any) => {
isError.value = false
console.log('PDF页数:', pdfInfo.numPages)
// 可在此处添加自定义逻辑
}
// PDF加载失败回调
const handlePdfError = (error: Error) => {
// isError.value = false
console.error('PDF加载失败:', error)
// 可在此处添加错误处理逻辑
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,114 @@
<template>
<div class="box-border w-913px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] px-30px py-21px">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="修改密码" name="修改密码">
<el-form ref="formRef" :model="form" label-width="120px" class="profile-form" autocomplete="off">
<el-form-item
label="手机号"
prop="phone"
class="mt-15px!"
:rules="[
{ required: true, message: '请输入手机号', trigger: ['blur', 'change'] },
{
pattern: /^1[3456789]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change'],
},
]"
>
<el-input v-model="form.phone" type="text" placeholder="请输入手机号" class="h-37px" />
</el-form-item>
<el-form-item label="验证码" prop="code" class="mt-15px!" :rules="{ required: true, message: '请输入验证码', trigger: ['blur', 'change'] }">
<div class="flex items-center gap10">
<el-input v-model="form.code" type="text" placeholder="请输入验证码" class="h-37px" />
<el-button
type="primary"
class="w-110px rd-4px bg-[#1A65FF] px-6px text-center color-#fff line-height-37px h-37px!"
:disabled="counting > 0"
@click="handleCode"
>
{{ counting > 0 ? `${counting}s后重新获取` : '获取验证码' }}</el-button
>
</div>
</el-form-item>
<el-form-item label="新密码" prop="password" class="mt-20px!" :rules="{ required: true, message: '请输入新密码', trigger: ['blur', 'change'] }">
<el-input v-model="form.password" autocomplete="new-password" type="password" placeholder="请输入新密码" class="h-37px" />
</el-form-item>
<el-form-item
label="确认密码"
prop="passwordV2"
class="mt-20px!"
:rules="{ required: true, message: '请再次输入新密码', trigger: ['blur', 'change'] }"
>
<el-input v-model="form.passwordV2" autocomplete="new-password" type="passwordV2" placeholder="请再次输入新密码" class="h-37px" />
</el-form-item>
<el-form-item class="mt-20px!">
<el-button type="primary" class="h-37px w-121px line-height-37px" @click="handleSave">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- <el-tab-pane label="登录设备管理" name="登录设备管理"> </el-tab-pane> -->
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { resetPassoword } from '@/api/login/index'
import { sendSms } from '@/api/common/index'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const activeName = ref('修改密码')
/** 请求入参 */
const form = reactive({
phone: '',
password: '',
passwordV2: '',
code: '',
})
/** 提交 */
const formRef = ref()
const handleSave = async () => {
await formRef.value.validate()
if (form.password !== form.passwordV2) return ElMessage.error('两次密码不一致')
try {
const res = await resetPassoword(form)
const { code } = res
if (code === 0) {
ElMessage.success(`修改密码成功,请重新登录`)
setTimeout(() => {
userStore.logout()
userStore.$reset()
}, 500)
}
} catch (error) {
console.log(error)
}
}
/** 获取验证码 */
const counting = ref(0)
const handleCode = async () => {
await formRef.value.validateField('phone')
try {
const res = await sendSms({ mobile: form.phone, scene: 3 })
const { code } = res
if (code === 0) {
ElMessage.success('验证码已发送')
counting.value = 60
const timer = setInterval(() => {
counting.value--
if (counting.value === 0) {
clearInterval(timer)
}
}, 1000)
}
} catch (error) {
counting.value = 0
console.log(error)
}
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<el-table :data="modelValue" style="width: 100%">
<el-table-column prop="date" label="文件信息">
<template #default="scope">
<div class="flex items-center">
<el-image :src="scope.row.url" alt="" fit="cover" srcset="" class="h-91px w-181px rd-4px" />
<div class="ml-17px">
<div class="text-16px text-[#333333] font-normal">{{ scope.row.title }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="type" label="文件类型" width="180">
<template #default="scope">{{ scope.row.type === 1 ? '图纸' : scope.row.type === 2 ? '文本' : '模型' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="下载时间" width="180">
<template #default="scope">{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { ProjectHistoryResVO } from '@/api/personal-center/types'
import dayjs from 'dayjs'
const modelValue = defineModel<ProjectHistoryResVO[]>('modelValue', {
required: true,
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,46 @@
<template>
<el-table :data="modelValue" style="width: 100%">
<el-table-column prop="date" label="文件信息">
<template #default="scope">
<div class="flex items-center">
<el-image :src="scope.row.iconUrl" alt="" fit="cover" srcset="" class="h-91px w-181px rd-4px" />
<div class="ml-17px">
<div class="text-16px text-[#333333] font-normal">{{ scope.row.title }}</div>
<div class="text-14px text-[#333333] font-normal my-10px!">by {{ scope.row?.ownedUserIdInfo?.nickName }}</div>
<div class="flex items-center">
<div class="flex items-center">
<img src="@/assets/images/look.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.previewPoint }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/add.png" alt="" srcset="" class="h-23px" />
<span class="ml-4px">{{ scope.row.hotPoint }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/chat.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.commentsPoint }}</span>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="文件类型" width="180">
<template #default="scope">{{ scope.row.type === 1 ? '图纸' : scope.row.type === 2 ? '文本' : scope.row.type === 3 ? '模型' : '工具箱' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="下载时间" width="180">
<template #default="scope">{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { ProjectHistoryResVO } from '@/api/personal-center/types'
import dayjs from 'dayjs'
const modelValue = defineModel<ProjectHistoryResVO[]>('modelValue', {
required: true,
}) // 双向绑定的value
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,96 @@
<template>
<KlTabBar v-model="type" :data="tabBar" @change="props.refresh()" />
<el-table :data="modelValue" style="width: 100%" class="mt-14px">
<el-table-column prop="date" label="文件信息">
<template #default="scope">
<div class="flex items-center">
<el-image :src="scope.row.iconUrl" alt="" fit="cover" srcset="" class="h-91px w-181px rd-4px" />
<div class="ml-17px">
<div class="text-16px text-[#333333] font-normal">{{ scope.row.title }}</div>
<div class="text-14px text-[#333333] font-normal my-10px!">{{ scope.row?.ownedUserIdInfo?.nickName }}</div>
<div class="flex items-center">
<div class="flex items-center">
<img src="@/assets/images/look.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.rowpreviewPoint || 0 }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/add.png" alt="" srcset="" class="h-23px" />
<span class="ml-4px">{{ scope.row.hotPoint || 0 }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/chat.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.commentsPoint || 0 }}</span>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="type" label="文件类型" width="100">
<template #default="scope">{{ scope.row.type === 1 ? '图纸' : scope.row.type === 2 ? '文本' : scope.row.type === 3 ? '模型' : '工具箱' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="下载时间" width="180">
<template #default="scope">{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</template>
</el-table-column>
<el-table-column prop="createTime" label="取消收藏" width="100" fixed="right">
<template #default="scope">
<el-link type="primary" :underline="false" @click="handleDelete(scope.row.id)">取消收藏</el-link>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import KlTabBar from '@/components/kl-tab-bar/v2/index.vue'
import { PageResultProjectMemberFavoritesRespVO } from '@/api/personal-center/types'
import { deleteProject } from '@/api/drawe-detail/index'
import dayjs from 'dayjs'
import { useMessage } from '@/utils/useMessage'
const message = useMessage()
const type = defineModel<number | string>('type', {
required: true,
}) // 双向绑定的value
const modelValue = defineModel<PageResultProjectMemberFavoritesRespVO['list']>('modelValue', {
required: true,
}) // 双向绑定的value
const props = defineProps({
// 刷新
refresh: {
type: Function,
default: () => Function,
},
})
const tabBar = ref([
{
label: '图纸',
value: 1,
},
{
label: '模型',
value: 3,
},
{
label: '文本',
value: 2,
},
])
const handleDelete = async (id: number) => {
const r = await message.confirm('确定取消收藏吗?')
if (!r) return
const res = await deleteProject({
id,
})
const { code } = res
if (code === 0) {
ElMessage.success('取消收藏成功')
props.refresh()
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,836 @@
<!-- 弄成一个聊天弹窗 仿照微信聊天弹窗 -->
<template>
<div class="chat-dialog">
<el-dialog
v-model="dialogVisible"
lock-scroll
draggable
:show-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="1000px"
@close="closeDialog"
>
<template #header>
<div class="dialog-header">
<div class="title">在线消息</div>
</div>
</template>
<div class="chat-layout">
<!-- 左侧用户列表 -->
<div class="user-list">
<div class="search-box">
<el-input v-model="searchText" placeholder="搜索" :prefix-icon="Search" clearable />
</div>
<div class="user-items">
<div v-for="(user, k) in filteredUsers" :key="k" :class="['user-item', { active: currentUserId === user.sessionId }]" @click="switchUser(user)">
<el-avatar :size="40" :src="userStore.userInfoRes.id === user.fromId ? user.toUser?.avatar : user.fromUser?.avatar" />
<div class="user-info">
<div class="user-name">{{ user.name }}</div>
<div class="last-message">{{ user.lastMsg.content }}</div>
</div>
<div class="message-status">
<div class="time">{{ handleCreateTime(user.updateTime) }}</div>
<el-badge v-if="user.unreadCount" :value="user.unreadCount" class="unread-badge" />
</div>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="chat-container">
<!-- 聊天头部 -->
<div class="chat-header">
<div class="header-left">
<!-- <el-avatar :size="36" :src="currentUser?.avatar" /> -->
<div class="title-info">
<div class="title">{{ currentUser?.fromId === userStore.userInfoRes.id ? currentUser?.toTitle : currentUser?.fromTitle }}</div>
<!-- <div class="subtitle">{{ currentUser?.status }}</div> -->
</div>
</div>
</div>
<!-- 消息列表区域 -->
<div ref="messageList" class="message-list" @scroll="handleScroll">
<!-- 加载更多提示 -->
<div v-if="chatMessageQuery.hasMore" class="load-more">
<el-icon v-if="chatMessageQuery.loading"><Loading /></el-icon>
<span v-else>上拉加载更多</span>
</div>
<div
v-for="(msg, index) in chatMessages"
:key="index"
:class="['message-item', msg.fromId === userStore.userInfoRes.id ? 'message-sent' : 'message-received']"
>
<div class="avatar">
<el-avatar :size="40" :src="msg.fromId === userStore.userInfoRes.id ? userStore.userInfoRes.avatar : userAvatar" />
</div>
<div class="message-content">
<div v-if="msg.msgType === 0" class="message-bubble whitespace-pre-wrap">{{ msg.content }}</div>
<div v-else-if="msg.msgType === 1" class="message-bubble max-w-50%">
<img :src="msg.content" alt="图片" class="w-100%" />
</div>
<div v-else class="message-bubble max-w-50%">
{{ msg.content.split('/').pop() }}
</div>
<div class="message-time">{{ dayjs(msg.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<div class="toolbar">
<div class="tool-left">
<el-tooltip content="发送图片" placement="top">
<el-icon class="tool-icon" @click="triggerImageUpload"><Picture /></el-icon>
</el-tooltip>
<input ref="imageInputRef" type="file" style="display: none" @change="handleImageUpload" />
<el-popover :visible="showEmoji" placement="top" :width="450" trigger="click">
<template #reference>
<el-icon class="tool-icon emoji-trigger" @click="showEmoji = !showEmoji"><Sunrise /></el-icon>
</template>
<div class="emoji-list">
<span v-for="emoji in emojiList" :key="emoji" class="emoji-item" @click="insertEmoji(emoji)">
{{ emoji }}
</span>
</div>
</el-popover>
</div>
</div>
<div class="input-box">
<el-input
v-model="inputMessage"
type="textarea"
:rows="3"
placeholder="请输入消息Enter 发送"
resize="none"
maxlength="255"
@keydown.enter.prevent="(e: any) => (e.shiftKey ? (inputMessage += '\n') : handleSend(0))"
/>
</div>
<div class="send-btn">
<el-button type="primary" :disabled="!inputMessage.trim() || userList.length === 0" @click="handleSend(0)">
<el-icon class="send-icon"><Position /></el-icon>
发送
</el-button>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { throttle } from 'lodash'
import { ref, computed, onMounted, nextTick } from 'vue'
import { Picture, Position, Sunrise, Search, Loading } from '@element-plus/icons-vue'
import { upload } from '@/api/common'
import { sendSingleChat, conversationList, getChatDetail, clearUnreadMessage } from '@/api/channel/index'
import { chatMessagesReq, msgType, PageResultSessionRespVO, PageResultMessageRespVO } from '@/api/channel/types'
import useUserStore from '@/store/user'
const userStore = useUserStore()
import dayjs from 'dayjs'
const inputMessage = ref('')
const messageList = ref()
const searchText = ref('')
const currentUserId = ref()
const dialogVisible = defineModel<boolean>('dialogVisible', { required: true })
// 获取传过来的数据
const props = defineProps({
sessionType: {
// 会话类型0:单聊 1:群聊,示例值(2)
type: String,
default: '1',
},
})
console.log('会话类型', props.sessionType)
// 模拟数据
const userAvatar = 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
const userList = ref<PageResultSessionRespVO[]>([])
// 获取会话列表
const getConversationList = async (type?: string) => {
const res = await conversationList()
if (res.code === 0) {
userList.value = res.data
?.map((item) => {
return {
...item,
name: userStore.userInfoRes.id === item.fromId ? item.toTitle : item.fromTitle,
lastMsg: item.lastMsg ? JSON.parse(item.lastMsg) : { content: '' },
}
})
.filter((user) => userStore.userInfoRes.nickname !== user.name) // 提出自己
// 默认选中第一个
if (type === 'init' && userList.value.length > 0) {
switchUser(userList.value[0])
}
}
}
getConversationList('init')
// 聊天记录
const chatMessages = ref<PageResultMessageRespVO['list']>([])
const filteredUsers = computed(() => {
if (!searchText.value) return userList.value
return userList.value.filter(
(user) => user.name.toLowerCase().includes(searchText.value.toLowerCase()) || user.lastMsg.content.toLowerCase().includes(searchText.value.toLowerCase())
)
})
const currentUser = computed(() => {
return userList.value.find((user) => user.sessionId === currentUserId.value)
})
const handleCreateTime = (time: string) => {
// 先判断是否是今天 今天取时分 不是今天取年月日
const today = dayjs()
const msgTime = dayjs(time)
if (today.isSame(msgTime, 'day')) {
return msgTime.format('HH:mm')
} else {
return msgTime.format('YYYY-MM-DD')
}
}
// 聊天消息查询
const chatMessageQuery = ref({
pageNo: 1,
pageSize: 10,
hasMore: true,
loading: false,
sessionId: 0,
toId: 0,
isPending: false,
})
const switchUser = async (user: PageResultSessionRespVO) => {
// 切换用户
chatMessageQuery.value.toId = user.fromId === userStore.userInfoRes.id ? user.toId : user.fromId
chatMessageQuery.value.sessionId = currentUserId.value = user.sessionId
user.unreadCount = 0 // 清除未读消息
// 切换用户时,清空聊天记录
chatMessageQuery.value.loading = true
chatMessageQuery.value.pageNo = 1
chatMessageQuery.value.hasMore = true
chatMessages.value = []
await getChatDetailList()
nextTick(() => {
scrollToBottom()
})
// 清空未读消息
clearUnreadMessage({ id: user.sessionId })
}
const getChatDetailList = async () => {
const res = await getChatDetail({
sessionId: chatMessageQuery.value.sessionId,
pageNo: chatMessageQuery.value.pageNo,
pageSize: chatMessageQuery.value.pageSize,
})
if (res.code === 0) {
chatMessageQuery.value.isPending = false
// 数据反过来
if (res.data.list.length > 0) {
// 保存当前滚动位置和内容高度
const scrollElement = messageList.value
const oldScrollHeight = scrollElement.scrollHeight
const oldScrollTop = scrollElement.scrollTop
chatMessages.value = [...res.data.list.reverse(), ...chatMessages.value]
// 在下一个 tick 后调整滚动位置
nextTick(() => {
const newScrollHeight = scrollElement.scrollHeight
const heightDiff = newScrollHeight - oldScrollHeight
scrollElement.scrollTop = oldScrollTop + heightDiff
})
}
chatMessageQuery.value.loading = false
chatMessageQuery.value.pageNo++
// 判断是否还有更多数据
if (res.data.list.length < chatMessageQuery.value.pageSize) {
chatMessageQuery.value.hasMore = false
}
}
}
// 滚动处理函数
const handleScroll = async (e: Event) => {
const target = e.target as HTMLElement
// 当滚动到顶部附近时触发加载更多
if (target.scrollTop < 50 && !chatMessageQuery.value.loading && chatMessageQuery.value.hasMore && !chatMessageQuery.value.isPending) {
// await loadMoreMessages()
chatMessageQuery.value.isPending = true
await throttledGetChatList()
}
}
// 创建节流函数,设置 500ms 的间隔
const throttledGetChatList = throttle(async () => {
await getChatDetailList()
}, 1500)
const handleSend = (msgType: msgType = 0) => {
if (!inputMessage.value.trim()) return
const newMessage = {
msgType: msgType,
content: inputMessage.value,
toId: chatMessageQuery.value.toId, // 对方userId
userId: userStore.userInfoRes.id, // 当前用户id
fromId: userStore.userInfoRes.id, // 当前用户id
createTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'), // 当前时间戳
}
chatMessages.value.push(newMessage)
userStore.mqttClient?.publish(`zbjk_message_single/${currentUserId.value}`, JSON.stringify(newMessage))
// 更新用户列表中的最后一条消息
userLastMessage(newMessage)
// 清空输入框
inputMessage.value = ''
// 滚动到底部
scrollToBottom()
// 发送单聊信息存储到服务器
sendSingleChat(newMessage)
}
const userLastMessage = (msg: chatMessagesReq) => {
console.log('msg', msg)
const user = userList.value.find((u) => u.sessionId === currentUserId.value)
if (user) {
user.lastMsg.content = msg.content
user.updateTime = user.createTime = msg.createTime
}
}
const scrollToBottom = () => {
if (messageList.value) {
nextTick(() => {
messageList.value.scrollTop = messageList.value.scrollHeight
})
}
}
onMounted(() => {
userStore.mqttClient?.onMessage((topic: string, message: any) => {
if (topic.indexOf(`zbjk_message_single`) === -1) return
console.log('接收消息---------', topic, message)
console.log('当前用户id', userStore.userInfoRes.id)
console.log('当前聊天用户id', currentUserId.value)
// 将消息添加到聊天记录中
const msg = JSON.parse(message)
if (msg.sessionId === currentUserId.value) {
msg.msgType = Number(msg.msgType)
// 当前用户发送的消息
chatMessages.value.push(msg)
// 更新当前用户列表中的最后一条消息
userLastMessage(msg)
// 清空未读消息
clearUnreadMessage({ id: msg.sessionId })
if (msg.msgType === 1) {
const img = new Image()
img.src = msg.content
img.onload = () => {
// 滚动到底部
scrollToBottom()
}
} else {
// 滚动到底部
scrollToBottom()
}
} else {
// 添加未读消息
const user = userList.value.find((u) => u.sessionId === msg.sessionId)
if (user) {
user.unreadCount++
user.updateTime = user.createTime = msg.createTime
user.lastMsg.content = msg.content
// 正常会拉取后端聊天接口
} else {
getConversationList('refresh')
}
}
})
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement
if (!target.closest('.emoji-popover') && !target.closest('.emoji-trigger')) {
showEmoji.value = false
}
})
})
// 触发图片上传
const imageInputRef = ref()
const triggerImageUpload = (): void => {
if (imageInputRef.value) {
imageInputRef.value.click()
}
}
// 处理图片上传
const handleImageUpload = async (event: Event) => {
const target = event.target as HTMLInputElement
const files = target.files
if (files && files.length > 0) {
const file = files[0]
// 检查文件类型 0:文本 1:图片 2:视频 3:文件
const msgType = file.type.startsWith('image/') ? 1 : file.type.startsWith('video/') ? 2 : 3
const formData = new FormData()
formData.append('fieldName', file?.name)
formData.append('file', file as Blob)
const res = await upload('/prod-api/app-api/infra/file/upload', formData)
if (res.code === 0) {
const imageUrl = res.data
if (msgType === 1) {
// 预加载图片
const img = new Image()
img.src = imageUrl
img.onload = () => {
inputMessage.value = imageUrl
handleSend(msgType)
}
} else {
inputMessage.value = imageUrl
handleSend(msgType)
}
}
// 重置文件输入以允许重复选择同一文件
if (imageInputRef.value) {
imageInputRef.value.value = ''
}
}
}
const showEmoji = ref(false)
const emojiList = [
'😀',
'😃',
'😄',
'😁',
'😆',
'😅',
'😂',
'🤣',
'😊',
'😇',
'🙂',
'🙃',
'😉',
'😌',
'😍',
'🥰',
'😘',
'😗',
'😙',
'😚',
'😋',
'😛',
'😝',
'😜',
'🤪',
'🤨',
'🧐',
'🤓',
'😎',
'🤩',
'🥳',
'😏',
'😒',
'😞',
'😔',
'😟',
'😕',
'🙁',
'☹️',
'😣',
]
const insertEmoji = (emoji: string) => {
inputMessage.value += emoji
showEmoji.value = false
}
const closeDialog = () => {
dialogVisible.value = false
}
</script>
<style lang="scss" scoped>
.emoji-list {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 8px;
.emoji-item {
cursor: pointer;
font-size: 20px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
border-radius: 4px;
transform: scale(1.2);
}
}
}
.chat-dialog {
:deep(.el-dialog__header) {
margin: 0;
padding: 0;
border-radius: 8px 8px 0 0;
overflow: hidden;
}
:deep(.el-dialog__body) {
padding: 0px !important;
overflow: hidden;
display: flex;
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 12px 32px 4px rgba(0, 0, 0, 0.1);
}
:deep(.el-dialog__headerbtn) {
top: 24px;
right: 12px;
z-index: 10;
.el-dialog__close {
font-size: 16px;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
}
}
}
.dialog-header {
display: flex;
align-items: center;
padding: 0 16px;
// height: 40px;
.title {
font-size: 14px;
font-weight: normal;
}
}
.chat-layout {
display: flex;
flex: 1;
}
.user-list {
width: 280px;
border-right: 1px solid #eee;
display: flex;
flex-direction: column;
background-color: #34353a;
.search-box {
padding: 12px 12px 12px 12px;
:deep(.el-input__wrapper) {
background-color: #4f5054 !important;
box-shadow: none !important;
}
:deep(.el-input__inner) {
color: #fff;
border-radius: 16px;
border: none;
&:focus {
box-shadow: none;
}
}
}
.user-items {
flex: 1;
overflow-y: auto;
.user-item {
display: flex;
align-items: flex-start;
padding: 12px;
cursor: pointer;
transition: all 0.3s;
&:hover {
// background-color: #f5f5f5;
}
&.active {
background-color: rgba(255, 255, 255, 0.12) !important;
}
.user-info {
flex: 1;
margin: 0 12px;
overflow: hidden;
.user-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
color: #fff;
}
.last-message {
font-size: 14px;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.message-status {
text-align: right;
.time {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.unread-badge {
:deep(.el-badge__content) {
background-color: #c561f9;
}
}
}
}
}
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
.chat-header {
padding: 16px 20px;
background: #fff;
border-bottom: 1px solid #eee;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.title-info {
.title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.subtitle {
font-size: 12px;
color: #999;
}
}
}
}
}
.message-list {
flex: 1;
padding: 24px;
box-sizing: border-box;
overflow-y: auto;
background-color: #f7f8fa;
min-height: 300px;
.load-more {
text-align: center;
padding: 10px 0;
color: #999;
font-size: 12px;
}
.message-item {
display: flex;
margin-bottom: 24px;
&.message-sent {
flex-direction: row-reverse;
.message-content {
margin-right: 12px;
margin-left: 80px;
align-items: flex-end;
.message-bubble {
background: #1a65ff;
color: #fff;
border-radius: 12px 2px 12px 12px;
box-shadow: 0 4px 12px rgba(197, 97, 249, 0.2);
}
}
}
&.message-received {
.message-content {
margin-left: 12px;
margin-right: 80px;
.message-bubble {
background-color: #fff;
border-radius: 2px 12px 12px 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}
}
}
.message-content {
display: flex;
flex-direction: column;
.message-bubble {
padding: 12px 16px;
font-size: 14px;
max-width: 400px;
word-break: break-word;
line-height: 1.6;
}
.message-time {
font-size: 12px;
color: #999;
margin-top: 6px;
}
}
}
.input-area {
background-color: #fff;
border-top: 1px solid #eee;
padding: 16px 24px;
box-sizing: border-box;
.toolbar {
display: flex;
justify-content: space-between;
padding: 0 0 12px;
.tool-left {
display: flex;
gap: 20px;
}
.tool-icon {
font-size: 20px;
color: #666;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #c561f9;
transform: scale(1.1);
}
}
}
.input-box {
margin-bottom: 12px;
:deep(.el-textarea__inner) {
border: 1px solid #fff !important;
border-radius: 8px;
padding: 12px;
font-size: 14px;
transition: all 0.3s;
outline: none !important;
box-shadow: none !important;
&:focus {
border-color: #c561f9;
box-shadow: none !important;
// box-shadow: 0 0 0 2px rgba(197, 97, 249, 0.1);
}
&::placeholder {
color: #999;
}
}
}
.send-btn {
display: flex;
justify-content: flex-end;
.el-button {
background: #1a65ff;
border: none;
padding: 10px 24px;
font-size: 14px;
border-radius: 8px;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
.send-icon {
font-size: 16px;
}
&:hover {
transform: translateY(-1px);
// box-shadow: 0 4px 12px rgba(197, 97, 249, 0.3);
}
&:active {
transform: translateY(0);
}
&.is-disabled {
background: #e0e0e0;
opacity: 0.6;
}
}
}
}
// 自定义滚动条样式
.message-list {
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 3px;
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<!-- 交易记录表格 -->
<el-table v-loading="result.loading" :data="result.data" style="width: 100%">
<el-table-column prop="bizType" label="交易分类">
<template #default="{ row }">
{{ bizTypeMap[row.bizType as number] || '未知' }}
</template>
</el-table-column>
<el-table-column prop="price" label="交易金额"> </el-table-column>
<el-table-column prop="title" label="标题" />
<el-table-column prop="createTime" label="交易时间">
<template #default="{ row }">
{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination mt-15px">
<el-pagination v-model:current-page="query.pageNo" :page-size="10" :total="result.total" background layout="prev, pager, next, jumper" />
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
// import { accDiv } from '@/utils/utils'
import dayjs from 'dayjs'
import { bizTypeMap } from '@/enum/index'
import { getWalletRechargeRecordPage } from '@/api/pay/index'
import { AppPayWalletRechargeRespVO } from '@/api/pay/types'
const result = reactive({
data: [] as AppPayWalletRechargeRespVO[],
total: 0,
loading: false,
})
const query = reactive({
pageNo: 1,
pageSize: 10,
})
// 获取交易记录
const getTradeRecords = async () => {
try {
result.loading = true
const res = await getWalletRechargeRecordPage(query)
result.data = res.data.list || []
result.total = res.data.total || 0
} finally {
result.loading = false
}
}
getTradeRecords()
</script>
<style lang="scss" scoped>
:deep(.el-pagination) {
.el-input__inner {
text-align: center !important;
}
}
</style>

View File

@ -0,0 +1,237 @@
<template>
<el-dialog v-model="visible" width="800px" class="vip-dialog" align-center>
<template #header>
<div class="vip-modal-title">钱包充值</div>
</template>
<div v-loading="loading" class="vip-cards">
<div v-for="item in viplist" :key="item.id" class="vip-card">
<div class="relative w-100% flex flex-col items-center">
<div class="vip-card-header basic">
<div class="vip-card-title">{{ item.name }}</div>
<!-- <div class="vip-card-subtitle">中小微企业</div> -->
</div>
<div class="vip-card-price">
<span class="price">{{ accDiv(item.payPrice || 0, 100) }}</span>
<!-- <span class="per">/1</span> -->
</div>
<ul class="vip-card-features">
<li
>赠送<span class="color-red">{{ accDiv(item.bonusPrice || 0, 100) }}</span></li
>
<!-- <li>1. {{ item.profile }}</li>
<li
>2. 佣金比例<span class="color-red">{{ item.brokerageRate }}</span
>%</li
> -->
</ul>
<div v-if="item.qrCodeUrl" class="vip-card-qrcode">
<el-icon class="absolute right--10px top-0px cursor-pointer" @click="item.qrCodeUrl = ''"><Close /></el-icon>
<qrcode-vue :value="item.qrCodeUrl" :size="140" level="H" />
<div>请使用微信扫二维码</div>
</div>
</div>
<el-button class="vip-card-btn" :loading="item.btnloading" @click="pay(item)">立即充值</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { listWalletRechargePackage, submitPayOrder, getPayStatus } from '@/api/pay/index'
import { accDiv } from '@/utils/utils'
import { Close } from '@element-plus/icons-vue'
import type { AppPayWalletPackageRespVO } from '@/api/pay/types'
// @ts-ignore
import QrcodeVue from 'qrcode.vue'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'refresh'])
const visible = ref(props.modelValue)
watch(
() => props.modelValue,
(val) => (visible.value = val)
)
watch(visible, (val) => emit('update:modelValue', val))
const viplist = ref<AppPayWalletPackageRespVO[]>([])
const loading = ref(false)
const init = async () => {
try {
loading.value = true
const res = await listWalletRechargePackage()
viplist.value = res.data
} finally {
loading.value = false
console.log(viplist.value)
}
}
onMounted(() => {
init()
})
const orderId = ref(0)
const pay = async (row: AppPayWalletPackageRespVO) => {
if (row.qrCodeUrl) return ElMessage.error('支付二维码已生成,请勿重复生成')
viplist.value.forEach((item) => {
if (item.id !== row.id) {
item.qrCodeUrl = ''
}
})
try {
row.btnloading = true
const res = await submitPayOrder({
id: row.id,
memberId: userStore.userId, // 用户id
channelCode: 'wx_native',
})
if (res.code === 0) {
row.qrCodeUrl = res.data.displayContent
orderId.value = res.data.orderId
// 打开轮询任务
createQueryInterval(row)
}
} finally {
row.btnloading = false
}
}
/** 轮询查询任务 */
const interval = ref()
const createQueryInterval = (row: AppPayWalletPackageRespVO) => {
if (interval.value) {
return
}
interval.value = setInterval(async () => {
const data = await getPayStatus({ id: orderId.value })
// 已支付
if (data.data.status === 10) {
clearQueryInterval(row)
ElMessage.success('支付成功!')
emit('refresh')
}
// 已取消
if (data.data.status === 20) {
clearQueryInterval(row)
ElMessage.error('支付已关闭!')
}
}, 1000 * 2)
}
/** 清空查询任务 */
const clearQueryInterval = (row: AppPayWalletPackageRespVO) => {
// 清空各种弹窗
row.qrCodeUrl = ''
// 清空任务
clearInterval(interval.value)
interval.value = undefined
}
</script>
<style scoped>
.vip-modal-title {
text-align: center;
font-size: 20px;
font-weight: bold;
margin-bottom: 24px;
}
.vip-cards {
display: flex;
gap: 32px;
justify-content: center;
margin: 24px 0;
}
.vip-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
padding: 24px 32px;
width: 260px;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 70px;
position: relative;
}
.vip-card-header {
width: 100%;
text-align: center;
padding-bottom: 12px;
border-radius: 8px 8px 0 0;
margin-bottom: 12px;
padding: 10px;
box-sizing: border-box;
}
.vip-card-header.basic {
background: linear-gradient(90deg, #f7b7a3, #e97c6a);
}
.vip-card-header.pro {
background: linear-gradient(90deg, #f7b7a3, #e97c6a);
}
.vip-card-title {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.vip-card-subtitle {
font-size: 14px;
color: #fff;
}
.vip-card-price {
margin: 12px 0;
font-size: 28px;
color: #e74c3c;
font-weight: bold;
display: flex;
align-items: baseline;
gap: 8px;
}
.vip-card-price .origin {
font-size: 16px;
color: #bbb;
text-decoration: line-through;
margin-left: 4px;
}
.vip-card-price .per {
font-size: 16px;
color: #888;
margin-left: 4px;
}
.vip-card-features {
list-style: none;
padding: 0;
margin: 0 0 16px 0;
color: #444;
font-size: 15px;
}
.vip-card-features li {
margin-bottom: 6px;
}
.vip-card-features .highlight {
color: #e74c3c;
font-weight: bold;
}
.vip-card-btn {
width: 80%;
margin-top: 8px;
border-radius: 24px;
position: absolute;
bottom: 20px;
}
:deep(.vip-card-qrcode) {
position: absolute !important;
inset: 0 !important;
z-index: 1;
text-align: center;
background-color: #fff;
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<KlTabBar v-model="type" :data="tabBar" @change="props.refresh()" />
<el-table :data="modelValue" style="width: 100%" class="mt-14px">
<el-table-column prop="date" label="文件信息">
<template #default="scope">
<div class="flex items-center">
<el-image :src="scope.row.iconUrl" fit="cover" alt="" srcset="" class="h-91px w-181px rd-4px" />
<div class="ml-17px">
<div class="text-16px text-[#333333] font-normal">{{ scope.row.title }}</div>
<div class="text-14px text-[#333333] font-normal my-10px!">{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div class="flex items-center">
<div class="flex items-center">
<img src="@/assets/images/look.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.previewPoint }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/add.png" alt="" srcset="" class="h-23px" />
<span class="ml-4px">{{ scope.row.hotPoint }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/chat.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.commentsPoint }}</span>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="上传状态" width="180">
<template #default="scope">
{{ handleStatus(scope.row.status) }}
</template>
</el-table-column>
<el-table-column prop="address" label="操作" width="100">
<template #default="scope">
<el-link v-if="scope.row.status === 4" type="primary" :underline="false" @click="handleXiaJia(scope.row)">下架</el-link>
<el-link type="primary" :underline="false" @click="handleDelete(scope.row)">删除</el-link>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { offShelf, deleteResource } from '@/api/personal-center'
import KlTabBar from '@/components/kl-tab-bar/v2/index.vue'
import dayjs from 'dayjs'
import { useMessage } from '@/utils/useMessage'
const message = useMessage()
const type = defineModel<number | string>('type', {
required: true,
}) // 双向绑定的value
const modelValue = defineModel<any>('modelValue', {
required: true,
}) // 双向绑定的value
const props = defineProps({
// 刷新
refresh: {
type: Function,
default: () => Function,
},
})
const tabBar = ref([
{
label: '图纸',
value: 1,
},
{
label: '模型',
value: 3,
},
{
label: '文本',
value: 2,
},
])
const handleStatus = (status: number) => {
switch (status) {
case 1:
return '草稿'
case 2:
return '提交审核'
case 3:
return '审核成功'
case 4:
return '下架'
default:
return ''
}
}
const handleXiaJia = (row: any) => {
offShelf(row.id).then((res: any) => {
if (res.code === 0) {
ElMessage.success('下架成功')
props.refresh()
}
})
}
const handleDelete = async (row: any) => {
const r = await message.confirm('是否删除该资源', '提示')
if (!r) return
deleteResource({ id: row.id }).then((res: any) => {
if (res.code === 0) {
ElMessage.success('删除成功')
props.refresh()
}
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,325 @@
<template>
<div class="dashboard">
<div class="chart-row">
<div class="chart-container">
<div ref="incomeChartRef" class="chart"></div>
</div>
<div class="chart-container">
<div ref="activeChartRef" class="chart"></div>
</div>
</div>
<div class="chart-container full-width">
<div ref="downloadChartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import { getRecentIncomeAndActive, getResourceDistribution } from '@/api/personal-center/index'
const incomeChartRef = ref<HTMLElement>()
const activeChartRef = ref<HTMLElement>()
const downloadChartRef = ref<HTMLElement>()
let incomeChart: echarts.ECharts | null = null
let activeChart: echarts.ECharts | null = null
let downloadChart: echarts.ECharts | null = null
// 收益图表配置
const incomeOption = {
title: {
text: '近7天收益',
top: 6,
left: 10,
// 添加样式属性
textStyle: {
fontSize: 16, // 字体大小
color: '#333', // 字体颜色
fontWeight: 400, // 字体粗细
},
},
tooltip: {
trigger: 'axis',
},
grid: {
top: '20%',
left: '4%',
right: '6%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
min: 0,
// max: 6000,
},
series: [
{
name: '收益',
type: 'line',
smooth: true,
areaStyle: {
opacity: 0.3,
color: '#4B7BEC',
},
lineStyle: {
color: '#4B7BEC',
},
data: [4000, 4500, 4200, 3800, 2800, 2600, 3200],
markLine: {
silent: true,
lineStyle: {
type: 'dashed',
color: '#4B7BEC',
},
data: [
{
xAxis: 4,
},
],
},
markPoint: {
data: [
{
coord: [4, 0.2],
value: '今日收益\n3624',
symbol: 'circle',
symbolSize: 8,
},
],
},
},
],
}
// 活跃度图表配置
const activeOption = {
title: {
text: '近7天活跃度',
top: 6,
left: 10,
// 添加样式属性
textStyle: {
fontSize: 16, // 字体大小
color: '#333', // 字体颜色
fontWeight: 400, // 字体粗细
},
},
tooltip: {
trigger: 'axis',
},
grid: {
top: '20%',
left: '4%',
right: '7%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
min: 0,
// max: 600,
},
series: [
{
name: '活跃度',
type: 'line',
smooth: true,
areaStyle: {
opacity: 0.3,
color: '#4B7BEC',
},
lineStyle: {
color: '#4B7BEC',
},
data: [400, 450, 420, 360, 280, 260, 320],
markLine: {
silent: true,
lineStyle: {
type: 'dashed',
color: '#4B7BEC',
},
data: [
{
xAxis: 3,
},
],
},
markPoint: {
data: [
{
coord: [3, 360],
value: '今日活跃度\n360次',
symbol: 'circle',
symbolSize: 8,
},
],
},
},
],
}
// 下载分布图表配置
const downloadOption = {
title: {
text: '资源下载分布',
top: 6,
left: 10,
// 添加样式属性
textStyle: {
fontSize: 16, // 字体大小
color: '#333', // 字体颜色
fontWeight: 400, // 字体粗细
},
},
tooltip: {
trigger: 'axis',
},
legend: {
data: ['图纸', '文本', '模型'],
right: 10,
top: 10,
},
grid: {
top: '20%',
left: '2%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日', '1月8日', '1月9日', '1月10日'],
},
yAxis: {
type: 'value',
max: 600,
},
series: [
{
name: '图纸',
type: 'bar',
data: [220, 340, 220, 340, 220, 340, 220, 340, 220, 340],
color: '#2ECC71',
},
{
name: '文本',
type: 'bar',
data: [300, 430, 320, 430, 320, 430, 320, 430, 320, 430],
color: '#4B7BEC',
},
{
name: '模型',
type: 'bar',
data: [340, 280, 340, 280, 340, 280, 340, 280, 340, 280],
color: '#FFA502',
},
],
}
// 初始化图表
const initCharts = async () => {
// 获取今天星期几
// const today = new Date().getDay()
if (incomeChartRef.value) {
const res = await getRecentIncomeAndActive({
type: 1,
limit: 7,
})
incomeOption.xAxis.data = res.data.xaxis
incomeOption.series[0].data = res.data.data
const index = res.data.xaxis.findIndex((item) => item === res.data.checkedXAxis)
incomeOption.series[0].markLine.data[0].xAxis = index
incomeOption.series[0].markPoint.data[0].coord = [index, res.data.data[index]]
incomeOption.series[0].markPoint.data[0].value = `今日收益\n${res.data.data[index]}`
incomeChart = echarts.init(incomeChartRef.value)
incomeChart.setOption(incomeOption)
}
if (activeChartRef.value) {
const res = await getRecentIncomeAndActive({
type: 2,
limit: 7,
})
activeOption.xAxis.data = res.data.xaxis
activeOption.series[0].data = res.data.data
const index = res.data.xaxis.findIndex((item) => item === res.data.checkedXAxis)
activeOption.series[0].markLine.data[0].xAxis = index
activeOption.series[0].markPoint.data[0].coord = [index, res.data.data[index]]
activeOption.series[0].markPoint.data[0].value = `今日活跃度\n${res.data.data[index]}`
activeChart = echarts.init(activeChartRef.value)
activeChart.setOption(activeOption)
}
if (downloadChartRef.value) {
const res = await getResourceDistribution({
type: 3,
limit: 10,
})
downloadOption.xAxis.data = res.data.xaxis
downloadOption.series[0].data = res.data.series[0].data
downloadOption.series[1].data = res.data.series[1].data
downloadOption.series[2].data = res.data.series[2].data
downloadChart = echarts.init(downloadChartRef.value)
downloadChart.setOption(downloadOption)
}
}
// 监听窗口大小变化
const handleResize = () => {
incomeChart?.resize()
activeChart?.resize()
downloadChart?.resize()
}
onMounted(() => {
initCharts()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
incomeChart?.dispose()
activeChart?.dispose()
downloadChart?.dispose()
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.dashboard {
/* padding: 20px; */
/* background-color: #f5f6fa; */
margin-top: 24px;
width: 913px;
}
.chart-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.chart-container {
flex: 1;
padding: 15px;
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); */
background: #ffffff;
border-radius: 6px;
border: 1px solid #eeeeee;
}
.chart {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<div class="box-border w-913px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] px-30px py-18px">
<div class="flex items-center">
<img :src="userStore.userInfoRes.avatar" alt="" srcset="" class="h-105px w-105px rounded-full" />
<div class="ml-29px">
<div class="flex items-center">
<span class="text-20px text-[#333333] font-normal">Hi{{ userStore.userInfoRes.nickname }}</span>
<img v-if="userStore.userInfoRes.vipLevel === 1" src="@/assets/svg/vip.svg" alt="" class="relative top-2px ml-5px" />
<img v-if="userStore.userInfoRes.vipLevel === 2" src="@/assets/svg/svip.svg" alt="" class="relative top-2px ml-5px" />
<div
class="ml-18px h-30px w-80px cursor-pointer border border-[#1A65FF] rounded-15px border-solid text-center text-14px text-[#1A65FF] font-normal line-height-30px"
@click="handleClick"
>编辑资料</div
>
</div>
<div class="mt-20px flex items-center text-14px text-[#333333] font-normal">
<div class="flex items-center">
<img src="@/assets/images/cad_0 (1).png" alt="" srcset="" />
<span class="ml-4px">我的积分: {{ userStaticInfo?.pointCount || 0 }}</span>
</div>
<div class="ml-37px flex items-center">
<img src="@/assets/images/cad_0 (2).png" alt="" srcset="" />
<span class="ml-4px">我的收藏: {{ userStaticInfo?.followCount || 0 }}</span>
</div>
<div class="ml-37px flex items-center">
<img src="@/assets/images/cad_0 (3).png" alt="" srcset="" />
<span class="ml-4px">我的发布: {{ userStaticInfo?.projectCount || 0 }}</span>
</div>
<div class="ml-37px flex items-center">
<img src="@/assets/images/cad_0 (4).png" alt="" srcset="" />
<span class="ml-4px">我的下载: {{ userStaticInfo?.downloadCount || 0 }}</span>
</div>
</div>
</div>
</div>
<div class="mt-30px flex items-center justify-around">
<div class="flex items-center">
<div class="relative">
<img src="@/assets/images/info_1 (3).png" alt="" srcset="" />
<img src="@/assets/images/info_1 (4).png" alt="" srcset="" class="absolute left-18px top-18px" />
</div>
<div class="ml-18px">
<div class="flex items-center">
<span class="info_num text-[#17A86D]">{{ userStaticInfo?.currencyCount || 0 }}</span>
<div class="info_pay cursor-pointer" @click="handlePay">充值</div>
</div>
<div class="font">金币余额</div>
</div>
</div>
<div class="flex items-center">
<div class="relative">
<img src="@/assets/images/info_1 (5).png" alt="" srcset="" />
<img src="@/assets/images/info_1 (6).png" alt="" srcset="" class="absolute left-18px top-22px" />
</div>
<div class="ml-18px">
<div>
<span class="info_num text-[#328CD7]">{{ userStaticInfo?.previewCount || 0 }}</span>
</div>
<div class="font">今日浏览量</div>
</div>
</div>
<div class="flex items-center">
<div class="relative">
<img src="@/assets/images/info_1 (1).png" alt="" srcset="" />
<img src="@/assets/images/info_1 (2).png" alt="" srcset="" class="absolute left-20px top-18px" />
</div>
<div class="ml-18px">
<div>
<span class="info_num text-[#FFC415]">{{ userStaticInfo?.revenueCount || 0 }}</span>
</div>
<div class="font">今日收益</div>
</div>
</div>
</div>
</div>
<!-- -->
<div class="mt-23px box-border h-183px w-913px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] px-30px py-21px">
<div class="title">快捷入口</div>
<div class="mt-20px flex items-center">
<div class="info_item cursor-pointer" @click="handleClickPush('/upnew/drawe')">
<img src="@/assets/images/fabu_2 (3).png" alt="" srcset="" />
<div class="mt-10px">发布资源</div>
</div>
<div class="info_item ml-31px cursor-pointer" @click="handleClickPush('/communication/channel')">
<img src="@/assets/images/fabu_2 (1).png" alt="" srcset="" />
<div class="mt-10px">交流频道</div>
</div>
<div class="info_item ml-31px cursor-pointer" @click="handleService">
<img src="@/assets/images/fabu_2 (2).png" alt="" srcset="" />
<div class="mt-10px">消息</div>
</div>
</div>
</div>
<!-- echarts -->
<InfoEcharts></InfoEcharts>
<!-- 打开消息弹窗 弄成组件 -->
<Message v-if="dialogVisible" v-model:dialog-visible="dialogVisible"></Message>
<!-- 充值弹窗 -->
<Pay v-if="payVisible" v-model="payVisible" @refresh="fetchUserStatistics"></Pay>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getUserStatistics } from '@/api/personal-center/index'
import { UserStatisticsCountRespVO } from '@/api/personal-center/types'
import Message from './components/message.vue'
import InfoEcharts from './info-echarts.vue'
import Pay from './components/pay.vue'
import useUserStore from '@/store/user'
const userStore = useUserStore()
// 路由跳转
import { useRouter } from 'vue-router'
const router = useRouter()
// 获取用户统计信息
const userStaticInfo = ref<UserStatisticsCountRespVO>()
const fetchUserStatistics = async () => {
const res = await getUserStatistics()
userStaticInfo.value = res.data
}
fetchUserStatistics()
const handleClick = () => {
router.push({ path: '/personal/profile' })
}
const payVisible = ref(false)
const handlePay = () => {
payVisible.value = true
// router.push({ path: '/personal/trading/center' })
}
const handleClickPush = (path: string) => {
if (!path) {
return
}
window.open(path, '_blank')
}
const dialogVisible = ref(false)
const handleService = () => {
dialogVisible.value = true
}
</script>
<style scoped lang="scss">
.info_num {
font-family: Microsoft YaHei;
font-weight: 400;
font-size: 24px;
}
.font {
font-weight: 400;
font-size: 14px;
color: #999999;
margin-top: 6px;
}
.info_pay {
width: 60px;
height: 24px;
border-radius: 12px;
border: 1px solid #1a65ff;
font-weight: 400;
font-size: 14px;
color: #1a65ff;
text-align: center;
line-height: 24px;
margin-left: 7px;
}
.title {
padding-bottom: 16px;
border-bottom: 1px solid #eeeeee;
font-weight: 400;
font-size: 16px;
color: #333333;
}
.info_item {
width: 121px;
height: 81px;
border-radius: 4px;
border: 1px solid #1a65ff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-weight: 400;
font-size: 16px;
color: #333333;
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<div class="system-message">
<!-- 顶部导航 -->
<div class="nav-tabs">
<el-tabs v-model="activeTab">
<el-tab-pane label="系统消息" name="system"></el-tab-pane>
<el-tab-pane label="交易消息" name="trade"></el-tab-pane>
<el-tab-pane label="论坛社区互动" name="forum"></el-tab-pane>
</el-tabs>
<div class="read-all">
<el-button type="primary" link>全部已读</el-button>
</div>
</div>
<!-- 消息列表 -->
<div class="message-list">
<div v-for="(message, index) in messages" :key="index" class="message-item">
<div class="message-icon">
<el-icon size="24">
<Bell />
</el-icon>
</div>
<div class="message-content">
<div class="message-title">
<span>{{ message.title }}</span>
</div>
<div class="message-desc">{{ message.description }}</div>
</div>
<div class="message-time">{{ message.time }}</div>
</div>
</div>
<!-- 查看更多 -->
<div class="view-more">
<el-button type="primary" link>查看更多 >></el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Bell } from '@element-plus/icons-vue'
const activeTab = ref('system')
interface Message {
title: string
description: string
time: string
}
const messages = ref<Message[]>([
{
title: '国庆夕金币充值优惠!',
description: '国庆夕限时优惠微信满500送50金币充200送10金币',
time: '2025-03-05 17:00:57',
},
{
title: '国庆夕金币充值优惠!',
description: '国庆夕限时优惠微信满500送50金币充200送10金币',
time: '2025-03-05 17:00:57',
},
])
</script>
<style scoped>
.system-message {
/* background-color: #f0f6ff; */
width: 913px;
padding: 20px 25px;
box-sizing: border-box;
background: #ffffff;
border-radius: 6px;
border: 1px solid #eeeeee;
}
.nav-tabs {
display: flex;
justify-content: space-between;
align-items: center;
background: white;
border-radius: 8px;
margin-bottom: 20px;
width: 100%;
}
.read-all {
padding-right: 20px;
}
.message-list {
background: white;
border-radius: 8px;
width: 100%;
}
.message-item {
display: flex;
align-items: flex-start;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.message-item:last-child {
border-bottom: none;
}
.message-icon {
color: #409eff;
margin-right: 15px;
flex-shrink: 0;
}
.message-content {
flex: 1;
min-width: 0; /* 防止文本溢出 */
}
.message-title {
font-weight: bold;
margin-bottom: 5px;
}
.message-desc {
color: #666;
font-size: 14px;
}
.message-time {
color: #999;
font-size: 12px;
flex-shrink: 0;
margin-left: 15px;
}
.view-more {
text-align: center;
padding: 10px 0;
color: #409eff;
background: white;
margin-top: 20px;
border-radius: 8px;
width: 100%;
}
:deep(.el-tabs__header) {
margin-bottom: 0;
padding-left: 20px;
}
:deep(.el-tabs__nav-wrap) {
padding-right: 20px;
}
</style>

View File

@ -0,0 +1,429 @@
<template>
<div class="box-border h-1082px w-913px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] px-30px py-21px">
<div class="flex items-center justify-between border-b-1px border-b-[#eeeeee] border-b-solid pb-18px">
<div class="text-16px text-[#333333] font-normal">个人资料</div>
<div class="flex items-center">
<!-- <img src="@/assets/images/fans.png" alt="" srcset="" /> -->
<span class="ml-8px text-14px text-[#333333] font-normal"></span>
</div>
</div>
<div class="user-profile-container">
<div class="avatar-section">
<el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleAvatarChange">
<div class="flex flex-col items-center">
<el-avatar :size="100" :src="userForm.avatar" />
<div class="mt-15px">
<el-button type="primary" plain>更改头像</el-button>
</div>
</div>
</el-upload>
</div>
<el-form ref="userFormRef" :model="userForm" label-width="120px" class="profile-form" :rules="rules">
<!-- User avatar section -->
<!-- User information section -->
<el-form-item label="用户名:" prop="nickname">
<div class="flex items-center">
<el-input v-model="userForm.nickname" class="w-247px" />
<el-button type="primary" class="verify-btn" @click="handleVerify">实名认证</el-button>
</div>
</el-form-item>
<!-- <el-form-item label="真实姓名:" prop="trueName">
<div class="flex items-center">
<el-input v-model="userForm.trueName" placeholder="请输入真实姓名" class="w-247px" />
<el-button type="primary" class="verify-btn" @click="handleVerify">实名认证</el-button>
</div>
</el-form-item> -->
<el-form-item label="手机号:" prop="phone">
<div class="flex items-center">
<el-input v-model="userForm.phone" disabled class="w-247px" />
<!-- <el-link type="primary" class="modify-link">修改</el-link> -->
</div>
</el-form-item>
<el-form-item label="电子邮箱:" prop="email">
<div class="flex items-center">
<el-input v-model="userForm.email" placeholder="请输入电子邮箱" class="w-247px" />
<!-- <el-link type="primary" class="modify-link">绑定</el-link> -->
</div>
</el-form-item>
<div class="flex items-center">
<el-form-item label="所在地区:" prop="isDomestic">
<el-select v-model="userForm.isDomestic" placeholder="请选择" class="w-120px!" @change="handleCountryChange">
<el-option label="国内" :value="1"></el-option>
<el-option label="国外" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label-width="6px" prop="province">
<el-select v-model="userForm.province" placeholder="请选择省份" class="w-120px" @change="handleProvinceChange">
<el-option v-for="item in provinceList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label-width="6px" prop="city">
<el-select v-model="userForm.city" placeholder="请选择城市" class="w-120px" @change="handleCityChange">
<el-option v-for="item in cityList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label-width="6px" prop="county">
<el-select v-model="userForm.county" placeholder="请选择区县" class="w-120px">
<el-option v-for="item in countyList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</div>
<el-form-item label="技能标签:" prop="labels">
<el-select
v-model="userForm.labels"
:remote-method="remoteMethod"
:loading="loading"
multiple
filterable
remote
placeholder="请输入搜索标签"
class="w-498px!"
>
<el-option v-for="(item, index) in labelsList" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="技能证书:" prop="files">
<KlUploader
v-model:file-list="userForm.files"
list-type="picture-card"
:limit="1000"
:size="1"
tips="上传图片支持jpg/gif/png格式、第一张为封面图片、每张图片大小不得超过1M"
>
<div class="h-77px w-161px flex items-center justify-center bg-[#fafafa]">
<el-icon class="text-[#999999]"><Plus /></el-icon>
<div class="ml-4px mt-2px text-14px text-[#999999] font-normal">上传图纸</div>
</div>
</KlUploader>
</el-form-item>
<el-form-item label="个人简介:" prop="description">
<el-input v-model="userForm.description" type="textarea" :rows="5" placeholder="请输入个人简介" class="full-width" />
</el-form-item>
<el-form-item>
<el-button type="primary" class="w-120px !h-37px" @click="submitForm">提交</el-button>
</el-form-item>
</el-form>
</div>
<div class="flex items-center justify-between border-b-1px border-b-[#eeeeee] border-b-solid pb-18px">
<div class="text-16px text-[#333333] font-normal">社交帐号绑定</div>
</div>
<div class="flex flex-col justify-center text-14px text-[#333333] font-normal">
<div class="mt-30px flex items-center">
<img src="@/assets/images/qq-v2.png" alt="" srcset="" class="h-35px w-34px" />
<div class="ml-19px">QQ</div>
<div class="ml-100px flex items-center"><div class="w-90px">QQ昵称</div><div class="w-180px">xxx</div></div>
<div class="btn">绑定</div>
</div>
<div class="mt-30px flex items-center">
<img src="@/assets/images/weixin-v2.png" alt="" srcset="" class="h-35px w-34px" />
<div class="ml-19px">微信</div>
<div class="ml-95px flex items-center"><div class="w-90px">微信昵称</div><div class="w-180px">xxx</div></div>
<div class="btn">绑定</div>
</div>
</div>
</div>
<verifyDialog ref="verifyDialogRef" />
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { tree, upload } from '@/api/common/index'
import { keywords } from '@/api/upnew/index'
import { userExtend, getUserInfo, updateUserExtend } from '@/api/personal-center/index.ts'
import { UserExtendSaveReqVO } from '@/api/personal-center/types'
import verifyDialog from './verify-dialog.vue'
const userFormRef = ref()
const userForm = reactive<UserExtendSaveReqVO>({
id: undefined,
phone: '',
username: '',
avatar: '',
trueName: '',
city: '',
email: '',
isDomestic: undefined,
area: '',
country: '',
province: '',
county: '',
labels: [],
description: '',
authStatus: 0,
files: [],
nickname: '',
})
const rules = {
username: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
city: [{ required: true, message: '请选择城市', trigger: 'change' }],
email: [
{ required: false, message: '请输入电子邮箱', trigger: 'blur' },
{ type: 'email' as const, message: '请输入正确的邮箱格式', trigger: 'blur' },
],
// 标签
labels: [{ required: false, message: '请选择技能标签', trigger: 'change' }],
// 技能证书
files: [{ required: false, message: '请上传技能证书', trigger: 'change' }],
// 所在地区
isDomestic: [{ required: false, message: '请选择国家', trigger: 'change' }],
province: [{ required: false, message: '请选择省份', trigger: 'change' }],
county: [{ required: false, message: '请选择区县', trigger: 'change' }],
}
const verifyDialogRef = ref()
const handleVerify = () => {
verifyDialogRef.value.open()
}
// 省份地址
const provinceList = ref()
// 城市地址
const cityList = ref()
// 区县地址
const countyList = ref()
// 获取地址
const getAdress = async (type: string, val?: any) => {
const res = await tree({
id: val,
})
if (res.code === 0) {
if (type === 'province') {
provinceList.value = res.data
} else if (type === 'city') {
cityList.value = res.data
} else if (type === 'county') {
countyList.value = res.data
}
}
}
// 切换国家
const handleCountryChange = (value: any) => {
userForm.province = ''
userForm.city = ''
userForm.county = ''
provinceList.value = []
cityList.value = []
countyList.value = []
getAdress('province', value)
}
// 监听省份变化
const handleProvinceChange = (value: string) => {
userForm.city = ''
userForm.county = ''
cityList.value = []
countyList.value = []
getAdress('city', value)
}
// 监听城市变化
const handleCityChange = (value: string) => {
userForm.county = ''
countyList.value = []
getAdress('county', value)
}
// 处理头像上传变化
const handleAvatarChange = (file: any) => {
// 这里可以添加文件类型和大小的验证
if (file) {
// 创建本地预览URL
userForm.avatar = URL.createObjectURL(file.raw)
// 这里可以添加实际的上传逻辑
console.log('Avatar file:', file)
// 上传文件到服务器
const formData = new FormData()
formData.append('fieldName', file?.name)
formData.append('file', file.raw as Blob)
upload('/prod-api/app-api/infra/file/upload', formData)
.then((response) => {
if (response.code === 0) {
userForm.avatar = response.data
}
})
.catch((error) => {
console.error('Error uploading avatar:', error)
})
}
}
const loading = ref(false)
/** 获取标签 */
const labelsList = ref<any>([])
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
keywords({
type: 1,
keywords: query,
})
.then((res) => {
labelsList.value = res.data
})
.finally(() => {
loading.value = false
})
} else {
labelsList.value = []
}
}
// 提交表单
const submitForm = async () => {
if (!userFormRef.value) return
try {
await userFormRef.value.validate()
const res = userForm.id ? await updateUserExtend(userForm) : await userExtend(userForm)
if (res.code === 0) {
ElMessage.success('个人信息提交成功')
} else {
ElMessage.error('个人信息提交失败')
}
} catch (error) {
console.error('Validation failed:', error)
ElMessage.error('表单验证失败,请检查输入')
}
}
// 初始回显
const init = async () => {
const res = await getUserInfo()
if (res.code === 0) {
userForm.id = res.data.id
userForm.nickname = res.data.nickname
userForm.phone = res.data.mobile
userForm.email = res.data.email
userForm.isDomestic = +res.data.isDomestic
userForm.country = res.data.country
await getAdress('province', userForm.isDomestic)
// @ts-ignore
userForm.province = +res.data.province
await getAdress('city', userForm.province)
// @ts-ignore
userForm.city = +res.data.city
await getAdress('county', userForm.city)
// @ts-ignore
userForm.county = +res.data.county
userForm.labels = res.data.labels
userForm.description = res.data.description
userForm.avatar = res.data.avatar
userForm.files = res.data.files.map((item: any) => {
return {
...item,
url: item.url,
name: item.title,
uid: item.id,
status: 'success',
}
})
}
}
init()
</script>
<style scoped>
.btn {
width: 91px;
height: 37px;
background: #1a65ff;
border-radius: 4px;
border: 1px solid #1a65ff;
text-align: center;
line-height: 37px;
font-weight: 400;
font-size: 14px;
color: #ffffff;
margin-left: 30px;
}
.user-profile-container {
margin: 0 auto;
padding: 30px;
display: flex;
}
.profile-form {
/* margin-top: 20px; */
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 30px;
}
.update-avatar-btn {
margin-top: 15px;
}
.verify-btn {
margin-left: 10px;
}
.modify-link {
margin-left: 10px;
min-width: fit-content;
white-space: nowrap;
}
.certificate-uploader {
width: 100%;
}
.upload-area {
width: 200px;
height: 100px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.upload-area:hover {
border-color: #409eff;
}
.full-width {
width: 100%;
}
.flex {
display: flex;
}
.gap-2 {
gap: 0.5rem;
}
.w-120px {
width: 120px;
}
/* 响应式布局调整 */
@media (max-width: 768px) {
.user-profile-container {
padding: 20px 10px;
}
.verify-btn {
margin-top: 10px;
margin-left: 0;
}
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<div class="box-border min-h-494px w-913px border border-[#EEEEEE] rounded-6px border-solid bg-[#FFFFFF] px-30px py-21px">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="我的上传" name="我的上传">
<uploadTable v-model:type="pageReq.type" v-model="result.list" :refresh="handlerefresh" />
</el-tab-pane>
<el-tab-pane label="我的购买" name="我的购买">
<downloadTable v-model="result.list" />
</el-tab-pane>
<el-tab-pane label="收藏夹" name="收藏夹">
<favoriteTable v-model:type="pageReq.type" v-model="result.list" :refresh="handlerefresh" />
</el-tab-pane>
<el-tab-pane label="浏览记录" name="浏览记录">
<browseTable v-model="result.list" />
</el-tab-pane>
</el-tabs>
<!-- 分页 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="pageReq.pageNum"
v-model:page-size="pageReq.pageSize"
:page-sizes="[10, 20, 30]"
:total="result.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleClickSize"
@current-change="handeClickCurrent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { getContentPage, getUserToolBoxPage, getUserFavoritePage, getOwnContentPage } from '@/api/personal-center/index'
// import { ProjectHistoryResVO } from '@/api/personal-center/types'
import uploadTable from './components/upload-table.vue'
import downloadTable from './components/download-table.vue'
import favoriteTable from './components/favorite-table.vue'
import browseTable from './components/browse-table.vue'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const activeName = ref('我的上传')
const pageReq = reactive({
pageNum: 1,
pageSize: 10,
type: 1, //类型 1 图纸 2 工具箱
})
const result = reactive({
total: 0, //总条数
list: [] as any[], //列表
})
const handleClickSize = (val: number) => {
pageReq.pageSize = val
fetchData()
}
const handeClickCurrent = (val: number) => {
pageReq.pageNum = val
fetchData()
}
const fetchData = async () => {
let res = {} as any
switch (activeName.value) {
case '我的上传':
res = await getOwnContentPage({ ...pageReq, pageNo: pageReq.pageNum }) // 我的上传
if (res.code === 0) {
result.total = res.data.total || 0
result.list = res.data.list || []
}
break
case '我的购买':
res = await getUserToolBoxPage({ ...pageReq, type: undefined }) // 我的下载
if (res.code === 0) {
result.total = res.data.total || 0
result.list = res.data.list || []
}
break
case '收藏夹':
res = await getUserFavoritePage({ ...pageReq, pageNo: pageReq.pageNum, userId: userStore.userId }) // 收藏夹
if (res.code === 0) {
result.total = res.data.total || 0
result.list = res.data.list || []
}
break
case '浏览记录':
res = await getContentPage(pageReq) // 浏览记录
if (res.code === 0) {
result.total = res.data.total || 0
result.list = res.data.list || []
}
break
default:
break
}
}
const handlerefresh = () => {
pageReq.pageNum = 1
fetchData()
}
const handleClick = (tab: any) => {
console.log(tab)
activeName.value = tab.props.name
pageReq.pageNum = 1
result.list = []
result.total = 0
fetchData()
}
fetchData()
</script>
<style scoped>
:deep(.el-input__inner) {
text-align: center !important;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="account-balance">
<!-- 余额展示区域 -->
<div class="balance-section">
<div class="balance-title">
<span>我的金币</span>
<el-tag size="small" type="primary">资产和使用</el-tag>
</div>
<div class="balance-amount">{{ userStaticInfo?.currencyCount || 0 }}</div>
<div class="balance-actions">
<el-button type="primary" @click="handlePay">充值</el-button>
<el-button>提现</el-button>
</div>
<div class="balance-tip">提示最低提现金额100 一元=10金币</div>
</div>
<!-- 交易记录区域 -->
<div class="transaction-section">
<div class="transaction-tabs">
<el-tabs v-model="activeTab">
<el-tab-pane label="充值记录" name="purchase">
<!-- 组件 -->
<PayRecords></PayRecords>
</el-tab-pane>
<el-tab-pane label="提现管理" name="withdraw"></el-tab-pane>
</el-tabs>
<!-- <div class="date-filter">
<span>当前时期</span>
<el-date-picker v-model="currentMonth" type="month" format="YYYY.MM" value-format="YYYY.MM" placeholder="选择月份" />
</div> -->
</div>
</div>
<!-- 充值弹窗 -->
<Pay v-if="payVisible" v-model="payVisible" @refresh="fetchUserStatistics"></Pay>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Pay from './components/pay.vue'
import PayRecords from './components/pay-records.vue'
import { getUserStatistics } from '@/api/personal-center/index'
import { UserStatisticsCountRespVO } from '@/api/personal-center/types'
const activeTab = ref('purchase')
// const currentMonth = ref('2025.03')
// 获取用户统计信息
const userStaticInfo = ref<UserStatisticsCountRespVO>()
const fetchUserStatistics = async () => {
const res = await getUserStatistics()
userStaticInfo.value = res.data
}
fetchUserStatistics()
const payVisible = ref(false)
const handlePay = () => {
payVisible.value = true
// router.push({ path: '/personal/trading/center' })
}
</script>
<style scoped>
.account-balance {
width: 913px;
min-height: 100vh;
background: #ffffff;
}
.balance-section {
background: white;
padding: 20px 25px;
border-radius: 8px;
margin-bottom: 20px;
border-radius: 6px;
border: 1px solid #eeeeee;
box-sizing: border-box;
}
.balance-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
color: #666;
}
.balance-amount {
font-size: 32px;
font-weight: bold;
margin: 20px 0;
}
.balance-actions {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.balance-tip {
color: #999;
font-size: 12px;
}
.transaction-section {
background: white;
padding: 20px 25px;
border-radius: 8px;
border-radius: 6px;
border: 1px solid #eeeeee;
box-sizing: border-box;
}
.transaction-tabs {
/* display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px; */
}
.date-filter {
display: flex;
align-items: center;
gap: 10px;
color: #666;
}
.amount {
color: #409eff;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
:deep(.el-pagination .el-pager li.is-active) {
background-color: #409eff;
color: white;
}
:deep(.el-input__wrapper) {
background-color: transparent;
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<el-dialog v-model="dialogVisible" title="实名认证" width="600px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" class="pa-20px">
<el-form-item label="真实姓名" prop="trueName">
<el-input v-model="form.trueName" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="身份证号" prop="idNo">
<el-input v-model="form.idNo" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="身份证照片" prop="files">
<KlUploader
v-model:file-list="form.files"
list-type="picture-card"
:limit="2"
:size="1"
tips="上传图片支持jpg/gif/png格式、正反两面、每张图片大小不得超过1M"
>
<div class="h-77px w-161px flex items-center justify-center border border-[#cdd0d6] rounded-1px border-dashed bg-[#fafafa]">
<el-icon class="text-[#999999]"><Plus /></el-icon>
<div class="ml-4px mt-2px text-14px text-[#999999] font-normal">上传照片</div>
</div>
</KlUploader>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { getUserAuthInfo, createUserAuthInfo, updateUserAuthInfo } from '@/api/personal-center/index.ts'
import { UserAuthInfoRespVO } from '@/api/personal-center/types'
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const form = reactive<UserAuthInfoRespVO>({
id: undefined,
trueName: '',
idNo: '',
files: [],
})
const rules = reactive<FormRules>({
trueName: [
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' },
],
idNo: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
files: [{ required: true, message: '请上传身份证正面反面照片', trigger: 'change' }],
})
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
// TODO: 这里添加提交逻辑
const res = form.id ? await updateUserAuthInfo(form) : await createUserAuthInfo(form)
if (res.code === 0) {
ElMessage.success('认证信息提交成功')
dialogVisible.value = false
}
} catch (error) {
console.error('表单验证失败:', error)
}
}
const handleCancel = () => {
dialogVisible.value = false
formRef.value?.resetFields()
}
// 对外暴露方法
const open = () => {
dialogVisible.value = true
getUserAuthInfo().then((res) => {
form.id = res.data.id
form.files = res.data.files.map((item: any) => {
return {
...item,
url: item.url,
name: item.title,
uid: item.id,
status: 'success',
}
})
form.trueName = res.data.trueName
form.idNo = res.data.idNo
})
}
defineExpose({
open,
})
</script>
<style scoped>
.id-card-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 178px;
height: 178px;
display: flex;
justify-content: center;
align-items: center;
}
.id-card-uploader:hover {
border-color: #409eff;
}
.id-card-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px;
}
.id-card-image {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
}
.upload-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<KlNavTab />
<div class="ma-auto w-1198px flex justify-between">
<div class="left mt-25px box-border h-370px w-260px border border-[#EEEEEE] rounded-4px border-solid bg-[#FFFFFF] text-15px text-[#333333] font-medium">
<router-link to="/personal/center/info" class="flex items-center justify-between py-14px">
<div class="flex items-center pl-20px">
<img v-if="route.path.startsWith('/personal/center/info')" src="@/assets/images/user3.png" alt="" srcset="" class="h-20px" />
<img v-else src="@/assets/images/个人.png" alt="" srcset="" class="h-20px" />
<span class="ml-10px">个人中心</span>
</div>
<div class="pr-20px">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
<router-link to="/personal/profile" class="flex items-center justify-between py-14px">
<div class="flex items-center pl-20px">
<img v-if="!route.path.startsWith('/personal/profile')" src="@/assets/images/user_zl.png" alt="" srcset="" class="h-16px" />
<img v-else src="@/assets/images/个人资料 (1).png" alt="" srcset="" class="h-16px" />
<span class="ml-10px">个人资料</span>
</div>
<div class="pr-20px">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
<router-link to="/personal/account/security" class="flex items-center justify-between py-14px">
<div class="flex items-center pl-20px">
<img v-if="!route.path.startsWith('/personal/account/security')" src="@/assets/images/account.png" alt="" srcset="" class="h-20px" />
<img v-else src="@/assets/images/账户安全.png" alt="" srcset="" class="h-20px" />
<span class="ml-14px">账户与安全</span>
</div>
<div class="pr-20px">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
<router-link to="/personal/resource/center" class="flex items-center justify-between py-14px">
<div class="flex items-center pl-20px">
<img v-if="!route.path.startsWith('/personal/resource/center')" src="@/assets/images/ziyuan.png" alt="" srcset="" class="h-18px" />
<img v-else src="@/assets/images/资源.png" alt="" srcset="" class="h-18px" />
<span class="ml-12px">资源中心</span>
</div>
<div class="pr-20px">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
<router-link to="/personal/trading/center" class="flex items-center justify-between py-14px">
<div class="flex items-center pl-20px">
<img v-if="!route.path.startsWith('/personal/trading/center')" src="@/assets/images/pay.png" alt="" srcset="" class="h-20px" />
<img v-else src="@/assets/images/交易管理.png" alt="" srcset="" class="h-20px" />
<span class="ml-12px">交易中心</span>
</div>
<div class="pr-20px">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
<router-link to="/personal/center/message" class="flex items-center justify-between py-14px">
<div class="flex items-center pl-20px">
<img v-if="!route.path.startsWith('/personal/center/message')" src="@/assets/images/message.png" alt="" srcset="" class="h-18px" />
<img v-else src="@/assets/images/消息.png" alt="" srcset="" class="h-18px" />
<span class="ml-14px">消息通知</span>
</div>
<div class="pr-20px">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
</div>
<div class="right mt-25px">
<router-view></router-view>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowRight } from '@element-plus/icons-vue'
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<style scoped lang="scss">
.router-link-active {
color: #1a65ff;
background: rgb(203, 221, 255);
position: relative;
&::before {
position: absolute;
left: 0;
content: '';
width: 4px;
height: 50px;
background: #1a65ff;
}
}
</style>

View File

@ -0,0 +1,492 @@
<template>
<KlNavTab active="" :type="1" />
<div class="personal-detail">
<!-- 个人信息头部 -->
<div class="profile-header">
<div class="profile-container">
<div class="avatar-container">
<el-image :src="userForm.avatar" alt="用户头像" class="avatar mt-4px" fit="cover" />
</div>
<div class="user-info">
<h2 class="username">{{ userForm.nickname }}</h2>
<!-- <div class="education">手机号码{{ userForm.phone }}</div> -->
<div class="stats">
技能标签<el-tag v-for="label in userForm.labels" :key="label" type="primary" class="mr-10px" size="small">{{ label }}</el-tag>
</div>
<div class="description">{{ userForm.description }}</div>
</div>
</div>
</div>
<div class="items-flex-start mx-auto mt-20px max-w-[1200px] flex justify-center">
<div class="flex-1">
<!-- 导航标签 -->
<div class="nav-tabs">
<div class="tabs-container">
<div v-for="tab in tabs" :key="tab.id" :class="['tab', { active: query.type === tab.id }]" @click="handleClick(tab.id)">
{{ tab.name }}
</div>
</div>
</div>
<!-- 作品展示区 -->
<div class="content w-800px">
<el-table v-loading="result.loading" :data="result.tableList" style="width: 100%" class="mt-14px">
<el-table-column prop="date" label="文件信息">
<template #default="scope">
<div class="flex items-center">
<el-image :src="scope.row.iconUrl" fit="cover" alt="" srcset="" class="h-91px w-181px rd-4px" />
<div class="ml-17px">
<div class="text-16px text-[#333333] font-normal">{{ scope.row.title }}</div>
<div class="text-14px text-[#666] font-normal my-10px!">{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div class="flex items-center">
<div class="flex items-center">
<img src="@/assets/images/look.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.previewPoint }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/add.png" alt="" srcset="" class="h-23px" />
<span class="ml-4px">{{ scope.row.hotPoint }}</span>
</div>
<div class="ml-13px flex items-center">
<img src="@/assets/images/chat.png" alt="" srcset="" class="h-17px" />
<span class="ml-4px">{{ scope.row.commentsPoint }}</span>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="上传状态" width="180">
<template #default="scope">
{{ handleStatus(scope.row.status) }}
</template>
</el-table-column>
<el-table-column prop="address" label="操作" width="100">
<template #default="scope">
<el-link v-if="scope.row.status === 4" type="primary" :underline="false" @click="handleXiaJia(scope.row)">下架</el-link>
<el-link type="primary" :underline="false" @click="handleDelete(scope.row)">删除</el-link>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="query.pageNo"
v-model:page-size="query.pageSize"
:page-sizes="[10, 20, 30]"
:total="result.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleClickSize"
@current-change="handeClickCurrent"
/>
</div>
</div>
</div>
<!-- 常用软件区域 -->
<div class="software-section">
<h3 class="section-title">最近发表</h3>
<div class="software-list">
<div
v-for="item in mainWork"
:key="item.id"
class="flex cursor-pointer items-center justify-between px-10px py-4px hover:bg-#f5f5f5"
@click="handleClickV2(item.id)"
>
<div class="ellipsis text-15px text-[#333333] font-normal">{{ item.title }}发货速度发货速度开发还是看东方航空</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { UserExtendSaveReqVO } from '@/api/personal-center/types'
import { getMainWork } from '@/api/drawe-detail/index'
import { getOwnContentPage } from '@/api/personal-center/index.ts'
import { offShelf, deleteResource, getUserExtend } from '@/api/personal-center'
import { ProjectDrawMemberRespVO } from '@/api/drawe-detail/types'
import dayjs from 'dayjs'
import { useMessage } from '@/utils/useMessage'
const message = useMessage()
// 用户信息
const userForm = reactive<UserExtendSaveReqVO>({
id: undefined,
phone: '',
username: '',
avatar: '',
trueName: '',
city: '',
email: '',
isDomestic: undefined,
area: '',
country: '',
province: '',
county: '',
labels: [],
description: '',
authStatus: 0,
files: [],
nickname: '',
memberId: undefined,
})
const result = reactive({
tableList: [] as any[],
total: 0,
loading: false,
})
const query = reactive({
pageNo: 1,
pageSize: 10,
type: 1,
})
// 标签页
const tabs = ref([
{ id: 1, name: '图纸' },
{ id: 3, name: '模型' },
{ id: 2, name: '文本' },
])
const handleStatus = (status: number) => {
switch (status) {
case 1:
return '草稿'
case 2:
return '提交审核'
case 3:
return '审核成功'
case 4:
return '下架'
default:
return ''
}
}
const handleXiaJia = (row: any) => {
offShelf(row.id).then((res: any) => {
if (res.code === 0) {
ElMessage.success('下架成功')
}
})
}
const handleDelete = async (row: any) => {
const r = await message.confirm('是否删除该资源', '提示')
if (!r) return
deleteResource({ id: row.id }).then((res: any) => {
if (res.code === 0) {
ElMessage.success('删除成功')
}
})
}
const init = async () => {
const res = await getUserExtend()
if (res.code === 0) {
userForm.id = res.data.id
userForm.nickname = res.data.nickname
userForm.phone = res.data.mobile
userForm.email = res.data.email
userForm.isDomestic = +res.data.isDomestic
userForm.country = res.data.country
// await getAdress('province', userForm.isDomestic)
// // @ts-ignore
// userForm.province = +res.data.province
// await getAdress('city', userForm.province)
// // @ts-ignore
// userForm.city = +res.data.city
// await getAdress('county', userForm.city)
// @ts-ignore
userForm.county = +res.data.county
userForm.labels = res.data.labels
userForm.description = res.data.description
userForm.avatar = res.data.avatar
userForm.memberId = res.data.userAuthInfo.memberId
userForm.files = res.data.files.map((item: any) => {
return {
...item,
url: item.url,
name: item.title,
uid: item.id,
status: 'success',
}
})
// 最新发布
handleGetMainWork()
}
}
const fetchData = async () => {
result.loading = true
const res = await getOwnContentPage(query)
if (res.code === 0) {
result.total = res.data.total || 0
result.tableList = res.data.list || []
}
result.loading = false
}
const handleClickSize = (val: number) => {
query.pageSize = val
fetchData()
}
const handleClick = (val: number) => {
query.pageNo = 1
query.type = val
fetchData()
}
const handeClickCurrent = (val: number) => {
query.pageNo = val
fetchData()
}
const handleClickV2 = (id: string | number) => {
window.open(`/down-drawe-detail?id=${id}`, '_blank') // 修改为在新窗口打开
}
// 获取最新发布
const mainWork = ref<ProjectDrawMemberRespVO[]>([])
const handleGetMainWork = () => {
getMainWork({ id: userForm.id, limit: 10, memberId: userForm.memberId }).then((res) => {
if (res.code === 0) {
mainWork.value = res.data
}
})
}
onMounted(() => {
// 可以在这里获取用户数据
init()
fetchData()
})
</script>
<style scoped lang="scss">
$primary-color: #2563eb;
$background-color: #f7f8fa;
$header-bg: linear-gradient(90deg, #2563eb 0%, #60a5fa 100%);
$max-width: 1200px;
$text-dark: #222;
$text-light: #888;
$card-radius: 12px;
$card-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
$font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
.personal-detail {
width: 100%;
background-color: $background-color;
font-family: $font-family;
.profile-header {
background: #011a52;
color: white;
padding: 40px 0 32px 0;
.profile-container {
max-width: $max-width;
margin: 0 auto;
display: flex;
align-items: flex-start;
padding: 0 32px;
}
}
.avatar-container {
margin-right: 32px;
.avatar {
width: 96px;
height: 96px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.7);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
}
.user-info {
flex: 1;
.username {
margin: 0 0 10px 0;
font-size: 24px;
font-weight: 700;
letter-spacing: 1px;
}
.stats {
margin-bottom: 10px;
font-size: 15px;
color: #e0e7ef;
.el-tag {
margin-right: 8px;
border-radius: 8px;
background: #e0e7ef;
color: $primary-color;
border: none;
font-size: 13px;
padding: 2px 12px;
}
}
.description {
font-size: 15px;
line-height: 1.7;
margin-top: 12px;
color: #f3f4f6;
letter-spacing: 0.2px;
}
}
.nav-tabs {
background-color: #fff;
border-radius: $card-radius;
box-shadow: $card-shadow;
margin-bottom: 24px;
.tabs-container {
max-width: $max-width;
margin: 0 auto;
display: flex;
padding: 0 32px;
}
.tab {
padding: 18px 32px 14px 32px;
cursor: pointer;
font-size: 15px;
color: $text-light;
position: relative;
transition: color 0.2s;
&.active {
color: $primary-color;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: $primary-color;
border-radius: 2px;
}
}
&:hover {
color: $primary-color;
}
}
}
.content {
background: #fff;
border-radius: $card-radius;
box-shadow: $card-shadow;
padding: 20px 32px 24px 32px;
min-height: 400px;
}
.el-table {
font-size: 15px;
th {
font-weight: 600;
background: #f3f4f6;
color: $text-dark;
letter-spacing: 0.5px;
}
td {
padding: 16px 8px;
color: $text-dark;
}
.el-link {
margin-right: 12px;
font-size: 14px;
letter-spacing: 0.2px;
}
}
.el-pagination {
margin-top: 24px;
}
.software-section {
width: 400px;
margin: 0 0 0 48px;
padding: 0 24px;
.section-title {
font-size: 17px;
font-weight: 700;
margin-bottom: 18px;
color: $primary-color;
letter-spacing: 0.5px;
padding-left: 2px;
}
.software-list {
display: flex;
flex-direction: column;
gap: 10px;
.flex {
border-radius: 4px;
align-items: center;
position: relative;
cursor: pointer;
&::before {
content: '';
position: absolute;
top: 12px;
left: 0;
width: 5px;
height: 5px;
background: #1a65ff;
border-radius: 100%;
}
}
.ellipsis {
font-size: 15px;
color: $text-dark;
font-weight: 500;
letter-spacing: 0.2px;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.color-999999 {
color: $text-light !important;
font-size: 13px;
font-weight: 400;
margin-left: 16px !important;
letter-spacing: 0.1px;
}
}
}
}
// 响应式优化
@media (max-width: 900px) {
.personal-detail {
.profile-container,
.tabs-container,
.content {
padding-left: 12px;
padding-right: 12px;
}
.software-section {
margin-left: 0;
padding: 0 8px;
}
}
}
@media (max-width: 600px) {
.personal-detail {
.profile-container {
flex-direction: column;
align-items: center;
.avatar-container {
margin-right: 0;
margin-bottom: 12px;
}
}
.content {
padding: 12px 4px;
}
.software-section {
padding: 0 2px;
}
}
}
:deep(.el-input__inner) {
text-align: center !important;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<KlNavTab />
<div class="sign-content">
<!-- 顶部统计 -->
<div class="sign-header">
<div class="sign-icon">
<div class="icon-text"></div>
</div>
<div class="sign-info">
<div class="search-container">
<div class="search-group">
<span class="search-label">积分标题</span>
<el-input v-model="query.title" placeholder="请输入关键词" class="search-input" clearable>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
</div>
<div class="sign-center ml-10px">
<el-button type="success" class="sign-btn" @click="handleClickSign"> 立即签到 </el-button>
</div>
</div>
</div>
</div>
<!-- Tab切换 -->
<!-- <div class="sign-tabs">
<div class="tab" @click="handleClickTab('today')" :class="{ active: sign_status_tab === 'today' }">今日签到</div>
<div class="tab" @click="handleClickTab('allRanking')" :class="{ active: sign_status_tab === 'allRanking' }">总排行</div>
<div class="tab" @click="handleClickTab('rewardRanking')" :class="{ active: sign_status_tab === 'rewardRanking' }">奖励排行</div>
</div> -->
<!-- 签到列表 -->
<div class="sign-table">
<div class="table-header">
<!-- <div>昵称</div> -->
<div>积分标题</div>
<div>积分描述</div>
<div>积分</div>
<div>发生时间</div>
</div>
<div v-for="item in result.list" :key="item.id" class="table-row">
<!-- <div class="avatar"> -->
<!-- <img :src="item.avatar" alt="avatar" /> -->
<!-- <span>{{ item.nickname }}</span> -->
<!-- </div> -->
<div>{{ item.title }}</div>
<div>{{ item.description }}</div>
<div>{{ item.point }}</div>
<div>{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
</div>
<!-- 分页组件 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="query.pageNo"
:page-size="query.pageSize"
layout="prev, pager, next"
:total="result.total"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { signIn, getUserPointPage } from '@/api/personal-center/index'
import { PageResultMemberPointRecordRespVO } from '@/api/personal-center/types'
import useUserStore from '@/store/user'
import dayjs from 'dayjs'
const userStore = useUserStore()
const query = reactive({
pageNo: 1,
pageSize: 10,
userId: userStore.userId,
title: '',
})
const result = reactive<PageResultMemberPointRecordRespVO>({
total: 0,
list: [],
})
// const signList = ref([
// {
// avatar: 'https://dummyimage.com/40x40/ccc/fff.png&text=美',
// name: '头街',
// totalDays: 756,
// continuousDays: 33,
// lastSignTime: '2025-05-11 09:22:13',
// reward: '3积分',
// },
// {
// avatar: 'https://dummyimage.com/40x40/ccc/fff.png&text=小',
// name: 'xiaoy94',
// totalDays: 648,
// continuousDays: 21,
// lastSignTime: '2025-05-11 09:21:15',
// reward: '3积分',
// },
// // ... 其余数据
// ])
// const sign_status_tab = ref('today') // today allRanking rewardRanking
// const handleClickTab = (val: string) => {
// sign_status_tab.value = val
// }
const handleCurrentChange = (val: number) => {
console.log(1)
query.pageNo = val // 更新当前页
getUserPointPageList()
}
const getUserPointPageList = async () => {
const res = await getUserPointPage(query)
if (res.code === 0) {
result.list = res.data.list
result.total = res.data.total
}
}
getUserPointPageList()
const handleClickSign = async () => {
const res = await signIn()
if (res.code === 0) {
ElMessage.success(res.msg)
query.pageNo = 1
getUserPointPageList()
}
console.log(res)
}
const handleSearch = () => {
query.pageNo = 1
getUserPointPageList()
}
</script>
<style lang="scss" scoped>
.sign-content {
// background: #f5f5f5;
padding: 24px;
.sign-header {
display: flex;
align-items: center;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
border: 1px solid #e9ecef;
.sign-icon {
.icon-text {
font-size: 32px;
color: #fff;
background: linear-gradient(45deg, #4f46e5, #8b5cf6);
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(79, 70, 229, 0.2);
}
}
.search-container {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 32px;
.search-group {
flex: 1;
display: flex;
align-items: center;
max-width: 600px;
.search-label {
font-size: 14px;
color: #495057;
margin-right: 12px;
white-space: nowrap;
}
.search-input {
flex: 1;
margin-right: 12px;
.el-input__inner {
height: 40px;
border-radius: 8px;
border-color: #dee2e6;
}
}
.search-btn {
height: 40px;
padding: 0 24px;
border-radius: 8px;
font-weight: 500;
}
}
.sign-btn {
height: 40px;
padding: 0 24px;
border-radius: 8px;
font-weight: 500;
background: linear-gradient(45deg, #10b981, #34d399);
border: none;
&:hover {
opacity: 0.9;
}
.el-icon {
margin-right: 8px;
}
}
}
}
.sign-tabs {
display: flex;
background: #fff;
border-radius: 8px 8px 0 0;
overflow: hidden;
border: 1px solid #eee;
.tab {
flex: 1;
text-align: center;
padding: 16px 0;
font-size: 16px;
cursor: pointer;
&.active {
background: #f44336;
color: #fff;
font-weight: bold;
}
}
}
.sign-table {
background: #fff;
border-radius: 0 0 8px 8px;
border: 1px solid #eee;
.table-header,
.table-row {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #eee;
div {
flex: 1;
text-align: center;
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
img {
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 8px;
}
}
}
.table-header {
background: #fafafa;
font-weight: bold;
}
.table-row:last-child {
border-bottom: none;
}
}
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="box-border w-100% border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-26px py-30px">
<div class="flex items-center">
<div class="text-28px text-[#333333] font-normal">精选专题</div>
<div class="ml-50px text-21px text-[#999999] font-normal">了解最新趋势发展</div>
</div>
<div class="mt-36px flex justify-between">
<div v-for="item in 4" :key="item" class="flex flex-col items-center">
<img :src="`https://picsum.photos/320/190?_t${new Date().getTime()}`" alt="" srcset="" class="h-190px w320 rounded-4px" />
<div class="mt-10px text-18px text-[#333333] font-normal">机器人</div>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,50 @@
<template>
<div class="relative mt-34px w-100%">
<KlTabBar v-model="query.source" :data="tabBar" />
<div class="absolute right-0px top-10px text-16px text-[#999999] font-normal"
><span class="color-#1A65FF">{{ result.total }}</span
>个筛选结果</div
>
<div class="content mt-10px">
<el-row :gutter="20">
<el-col v-for="(item, index) in result.list" :key="index" :span="6">
<CardPicture :item-info="item" />
</el-col>
</el-row>
<el-empty v-if="!result.list.length" :image="emptyImg"></el-empty>
</div>
</div>
</template>
<script lang="ts" setup>
import KlTabBar from '@/components/kl-tab-bar/index.vue'
import CardPicture from '@/components/kl-card-picture/index.vue'
import { ref } from 'vue'
import { pageRes, pageReq } from '@/api/upnew/types'
import emptyImg from '@/assets/images/empty.png'
const query = defineModel<pageReq>('modelValue', {
required: true,
})
const result = defineModel<pageRes>('result', {
required: true,
})
const tabBar = ref([
{
label: '文本推荐',
value: '',
},
{
label: '原创文本',
value: 1,
},
{
label: '转载分享',
value: 2,
},
])
</script>
<style lang="scss" scoped></style>

102
pages/text/index.vue Normal file
View File

@ -0,0 +1,102 @@
<template>
<!-- 导航 -->
<KlNavTab active="文本" :type="2" />
<div class="ma-auto w-1440px">
<!-- 图纸分类 -->
<KlWallpaperCategory v-model="query" v-model:level="level" :type="2" />
<!-- 推荐栏目 -->
<RecommendedColumnsV2 v-model="query" v-model:result="result"></RecommendedColumnsV2>
<!-- 精选专题 -->
<!-- <FeaturedSpecials></FeaturedSpecials> -->
<!-- 分页 -->
<div class="mt-10px flex justify-center">
<el-pagination
v-model:current-page="query.pageNo"
v-model:page-size="query.pageSize"
:page-sizes="[12, 24, 48]"
:total="result.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handeChangeSize"
@current-change="handeChangeCurrent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import KlWallpaperCategory from '@/components/kl-wallpaper-category/index.vue'
import RecommendedColumnsV2 from './components/RecommendedColumnsV2.vue'
// import FeaturedSpecials from './components/FeaturedSpecials.vue'
import { reactive, watch, ref } from 'vue'
import { page } from '@/api/upnew/index'
import { pageRes, pageReq } from '@/api/upnew/types'
import { useRoute } from 'vue-router'
const route = useRoute()
const level = ref(
route.query.level
? JSON.parse(route.query.level as string)
: [
{
id: '0',
name: '文本库',
isChildren: false,
},
]
)
const query = reactive<pageReq>({
pageNo: 1,
pageSize: 12,
projectType: '',
editions: '',
source: '',
type: 2,
})
const result = reactive<pageRes>({
list: [],
total: 0,
})
// 如果id存在则设置projectType
if (level.value.length) {
query.projectType = level.value[level.value.length - 1].id || ''
}
const getPage = () => {
page(query).then((res) => {
const { data, code } = res
if (code === 0) {
result.list = data.list
result.total = data.total
}
})
}
getPage()
const handeChangeSize = (val: number) => {
query.pageSize = val
query.pageNo = 1
getPage()
}
const handeChangeCurrent = (val: number) => {
query.pageNo = val
getPage()
}
watch([() => query.projectType, () => query.editions, () => query.source], (val) => {
if (val) {
getPage()
}
})
</script>
<style lang="scss" scoped>
:deep(.el-pagination) {
.el-input__inner {
text-align: center !important;
}
}
</style>

View File

@ -0,0 +1,192 @@
<template>
<KlNavTab />
<div class="mx-auto mt-30px box-border w-1200px border border-[#EEEEEE] rounded-12px border-solid bg-white px-30px py-40px">
<el-form ref="formRef" :model="form" label-width="110px" size="large">
<el-form-item label-width="110px" label="标题:" prop="title" :rules="{ required: true, message: '请输入标题', trigger: ['blur', 'change'] }">
<el-input v-model="form.title" placeholder="请输入标题" class="w-361px!" maxlength="128"></el-input>
</el-form-item>
<el-form-item label-width="110px" label="分类:" prop="projectType" :rules="{ required: true, message: '请选择分类', trigger: ['blur', 'change'] }">
<el-select v-model="form.projectType" placeholder="请选择分类" class="w-361px!" multiple>
<el-option v-for="(item, index) in projectTypeList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label-width="110px" label="标签:" prop="labels" :rules="{ required: true, message: '请选择标签', trigger: ['blur', 'change'] }">
<el-select
v-model="form.labels"
:remote-method="remoteMethod"
:loading="loading"
multiple
filterable
remote
placeholder="请输入搜索标签"
class="w-361px!"
>
<el-option v-for="(item, index) in labelsList" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label-width="110px" label="金币:" prop="points" :rules="{ required: true, message: '请输入金币', trigger: ['blur', 'change'] }">
<el-input-number v-model="form.points" :controls="false" :precision="0" :min="0" placeholder="请输入金币" class="w-361px! text-left!"></el-input-number>
</el-form-item>
<el-form-item
label-width="110px"
label="上传封面:"
prop="coverImages"
:rules="{ required: true, message: '请上传上传封面', trigger: ['blur', 'change'] }"
>
<KlUploader
v-model:file-list="form.coverImages"
list-type="picture-card"
:limit="1000"
:size="1"
tips="上传图片支持jpg/gif/png格式、第一张为封面图片、每张图片大小不得超过1M"
@validate="formRef.validateField('coverImages')"
>
<div class="h-77px w-161px flex items-center justify-center border border-[#cdd0d6] rounded-1px border-dashed bg-[#fafafa]">
<el-icon class="text-[#999999]"><Plus /></el-icon>
<div class="ml-4px mt-2px text-14px text-[#999999] font-normal">上传图纸</div>
</div>
</KlUploader>
</el-form-item>
<el-form-item label-width="110px" label="上传附件:" prop="files" :rules="{ required: true, message: '请上传附件', trigger: ['blur', 'change'] }">
<KlUploader v-model:file-list="form.files" tips="请将系列文件分别压缩后上传,支持批量上传"> </KlUploader>
</el-form-item>
<el-form-item
label-width="110px"
label="描述:"
prop="description"
:rules="[
{ required: true, message: '请输入描述', trigger: ['blur', 'change'] },
{
validator: (rule, value, callback) => {
console.log(rule)
if (value.length < 70) {
callback(new Error('输入内容不能少于 70 字'))
} else {
callback()
}
},
trigger: ['blur', 'change'],
},
]"
>
<el-input v-model="form.description" type="textarea" :rows="6" placeholder="请输入描述" class="w-361px!" minlength="70" show-word-limit></el-input>
</el-form-item>
<!-- 添加预览和保存按钮 -->
<el-form-item label-width="110px" label=" ">
<div class="flex justify-start">
<!-- <el-button @click="save" :loading="saveLoading" type="primary" class="btn">预览</el-button> -->
<el-button :loading="saveLoading" type="primary" class="btn" @click="save">保存</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import KlUploader from '@/components/kl-uploader/index.vue'
import { parent, keywords, labels } from '@/api/upnew/index'
import { create } from '@/api/toolbox/index.js'
import { TcreateReq } from '@/api/toolbox/types'
const form = reactive<TcreateReq>({
title: '',
projectType: [],
labels: [],
points: 0,
coverImages: [],
files: [],
description: '',
})
const loading = ref(false)
/** 获取标签 */
const labelsList = ref<any>([])
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
keywords({
type: 1,
keywords: query,
})
.then((res) => {
labelsList.value = res.data
})
.finally(() => {
loading.value = false
})
} else {
labelsList.value = []
}
}
const formatTypeList = ref<any>([])
const getFormatTypeList = () => {
labels({
type: 2,
}).then((res) => {
// @ts-ignore
formatTypeList.value = res.data[0]
})
}
getFormatTypeList()
const projectTypeList = ref<any>([])
/** 获取分类下拉框 */
const getParent = () => {
parent({
type: 1,
parentId: 0,
}).then((res) => {
projectTypeList.value = res.data
})
}
getParent()
/** 版本 */
const editionsList = ref<any>([])
const getEditionsList = () => {
parent({
type: 2,
parentId: 0,
}).then((res) => {
editionsList.value = res.data
})
}
getEditionsList()
const formRef = ref()
const saveLoading = ref(false)
const save = () => {
formRef.value.validate((valid: any) => {
if (valid) {
// 工具箱接口添加
create(form)
.then((res) => {
console.log(res)
if (res.code === 0) {
ElMessage.success('发布成功')
window.setTimeout(() => {
window.close()
}, 1000)
}
})
.finally(() => {
saveLoading.value = false
})
} else {
console.log('error submit!')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.btn) {
padding: 20px 40px !important;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div>
<header class="h-90px">
<div class="mx-a h-full flex items-center justify-center">
<!-- 搜索区域 -->
<div class="relative w-647px px4 p-r-0px!">
<div class="search-input relative w-100%">
<el-input
v-model="searchQuery"
type="text"
placeholder="搜一搜"
:prefix-icon="Search"
class="no-right-border box-border h40 w-100% rounded-bl-4px rounded-br-0px rounded-tl-4px rounded-tr-0px bg-[#F8F8F8] text-14px outline-#999"
@keyup.enter="search"
/>
</div>
</div>
<!-- 按钮区域 -->
<div class="flex items-center">
<button
class="h-40px w-111px cursor-pointer border-width-1px border-color-#1A65FF rounded-bl-0px rounded-br-4px rounded-tl-0px rounded-tr-4px border-none border-solid text-center text-14px color-#fff bg-#1A65FF!"
@click="search"
>
搜索
</button>
<button
class="m-l-16px h-40px w-111px cursor-pointer border-width-1px border-color-#E7B03B rounded-bl-6px rounded-br-6px rounded-tl-4px rounded-tr-6px border-none border-solid text-14px color-#fff bg-#E7B03B!"
@click="handleUpload"
>
上传工具
</button>
</div>
</div>
</header>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Search } from '@element-plus/icons-vue'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const emits = defineEmits(['search'])
const searchQuery = ref('')
const handleUpload = () => {
// 是否登录
if (!userStore.token) return ElMessage.error('请先登录')
// 新开窗口 用router跳转 新窗口打开
window.open('/toolbox-publish', '_blank')
}
const search = () => {
emits('search', searchQuery.value)
}
</script>
<style scoped>
.no-right-border :deep(.el-input__wrapper) {
border-right: none !important;
/* box-shadow: -1px 0 0 0 var(--el-input-border-color, var(--el-border-color)) inset !important; */
}
/* 如果需要调整右侧圆角,可以添加 */
.no-right-border :deep(.el-input__wrapper) {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
</style>

164
pages/toolbox/index.vue Normal file
View File

@ -0,0 +1,164 @@
<template>
<KlNavTab active="工具箱" />
<!-- 搜索 -->
<KlSearch @search="search"></KlSearch>
<div v-loading="loading" class="ml-auto mr-auto mt-20px w1440 flex justify-center gap-60px">
<div class="left w-821px">
<img src="@/assets/images/banner2.png" alt="" srcset="" class="h-284px w-100%" />
<div
class="box-border border border-t-0px border-t-0px border-[#EEEEEE] rounded-12px border-solid border-t-none bg-[#FFFFFF] px-28px py-17px"
style="border-top-left-radius: 0px; border-top-right-radius: 0px"
>
<div v-for="item in pageRes.list" :key="item.id" class="mt-20px flex border-b-1px border-b-[#eee] border-b-solid pb-20px">
<div class="h-142px w-200px text-center">
<el-image :src="item.iconUrl" alt="" srcset="" class="max-w-100% rd-4px" fit="cover" />
</div>
<div class="ml-25px flex-1">
<div class="text-16px text-[#333333] font-normal">{{ item.title }}</div>
<div class="mt-8px text-14px text-[#999999] font-normal">{{ item.description }}</div>
<div class="mt-10px flex items-center justify-between">
<div class="flex items-center">
<div class="flex items-center text-14px text-[#666666] font-normal">
<img src="@/assets/images/look.png" alt="" srcset="" class="mr-4px h-17px w-23px" />{{ item.previewPoint }}
</div>
<div class="ml-26px flex items-center text-14px text-[#666666] font-normal">
<img src="@/assets/images/chat.png" alt="" srcset="" class="mr-4px h-17px w-19px" /> {{ item.commentsPoint }}
</div>
<div class="ml-20px">
<div v-for="(v, index) in item.labels" :key="index" class="mr-10px inline-block text-14px text-[#1A65FF] font-normal">#{{ v }}</div>
</div>
</div>
<div class="text-14px text-[#999999] font-normal">{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
</div>
</div>
<!-- 暂无数据 -->
<div v-if="pageRes.list.length === 0" class="mt-10px flex items-center justify-center">
<!-- <div class="text-16px text-[#999999] font-normal">暂无数据</div> -->
<el-empty v-if="!pageRes.list.length" :image="emptyImg"></el-empty>
</div>
<div class="mt-20px">
<el-pagination
v-model:current-page="pageReq.pageNum"
:page-size="pageReq.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageRes.total"
:page-sizes="[10, 20, 30]"
class="justify-center!"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
<div class="right w-398px">
<div class="box-border border border-[#EEEEEE] rounded-10px border-solid bg-[#FFFFFF] pa-20px">
<div class="flex items-center text-16px text-[#333333] font-normal">
<div class="mr-14px h-24px w-4px rounded-1px bg-[#1A65FF]"></div>
热门排行
</div>
<div v-for="item in recommendList" :key="item.id" class="mt-20px text-14px text-[#666] font-normal">{{ item.title }}</div>
</div>
<!-- -->
<!-- <div class="mt-20px box-border w-398px border border-[#EEEEEE] rounded-10px border-solid bg-[#FFFFFF] pa-20px">
<div class="mb-20px flex items-center text-16px text-[#333333] font-normal">
<div class="mr-14px h-24px w-4px rounded-1px bg-[#1A65FF]"></div>
标签列表
</div>
<div class="flex flex-wrap">
<div
v-for="item in 10"
:key="item"
class="mb-19px mr-26px box-border border border-[#D7D7D7] rounded-4px border-solid px-8px py-12px text-16px text-[#1A65FF] font-normal"
># 标签名称1</div
>
</div>
</div> -->
<!-- -->
<!-- <div class="mt-20px box-border w-398px border border-[#EEEEEE] rounded-10px border-solid bg-[#FFFFFF] pa-20px">
<div class="mb-20px flex items-center text-16px text-[#333333] font-normal">
<div class="mr-14px h-24px w-4px rounded-1px bg-[#1A65FF]"></div>
猜你喜欢
</div>
<div
v-for="item in 4"
:key="item"
class="mt-16px flex items-center border-b-1px border-b-[#eee] border-b-solid pb-16px text-16px text-[#333333] font-normal"
>
<img src="@/assets/images/aucad.png" alt="" srcset="" class="h-68px w-110px" />
<div class="ml-20px text-16px text-[#333333] font-normal">Stable Diffusion 商业变现与 绘画大模型多场景实战</div>
</div>
</div> -->
</div>
</div>
</template>
<script setup lang="ts">
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import { page } from '@/api/toolbox/index.js'
import { TpageReq, TpageRes } from '@/api/toolbox/types'
import { reactive, ref } from 'vue'
import KlSearch from '@/pages/toolbox/components/search.vue'
import { getRelationRecommend } from '@/api/drawe-detail/index'
import { ProjectDrawPageRespVO } from '@/api/drawe-detail/types'
import emptyImg from '@/assets/images/empty.png'
import dayjs from 'dayjs'
const pageReq = reactive<TpageReq>({
pageNum: 1,
pageSize: 10,
title: '',
})
const pageRes = ref<TpageRes>({
list: [],
total: 0,
})
const loading = ref(false)
const getPage = () => {
loading.value = true
page(pageReq)
.then((res) => {
if (res.code === 0) {
pageRes.value = res.data
}
})
.finally(() => {
loading.value = false
})
}
getPage()
const handleCurrentChange = (page: number) => {
pageReq.pageNum = page
getPage()
}
const handleSizeChange = (size: number) => {
pageReq.pageSize = size
pageReq.pageNum = 1
getPage()
}
const search = (val?: string) => {
pageReq.pageNum = 1
pageReq.title = val || ''
getPage()
}
// 猜你喜欢
const recommendList = ref<ProjectDrawPageRespVO[]>([]) // 猜你喜欢数据
const getRelationRecommendList = () => {
getRelationRecommend({
type: 4,
}).then((res) => {
if (res.code === 0) {
console.log(res.data)
recommendList.value = res.data
}
})
}
getRelationRecommendList()
</script>

View File

@ -0,0 +1,250 @@
<template>
<div>
<el-form-item label-width="110px" :prop="`${props.vaildRules}.files`" :rules="{ required: true, message: '请上传图纸', trigger: ['blur', 'change'] }">
<KlUploader
v-model:file-list="form.files"
:limit="1000"
:show-tip="false"
:cad-show="true"
:file-type="computFileType"
@validate="formRef?.validateField(`${props.vaildRules}.files`)"
@preview="handlePreview"
>
</KlUploader>
</el-form-item>
<el-form-item
label-width="110px"
label="标题:"
:prop="`${props.vaildRules}.title`"
:rules="{ required: true, message: '请输入标题', trigger: ['blur', 'change'] }"
>
<el-input v-model="form.title" placeholder="请输入标题" class="w-361px!" maxlength="128"></el-input>
</el-form-item>
<el-form-item
label-width="110px"
label="分类:"
:prop="`${props.vaildRules}.projectType`"
:rules="{ required: true, message: '请选择分类', trigger: ['blur', 'change'] }"
>
<!-- <el-select v-model="form.projectType" placeholder="请选择分类" class="w-361px!" multiple>
<el-option v-for="(item, index) in projectTypeList" :key="index" :label="item.name" :value="item.id" />
</el-select> -->
<el-cascader v-model="form.projectType" class="w-361px!" :options="projectTypeList" :props="cascaderProps" clearable collapse-tags />
</el-form-item>
<el-form-item
label-width="110px"
label="软件版本:"
:prop="`${props.vaildRules}.editions`"
:rules="{ required: true, message: '请选择软件版本', trigger: ['blur', 'change'] }"
>
<el-select v-model="form.editions" placeholder="请选择软件版本" class="w-361px!">
<el-option v-for="(item, index) in editionsList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item
label-width="110px"
label="作品来源:"
:prop="`${props.vaildRules}.source`"
:rules="{ required: true, message: '请选择作品来源', trigger: ['blur', 'change'] }"
>
<el-radio-group v-model="form.source">
<el-radio :label="1">原创图纸</el-radio>
<el-radio :label="2">转载分享</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label-width="110px"
label="标签:"
:prop="`${props.vaildRules}.labels`"
:rules="{ required: true, message: '请选择标签', trigger: ['blur', 'change'] }"
>
<el-select
v-model="form.labels"
:remote-method="remoteMethod"
:loading="loading"
multiple
filterable
remote
placeholder="请输入搜索标签"
class="w-361px!"
>
<el-option v-for="(item, index) in labelsList" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item
label-width="110px"
label="金币:"
:prop="`${props.vaildRules}.points`"
:rules="{ required: true, message: '请输入金币', trigger: ['blur', 'change'] }"
>
<el-input-number v-model="form.points" :controls="false" :precision="0" :min="0" placeholder="请输入金币" class="w-361px! text-left!"></el-input-number>
</el-form-item>
<el-form-item
label-width="110px"
label="图形格式:"
:prop="`${props.vaildRules}.formatType`"
:rules="{ required: true, message: '请选择图形格式', trigger: ['blur', 'change'] }"
>
<el-checkbox-group v-model="form.formatType">
<el-checkbox v-for="(item, index) in formatTypeList" :key="index" :label="item" :value="item"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item
label-width="110px"
label="上传封面:"
:prop="`${props.vaildRules}.coverImages`"
:rules="{ required: true, message: '请上传上传封面', trigger: ['blur', 'change'] }"
>
<KlUploader
v-model:file-list="form.coverImages"
list-type="picture-card"
:limit="1000"
:size="1"
tips="上传图片支持jpg/gif/png格式、第一张为封面图片、每张图片大小不得超过1M"
@validate="formRef?.validateField(`${props.vaildRules}.coverImages`)"
>
<div class="h-77px w-161px flex items-center justify-center border border-[#cdd0d6] rounded-1px border-dashed bg-[#fafafa]">
<el-icon class="text-[#999999]"><Plus /></el-icon>
<div class="ml-4px mt-2px text-14px text-[#999999] font-normal">上传图纸</div>
</div>
</KlUploader>
</el-form-item>
<el-form-item
v-if="form?.type === 3"
label-width="110px"
label="效果图:"
:prop="`${props.vaildRules}.renderings`"
:rules="{ required: true, message: '请上传效果图', trigger: ['blur', 'change'] }"
>
<KlUploader
v-model:file-list="form.renderings"
list-type="picture-card"
:limit="1000"
:show-tip="false"
@validate="formRef?.validateField(`${props.vaildRules}.renderings`)"
>
<div class="h-77px w-161px flex items-center justify-center border border-[#cdd0d6] rounded-1px border-dashed bg-[#fafafa]">
<el-icon class="text-[#999999]"><Plus /></el-icon>
<div class="ml-4px mt-2px text-14px text-[#999999] font-normal">上传图纸</div>
</div>
</KlUploader>
</el-form-item>
<el-form-item label-width="110px" label="上传附件:">
<KlUploader v-model:file-list="form.otherFiles" tips="请将系列文件分别压缩后上传,支持批量上传"> </KlUploader>
</el-form-item>
<el-form-item
label-width="110px"
label="描述:"
:prop="`${props.vaildRules}.description`"
:rules="[
{ required: true, message: '请输入描述', trigger: ['blur', 'change'] },
{
validator: (rule, value, callback) => {
console.log(rule)
if (value.length < 70) {
callback(new Error('输入内容不能少于 70 字'))
} else {
callback()
}
},
trigger: ['blur', 'change'],
},
]"
>
<el-input v-model="form.description" type="textarea" :rows="6" placeholder="请输入描述" class="w-361px!" minlength="70" show-word-limit></el-input>
</el-form-item>
</div>
</template>
<script setup lang="ts">
import { ref, PropType, computed } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import KlUploader from '@/components/kl-uploader/index.vue'
import { parent, keywords, labels, indexTabs } from '@/api/upnew/index'
import { TcreateReq } from '@/api/upnew/types'
const props = defineProps({
vaildRules: {
type: String as PropType<string>,
required: true,
},
formRef: {
type: Object as PropType<any>,
required: true,
},
})
const cascaderProps = { multiple: true, label: 'name', value: 'id', emitPath: false }
const emit = defineEmits(['preview'])
const form = defineModel<TcreateReq['draws'][0]>('modelValue', {
required: true,
})
const computFileType = computed(() => {
return form.value.type === 1 ? ['dwg'] : form.value.type === 2 ? ['pdf', 'xlsx', 'docx', 'xls'] : ['max', 'skp']
})
const loading = ref(false)
/** 获取标签 */
const labelsList = ref<any>([])
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
keywords({
type: 1,
keywords: query,
})
.then((res) => {
labelsList.value = res.data
})
.finally(() => {
loading.value = false
})
} else {
labelsList.value = []
}
}
const formatTypeList = ref<any>([])
const getFormatTypeList = () => {
labels({
type: 2,
}).then((res) => {
// @ts-ignore
formatTypeList.value = res.data
})
}
getFormatTypeList()
const projectTypeList = ref<any>([])
/** 获取分类下拉框 */
const getParent = () => {
indexTabs().then((res) => {
projectTypeList.value = res.data
})
}
getParent()
/** 版本 */
const editionsList = ref<any>([])
const getEditionsList = () => {
parent({
type: 2,
parentId: 0,
}).then((res) => {
editionsList.value = res.data
})
}
getEditionsList()
const handlePreview = (val: any) => {
emit('preview', val)
}
</script>
<style lang="scss" scoped>
::v-deep(.el-form-item__label) {
// font-size: 16px;
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<div class="box-border border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-33px py-22px pb-10px!">
<el-form-item
label="选择上传资料类型:"
label-width="170px"
label-position="left"
prop="type"
:rules="[{ required: true, message: '请选择上传资料类型', trigger: 'change' }]"
>
<el-checkbox-group v-model="form.type" @change="handleTypeChange">
<el-checkbox :value="1">图纸</el-checkbox>
<el-checkbox :value="3">模型</el-checkbox>
<el-checkbox :value="2">文本</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item
label="选择国家:"
label-width="110px"
label-position="left"
prop="isDomestic"
:rules="[{ required: true, message: '请选择国家', trigger: 'change' }]"
>
<el-select v-model="form.isDomestic" placeholder="请选择" class="w-200px!" @change="handleCountryChange">
<el-option label="国内" :value="1"></el-option>
<el-option label="国外" :value="0"></el-option>
</el-select>
</el-form-item>
<div class="flex">
<el-form-item
label="所在地区:"
label-width="110px"
label-position="left"
prop="province"
:rules="[{ required: true, message: '请选择所在地区', trigger: 'change' }]"
>
<el-select v-model="form.province" placeholder="请选择" class="w-200px!" @change="handleProvinceChange">
<el-option v-for="(item, index) in provinceList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label-width="10px" label-position="left" prop="city" :rules="[{ required: true, message: '请选择地址', trigger: 'change' }]">
<el-select v-model="form.city" placeholder="请选择" class="w-200px!" @change="handleCityChange">
<el-option v-for="(item, index) in cityList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label-width="10px" label-position="left" prop="county" :rules="[{ required: true, message: '请选择地区', trigger: 'change' }]">
<el-select v-model="form.county" placeholder="请选择" class="w-200px!">
<el-option v-for="(item, index) in countyList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { TcreateReq } from '@/api/upnew/types'
import { tree } from '@/api/common/index'
import { DrawsEnmu } from '../util.ts'
const form = defineModel<TcreateReq>('modelValue', {
required: true,
})
// 添加旧值引用
const oldTypeValue = ref<(number | string)[]>([])
const handleTypeChange = (val: any) => {
console.log(val, 'val')
// 新增的项(当前值有但旧值没有的)
const added = val.filter((item: number | string) => !oldTypeValue.value.includes(item))
// 减少的项(旧值有但当前值没有的)
const removed = oldTypeValue.value.filter((item) => !val.includes(item))
if (added.length) {
form.value.draws.push({
...JSON.parse(JSON.stringify(DrawsEnmu)),
type: added[0],
})
} else if (removed.length) {
form.value.draws = form.value.draws.filter((item) => item.type !== removed[0])
}
// 更新旧值
oldTypeValue.value = [...val]
// 更新当前activeName值
if (!val?.length) {
form.value.activeName = ''
}
nextTick(() => {
form.value.activeName = val[val.length - 1]
})
}
// 切换国家
const handleCountryChange = (val: any) => {
form.value.province = ''
form.value.city = ''
form.value.county = ''
provinceList.value = []
cityList.value = []
countyList.value = []
getAdress('province', val)
}
// 选择省份 获取市区
const handleProvinceChange = (val: any) => {
form.value.city = ''
form.value.county = ''
cityList.value = []
countyList.value = []
getAdress('city', val)
}
// 选择市份 获取区
const handleCityChange = (val: any) => {
form.value.county = ''
countyList.value = []
getAdress('county', val)
}
// 获取地址
const provinceList = ref()
const cityList = ref()
const countyList = ref()
const getAdress = async (type: string, val?: any) => {
const res = await tree({
id: val,
})
if (res.code === 0) {
if (type === 'province') {
provinceList.value = res.data
} else if (type === 'city') {
cityList.value = res.data
} else if (type === 'county') {
countyList.value = res.data
}
}
}
defineExpose({
handleTypeChange,
})
</script>
<style scoped lang="scss">
::v-deep(.el-form-item__label) {
// font-size: 16px;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="ml-23px box-border min-h-930px w-516px border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-33px py-22px">
<div class="flex items-center">
<img src="@/assets/images/preview.png" alt="" srcset="" width="16px" height="19px" /><span class="ml-7px text-18px text-[#333333] font-normal"
>预览</span
></div
>
<div class="mt-20px">
<el-image :src="previewUrl" class="mb-16px max-h-320px max-w-460px min-h-200px" fit="contain"></el-image>
<span class="text-16px text-[#333333] font-normal">{{ previewName || '图纸标题' }}</span></div
>
<div class="my-30px h-1px w-460px rounded-1px bg-[#EEEEEE]"></div>
<div class="flex items-center"
><img src="@/assets/images/tip.png" width="20px" height="20px" /><span class="ml-7px text-18px text-[#333333] font-normal"
>上传遇到问题可以咨询</span
></div
>
<div class="mt-20px text-center"><el-image src="https://picsum.photos/290/290?_t" alt="" srcset="" class="h-290px w290" /></div>
<div class="mt-30px text-center text-16px text-[#333333] font-normal">
<div>TEL13315189735 </div>
<div class="mt-4px">在线时间8:30-18:00</div>
</div>
</div>
</template>
<script setup lang="ts">
const previewUrl = defineModel<string>('previewUrl', {
required: true,
})
const previewName = defineModel<string>('previewName', {
required: true,
})
</script>

145
pages/upnew/index.vue Normal file
View File

@ -0,0 +1,145 @@
<template>
<!-- 导航 -->
<KlNavTab />
<!-- 发布图纸 -->
<div class="ma-auto mt-30px w-1440px flex">
<div class="w-900px">
<el-form ref="formRef" :model="form" label-width="120px">
<!-- 图纸分类 -->
<DrawType ref="drawTypeRef" v-model="form" />
<div class="mt-24px box-border border border-[#EEEEEE] rounded-12px border-solid bg-[#FFFFFF] px-33px py-22px">
<el-tabs v-model="form.activeName" class="demo-tabs">
<el-tab-pane v-for="(item, index) in form.draws" :key="item.type" :label="computLabel(item.type)" :name="item.type">
<DrawForm v-if="form.draws[index]" v-model="form.draws[index]" :vaild-rules="'draws.' + index" :form-ref="formRef" @preview="handlePreview" />
</el-tab-pane>
</el-tabs>
<el-form-item label-width="110px" class="mt-20px">
<!-- <el-button class="w-121px h-37px!" :loading="loading" @click="handleSubmit">预览</el-button> -->
<el-button type="primary" class="w-121px h-37px!" :loading="loading" @click="handleSubmit">发布</el-button>
</el-form-item>
</div>
</el-form>
</div>
<!-- 预览图 -->
<PreView v-model:preview-url="previewUrl" v-model:preview-name="previewName"></PreView>
</div>
</template>
<script setup lang="ts">
import PreView from './components/Preview.vue'
import DrawType from './components/DrawType.vue'
import DrawForm from './components/DrawForm.vue'
import KlNavTab from '@/components/kl-nav-tab/index.vue'
import { reactive, ref, onMounted, computed } from 'vue'
import { TcreateReq } from '@/api/upnew/types'
import { create } from '@/api/upnew/index'
const form = reactive<TcreateReq>({
activeName: '', // 标签
id: '', // 图纸id id,示例值(24548)
type: [], // 图纸类型 类型,示例值(1)
isDomestic: '', // 是否是国内: 1是 0否
province: '', // 省份编码
city: '', // 城市编码
county: '', // 区县编码
draws: [], // 图纸信息
})
// const previewForm = reactive({
// url: '',
// name: '',
// })
const computLabel = (type: number) => {
switch (type) {
case 1:
return '图纸'
case 2:
return '文本'
case 3:
return '模型'
}
}
const previewUrl = computed(() => {
if (form.draws.length && form.activeName) {
const info = form.draws.find((c) => c.type === Number(form.activeName))
if (info && info.coverImages.length) return info.coverImages[0].url
}
return ''
})
const previewName = computed(() => {
if (form.draws.length && form.activeName) {
const info = form.draws.find((c) => c.type === Number(form.activeName))
if (info && info.coverImages.length) return info.coverImages[0].name
}
return ''
})
/**
* 提交
*/
const loading = ref(false)
const formRef = ref()
const handleSubmit = () => {
console.log(form)
formRef.value.validate((valid: boolean, val: any) => {
console.log('00000----', val)
if (valid) {
loading.value = true
create(form)
.then((res) => {
console.log(res)
const { code } = res
if (code === 0) {
// 弹窗提示
ElMessage.success('操作成功')
// 关闭弹窗
setTimeout(() => {
window.close()
}, 100)
}
})
.finally(() => {
loading.value = false
})
} else {
console.log('error submit!')
// 弹窗提示
ElMessage.error('请填写以上信息')
return false
}
})
}
/** 暂时不用了 直接展示封面图 */
const handlePreview = (val: any) => {
console.log(val)
// previewForm.url = val.url
// previewForm.name = val.name
}
// 图纸类型
const drawTypeRef = ref()
onMounted(() => {
// 初始化图纸类型
drawTypeRef.value.handleTypeChange([1])
// 初始化图纸类型
form.type = [1]
})
</script>
<style lang="scss" scoped>
.demo-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
::v-deep(.el-tabs__item) {
font-size: 16px;
}
</style>

25
pages/upnew/util.ts Normal file
View File

@ -0,0 +1,25 @@
export const DrawsEnmu = {
id: '', // 图纸id id,示例值(24548)
projectId: '', // 所属,示例值(25728)
title: '', // 标题
description: '', // 描述
ownedUserId: '', // 所属人
editions: '', // 软件版本信息,最多上传5个
labels: [], // 标签内容最多5个
type: undefined, // 图纸类型 类型,示例值(1)
source: '', // 来源
editType: '', // 编辑类型
status: [], // 状态值,示例值(2)
createAddress: '', // 创建地址编码信息
createIp: '', // 创建地址ipv4信息
files: [], // 文件信息
province: '', // 省份编码
city: '', // 城市编码
county: '', // 区县编码
points: undefined, // 金币
projectType: [], // 分类
formatType: [], // 格式
coverImages: [], // 封面图片
otherFiles: [], // 附件信息
renderings: [], // 渲染信息
}