Commit 37e7420a by lijiabin

【需求 17679】 feat: 继续优化等

parent 39a2c4f6
import service from '@/utils/request/index'
import type { AddOrUpdateCaseDto } from './types'
import type { BooleanFlag } from '@/constants'
// 其他用户页面数据
/**
* 获取其他用户的用户信息
*/
export const getOtherUserData = (data: { userId: string; isReal: BooleanFlag }) => {
return service.request<string>({
url: '/api/personalCenter/userData',
method: 'POST',
data,
})
}
/**
* 显示其他用户的发帖数据
*/
export const getOtherUserPostData = (data: {
userId: string
isReal: BooleanFlag
current: number
size: number
}) => {
return service.request({
url: '/api/personalCenter/userData',
method: 'POST',
data,
})
}
/**
* 添加或更新案例库DTO
*/
import { TagTypeEnum, TagLevelEnum, BooleanFlag, ReleaseStatusTypeEnum } from '@/constants'
export type TagItemDto = {
tagId: number
type: TagTypeEnum
keywordType: TagLevelEnum
}
export interface AddOrUpdateCaseDto {
id?: number
content: string
title: string
tagRelationDtoList: TagItemDto[]
isSync: BooleanFlag
releaseStatus: ReleaseStatusTypeEnum
}
......@@ -58,7 +58,7 @@
</h1>
<!-- 文章内容 -->
<div class="prose prose-lg max-w-none">
<div v-if="!isHtml" class="prose prose-lg max-w-none">
<div class="text-gray-700 leading-relaxed space-y-4">
{{ articleDetail?.content }}
</div>
......@@ -83,6 +83,8 @@
</div>
</div>
<div v-else v-html="articleDetail.content" />
<!-- 标签 -->
<div class="flex flex-wrap gap-2 mt-6">
<span
......@@ -109,4 +111,8 @@ const { articleDetail } = defineProps<{
const articleType = computed(() => {
return articleTypeListOptions.find((item) => item.value === articleDetail.type)?.label
})
const isHtml = computed(() => {
return articleDetail.content.includes('<') || articleDetail.content.includes('</')
})
</script>
......@@ -100,7 +100,12 @@
>
<div class="p-4 transition-colors">
<div class="flex gap-3">
<img :src="item.avatar" alt="" class="w-10 h-10 rounded-full object-cover" />
<img
@click="handleUserInfo(item)"
:src="item.avatar"
alt=""
class="w-10 h-10 rounded-full object-cover cursor-pointer"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-gray-800">{{ item.replyUser }}</span>
......@@ -146,7 +151,12 @@
:key="child.id"
class="flex gap-2 p-3 rounded-lg"
>
<img :src="child.avatar" alt="" class="w-8 h-8 rounded-full object-cover" />
<img
@click="handleUserInfo(child)"
:src="child.avatar"
alt=""
class="w-8 h-8 rounded-full object-cover cursor-pointer"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-sm text-gray-800"
......@@ -292,6 +302,7 @@ const { id, defaultSize = 10 } = defineProps<{
const emit = defineEmits<{
(e: 'commentSuccess'): void
}>()
const router = useRouter()
const total = defineModel<number>('total', { required: true, default: 0 })
......@@ -463,6 +474,11 @@ const getCurrentChildrenList = (item: CommentItemDto) => {
}
}
const handleUserInfo = (item: CommentItemDto) => {
console.log(item)
router.push(`/otherUserPage/${item.articleId}/1`)
}
defineExpose({
scrollToCommentBox: () => handleBackTop(),
})
......
<template>
<md-editor
v-model="value"
preview-theme="arknights"
:class="!preview && height === 'auto' ? 'md_fixed' : ''"
v-bind="isPreview"
:style="{ height }"
@on-upload-img="handleUpload"
></md-editor>
<!-- 图片预览插件 -->
<ElImageViewer v-if="previewShow" :url-list="previewList" :initial-index="previewIndex" @close="closePreview"></ElImageViewer>
</template>
<script setup lang="ts">
import { uploadImage } from '@/api'
import MdEditor from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
interface IProp {
modelValue?: string
preview?: boolean
height?: string
}
const prop = withDefaults(defineProps<IProp>(), {
modelValue: '',
preview: false,
height: 'auto'
})
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const value = computed({
get() {
return prop.modelValue || ''
},
set(val: string) {
emit('update:modelValue', val)
}
})
// 预览
const isPreview = computed(() => {
if (!prop.preview) return {}
return {
preview: true,
htmlPreview: true,
previewOnly: true
}
})
/**
* 文件上传
*/
type InsertFnType = (urls: string[]) => void
const handleUpload = async (files: File[], callback: InsertFnType) => {
const res = await uploadImage(files)
const url = res.data.originList
// 最后插入图片
callback(url)
}
/**
* 图片预览
*/
const previewShow = ref(false)
const previewList = ref<string[]>([])
const previewIndex = ref(0)
const req = /<img[^>]+src=['"]([^'"]+)['"]+/g
watch(
() => prop.modelValue,
val => {
let temp: RegExpExecArray | null
while ((temp = req.exec(val)) != null) {
previewList.value.push(temp[1])
}
}
)
MdEditor.config({
markedRenderer(renderer) {
renderer.image = (href, title = '', desc = '') => {
if (!href) return ''
let previewHref = href
if (href.indexOf('-small') > -1) {
previewHref = href.replace('-small', '')
} else if (href.indexOf('-mid') > -1) {
previewHref = href.replace('-mid', '')
}
previewList.value.push(previewHref)
return `<img src="${href}" title="${title}" alt="${desc}" style="cursor: zoom-in" >`
}
return renderer
}
})
onMounted(() => {
nextTick(() => {
document.querySelector('.md-preview')?.addEventListener('click', e => {
const el = e.target as HTMLImageElement
if (el?.nodeName == 'IMG') {
let imgSrc = el.src
if (imgSrc.indexOf('-small') > -1) {
imgSrc = imgSrc.replace('-small', '')
} else if (imgSrc.indexOf('-mid') > -1) {
imgSrc = imgSrc.replace('-mid', '')
}
const index = previewList.value.findIndex(v => v === imgSrc)
if (index >= 0) {
previewIndex.value = index
} else {
previewList.value.push(imgSrc)
previewIndex.value = previewList.value.length - 1
}
showPreview()
}
})
})
})
const showPreview = () => {
// PC端预览
previewShow.value = true
}
const closePreview = () => {
previewShow.value = false
}
</script>
<style lang="scss" scoped>
.md_fixed {
overflow: visible;
min-height: 400px;
}
</style>
<style lang="scss">
.md {
font-family: 'Segoe UI Emoji', 'Apple Color Emoji', -apple-system, BlinkMacSystemFont, Segoe UI Variable, Segoe UI, system-ui,
ui-sans-serif, Helvetica, Arial, sans-serif;
}
.md_fixed {
.md-toolbar-wrapper {
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
box-shadow: 0 6px 6px -6px rgba(0, 0, 0, 0.1);
overflow: visible;
height: auto;
}
.md-toolbar-wrapper .md-toolbar {
position: relative;
min-width: 0;
flex-wrap: wrap;
}
}
.md_fixed .md-toolbar-wrapper .md-toolbar-left,
.md-toolbar-wrapper .md-toolbar-right {
flex-wrap: wrap;
}
.md-preview {
line-height: 1.5;
img {
max-width: 100%;
}
}
</style>
......@@ -102,6 +102,12 @@ export const constantsRoute = [
},
],
},
// 点进去其他用户的个人中心
{
path: 'otherUserPage/:id/:isShiming',
name: 'CultureOtherUserPage',
component: () => import('@/views/otherUserPage/index.vue'),
},
{
path: 'videoDetail/:id',
name: 'CultureVideoDetail',
......
......@@ -27,7 +27,9 @@
<!-- 内容摘要 -->
<div class="my-2 space-y-1">
<p class="text-gray-600 text-sm sm:text-base leading-relaxed line-clamp-1">
<p
class="text-gray-600 text-sm sm:text-base leading-relaxed line-clamp-1 break-all"
>
{{ item.content }}
</p>
</div>
......
<template>
<div class="min-h-screen bg-gray-50 rounded-lg">
<!-- 顶部背景区域 -->
<div class="box relative h-200px bg-gradient-to-r from-purple-400 via-pink-300 to-blue-300">
<!-- 顶部操作按钮 -->
<div class="absolute top-4 right-4 flex gap-2">
<el-button
v-if="!userInfo.isOfficialAccount"
type="info"
plain
size="small"
@click="handleClearCache"
>清除缓存</el-button
>
<el-button
v-if="officialAccountList.length"
type="info"
plain
size="small"
@click="handleSwitchAccount"
>切换账号</el-button
>
<el-button
v-if="userInfo.isOfficialAccount"
type="info"
plain
size="small"
@click="handleBackUser"
>返回个人账号</el-button
>
<!-- v-if="userInfo.isAdmin" 暂时不加权限 -->
<el-button type="primary" plain size="small" @click="handleAdmin">后台管理</el-button>
</div>
</div>
<!-- 主要内容区域 -->
<div class="relative -mt-20 p-6">
<div class="flex gap-6">
<!-- 左侧个人信息卡片 -->
<div class="w-full">
<!-- 个人信息卡片 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-4">
<div class="flex items-start gap-4">
<el-avatar
:size="80"
:src="userInfo?.avatar"
class="border-4 border-white shadow-lg"
/>
<div class="flex-1">
<h2 class="text-xl font-semibold text-gray-800 mb-1">{{ userInfo?.name }}</h2>
<p class="text-gray-500 text-sm mb-2">{{ userInfo?.signature }}</p>
<el-button type="warning" size="small" plain @click="handleEdit">
<el-icon><Edit /></el-icon>
修改资料
</el-button>
</div>
</div>
</div>
<!-- 左侧菜单 ——个人菜单 -->
<div class="bg-white rounded-lg shadow-sm mb-4">
<div v-if="!list.length" class="flex flex-col items-center justify-center h-64">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<el-icon class="text-2xl text-gray-300"><Document /></el-icon>
</div>
<div class="text-gray-500 text-lg mb-2">暂无内容</div>
</div>
<div v-else class="space-y-4">
<div
v-for="item in list"
:key="item.id"
class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
>
<div class="flex-1 min-w-0">
<div class="text-gray-900 font-medium truncate">{{ item.title }}</div>
<div class="text-gray-500 text-sm mt-1 truncate">
<span class="mr-2">
{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</span>
<span class="mr-2">评论 {{ item.collectionCount }}</span>
</div>
</div>
<div class="flex items-center text-gray-400 text-sm ml-4">
<el-button type="primary" link @click="openArticleDetail(item.id)">
去回复
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<EditUserInfo ref="editUserInfoRef" />
</div>
</template>
<script lang="tsx" setup>
import { Document } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import EditUserInfo from './components/editUserInfo.vue'
import { generateLoginKey, hasOfficialAccount } from '@/api'
import type { OfficialAccountItemDto } from '@/api/user/types'
import { wxLogin } from '@/utils/wxUtil'
import dayjs from 'dayjs'
const route = useRoute()
const list = [{}, {}]
const editUserInfoRef = useTemplateRef<InstanceType<typeof EditUserInfo>>('editUserInfoRef')
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const handleEdit = () => {
console.log('修改资料')
editUserInfoRef.value?.open({
hiddenAvatar: userInfo.value.avatar,
hiddenName: userInfo.value.name,
signature: '',
})
}
const officialAccountList = ref<OfficialAccountItemDto[]>([])
const getIsOfficial = async () => {
const { data } = await hasOfficialAccount()
console.log(data)
officialAccountList.value = data
}
onMounted(() => {
getOtherUserData({
userId: route.params.userId as string,
isReal: 1,
})
getOtherUserPostData({
userId: route.params.userId as string,
isReal: 1,
current: 1,
size: 10,
})
})
const handleSwitchAccount = async () => {
const selectedEmail = ref('')
ElMessageBox({
title: '切换官方账号',
message: () => (
<div class="flex flex-wrap gap-3 p-4">
{officialAccountList.value.map((item) => {
return (
<el-card
key={item.email}
shadow="hover"
class={[
'flex items-center justify-center cursor-pointer transition-all relative',
'hover:shadow-lg border-2 rounded-lg',
selectedEmail.value === item.email
? 'border-blue-500! bg-blue-50!'
: 'border-gray-200!',
]}
style="width: 160px; padding: 16px"
onClick={() => {
selectedEmail.value = item.email
}}
>
{/* 右上角单选框 */}
<div class="absolute top-2 right-2">
<el-checkbox
modelValue={selectedEmail.value}
true-value={item.email}
size="large"
/>
</div>
{/* 内容区域 */}
<div class="flex flex-col items-center gap-3 w-full">
<el-avatar size={80} src={item.avatar} />
<div class="flex flex-col items-center gap-1 w-full">
<span class="text-sm font-medium text-gray-800 truncate w-full text-center">
{item.officialTag}
</span>
</div>
</div>
</el-card>
)
})}
</div>
),
showCancelButton: true,
confirmButtonText: '确认切换',
cancelButtonText: '取消',
customClass: 'min-w-740px',
beforeClose: async (action, instance, done) => {
if (action === 'confirm') {
if (!selectedEmail.value) return ElMessage.warning('请选择要切换的账号')
instance.confirmButtonLoading = true
try {
const { data } = await generateLoginKey({
timestamp: Date.now(),
type: 2,
userId: userInfo.value.userId,
cutEmail: selectedEmail.value,
})
console.log(data)
sessionStorage.clear()
await userStore.getUserInfoByCode({
code: data,
isCodeLogin: 1,
cutEmail: selectedEmail.value,
})
done()
window.location.reload()
} catch (error) {
console.log(error)
} finally {
instance.confirmButtonLoading = false
}
} else {
done()
}
},
})
}
const handleBackUser = () => {
sessionStorage.clear()
wxLogin(route.fullPath)
}
const handleAdmin = () => {
window.open('/backend')
}
const handleClearCache = async () => {
// 获取登陆的key
const { data } = await generateLoginKey({
timestamp: Date.now(),
type: 1,
userId: userInfo.value.userId,
})
// 清理sessionStorage
sessionStorage.clear()
await userStore.getUserInfoByCode({
code: data,
isCodeLogin: 1,
})
window.location.reload()
}
onMounted(() => {
getIsOfficial()
})
</script>
<style scoped>
.box {
background-image: url('http://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/2022/10/13/1665643787806.jpg');
}
/* 如果需要额外的样式可以在这里添加 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment