Merge branch 'planet'

master
shenhan000 2025-04-26 17:01:17 +08:00
commit c3841d66e9
57 changed files with 7104 additions and 1314 deletions

View File

@ -1,2 +1,2 @@
NODE_ENV = 'development'
VITE_NUXT_ENV = 'http://1.13.246.108:8080/'
NODE_ENV = 'dev'
VITE_NUXT_ENV = 'http://www.mc158c.com'

View File

@ -1,2 +1,2 @@
NODE_ENV = 'production'
VITE_NUXT_ENV = 'http://113.45.190.154:8080/'
VITE_NUXT_ENV = 'http://www.mc158c.com'

4
.env.production 100644
View File

@ -0,0 +1,4 @@
# 生产环境配置
VITE_APP_TITLE=魔创未来
VITE_NUXT_ENVL=http://113.45.9.111
VITE_APP_ENV=production

View File

@ -82,3 +82,5 @@ npx degit antfu/vitesse-nuxt my-nuxt-app
cd my-nuxt-app
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
```
<!-- box-shadow: 0 4px 14px 0 rgba(0, 0, 0, .1); -->

View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

3
app/components.d.ts vendored
View File

@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAffix: typeof import('naive-ui')['NAffix']
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
@ -24,9 +25,11 @@ declare module 'vue' {
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NModal: typeof import('naive-ui')['NModal']
NPagination: typeof import('naive-ui')['NPagination']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NQrCode: typeof import('naive-ui')['NQrCode']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NTabPane: typeof import('naive-ui')['NTabPane']

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
// import { NConfigProvider, NInfiniteScroll, NInput } from 'naive-ui'
import {
ThumbsUp
} from 'lucide-vue-next';
ThumbsUp,
} from 'lucide-vue-next'
const props = defineProps({
height: {
@ -52,7 +52,7 @@ const urlList = ref({
const likeList = ref({
workflow: '/WorkFlowComment/commentLike?',
pictrue: '/imageComment/commentLike?',
model:'ModelComment/commentLike?'
model: 'ModelComment/commentLike?',
})
//
const sendMessageList = ref({
@ -81,9 +81,11 @@ async function getCommentList() {
let url = ''
if (props.type === 'workflow') {
url = `${urlList.value[props.type]}commentId=${props.detailsInfo.id}&sortType=${sortType.value}`
}else if(props.type === 'pictrue'){
}
else if (props.type === 'pictrue') {
url = `${urlList.value[props.type]}imageId=${props.detailsInfo.id}&sortType=${sortType.value}`
}else{
}
else {
url = `${urlList.value[props.type]}modelId=${props.detailsInfo.id}&sortType=${sortType.value}`
}
const res = await request.get(url)
@ -115,7 +117,7 @@ function handleBlur(ele) {
}
}
//
async function sendMessage(ele, index) {
async function sendMessage(ele: any, index: number) {
if (ele && ele.userId) {
try {
if (ele.word) {
@ -124,7 +126,8 @@ async function sendMessage(ele, index) {
commentParams.value.replyUserId = ele.commentId
if (props.type === 'pictrue') {
commentParams.value.modelImageId = props.detailsInfo.id
}else if(props.type === 'model'){
}
else if (props.type === 'model') {
commentParams.value.modelId = props.detailsInfo.id
}
const res = await request.post(sendMessageList.value[props.type], commentParams.value)
@ -151,7 +154,8 @@ async function sendMessage(ele, index) {
commentParams.value.replyUserId = ''
if (props.type === 'pictrue') {
commentParams.value.modelImageId = props.detailsInfo.id
}else if(props.type === 'model'){
}
else if (props.type === 'model') {
commentParams.value.modelId = props.detailsInfo.id
}
const res = await request.post(sendMessageList.value[props.type], commentParams.value)
@ -239,11 +243,11 @@ function changeType(type: string) {
<div class="left text-[20px] mr-2">
讨论
</div>
<div class="text-[#999]" v-if="props.type !== 'pictrue'">
<div v-if="props.type !== 'pictrue'" class="text-[#999]">
{{ commentCount }}
</div>
</div>
<div class="flex items-center" v-if="props.type !== 'pictrue'">
<div v-if="props.type !== 'pictrue'" class="flex items-center">
<div class="cursor-pointer" :class="sortType === 0 ? '' : 'text-[#999]'" @click="changeType(0)">
最热
</div>

View File

@ -0,0 +1,285 @@
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
import { commonApi } from '@/api/common'
import { uploadImagesInBatches } from '@/utils/uploadImg.ts'
import { NButton, NForm, NFormItem, NInput, NInputNumber, NModal, NRadio, NRadioGroup, NSelect, NSpace, NTextarea, useMessage } from 'naive-ui'
import { computed, ref } from 'vue'
const props = withDefaults(defineProps<Props>(), {
show: false,
})
const emit = defineEmits<{
(e: 'update:show', value: boolean): void
(e: 'success'): void
(e: 'refresh'): void
}>()
const message = useMessage()
interface Props {
show: boolean
}
const formRef = ref<FormInst | null>(null)
const loading = ref(false)
const formValue = ref({
imageUrl: '',
communityName: '',
communityTag: null,
type: '1',
price: 0,
validityDay: null,
description: '', //
})
const rules = {
imageUrl: {
required: true,
message: '请上传封面',
trigger: 'change',
},
communityName: {
required: true,
message: '请输入星球名称',
},
communityTag: {
required: true,
message: '请选择星球标签',
},
validityDay: {
required: true,
message: '请选择有效期',
},
}
//
const tagList = ref<{ dictLabel: string, dictValue: string }[]>([])
async function getDictType() {
try {
const res = await commonApi.dictType({ type: 'community_tag' })
if (res.code === 200)
tagList.value = res.data
}
catch (error) {
console.error(error)
}
}
getDictType()
const validTimeOptions = [
{ label: '1年', value: '1' },
{ label: '2年', value: '2' },
{ label: '3年', value: '3' },
]
const modelValue = computed({
get: () => props.show,
set: (value: boolean) => emit('update:show', value),
})
async function handleSubmit() {
try {
await formRef.value?.validate()
loading.value = true
const params = {
...formValue.value,
price: formValue.value.type === '1' ? formValue.value.price : 0,
}
const res = await request.post('/community/addCommunity', params)
if (res.code === 200) {
message.success('创建成功')
emit('success')
emit('refresh')
handleClose()
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
function handleClose() {
emit('update:show', false)
formValue.value = {
name: '',
tags: [],
type: '',
price: 0,
validTime: 0,
description: '', //
}
}
const pictureInput = ref<HTMLInputElement | null>(null)
function handlePictureInput() {
pictureInput.value?.click()
}
//
async function handlePictureChange(event: Event) {
const files = (event.target as HTMLInputElement).files
if (!files || files.length === 0)
return
if (files.length > 1) {
message.error('只能选择 1 张图片')
return
}
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'))
if (imageFiles.length === 0) {
message.error('请选择有效的图片文件')
return
}
try {
const pictureResultList = await uploadImagesInBatches(imageFiles)
formValue.value.imageUrl = pictureResultList[0].url
;(event.target as HTMLInputElement).value = ''
}
catch (error: any) {
message.error('图片上传失败')
}
}
</script>
<template>
<NModal
v-model:show="modelValue"
preset="dialog"
title="创建星球"
:show-icon="false"
:mask-closable="false"
style="width: 420px"
:style="{
'width': '420px',
'--n-title-text-align': 'center',
}"
@close="handleClose"
>
<NForm
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="top"
label-width="auto"
require-mark-placement="right-hanging"
size="large"
>
<!-- 上传封面 -->
<div class="flex justify-center">
<NFormItem path="imageUrl" style="margin-bottom: 0">
<div
class="w-[120px] h-[120px] bg-[#f7f8fa] rounded-lg flex items-center justify-center cursor-pointer mb-6 overflow-hidden"
@click="handlePictureInput"
>
<img
v-if="formValue.imageUrl"
:src="formValue.imageUrl"
class="w-full h-full object-cover"
alt="封面"
>
<span v-else></span>
</div>
</NFormItem>
</div>
<!-- 星球名称 -->
<NFormItem label="星球名称" path="communityName">
<NInput v-model:value="formValue.communityName" placeholder="请输入星球名称" />
</NFormItem>
<!-- 星球标签 -->
<NFormItem label="星球标签" path="communityTag">
<NSelect
v-model:value="formValue.communityTag"
:options="tagList.map(item => ({ label: item.dictLabel, value: item.dictValue }))"
placeholder="请选择星球标签"
/>
</NFormItem>
<!-- 描述 -->
<!-- <NFormItem label="描述" path="description">
<NTextarea
v-model:value="formValue.description"
placeholder="请输入星球描述"
:autosize="{
minRows: 3,
maxRows: 5,
}"
/>
</NFormItem> -->
<NFormItem label="描述" path="description">
<NInput
v-model:value="formValue.description"
placeholder="请输入星球描述"
type="textarea"
:autosize="{
minRows: 3,
maxRows: 5,
}"
/>
</NFormItem>
<!-- 星球类型 -->
<NFormItem label="星球类型" path="type">
<NRadioGroup v-model:value="formValue.type" name="type">
<NSpace>
<NRadio value="1">
付费星球
</NRadio>
<NRadio value="0">
免费星球
</NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
<!-- 设置加入费用 -->
<NFormItem v-if="formValue.type === '1'" label="设置加入费用" path="price">
<NInputNumber
v-model:value="formValue.price"
:min="0"
:max="99999"
:step="1"
placeholder="请输入加入费用"
>
<template #suffix>
积分
</template>
</NInputNumber>
</NFormItem>
<!-- 设置成员加入有效期 -->
<NFormItem label="设置成员加入有效期" path="validityDay">
<NSelect
v-model:value="formValue.validityDay"
:options="validTimeOptions"
placeholder="请选择有效期"
/>
</NFormItem>
</NForm>
<template #action>
<NSpace>
<NButton @click="handleClose">
取消
</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">
创建
</NButton>
</NSpace>
</template>
</NModal>
<input
ref="pictureInput"
type="file"
accept="image/*"
class="hidden"
@change="handlePictureChange"
>
</template>

View File

@ -0,0 +1,149 @@
# 创建新文件
<script setup lang="ts">
import { Download } from 'lucide-vue-next'
import { useMessage } from 'naive-ui'
import { onMounted, ref, watch } from 'vue'
interface Props {
communityId: number | string
tenantId: number | string
show?: boolean
}
interface FileItem {
fileName: string
fileSize: string
uploadTime: string
uploadBy: string
url: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:show': [value: boolean]
}>()
const message = useMessage()
const loading = ref(false)
const fileList = ref<FileItem[]>([])
const currentFile = ref<FileItem | null>(null)
//
async function getFileList() {
try {
const res = await request.post('/communityFile/list', {
tenantId: props.tenantId,
communityId: props.communityId,
})
if (res.code === 200) {
fileList.value = res.rows
}
}
catch (error) {
message.error('获取文件列表失败')
}
}
//
async function handleDownload(file: FileItem) {
if (!file.url) {
message.error('文件链接不存在')
return
}
currentFile.value = file
loading.value = true
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
message.success('下载成功')
}
catch (error) {
message.error('下载失败')
}
finally {
loading.value = false
currentFile.value = null
}
}
//
onMounted(() => {
if (props.show) {
getFileList()
}
})
// show
watch(() => props.show, (newVal) => {
if (newVal) {
getFileList()
}
})
</script>
<template>
<n-modal
:show="props.show"
:mask-closable="true"
preset="dialog"
class="w-[600px]"
:show-icon="false"
title="文件下载"
@update:show="emit('update:show', $event)"
>
<div class="py-4">
<div v-if="fileList.length > 0" class="space-y-3">
<div
v-for="file in fileList"
:key="file.fileName"
class="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div class="flex-1 min-w-0">
<div class="text-base font-medium mb-1 truncate">
{{ file.fileName }}
</div>
<div class="text-gray-500 text-sm">
{{ file.fileSize }} · {{ file.uploadBy }} · {{ file.uploadTime }}
</div>
</div>
<n-button
type="primary"
class="ml-4 !px-6"
:loading="currentFile?.fileName === file.fileName && loading"
:theme-overrides="{
common: {
primaryColor: '#3f7ef7',
primaryColorHover: '#3f7ef7',
},
}"
@click="handleDownload(file)"
>
<template #icon>
<Download class="w-4 h-4 mr-1" />
</template>
下载
</n-button>
</div>
</div>
<div v-else class="text-center text-gray-500 py-8">
暂无文件
</div>
</div>
<template #action>
<div class="flex justify-end">
<n-button
size="large"
class="!px-8"
@click="emit('update:show', false)"
>
关闭
</n-button>
</div>
</template>
</n-modal>
</template>

View File

@ -0,0 +1,320 @@
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
import { commonApi } from '@/api/common'
import { uploadImagesInBatches } from '@/utils/uploadImg'
import { NButton, NForm, NFormItem, NInput, NInputNumber, NModal, NRadio, NRadioGroup, NSelect, NSpace, useMessage } from 'naive-ui'
import { computed, defineEmits, ref, watchEffect } from 'vue'
interface Props {
show: boolean
communityId: string | number
tenantId: string | number
}
const props = withDefaults(defineProps<Props>(), {
show: false,
communityId: '',
tenantId: '',
})
const emit = defineEmits<{
(e: 'update:show', value: boolean): void
(e: 'success'): void
}>()
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const loading = ref(false)
const formValue = ref({
imageUrl: '',
communityName: '',
communityTag: null,
type: '1',
price: 0,
validityDay: null,
description: '',
})
const rules = {
imageUrl: {
required: true,
message: '请上传封面',
trigger: 'change',
},
communityName: {
required: true,
message: '请输入星球名称',
},
communityTag: {
required: true,
message: '请选择星球标签',
},
validityDay: {
required: true,
message: '请选择有效期',
},
}
//
const tagList = ref<{ dictLabel: string, dictValue: string }[]>([])
async function getDictType() {
try {
const res = await commonApi.dictType({ type: 'community_tag' })
if (res.code === 200)
tagList.value = res.data
}
catch (error) {
console.error(error)
}
}
getDictType()
//
async function getCommunityDetail() {
try {
const res = await request.get(`/community/detail?communityId=${props.communityId}&tenantId=${props.tenantId}`)
if (res.code === 200) {
const { imageUrl, communityName, communityTag, type, price, validityDay, description, id } = res.data
formValue.value = {
imageUrl,
communityName,
communityTag: communityTag.toString(),
type: type.toString(),
price,
id,
validityDay,
description,
}
}
}
catch (error) {
console.error(error)
message.error('获取星球详情失败')
}
}
// id
watchEffect(() => {
if (props.show && props.communityId && props.tenantId)
getCommunityDetail()
})
const validTimeOptions = [
{ label: '1年', value: 1 },
{ label: '2年', value: 2 },
{ label: '3年', value: 3 },
]
const modelValue = computed({
get: () => props.show,
set: (value: boolean) => emit('update:show', value),
})
async function handleSubmit() {
try {
await formRef.value?.validate()
loading.value = true
const params = {
...formValue.value,
price: formValue.value.type === '1' ? formValue.value.price : 0,
// communityId: props.communityId,
// tenantId: props.tenantId,
}
const res = await request.post('/community/edit', params)
if (res.code === 200) {
message.success('更新成功')
emit('success')
handleClose()
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
function handleClose() {
emit('update:show', false)
formValue.value = {
imageUrl: '',
communityName: '',
communityTag: null,
type: '1',
price: 0,
validityDay: null,
description: '',
}
}
const pictureInput = ref<HTMLInputElement | null>(null)
function handlePictureInput() {
pictureInput.value?.click()
}
//
async function handlePictureChange(event: Event) {
const files = (event.target as HTMLInputElement).files
if (!files || files.length === 0)
return
if (files.length > 1) {
message.error('只能选择 1 张图片')
return
}
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'))
if (imageFiles.length === 0) {
message.error('请选择有效的图片文件')
return
}
try {
const pictureResultList = await uploadImagesInBatches(imageFiles)
formValue.value.imageUrl = pictureResultList[0].url
;(event.target as HTMLInputElement).value = ''
}
catch (error: any) {
message.error('图片上传失败')
}
}
</script>
<template>
<NModal
v-model:show="modelValue"
preset="dialog"
title="编辑星球"
:show-icon="false"
:mask-closable="false"
style="width: 420px"
:style="{
'width': '420px',
'--n-title-text-align': 'center',
}"
@close="handleClose"
>
<NForm
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="top"
label-width="auto"
require-mark-placement="right-hanging"
size="large"
class="edit-planet-form"
>
<!-- 上传封面 -->
<div class="flex justify-center">
<NFormItem path="imageUrl" style="margin-bottom: 0">
<div
class="w-[120px] h-[120px] bg-[#f7f8fa] rounded-lg flex items-center justify-center cursor-pointer mb-3 overflow-hidden"
@click="handlePictureInput"
>
<img
v-if="formValue.imageUrl"
:src="formValue.imageUrl"
class="w-full h-full object-cover"
alt="封面"
>
<span v-else></span>
</div>
</NFormItem>
</div>
<!-- 星球名称 -->
<NFormItem label="星球名称" path="communityName">
<NInput v-model:value="formValue.communityName" placeholder="请输入星球名称" />
</NFormItem>
<!-- 星球标签 -->
<NFormItem label="星球标签" path="communityTag">
<NSelect
v-model:value="formValue.communityTag"
:options="tagList.map(item => ({ label: item.dictLabel, value: item.dictValue }))"
placeholder="请选择星球标签"
/>
</NFormItem>
<!-- 描述 -->
<NFormItem label="描述" path="description">
<NInput
v-model:value="formValue.description"
placeholder="请输入星球描述"
type="textarea"
:autosize="{
minRows: 3,
maxRows: 5,
}"
/>
</NFormItem>
<!-- 星球类型 -->
<NFormItem label="星球类型" path="type">
<NRadioGroup v-model:value="formValue.type" name="type">
<NSpace>
<NRadio value="1">
付费星球
</NRadio>
<NRadio value="0">
免费星球
</NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
<!-- 设置加入费用 -->
<NFormItem v-if="formValue.type === '1'" label="设置加入费用" path="price">
<NInputNumber
v-model:value="formValue.price"
:min="0"
:max="99999"
:step="1"
placeholder="请输入加入费用"
>
<template #suffix>
积分
</template>
</NInputNumber>
</NFormItem>
<!-- 设置成员加入有效期 -->
<NFormItem label="设置成员加入有效期" path="validityDay">
<NSelect
v-model:value="formValue.validityDay"
:options="validTimeOptions"
placeholder="请选择有效期"
/>
</NFormItem>
</NForm>
<template #action>
<NSpace>
<NButton @click="handleClose">
取消
</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">
保存
</NButton>
</NSpace>
</template>
</NModal>
<input
ref="pictureInput"
type="file"
accept="image/*"
class="hidden"
@change="handlePictureChange"
>
</template>
<style scoped>
.edit-planet-form :deep(.n-form-item) {
margin-bottom: 0;
}
.edit-planet-form :deep(.n-form-item:last-child) {
margin-bottom: 0;
}
</style>

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import type { FormInst } from 'naive-ui';
import { ref } from 'vue';
import { uploadImagesInBatches } from '../utils/uploadImg.ts';
import type { FormInst } from 'naive-ui'
import { ref } from 'vue'
import { uploadImagesInBatches } from '../utils/uploadImg.ts'
const userStore = useUserStore()
const userInfo = userStore.userInfo
const message = useMessage()
@ -58,17 +59,17 @@ async function saveInfo() {
if (!errors) {
const res1 = await request.post('/system/user/updateUserInfo', ruleForm.value)
if (res1.code === 200) {
const res = await request.get('/system/user/selectUserById')
if (res.code === 200) {
message.success('修改成功!')
userStore.setUserInfo(res.data)
await userStore.getUserInfo()
onCloseModel()
}
// const res = await request.get('/system/user/selectUserById')
// if (res.code === 200) {
// message.success('!')
// userStore.setUserInfo(res.data)
// onCloseModel()
// }
}
}
})
}
watch(

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { RotateCcw, X } from 'lucide-vue-next';
import type { FormInst } from 'naive-ui';
import { defineProps, onBeforeMount, onBeforeUnmount, ref } from 'vue';
import type { FormInst } from 'naive-ui'
import { RotateCcw, X } from 'lucide-vue-next'
import { defineProps, onBeforeMount, onBeforeUnmount, ref } from 'vue'
const props = defineProps({
isMember: {
@ -167,7 +167,7 @@ onBeforeUnmount(() => {
label-placement="left"
style="width: 240px"
>
<n-form-item label="输入积分" path="modelProduct.modelName">
<n-form-item label="输入金币" path="modelProduct.modelName">
<n-input v-model:value="amount" type="number" placeholder="输入积分" />
</n-form-item>
</n-form>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
//
import { headerRole } from "@/constants/index";
import { headerRole } from '@/constants/index'
import {
Bell,
CirclePlus,
@ -8,134 +8,140 @@ GraduationCap,
HardDriveUpload,
Image,
Monitor,
Workflow
} from "lucide-vue-next";
import { NConfigProvider, NMessageProvider } from "naive-ui";
Workflow,
} from 'lucide-vue-next'
import { NConfigProvider, NMessageProvider } from 'naive-ui'
// import { AtCircle } from '@vicons/ionicons5'
import { NIcon } from "naive-ui";
import { onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { NIcon } from 'naive-ui'
import { onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const modalStore = useModalStore();
const currentUseRoute = ref("");
const isShowPublishPicture = ref<boolean>(false);
const PublishPictureRef = ref<Payment | null>(null);
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const modalStore = useModalStore()
const currentUseRoute = ref('')
const isShowPublishPicture = ref<boolean>(false)
const PublishPictureRef = ref<Payment | null>(null)
const publishPicture = ref({
title: "",
title: '',
tags: [],
description: "",
description: '',
imagePaths: [],
});
})
watch(
() => route.path, // route.path
(newPath) => {
currentUseRoute.value = newPath;
(newPath: any) => {
currentUseRoute.value = newPath
},
{ immediate: true } //
);
{ immediate: true }, //
)
function hasItem(path: string, list: any) {
return !list.includes(path);
return !list.includes(path)
}
const searchText = ref("");
const searchText = ref('')
function onSearch(value: any) {
console.log("搜索:", value);
console.log('搜索:', value)
//
}
function initSearch(){
console.log( 'initSearch');
}
//
const notificationOptions = [
{
label: "系统通知",
key: "system",
},
{
label: "互动消息",
key: "interaction",
},
];
// const notificationOptions = [
// {
// label: '',
// key: 'system',
// },
// {
// label: '',
// key: 'interaction',
// },
// ]
function renderIcon(icon: Component) {
return () => {
return h(NIcon, null, {
default: () => h(icon),
});
};
})
}
}
//
const publishOptions = [
{
label: "模型",
key: "publish-model",
label: '模型',
key: 'publish-model',
icon: renderIcon(HardDriveUpload),
},
{
label: "图片",
key: "picture",
label: '图片',
key: 'picture',
icon: renderIcon(Image),
},
{
label: "工作流",
key: "publish-workflow",
label: '工作流',
key: 'publish-workflow',
icon: renderIcon(Workflow),
},
];
]
const userOptions = ref([
{
label: "我的模型",
key: "0",
label: '我的模型',
key: '0',
},
{
label: "我的作品",
key: "2",
label: '我的作品',
key: '2',
},
{
label: "我的点赞",
key: "like",
label: '我的点赞',
key: 'like',
},
// {
// label: "",
// key: "userSettings",
// },
{
label: "退出登录",
key: "logout",
label: '退出登录',
key: 'logout',
},
]);
])
//
async function handleUserSelect(key: string) {
if (key === "logout") {
if (key === 'logout') {
try {
await request.post("/logout");
userStore.logout();
navigateTo("/model-square");
} catch (error) {
console.error("Logout failed:", error);
await request.post('/logout')
userStore.logout()
navigateTo('/model-square')
}
}else if(key === "like"){
catch (error) {
console.error('Logout failed:', error)
}
}
else if (key === 'like') {
router.push(`/personal-center?type=${key}`)
}else{
}
else {
router.push(`/personal-center?status=${key}`)
}
}
//
async function handlePublishSelect(key: string) {
if (!userStore?.userInfo.name)
return
if (!userStore.isLoggedIn) {
modalStore.showLoginModal();
} else {
if (key === "picture") {
isShowPublishPicture.value = true;
if (PublishPictureRef.value) {
PublishPictureRef.value.isVisible = true;
modalStore.showLoginModal()
}
} else {
const baseUrl = window.location.origin;
window.open(`${baseUrl}/${key}?type=add`, "_blank", "noopener,noreferrer");
else {
if (key === 'picture') {
isShowPublishPicture.value = true
if (PublishPictureRef.value) {
PublishPictureRef.value.isVisible = true
}
}
else {
const baseUrl = window.location.origin
debugger
window.open(`${baseUrl}/${key}?type=add`, '_blank', 'noopener,noreferrer')
// router.push({
// path: `/${key}`,
// query: {
@ -146,37 +152,39 @@ async function handlePublishSelect(key: string) {
}
}
function closePublishImg() {
isShowPublishPicture.value = false;
isShowPublishPicture.value = false
if (PublishPictureRef.value) {
PublishPictureRef.value.isVisible = false;
PublishPictureRef.value.isVisible = false
}
}
function handleLogin() {
modalStore.showLoginModal();
modalStore.showLoginModal()
}
const msgList = ref([]);
const isRead = ref('0')
const msgList = ref([])
async function getAllMessage() {
try {
const res = await request.get("/advice/getAllMsg");
if (res.code == 200) {
msgList.value = res.data;
const res = await request.get('/advice/getAllMsg')
if (res.code === 200) {
msgList.value = res.data
}
} catch (err) {
console.log(err);
}
catch (err) {
console.log(err)
}
}
// getAllMessage();
//
function toDetail() {
const baseUrl = window.location.origin;
window.open(`${baseUrl}/message`, "_blank", "noopener,noreferrer");
const baseUrl = window.location.origin
window.open(`${baseUrl}/message`, '_blank', 'noopener,noreferrer')
}
onMounted(() => {});
function handleTabClick(path: string) {
router.push(path)
}
onMounted(() => {})
</script>
<template>
@ -188,9 +196,17 @@ onMounted(() => {});
<!-- Logo 区域调整 -->
<div class="flex min-w-[130px] items-center gap-3 pr-4">
<div class="flex items-center gap-2">
<div class="flex items-center cursor-pointer" @click="handleTabClick('/')">
<img
src="@/assets/img/logo.png"
alt="魔创未来"
class="h-8 w-8"
>
<span class="text-[#328AFE] text-xl font-bold ml-2">魔创未来</span>
</div>
<NuxtLink to="/" class="text-xl font-semibold tracking-tight no-underline">
<!-- <img src="/vite.png" alt="Logo" class="h-9 w-9" /> -->
魔创未来
<!-- 魔创未来 -->
<!-- <img
src="https://liblibai-web-static.liblib.cloud/liblibai_v4_online/static/_next/static/images/icon-logo.e3ce24f316fb81dbde1cafc3bf956080.svg"
alt=""
@ -287,13 +303,15 @@ onMounted(() => {});
>
{{ item.content }}
</n-ellipsis>
<div class="text-[12px] text-gray-400">{{ item.createTime }}</div>
<div class="text-[12px] text-gray-400">
{{ item.createTime }}
</div>
</div>
<div class="w-4 flex h-[100%] items-center justify-center">
<div
class="w-2 h-2 bg-[#ea5049] rounded"
v-if="item.isRead === '0'"
></div>
class="w-2 h-2 bg-[#ea5049] rounded"
/>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { RotateCcw, X } from 'lucide-vue-next';
import type { FormInst } from 'naive-ui';
import { defineProps, onBeforeMount, onBeforeUnmount, ref } from 'vue';
import type { FormInst } from 'naive-ui'
import { RotateCcw, X } from 'lucide-vue-next'
import { defineProps, onBeforeMount, onBeforeUnmount, ref } from 'vue'
const props = defineProps({
isMember: {

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { FormInst, FormRules } from 'naive-ui'
import phoneImg from '@/assets/img/phone.png'
import wechatImg from '@/assets/img/wechat.png'
import type { FormInst, FormRules } from 'naive-ui'
import { createDiscreteApi } from 'naive-ui'
import { ref } from 'vue'
@ -19,7 +19,7 @@ const rules: FormRules = {
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 4, message: '验证码长度为4位', trigger: 'blur' },
{ len: 6, message: '验证码长度为6位', trigger: 'blur' },
],
}
//
@ -93,13 +93,19 @@ async function handleValidateClick(e: MouseEvent) {
const res = await request.post<ApiResponse<UserData>>('/phoneLogin', {
...formData.value,
})
userStore.setToken(res.token)
const res1 = await request.get<ApiResponse<UserToken>>('/getInfo', {
token: res.token,
})
userStore.setUserInfo(res1.user)
await userStore.setToken(res.token)
await userStore.getUserInfo()
// const res1 = await request.get<ApiResponse<UserToken>>('/getInfo', {
// token: res.token,
// })
// await userStore.getUserInfo()
// const res1 = await request.get('/system/user/selectUserById', {
// token: res.token,
// })
// debugger
// userStore.setUserInfo(res1.user)
onCloseLogin()
window.location.reload();
window.location.reload()
// window.location.href = '/'
}
else {

View File

@ -0,0 +1,215 @@
<script setup lang="ts">
import {
FileChartColumnIncreasing,
} from 'lucide-vue-next'
import { ref, watchEffect } from 'vue'
import { useRouter } from 'vue-router'
import DownPlanetFile from './DownPlanetFile.vue'
import QuestionPublish from './QuestionPublish.vue'
const props = defineProps<Props>()
const router = useRouter()
const message = useMessage()
const userStore = useUserStore()
interface Props {
communityId: string | number
tenantId: string | number
}
const planetInfo = ref({
imageUrl: '',
communityName: '',
description: '',
createDay: 0,
publishNum: 0,
})
const defaultCover = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjU4IiBoZWlnaHQ9IjI1OCIgdmlld0JveD0iMCAwIDI1OCAyNTgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjI1OCIgaGVpZ2h0PSIyNTgiIGZpbGw9IiNmMmYzZjUiLz48cGF0aCBkPSJNMTI5IDg2QzExMC43NzQgODYgOTYgMTAwLjc3NCA5NiAxMTlDOTYgMTM3LjIyNiAxMTAuNzc0IDE1MiAxMjkgMTUyQzE0Ny4yMjYgMTUyIDE2MiAxMzcuMjI2IDE2MiAxMTlDMTYyIDEwMC43NzQgMTQ3LjIyNiA4NiAxMjkgODZaTTg0LjU3MTQgMTUyQzc1LjE0MjkgMTUyIDY3LjQyODYgMTU5LjcxNCA2Ny40Mjg2IDE2OVYxNzQuNzE0QzY3LjQyODYgMTc0LjcxNCA4NC41NzE0IDE5MiAxMjkgMTkyQzE3My40MjkgMTkyIDE5MC41NzEgMTc0LjcxNCAxOTAuNTcxIDE3NC43MTRWMTY5QzE5MC41NzEgMTU5LjcxNCAxODIuODU3IDE1MiAxNzMuNDI5IDE1MkMxNjQuNTcxIDE1MiAxMjkgMTUyIDg0LjU3MTQgMTUyWiIgZmlsbD0iI2U1ZTZlYiIvPjwvc3ZnPg=='
//
async function getCommunityDetail() {
try {
const res = await request.get(`/community/detail?communityId=${props.communityId}&tenantId=${props.tenantId}`)
if (res.code === 200) {
planetInfo.value = res.data
}
}
catch (error) {
console.error(error)
}
}
//
const currentUserInfo = ref({})
async function getUserInfo() {
try {
const res = await request.get(
`/system/user/selectUserById?id=${props.tenantId}`,
)
if (res.code === 200) {
currentUserInfo.value = res.data
}
}
catch (error) {
console.log(error)
}
}
getUserInfo()
//
const isSelectAttention = ref(null)
async function getSelectAttention() {
try {
const res = await request.get(
`/attention/selectAttention?userId=${props.tenantId}`,
)
if (res.code === 200) {
isSelectAttention.value = res.data
}
}
catch (error) {
console.log(error)
}
}
getSelectAttention()
// /
async function onAddAttention() {
try {
const res = await request.get(
`/attention/addAttention?userId=${props.tenantId}`,
)
if (res.code === 200) {
isSelectAttention.value = res.data
if (res.data) {
message.success('关注成功')
}
else {
message.success('取消关注成功')
}
}
}
catch (error) {
console.log(error)
}
}
//
const showQuestionModal = ref(false)
//
function handleShowQuestion() {
showQuestionModal.value = true
}
const showFileModal = ref(false)
function toFileList() {
router.push({
path: '/planetAllFile',
query: {
communityId: props.communityId,
tenantId: props.tenantId,
},
})
}
// id
watchEffect(() => {
if (props.communityId && props.tenantId)
getCommunityDetail()
})
</script>
<template>
<div class="bg-white rounded-lg w-[290px] p-4 h-fit">
<div class="flex flex-col gap-4">
<!-- 左侧图片 -->
<div class="w-[258px] h-[258px] rounded-lg relative bg-[#f2f3f5] overflow-hidden">
<img
:src="planetInfo.imageUrl || defaultCover"
class="w-full h-full object-cover flex-shrink-0 rounded-lg"
:alt="planetInfo.communityName"
@error="$event.target.src = defaultCover"
>
<div class="flex justify-between text-xs items-center absolute left-0 bottom-0 w-full p-2">
<div class="flex items-center">
<div class="w-6 h-6 rounded-full bg-[#f2f3f5] overflow-hidden mr-2 flex-shrink-0">
<img
:src="currentUserInfo.avatar"
:alt="currentUserInfo.nickName"
class="w-full h-full object-cover"
@error="$event.target.src = defaultAvatar"
>
</div>
<div class="text-white">
{{ currentUserInfo.nickName }}
</div>
</div>
<div v-if="Number(userStore.userInfo.userId) !== Number(tenantId)" class="bg-[#1e80ff] text-white py-1 px-2 rounded-lg cursor-pointer" @click="onAddAttention">
{{ isSelectAttention ? '已关注' : '关注' }}
</div>
</div>
</div>
<!-- 右侧信息 -->
<div class="flex flex-col justify-between flex-1">
<div class="space-y-2">
<h1 class="text-xl font-bold">
{{ planetInfo.communityName }}
</h1>
</div>
<div class="flex items-center gap-8 text-xs text-[#878d95] mt-1 mb-4">
<div>创建{{ planetInfo.validityDay }}</div>
</div>
<div class="text-sm text-[#4a5563] line-clamp-3">
{{ planetInfo.description }}
</div>
</div>
</div>
<div
class="flex items-center bg-[#F4F9FF] px-2 py-3 rounded-lg mt-4 text-sm text-gray-800 cursor-pointer"
@click="toFileList"
>
<FileChartColumnIncreasing class="w-4 h-4 mr-2" />
全部文件
</div>
<!-- 在线咨询 -->
<div class="mt-6">
<button
class="w-full bg-[#1e80ff] hover:bg-[#3b8fff] text-white rounded-lg py-3 mt-4"
@click="handleShowQuestion"
>
我要提问
</button>
<n-modal
v-model:show="showQuestionModal"
:style="{ width: '640px' }"
preset="card"
:mask-closable="false"
class="rounded-lg"
>
<QuestionPublish
:community-id="communityId"
:tenant-id="tenantId"
@success="showQuestionModal = false"
/>
</n-modal>
</div>
<DownPlanetFile
v-model:show="showFileModal"
:community-id="props.communityId"
:tenant-id="props.tenantId"
/>
</div>
</template>
<style scoped>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,667 @@
<script setup lang="ts">
// import { NConfigProvider, NInfiniteScroll, NInput } from 'naive-ui'
import {
EllipsisVertical,
Star,
ThumbsUp,
} from 'lucide-vue-next'
import { watch } from 'vue'
interface LikeParams {
id: string | number
commentId?: string | number
communityId?: string | number
tenantId?: string | number
publishId?: string | number
}
const props = defineProps({
isMy: {
type: Number,
default: 0,
},
height: {
type: Number,
default: 800,
},
type: {
type: String,
default: '',
},
publishListParams: {
type: Object,
default: () => ({}),
},
})
// const $props = defineProps(['headUrl', 'dataList', 'height'])
const userStore = useUserStore()
const message = useMessage()
//
const commentParams = ref({
content: '',
parentId: '',
replyUserId: '',
userId: userStore?.userInfo?.userId,
workFlowId: props.publishListParams.id,
})
function commentParamsInit() {
commentParams.value.content = ''
commentParams.value.replyUserId = ''
commentParams.value.parentId = ''
}
const isShowSend = ref(false)
const publicWord = ref('')
//
const commentList = ref([])
async function getCommentList() {
try {
const url = props.publishListParams.isMy === 1 ? '/personHome/getPersonHomeList' : '/publish/publishList'
const res = await request.post(url, props.publishListParams)
if (res.code === 200) {
for (let i = 0; i < res.rows.length; i++) {
res.rows[i].isShowInput = false
res.rows[i].isShowSend = false
if (res.rows[i].contentList && res.rows[i].contentList.length > 0) {
for (let j = 0; j < res.rows[i].contentList.length; j++) {
res.rows[i].contentList[j].isShowInput = false
res.rows[i].contentList[j].isShowSend = false
}
}
}
commentList.value = res.rows
}
}
catch (error) {
console.error(error)
}
// try {
// let url = ''
// if (props.type === 'workflow') {
// url = `${urlList.value[props.type]}commentId=${props.detailsInfo.id}&sortType=${sortType.value}`
// }
// else if (props.type === 'pictrue') {
// url = `${urlList.value[props.type]}imageId=${props.detailsInfo.id}&sortType=${sortType.value}`
// }
// else {
// url = `${urlList.value[props.type]}modelId=${props.detailsInfo.id}&sortType=${sortType.value}`
// }
// const res = await request.get(url)
// for (let i = 0; i < res.data.length; i++) {
// res.data[i].isShowInput = false
// res.data[i].isShowSend = false
// if (res.data[i].contentList.length > 0) {
// for (let j = 0; j < res.data[i].contentList.length; j++) {
// res.data[i].contentList[j].isShowInput = false
// res.data[i].contentList[j].isShowSend = false
// }
// }
// }
// commentList.value = res.data
// }
// catch (error) {
// console.log(error)
// }
}
// getCommentList()
// publishListParams
watch(
() => props.publishListParams,
() => {
if (props.publishListParams.type !== 999) {
if (props.publishListParams.communityId && props.publishListParams.tenantId) {
getCommentList()
}
}
},
{ deep: true, immediate: true },
)
function handleBlur(ele) {
//
if (!ele) {
isShowSend.value = !!publicWord.value
}
else {
ele.isShowSend = !!ele.word
}
}
//
async function sendMessage(type: string, ele: any, index: number, subIndex?: number) {
if (ele && ele.userId) {
try {
if (ele.word) {
commentParams.value.content = ele.word
commentParams.value.publishId = commentList.value[index].id
if (type === 'sub') {
commentParams.value.parentId = commentList.value[index].commentList[subIndex].id
}
else {
commentParams.value.parentId = commentList.value[index].id
}
commentParams.value.communityId = props.publishListParams.communityId
commentParams.value.tenantId = props.publishListParams.tenantId
const res = await request.post(`/publishComment/save`, commentParams.value)
if (res.code === 200) {
ele.word = ''
message.success('评论成功!')
getCommentList()
// getCommentNum()
commentParamsInit()
}
}
else {
message.warning('评论不能为空!')
}
}
catch (error) {
console.log(error)
}
}
}
//
// async function getCommentNum() {
// if (props.type !== 'pictrue') {
// try {
// const res = await request.get(`${commentNumUrl.value[props.type]}=${props.detailsInfo.id}`)
// if (res.code === 200) {
// commentCount.value = res.data
// }
// }
// catch (error) {
// console.log(error)
// }
// }
// }
// getCommentNum()
// /
async function handleFocus(type: string, item: any) {
const params: LikeParams = { id: '' }
if (type === 'parent') {
params.communityId = item.item.communityId
params.publishId = item.item.id
params.tenantId = item.item.tenantId
}
else {
const { id, communityId, tenantId } = item.sub
params.commentId = id
params.communityId = communityId
params.tenantId = tenantId
params.publishId = item.item.id
}
try {
const url = type === 'sub' ? '/publishComment/like' : '/PublishLike/like'
const res = await request.post(url, params)
if (res.code === 200) {
// message.success('')
getCommentList()
}
}
catch (error) {
console.log(error)
}
// const { id, communityId, tenantId, operatorId } = item
// const res = await request.post(`PublishLike/like`, { commentId: id, communityId, tenantId, operatorId })
// if (res.code === 200) {
// if (item.isLike === 0) {
// item.isLike = 1
// item.likeNum++
// message.success('')
// }
// else {
// item.isLike = 0
// item.likeNum--
// message.success('')
// }
// }
}
// /handleCollect
async function handleCollect(item: any) {
const params = {
publishId: item.id,
communityId: item.communityId,
tenantId: item.tenantId,
}
try {
const res = await request.post('/publish/collect', params)
if (res.code === 200) {
// message.success('')
getCommentList()
}
}
catch (error) {
console.log(error)
}
}
//
function handleMessage(item: any) {
if (!item.isShowInput) {
item.word = ''
}
item.isShowInput = !item.isShowInput
}
//
async function handleDel(type: string, item: any) {
const params = {}
if (type === 'parent') {
params.publishId = item.item.id
params.communityId = item.item.communityId
}
else {
const { id, communityId, tenantId } = item.sub
params.id = id
params.communityId = communityId
params.tenantId = tenantId
params.publishId = item.item.id
}
try {
const url = type === 'sub' ? '/publishComment/delete' : '/publish/remove'
const res = await request.post(url, params)
if (res.code === 200) {
message.success('删除成功!')
getCommentList()
}
}
catch (error) {
console.log(error)
}
}
async function handleSelect(event: any, type: string, item: any) {
event.stopPropagation() //
if (type === 'choiceness') {
try {
const res = await request.get(`publish/elite?publishId=${item.id}&communityId=${item.communityId}`)
if (res.code === 200) {
message.success(item.isElite === 1 ? '取消精选成功!' : '精选成功!')
getCommentList()
}
}
catch (error) {
console.log(error)
}
}
else if (type === 'complaint') {
console.log('投诉')
}
else if (type === 'delete') {
console.log('删除')
}
}
</script>
<template>
<div class="bg-white py-4 rounded min-h-[320px]">
<div
v-for="(item, index) in commentList"
:key="index"
class="px-4"
>
<div class="nav-wrap">
<div class="left-img">
<img :src="item.avatar">
</div>
<div class="right">
<div class="name flex justify-between items-center relative">
<span>{{ item.userName }}</span>
<img
v-if="item.isElite === 1"
class="cursor-pointer absolute right-0 top-[4px] w-10 h-10"
src="@/assets/img/elite.png"
alt=""
@click="showActivityList"
>
<div class="relative group">
<span>
<EllipsisVertical size="16" class="cursor-pointer rotate-90" />
</span>
<div
class="absolute right-0 top-[4px] hidden group-hover:block text-gray-500 text-[12px] bg-white rounded-lg text-center px-2 py-2 w-20 mt-2 shadow-lg z-10"
>
<!-- v-if="props.publishListParams.type === 0" -->
<div
class="menu-item hover:bg-gray-100 py-2 cursor-pointer rounded-lg"
@click="(event) => handleSelect(event, 'choiceness', item)"
>
{{ item.isElite === 1 ? '取消精选' : '精选' }}
</div>
<!-- <template v-if="userStore?.userInfo?.userId === detailsInfo.userId"> -->
<div
class="menu-item hover:bg-gray-100 py-2 cursor-pointer rounded-lg"
@click="(event) => handleSelect(event, 'complaint', item)"
>
投诉
</div>
<!-- <div
v-if="props.publishListParams.type === 0"
class="menu-item hover:bg-gray-100 py-2 cursor-pointer rounded-lg"
@click="(event) => handleSelect(event, 'delete')"
>
删除
</div> -->
<!-- </template> -->
</div>
</div>
</div>
<div class="des">
{{ item.content }}
</div>
<div class="star-wrap">
<span class="time">{{ item.createTime }}</span>
<div class="star-operator">
<div class="icon-wrap">
<ThumbsUp size="16" class="mr-1" :color="item.isLike !== 0 ? '#ff0000' : '#000000'" @click="handleFocus('parent', { item })" />
<span style="vertical-align: middle">{{ item.likeNum }}</span>
</div>
<div class="icon-wrap">
<Star size="16" class="mr-1" :color="item.isCollect !== 0 ? '#ff0000' : '#000000'" @click="handleCollect(item)" />
</div>
<span class="cursor-pointer m-r16" @click="handleMessage(item)"></span>
<span class="cursor-pointer" @click="handleDel('parent', { item })">删除</span>
</div>
</div>
<!-- 父项评论回复操作 -->
<div v-if="item.isShowInput" class="input-wrap" style="margin-top: 10px;">
<NConfigProvider inline-theme-disabled style="width:100%">
<NInput
v-model:value="item.word"
type="textarea"
:autosize="{ minRows: 1 }"
:placeholder="`回复: @${item.userName}`"
class="text"
@focus="item.isShowSend = true"
@blur="handleBlur(item)"
/>
</NConfigProvider>
<div class="send-btn" :class="{ active: publicWord }" @click="sendMessage('parent', item, index)">
<span v-if="item.isShowSend"></span>
</div>
</div>
</div>
</div>
<div class="child-wrap">
<div v-for="(ele, subIndex) in item.commentList" :key="subIndex" class="child-wrap-item">
<div class="left-img">
<img :src="ele.userAvatar">
</div>
<div class="right">
<div class="name">
{{ ele.userName }}
<!-- <div v-if="ele.userId === props.detailsInfo.userId" class="author">
作者
</div> -->
</div>
<div class="des">
<span v-if="ele.replyUserName">
<span>回复</span>
<span class="u-name">@{{ ele.replyUserName }}</span>
</span>
{{ ele.content }}
</div>
<div class="star-wrap">
<span class="time">{{ ele.createTime }}</span>
<div class="star-operator">
<div class="icon-wrap">
<ThumbsUp size="16" class="mr-1" :color="ele.isLike !== 0 ? '#ff0000' : '#000000'" @click="handleFocus('sub', { sub: ele, item })" />
<span style="vertical-align: middle">{{ ele.likeNum }}</span>
</div>
<span class="cursor-pointer m-r16" @click="handleMessage(ele)"></span>
<span v-if="item.userId === userStore?.userInfo?.userId" class="cursor-pointer" @click="handleDel('sub', { item, sub: ele })"></span>
</div>
</div>
<!-- 子项评论回复操作 -->
<div v-if="ele.isShowInput" class="input-wrap" style="margin-top: 10px;">
<NConfigProvider inline-theme-disabled style="width:100%">
<NInput
v-model:value="ele.word"
type="textarea"
:autosize="{ minRows: 1 }"
:placeholder="`回复: @${ele.userName}`"
class="text"
@focus="ele.isShowSend = true"
@blur="handleBlur(ele)"
/>
</NConfigProvider>
<div class="send-btn" :class="{ active: publicWord }" @click="sendMessage('sub', ele, index, subIndex)">
<span v-if="ele.isShowSend"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="no-more">
暂时没有更多评论
</div>
</div>
</template>
<style scoped lang="scss">
.header-wrap {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.left {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
border: 1px solid #2d28ff;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.right {
}
}
.input-wrap {
display: flex;
align-items: center;
line-height: 1.55;
border-radius: 8px;
caret-color: #999;
font-size: 14px;
background: #f2f5f9;
padding: 10px 12px;
flex: 1;
:deep(.n-input) {
--n-padding-left: 0 !important;
--n-border: 0 !important;
--n-border-hover: 0 !important;
--n-border-focus: 0 !important;
--n-box-shadow-focus: unset;
--n-padding-vertical: 0px !important;
&::hover {
border: 0;
}
&:active,
&:focus {
border: 0 !important;
}
background-color: transparent;
}
.send-btn {
margin-left: 16px;
margin-right: 16px;
flex-shrink: 0;
color: #999;
font-size: 14px;
cursor: pointer;
&.active {
color: #222;
}
}
}
.nav-wrap {
margin: 10px 0;
display: flex;
font-size: 14px;
// align-items: center;
.left-img {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.right {
width: 100%;
.name {
margin-top: 10px;
margin-bottom: 7px;
font-weight: 500;
color: #333;
}
.des {
line-height: 1.25;
}
.star-wrap {
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
.time {
color: #999;
font-size: 12px;
}
.star-operator {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
cursor: pointer;
.icon-wrap {
display: flex;
align-items: center;
margin-right: 16px;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 3px;
}
}
.m-r16 {
margin-right: 16px;
}
}
}
}
}
.child-wrap {
padding-left: 52px;
.child-wrap-item {
display: flex;
font-size: 14px;
margin-top: 16px;
// align-items: center;
.left-img {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 12px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.right {
width: 100%;
.name {
margin-bottom: 7px;
font-weight: 500;
color: #333;
display: flex;
align-items: center;
.author {
margin-left: 8px;
color: #fff;
background: linear-gradient(90deg, #2d28ff, #1a7dff);
padding: 3px 4px;
font-size: 12px;
line-height: 12px;
border-radius: 4px;
}
}
.des {
line-height: 1.5;
// display: flex;
align-items: center;
.u-name {
color: #1880ff;
margin-right: 6px;
}
}
.star-wrap {
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
.time {
color: #999;
font-size: 12px;
}
.star-operator {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
cursor: pointer;
.icon-wrap {
display: flex;
align-items: center;
margin-right: 16px;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 3px;
}
}
.m-r16 {
margin-right: 16px;
}
}
}
}
}
}
.no-more {
color: #999;
font-size: 14px;
text-align: center;
padding: 20px 0;
}
</style>

View File

@ -0,0 +1,186 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const route = useRoute()
// import { useRoute } from 'vue-router'
// const menuStore = useMenuStore()
const router = useRouter()
const userStore = useUserStore()
const modalStore = useModalStore()
// const route = useRoute()
// const currentPath = ref('/planet/')
const currentPath = ref(route.path)
if (route.path === '/planet') {
currentPath.value = '/planet/'
}
const searchQuery = ref('')
const tabList = ref([
{
name: '首页',
path: '/planet/',
},
{
name: '我的星球',
path: '/my-planet',
},
])
const showMessage = ref(false)
//
function handleLogin() {
modalStore.showLoginModal()
}
async function handleLogout() {
try {
await request.post('/logout')
userStore.logout()
navigateTo('/model-square')
}
catch (error) {
console.error('Logout failed:', error)
}
}
function handleTabClick(path: string) {
router.push(path)
currentPath.value = path
// menuStore.setActiveMenu(path)
// if (path === '/my-planet') {
// navigateTo('/my-planet')
// }
}
function openPersonalPage() {
const url = `${window.location.origin}${router.resolve('/myPlanetDetail').href}`
window.open(url, '_blank')
}
function openEarningsPage() {
const url = `${window.location.origin}${router.resolve('/planet-earnings').href}`
window.open(url, '_blank')
}
function handleShowMessage() {
showMessage.value = true
}
</script>
<template>
<header class="w-full h-16 bg-white border-b border-gray-100">
<div class="max-w-[1920px] mx-auto">
<div class="flex items-center h-16 px-10">
<!-- Logo部分 -->
<div class="flex items-center cursor-pointer" @click="handleTabClick('/')">
<img
src="@/assets/img/logo.png"
alt="魔创未来"
class="h-8 w-8"
>
<span class="text-[#328AFE] text-xl font-bold ml-2">魔创未来</span>
</div>
<!-- 导航链接 -->
<nav class="flex items-center ml-20">
<NuxtLink
v-for="(item, index) in tabList"
:key="index"
to="/"
class="text-xl font-bold px-5 transition-colors"
:class="{ 'text-[#192029]': currentPath === item.path, 'text-[#878D95]': currentPath !== item.path }"
@click="handleTabClick(item.path)"
>
{{ item.name }}
</NuxtLink>
<!-- <NuxtLink
to="/"
class="text-xl font-bold px-5 transition-colors text-[#878D95]"
>
魔创未来
</NuxtLink> -->
</nav>
<!-- 右侧功能区 -->
<div class="flex items-center ml-auto space-x-4">
<!-- 搜索框 -->
<div class="relative flex items-center w-[360px]">
<input
v-model="searchQuery"
type="text"
placeholder="搜索星球、用户或内容"
class="w-full h-10 pl-4 pr-16 bg-[#F4F6F9] rounded-lg text-sm placeholder-[#878D95] outline-none"
>
<button class="absolute right-3 flex items-center space-x-1 text-[#4A5563]">
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667zM14 14l-2.9-2.9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="text-sm font-bold">搜索</span>
</button>
</div>
<!-- 通知铃铛 -->
<button class="relative p-2 hover:bg-gray-50 rounded-lg" @click="handleShowMessage">
<svg class="w-6 h-6 text-[#4A5563]" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z" fill="currentColor" />
</svg>
</button>
<PlanetMessage v-if="showMessage" @close="showMessage = false" />
<!-- 登录按钮 -->
<client-only v-if="!userStore.token">
<button class="h-10 px-8 rounded-lg bg-gradient-to-l from-[#3BEDFF] to-[#328AFE] text-white text-sm font-bold hover:opacity-90 transition-opacity" @click="handleLogin">
登录
</button>
</client-only>
<div v-else class="relative group">
<NAvatar
class="cursor-pointer w-10 h-10"
round
size="small"
:src="userStore?.userInfo?.avatar"
/>
<div class="p-2 absolute hidden group-hover:block z-50 top-11 right-0 bg-white border border-[#E5E5E5] rounded-lg w-60">
<div class="flex items-center gap-2 bg-[rgba(50,138,254,0.05)] opacity-0.5 p-2 mb-2">
<div class="w-10 h-10">
<img class="w-full h-full rounded-full" :src="userStore?.userInfo?.avatar" alt="">
</div>
<div class="flex flex-col justify-between">
<div class="text-base font-bold">
{{ userStore.userInfo.nickName }}
</div>
<div class="text-xs text-[#878D95]">
欢迎来到魔创星球
</div>
</div>
</div>
<div class="flex justify-between text-sm text-[#4a5563]">
<div class="flex-1 text-center py-2 cursor-pointer" @click="openPersonalPage">
个人主页
</div>
<div class="flex-1 text-center py-2 cursor-pointer" @click="openEarningsPage">
收益明细
</div>
</div>
<div class="text-sm text-[#4a5563] flex justify-center items-center my-4 cursor-pointer" @click="handleLogout">
推出登录
</div>
</div>
</div>
</div>
</div>
</div>
</header>
</template>
<style scoped>
/* 如果需要自定义样式可以在这里添加 */
</style>

View File

@ -0,0 +1,216 @@
<script setup lang="ts">
import request from '@/utils/request'
import { Bell, MessageCircle, ShieldQuestion, ThumbsUp, X } from 'lucide-vue-next'
import { ref } from 'vue'
interface MessageItem {
id: number
name: string
type: number
icon: any
}
const emit = defineEmits(['close'])
const messageList = ref<MessageItem[]>([
{
id: 1,
name: '星球通知',
type: 0,
icon: Bell,
},
// {
// id: 2,
// name: '',
// type: 2,
// icon: ShieldQuestion,
// },
{
id: 3,
name: '回复我的',
type: 1,
icon: MessageCircle,
},
{
id: 4,
name: '收藏/赞',
type: 3,
icon: ThumbsUp,
},
])
const messageData = ref([])
const loading = ref(false)
function handleClose() {
emit('close')
}
//
const params = ref({
adviceType: 0,
pageNum: 1,
pageSize: 10,
})
async function changeType(type: number) {
params.value.adviceType = type
params.value.pageNum = 1
initGetList()
}
// async function getMessageList() {
// loading.value = true
// try {
// const res = await request.post('/communityAdvice/adviceList', params.value)
// if (res.code === 200) {
// messageData.value = res.rows || []
// }
// }
// catch (error) {
// console.error(error)
// }
// finally {
// loading.value = false
// }
// }
//
const listFinish = ref(false)
function initGetList() {
listFinish.value = false
params.value.pageNum = 1
getMessageList()
}
async function getMessageList() {
try {
if (listFinish.value || loading.value) // loading,
return
loading.value = params.value.pageNum === 1 // loading
const res = await request.post('/communityAdvice/adviceList', params.value)
if (res.code === 200) {
if (params.value.pageNum === 1) {
messageData.value = res.rows
}
else {
messageData.value = [...messageData.value, ...res.rows]
}
if (messageData.value.length >= res.total) {
listFinish.value = true
}
params.value.pageNum++
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
getMessageList()
//
onMounted(() => {
// getMessageList(1)
})
</script>
<template>
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white w-[800px] rounded-lg overflow-hidden">
<!-- 标题栏 -->
<div class="flex items-center justify-between px-6 h-14 border-b border-[#eee]">
<div class="text-lg font-medium">
通知
</div>
<div
class="i-lucide:x w-5 h-5 text-[#86909c] cursor-pointer hover:text-[#1e80ff]"
@click="handleClose"
>
<X />
</div>
</div>
<!-- 内容区 -->
<div class="flex h-[600px]">
<!-- 左侧菜单 -->
<div class="w-[200px] border-r border-[#eee] py-4">
<div
v-for="item in messageList"
:key="item.id"
class="flex items-center gap-2 px-6 h-12 cursor-pointer my-4 hover:bg-[#f2f3f5]"
:class="{ 'text-[#1e80ff] bg-[#f2f3f5]': params.adviceType === item.type }"
@click="changeType(item.type)"
>
<component :is="item.icon" class="w-5 h-5" />
<span>{{ item.name }}</span>
</div>
</div>
<!-- 右侧消息列表 -->
<div class="flex-1 overflow-y-auto">
<n-spin :show="loading">
<template v-if="messageData.length > 0">
<n-infinite-scroll style="height: calc(100vh - 200px)" :distance="10" @load="getMessageList">
<div
v-for="item in messageData"
:key="item.id"
class="flex items-center gap-3 p-4 border-b border-[#eee] hover:bg-[#f7f8fa]"
>
<img :src="item.sendUserAvatar" class="w-12 h-12 rounded-full">
<div v-if="params.adviceType === 0" class="flex-1">
<div class="overflow-hidden">
<p class="line-clamp-1 text-[#1f2329]">
{{ item.content }}
</p>
</div>
<div class="text-gray-500 text-sm mt-2">
{{ item.createTime }}
</div>
</div>
<div v-if="params.adviceType === 1" class="flex-1 flex flex-col justify-between">
<div>
{{ item.title }}
</div>
<div class="text-[#1e80ff]">
{{ item.content }}
</div>
<div class="text-[#86909c] text-xs">
{{ item.createTime }}
</div>
</div>
<div v-if="params.adviceType === 3" class="flex-1">
<div class="overflow-hidden">
<p class="line-clamp-1 text-[#1f2329]">
{{ item.content }}
</p>
</div>
<div class="text-gray-500 text-sm mt-2">
{{ item.createTime }}
</div>
</div>
</div>
</n-infinite-scroll>
</template>
<template v-else>
<div class="flex items-center justify-center min-h-[200px] text-[#86909c]">
暂无消息
</div>
</template>
</n-spin>
</div>
</div>
</div>
</div>
</template>
<style scoped>
:deep(.n-spin-container) {
height: 100%;
}
</style>

View File

@ -0,0 +1,704 @@
<script setup lang="ts">
import {
ThumbsUp,
} from 'lucide-vue-next'
import { watch } from 'vue'
interface LikeParams {
id: string | number
communityId?: string | number
tenantId?: string | number
publishId?: string | number
}
const props = defineProps({
height: {
type: Number,
default: 800,
},
type: {
type: String,
default: '',
},
publishListParams: {
type: Object,
default: () => ({}),
},
})
// const $props = defineProps(['headUrl', 'dataList', 'height'])
const userStore = useUserStore()
const message = useMessage()
const showQuestionModal = ref(false)
const questionItem = ref({})
function handleShowQuestion(item: any) {
questionItem.value = item
showQuestionModal.value = true
}
const questionContent = ref('')
//
const commentParams = ref({
content: '',
parentId: '',
replyUserId: '',
userId: userStore?.userInfo?.userId,
workFlowId: props.publishListParams.id,
})
function commentParamsInit() {
commentParams.value.content = ''
commentParams.value.replyUserId = ''
commentParams.value.parentId = ''
}
const isShowSend = ref(false)
const publicWord = ref('')
//
const commentList = ref([])
async function getCommentList() {
try {
const res = await request.post(`/question/list`, props.publishListParams)
if (res.code === 200) {
// for (let i = 0; i < res.rows.length; i++) {
// res.rows[i].isShowInput = false
// res.rows[i].isShowSend = false
// if (res.rows[i].contentList && res.rows[i].contentList.length > 0) {
// for (let j = 0; j < res.rows[i].contentList.length; j++) {
// res.rows[i].contentList[j].isShowInput = false
// res.rows[i].contentList[j].isShowSend = false
// }
// }
// }
commentList.value = res.rows
}
}
catch (error) {
console.error(error)
}
// try {
// let url = ''
// if (props.type === 'workflow') {
// url = `${urlList.value[props.type]}commentId=${props.detailsInfo.id}&sortType=${sortType.value}`
// }
// else if (props.type === 'pictrue') {
// url = `${urlList.value[props.type]}imageId=${props.detailsInfo.id}&sortType=${sortType.value}`
// }
// else {
// url = `${urlList.value[props.type]}modelId=${props.detailsInfo.id}&sortType=${sortType.value}`
// }
// const res = await request.get(url)
// for (let i = 0; i < res.data.length; i++) {
// res.data[i].isShowInput = false
// res.data[i].isShowSend = false
// if (res.data[i].contentList.length > 0) {
// for (let j = 0; j < res.data[i].contentList.length; j++) {
// res.data[i].contentList[j].isShowInput = false
// res.data[i].contentList[j].isShowSend = false
// }
// }
// }
// commentList.value = res.data
// }
// catch (error) {
// console.log(error)
// }
}
getCommentList()
// publishListParams
watch(
() => props.publishListParams,
() => {
if (props.publishListParams.type !== 999) {
getCommentList()
}
},
{ deep: true, immediate: true },
)
function handleBlur(ele) {
//
if (!ele) {
isShowSend.value = !!publicWord.value
}
else {
ele.isShowSend = !!ele.word
}
}
//
async function sendMessage(type: string, ele: any, index: number, subIndex?: number) {
if (ele && ele.userId) {
try {
if (ele.word) {
commentParams.value.content = ele.word
commentParams.value.publishId = commentList.value[index].id
if (type === 'sub') {
commentParams.value.parentId = commentList.value[index].commentList[subIndex].id
}
else {
commentParams.value.parentId = commentList.value[index].id
}
commentParams.value.communityId = props.publishListParams.communityId
commentParams.value.tenantId = props.publishListParams.tenantId
const res = await request.post(`/publishComment/save`, commentParams.value)
if (res.code === 200) {
ele.word = ''
message.success('评论成功!')
getCommentList()
// getCommentNum()
commentParamsInit()
}
}
else {
message.warning('评论不能为空!')
}
}
catch (error) {
console.log(error)
}
}
}
//
// async function getCommentNum() {
// if (props.type !== 'pictrue') {
// try {
// const res = await request.get(`${commentNumUrl.value[props.type]}=${props.detailsInfo.id}`)
// if (res.code === 200) {
// commentCount.value = res.data
// }
// }
// catch (error) {
// console.log(error)
// }
// }
// }
// getCommentNum()
// /
async function handleFocus(type: string, item: any) {
const params: LikeParams = { id: '' }
if (type === 'parent') {
params.id = item.item.id
}
else {
const { id, communityId, tenantId } = item.sub
params.id = id
params.communityId = communityId
params.tenantId = tenantId
params.publishId = item.item.id
}
try {
const url = type === 'sub' ? '/publishComment/like' : '/PublishLike/like'
const res = await request.post(url, params)
if (res.code === 200) {
// message.success('')
getCommentList()
}
}
catch (error) {
console.log(error)
}
// const { id, communityId, tenantId, operatorId } = item
// const res = await request.post(`PublishLike/like`, { commentId: id, communityId, tenantId, operatorId })
// if (res.code === 200) {
// if (item.isLike === 0) {
// item.isLike = 1
// item.likeNum++
// message.success('')
// }
// else {
// item.isLike = 0
// item.likeNum--
// message.success('')
// }
// }
}
//
function handleMessage(item: any) {
if (!item.isShowInput) {
item.word = ''
}
item.isShowInput = !item.isShowInput
}
//
async function handleDel(type: string, item: any) {
const params = {}
if (type === 'parent') {
params.publishId = item.item.id
params.communityId = item.item.communityId
}
else {
const { id, communityId, tenantId } = item.sub
params.id = id
params.communityId = communityId
params.tenantId = tenantId
params.publishId = item.item.id
}
try {
const url = type === 'sub' ? '/publishComment/delete' : '/publish/remove'
const res = await request.post(url, params)
if (res.code === 200) {
message.success('删除成功!')
getCommentList()
}
}
catch (error) {
console.log(error)
}
}
//
async function handlePublish() {
const params = {
content: questionContent.value,
questionId: questionItem.value.id,
communityId: questionItem.value.communityId,
tenantId: questionItem.value.tenantId,
}
try {
const res = await request.post('questionComment/comment', params)
if (res.code === 200) {
message.success('回答成功!')
getCommentList()
showQuestionModal.value = false
questionContent.value = ''
}
}
catch (error) {
console.log(error)
}
}
</script>
<template>
<div class="bg-white py-4 rounded min-h-[320px]">
<!-- <div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<div class="left text-[20px] mr-2">
讨论
</div>
<div v-if="props.type !== 'pictrue'" class="text-[#999]">
{{ commentCount }}
</div>
</div>
<div v-if="props.type !== 'pictrue'" class="flex items-center">
<div class="cursor-pointer" :class="sortType === 0 ? '' : 'text-[#999]'" @click="changeType(0)">
最热
</div>
<div class="cursor-pointer" :class="sortType === 1 ? '' : 'text-[#999]'" @click="changeType(1)">
最新
</div>
</div>
</div> -->
<!-- <div class="header-wrap">
<div v-if="userStore?.userInfo?.avatar" class="left">
<img
alt="avatar"
:src="userStore?.userInfo?.avatar"
>
</div>
<div class="input-wrap">
<NConfigProvider inline-theme-disabled style="width:100%">
<NInput
v-model:value="publicWord"
type="textarea"
:autosize="{ minRows: 1 }"
placeholder="善语结善缘,恶言伤人心~"
class="text"
@focus="isShowSend = true"
@blur="handleBlur"
/>
</NConfigProvider>
<div class="send-btn" :class="{ active: publicWord }" @click="sendMessage">
<span v-if="isShowSend"></span>
</div>
</div>
</div> -->
<div
v-for="(item, index) in commentList"
:key="index"
class="px-4"
>
<div class="nav-wrap">
<div class="left-img">
<img :src="item.questionUserAvatar">
</div>
<div class="right">
<div class="name">
{{ item.questionUserName }}
</div>
<div class="des">
{{ item.content }}
</div>
<div class="star-wrap">
<span class="time">{{ item.createTime }}</span>
<!-- <div class="star-operator">
<div class="icon-wrap">
<ThumbsUp size="16" class="mr-1" :color="item.isLike !== 0 ? '#ff0000' : '#000000'" @click="handleFocus('parent', { item })" />
<span style="vertical-align: middle">{{ item.likeNum }}</span>
</div>
<span class="cursor-pointer m-r16" @click="handleMessage(item)"></span>
<span v-if="item.userId === userStore?.userInfo?.userId" class="cursor-pointer" @click="handleDel('parent', { item })"></span>
</div> -->
</div>
<div v-if="item.questionUrl" class="flex flex-wrap gap-2">
<img v-for="(ele, index) in item.questionUrl.split(',')" :key="index" class="w-16 h-16 rounded-md" :src="ele">
</div>
<!-- 父项评论回复操作 -->
<!-- <div v-if="item.isShowInput" class="input-wrap" style="margin-top: 10px;">
<NConfigProvider inline-theme-disabled style="width:100%">
<NInput
v-model:value="item.word"
type="textarea"
:autosize="{ minRows: 1 }"
:placeholder="`回复: @${item.userName}`"
class="text"
@focus="item.isShowSend = true"
@blur="handleBlur(item)"
/>
</NConfigProvider>
<div class="send-btn" :class="{ active: publicWord }" @click="sendMessage('parent', item, index)">
<span v-if="item.isShowSend"></span>
</div>
</div> -->
</div>
</div>
<div class="child-wrap">
<div v-for="(ele, subIndex) in item.commentList" :key="subIndex" class="child-wrap-item">
<div class="left-img">
<img :src="ele.avatar">
</div>
<div class="right">
<div class="name">
{{ ele.userName }}
<!-- <div v-if="ele.userId === props.detailsInfo.userId" class="author">
作者
</div> -->
</div>
<div class="des">
<!-- <span v-if="ele.replyUserName">
<span>回复</span>
<span class="u-name">@{{ ele.replyUserName }}</span>
</span> -->
{{ ele.content }}
</div>
<div class="star-wrap">
<span class="time">{{ ele.createTime }}</span>
<!-- <div class="star-operator">
<div class="icon-wrap">
<ThumbsUp size="16" class="mr-1" :color="ele.isLike !== 0 ? '#ff0000' : '#000000'" @click="handleFocus('sub', { sub: ele, item })" />
<span style="vertical-align: middle">{{ ele.likeNum }}</span>
</div>
<span class="cursor-pointer m-r16" @click="handleMessage(ele)"></span>
<span v-if="item.userId === userStore?.userInfo?.userId" class="cursor-pointer" @click="handleDel('sub', { item, sub: ele })"></span>
</div> -->
</div>
<!-- 子项评论回复操作 -->
<!-- <div v-if="ele.isShowInput" class="input-wrap" style="margin-top: 10px;">
<NConfigProvider inline-theme-disabled style="width:100%">
<NInput
v-model:value="ele.word"
type="textarea"
:autosize="{ minRows: 1 }"
:placeholder="`回复: @${ele.userName}`"
class="text"
@focus="ele.isShowSend = true"
@blur="handleBlur(ele)"
/>
</NConfigProvider>
<div class="send-btn" :class="{ active: publicWord }" @click="sendMessage('sub', ele, index, subIndex)">
<span v-if="ele.isShowSend"></span>
</div>
</div> -->
</div>
</div>
</div>
<div class="flex justify-center items-center text-sm my-4">
<div v-if="item.amount" class="cursor-pointer" @click="handleShowQuestion(item)">
<span class="text-blue-500">回答该问题可获得</span>
<span class="text-[#ffa820]">{{ item.amount }}积分</span>
</div>
<div v-else class="cursor-pointer" @click="handleShowQuestion(item)">
<span class="text-blue-500">回答该问题</span>
</div>
</div>
</div>
<div class="no-more">
暂时没有更多评论
</div>
<n-modal
v-model:show="showQuestionModal"
:style="{ width: '640px' }"
preset="dialog"
transform-origin="center"
class="custom-modal"
:show-icon="false"
>
<div class="bg-white rounded-lg px-6">
<div class="flex items-center justify-between pb-4">
<div class="flex items-center">
<div class="flex items-center">
回答<img class="w-4 h-4 rounded-full mx-2" :src="questionItem.questionUserAvatar">
</div>
<span>{{ questionItem.questionUserName }}</span>
<span>发起的咨询</span>
</div>
</div>
<n-form
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<n-form-item path="content" :show-label="false">
<n-input
v-model:value="questionContent"
type="textarea"
placeholder="请输入提问内容"
:required="true"
:autosize="{ minRows: 5, maxRows: 10 }"
class="rounded-lg"
/>
</n-form-item>
<div class="flex items-center justify-between my-4">
<div class="flex items-center justify-end gap-8 w-full">
<n-button
type="primary"
class="!px-8 rounded"
:theme-overrides="{
common: {
primaryColor: '#3f7ef7',
primaryColorHover: '#3f7ef7',
},
}"
@click="handlePublish"
>
回答
</n-button>
</div>
</div>
</n-form>
</div>
</n-modal>
</div>
</template>
<style scoped lang="scss">
.header-wrap {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.left {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
border: 1px solid #2d28ff;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.right {
}
}
.input-wrap {
display: flex;
align-items: center;
line-height: 1.55;
border-radius: 8px;
caret-color: #999;
font-size: 14px;
background: #f2f5f9;
padding: 10px 12px;
flex: 1;
:deep(.n-input) {
--n-padding-left: 0 !important;
--n-border: 0 !important;
--n-border-hover: 0 !important;
--n-border-focus: 0 !important;
--n-box-shadow-focus: unset;
--n-padding-vertical: 0px !important;
&::hover {
border: 0;
}
&:active,
&:focus {
border: 0 !important;
}
background-color: transparent;
}
.send-btn {
margin-left: 16px;
margin-right: 16px;
flex-shrink: 0;
color: #999;
font-size: 14px;
cursor: pointer;
&.active {
color: #222;
}
}
}
.nav-wrap {
margin: 10px 0;
display: flex;
font-size: 14px;
// align-items: center;
.left-img {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.right {
width: 100%;
.name {
// margin-top: 10px;
margin-bottom: 7px;
font-weight: 500;
color: #333;
}
.des {
line-height: 1.25;
}
.star-wrap {
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
.time {
color: #999;
font-size: 12px;
}
.star-operator {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
cursor: pointer;
.icon-wrap {
display: flex;
align-items: center;
margin-right: 16px;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 3px;
}
}
.m-r16 {
margin-right: 16px;
}
}
}
}
}
.child-wrap {
padding-left: 52px;
.child-wrap-item {
display: flex;
font-size: 14px;
margin-top: 16px;
// align-items: center;
.left-img {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 12px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.right {
width: 100%;
.name {
margin-bottom: 7px;
font-weight: 500;
color: #333;
display: flex;
align-items: center;
.author {
margin-left: 8px;
color: #fff;
background: linear-gradient(90deg, #2d28ff, #1a7dff);
padding: 3px 4px;
font-size: 12px;
line-height: 12px;
border-radius: 4px;
}
}
.des {
line-height: 1.5;
// display: flex;
align-items: center;
.u-name {
color: #1880ff;
margin-right: 6px;
}
}
.star-wrap {
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
.time {
color: #999;
font-size: 12px;
}
.star-operator {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
cursor: pointer;
.icon-wrap {
display: flex;
align-items: center;
margin-right: 16px;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 3px;
}
}
.m-r16 {
margin-right: 16px;
}
}
}
}
}
}
.no-more {
color: #999;
font-size: 14px;
text-align: center;
padding: 20px 0;
}
</style>

View File

@ -0,0 +1,217 @@
<script setup lang="ts">
import { CloseOutline } from '@vicons/ionicons5'
import { FolderPlus, Image, ImagePlus } from 'lucide-vue-next'
import { useMessage } from 'naive-ui'
import { computed, ref } from 'vue'
import { uploadImagesInBatches } from '../utils/uploadImg.ts'
const props = defineProps<Props>()
const emit = defineEmits(['success'])
const userStore = useUserStore()
interface Props {
communityId: number | string
tenantId: number | string
}
const message = useMessage()
//
const formData = ref({
content: '',
fileUrl: '',
fileName: '',
imageUrl: '',
})
//
const fileUrl = ref<string[]>([])
const fileName = ref<string[]>([])
const imageUrl = ref<string[]>([])
const loading = ref(false)
// ref
const formRef = ref()
//
async function handlePublish() {
try {
await formRef.value?.validate()
loading.value = true
const params = {
...formData.value,
communityId: props.communityId,
tenantId: props.tenantId,
fileUrl: fileUrl.value.join(','),
fileName: fileName.value.join(','),
imageUrl: imageUrl.value.join(','),
}
const res = await request.post('/publish/publish', params)
if (res.code === 200) {
message.success('发布成功')
//
formData.value = {
content: '',
fileUrl: '',
fileName: '',
imageUrl: '',
}
fileUrl.value = []
fileName.value = []
imageUrl.value = [] // URL
emit('success') //
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
//
const currentUploadType = ref<string | null>(null)
const acceptTypes = computed(() => {
return currentUploadType.value === 'img'
? 'image/*'
: 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,text/plain,application/zip'
})
// input
const pictureInput = ref<HTMLInputElement | null>(null)
function handlePictureInput(type: string) {
currentUploadType.value = type
nextTick(() => {
pictureInput.value?.click()
})
}
//
async function handlePictureChange(event: Event) {
const files = (event.target as HTMLInputElement).files
if (!files || files.length === 0)
return message.error('请选择有效的文件')
try {
const fileList = Array.from(files)
const isImageUpload = currentUploadType.value === 'img'
//
const validFiles = fileList.filter(file =>
isImageUpload ? file.type.startsWith('image/') : !file.type.startsWith('image/'),
)
if (validFiles.length === 0)
return message.error(`请选择${isImageUpload ? '图片' : '文档'}文件`)
const uploadResults = await uploadImagesInBatches(validFiles)
//
if (isImageUpload) {
imageUrl.value.push(...uploadResults.map((item: any) => item.url))
}
else {
fileUrl.value.push(...uploadResults.map((item: any) => item.url))
fileName.value.push(...uploadResults.map((item: any) => item.fileName))
}
pictureInput.value!.value = '' // input
}
catch (error) {
message.error(currentUploadType.value === 'img' ? '图片上传失败' : '文件上传失败')
}
}
</script>
<template>
<div class="bg-white rounded-lg px-6">
<n-form
ref="formRef"
:model="formData"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<n-form-item path="content" :show-label="false">
<n-input
v-model:value="formData.content"
type="textarea"
placeholder="点击发表主题"
:autosize="{ minRows: 5, maxRows: 10 }"
class="rounded-lg"
/>
</n-form-item>
<div v-if="fileName.length > 0" class="mt-4 flex flex-wrap gap-3 mb-2">
<div
v-for="(item, index) in fileName"
:key="index"
class="flex items-center gap-2"
>
{{ item }}
<n-icon color="#3f7ef7" class="cursor-pointer flex items-center justify-center w-6 h-6">
<CloseOutline class="text-[20px]" size="20" @click="fileName.splice(index, 1)" />
</n-icon>
</div>
</div>
<!-- 上传的图片预览 -->
<div v-if="imageUrl.length > 0" class="mt-4 flex flex-wrap gap-3 mb-2">
<div
v-for="(item, index) in imageUrl"
:key="index"
class="relative group"
>
<img
:src="item"
class="w-[125px] h-[125px] rounded"
>
<n-icon color="#fff" class="absolute top-1 right-1 p-1 z-40 cursor-pointer w-6 h-6">
<CloseOutline class="text-[20px]" size="20" @click="questionUrlList.splice(index, 1)" />
</n-icon>
</div>
</div>
<!-- <n-form-item v-if="formData.type === 1" path="amount" class="mb-3"> -->
<!-- </n-form-item> -->
<div class="flex items-center justify-between my-2">
<div class="flex">
<div class="border border-gray-300 rounded p-1 cursor-pointer hover:bg-gray-50 transition-colors mr-6" @click="handlePictureInput('img')">
<ImagePlus class="w-5 h-5" />
</div>
<div class="border border-gray-300 rounded p-1 cursor-pointer hover:bg-gray-50 transition-colors" @click="handlePictureInput('file')">
<FolderPlus class="w-5 h-5" />
</div>
</div>
<div class="flex items-center gap-8">
<n-button
type="primary"
:loading="loading"
class="!px-8 rounded"
:theme-overrides="{
common: {
primaryColor: '#3f7ef7',
primaryColorHover: '#3f7ef7',
},
}"
@click="handlePublish"
>
发布
</n-button>
</div>
</div>
</n-form>
<input
ref="pictureInput"
type="file"
:accept="acceptTypes"
class="hidden"
multiple="true"
@change="handlePictureChange"
>
</div>
</template>

View File

@ -0,0 +1,212 @@
<script setup lang="ts">
import { CloseOutline } from '@vicons/ionicons5'
import { Image } from 'lucide-vue-next'
import { useMessage } from 'naive-ui'
import { ref } from 'vue'
import { uploadImagesInBatches } from '../utils/uploadImg.ts'
const props = defineProps<Props>()
const emit = defineEmits(['success'])
const userStore = useUserStore()
interface Props {
communityId: number | string
tenantId: number | string
}
const message = useMessage()
//
const formData = ref({
type: 0, // 01
amount: 10, //
content: '', //
isAnonymous: 0, // 1 0
questionUrl: '', //
})
//
const fileList = ref<string[]>([])
const questionUrlList = ref<string[]>([])
const loading = ref(false)
//
const rules = {
content: {
required: true,
message: '请输入提问内容',
// trigger: ['blur'],
},
amount: {
required: (formData: any) => formData.type === 1,
message: '请输入付费金额',
// trigger: ['blur', 'change'],
type: 'number',
},
}
// ref
const formRef = ref()
//
async function handlePublish() {
try {
await formRef.value?.validate()
loading.value = true
const params = {
...formData.value,
communityId: props.communityId,
tenantId: props.tenantId,
questionUserId: userStore.userInfo.userId,
questionUrl: questionUrlList.value.join(','), // URL
}
const res = await request.post('/question/addQuestion', params)
if (res.code === 200) {
message.success('提问成功')
//
formData.value = {
type: 0,
amount: undefined,
content: '',
isAnonymous: 0,
questionUrl: '',
}
fileList.value = []
questionUrlList.value = [] // URL
emit('success') //
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
const pictureInput = ref<HTMLInputElement | null>(null)
function handlePictureInput() {
pictureInput.value?.click()
}
//
async function handlePictureChange(event: Event) {
const files = (event.target as HTMLInputElement).files
if (!files || files.length === 0)
return
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'))
if (imageFiles.length === 0) {
message.error('请选择有效的图片文件')
return
}
try {
const pictureResultList = await uploadImagesInBatches(imageFiles)
questionUrlList.value.push(...pictureResultList.map((item: any) => item.url))
pictureInput.value!.value = ''
}
catch (error: any) {
message.error('图片上传失败')
}
}
</script>
<template>
<div class="bg-white rounded-lg px-6">
<n-form
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
>
<n-form-item path="content" :show-label="false">
<n-input
v-model:value="formData.content"
type="textarea"
placeholder="请输入提问内容"
:autosize="{ minRows: 5, maxRows: 10 }"
class="rounded-lg"
/>
</n-form-item>
<!-- 上传的图片预览 -->
<div v-if="questionUrlList.length > 0" class="mt-4 flex flex-wrap gap-3 mb-2">
<div
v-for="(url, index) in questionUrlList"
:key="index"
class="relative group"
>
<img
:src="url"
class="w-[125px] h-[125px] rounded"
>
<n-icon color="#fff" class="absolute top-1 right-1 p-1 z-40 cursor-pointer w-6 h-6">
<CloseOutline class="text-[20px]" size="20" @click="questionUrlList.splice(index, 1)" />
</n-icon>
</div>
</div>
<!-- <n-form-item v-if="formData.type === 1" path="amount" class="mb-3"> -->
<n-input-number
v-if="formData.type === 1"
v-model:value="formData.amount"
:min="10"
:max="999.9"
:precision="1"
placeholder="请输入付费金额"
class="w-[240px] my-5"
>
<template #prefix>
¥
</template>
</n-input-number>
<!-- </n-form-item> -->
<div class="flex items-center justify-between my-4">
<div class="border border-gray-300 rounded p-1 cursor-pointer hover:bg-gray-50 transition-colors" @click="handlePictureInput">
<Image class="w-6 h-6 size-6" />
</div>
<div class="flex items-center gap-8">
<n-radio-group v-model:value="formData.type">
<n-radio :value="0">
免费
</n-radio>
<n-radio :value="1">
付费
</n-radio>
</n-radio-group>
<n-checkbox v-model:checked="formData.isAnonymous" unchecked-value="0" checked-value="1">
匿名提问
</n-checkbox>
<n-button
type="primary"
:loading="loading"
class="!px-8 rounded"
:theme-overrides="{
common: {
primaryColor: '#3f7ef7',
primaryColorHover: '#3f7ef7',
},
}"
@click="handlePublish"
>
提问
</n-button>
</div>
</div>
</n-form>
<input
ref="pictureInput"
type="file"
accept="image/*"
class="hidden"
@change="handlePictureChange"
>
</div>
</template>

View File

@ -3,8 +3,8 @@
// import { getUUid, uuidLogin } from '@api/login'
// import { useStore } from '@store/index'
// import { IosRefresh } from '@vicons/ionicons5';
import { RotateCcw } from 'lucide-vue-next';
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { RotateCcw } from 'lucide-vue-next'
import { onBeforeUnmount, onMounted, ref } from 'vue'
//
const emit = defineEmits<{
(event: 'login-success', data: any): void
@ -50,14 +50,18 @@ async function onGetUUid() {
if (res.status === 1) {
clearTimeout(pollingTimer)
const userStore = useUserStore()
userStore.setToken(res.token)
const res1 = await request.get<ApiResponse<UserToken>>('/getInfo', {
token: res.token,
})
userStore.setUserInfo(res1.user)
await userStore.setToken(res.token)
await userStore.getUserInfo()
// const res1 = await request.get<ApiResponse<UserToken>>('/getInfo', {
// token: res.token,
// })
// const res1 = await request.get('/system/user/selectUserById', {
// token: token.value,
// })
// userStore.setUserInfo(res1.user)
// window.location.href = '/'
//
window.location.reload();
window.location.reload()
}
}).catch(() => {
clearTimeout(pollingTimer)
@ -87,7 +91,6 @@ onBeforeUnmount(() => {
<template>
<div class="w-[280px] px-5 text-center relative">
<div v-if="qrUrl" class="relative w-full">
<div v-if="bindTimeout" class="absolute py-3 left-0 w-full h-full top-0 z-[9999] flex items-center justify-center flex-col cursor-pointer text-white bg-black/40" @click="onGetUUid">
刷新二维码

View File

@ -1,3 +1,124 @@
<script setup lang="ts">
import { Download, Play } from 'lucide-vue-next'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
// import { useRouter } from 'vue-router'
const props = defineProps({
params: {
type: Object,
default: () => {},
},
})
const emit = defineEmits(['workFlowTotal'])
const modalStore = useModalStore()
// const router = useRouter()
const loading = ref(false)
const finished = ref(false)
const total = ref(0) //
const loadingTrigger = ref(null)
const observer = ref<IntersectionObserver | null>(null)
const listParams = ref({
...props.params,
pageNumber: 1,
pageSize: 20,
name: modalStore.searchQuery,
isCollect: 1,
})
function initPageNUm() {
listParams.value.pageNumber = 1
finished.value = false //
listParams.value = Object.assign({}, listParams.value, props.params)
getDataList()
}
const dataList = ref([])
async function getDataList() {
if (loading.value || finished.value)
return
loading.value = true
try {
const res = await request.post('/WorkFlow/workFlowList', { ...listParams.value })
if (res.code === 200) {
//
if (listParams.value.pageNumber === 1) {
dataList.value = res.data.list
}
else {
dataList.value = [...dataList.value, ...res.data.list]
}
total.value = res.data.total //
//
if (dataList.value.length >= total.value) {
finished.value = true
}
//
listParams.value.pageNumber++
emit('workFlowTotal', total.value)
}
}
catch (err) {
dataList.value = []
finished.value = true
console.log(err)
}
finally {
loading.value = false
}
}
getDataList()
//
function toDetail(item: any) {
// router.push(`/workflow-details/${item.id}`)
const baseUrl = window.location.origin
window.open(`${baseUrl}/workflow-details/${item.id}`, '_blank', 'noopener,noreferrer')
}
onMounted(() => {
window.addEventListener('scroll', topedRefresh)
observer.value = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loading.value && !finished.value) {
getDataList()
}
},
{
threshold: 0.1,
},
)
if (loadingTrigger.value) {
observer.value.observe(loadingTrigger.value)
}
})
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()
}
defineExpose({
initPageNUm,
})
</script>
<template>
<div class="flex flex-wrap w-full">
<div class="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-7 gap-4 p-4">
@ -12,14 +133,14 @@
:src="item.coverPath"
class="w-full h-full object-cover rounded-lg cursor-pointer ransform transition-transform duration-300 hover:scale-110"
alt=""
/>
>
<!-- 左上角标签 -->
<div
<!-- <div
class="absolute top-2 left-2 bg-black/50 text-white text-xs px-2 py-1 rounded"
>
{{ item.type }}
</div>
</div> -->
<!-- 底部数据统计 -->
<div
@ -47,143 +168,27 @@
</span>
</div>
<div class="flex mt-1">
<img :src="item.avatar" class="w-4 h-4 rounded-full mr-2" alt="" />
<img :src="item.avatar" class="w-4 h-4 rounded-full mr-2" alt="">
<span class="text-xs text-gray-500 truncate">{{ item.nickName }}</span>
</div>
</div>
</div>
</div>
<div class="w-full flex justify-center" v-if="!loading && dataList.length === 0">
<Empty></Empty>
<div v-if="!loading && dataList.length === 0" class="w-full flex justify-center">
<Empty />
</div>
<div
ref="loadingTrigger"
class="h-20 w-full text-gray-500 flex justify-center items-center"
>
<div v-if="loading">...</div>
<div v-if="finished && dataList.length >= 20"></div>
<div v-if="loading">
加载中...
</div>
<div v-if="finished && dataList.length >= 20">
没有更多数据了
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Download, Play } from "lucide-vue-next";
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { useRouter } from 'vue-router';
const emit = defineEmits(['workFlowTotal'])
const modalStore = useModalStore()
const router = useRouter()
const loading = ref(false);
const finished = ref(false);
const total = ref(0); //
const loadingTrigger = ref(null);
const observer = ref<IntersectionObserver | null>(null);
const props = defineProps({
params: {
type: Object,
default: () => {},
},
});
const listParams = ref({
...props.params,
pageNumber: 1,
pageSize: 20,
name:modalStore.searchQuery,
isCollect:1,
});
function initPageNUm() {
listParams.value.pageNumber = 1;
finished.value = false; //
listParams.value = Object.assign({}, listParams.value, props.params);
getDataList();
}
const dataList = ref([]);
async function getDataList() {
if (loading.value || finished.value) return;
loading.value = true;
try {
const res = await request.post("/WorkFlow/workFlowList", { ...listParams.value });
if (res.code === 200) {
//
if (listParams.value.pageNumber === 1) {
dataList.value = res.data.list;
} else {
dataList.value = [...dataList.value, ...res.data.list];
}
total.value = res.data.total; //
//
if (dataList.value.length >= total.value) {
finished.value = true;
}
//
listParams.value.pageNumber++;
emit('workFlowTotal', total.value)
}
} catch (err) {
dataList.value = [];
finished.value = true;
console.log(err);
} finally {
loading.value = false;
}
}
getDataList();
//
function toDetail(item:any){
// router.push(`/workflow-details/${item.id}`)
const baseUrl = window.location.origin
window.open(`${baseUrl}/workflow-details/${item.id}`, '_blank', 'noopener,noreferrer')
}
onMounted(() => {
window.addEventListener("scroll", topedRefresh);
observer.value = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loading.value && !finished.value) {
getDataList();
}
},
{
threshold: 0.1,
}
);
if (loadingTrigger.value) {
observer.value.observe(loadingTrigger.value);
}
});
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();
}
defineExpose({
initPageNUm,
});
</script>
<style scoped></style>

View File

@ -1,49 +1,9 @@
<template>
<div class="upload-container">
<div class="fileUpload">
<div class="item-upload">
<!-- <span v-if="required" class="required">*</span> -->
<!-- <n-button type="success" @click="triggerFileSelect"> </n-button> -->
<input
type="file"
ref="fileInputRef"
class="file2"
style="display: none"
@change="selectFile"
/>
</div>
<!-- <div v-if="showFileName" class="item-info">
{{ fileName }}
</div>
<div v-else class="item-info">支持{{ getAcceptType() }}文件格式</div>
<div v-if="progress > 0" class="item-cancel">
<n-button type="success" style="margin-top: 5px" @click="cancelUploadInfo">
X
</n-button>
</div> -->
</div>
<div class="item-process" v-if="progress > 0">
<n-progress
type="line"
:percentage="progress"
indicator-placement="inside"
processing
/>
</div>
<!-- <div v-if="requiredValid" class="item-valid"></div> -->
<!-- <div class="resume-upload">
<n-button type="primary" @click="resumeUpload">
继续上传
</n-button>
</div> -->
</div>
</template>
<script setup lang="ts">
import { MessageReactive, useMessage } from "naive-ui";
import SparkMD5 from 'spark-md5';
import { onMounted, ref } from "vue";
const messageReactive: MessageReactive | null = null;
import type { MessageReactive } from 'naive-ui'
import { useMessage } from 'naive-ui'
import SparkMD5 from 'spark-md5'
import { onMounted, ref } from 'vue'
// const { calculateHash } = useFileHash();
// const { calculateHash, isCalculating } = useFileHash()
// debugger
@ -51,7 +11,7 @@ const messageReactive: MessageReactive | null = null;
const props = defineProps({
accept: {
type: Array as PropType<string[]>,
default: () => ["doc", "docx", "pdf", "safetensors", "ckpt"],
default: () => ['doc', 'docx', 'pdf', 'safetensors', 'ckpt'],
},
required: {
type: Boolean,
@ -71,69 +31,82 @@ const props = defineProps({
},
type: {
type: String,
default: "model",
default: 'model',
},
});
})
//
const emit = defineEmits<{
'upload-success': [
{
objectKey: string
objectUrl: string
fileName: string
currentFileSize: number
hashCode: number
},
]
}>()
const messageReactive: MessageReactive | null = null
//
const fileInputRef = ref<HTMLInputElement | null>(null);
const file = ref<File | null>(null);
const fileName = ref("");
const fileSize = ref(0);
const progress = ref(0);
const objectKey = ref("");
const chunkSize = ref(5 * 1024 * 1024);
const totalChunks = ref(0);
const currentChunk = ref(1);
const uploadId = ref("");
const partArr = ref([]);
const showFileName = ref(false);
const bucketName = ref("");
const objectKeyName = ref("");
const requiredValid = ref(false);
const hashCode = ref("");
const message = useMessage();
const fileInputRef = ref<HTMLInputElement | null>(null)
const file = ref<File | null>(null)
const fileName = ref('')
const fileSize = ref(0)
const progress = ref(0)
const objectKey = ref('')
const chunkSize = ref(5 * 1024 * 1024)
const totalChunks = ref(0)
const currentChunk = ref(1)
const uploadId = ref('')
const partArr = ref([])
const showFileName = ref(false)
const bucketName = ref('')
const objectKeyName = ref('')
const requiredValid = ref(false)
const hashCode = ref('')
const message = useMessage()
const currentFileSize = ref(0)
//
const uploadStatus = ref<'idle' | 'uploading' | 'paused' | 'completed'>('idle');
const uploadedChunks = ref<Set<number>>(new Set());
const retryCount = ref<number>(0);
const maxRetries = 5; //
const uploadStatus = ref<'idle' | 'uploading' | 'paused' | 'completed'>('idle')
const uploadedChunks = ref<Set<number>>(new Set())
const retryCount = ref<number>(0)
const maxRetries = 5 //
//
const triggerFileSelect = () => {
fileInputRef.value?.click();
};
function triggerFileSelect() {
fileInputRef.value?.click()
}
//
const getAcceptType = () => {
return props.accept.join(",");
};
function getAcceptType() {
return props.accept.join(',')
}
// hash
const calculateFileHash = (file: File): Promise<string> => {
function calculateFileHash(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
const reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = (event) => {
try {
const buffer = event.target?.result as ArrayBuffer;
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
const hash = spark.end();
resolve(hash);
} catch (error) {
reject(error);
const buffer = event.target?.result as ArrayBuffer
const spark = new SparkMD5.ArrayBuffer()
spark.append(buffer)
const hash = spark.end()
resolve(hash)
}
catch (error) {
reject(error)
}
}
};
reader.onerror = () => {
reject(new Error('Failed to read file'));
};
});
};
reject(new Error('Failed to read file'))
}
})
}
// const calculateFileHash = (file: File): Promise<string> => {
// return new Promise((resolve, reject) => {
// const reader = new FileReader();
@ -166,81 +139,85 @@ const calculateFileHash = (file: File): Promise<string> => {
// };
//
const initParam = () => {
file.value = null;
objectKey.value = "";
totalChunks.value = 0;
currentChunk.value = 1;
uploadId.value = "";
partArr.value = [];
requiredValid.value = false;
uploadStatus.value = 'idle';
uploadedChunks.value.clear();
retryCount.value = 0;
};
function initParam() {
file.value = null
objectKey.value = ''
totalChunks.value = 0
currentChunk.value = 1
uploadId.value = ''
partArr.value = []
requiredValid.value = false
uploadStatus.value = 'idle'
uploadedChunks.value.clear()
retryCount.value = 0
}
function getOssDefaultPath(name: any) {
const timestamp = Date.now();
const now = new Date();
const year = now.getFullYear(); //
const month = String(now.getMonth() + 1).padStart(2, "0"); //
const day = String(now.getDate()).padStart(2, "0"); //
const timestamp = Date.now()
const now = new Date()
const year = now.getFullYear() //
const month = String(now.getMonth() + 1).padStart(2, '0') //
const day = String(now.getDate()).padStart(2, '0') //
return `${year}/${month}/${day}/${timestamp}/${name}`; //
return `${year}/${month}/${day}/${timestamp}/${name}` //
}
//
const selectFile = async (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.files?.length) return;
async function selectFile(event: Event) {
const target = event.target as HTMLInputElement
if (!target.files?.length)
return
file.value = target.files[0];
target.value = "";
file.value = target.files[0]
target.value = ''
if (file.value?.size) {
currentFileSize.value = file.value?.size
//
const calculatedFileSize = Math.round((file.value.size / 1024 / 1024) * 100) / 100;
const calculatedFileSize = Math.round((file.value.size / 1024 / 1024) * 100) / 100
if (calculatedFileSize > props.fileSize) {
message.warning(`文件大小超过上限${props.fileSize}MB`);
return;
message.warning(`文件大小超过上限${props.fileSize}MB`)
return
}
fileSize.value = calculatedFileSize;
fileSize.value = calculatedFileSize
//
const selectedFileName = file.value.name;
const fileType = selectedFileName.split(".").pop()?.toLowerCase();
const selectedFileName = file.value.name
const fileType = selectedFileName.split('.').pop()?.toLowerCase()
if (!fileType || !props.accept.includes(fileType)) {
message.warning("文件类型不支持");
return;
message.warning('文件类型不支持')
return
}
try {
//
if (uploadStatus.value === 'paused' && file.value) {
const shouldResume = window.confirm('检测到未完成的上传,是否继续上传?');
const shouldResume = window.confirm('检测到未完成的上传,是否继续上传?')
if (shouldResume) {
await resumeUpload();
return;
} else {
await resumeUpload()
return
}
else {
//
initParam();
initParam()
}
}
//
progress.value = 0;
fileName.value = file.value.name;
showFileName.value = true;
progress.value = 0
fileName.value = file.value.name
showFileName.value = true
// hash
if (props.verifyHash) {
message.info("正在校验文件hash...");
message.info('正在校验文件hash...')
try {
hashCode.value = await calculateFileHash(file.value);
} catch (err) {
hashCode.value = await calculateFileHash(file.value)
}
catch (err) {
message.warning(err)
hashCode.value = ''
}finally{
}
finally {
// messageReactive.destroy();
// messageReactive = null;
}
@ -250,14 +227,15 @@ const selectFile = async (event: Event) => {
try {
const hashRes = await request.get(
`/file/selectHash?hashCode=${hashCode.value}&type=${
props.type === "model" ? 0 : 1
}`
);
props.type === 'model' ? 0 : 1
}`,
)
if (hashRes.data !== 1) {
message.warning("文件已存在");
return;
message.warning('文件已存在')
return
}
} finally {
}
finally {
if (messageReactive) {
// messageReactive.destroy();
// messageReactive = null;
@ -267,7 +245,7 @@ const selectFile = async (event: Event) => {
//
if (props.verifyName) {
message.info("正在校验文件名...");
message.info('正在校验文件名...')
//
// 0 1
try {
@ -278,14 +256,15 @@ const selectFile = async (event: Event) => {
`/file/selectFile`,
{
type: props.type,
name:fileName.value
}
);
name: fileName.value,
},
)
if (res.data !== 1) {
message.warning("文件名已存在");
return;
message.warning('文件名已存在')
return
}
} finally {
}
finally {
if (messageReactive) {
// messageReactive.destroy();
// messageReactive = null;
@ -293,17 +272,18 @@ const selectFile = async (event: Event) => {
}
}
totalChunks.value = Math.ceil(file.value.size / chunkSize.value);
objectKey.value = `${getOssDefaultPath(fileName.value)}`;
totalChunks.value = Math.ceil(file.value.size / chunkSize.value)
objectKey.value = `${getOssDefaultPath(fileName.value)}`
// ID
const res = await request.get(`/file/getUploadId?objectKey=${objectKey.value}`);
uploadId.value = res.data;
progress.value = 1;
uploadFile();
} catch (error) {
console.error("文件处理失败:", error);
message.error("文件处理失败");
initParam();
const res = await request.get(`/file/getUploadId?objectKey=${objectKey.value}`)
uploadId.value = res.data
progress.value = 1
uploadFile()
}
catch (error) {
console.error('文件处理失败:', error)
message.error('文件处理失败')
initParam()
}
}
// const target = event.target as HTMLInputElement;
@ -342,178 +322,210 @@ const selectFile = async (event: Event) => {
// message.error("ID");
// }
// }
};
}
//
const uploadFile = async () => {
if (!file.value) return;
async function uploadFile() {
if (!file.value)
return
while (currentChunk.value <= totalChunks.value) {
//
if (uploadedChunks.value.has(currentChunk.value)) {
currentChunk.value++;
continue;
currentChunk.value++
continue
}
const index = currentChunk.value - 1;
const start = index * chunkSize.value;
const end = Math.min((index + 1) * chunkSize.value, file.value.size);
const index = currentChunk.value - 1
const start = index * chunkSize.value
const end = Math.min((index + 1) * chunkSize.value, file.value.size)
const formData = new FormData();
formData.append("file", file.value.slice(start, end));
formData.append("chunk", String(currentChunk.value));
formData.append("objectKey", objectKey.value);
formData.append("uploadId", uploadId.value);
const formData = new FormData()
formData.append('file', file.value.slice(start, end))
formData.append('chunk', String(currentChunk.value))
formData.append('objectKey', objectKey.value)
formData.append('uploadId', uploadId.value)
try {
uploadStatus.value = 'uploading';
const res = await request.post("/file/chunk", formData, {
headers: { "Content-Type": "multipart/form-data" },
uploadStatus.value = 'uploading'
const res = await request.post('/file/chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
//
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
((currentChunk.value - 1) / totalChunks.value) * 100 +
(progressEvent.loaded / progressEvent.total!) * (100 / totalChunks.value)
);
progress.value = percentCompleted;
}
});
((currentChunk.value - 1) / totalChunks.value) * 100
+ (progressEvent.loaded / progressEvent.total!) * (100 / totalChunks.value),
)
progress.value = percentCompleted
},
})
//
uploadedChunks.value.add(currentChunk.value);
partArr.value.push(res.data);
currentChunk.value++;
retryCount.value = 0; //
uploadedChunks.value.add(currentChunk.value)
partArr.value.push(res.data)
currentChunk.value++
retryCount.value = 0 //
progress.value = Math.floor((currentChunk.value / totalChunks.value) * 100);
} catch (error) {
console.error("切片上传失败:", error);
progress.value = Math.floor((currentChunk.value / totalChunks.value) * 100)
}
catch (error) {
console.error('切片上传失败:', error)
//
if (retryCount.value < maxRetries) {
retryCount.value++;
message.warning(`${currentChunk.value} 个分片上传失败,正在进行第 ${retryCount.value} 次重试...`);
await new Promise(resolve => setTimeout(resolve, 1000)); // 1
continue; //
retryCount.value++
message.warning(`${currentChunk.value} 个分片上传失败,正在进行第 ${retryCount.value} 次重试...`)
await new Promise(resolve => setTimeout(resolve, 1000)) // 1
continue //
}
//
uploadStatus.value = 'paused';
message.error(`上传失败,已暂停。您可以稍后继续上传。`);
return;
uploadStatus.value = 'paused'
message.error(`上传失败,已暂停。您可以稍后继续上传。`)
return
}
}
//
if (currentChunk.value > totalChunks.value) {
progress.value = 99;
uploadStatus.value = 'completed';
await complete();
progress.value = 99
uploadStatus.value = 'completed'
await complete()
}
}
};
//
const resumeUpload = async () => {
async function resumeUpload() {
if (uploadStatus.value === 'paused' && file.value) {
retryCount.value = 0; //
message.info('正在继续上传...');
await uploadFile();
retryCount.value = 0 //
message.info('正在继续上传...')
await uploadFile()
}
}
};
//
const complete = async () => {
async function complete() {
try {
const res = await request.post(
`/file/completeUpload?objectKey=${objectKey.value}&uploadId=${uploadId.value}`,
partArr.value
);
partArr.value,
)
bucketName.value = res.data.bucketName;
objectKeyName.value = res.data.objectKey;
progress.value = 100;
bucketName.value = res.data.bucketName
objectKeyName.value = res.data.objectKey
progress.value = 100
//
emit("upload-success", {
emit('upload-success', {
objectKey: objectKeyName.value,
objectUrl: res.data.objectUrl, //
fileName: fileName.value,
currentFileSize: currentFileSize.value,
hashCode: hashCode.value,
});
})
initParam();
} catch (error) {
console.error("文件上传失败:", error);
message.warning("文件上传失败");
initParam()
}
catch (error) {
console.error('文件上传失败:', error)
message.warning('文件上传失败')
}
};
//
const emit = defineEmits<{
"upload-success": [
{
objectKey: string;
objectUrl: string;
fileName: string;
currentFileSize: number;
hashCode: number;
}
];
}>();
//
const cancelUploadInfo = async () => {
async function cancelUploadInfo() {
if (uploadStatus.value !== 'completed') {
try {
await request.post("/file/cancelUpload", {
await request.post('/file/cancelUpload', {
objectKey: objectKey.value,
uploadId: uploadId.value,
});
uploadStatus.value = 'idle';
uploadedChunks.value.clear();
progress.value = 0;
showFileName.value = false;
initParam();
} catch (error) {
message.error("取消上传失败");
})
uploadStatus.value = 'idle'
uploadedChunks.value.clear()
progress.value = 0
showFileName.value = false
initParam()
}
catch (error) {
message.error('取消上传失败')
}
}
}
};
//
const validRequired = () => {
function validRequired() {
if (bucketName.value && objectKeyName.value) {
requiredValid.value = false;
return true;
requiredValid.value = false
return true
}
requiredValid.value = true
return false
}
requiredValid.value = true;
return false;
};
//
const getUploadInfo = () => {
function getUploadInfo() {
return {
bucketName: bucketName.value,
fileName: objectKeyName.value,
fileSize: fileSize.value,
};
};
}
}
//
onMounted(() => {
progress.value = 0;
});
progress.value = 0
})
//
defineExpose({
triggerFileSelect,
validRequired,
getUploadInfo,
});
})
</script>
<style scoped>
<template>
<div class="upload-container">
<div class="fileUpload">
<div class="item-upload">
<!-- <span v-if="required" class="required">*</span> -->
<!-- <n-button type="success" @click="triggerFileSelect"> </n-button> -->
<input
ref="fileInputRef"
type="file"
class="file2"
style="display: none"
@change="selectFile"
>
</div>
<!-- <div v-if="showFileName" class="item-info">
{{ fileName }}
</div>
<div v-else class="item-info">支持{{ getAcceptType() }}文件格式</div>
<div v-if="progress > 0" class="item-cancel">
<n-button type="success" style="margin-top: 5px" @click="cancelUploadInfo">
X
</n-button>
</div> -->
</div>
<div v-if="progress > 0" class="item-process">
<n-progress
type="line"
:percentage="progress"
indicator-placement="inside"
processing
/>
</div>
<!-- <div v-if="requiredValid" class="item-valid"></div> -->
<!-- <div class="resume-upload">
<n-button type="primary" @click="resumeUpload">
继续上传
</n-button>
</div> -->
</div>
</template>
<style scoped lang="scss">
.upload-container {
.fileUpload {
display: flex;

View File

@ -0,0 +1,246 @@
<script setup lang="ts">
import { EllipsisVertical, User } from 'lucide-vue-next'
import { defineEmits, defineProps } from 'vue'
import { useRouter } from 'vue-router'
import EditPlanet from './EditPlanet.vue'
const props = defineProps<{
item: PlanetItem
type: 'myCreate' | 'myJoin' | 'all'
}>()
const emit = defineEmits<{
(e: 'click', id: number): void
(e: 'refresh'): void
}>()
const router = useRouter()
const message = useMessage()
const isDelete = ref(false)
interface PlanetItem {
id: number
imageUrl: string
publishNum: number
communityName: string
avatar: string
nickName: string
createDay: number
description: string
price: number | null
userType: number
}
const showEditPlanet = ref(false)
const menuItems = computed(() => {
const items = [
{
label: '分享星球',
action: () => {
const url = window.location.href
navigator.clipboard.writeText(url)
message.success('链接已复制到剪贴板')
},
},
{
label: '退出星球',
action: async () => {
isDelete.value = true
},
},
]
if (props.type === 'myCreate') {
items.splice(1, 0, {
label: '星球设置',
action: () => {
showEditPlanet.value = true
},
})
}
if (props.item.userType !== 0) {
items.splice(1, 0, {
label: '成员管理',
action: () => window.open(`/planetMember?communityId=${props.item.id}&tenantId=${props.item.userId}`, '_blank'),
})
}
return items
})
async function onDelete() {
try {
const res = await request.post('/community/quit', { communityId: props.item.id, tenantId: props.item.userId })
if (res.code === 200) {
message.success('退出成功!')
emit('refresh')
isDelete.value = false
}
}
catch (error: any) {
if (error.code !== 12202) {
message.error('退出失败,请重新退出!')
}
}
}
const showDropdown = ref(false)
let timeoutId: number | null = null
function showMenu() {
if (timeoutId)
clearTimeout(timeoutId)
showDropdown.value = true
}
function hideMenu() {
timeoutId = setTimeout(() => {
showDropdown.value = false
}, 100)
}
function handleMenuClick(e: Event) {
e.stopPropagation() // goDetail
}
function handleSuccess() {
emit('refresh')
}
function goDetail(item: any) {
if (item.isJoin === 0) {
router.push({
path: '/public-planet-detail',
state: {
communityId: item.id,
tenantId: item.tenantId,
userId: item.userId,
},
})
}
else {
router.push(`/planet-detail?communityId=${item.id}&tenantId=${item.tenantId}`)
}
}
</script>
<template>
<div>
<div class="bg-white rounded-lg shadow-sm flex p-4 cursor-pointer hover:shadow-[0_4px_14px_0_rgba(0,0,0,0.1)]" @click="goDetail(item)">
<div class="w-[161px] h-[161px] relative rounded-lg">
<img class="w-full h-full object-cover rounded-lg" :src="item.imageUrl" alt="">
<span class="absolute bottom-2 left-2 text-white text-xs">{{ item.publishNum || 0 }}人已经加入</span>
</div>
<div class="flex flex-col gap-2 px-4 flex-1 justify-between">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<div class="text-base font-bold flex-1 truncate">
{{ item.communityName }}
</div>
<div v-if="props.type === 'myCreate'" class="relative" @click="handleMenuClick">
<div
class="cursor-pointer p-1 hover:bg-gray-100 rounded-full"
@mouseenter="showMenu"
@mouseleave="hideMenu"
>
<User class="w-4 h-4" />
</div>
<!-- 下拉菜单 -->
<div
v-if="showDropdown"
class="absolute right-0 top-full mt-1 bg-white border border-[#e5e6eb] rounded-lg py-2 min-w-[120px] z-999 shadow-[0_4px_14px_0_rgba(0,0,0,0.1)]"
@mouseenter="showMenu"
@mouseleave="hideMenu"
>
<div
v-for="item in menuItems"
:key="item.label"
class="px-4 py-2 text-sm hover:bg-gray-100 cursor-pointer"
@click="item.action"
>
{{ item.label }}
</div>
</div>
</div>
<div v-if="props.type === 'myJoin'" class="relative" @click="handleMenuClick">
<div
class="cursor-pointer p-1 hover:bg-gray-100 rounded-full"
@mouseenter="showMenu"
@mouseleave="hideMenu"
>
<EllipsisVertical class="w-6 h-4 rotate-90" />
</div>
<!-- 下拉菜单 -->
<div
v-if="showDropdown"
class="absolute right-0 top-full mt-1 bg-white border border-[#e5e6eb] rounded-lg py-2 min-w-[120px] z-10 shadow-[0_4px_14px_0_rgba(0,0,0,0.1)]"
@mouseenter="showMenu"
@mouseleave="hideMenu"
>
<div
v-for="item in menuItems"
:key="item.label"
class="px-4 py-2 text-sm hover:bg-gray-100 cursor-pointer"
@click="item.action"
>
{{ item.label }}
</div>
</div>
</div>
</div>
<div class="flex items-center gap-2 text-xs text-[#878d95]">
<img class="w-[20px] h-[20px] rounded-full" :src="item.avatar" alt="">
<div>
{{ item.nickName }}
</div>
<div>创建{{ item.createDay || 0 }}</div>
</div>
<div class="text-sm text-[#4a5563] line-clamp-3">
{{ item.description }}
</div>
</div>
<div class="text-[#fc4141]">
<div v-if="item.price">
<span class="text-base font-bold mr-1">
{{ item.price }}
</span>
<span class="text-xs">
金币
</span>
</div>
<div v-else>
免费
</div>
</div>
</div>
</div>
<n-modal
v-model:show="isDelete"
:mask-closable="false"
preset="dialog"
title="提示!"
content="退出后将无法查看/操作该星球的内容,是否确认退出该星球"
negative-text="取消"
positive-text="确认退出"
@negative-click="isDelete = false"
@positive-click="onDelete"
/>
<EditPlanet
v-model:show="showEditPlanet"
:community-id="props.item.id"
:tenant-id="props.item.userId"
@success="handleSuccess"
/>
</div>
</template>
<style scoped>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@ -1,11 +1,9 @@
<script setup lang="ts">
import { commonApi } from "@/api/common";
import { cloneDeep } from "lodash-es";
import { Asterisk, Trash } from "lucide-vue-next";
import type { FormInst } from "naive-ui";
import { computed, ref, watch } from "vue";
const message = useMessage();
import type { FormInst } from 'naive-ui'
import { commonApi } from '@/api/common'
import { cloneDeep } from 'lodash-es'
import { Asterisk, Trash } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
//
const props = defineProps({
@ -13,37 +11,41 @@ const props = defineProps({
type: Object,
required: true,
},
});
const emit = defineEmits(["update:modelValue", "nextStep", "prevStep"]);
const acceptTypes =
".safetensors,.ckpt,.pt,.bin,.pth,.zip,.json,.flow,.lightflow,.yaml,.yml,.onnx,.gguf,.sft";
})
const emit = defineEmits(['update:modelValue', 'nextStep', 'prevStep'])
const message = useMessage()
const acceptTypes
= '.safetensors,.ckpt,.pt,.bin,.pth,.zip,.json,.flow,.lightflow,.yaml,.yml,.onnx,.gguf,.sft'
const acceptTypesList = [
"safetensors",
"ckpt",
"pt",
"bin",
"pth",
"zip",
"json",
"flow",
"lightflow",
"yaml",
"yml",
"onnx",
"gguf",
"sft",
];
'safetensors',
'ckpt',
'pt',
'bin',
'pth',
'zip',
'json',
'flow',
'lightflow',
'yaml',
'yml',
'onnx',
'gguf',
'sft',
]
const modelVersionItem = {
objectKey: null,
isEncrypt: 0, // 0
delFlag: "0", // 0 2
versionName: "", //
delFlag: '0', // 0 2
versionName: '', //
modelVersionType: null, //
versionDescription: "", //
filePath: "", //
fileName: "", //
versionDescription: '', //
filePath: '', //
fileName: '', //
sampleImagePaths: [], // 20,
triggerWords: "", //
triggerWords: '', //
isPublic: null, //
isOnlineUse: 1, // 线使
allowFusion: 1, //
@ -55,92 +57,93 @@ const modelVersionItem = {
isExclusiveModel: 1, //
hideImageGenInfo: 0, //
id: null,
};
}
defineExpose({
addVersion,
});
const loading = ref(false);
})
const loading = ref(false)
const localForm = computed({
get() {
return props.modelValue;
return props.modelValue
},
set(value) {
emit("update:modelValue", value);
emit('update:modelValue', value)
},
});
})
watch(
() => localForm.value,
(newVal) => {
emit("update:modelValue", newVal);
emit('update:modelValue', newVal)
},
{ immediate: true, deep: true }
);
{ immediate: true, deep: true },
)
const formRefs = ref<(FormInst | null)[]>([]);
const formRefs = ref<(FormInst | null)[]>([])
function setFormRef(el: FormInst | null, index: number) {
if (el) {
formRefs.value[index] = el;
formRefs.value[index] = el
}
}
const rules = {
versionName: {
required: true,
message: "",
trigger: "blur",
message: '',
trigger: 'blur',
},
modelVersionType: {
required: true,
message: "",
trigger: "blur",
message: '',
trigger: 'blur',
},
};
}
function addVersion() {
const param = cloneDeep(modelVersionItem);
localForm.value.modelVersionList.unshift(param);
const param = cloneDeep(modelVersionItem)
localForm.value.modelVersionList.unshift(param)
}
const originalBtnList = ref([
{
label: "免费",
label: '免费',
value: 1,
},
{
label: "会员下载",
label: '会员下载',
value: 0,
},
]);
])
async function nextStep() {
for (let i = 0; i < localForm.value.modelVersionList.length; i++) {
if (
localForm.value.modelVersionList[i].delFlag === "0" &&
localForm.value.modelVersionList[i].fileName === ""
localForm.value.modelVersionList[i].delFlag === '0'
&& localForm.value.modelVersionList[i].fileName === ''
) {
return message.error("请上传文件");
return message.error('请上传文件')
}
const regex = /[\u4E00-\u9FA5]/; //
const regex = /[\u4E00-\u9FA5]/ //
if (
localForm.value.modelVersionList[i].delFlag === "0" &&
!regex.test(localForm.value.modelVersionList[i].versionDescription)
localForm.value.modelVersionList[i].delFlag === '0'
&& !regex.test(localForm.value.modelVersionList[i].versionDescription)
) {
return message.error("请用中文填写版本介绍");
return message.error('请用中文填写版本介绍')
}
}
try {
const promises = formRefs.value
.filter((form): form is FormInst => form !== null)
.map((form) => form.validate());
.map(form => form.validate())
await Promise.all(promises);
emit("nextStep");
} catch (errors) {
console.error("部分表单验证失败:", errors);
await Promise.all(promises)
emit('nextStep')
}
catch (errors) {
console.error('部分表单验证失败:', errors)
}
}
//
const uploadRef = ref<InstanceType<typeof FileUpload> | null>(null);
const uploadRef = ref<InstanceType<typeof FileUpload> | null>(null)
// const getFileInfo = () => {
// if (uploadRef.value[0].validRequired()) {
// const fileInfo = uploadRef.value[0].getUploadInfo()
@ -148,53 +151,55 @@ const uploadRef = ref<InstanceType<typeof FileUpload> | null>(null);
// }
// }
const handleUploadSuccess = (fileInfo: {
objectKey: string;
objectUrl: string;
fileName: string;
currentFileSize: number;
function handleUploadSuccess(fileInfo: {
objectKey: string
objectUrl: string
fileName: string
currentFileSize: number
hashCode: string
}) => {
localForm.value.modelVersionList[uploadFileIndex.value].filePath = fileInfo.objectUrl;
localForm.value.modelVersionList[uploadFileIndex.value].objectKey = fileInfo.objectKey;
localForm.value.modelVersionList[uploadFileIndex.value].fileName = fileInfo.fileName;
localForm.value.modelVersionList[uploadFileIndex.value].fileSize = fileInfo.currentFileSize;
localForm.value.modelVersionList[uploadFileIndex.value].fileHash = fileInfo.hashCode;
}) {
localForm.value.modelVersionList[uploadFileIndex.value].filePath = fileInfo.objectUrl
localForm.value.modelVersionList[uploadFileIndex.value].objectKey = fileInfo.objectKey
localForm.value.modelVersionList[uploadFileIndex.value].fileName = fileInfo.fileName
localForm.value.modelVersionList[uploadFileIndex.value].fileSize = fileInfo.currentFileSize
localForm.value.modelVersionList[uploadFileIndex.value].fileHash = fileInfo.hashCode
// message.success('')
//
//
};
}
//
const uploadFileIndex = ref(0);
const uploadFileIndex = ref(0)
async function triggerFileInput(index: number) {
uploadRef.value[0].triggerFileSelect()
uploadFileIndex.value = index;
uploadFileIndex.value = index
}
function prevStep() {
emit("prevStep");
emit('prevStep')
}
const baseModelTypeList = ref([]);
const baseModelTypeList = ref([])
async function getDictType() {
try {
const res = await commonApi.dictType({ type: "mode_version_type" });
const res = await commonApi.dictType({ type: 'mode_version_type' })
if (res.code === 200) {
baseModelTypeList.value = res.data;
}
} catch (error) {
console.log(error);
baseModelTypeList.value = res.data
}
}
getDictType();
catch (error) {
console.log(error)
}
}
getDictType()
function computedDelFlag() {
return localForm.value.modelVersionList.filter((item) => item.delFlag === "0");
return localForm.value.modelVersionList.filter(item => item.delFlag === '0')
}
function onDelete(index: number) {
if (computedDelFlag().length === 1) return;
localForm.value.modelVersionList[index].delFlag = "2";
if (computedDelFlag().length === 1)
return
localForm.value.modelVersionList[index].delFlag = '2'
}
function handledeleteFile(item: any) {
@ -280,7 +285,9 @@ function handledeleteFile(item:any){
>
上传文件
</div>
<div class="my-3">点击上传文件</div>
<div class="my-3">
点击上传文件
</div>
<div class="text-[#999999] text-xs">
.safetensors/.ckpt/.pt/.bin/.pth/.zip/.json/.flow/.lightflow/.yaml/.yml/.onnx/.gguf/.sft
</div>
@ -291,10 +298,10 @@ function handledeleteFile(item:any){
:verify-name="true"
type="model"
:accept="acceptTypesList"
@upload-success="handleUploadSuccess"
:required="true"
:file-size="30000"
></fileUpload>
@upload-success="handleUploadSuccess"
/>
</div>
</n-spin>
</div>
@ -306,8 +313,12 @@ function handledeleteFile(item:any){
<WangEditor v-model="item.versionDescription" />
</client-only>
</div>
<div class="mt-6">触发词</div>
<div class="-mb-5 text-gray-400 text-[12px]">请输入您用来训练的单词</div>
<div class="mt-6">
触发词
</div>
<div class="-mb-5 text-gray-400 text-[12px]">
请输入您用来训练的单词
</div>
<n-form-item path="triggerWords">
<n-input v-model:value="item.triggerWords" placeholder="例如: 1boy" />
</n-form-item>
@ -328,7 +339,9 @@ function handledeleteFile(item:any){
</div> -->
</n-form>
<div class="text-gray-400 text-[12px] my-4">许可范围</div>
<div class="text-gray-400 text-[12px] my-4">
许可范围
</div>
<div class="flex flex-wrap">
<div class="w-[50%] mb-2">
<n-checkbox
@ -366,7 +379,9 @@ function handledeleteFile(item:any){
</div>
</div>
<div class="text-gray-400 text-[12px] my-4">商用许可范围</div>
<div class="text-gray-400 text-[12px] my-4">
商用许可范围
</div>
<div class="flex flex-wrap">
<div class="w-[50%] mb-2">
<n-checkbox
@ -387,7 +402,9 @@ function handledeleteFile(item:any){
</div>
<div class="mb-4">
<div class="text-gray-400 text-[12px] my-4">独家设置</div>
<div class="text-gray-400 text-[12px] my-4">
独家设置
</div>
<div class="flex items-center mb-2">
<n-checkbox
v-model:checked="item.isExclusiveModel"

View File

@ -1,8 +1,7 @@
export const appName = '魔创未来'
export const appDescription = '魔创未来'
export const authRoutes = ['/personal-center']
export const verifyBlankRoute = ['/member-center']
export const verifyBlankRoute = ['/member-center', '/planet']
export const isOriginalList = [{
label: '原创',
value: 0,
@ -18,7 +17,7 @@ export const isPublicList = [{
value: 2,
}]
export const headerRole = { // 包括就隐藏
inputSearch: ['/model-square']
inputSearch: ['/model-square'],
}
// export const searchType = { // 包括就隐藏
// 'picture-square':'image',

View File

@ -10,10 +10,11 @@ Lightbulb,
Maximize,
Network,
User,
Workflow
} from "lucide-vue-next";
import { useRouter } from "vue-router";
const userStore = useUserStore();
Workflow,
} from 'lucide-vue-next'
// import { useRouter } from 'vue-router'
const userStore = useUserStore()
// definePageMeta({
// middleware:[
// function (to, from ){
@ -23,102 +24,103 @@ const userStore = useUserStore();
// }
// ]
// })
const modalStore = useModalStore();
const modalStore = useModalStore()
const menuStore = useMenuStore();
const router = useRouter();
const menuStore = useMenuStore()
// const router = useRouter()
//
const iconMap: any = {
"/model-square": LayoutGrid,
"/picture-square": Lightbulb,
"/work-square": Workflow,
"/web-ui": Image,
"/comfy-ui": Workflow,
"/training-lora": Binary,
"/high-availability": Maximize,
"/api-platform": Code2,
"/creator-center": Code2,
"/personal-center": User,
"/member-center": Crown,
};
const route = useRoute();
'/model-square': LayoutGrid,
'/picture-square': Lightbulb,
'/work-square': Workflow,
'/web-ui': Image,
'/comfy-ui': Workflow,
'/training-lora': Binary,
'/high-availability': Maximize,
'/api-platform': Code2,
'/creator-center': Code2,
'/personal-center': User,
'/member-center': Crown,
}
const route = useRoute()
//
watch(
() => route.path,
(path) => {
menuStore.setActiveMenu(path);
menuStore.setActiveMenu(path)
},
{ immediate: true } //
);
{ immediate: true }, //
)
const menuItems1 = ref({
title: "探索",
title: '探索',
list: [
{
icon: LayoutGrid,
text: "模型广场",
route: "/model-square",
text: '模型广场',
route: '/model-square',
},
{
icon: Lightbulb,
text: "作品灵感",
route: "/picture-square",
text: '作品灵感',
route: '/picture-square',
},
{
icon: Workflow,
text: "工作流",
route: "/work-square",
text: '工作流',
route: '/work-square',
},
],
});
})
const menuItems2 = ref({
title: "创作",
title: '创作',
list: [
{
icon: Network,
text: "在线工作流",
route: "",
desc: "Comfy UI",
text: '在线工作流',
route: '',
desc: 'Comfy UI',
},
{
icon: Earth,
text: "魔创星球",
route: "",
text: '魔创星球',
route: '/planet',
},
],
});
})
const menuItems3 = ref({
title: "其他",
title: '其他',
list: [
{
icon: User,
text: "个人中心",
route: "/personal-center",
text: '个人中心',
route: '/personal-center',
},
{
icon: Crown,
text: "会员中心",
route: "/member-center",
text: '会员中心',
route: '/member-center',
},
],
});
})
//
menuStore.menuItems = menuStore.menuItems.map((item: any) => ({
...item,
LucideIcon: iconMap[item.path], // Lucide
}));
}))
function handleSide(event: Event, path: string) {
if (path === "/member-center") {
if (path === '/member-center') {
if (!userStore.isLoggedIn) {
modalStore.showLoginModal();
} else {
event.preventDefault(); //
event.stopPropagation(); //
const baseUrl = window.location.origin;
window.open(`${baseUrl}/member-center`, "_blank", "noopener,noreferrer");
modalStore.showLoginModal()
}
else {
event.preventDefault() //
event.stopPropagation() //
const baseUrl = window.location.origin
window.open(`${baseUrl}/member-center`, '_blank', 'noopener,noreferrer')
}
//
// nextTick(() => {
@ -126,29 +128,32 @@ function handleSide(event: Event, path: string) {
// navigateTo(route.path, { replace: true })
// }
// })
} else if (path === "/personal-center" && !userStore.isLoggedIn) {
modalStore.showLoginModal();
} else {
menuStore.setActiveMenu(path);
}
else if (path === '/personal-center' && !userStore.isLoggedIn) {
modalStore.showLoginModal()
}
else {
menuStore.setActiveMenu(path)
}
}
const appList = ref([]);
const appList = ref([])
async function getAppList() {
try {
const res = await request.get(`/app/list`);
const res = await request.get(`/app/list`)
if (res.code === 200) {
appList.value = res.data;
}
} catch (err) {
console.log(err);
appList.value = res.data
}
}
getAppList();
catch (err) {
console.log(err)
}
}
getAppList()
function toUs(url: string) {
const baseUrl = window.location.origin;
window.open(`${baseUrl}/us/${url}`, "_blank", "noopener,noreferrer");
const baseUrl = window.location.origin
window.open(`${baseUrl}/us/${url}`, '_blank', 'noopener,noreferrer')
}
</script>
@ -160,7 +165,7 @@ function toUs(url: string) {
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar -->
<nav
class="w-[230px] border-r border-gray-100 bg-gray-50/50 dark:border-dark-700 dark:bg-dark-800/50 overflow-y-auto"
class="w-[230px] border-r border-gray-100 bg-gray-50/50 dark:border-dark-700 dark:bg-dark-800/50 overflow-y-auto scrollbar-hide [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
>
<div class="space-y-1 px-2">
<div class="text-[#000] p-4">
@ -270,36 +275,55 @@ function toUs(url: string) {
<div class="px-4">
<div class="text-xs text-gray-500 flex flex-wrap gap-2">
<div
class="cursor-pointer relative group"
v-for="(item, index) in appList"
:key="index"
class="cursor-pointer relative group"
>
{{ item.name }}
<div
class="flex flex-col justify-center items-center gap-2 absolute bottom-4 left-0 w-[90px] h-[115px] hidden group-hover:block bg-white p-2 z-index-999 border border-solid border-[#ccc] rounded-lg"
>
<div class="text-xs text-center mb-2">扫码关注</div>
<img class="w-[80px] h-[70px]" :src="item.url" alt="" />
<div class="text-xs text-center mb-2">
扫码关注
</div>
<img class="w-[80px] h-[70px]" :src="item.url" alt="">
</div>
</div>
</div>
<div class="flex justify-between text-xs text-gray-400 mt-2">
<div class="cursor-pointer" @click="toUs('agreement')"></div>
<div class="cursor-pointer" @click="toUs('privacy')"></div>
<div class="cursor-pointer" @click="toUs('aboutUs')"></div>
<div class="flex justify-between text-xs text-gray-400 mt-2 mb-2">
<div class="cursor-pointer" @click="toUs('agreement')">
用户协议
</div>
<div class="cursor-pointer" @click="toUs('privacy')">
隐私政策
</div>
<div class="cursor-pointer" @click="toUs('aboutUs')">
关于我们
</div>
</div>
<div class="justify-between text-xs text-gray-400 mt-2 scale-80">
<div class="scale-80">
魔创未来盘锦量子科技有限公司
</div>
</div>
<div class="justify-between text-xs text-gray-400 mt-2 scale-80">
<!-- <a class="cursor-pointer scale-80">
辽ICP备2024045484号-1
</div> -->
<a class="text-xs leading-7 hover:opacity-80 mt-[8px]" href="https://beian.miit.gov.cn" rel="noreferrer" target="_blank">辽ICP备2024045484号-1</a>
</div>
<!-- <div class="justify-between text-xs text-gray-400 mt-2 scale-80">
<div class="cursor-pointer scale-80">广州魔创未来科技有限公司</div>
<div class="cursor-pointer scale-80">
网信算备
</div>
<div class="cursor-pointer scale-80">
110112129623601230015
</div>
</div>
<div class="justify-between text-xs text-gray-400 mt-2 scale-80">
<div class="cursor-pointer scale-80">京ICP备2023015442号</div>
<div class="cursor-pointer scale-80">
备案号: Beijing-PianYu-202402
</div>
<div class="justify-between text-xs text-gray-400 mt-2 scale-80">
<div class="cursor-pointer scale-80">网信算备</div>
<div class="cursor-pointer scale-80">110112129623601230015</div>
</div>
<div class="justify-between text-xs text-gray-400 mt-2 scale-80">
<div class="cursor-pointer scale-80">备案号: Beijing-PianYu-202402</div>
</div> -->
</div>
<!-- <NuxtLink

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
</script>
<template>
<div>
<PlanetHeader />
<main class="flex-1 overflow-auto">
<slot />
</main>
</div>
</template>
<style></style>

View File

@ -1,3 +1,57 @@
<script setup>
import { useRoute } from '#app'
import { onMounted, onUnmounted, ref } from 'vue'
const route = useRoute()
definePageMeta({
layout: 'home',
})
const countdown = ref(3)
const countdownText = ref('页面即将关闭...')
let timer = null
let urlParams = null
// const urlParams = new URLSearchParams(window.location.search);
const isSuccess = ref(false)
async function handleAuthorization() {
const uuid = route.query.uuid
const code = route.query.code
// const uuid = urlParams.get('uuid');
// const code = urlParams.get('code');
const url = `/wx/uuid/bind/openid?uuid=${uuid}&code=${code}`
try {
const res = await request.get(url)
isSuccess.value = true
}
catch (err) {}
}
onMounted(() => {
handleAuthorization()
// urlParams = new URLSearchParams(window.location.search);
// const uuid = urlParams.get('uuid');
// console.log(uuid);
// timer = setInterval(() => {
// countdown.value--;
// countdownText.value = `${countdown.value}...`;
// if (countdown.value <= 0) {
// clearInterval(timer);
// //
// window.close();
// // window.close()
// // window.location.href = 'URL'
// }
// }, 1000);
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
<div class="min-h-screen bg-white flex items-center justify-center px-4">
<!-- <n-button @click="handleAuthorization" type="info">
@ -23,7 +77,9 @@
</svg>
</div>
<h2 class="text-xl font-medium text-gray-900 mb-2 animate-fade-in">登录成功</h2>
<h2 class="text-xl font-medium text-gray-900 mb-2 animate-fade-in">
登录成功
</h2>
<!-- <p class="text-sm text-gray-500 animate-fade-in-delay">
{{ countdownText }}
</p> -->
@ -32,9 +88,9 @@
<div v-else>
<div class="wave-loading mb-8">
<div class="wave-circle">
<div class="wave"></div>
<div class="wave"></div>
<div class="wave"></div>
<div class="wave" />
<div class="wave" />
<div class="wave" />
<div class="logo-container">
<!-- <svg class="logo-icon" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="currentColor"/>
@ -69,7 +125,9 @@
<!-- 提示信息 -->
<div class="mt-4 space-y-2 animate-fade-in">
<p class="text-gray-500 text-sm">正在验证您的身份</p>
<p class="text-gray-500 text-sm">
正在验证您的身份
</p>
<div class="flex items-center justify-center space-x-2">
<svg class="w-4 h-4 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
@ -79,12 +137,12 @@
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
/>
</svg>
<span class="text-sm text-gray-400">请稍候片刻</span>
</div>
@ -106,58 +164,6 @@
</div>
</template>
<script setup>
import { useRoute } from "#app";
import { onMounted, onUnmounted, ref } from "vue";
const route = useRoute();
definePageMeta({
layout: "home",
});
const countdown = ref(3);
const countdownText = ref("页面即将关闭...");
let timer = null;
let urlParams = null;
// const urlParams = new URLSearchParams(window.location.search);
const isSuccess = ref(false);
const handleAuthorization = async () => {
const uuid = route.query.uuid;
const code = route.query.code;
// const uuid = urlParams.get('uuid');
// const code = urlParams.get('code');
const url = `/wx/uuid/bind/openid?uuid=${uuid}&code=${code}`;
try {
const res = await request.get(url);
isSuccess.value = true;
} catch (err) {}
};
onMounted(() => {
handleAuthorization()
// urlParams = new URLSearchParams(window.location.search);
// const uuid = urlParams.get('uuid');
// console.log(uuid);
// timer = setInterval(() => {
// countdown.value--;
// countdownText.value = `${countdown.value}...`;
// if (countdown.value <= 0) {
// clearInterval(timer);
// //
// window.close();
// // window.close()
// // window.location.href = 'URL'
// }
// }, 1000);
});
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style>
@keyframes scale-in {
0% {

View File

@ -43,7 +43,7 @@ getIsMember()
const MemberBenefitList = ref([])
async function getMemberBenefitList() {
try {
const res = await request.get('/memberLevel/getMemberBenefitList')
const res = await request.get('/memberBenefit/getMemberBenefitList')
if (res.code === 200) {
MemberBenefitList.value = res.data
}

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import AttentionMsg from "@/components/message/AttentionMsg.vue";
import LikeMsg from "@/components/message/LikeMsg.vue";
import OfficialMsg from "@/components/message/OfficialMsg.vue";
import ReplyMsg from "@/components/message/ReplyMsg.vue";
import { Check, ChevronDown, Heart, MessageSquareMore, UserPlus, Volume2 } from "lucide-vue-next";
import AttentionMsg from '@/components/message/AttentionMsg.vue'
import LikeMsg from '@/components/message/LikeMsg.vue'
import OfficialMsg from '@/components/message/OfficialMsg.vue'
import ReplyMsg from '@/components/message/ReplyMsg.vue'
import { Check, ChevronDown, Heart, MessageSquareMore, UserPlus, Volume2 } from 'lucide-vue-next'
const currentMsgType = ref("reply");
const currentMsgType = ref('reply')
const MsgTypeList = ref([
// {
// label:'',
@ -13,27 +13,27 @@ const MsgTypeList = ref([
// icon:'Volume2'
// },
{
label: "回复我的",
value: "reply",
icon: "MessageSquareMore",
label: '回复我的',
value: 'reply',
icon: 'MessageSquareMore',
},
{
label: "收到的赞",
value: "like",
icon: "Heart",
label: '收到的赞',
value: 'like',
icon: 'Heart',
},
{
label: "关注我的",
value: "attention",
icon: "UserPlus",
label: '关注我的',
value: 'attention',
icon: 'UserPlus',
},
]);
])
const iconMap: any = {
'official':Volume2,
'reply': MessageSquareMore,
'like':Heart,
'attention':UserPlus,
official: Volume2,
reply: MessageSquareMore,
like: Heart,
attention: UserPlus,
}
MsgTypeList.value = MsgTypeList.value.map(item => ({
@ -42,16 +42,16 @@ MsgTypeList.value = MsgTypeList.value.map(item => ({
}))
const urlList = ref({
official: "",
reply: "/advice/getCommentMsg",
like: "/advice/getLikeMsg",
attention: "/advice/getAttentionMsg",
});
official: '',
reply: '/advice/getCommentMsg',
like: '/advice/getLikeMsg',
attention: '/advice/getAttentionMsg',
})
const allList = ref({
reply: [],
like: [],
attention: [],
});
})
// const officialList = ref([])
// const replyList = ref([])
// const likeList = ref([])
@ -64,95 +64,99 @@ const allList = ref({
// value:'4'
// }
// ])
const replyParamsType = ref("3");
const replyParamsType = ref('3')
const replyParamsTypeList = ref([
{
label: "全部通知",
value: "3",
label: '全部通知',
value: '3',
},
{
label: "模型评论",
value: "0",
label: '模型评论',
value: '0',
},
{
label: "图片评论",
value: "2",
label: '图片评论',
value: '2',
},
{
label: "工作流评论",
value: "1",
label: '工作流评论',
value: '1',
},
]);
])
const likeParamsType = ref("4");
const likeParamsType = ref('4')
const likeParamsTypeList = ref([
{
label: "全部通知",
value: "4",
label: '全部通知',
value: '4',
},
{
label: "模型点赞",
value: "0",
label: '模型点赞',
value: '0',
},
{
label: "图片点赞",
value: "2",
label: '图片点赞',
value: '2',
},
{
label: "工作流点赞",
value: "1",
label: '工作流点赞',
value: '1',
},
{
label: "评论点赞",
value: "3",
label: '评论点赞',
value: '3',
},
]);
])
async function getList() {
const url = urlList.value[currentMsgType.value];
let productType = "";
if (currentMsgType.value === "reply") {
productType = replyParamsType.value;
} else if (currentMsgType.value === "like") {
productType = likeParamsType.value;
} else {
productType = "";
const url = urlList.value[currentMsgType.value]
let productType = ''
if (currentMsgType.value === 'reply') {
productType = replyParamsType.value
}
const res = await request.get(url, { productType });
else if (currentMsgType.value === 'like') {
productType = likeParamsType.value
}
else {
productType = ''
}
const res = await request.get(url, { productType })
if (res.code === 200) {
allList.value[currentMsgType.value] = res.data;
allList.value[currentMsgType.value] = res.data
}
}
getList();
getList()
function changeType(item: any) {
currentMsgType.value = item.value;
likeParamsType.value = "4";
replyParamsType.value = "3";
getList();
currentMsgType.value = item.value
likeParamsType.value = '4'
replyParamsType.value = '3'
getList()
}
function handleSelect(item: any, type: string) {
if (type === "like") {
likeParamsType.value = item.value;
} else {
replyParamsType.value = item.value;
if (type === 'like') {
likeParamsType.value = item.value
}
getList();
else {
replyParamsType.value = item.value
}
getList()
}
//
async function onAllRead() {
try {
const res = await request.get("/advice/readAll");
const res = await request.get('/advice/readAll')
if (res.code === 200) {
getList();
getList()
}
} catch (err) {
console.log(err);
}
catch (err) {
console.log(err)
}
}
definePageMeta({
layout: "default",
});
layout: 'default',
})
</script>
<template>
@ -168,13 +172,15 @@ definePageMeta({
class="w-[287px] bg-white p-3 box-content rounded-l-lg text-gray-600 flex flex-col justify-between"
>
<div>
<div class="p-3 text-xl">通知</div>
<div class="p-3 text-xl">
通知
</div>
<div
class="msg-item"
:style="{ background: item.value === currentMsgType ? '#f5f6fa' : '' }"
v-for="(item, index) in MsgTypeList"
:key="index"
class="msg-item"
:style="{ background: item.value === currentMsgType ? '#f5f6fa' : '' }"
@click="changeType(item)"
>
<!-- <Volume2 size="18" /> -->
@ -218,10 +224,10 @@ definePageMeta({
>
<div
v-for="(item, index) in replyParamsTypeList"
:key="index"
:style="{
color: item.value === replyParamsType ? '#3a75f6' : '',
}"
:key="index"
class="hover:bg-gray-100 py-2 cursor-pointer rounded-lg"
@click="(event) => handleSelect(item, 'reply')"
>
@ -234,10 +240,10 @@ definePageMeta({
>
<div
v-for="(item, index) in likeParamsTypeList"
:key="index"
:style="{
color: item.value === likeParamsType ? '#3a75f6' : '',
}"
:key="index"
class="hover:bg-gray-100 py-2 cursor-pointer rounded-lg"
@click="(event) => handleSelect(item, 'like')"
>
@ -259,6 +265,7 @@ definePageMeta({
</div>
</div>
</template>
<style scoped>
.msg-item {
@apply h-14 flex items-center mb-2 py-3 pl-6 rounded-lg cursor-pointer;

View File

@ -200,9 +200,9 @@ async function onLike() {
//
const reportParams = ref({
reportId: undefined,
reportType: undefined,
text: '',
type: 1,
type: 0,
})
const reportList = ref([])
async function getDictType() {
@ -218,7 +218,7 @@ async function getDictType() {
}
getDictType()
function handleChange(item) {
reportParams.value.reportId = item.dictValue
reportParams.value.reportType = item.dictValue
if (item.dictValue !== '5') {
reportParams.value.text = ''
}
@ -231,8 +231,9 @@ function closeReport() {
//
async function onReport() {
if (reportParams.value.reportId !== undefined) {
if (reportParams.value.reportType !== undefined) {
reportParams.value.productId = detailsInfo.value.id
reportParams.value.productName = detailsInfo.value.modelName
try {
const res = await request.post('/report/addReport', reportParams.value)
if (res.code === 200) {
@ -682,7 +683,7 @@ async function buyModel() {
:key="index"
class="m-4 text-[#fff000]"
size="large"
:checked="reportParams.reportId === item.dictValue"
:checked="reportParams.reportType === item.dictValue"
value="Definitely Maybe"
name="basic-demo"
@change="handleChange(item)"
@ -691,7 +692,7 @@ async function buyModel() {
</n-radio>
<n-input
v-if="
reportParams.reportId !== undefined && reportParams.reportId === '5'
reportParams.reportType !== undefined && reportParams.reportType === '5'
"
v-model:value="reportParams.text"
placeholder="点击输入"
@ -704,7 +705,7 @@ async function buyModel() {
<div
class="mt-4 w-[100%] h-10 flex rounded-lg text-white items-center justify-center cursor-pointer"
:class="[
reportParams.reportId !== undefined ? 'bg-[#4c79ee]' : 'bg-[#cccccc]',
reportParams.reportType !== undefined ? 'bg-[#4c79ee]' : 'bg-[#cccccc]',
]"
@click="onReport"
>

View File

@ -0,0 +1,181 @@
<script setup lang="ts">
import { User } from 'lucide-vue-next'
const emit = defineEmits(['refresh'])
// import CreatePlanet from '~/components/CreatePlanet.vue'
definePageMeta({
layout: 'planet',
})
const userStore = useUserStore()
const communityTag = ref('myCreate')
const communityTagList = ref([
{
dictLabel: '我创建的',
dictValue: 'myCreate',
},
{
dictLabel: '我加入的',
dictValue: 'myJoin',
},
])
//
const listParams = ref({
communityTag: null as string | null,
pageNum: 1,
pageSize: 20,
userId: userStore.userInfo?.userId,
isMyCreate: 0,
})
interface CommunityItem {
imageUrl: string
publishNum: number
communityName: string
avatar: string
createBy: string
createDay: number
description: string
price: number | null
}
const listFinish = ref(false)
const loading = ref(false)
const dataList = ref<CommunityItem[]>([])
function refresh() {
listFinish.value = false
listParams.value.pageNum = 1
getList()
emit('refresh')
}
async function getList() {
try {
if (listFinish.value || loading.value) // loading,
return
loading.value = listParams.value.pageNum === 1 // loading
const url = communityTag.value === 'myCreate' ? '/community/list' : `/community/myJoin`
const res = await request.post<ApiResponse<CommunityItem>>(url, listParams.value)
if (res.code === 200) {
if (listParams.value.pageNum === 1) {
dataList.value = res.rows
}
else {
dataList.value = [...dataList.value, ...res.rows]
}
if (dataList.value.length >= res.total) {
listFinish.value = true
}
listParams.value.pageNum++
}
}
catch (error) {
dataList.value = []
listFinish.value = true
console.error(error)
}
finally {
loading.value = false
}
}
getList()
function changeTag(value: string) {
communityTag.value = value
listParams.value.pageNum = 1
listFinish.value = false
getList()
}
const isShowCreateModal = ref(false)
function showCreateModal() {
isShowCreateModal.value = true
}
function handleCreateSuccess() {
refresh()
}
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA]">
<div class="px-10 bg-[#fff]">
<div class="flex flex-wrap gap-2 py-4 ">
<div
v-for="(item, index) in communityTagList"
:key="index"
class="px-[12px] py-[9px] text-sm rounded-lg cursor-pointer"
:class="{ 'bg-black text-white': communityTag === item.dictValue, 'bg-white text-[#878d95] hover:bg-gray-100': communityTag !== item.dictValue }"
@click="changeTag(item.dictValue)"
>
{{ item.dictLabel }}
</div>
</div>
</div>
<article class="px-10 py-6">
<n-infinite-scroll :distance="10" trigger="once" @load="getList">
<!-- 添加trigger="once"属性 -->
<div v-if="loading" class="grid grid-cols-3 gap-2">
<div v-for="i in 9" :key="i" class="bg-white rounded-lg overflow-hidden shadow-sm flex p-4">
<n-skeleton class="w-[161px] h-[161px] rounded-lg flex-shrink-0" />
<div class="flex flex-col gap-2 px-4 flex-1">
<div class="flex flex-col gap-2">
<n-skeleton text :width="140" />
<div class="flex items-center gap-2">
<n-skeleton circle width="20px" height="20px" />
<n-skeleton text :width="60" />
<n-skeleton text :width="80" />
</div>
<n-skeleton text :repeat="2" />
</div>
<n-skeleton text :width="60" />
</div>
</div>
</div>
<div v-else-if="dataList.length > 0" class="grid grid-cols-3 gap-2">
<div v-if="communityTag === 'myCreate'" class="bg-black rounded-lg flex flex-col justify-center items-center p-4 hover:shadow-[0_4px_14px_0_rgba(0,0,0,0.1)] transition-all duration-300 cursor-pointer" @click="showCreateModal">
<div class="text-white text-xl">
魔创星球
</div>
<div class="text-xs text-[#878d95] mt-1 mb-4">
加入星球发布优质的内容吧
</div>
<div class="flex text-white bg-gradient-to-r from-[#197dff] to-[#2c4dff] rounded-[4px] px-8 py-3 text-sm cursor-pointer hover:from-[#3b8fff] hover:to-[#4465ff]">
创建星球
</div>
</div>
<PlanetItem v-for="(item, index) in dataList" :key="index" :item="item" :type="communityTag" @refresh="refresh" />
</div>
<div v-if="dataList.length === 0 && listFinish" class="flex flex-col items-center justify-center py-12">
<div class="w-48 h-48 mb-4 flex items-center justify-center">
<div class="relative">
<div class="w-32 h-32 rounded-full border-4 border-gray-200" />
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-24 h-24 rounded-full border-4 border-gray-100" />
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 rounded-full bg-gray-50" />
</div>
</div>
<p class="text-gray-500 mb-4">
这里空空如也什么都没有找到~
</p>
<!-- <n-button type="primary" size="small" class="px-6">
去发现更多
</n-button> -->
</div>
</n-infinite-scroll>
</article>
<CreatePlanet
v-model:show="isShowCreateModal"
@success="handleCreateSuccess"
@refresh="refresh"
/>
</div>
</template>
<style scoped>
body,
html {
background-color: #f7f8fa;
}
</style>

View File

@ -0,0 +1,265 @@
<script setup lang="ts">
import planetBg from '@/assets/img/planetBg.png'
import { NConfigProvider, NImage, NMessageProvider } from 'naive-ui'
import { ref, watch } from 'vue'
const userStore = useUserStore()
definePageMeta({
layout: 'planet',
})
const isShow = ref('publish')
const listParams = ref({
communityTag: null as string | null,
pageNum: 1,
pageSize: 100,
userId: userStore.userInfo?.userId,
isMyCreate: 1,
})
interface CommunityItem {
imageUrl: string
publishNum: number
communityName: string
avatar: string
createBy: string
createDay: number
description: string
price: number | null
}
const listFinish = ref(false)
const loading = ref(false)
const communityList = ref<CommunityItem[]>([])
const tabList = [
{ key: 0, label: '发布的帖子' },
{ key: 1, label: '我的问答' },
{ key: 2, label: '我的收藏' },
]
const commentParams = ref({
communityId: null as string | null,
pageNum: 1,
pageSize: 10,
tenantId: null as string | null,
type: 0,
isMy: 1,
})
// function refresh() {
// listParams.value.pageNum = 1
// getList()
// }
//
async function getCommunityList() {
try {
if (listFinish.value || loading.value) // loading,
return
loading.value = listParams.value.pageNum === 1 // loading
const res = await request.post<ApiResponse<CommunityItem>>(`/community/myJoin`, listParams.value)
if (res.code === 200) {
if (res.rows.length !== 0) {
const { id, tenantId } = res.rows[0]
commentParams.value.tenantId = tenantId
commentParams.value.communityId = id
getCommentList()
}
if (listParams.value.pageNum === 1) {
communityList.value = res.rows
}
else {
communityList.value = [...communityList.value, ...res.rows]
}
if (communityList.value.length >= res.total) {
listFinish.value = true
}
listParams.value.pageNum++
}
}
catch (error) {
communityList.value = []
listFinish.value = true
console.error(error)
}
finally {
loading.value = false
}
}
getCommunityList()
//
const commentList = ref([])
const commentFinish = ref(false)
const commentLoading = ref(false)
async function getCommentList() {
try {
if (commentFinish.value || commentLoading.value) // loading,
return
commentLoading.value = commentParams.value.pageNum === 1 // loading personHome/getPersonHomeLis
const res = await request.post<ApiResponse<CommunityItem>>(`/personHome/getPersonHomeList`, commentParams.value)
if (res.code === 200) {
if (listParams.value.pageNum === 1) {
commentList.value = res.rows
}
else {
commentList.value = [...commentList.value, ...res.rows]
}
if (commentList.value.length >= res.total) {
commentFinish.value = true
}
listParams.value.pageNum++
}
}
catch (error) {
commentList.value = []
commentFinish.value = true
console.error(error)
}
finally {
commentLoading.value = false
}
}
//
function refresh() {
commentFinish.value = false
commentLoading.value = false
commentParams.value.pageNum = 1
getCommentList()
}
// const userInfo = ref({
// avatar: userStore.userInfo?.avatar,
// nickname: userStore.userInfo?.nickName,
// createdCount: 14456,
// followCount: 145,
// collectCount: 46464,
// })
//
function changeCommunity(item: any) {
commentParams.value.tenantId = item.tenantId
commentParams.value.communityId = item.id
refresh()
}
//
function changeTab(key: number) {
if (key !== 1) {
isShow.value = 'publish'
}
else {
isShow.value = 'question'
}
commentParams.value.type = key
refresh()
}
// //
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()
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA]">
<div class="relative">
<!-- 背景图片区域 -->
<div class="h-[300px] w-full bg-[#E8F3FF]">
<img :src="planetBg" alt="背景图片" class="w-full h-full object-cover">
</div>
<!-- 主要内容区域 -->
<div class="absolute inset-x-0 top-[80px]">
<div class="max-w-[1140px] mx-auto">
<!-- 用户基本信息 -->
<client-only>
<div class="flex items-center gap-4 mb-6">
<img :src="userStore.userInfo?.avatar" class="w-20 h-20 rounded-full" alt="头像">
<div>
<div class="text-lg font-medium text-[#1f2329]">
{{ userStore.userInfo?.nickName }}
</div>
<div class="flex items-center gap-6 mt-2 text-[#86909C]">
<div>{{ selectUserInfo.bean || 0 }} 粉丝</div>
<div>{{ selectUserInfo.attention || 0 }} 关注</div>
</div>
</div>
</div>
</client-only>
<!-- 内容区域白色背景 -->
<div class="bg-white rounded-lg min-h-[500px] mt-8">
<!-- 导航标签 -->
<div class="flex border-b border-[#E5E6EB]">
<div
v-for="(item, index) in tabList"
:key="index"
class="px-6 py-3 text-[#1f2329] font-medium cursor-pointer"
:class="{ 'text-[#3f7ef7] border-b-2 border-[#3f7ef7]': commentParams.type === item.key }"
@click="changeTab(item.key)"
>
{{ item.label }}
</div>
</div>
<!-- 星球分类标签 -->
<div class="flex items-center gap-2 p-3">
<div
v-for="item in communityList"
:key="item.id"
class="px-3 py-1 text-sm rounded-lg cursor-pointer"
:class="[
commentParams.communityId === item.id
? 'text-white bg-[#1f2329]'
: 'text-[#1f2329] hover:text-white hover:bg-[#1f2329]',
]"
@click="changeCommunity(item)"
>
{{ item.communityName }}
</div>
</div>
<div v-if="isShow === 'publish'">
<NConfigProvider>
<NMessageProvider>
<PlanetComment
:publish-list-params="commentParams"
/>
</NMessageProvider>
</NConfigProvider>
</div>
<div v-if="isShow === 'question'">
<NConfigProvider>
<NMessageProvider>
<PlanetQuestionComment
:publish-list-params="commentParams"
/>
</NMessageProvider>
</NConfigProvider>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -572,9 +572,9 @@ async function showBinding() {
try {
const res2 = await request.get(`/ali/pay/queryBindStatus`)
if (res2.data === '1') {
await userStore.getUserInfo()
closeBindingModal()
message.success('绑定成功!')
// window.location.reload()
getBindStatus()
}
}

View File

@ -1,3 +1,267 @@
<script setup lang="ts">
import { Close } from '@vicons/ionicons5'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
const observer = ref<IntersectionObserver | null>(null)
const message = useMessage()
const userStore = useUserStore()
const route = useRoute()
const { userId } = route.query
const loading = ref(false)
const finished = ref(false)
const total = ref(0) //
const loadingTrigger = ref(null)
const urlList = ref({
0: '/personalCenter/selectByUserIdModel',
1: '/personalCenter/selectByUserIdWorkFlow',
2: '/personalCenter/selectByUserIdImage',
})
const currentType = ref('0')
const typeList = ref([
{ id: '0', title: '模型' },
{ id: '1', title: '工作流' },
{ id: '2', title: '图片' },
])
const orderOptions = ref([
{
dictLabel: '最新',
dictValue: 'create_time',
},
{
dictLabel: '最热',
dictValue: 'like_num',
},
])
const isShowFan = ref(false)
const activeTab = ref('like')
function closefanList() {
isShowFan.value = false
}
const bannerStyle = {
backgroundImage:
'url(\'https://img.zcool.cn/community/special_cover/3a9a64d3628c000c2a1000657eec.jpg\')',
}
const publishParams = ref({
pageNum: 1,
pageSize: 20,
orderByColumn: 'create_time',
userId,
})
const currentUserInfo = ref({})
async function getCurrentUserInfo() {
const res = await request.get(`/system/user/selectUserById?id=${userId}`)
if (res.code === 200) {
currentUserInfo.value = res.data
}
}
getCurrentUserInfo()
//
const attentionList = ref([])
const attentionListParams = ref({
pageNumber: 1,
pageSize: 20,
type: userId,
})
async function getAttentionList() {
const res = await request.post(`/attention/selectToAttention`, {
...attentionListParams.value,
})
if (res.code === 200) {
attentionList.value = res.data.list
}
}
getAttentionList()
//
const likeList = ref([])
const likeListParams = ref({
pageNumber: 1,
pageSize: 20,
type: userId,
})
async function getLikeList() {
const res = await request.post(`/attention/selectAttentionList`, {
...likeListParams.value,
})
if (res.code === 200) {
likeList.value = res.data.list
}
}
getLikeList()
//
const isAttention = ref(false)
async function getIsAttention() {
const res = await request.get(`/attention/selectAttention?userId=${userId}`)
if (res.code === 200) {
isAttention.value = res.data
}
}
getIsAttention()
// /
async function onAttention(item: any) {
const myUserId = userStore.userInfo.userId
if (myUserId === item.userId) {
return message.warning('自己不能关注自己')
}
let paramsUserId
if (item.userId) {
paramsUserId = item.userId
}
else {
paramsUserId = userId
}
try {
const res = await request.get(`/attention/addAttention?userId=${paramsUserId}`)
if (res.code === 200) {
if (res.data) {
message.success('关注成功')
}
else {
message.success('取消关注成功')
}
if (item.userId) {
item.attention = !item.attention
}
else {
isAttention.value = !isAttention.value
}
}
}
catch (err) {
console.log(err)
}
}
// //
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?userId=${userId}`)
if (res.code === 200) {
selectUserInfo.value = res.data
}
}
catch (err) {
console.log(err)
}
}
getAttention()
// //
function changeType(id: string) {
currentType.value = id
initPageNUm()
}
// /
function changeLikeOrder(value: string) {
publishParams.value.orderByColumn = value
initPageNUm()
}
function initPageNUm() {
publishParams.value.pageNum = 1
finished.value = false //
getList()
}
//
const dataList = ref([])
async function getList() {
if (loading.value || finished.value)
return
loading.value = true
const url = urlList.value[currentType.value]
try {
const res = await request.post(url, publishParams.value)
if (res.code === 200) {
//
if (publishParams.value.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
}
publishParams.value.pageNum++
}
}
catch (err) {
dataList.value = []
finished.value = true
console.log(err)
}
finally {
loading.value = false
}
}
getList()
async function topedRefresh() {
if (import.meta.client) {
await nextTick()
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
initPageNUm()
}
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)
}
})
onUnmounted(() => {
window.removeEventListener('scroll', topedRefresh)
if (observer.value) {
observer.value.disconnect()
}
})
</script>
<template>
<div class="mx-auto container">
<div
@ -15,16 +279,15 @@
class="head-img m-1 h-16 w-16 rounded-full bg-white"
:src="currentUserInfo.avatar"
alt="User Avatar"
/>
>
</client-only>
<!-- {{ userStore.userInfo.avatar }} -->
</div>
</div>
<div
v-if="isAttention !== null"
class="mr-2 cursor-pointer rounded-full bg-gradient-to-r from-[#1f66df] to-[#3faeff] px-4 py-1 text-white"
@click="onAttention"
v-if="isAttention !== null"
>
{{ isAttention ? '已关注' : '关注' }}
</div>
@ -111,11 +374,14 @@
/>
</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 v-if="loading" class="text-center text-gray-500">
加载中...
</div>
<div class="fan-centent" v-if="isShowFan">
<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"
>
@ -137,7 +403,7 @@
class="flex justify-between items-center py-2"
>
<div class="flex items-center">
<img :src="item.avatar || ''" alt="" class="w-14 h-14 rounded-full mr-2" />
<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, 'attention')">
@ -158,7 +424,7 @@
class="flex justify-between items-center py-2"
>
<div class="flex items-center">
<img :src="item.avatar || ''" alt="" class="w-14 h-14 rounded-full mr-2" />
<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, 'like')">
@ -176,263 +442,6 @@
</div>
</template>
<script setup lang="ts">
import { Close } from "@vicons/ionicons5";
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { useRoute } from "vue-router";
const observer = ref<IntersectionObserver | null>(null);
const message = useMessage()
const userStore = useUserStore();
const route = useRoute();
const { userId } = route.query;
const loading = ref(false);
const finished = ref(false);
const total = ref(0); //
const loadingTrigger = ref(null);
const urlList = ref({
"0": "/personalCenter/selectByUserIdModel",
"1": "/personalCenter/selectByUserIdWorkFlow",
"2": "/personalCenter/selectByUserIdImage",
});
const currentType = ref("0");
const typeList = ref([
{ id: "0", title: "模型" },
{ id: "1", title: "工作流" },
{ id: "2", title: "图片" },
]);
const orderOptions = ref([
{
dictLabel: "最新",
dictValue: "create_time",
},
{
dictLabel: "最热",
dictValue: "like_num",
},
]);
const isShowFan = ref(false);
const activeTab = ref("like");
function closefanList() {
isShowFan.value = false
}
const bannerStyle = {
backgroundImage:
"url('https://img.zcool.cn/community/special_cover/3a9a64d3628c000c2a1000657eec.jpg')",
};
const publishParams = ref({
pageNum: 1,
pageSize: 20,
orderByColumn: "create_time",
userId: userId,
});
const currentUserInfo = ref({});
async function getCurrentUserInfo() {
const res = await request.get(`/system/user/selectUserById?id=${userId}`);
if (res.code === 200) {
currentUserInfo.value = res.data;
}
}
getCurrentUserInfo();
//
const attentionList = ref([]);
const attentionListParams = ref({
pageNumber: 1,
pageSize: 20,
type: userId,
});
async function getAttentionList() {
const res = await request.post(`/attention/selectToAttention`, {
...attentionListParams.value,
});
if (res.code === 200) {
attentionList.value = res.data.list;
}
}
getAttentionList();
//
const likeList = ref([]);
const likeListParams = ref({
pageNumber: 1,
pageSize: 20,
type: userId,
});
async function getLikeList() {
const res = await request.post(`/attention/selectAttentionList`, {
...likeListParams.value,
});
if (res.code === 200) {
likeList.value = res.data.list;
}
}
getLikeList();
//
const isAttention = ref(false)
async function getIsAttention(){
const res = await request.get(`/attention/selectAttention?userId=${userId}`)
if(res.code === 200){
isAttention.value = res.data
}
}
getIsAttention()
// /
async function onAttention(item:any){
const myUserId = userStore.userInfo.userId
if(myUserId === item.userId){
return message.warning('自己不能关注自己')
}
let paramsUserId
if(item.userId){
paramsUserId = item.userId
}else{
paramsUserId = userId
}
try {
const res = await request.get(`/attention/addAttention?userId=${paramsUserId}`);
if (res.code === 200) {
if(res.data){
message.success('关注成功')
}else{
message.success('取消关注成功')
}
if(item.userId){
item.attention = !item.attention
}else{
isAttention.value = !isAttention.value
}
}
} catch (err) {
console.log(err);
}
}
// //
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?userId=${userId}`);
if (res.code === 200) {
selectUserInfo.value = res.data;
}
} catch (err) {
console.log(err);
}
}
getAttention();
// //
function changeType(id: string) {
currentType.value = id;
initPageNUm();
}
// /
function changeLikeOrder(value: string) {
publishParams.value.orderByColumn = value;
initPageNUm();
}
function initPageNUm() {
publishParams.value.pageNum = 1;
finished.value = false; //
getList();
}
//
const dataList = ref([]);
async function getList() {
if (loading.value || finished.value) return;
loading.value = true;
const url = urlList.value[currentType.value];
try {
const res = await request.post(url, publishParams.value);
if (res.code === 200) {
//
if (publishParams.value.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;
}
publishParams.value.pageNum++;
}
} catch (err) {
dataList.value = [];
finished.value = true;
console.log(err);
} finally {
loading.value = false;
}
}
getList();
async function topedRefresh() {
if (import.meta.client) {
await nextTick();
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
initPageNUm();
}
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);
}
});
onUnmounted(() => {
window.removeEventListener("scroll", topedRefresh);
if (observer.value) {
observer.value.disconnect();
}
});
</script>
<style scoped lang="scss">
.fan-centent {
position: fixed;

View File

@ -0,0 +1,152 @@
<script setup lang="ts">
import request from '@/utils/request'
import { FolderPlus, ImagePlus, MessageCircle, Star, ThumbsUp } from 'lucide-vue-next'
import { NConfigProvider, NImage, NMessageProvider } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
const message = useMessage()
definePageMeta({
layout: 'planet',
})
const route = useRoute()
const typeList = ref([
{
label: '最新',
value: null,
},
{
label: '只看星主',
value: 0,
},
{
label: '精选',
value: 1,
},
{
label: '问答',
value: 999,
},
])
const showPublishModal = ref(false)
const publishListParams = ref({
communityId: route.query.communityId,
tenantId: route.query.tenantId,
type: null,
pageNum: 1,
pageSize: 10,
})
const questionParams = ref({
communityId: route.query.communityId,
tenantId: route.query.tenantId,
pageNum: 1,
pageSize: 10,
})
const questionList = ref([])
const isShow = ref('publish')
async function handleTypeChange(type: number | null) {
publishListParams.value.type = type
questionParams.value.pageNum = 1
if (type === 999) { //
isShow.value = 'question'
try {
const res = await request.post('/question/list', questionParams.value)
if (res.code === 200) {
questionList.value = res.rows
}
}
catch (error) {
console.error(error)
}
}
else {
publishListParams.value.pageNum = 1
isShow.value = 'publish'
}
}
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA] px-10 py-4 flex gap-4">
<div class="flex-1">
<div class="rounded-lg bg-white">
<div class="flex flex-col">
<div class="p-4 pb-0">
<textarea
class="w-full min-h-[100px] bg-[#F7F8FA] rounded-lg p-4 resize-none outline-none cursor-pointer"
placeholder="此刻的想法..."
@click="showPublishModal = true"
/>
</div>
<div class="flex items-center justify-between p-2 pt-0">
<div class="flex items-center">
<button class="p-2 hover:bg-gray-100 rounded text-gray-500" @click="showPublishModal = true">
<ImagePlus class="w-5 h-5" />
</button>
<button class="p-2 hover:bg-gray-100 rounded text-gray-500" @click="showPublishModal = true">
<FolderPlus class="w-5 h-5" />
</button>
</div>
</div>
</div>
</div>
<div class="rounded-lg py-4">
<div class="flex items-center">
<div
v-for="item in typeList"
:key="item.value"
class="flex items-center px-4 py-1 rounded cursor-pointer mr-2"
:style="{
backgroundColor: publishListParams.type === item.value ? '#000000' : '#ffffff',
color: publishListParams.type === item.value ? '#ffffff' : '#878d95',
}"
@click="handleTypeChange(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
<div v-if="isShow === 'publish'">
<NConfigProvider>
<NMessageProvider>
<PlanetComment
:publish-list-params="publishListParams"
/>
</NMessageProvider>
</NConfigProvider>
</div>
<div v-if="isShow === 'question'">
<NConfigProvider>
<NMessageProvider>
<PlanetQuestionComment
:publish-list-params="questionParams"
/>
</NMessageProvider>
</NConfigProvider>
</div>
</div>
<div class="w-[300px]">
<PlanetBaseInfo :community-id="route.query.communityId" :tenant-id="route.query.tenantId" />
</div>
<n-modal
v-model:show="showPublishModal"
:style="{ width: '640px' }"
preset="card"
:mask-closable="false"
class="rounded-lg"
>
<PublishContent
:community-id="route.query.communityId"
:tenant-id="route.query.tenantId"
@success="showPublishModal = false"
/>
</n-modal>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,358 @@
<script setup lang="ts">
import { formatAmount } from '@/utils'
import request from '@/utils/request'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
// import { useRoute } from 'vue-router'
definePageMeta({
layout: 'planet',
})
// const route = useRoute()
// const message = useMessage()
interface DataDetail {
communityIncome: {
todayIncome: number
yesterdayIncome: number
}
questionIncome: {
todayIncome: number
yesterdayIncome: number
}
totalIncome: number
}
const dataDetail = ref<DataDetail>({
communityIncome: {
todayIncome: 0,
yesterdayIncome: 0,
},
questionIncome: {
todayIncome: 0,
yesterdayIncome: 0,
},
totalIncome: 0,
})
async function getEarningDetail() {
try {
const res = await request.get('/incomeInfo/communityIncome')
if (res.code === 200) {
dataDetail.value = res.data
}
}
catch (error) {
console.error(error)
}
}
getEarningDetail()
const integralGold = ref({})
async function getIntegralGold() {
try {
const res = await request.post('/personalCenter/getPointAndWallet')
if (res.code === 200) {
integralGold.value = res.data
}
}
catch (err) {
console.log(err)
}
}
getIntegralGold()
//
interface TransactionRecord {
userId: string
userName: string
avatar: string
planetId: string
planetName: string
type: string
amount: number
planetIncome: number
createTime: string
}
const transactionList = ref<TransactionRecord[]>([])
const transactionListParams = ref({
searchContent: '',
pageNum: 1,
pageSize: 10,
})
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
})
//
async function getTransactionList() {
try {
const res = await request.post('/incomeInfo/incomeList', {
...transactionListParams.value,
pageNum: pagination.value.page,
pageSize: pagination.value.pageSize,
})
if (res.code === 200) {
transactionList.value = res.rows
pagination.value.itemCount = res.total
}
}
catch (error) {
console.error(error)
}
}
//
function handlePageChange(page: number) {
pagination.value.page = page
getTransactionList()
}
//
function handleSearch() {
pagination.value.page = 1
getTransactionList()
}
const router = useRouter()
function openWalletPage() {
const url = `${window.location.origin}${router.resolve('/wallet').href}`
window.open(url, '_blank')
}
onMounted(() => {
getTransactionList()
})
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA] py-4">
<div class="bg-white rounded-lg py-4 px-6 max-w-[1025px] mx-auto">
<!-- 星球收益数据卡片 -->
<div class="grid grid-cols-4 gap-6 mb-8">
<div class="bg-[#F7F8FA] rounded-lg p-4">
<div class="text-sm text-gray-500 mb-2">
星球收益(金币)
</div>
<div class="text-2xl font-medium">
{{ formatAmount(dataDetail.communityIncome?.todayIncome) }}
</div>
<div class="text-xs mt-1">
昨日 <span class="text-[#3f7ef7]">{{ formatAmount(dataDetail.communityIncome?.yesterdayIncome) }}</span>
</div>
</div>
<div class="bg-[#F7F8FA] rounded-lg p-4">
<div class="text-sm text-gray-500 mb-2">
问答收益(金币)
</div>
<div class="text-2xl font-medium">
{{ formatAmount(dataDetail.questionIncome?.todayIncome) }}
</div>
<div class="text-xs mt-1">
昨日 <span class="text-[#3f7ef7]">{{ formatAmount(dataDetail.questionIncome?.yesterdayIncome) }}</span>
</div>
</div>
<div class="bg-[#F7F8FA] rounded-lg p-4">
<div class="text-sm text-gray-500 mb-2">
星球累计收益(金币)
</div>
<div class="text-2xl font-medium">
{{ formatAmount(dataDetail.totalIncome) }}
</div>
</div>
<div class="bg-[#F7F8FA] rounded-lg p-4">
<div class="text-sm text-gray-500 mb-2">
金币余额
</div>
<div class="text-2xl font-medium">
{{ formatAmount(integralGold.wallet) }}
</div>
<div class="text-xs text-[#3f7ef7] mt-1 cursor-pointer" @click="openWalletPage">
去提现 >
</div>
</div>
</div>
<!-- 积分收益明细 -->
<div class="flex items-center justify-between mb-4">
<div class="text-lg font-medium">
金币收益明细
</div>
<div>
<div class="relative">
<input
v-model="transactionListParams.searchContent"
type="text"
class="pl-3 pr-8 py-1 border border-gray-200 rounded-lg outline-none focus:border-[#3f7ef7] transition-colors"
placeholder="请输入你需要搜索的内容"
@keyup.enter="handleSearch"
>
<div
class="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer"
@click="handleSearch"
>
<div class="i-lucide:search w-4 h-4 text-gray-400" />
</div>
</div>
</div>
</div>
<div class="grid grid-cols-[200px_120px_120px_200px_200px_200px] border-b border-[#eee]">
<div class="p-3 text-[#1f2329] font-medium">
用户
</div>
<div class="p-3 text-[#1f2329] font-medium">
星球名称
</div>
<div class="p-3 text-[#1f2329] font-medium">
类型
</div>
<div class="p-3 text-[#1f2329] font-medium">
支付金额(金币)
</div>
<div class="p-3 text-[#1f2329] font-medium">
时间
</div>
</div>
<div v-if="transactionList.length !== 0">
<div
v-for="item in transactionList"
:key="item.id"
class="grid grid-cols-[200px_120px_120px_200px_200px_200px] border-b border-[#eee] hover:bg-gray-50"
>
<div class="p-3 flex items-center gap-2 w-[200px]">
<img :src="item.avatar" class="w-6 h-6 rounded-full" :alt="item.avatar">
<span
class="truncate flex-1 cursor-pointer"
title="双击复制"
@dblclick="copyToClipboard(item.userName)"
>
{{ item.userName }}
</span>
</div>
<div class="p-3 flex items-center">
{{ item.communityName }}
</div>
<div class="p-3 flex items-center">
{{ item.type === 0 ? '付费加入' : '付费问答' }}
</div>
<div class="p-3 flex items-center">
{{ formatAmount(item.amount) }}
</div>
<div class="p-3 w-[300px] flex items-center">
{{ item.createTime || '-' }}
</div>
</div>
<div class="flex justify-center mt-6">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:page-slot="5"
class="py-2"
@update:page="handlePageChange"
/>
</div>
</div>
<client-only v-else>
<div class="flex justify-center items-center min-h-[200px] text-[#86909c]">
暂无数据
</div>
</client-only>
<!-- 交易记录列表 -->
<!-- <div class="space-y-4">
<div class="flex items-center space-x-2">
<div class="text-sm text-gray-500">
用户
</div>
<div class="text-sm text-gray-500">
星球
</div>
<div class="text-sm text-gray-500">
类型
</div>
<div class="text-sm text-gray-500">
支付金额(积分)
</div>
<div class="text-sm text-gray-500">
星主收入(积分)
</div>
<div class="text-sm text-gray-500">
时间
</div>
</div>
<div
v-for="item in transactionList"
:key="item.userId"
class="flex items-center justify-between py-2"
>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<img :src="item.avatar" class="w-6 h-6 rounded-full">
<span>{{ item.userName }}</span>
</div>
<div>星球{{ item.planetName }}</div>
<div>{{ item.type }}</div>
<div>{{ item.amount }}</div>
<div>{{ item.planetIncome }}</div>
<div class="text-gray-400">
{{ item.createTime }}
</div>
</div>
</div>
</div> -->
</div>
</div>
</template>
<style scoped>
:deep(.n-pagination .n-pagination-item--button) {
border: 1px solid #e5e6eb;
border-radius: 4px;
height: 32px;
line-height: 32px;
padding: 0 8px;
min-width: 32px;
}
:deep(.n-pagination .n-pagination-item) {
height: 32px;
line-height: 32px;
padding: 0 8px;
min-width: 32px;
border-radius: 4px;
margin: 0 4px;
}
:deep(.n-pagination .n-pagination-item:hover) {
color: #1e80ff;
background-color: #f2f3f5;
}
:deep(.n-pagination .n-pagination-item--active) {
background-color: #1e80ff;
color: #fff;
border: none;
}
:deep(.n-pagination .n-pagination-item--active:hover) {
color: #fff;
}
:deep(.n-pagination .n-pagination-quick-jumper) {
height: 32px;
line-height: 32px;
}
:deep(.n-pagination .n-pagination-quick-jumper input) {
height: 32px;
line-height: 32px;
border: 1px solid #e5e6eb;
border-radius: 4px;
width: 50px;
text-align: center;
}
</style>

View File

@ -0,0 +1,211 @@
<script setup lang="ts">
import { commonApi } from '@/api/common'
import { useRouter } from 'vue-router'
const router = useRouter()
definePageMeta({
layout: 'planet',
})
const listParams = ref({
communityTag: null as string | null,
pageNum: 1,
pageSize: 12,
})
const containerRef = ref<HTMLElement | undefined>(undefined)
//
interface CommunityTag {
dictLabel: string
dictValue: string | null
}
const communityTagList = ref<CommunityTag[]>([])
async function getDictType() {
try {
const res = await commonApi.dictType({ type: 'community_tag' })
if (res.code === 200) {
communityTagList.value = [{
dictLabel: '全部',
dictValue: null,
}, ...res.data] as CommunityTag[]
}
}
catch (error) {
console.error(error)
}
}
function changeTag(type: string | null) {
listParams.value.communityTag = type
initGetList()
}
getDictType()
interface ApiResponse<T> {
code: number
rows: T[]
total: number
}
interface CommunityItem {
imageUrl: string
publishNum: number
communityName: string
avatar: string
createBy: string
createDay: number
description: string
price: number | null
}
//
const listFinish = ref(false)
const loading = ref(false)
const dataList = ref<CommunityItem[]>([])
function initGetList() {
listFinish.value = false
listParams.value.pageNum = 1
getList()
}
async function getList() {
try {
if (listFinish.value || loading.value) // loading,
return
loading.value = listParams.value.pageNum === 1 // loading
const res = await request.post<ApiResponse<CommunityItem>>('/community/list', listParams.value)
if (res.code === 200) {
if (listParams.value.pageNum === 1) {
dataList.value = res.rows
}
else {
dataList.value = [...dataList.value, ...res.rows]
}
if (dataList.value.length >= res.total) {
listFinish.value = true
}
listParams.value.pageNum++
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
getList()
function goDetail(id: string) {
router.push(`/planet-detail/${id}`)
}
</script>
<template>
<div class="min-h-screen bg-[#f7f8fa] relative">
<n-affix
:trigger-top="0"
position="absolute"
:listen-to="() => containerRef"
>
<div class="px-10 bg-[#fff] sticky top-0 left-0 right-0 z-10">
<div class="flex flex-wrap gap-2 py-4">
<div
v-for="(item, index) in communityTagList"
:key="index"
class="px-[12px] py-[9px] text-sm rounded-lg cursor-pointer"
:class="{ 'bg-black text-white': listParams.communityTag === item.dictValue, 'bg-white text-[#878d95] hover:bg-gray-100': listParams.communityTag !== item.dictValue }"
@click="changeTag(item.dictValue)"
>
{{ item.dictLabel }}
</div>
</div>
</div>
</n-affix>
<article class="px-10 py-6">
<n-infinite-scroll :distance="10" trigger="once" @load="getList">
<!-- 添加trigger="once"属性 -->
<div v-if="loading" class="grid grid-cols-3 gap-2">
<div v-for="i in 9" :key="i" class="bg-white rounded-lg overflow-hidden shadow-sm flex p-4">
<n-skeleton class="w-[161px] h-[161px] rounded-lg flex-shrink-0" />
<div class="flex flex-col gap-2 px-4 flex-1">
<div class="flex flex-col gap-2">
<n-skeleton text :width="140" />
<div class="flex items-center gap-2">
<n-skeleton circle width="20px" height="20px" />
<n-skeleton text :width="60" />
<n-skeleton text :width="80" />
</div>
<n-skeleton text :repeat="2" />
</div>
<n-skeleton text :width="60" />
</div>
</div>
</div>
<div v-else-if="dataList.length > 0" class="grid grid-cols-3 gap-2">
<PlanetItem v-for="(item, index) in dataList" :key="index" :item="item" type="all" />
<!--
<div v-for="(item, index) in dataList" :key="index" class="bg-white rounded-lg overflow-hidden shadow-sm flex p-4 cursor-pointer hover:shadow-[0_4px_14px_0_rgba(0,0,0,0.1)] hover:-translate-y-1 transition-all duration-300" @click="goDetail(item.id)">
<div class="w-[161px] h-[161px] relative rounded-lg">
<img class="w-full h-full object-cover rounded-lg" :src="item.imageUrl" alt="">
<span class="absolute bottom-2 left-2 text-white text-xs">{{ item.publishNum || 0 }}人已经加入</span>
</div>
<div class="flex flex-col gap-2 px-4 flex-1 justify-between">
<div class="flex flex-col gap-2">
<div class="text-base font-bold">
{{ item.communityName }}
</div>
<div class="flex items-center gap-2 text-xs text-[#878d95]">
<img class="w-[20px] h-[20px] rounded-full" :src="item.avatar" alt="">
<div>
{{ item.nickName }}
</div>
<div>创建{{ item.createDay || 0 }}</div>
</div>
<div class="text-sm text-[#4a5563] line-clamp-3">
{{ item.description }}
</div>
</div>
<div class="text-[#fc4141]">
<div v-if="item.price">
<span class="text-base font-bold mr-1">
{{ item.price }}
</span>
<span class="text-xs">
金币
</span>
</div>
<div v-else>
免费
</div>
</div>
</div>
</div> -->
</div>
<div v-if="dataList.length === 0 && listFinish" class="flex flex-col items-center justify-center py-12">
<div class="w-48 h-48 mb-4 flex items-center justify-center">
<div class="relative">
<div class="w-32 h-32 rounded-full border-4 border-gray-200" />
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-24 h-24 rounded-full border-4 border-gray-100" />
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 rounded-full bg-gray-50" />
</div>
</div>
<p class="text-gray-500 mb-4">
这里空空如也什么都没有找到~
</p>
<!-- <n-button type="primary" size="small" class="px-6">
去发现更多
</n-button> -->
</div>
</n-infinite-scroll>
</article>
</div>
</template>
<style lang="scss" scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,367 @@
<script setup lang="ts">
import { formatFileSize } from '@/utils/index.ts'
import { uploadFileBatches } from '@/utils/uploadImg'
import { useMessage } from 'naive-ui'
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
interface MemberItem {
communityId: number
createTime: string
downloadFileUser: {
avatarList: string[]
count: number
}
fileName: string
fileSize: number
id: number
tenantId: number
uploadUserName: string
}
definePageMeta({
layout: 'planet',
})
const route = useRoute()
const message = useMessage()
const memberList = ref<MemberItem[]>([])
const loading = ref(false)
const isInitialized = ref(false)
const params = ref({
orderByColum: '',
pageNum: 1,
pageSize: 2,
communityId: route.query.communityId,
tenantId: route.query.tenantId,
search: '',
})
//
const pagination = ref({
page: 1,
pageSize: 2,
itemCount: 0,
showSizePicker: false,
})
//
function handlePageChange(page: number) {
pagination.value.page = page
params.value.pageNum = page
getMemberList()
}
//
function handleSearch(e: KeyboardEvent) {
if (e.key === 'Enter') {
params.value.pageNum = 1
pagination.value.page = 1
getMemberList()
}
}
//
async function getMemberList() {
try {
loading.value = true
const res = await request.post('/communityFile/list', params.value)
if (res.code === 200) {
memberList.value = res.rows
pagination.value.itemCount = res.total
isInitialized.value = true
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
onMounted(() => {
getMemberList()
})
//
async function handleSetAdmin(item: MemberItem) {
const { id, fileName } = item
const res = await request.post('/communityFile/download', {
communityId: route.query.communityId,
tenantId: route.query.tenantId,
fileId: id,
})
if (res.code === 200) {
const { data } = res
//
const link = document.createElement('a')
link.href = data
link.download = fileName // 使
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
//
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
}).catch(() => {
message.error('复制失败')
})
}
const pictureInput = ref<HTMLInputElement | null>(null)
function handlePictureInput() {
pictureInput.value?.click()
}
//
const allowedFileTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain',
'application/zip',
]
async function handlePictureChange(event: Event) {
const files = (event.target as HTMLInputElement).files
if (!files || files.length === 0)
return
if (files.length > 1) {
message.error('只能选择 1 个文件')
return
}
const file = files[0]
if (!allowedFileTypes.includes(file.type)) {
message.error('只支持 PDF、Word、Excel、PPT、TXT 和 ZIP 文件')
return
}
try {
const res = await uploadFileBatches([file])
;(event.target as HTMLInputElement).value = ''
const { fileName, objectKey, path, size } = res[0]
const res1 = await request.post('/communityFile/upload', {
communityId: route.query.communityId,
tenantId: route.query.tenantId,
fileName,
objectKey,
fileSize: size,
fileUrl: path,
})
if (res1.code === 200) {
message.success('文件上传成功')
params.value.search = ''
params.value.pageNum = 1
pagination.value.page = 1
getMemberList()
}
}
catch (error) {
console.error(error)
message.error('文件上传失败')
}
}
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA] px-10 py-6 flex gap-4">
<div class="bg-white rounded-lg flex-1">
<div class="flex justify-between items-center m-3 text-sm">
<div class="flex items-center gap-2">
<div class="bg-[#328afe] text-white px-3 py-1 rounded-sm cursor-pointer" @click="handlePictureInput">
上传文件
</div>
<div>
<!-- 111 -->
</div>
</div>
<div class="relative flex items-center w-[300px]">
<input
v-model="params.search"
type="text"
placeholder="请输入文件名"
class="w-full h-9 pl-4 pr-14 border border-[#eee] rounded-lg text-sm placeholder-[#878D95] outline-none focus:border-[#3f7ef7] focus:ring-1 focus:ring-[#3f7ef7] transition-colors"
@keyup="handleSearch"
>
<button class="absolute right-3 flex items-center space-x-1 text-[#4A5563]">
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667zM14 14l-2.9-2.9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<!-- <span class="text-sm font-bold">搜索</span> -->
</button>
</div>
</div>
<div class="grid grid-cols-[300px_150px_150px_200px_130px_100px] border-b border-[#eee]">
<div class="p-3 text-[#1f2329] font-medium">
全部文件
</div>
<div class="p-3 text-[#1f2329] font-medium">
文件大小
</div>
<div class="p-3 text-[#1f2329] font-medium">
上传人
</div>
<div class="p-3 text-[#1f2329] font-medium">
上传时间
</div>
<div class="p-3 text-[#1f2329] font-medium">
下载次数
</div>
<div class="p-3 text-[#1f2329] font-medium">
操作
</div>
</div>
<div v-if="loading" class="p-10">
<n-spin />
</div>
<template v-else>
<div
v-for="item in memberList"
:key="item.id"
class="grid grid-cols-[300px_150px_150px_200px_130px_100px] border-b border-[#eee] hover:bg-gray-50"
>
<div class="p-3 flex items-center gap-2">
<!-- <img :src="item.avatar" class="w-8 h-8 rounded-full" :alt="item.nickname"> -->
<span
class="truncate flex-1 cursor-pointer"
:title="item.fileName"
@dblclick="copyToClipboard(item.fileName)"
>
{{ item.fileName }}
</span>
</div>
<div class="p-3 flex items-center">
{{ formatFileSize(item.fileSize) }}
</div>
<div class="p-3 flex items-center">
{{ item.uploadUserName || '-' }}
</div>
<div class="p-3 flex items-center">
{{ item.createTime }}
</div>
<div class="p-3 flex items-center">
{{ item.downloadFileUser.count }}
</div>
<div class="p-3 flex items-center">
<div class="w-[100px]">
<button
class="text-[#1e80ff] hover:text-[#3b8fff]"
@click="handleSetAdmin(item)"
>
下载
</button>
</div>
</div>
</div>
</template>
<div v-if="isInitialized && memberList.length > 0" class="flex justify-center mt-4">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:show-size-picker="false"
@update:page="handlePageChange"
/>
</div>
<div v-else class="flex justify-center mt-10">
暂无数据
</div>
<input
ref="pictureInput"
type="file"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip"
class="hidden"
@change="handlePictureChange"
>
</div>
<div class="w-[300px]">
<PlanetBaseInfo :community-id="route.query.communityId" :tenant-id="route.query.tenantId" />
</div>
<!-- <n-modal
v-model:show="showBlackModal"
:style="{ width: '480px' }"
:mask-closable="false"
class="rounded-lg"
>
<div class="p-6 bg-white rounded-lg">
<div class="text-center text-lg font-medium mb-6">
拉黑成员
</div>
<n-form
ref="formRef"
:model="blackForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
class="space-y-4"
>
<n-form-item label="拉黑时长" path="blackDay">
<div class="flex items-center gap-2">
<n-input-number
v-model:value="blackForm.blackDay"
placeholder="请输入"
:min="1"
@update:value="() => formRef.value?.validate(['blackDay'])"
/>
<span class="text-sm"></span>
<div class="relative group">
<div class="i-carbon-information text-gray-400 cursor-help" />
<div class="absolute left-6 top-0 hidden group-hover:block w-64 p-3 bg-black bg-opacity-75 text-white text-xs rounded-lg">
拉黑后该成员将不能对星球进行内容发布点赞评论内容等操作拉黑时长结束后将自动解除
</div>
</div>
</div>
</n-form-item>
<n-form-item label="拉黑原因" path="blackReason">
<n-input
v-model:value="blackForm.blackReason"
type="textarea"
placeholder="请输入内容"
:autosize="{ minRows: 3, maxRows: 5 }"
class="w-full"
/>
</n-form-item>
</n-form>
<div class="flex justify-center gap-4 mt-8">
<n-button
class="w-24 h-9 hover:opacity-90"
@click="showBlackModal = false"
>
取消
</n-button>
<n-button
type="primary"
class="w-24 h-9 hover:opacity-90"
:theme-overrides="{
common: {
primaryColor: '#3f7ef7',
primaryColorHover: '#3f7ef7',
},
}"
@click="handleBlack"
>
确定拉黑
</n-button>
</div>
</div>
</n-modal> -->
</div>
</template>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import PlanetBaseInfo from '@/components/PlanetBaseInfo.vue'
import PlanetComment from '@/components/PlanetComment.vue'
import PlanetQuestionComment from '@/components/PlanetQuestionComment.vue'
import PublishContent from '@/components/PublishContent.vue'
import { useUserStore } from '@/stores/user'
import request from '@/utils/request'
import { FolderPlus, ImagePlus } from 'lucide-vue-next'
import { NModal } from 'naive-ui'
import { ref, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userStore = useUserStore()
//
interface CommunityDetail {
id: string
communityName: string
coverImage: string
memberCount: number
contentCount: number
createDays: number
score: number
}
const communityDetail = ref<CommunityDetail>()
const loading = ref(false)
//
async function getCommunityDetail() {
try {
loading.value = true
const res = await request.get('/community/detail', {
params: {
communityId: route.query.communityId,
tenantId: route.query.tenantId,
},
})
if (res.code === 200) {
communityDetail.value = res.data
}
}
catch (err) {
console.error('获取社区详情失败:', err)
}
finally {
loading.value = false
}
}
//
watchEffect(() => {
if (route.query.communityId && route.query.tenantId) {
getCommunityDetail()
}
})
const router = useRouter()
</script>
<template>
<main p="x4 y10" text="center teal-700 dark:gray-200">
<div text-4xl>
<div i-carbon-warning inline-block />
</div>
<div>Not found</div>
<div>
<button text-sm btn m="3 t8" @click="router.back()">
Back
</button>
</div>
</main>
</template>

View File

@ -0,0 +1,388 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui'
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
interface MemberItem {
userId: number | string
id: number | string
tenantId: number | string
avatar: string
nickname: string
role: '星主' | '管理人' | '成员'
type: '付费' | '免费'
joinTime: string
expireTime: string
lastLoginTime: string
isBlack: '0' | '1'
userType: number
communityId: number | string
joinType: string
}
definePageMeta({
layout: 'planet',
})
const route = useRoute()
const message = useMessage()
const memberList = ref<MemberItem[]>([])
const loading = ref(false)
const isInitialized = ref(false)
const params = ref({
orderByColum: '',
pageNum: 1,
pageSize: 10,
communityId: route.query.communityId,
tenantId: route.query.tenantId,
searchContent: '',
})
//
const pagination = ref({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: false,
})
//
const showBlackModal = ref(false)
const blackForm = ref({
blackDay: undefined,
blackReason: '',
userId: '',
})
//
const rules = {
blackDay: {
required: true,
message: '请输入拉黑时长',
trigger: ['blur', 'change'],
type: 'number',
validator: (rule: any, value: any) => {
if (value === undefined || value === null)
return new Error('请输入拉黑时长')
return true
},
},
blackReason: {
required: true,
message: '请输入拉黑原因',
trigger: ['blur', 'change'],
},
}
// ref
const formRef = ref()
//
function handlePageChange(page: number) {
pagination.value.page = page
params.value.pageNum = page
getMemberList()
}
//
function handleSearch(e: KeyboardEvent) {
if (e.key === 'Enter') {
params.value.pageNum = 1
pagination.value.page = 1
getMemberList()
}
}
//
async function getMemberList() {
try {
loading.value = true
const res = await request.post('/communityUser/list', params.value)
if (res.code === 200) {
memberList.value = res.rows
pagination.value.itemCount = res.total
isInitialized.value = true
}
}
catch (error) {
console.error(error)
}
finally {
loading.value = false
}
}
onMounted(() => {
getMemberList()
})
// /
async function handleRemove(item: MemberItem) {
if (item.userType === 2)
return
if (item.isBlack === '1') {
//
try {
const res = await request.post('/communityUser/unBlack', {
communityId: route.query.communityId,
tenantId: route.query.tenantId, // userId
userId: item.userId,
})
if (res.code === 200) {
message.success('解除拉黑成功')
params.value.pageNum = 1
pagination.value.page = 1
getMemberList()
}
}
catch (error) {
console.error(error)
}
}
else {
//
blackForm.value = {
blackDay: undefined,
blackReason: '',
userId: item.userId,
}
showBlackModal.value = true
}
}
//
async function handleBlack() {
try {
await formRef.value?.validate()
const res = await request.post('/communityUser/black', {
communityId: route.query.communityId,
tenantId: route.query.tenantId,
userId: blackForm.value.userId,
blackDay: blackForm.value.blackDay,
blackReason: blackForm.value.blackReason,
})
if (res.code === 200) {
message.success('拉黑成功')
showBlackModal.value = false
params.value.pageNum = 1
pagination.value.page = 1
getMemberList()
}
}
catch (error) {
console.error(error)
}
}
//
async function handleSetAdmin(item: MemberItem) {
if (item.userType === 2)
return
const { tenantId, communityId, userId } = item
try {
const res = await request.post('/communityUser/manage', {
communityId,
tenantId,
userId,
})
if (res.code === 200) {
message.success('设置成功')
params.value.pageNum = 1
pagination.value.page = 1
getMemberList()
}
}
catch (error) {
console.error(error)
}
}
//
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(() => {
message.success('复制成功')
}).catch(() => {
message.error('复制失败')
})
}
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA] px-10 py-6 flex gap-4">
<div class="bg-white rounded-lg flex-1">
<div class="flex justify-between items-center m-3 text-sm">
<div class="bg-[#328afe] text-white px-3 py-1 rounded-sm cursor-pointer">
分享星球
</div>
<div class="relative flex items-center w-[300px]">
<input
v-model="params.searchContent"
type="text"
placeholder="请输入用户名称"
class="w-full h-9 pl-4 pr-14 border border-[#eee] rounded-lg text-sm placeholder-[#878D95] outline-none focus:border-[#3f7ef7] focus:ring-1 focus:ring-[#3f7ef7] transition-colors"
@keyup="handleSearch"
>
<button class="absolute right-3 flex items-center space-x-1 text-[#4A5563]">
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667zM14 14l-2.9-2.9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<!-- <span class="text-sm font-bold">搜索</span> -->
</button>
</div>
</div>
<div class="grid grid-cols-[200px_120px_120px_200px_200px_200px] border-b border-[#eee]">
<div class="p-3 text-[#1f2329] font-medium">
全部成员
</div>
<div class="p-3 text-[#1f2329] font-medium">
权限
</div>
<div class="p-3 text-[#1f2329] font-medium">
加入类型
</div>
<div class="p-3 text-[#1f2329] font-medium">
首次加入时间
</div>
<div class="p-3 text-[#1f2329] font-medium">
到期时间
</div>
<div class="p-3 text-[#1f2329] font-medium">
操作
</div>
</div>
<div v-if="loading" class="p-10">
<n-spin />
</div>
<template v-else>
<div
v-for="item in memberList"
:key="item.id"
class="grid grid-cols-[200px_120px_120px_200px_200px_200px] border-b border-[#eee] hover:bg-gray-50"
>
<div class="p-3 flex items-center gap-2 w-[200px]">
<img :src="item.avatar" class="w-8 h-8 rounded-full" :alt="item.nickname">
<span
class="truncate flex-1 cursor-pointer"
title="双击复制"
@dblclick="copyToClipboard(item.nickName)"
>
{{ item.nickName }}
</span>
</div>
<div class="p-3 flex items-center">
{{ item.userType === 0 ? '成员' : item.userType === 1 ? '管理员' : '群主' }}
</div>
<div class="p-3 flex items-center">
{{ item.userType !== 2 ? item.joinType : '-' }}
</div>
<div class="p-3 flex items-center">
{{ item.startTime || '-' }}
</div>
<div class="p-3 flex items-center">
{{ item.endTime || '-' }}
</div>
<div class="p-3 w-[300px] flex items-center">
<div class="w-[100px]">
<button
class="text-[#1e80ff] hover:text-[#3b8fff]"
@click="handleSetAdmin(item)"
>
{{ item.userType === 2 ? '-' : item.userType === 0 ? '设为管理员' : '取消管理员' }}
</button>
</div>
<div class="w-[100px]">
<button
class="text-[#f85149] hover:text-[#ff6b64]"
@click="handleRemove(item)"
>
{{ item.userType === 2 ? '-' : item.isBlack === '0' ? '拉黑' : '解除拉黑' }}
</button>
</div>
</div>
</div>
</template>
<div v-if="isInitialized" class="flex justify-center mt-4">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:show-size-picker="false"
@update:page="handlePageChange"
/>
</div>
<n-modal
v-model:show="showBlackModal"
:style="{ width: '480px' }"
:mask-closable="false"
class="rounded-lg"
>
<div class="p-6 bg-white rounded-lg">
<div class="text-center text-lg font-medium mb-6">
拉黑成员
</div>
<n-form
ref="formRef"
:model="blackForm"
:rules="rules"
label-placement="left"
label-width="auto"
require-mark-placement="right-hanging"
class="space-y-4"
>
<n-form-item label="拉黑时长" path="blackDay">
<div class="flex items-center gap-2">
<n-input-number
v-model:value="blackForm.blackDay"
placeholder="请输入"
:min="1"
@update:value="() => formRef.value?.validate(['blackDay'])"
/>
<span class="text-sm"></span>
<div class="relative group">
<div class="i-carbon-information text-gray-400 cursor-help" />
<div class="absolute left-6 top-0 hidden group-hover:block w-64 p-3 bg-black bg-opacity-75 text-white text-xs rounded-lg">
拉黑后该成员将不能对星球进行内容发布点赞评论内容等操作拉黑时长结束后将自动解除
</div>
</div>
</div>
</n-form-item>
<n-form-item label="拉黑原因" path="blackReason">
<n-input
v-model:value="blackForm.blackReason"
type="textarea"
placeholder="请输入内容"
:autosize="{ minRows: 3, maxRows: 5 }"
class="w-full"
/>
</n-form-item>
</n-form>
<div class="flex justify-center gap-4 mt-8">
<n-button
class="w-24 h-9 hover:opacity-90"
@click="showBlackModal = false"
>
取消
</n-button>
<n-button
type="primary"
class="w-24 h-9 hover:opacity-90"
:theme-overrides="{
common: {
primaryColor: '#3f7ef7',
primaryColorHover: '#3f7ef7',
},
}"
@click="handleBlack"
>
确定拉黑
</n-button>
</div>
</div>
</n-modal>
</div>
<div class="w-[300px]">
<PlanetBaseInfo :community-id="route.query.communityId" :tenant-id="route.query.tenantId" />
</div>
</div>
</template>

View File

@ -0,0 +1,215 @@
<script setup lang="ts">
import planetBg from '@/assets/img/planetBg.png'
import { NConfigProvider, NImage, NMessageProvider } from 'naive-ui'
import { onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const message = useMessage()
const route = useRoute()
const router = useRouter()
// history state
const params = ref({
communityId: '',
tenantId: '',
userId: '',
})
onMounted(() => {
const state = history.state
if (state && state.communityId && state.tenantId) {
params.value = {
communityId: state.communityId,
tenantId: state.tenantId,
userId: state.userId,
}
}
else {
// state退 URL
params.value = {
communityId: route.query.communityId as string,
tenantId: route.query.tenantId as string,
userId: route.query.userId as string,
}
}
getCommunityDetail()
getUserInfo()
getAttention()
})
const userStore = useUserStore()
definePageMeta({
layout: 'planet',
})
const communityDetail = ref({})
async function getCommunityDetail() {
try {
const res = await request.get(`/community/detail?communityId=${params.value.communityId}&tenantId=${params.value.tenantId}`)
if (res.code === 200) {
communityDetail.value = res.data
}
}
catch (error) {
console.error(error)
}
}
//
const userInfo = ref({})
async function getUserInfo() {
try {
const res = await request.get(
`/system/user/selectUserById?id=${params.value.userId}`,
)
if (res.code === 200) {
userInfo.value = res.data
}
}
catch (error) {
console.log(error)
}
}
//
const selectUserInfo = ref({})
async function getAttention() {
try {
const res = await request.get(`/attention/selectUserInfo?userId=${params.value.userId}`)
if (res.code === 200) {
selectUserInfo.value = res.data
}
}
catch (err) {
console.log(err)
}
}
async function handleJoin() {
try {
const data = {
communityId: params.value.communityId,
tenantId: params.value.tenantId,
}
const res = await request.post(`/community/join`, data)
if (res.code === 200) {
message.success('加入成功')
setTimeout(() => {
router.push(`/planet-detail?communityId=${params.value.communityId}&tenantId=${params.value.tenantId}`)
}, 1000)
}
}
catch (err) {
console.log(err)
}
}
//
// async function getAttention() {
// try {
// const res = await request.get('/attention/selectUserInfo')
// if (res.code === 200) {
// selectUserInfo.value = res.data
// }
// }
// catch (err) {
// console.error(err)
// }
// }
// getAttention()
</script>
<template>
<div class="min-h-screen bg-[#F7F8FA]">
<div class="relative">
<!-- 背景图片区域 -->
<div class="h-[300px] w-full bg-[#E8F3FF]">
<img :src="planetBg" alt="背景图片" class="w-full h-full object-cover">
</div>
<!-- 主要内容区域 -->
<div class="absolute inset-x-0 top-[30px]">
<div class="max-w-[1140px] mx-auto">
<!-- 用户基本信息 -->
<client-only>
<div class="flex items-center gap-4 mb-6">
<img :src="communityDetail.imageUrl" class="w-[224px] h-[224px] rounded" alt="头像">
<div class="flex flex-col justify-between h-[224px] w-full">
<div class="flex flex-col gap-3">
<div class="text-[20px] font-medium text-[#192029]">
{{ communityDetail.communityName }}
</div>
<div class="text-[14px] text-[#878d95]">
最新更新时间: {{ communityDetail.lastUpdateTime }}
</div>
<div class="text-[14px] flex gap-8">
<div class="flex items-center gap-3">
<img v-for="(item, index) in communityDetail.avatarList" :key="index" :src="item" class="w-[20px] h-[20px] rounded-full" alt="头像">
<div>
成员数量: {{ communityDetail.userNum }}
</div>
</div>
<div>
累计更新内容: {{ communityDetail.updateNum }}
</div>
<div>
星球创建天数: {{ communityDetail.createDay }}
</div>
</div>
</div>
<div class="flex justify-between p-2 rounded bg-white items-center">
<div class="flex items-center gap-2 text-[#fc4141]">
<span class="font-bold text-[28px]">{{ communityDetail.price ? communityDetail.price : '免费' }}</span> <span v-if="communityDetail.price" class="text-[16px]"></span>
</div>
<div
class="text-white text-[14px] flex items-center justify-center cursor-pointer w-[128px] h-[40px] bg-[linear-gradient(207deg,#3BEDFF_0%,#328AFE_100%)] rounded-lg"
@click="handleJoin"
>
立即加入
</div>
</div>
</div>
</div>
</client-only>
<div class="max-w-[1140px] mx-auto bg-white rounded-lg p-4">
<div class="text-[20px] pb-4">
星球概况
</div>
<div class="flex gap-4 bg-[#F9FBFF] rounded-lg p-2">
<div class="w-[80px] h-[80px] rounded">
<img class="rounded-full" :src="userInfo.avatar" alt="星球概况">
</div>
<div class="flex flex-col justify-between py-2">
<div class="flex gap-4">
<div class="text-[20px] font-medium text-[#192029]">
{{ userInfo.nickName }}
</div>
<!-- <div>
关注
</div> -->
</div>
<div class="flex gap-4">
<div>{{ selectUserInfo.bean }}粉丝</div>
<div>{{ selectUserInfo.attention }}关注</div>
<div>{{ Number(selectUserInfo.imageLikeNum) + Number(selectUserInfo.modelLikeNum) }}获赞</div>
</div>
</div>
</div>
<div class="text-[16px] text-[#4a5563] my-5">
{{ communityDetail.description }}
</div>
<div>
<div class="font-bold text-[20px]">
付费通知
</div>
<div class="bg-[#F9FBFF] rounded-lg p-4 mt-2">
<div>1付费后你可以使用当前付款的帐号在有效期内查看参与内容互动向星主群成员提问 </div>
<div>2本星球由星主自行创建加入前请确认风险平台不提供相关保证若发现违反法律法规的星球请勿加入并投诉</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -3,16 +3,14 @@ import { isAmount } from '@/utils/index.ts'
import { Close } from '@vicons/ionicons5'
import { ref } from 'vue'
const loading = ref(false)
const activeTab = ref('withdrawDetail')
const showWalletModal = ref(false)
function showModal() {
showWalletModal.value = true
}
const message = useMessage()
function closeWalletModal() {
showWalletModal.value = false
needWalletNum.value = 0
}
// const allDetailList = ref([]);
//
const integralGold = ref({})
@ -32,10 +30,13 @@ getIntegralGold()
//
const needWalletNum = ref(0)
async function handleWallet() {
if (loading.value)
return
if (isAmount(needWalletNum.value)) {
if (needWalletNum.value > integralGold.value.wallet)
return message.warning('可提现金额不足')
try {
loading.value = true
const res = await request.get(`/ali/pay/fetch?amount=${needWalletNum.value}`)
if (res.code === 200) {
message.success('提现成功!')
@ -47,12 +48,20 @@ async function handleWallet() {
catch (err) {
console.log(err)
}
finally {
loading.value = false
}
}
else {
message.warning('请输入正确的金额')
}
}
function closeWalletModal() {
showWalletModal.value = false
needWalletNum.value = 0
}
//
const totalAmount = ref(0)
async function getTotalAmount() {
@ -228,18 +237,21 @@ getWithdrawDetail()
可提现金额: ¥ {{ integralGold.wallet || 0 }}
</div>
<div class="mt-6">
<n-spin :show="loading">
<n-input-number
v-model:value="needWalletNum"
placeholder="输入要提现的金额"
:precision="2"
clearable
/>
</n-spin>
</div>
</div>
<div class="flex justify-center mt-6">
<n-button type="error" @click="closeWalletModal">
取消
</n-button>
<!-- :loading="loading" -->
<n-button type="primary" class="ml-8" @click="handleWallet">
确定
</n-button>

View File

@ -184,9 +184,9 @@ async function onLike() {
//
const reportParams = ref({
reportId: undefined,
reportType: undefined,
text: '',
type: 3,
type: 1,
})
const reportList = ref([])
async function getDictType() {
@ -202,7 +202,7 @@ async function getDictType() {
}
getDictType()
function handleChange(item) {
reportParams.value.reportId = item.dictValue
reportParams.value.reportType = item.dictValue
if (item.dictValue !== '5') {
reportParams.value.text = ''
}
@ -214,8 +214,9 @@ function closeReport() {
}
async function onReport() {
if (reportParams.value.reportId !== undefined) {
if (reportParams.value.reportType !== undefined) {
reportParams.value.productId = detailsInfo.value.id
reportParams.value.productName = detailsInfo.value.workflowName
try {
const res = await request.post('/report/addReport', reportParams.value)
if (res.code === 200) {
@ -601,7 +602,7 @@ async function handelDown() {
:key="index"
class="m-4 text-[#fff000]"
size="large"
:checked="reportParams.reportId === item.dictValue"
:checked="reportParams.reportType === item.dictValue"
value="Definitely Maybe"
name="basic-demo"
@change="handleChange(item)"
@ -609,7 +610,7 @@ async function handelDown() {
{{ item.dictLabel }}
</n-radio>
<n-input
v-if="reportParams.reportId !== undefined && reportParams.reportId === '5'"
v-if="reportParams.reportType !== undefined && reportParams.reportType === '5'"
v-model:value="reportParams.text"
placeholder="点击输入"
type="textarea"
@ -621,7 +622,7 @@ async function handelDown() {
<div
class="mt-4 w-[100%] h-10 flex rounded-lg text-white items-center justify-center cursor-pointer"
:class="[
reportParams.reportId !== undefined
reportParams.reportType !== undefined
? 'bg-[#4c79ee]'
: 'bg-[#cccccc]',
]"

View File

@ -29,6 +29,7 @@ export const useUserStore = defineStore('user', () => {
}
}
async function getUserInfo() {
if (token.value) {
const res = await request.get('/system/user/selectUserById', {
token: token.value,
})
@ -37,6 +38,7 @@ export const useUserStore = defineStore('user', () => {
setUserInfo(res.data)
}
}
}
// 登出
function logout() {
isLoggedIn.value = false

View File

@ -7,19 +7,30 @@ export async function formatDate(timestamp: string) {
}
export function formatFileSize(bytes: number = 0, decimals = 2) {
if (bytes === 0) return '0 B';
if (bytes === 0)
return '0 B'
const k = 1024; // 1 KB = 1024 B
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); // 计算单位索引
const k = 1024 // 1 KB = 1024 B
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k)) // 计算单位索引
// 转换为合适的单位并保留指定小数位数
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
return `${Number.parseFloat((bytes / k ** i).toFixed(decimals))} ${sizes[i]}`
}
export function isAmount(str: any) {
const amountRegex = /^\d+(\.\d{1,2})?$/;
return amountRegex.test(str);
const amountRegex = /^\d+(\.\d{1,2})?$/
return amountRegex.test(str)
}
/**
*
* @param amount
* @returns
*/
export function formatAmount(amount: number | undefined): string {
if (amount === undefined || amount === null)
return '-'
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// function isAmount(str) {

View File

@ -66,10 +66,9 @@ class RequestHttp {
modalStore.showLoginModal()
const userStore = useUserStore()
try {
// await request.post('/logout')
// userStore.logout()
// navigateTo('/model-square')
await request.post('/logout')
userStore.logout()
navigateTo('/model-square')
}
catch (error) {
console.log(error)
@ -177,7 +176,7 @@ class RequestHttp {
}
const request = new RequestHttp({
baseURL: import.meta.env.VITE_NUXT_ENV || '/api',
baseURL: import.meta.env.NODE_ENV === 'production' ? `${import.meta.env.VITE_NUXT_ENV}/api` : '/api',
timeout: 20000,
headers: {
'Content-Type': 'application/json',

View File

@ -87,7 +87,7 @@ export default defineNuxtConfig({
'/api': {
// target: 'http://113.45.190.154:8080', // 线上
// target: 'http://192.168.2.29:8080', // 代
target: 'http://192.168.2.4:8080', // 嗨
target: 'http://192.168.2.21:8080', // 嗨
// target: 'https://2d1a399f.r27.cpolar.top', // 嗨
changeOrigin: true,
prependPath: true,

View File

@ -5,9 +5,11 @@
"private": true,
"packageManager": "pnpm@9.15.1",
"scripts": {
"build:production": "nuxi build --dotenv .env.production",
"build:dev": "nuxi build --dotenv .env.dev",
"build": "nuxi build --dotenv .env.pro",
"dev:pwa": "VITE_PLUGIN_PWA=true nuxi dev",
"dev": "nuxi dev --port 3080",
"dev": "nuxi dev --port 8080",
"generate": "nuxi generate",
"prepare": "nuxi prepare",
"start": "node .output/server/index.mjs",