mcwl-pc/app/pages/personal-center/index.vue

1008 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script setup lang="ts">
import { commonApi } from '@/api/common'
import EditUserInfo from '@/components/EditUserInfo.vue'
import { useUserStore } from '@/stores/user'
import { formatDate } from '@/utils/index.ts'
import { Close } from '@vicons/ionicons5'
import { NConfigProvider, NMessageProvider } from 'naive-ui'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const message = useMessage()
let pollingTimer: ReturnType<typeof setInterval> | undefined
const route = useRoute()
const { type, status } = route.query as { type: string, status: string }
const loading = ref(false)
const finished = ref(false)
const total = ref(0) // 总条数
const loadingTrigger = ref(null)
const router = useRouter()
const observer = ref<IntersectionObserver | null>(null)
definePageMeta({
layout: 'default',
})
interface UserInfo {
nickName?: string // 使用 ? 表示 nickName 是可选的
avatar?: string
name?: string
brief?: string
}
const userStore = useUserStore()
const userInfo: UserInfo = userStore.userInfo
// 当前是发布还是点赞?
const currentState = ref('mallProduct')
// 当前是模型还是工作流还是图片?
const currentType = ref('0')
const orderOptions = ref([
{
dictLabel: '最新',
dictValue: 'create_time',
},
{
dictLabel: '最热',
dictValue: 'like_num',
},
])
const stateList = ref([
{ id: 'mallProduct', title: '发布' },
{ id: 'like', title: '点赞' },
])
const typeList = ref([
{ id: '0', title: '模型' },
{ id: '1', title: '工作流' },
{ id: '2', title: '图片' },
])
// 发布的form查询条件
const publishParams = ref({
pageNum: 1,
pageSize: 20,
status: '0',
orderByColumn: 'create_time',
date: null,
endTime: '',
startTime: '',
})
function initPublishParams() {
publishParams.value = {
pageNum: 1,
pageSize: 20,
status: '0',
orderByColumn: 'create_time',
date: null,
endTime: '',
startTime: '',
}
}
// 点赞form的查询条件
const likesParams = ref({
pageNum: 1,
pageSize: 20,
orderByColumn: 'create_time',
})
function initLikesParams() {
likesParams.value = {
pageNum: 1,
pageSize: 20,
orderByColumn: 'create_time',
}
}
const urlList = ref({
mallProduct: {
0: '/personalCenter/selectByUserIdModel',
1: '/personalCenter/selectByUserIdWorkFlow',
2: '/personalCenter/selectByUserIdImage',
},
like: {
0: '/personalCenter/likeModel',
1: '/personalCenter/likeWorkFlow',
2: '/personalCenter/likeImage',
},
})
const showInvitationModal = ref(false)
// 获取数据字典
const statusOptions = ref([])
async function getDictType() {
try {
const res = await commonApi.dictType({
type: 'mall_product_status',
})
if (res.code === 200 && res.data.length > 0) {
statusOptions.value = res.data
publishParams.value.status = res.data[0].dictValue
}
}
catch (error) {
console.log(error)
}
}
getDictType()
// 编辑用户信息
interface EditUserInfoType {
isVisible: boolean
}
const editUserInfoRef = ref<EditUserInfoType | null>(null)
function onEditInfo() {
if (editUserInfoRef.value) {
editUserInfoRef.value.isVisible = true
}
}
// 实名认证
interface AuthComponentType {
isVisible: boolean
}
const authenticationRef = ref<AuthComponentType | null>(null)
function onAuth() {
if (authenticationRef.value) {
authenticationRef.value.isVisible = true
}
}
function initChangeParams() {
if (currentState.value === 'mallProduct') {
initPublishParams()
}
else {
initLikesParams()
}
finished.value = false // 重置加载完成状态
getList()
}
// 切换发布/点赞
function changeTabs(id: string) {
currentState.value = id
currentType.value = '0'
initChangeParams()
}
// 切换模型/工作流/图片
function changeType(id: string) {
if (id === '2') {
statusOptions.value.forEach((item) => {
if (item.dictValue === '2') {
item.disabled = true
}
})
}
else {
statusOptions.value.forEach((item) => {
if (item.dictValue === '2') {
item.disabled = false
}
})
}
currentType.value = id
initChangeParams()
}
function initPageNUm() {
if (currentState.value === 'mallProduct') {
publishParams.value.pageNum = 1
}
else {
likesParams.value.pageNum = 1
}
finished.value = false // 重置加载完成状态
getList()
}
// 切换select全部状态/已发布/
function changeStatus(value: string) {
publishParams.value.status = value
initPageNUm()
}
// 切换发布的最热/最新
function changeOrder(value: string) {
publishParams.value.orderByColumn = value
initPageNUm()
}
// 切换点赞的最热/最新
function changeLikeOrder(value: string) {
likesParams.value.orderByColumn = value
initPageNUm()
}
// 切换日期
async function changeDate(value: string[]) {
publishParams.value.startTime = `${await formatDate(value[0] as string)} 00:00:00`
publishParams.value.endTime = `${await formatDate(value[1] as string)} 23:59:59`
initPageNUm()
}
// 获取用户点赞/粉丝/关注数量
interface SelectUserInfo {
likeCount: number
bean: number
download: number
attention: number
}
const selectUserInfo = ref<SelectUserInfo>({
likeCount: 0,
bean: 0,
download: 0,
attention: 0,
})
async function getAttention() {
try {
const res = await request.get('/attention/selectUserInfo')
if (res.code === 200) {
selectUserInfo.value = res.data
}
}
catch (err) {
console.log(err)
}
}
getAttention()
// Banner 样式
const bannerStyle = {
backgroundImage:
'url(\'https://img.zcool.cn/community/special_cover/3a9a64d3628c000c2a1000657eec.jpg\')',
}
// 定义响应接口
interface ApiResponse<T> {
code: number
rows: T[]
message: string
}
// 定义数据接口
interface UserData {
id: number
name: string
email: string
status: number
}
// 查询发布模型接口
const dataList = ref([])
async function getList() {
if (loading.value || finished.value)
return
loading.value = true
let params = {}
if (currentState.value === 'mallProduct') {
params = publishParams.value
}
else {
params = likesParams.value
}
const url = urlList.value[currentState.value][currentType.value]
try {
const res = await request.post<ApiResponse<UserData>>(url, params)
if (res.code === 200) {
// 如果是第一页,直接赋值,否则追加数据
if (params.pageNum === 1) {
dataList.value = res.rows
}
else {
dataList.value = [...dataList.value, ...res.rows]
}
total.value = res.total // 假设接口返回了总条数
// 判断是否加载完所有数据
if (dataList.value.length >= total.value) {
finished.value = true
}
// 自动增加页码
if (currentState.value === 'mallProduct') {
publishParams.value.pageNum++
}
else {
likesParams.value.pageNum++
}
}
}
catch (err) {
dataList.value = []
finished.value = true
console.log(err)
}
finally {
loading.value = false
}
}
onMounted(() => {
window.addEventListener('scroll', topedRefresh)
observer.value = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loading.value && !finished.value) {
getList()
}
},
{
threshold: 0.1,
},
)
if (loadingTrigger.value) {
observer.value.observe(loadingTrigger.value)
}
if (type && type === 'like') {
currentState.value = type
}
if (status === '0' || status === '1' || status === '2') {
currentType.value = status
}
getList()
})
onUnmounted(() => {
window.removeEventListener('scroll', topedRefresh)
if (observer.value) {
observer.value.disconnect()
}
})
async function topedRefresh() {
if (import.meta.client) {
await nextTick()
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
initPageNUm()
}
function onClearDate() {
(publishParams.value.startTime = ''), (publishParams.value.endTime = ''), initPageNUm()
}
// 获取粉丝的列表
const isShowFan = ref(false)
const attentionFinished = ref(false)
const attentionList = ref([])
const attentionListParams = ref({
pageNumber: 1,
pageSize: 20,
})
async function getAttentionList() {
try {
if (attentionFinished.value)
return
const res = await request.post(`/attention/selectToAttention`, {
...attentionListParams.value,
})
if (res.code === 200) {
if (attentionListParams.value.pageNumber === 1) {
attentionList.value = res.data.list
}
else {
attentionList.value = [...attentionList.value, ...res.data.list]
}
total.value = res.data.total // 假设接口返回了总条数
// 判断是否加载完所有数据
if (attentionList.value.length >= total.value) {
attentionFinished.value = true
}
attentionListParams.value.pageNumber++
}
}
catch (err) {
attentionList.value = []
attentionFinished.value = true
console.log(err)
}
}
getAttentionList()
// 获取关注的列表
const likeFinished = ref(false)
const likeList = ref([])
const likeListParams = ref({
pageNumber: 1,
pageSize: 20,
type: null,
})
async function getLikeList() {
try {
if (likeFinished.value)
return
const res = await request.post(`/attention/selectAttentionList`, {
...likeListParams.value,
})
if (res.code === 200) {
if (likeListParams.value.pageNumber === 1) {
likeList.value = res.data.list
}
else {
likeList.value = [...likeList.value, ...res.data.list]
}
total.value = res.data.total // 假设接口返回了总条数
// 判断是否加载完所有数据
if (likeList.value.length >= total.value) {
likeFinished.value = true
}
likeListParams.value.pageNumber++
}
}
catch (err) {
likeList.value = []
likeFinished.value = true
console.log(err)
}
}
getLikeList()
function closefanList() {
isShowFan.value = false
}
// 去关注/取消关注当前用户
async function onAttention(item: any) {
try {
const userId = userStore.userInfo.userId
if (userId === item.userId) {
return message.warning('自己不能关注自己')
}
const res = await request.get(`/attention/addAttention?userId=${item.userId}`)
if (res.code === 200) {
if (res.data) {
message.success('关注成功')
}
else {
message.success('取消关注成功')
}
item.attention = !item.attention
}
}
catch (err) {
console.log(err)
}
}
// 过去邀请码
const invitationCode = ref('')
async function getInvitation() {
try {
const res = await request.get(`/invitation/getInvitationCode`)
if (res.code === 200) {
invitationCode.value = res.msg
}
}
catch (err) {
console.log(err)
}
}
getInvitation()
// 获取邀请列表
// async function getInvitationList() {
// }
// 复制到粘贴板
function copyToClipboard(text: string) {
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
const success = document.execCommand('copy')
document.body.removeChild(textarea)
if (success) {
message.success('复制成功!')
}
else {
message.success('复制成功!')
}
}
// 复制到粘贴板
function handlePositiveClick() {
copyToClipboard(invitationCode.value)
}
const activeTab = ref('like')
// 邀请码列表
const invitationList = ref({})
async function handleNegativeClick() {
try {
const res = await request.get(`/invitation/earningsDisplay`)
if (res.code === 200) {
invitationList.value = res.data
showInvitationModal.value = true
}
}
catch (err) {
console.log(err)
}
}
function showLike(type: string) {
isShowFan.value = true
activeTab.value = type
}
function toWallet() {
router.push({
path: `/wallet`,
})
}
// 查询是否已绑定支付宝
const zfbStatus = ref('0')
async function getBindStatus() {
const res = await request.get(`/ali/pay/queryBindStatus`)
if (res.code === 200) { // '1 绑定 0不绑定
zfbStatus.value = res.data
}
}
getBindStatus()
// 绑定支付宝
const qrUrl = ref('')
const isShowBindingModal = ref(false)
async function showBinding() {
try {
const res = await request.get(`/ali/pay/generateQrCode`)
if (res.code === 200) {
qrUrl.value = res.data
isShowBindingModal.value = true
pollingTimer && clearTimeout(pollingTimer)
pollingTimer = setInterval(async () => {
try {
const res2 = await request.get(`/ali/pay/queryBindStatus`)
if (res2.data === '1') {
await userStore.getUserInfo()
closeBindingModal()
message.success('绑定成功!')
getBindStatus()
}
}
catch (err) {
closeBindingModal()
message.warning('绑定失败,请稍后再试!')
}
}, 2000)
}
}
catch (err) {
console.log(err)
}
}
function closeBindingModal() {
pollingTimer && clearTimeout(pollingTimer)
isShowBindingModal.value = false
}
</script>
<template>
<div class="mx-auto relative">
<!-- Banner Section -->
<div class="banner-content h-32 bg-blue bg-cover bg-center" :style="bannerStyle" />
<!-- User Info Section -->
<div class="info-content mt-[-50px] p-5">
<div class="edit-info-content flex items-center">
<!-- Avatar -->
<div
class="mc-head mr-5 h-20 w-20 rounded-full bg-white shadow-lg flex items-center justify-center"
>
<div class="mc-head-inner h-18 w-18 m-1 rounded-full bg-blue-200">
<client-only>
<img
class="head-img m-1 h-16 w-16 rounded-full bg-white"
:src="userStore?.userInfo?.avatar"
alt="User Avatar"
>
</client-only>
<!-- {{ userStore.userInfo.avatar }} -->
</div>
</div>
<!-- Edit Info Button -->
<div
class="edit-info mr-2 cursor-pointer rounded-full bg-white px-5 py-2 shadow-md"
@click="onEditInfo()"
>
编辑资料
</div>
<!-- Real Name Verification -->
<div
v-if="userStore?.userInfo.name"
class="edit-info rounded-full bg-white px-5 py-2 shadow-md"
>
已经实名
</div>
<div
v-else
class="edit-info cursor-pointer rounded-full bg-white px-5 py-2 shadow-md"
@click="onAuth()"
>
去实名
</div>
<div>
<n-popconfirm
class="bg-white ml-2"
positive-text="复制邀请码"
negative-text="查看列表"
:show-icon="false"
@positive-click="handlePositiveClick"
@negative-click="handleNegativeClick"
>
<template #trigger>
<n-button class="ml-2 rounded-lg" type="info" round>
获取邀请码
</n-button>
</template>
邀请码: {{ invitationCode }}
</n-popconfirm>
</div>
<div class="ml-4 cursor-pointer" @click="toWallet()">
<img src="@/assets/img/wallet.png" alt="">
</div>
<div v-if="zfbStatus === '1'" class="bg-[#3875f6] rounded-full px-4 py-2 ml-4 text-white">
已绑定支付宝
</div>
<div v-else class="text-xs px-3 py-2 border border-solid border-[#ccc] rounded-full ml-4 bg-white cursor-pointer" @click="showBinding">
绑定支付宝
</div>
</div>
<!-- User Details -->
<div class="user-info mt-4">
<div v-if="userStore.userInfo" class="nickname text-xl font-bold">
{{ userStore.userInfo.nickName }}
<!-- {{ userInfo.nickName }} -->
</div>
<div v-if="userStore.userInfo.brief" class="info-desc mt-1 text-sm text-gray-700">
{{ userStore.userInfo.brief }}
</div>
<div class="production-state mt-4 flex text-sm text-gray-700">
<div
class="production-state-item mr-5 cursor-pointer"
@click="showLike('like')"
>
<span class="production-state-number font-bold">{{
selectUserInfo.bean ? selectUserInfo.bean : 0
}}</span>
粉丝
</div>
<div
class="production-state-item mr-5 cursor-pointer"
@click="showLike('attention')"
>
<span class="production-state-number font-bold">{{
selectUserInfo.attention ? selectUserInfo.attention : 0
}}</span>
关注
</div>
<div class="production-state-item mr-5">
<span class="production-state-number font-bold">{{
selectUserInfo.modelDownloadNum + selectUserInfo.modelRunNum
}}</span>
作品被使用次数
</div>
<div class="production-state-item">
<span class="production-state-number font-bold">{{
selectUserInfo.imageLikeNum + selectUserInfo.modelLikeNum
}}</span>
作品被点赞次数
</div>
</div>
</div>
</div>
<div class="mc-tabs flex px-5 pb-3" style="border-bottom: 1px solid #e0e0e0">
<div
v-for="(item, index) in stateList"
:key="index"
class="mc-tabs-btn mr-2 cursor-pointer rounded-full px-5 py-1"
:class="{
'bg-black text-white': currentState === item.id,
'bg-white text-black': currentState !== item.id,
}"
@click="changeTabs(item.id)"
>
{{ item.title }}
</div>
</div>
<div class="select-content mt-4 flex items-center justify-between px-5">
<div class="flex items-center rounded-full bg-gray-100">
<div
v-for="(item, index) in typeList"
:key="index"
class="m-1 mr-2 cursor-pointer rounded-full px-4 py-1 text-sm"
:class="{
'bg-white': item.id === currentType,
}"
@click="changeType(item.id)"
>
{{ item.title }}
</div>
</div>
<!-- Published Works Filters -->
<div v-if="currentState === 'mallProduct'" class="flex items-center">
<div>
<n-select
v-model:value="publishParams.status"
:options="statusOptions"
label-field="dictLabel"
value-field="dictValue"
placeholder="请选择"
style="width: 180px"
class="mr-2"
@update:value="changeStatus"
/>
</div>
<div>
<n-select
v-model:value="publishParams.orderByColumn"
:options="orderOptions"
label-field="dictLabel"
value-field="dictValue"
placeholder="请选择"
class="mr-2 w-28"
@update:value="changeOrder"
/>
</div>
<div>
<n-date-picker
v-model:value="publishParams.date"
type="daterange"
style="width: 230px"
format="yyyy-MM-dd"
value-format="yyyy.MM.dd"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:on-clear="onClearDate"
clearable
@update:value="changeDate"
/>
</div>
</div>
<!-- Liked Works Filters -->
<div v-if="currentState === 'like'" class="flex items-center">
<NSelect
v-model:value="likesParams.orderByColumn"
:options="orderOptions"
label-field="dictLabel"
value-field="dictValue"
placeholder="请选择"
class="w-32"
@update:value="changeLikeOrder"
/>
</div>
</div>
<NConfigProvider>
<NMessageProvider>
<Authentication ref="authenticationRef" />
<EditUserInfo ref="editUserInfoRef" />
</NMessageProvider>
</NConfigProvider>
<!-- Dialog Components -->
<!-- grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4 p-4 -->
<div class="login-content my-4 grid xl:grid-cols-4 2xl:grid-cols-5 gap-4 px-5">
<PersonalCenterCard
v-for="(item, index) in dataList"
:key="index"
:item="item"
:current-type="currentType"
:current-state="currentState"
@toped-refresh="topedRefresh"
/>
</div>
<div ref="loadingTrigger" class="h-10">
<div v-if="loading" class="text-center text-gray-500">
加载中...
</div>
<div v-if="finished" class="text-center text-gray-500">
没有更多数据了
</div>
</div>
<div v-if="isShowFan" class="fan-centent">
<div
class="w-[550px] h-[calc(100vh-100px)] max-h-[700px] m-auto py-0 px-8 pb-[43px] bg-[#fff] rounded-lg relative"
>
<n-icon
size="20"
class="mr-2 cursor-pointer absolute top-4 right-2"
@click="closefanList"
>
<Close />
</n-icon>
<n-tabs
v-model:value="activeTab"
default-value="like"
justify-content="space-evenly"
type="line"
>
<n-tab-pane name="like" :tab="`粉丝${selectUserInfo.bean || 0}`">
<n-infinite-scroll
style="height: calc(100vh - 200px); padding: 0 10px"
:distance="10"
@load="getAttentionList"
>
<!-- <div class="overflow-y-auto h-[calc(100vh-200px)] px-2"> -->
<div
v-for="(item, index) in attentionList"
:key="index"
class="flex justify-between items-center p-2"
>
<div class="flex items-center">
<img
:src="item.avatar || ''"
alt=""
class="w-14 h-14 rounded-full mr-2"
>
{{ item.nickName }}
</div>
<div
class="bg-[#f4f5f9] px-4 py-2 rounded-full cursor-pointer"
@click="onAttention(item)"
>
{{ item.attention ? "已关注" : "未关注" }}
</div>
</div>
<div
v-if="attentionList.length === 0"
class="w-full text-center text-gray-500 font-bold mt-2"
>
暂无数据
</div>
<!-- </div> -->
</n-infinite-scroll>
</n-tab-pane>
<n-tab-pane name="attention" :tab="`关注${selectUserInfo.attention || 0}`">
<!-- <div class="overflow-y-auto h-[calc(100vh-200px)] px-2"> -->
<n-infinite-scroll
style="height: calc(100vh - 200px); padding: 0 10px"
:distance="10"
@load="getLikeList"
>
<div
v-for="(item, index) in likeList"
:key="index"
class="flex justify-between items-center p-2"
>
<div class="flex items-center">
<img
:src="item.avatar || ''"
alt=""
class="w-14 h-14 rounded-full mr-2"
>
{{ item.nickName }}
</div>
<div
class="bg-[#f4f5f9] px-4 py-2 rounded-full cursor-pointer"
@click="onAttention(item)"
>
{{ item.attention ? "已关注" : "未关注" }}
</div>
</div>
<div
v-if="attentionList.length === 0"
class="w-full text-center text-gray-500 font-bold mt-2"
>
暂无数据
</div>
<!-- </div> -->
</n-infinite-scroll>
</n-tab-pane>
</n-tabs>
</div>
</div>
<n-modal v-model:show="showInvitationModal">
<n-card
style="width: 600px"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<!-- <template #header-extra>
</template> -->
<div class="py-4 flex justify-between">
<div class="text-xl font-bold">
邀请列表
</div>
<div class="text-sm text-gray-600">
总收益: {{ invitationList.totalAmount }}
</div>
</div>
<div class="rounded-lg">
<div class="flex w-[100%]">
<div class="w-[180px] flex items-center py-2">
头像
</div>
<div class="w-[180px] flex items-center py-2">
邀请人购买数量
</div>
<div class="w-[180px] flex items-center py-2">
邀请人名字
</div>
</div>
<div class="max-h-[500px] overflow-y-auto">
<div
v-for="(item, index) in invitationList.earningsDisplayList"
:key="index"
class="flex w-[100%] hover:bg-[#f3f3f3] px-2"
>
<div class="w-[180px] flex items-center py-2">
<img class="w-10 h-10 rounded-full" :src="item.user.avatar" alt="">
</div>
<div class="w-[180px] flex items-center py-2">
{{ item.count }}
</div>
<div class="w-[180px] flex items-center py-2">
{{ item.user.userName }}
</div>
</div>
</div>
</div>
</n-card>
</n-modal>
<div v-if="isShowBindingModal" class="fan-centent">
<div class="relative flex flex-col items-center">
<div class="bg-[#000] bg-opacity-80 py-10 px-20 flex flex-col items-center justify-center rounded-lg">
<div class="text-xl text-white mb-4">
扫码绑定
</div>
<n-qr-code :value="qrUrl" :size="150" style="padding: 0;" />
<div class="text-base text-white mt-4">
</div>
</div>
<n-icon
size="30"
class="cursor-pointer mt-4 text-white"
@click="closeBindingModal"
>
<Close />
</n-icon>
</div>
</div>
</div>
</template>
<style scoped>
.fan-centent {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.4);
}
.edit-info {
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2);
}
</style>