Commit 5d016c03 by lijiabin

【需求 21402】 wip: 暂时加入私信页面

parent d15d4d58
...@@ -189,6 +189,12 @@ export const constantsRoute = [ ...@@ -189,6 +189,12 @@ export const constantsRoute = [
name: 'Auction', name: 'Auction',
component: () => import('@/views/auction/index.vue'), component: () => import('@/views/auction/index.vue'),
}, },
// 我的私信
{
path: 'selfMessage',
name: 'CultureSelfMessage',
component: () => import('@/views/selfMessage/index.vue'),
},
], ],
}, },
......
<script setup lang="ts">
import CommentBox from '@/components/common/CommentBox/index.vue'
import { useUserStore } from '@/stores'
import dayjs from 'dayjs'
import { push } from 'notivue'
import { storeToRefs } from 'pinia'
import BackButton from '@/components/common/BackButton/index.vue'
import type { ScrollbarInstance } from 'element-plus'
type MessageCategory = 'anonymous' | 'realname'
type MessageSender = 'self' | 'other'
interface ChatMessage {
id: number
sender: MessageSender
text: string
images: string[]
createdAt: string
}
interface ConversationItem {
id: number
category: MessageCategory
name: string
avatar: string
badge: string
subtitle: string
sourceTitle: string
sourceTypeLabel: string
onlineStatus: string
unread: number
isPinned?: boolean
messages: ChatMessage[]
}
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const categoryTabs = [
{
key: 'anonymous' as MessageCategory,
label: '匿名私信',
description: '适用于除问吧、实践、专访文章以外的内容',
},
{
key: 'realname' as MessageCategory,
label: '实名私信',
description: '适用于问吧、实践、专访文章',
},
]
const conversationList = ref<ConversationItem[]>([
{
id: 1,
category: 'realname',
name: '陈菲',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei',
badge: '实名',
subtitle: '你上次提到的培训案例已经补充好了',
sourceTitle: '实践 · 从复盘到落地的团队协作方法',
sourceTypeLabel: '实践',
onlineStatus: '29分钟前活跃',
unread: 2,
isPinned: true,
messages: [
{
id: 101,
sender: 'other',
text: '你好,这篇实践文章里提到的复盘模板能发我看一下吗?',
images: [],
createdAt: '2026-04-09 09:18',
},
{
id: 102,
sender: 'self',
text: '可以,我整理完私信给你。',
images: [],
createdAt: '2026-04-09 09:22',
},
{
id: 103,
sender: 'other',
text: '好的,我也很想看看你们怎么把共识拆成执行动作。',
images: [],
createdAt: '2026-04-09 09:24',
},
],
},
{
id: 2,
category: 'anonymous',
name: '树洞来信',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA',
badge: '匿名',
subtitle: '谢谢你愿意认真回复我的困惑',
sourceTitle: '案例库 · 一次被看见的小改变',
sourceTypeLabel: '案例',
onlineStatus: '刚刚',
unread: 0,
messages: [
{
id: 201,
sender: 'other',
text: '看到你的评论后想私信你,原来很多人也会经历类似阶段。',
images: [],
createdAt: '2026-04-09 08:31',
},
{
id: 202,
sender: 'self',
text: '会的,所以别急着否定自己,先把能做的小事做起来。',
images: [],
createdAt: '2026-04-09 08:36',
},
],
},
{
id: 3,
category: 'realname',
name: '王守勇',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong',
badge: '实名',
subtitle: '关于专访提到的组织演进,我补了两张图',
sourceTitle: '专访 · 从宏大叙事到实事求是',
sourceTypeLabel: '专访',
onlineStatus: '今天 08:45',
unread: 1,
messages: [
{
id: 301,
sender: 'other',
text: '这两张图是给你补充的版本,可以一起带给同事看。',
images: [
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80',
],
createdAt: '2026-04-09 08:45',
},
],
},
{
id: 4,
category: 'anonymous',
name: '未署名同事',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB',
badge: '匿名',
subtitle: '想继续请教你怎么带新人',
sourceTitle: '视频 · 一次普通晨会为什么能开得更好',
sourceTypeLabel: '视频',
onlineStatus: '昨天',
unread: 0,
messages: [
{
id: 401,
sender: 'other',
text: '我发现自己总在说结论,带新人时对方不太能跟上。',
images: [],
createdAt: '2026-04-08 17:20',
},
{
id: 402,
sender: 'self',
text: '可以先从为什么这么做开始讲,再拆成两个最关键动作。',
images: [],
createdAt: '2026-04-08 17:35',
},
],
},
{
id: 11,
category: 'realname',
name: '陈菲',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei',
badge: '实名',
subtitle: '你上次提到的培训案例已经补充好了',
sourceTitle: '实践 · 从复盘到落地的团队协作方法',
sourceTypeLabel: '实践',
onlineStatus: '29分钟前活跃',
unread: 2,
isPinned: true,
messages: [
{
id: 101,
sender: 'other',
text: '你好,这篇实践文章里提到的复盘模板能发我看一下吗?',
images: [],
createdAt: '2026-04-09 09:18',
},
{
id: 102,
sender: 'self',
text: '可以,我整理完私信给你。',
images: [],
createdAt: '2026-04-09 09:22',
},
{
id: 103,
sender: 'other',
text: '好的,我也很想看看你们怎么把共识拆成执行动作。',
images: [],
createdAt: '2026-04-09 09:24',
},
],
},
{
id: 21,
category: 'anonymous',
name: '树洞来信',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA',
badge: '匿名',
subtitle: '谢谢你愿意认真回复我的困惑',
sourceTitle: '案例库 · 一次被看见的小改变',
sourceTypeLabel: '案例',
onlineStatus: '刚刚',
unread: 0,
messages: [
{
id: 201,
sender: 'other',
text: '看到你的评论后想私信你,原来很多人也会经历类似阶段。',
images: [],
createdAt: '2026-04-09 08:31',
},
{
id: 202,
sender: 'self',
text: '会的,所以别急着否定自己,先把能做的小事做起来。',
images: [],
createdAt: '2026-04-09 08:36',
},
],
},
{
id: 31,
category: 'realname',
name: '王守勇',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong',
badge: '实名',
subtitle: '关于专访提到的组织演进,我补了两张图',
sourceTitle: '专访 · 从宏大叙事到实事求是',
sourceTypeLabel: '专访',
onlineStatus: '今天 08:45',
unread: 1,
messages: [
{
id: 301,
sender: 'other',
text: '这两张图是给你补充的版本,可以一起带给同事看。',
images: [
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80',
],
createdAt: '2026-04-09 08:45',
},
],
},
{
id: 41,
category: 'anonymous',
name: '未署名同事',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB',
badge: '匿名',
subtitle: '想继续请教你怎么带新人',
sourceTitle: '视频 · 一次普通晨会为什么能开得更好',
sourceTypeLabel: '视频',
onlineStatus: '昨天',
unread: 0,
messages: [
{
id: 401,
sender: 'other',
text: '我发现自己总在说结论,带新人时对方不太能跟上。',
images: [],
createdAt: '2026-04-08 17:20',
},
{
id: 402,
sender: 'self',
text: '可以先从为什么这么做开始讲,再拆成两个最关键动作。',
images: [],
createdAt: '2026-04-08 17:35',
},
],
},
{
id: 12,
category: 'realname',
name: '陈菲',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei',
badge: '实名',
subtitle: '你上次提到的培训案例已经补充好了',
sourceTitle: '实践 · 从复盘到落地的团队协作方法',
sourceTypeLabel: '实践',
onlineStatus: '29分钟前活跃',
unread: 2,
isPinned: true,
messages: [
{
id: 101,
sender: 'other',
text: '你好,这篇实践文章里提到的复盘模板能发我看一下吗?',
images: [],
createdAt: '2026-04-09 09:18',
},
{
id: 102,
sender: 'self',
text: '可以,我整理完私信给你。',
images: [],
createdAt: '2026-04-09 09:22',
},
{
id: 103,
sender: 'other',
text: '好的,我也很想看看你们怎么把共识拆成执行动作。',
images: [],
createdAt: '2026-04-09 09:24',
},
],
},
{
id: 22,
category: 'anonymous',
name: '树洞来信',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA',
badge: '匿名',
subtitle: '谢谢你愿意认真回复我的困惑',
sourceTitle: '案例库 · 一次被看见的小改变',
sourceTypeLabel: '案例',
onlineStatus: '刚刚',
unread: 0,
messages: [
{
id: 201,
sender: 'other',
text: '看到你的评论后想私信你,原来很多人也会经历类似阶段。',
images: [],
createdAt: '2026-04-09 08:31',
},
{
id: 202,
sender: 'self',
text: '会的,所以别急着否定自己,先把能做的小事做起来。',
images: [],
createdAt: '2026-04-09 08:36',
},
],
},
{
id: 32,
category: 'realname',
name: '王守勇',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong',
badge: '实名',
subtitle: '关于专访提到的组织演进,我补了两张图',
sourceTitle: '专访 · 从宏大叙事到实事求是',
sourceTypeLabel: '专访',
onlineStatus: '今天 08:45',
unread: 1,
messages: [
{
id: 301,
sender: 'other',
text: '这两张图是给你补充的版本,可以一起带给同事看。',
images: [
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80',
],
createdAt: '2026-04-09 08:45',
},
],
},
{
id: 4,
category: 'anonymous',
name: '未署名同事',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB',
badge: '匿名',
subtitle: '想继续请教你怎么带新人',
sourceTitle: '视频 · 一次普通晨会为什么能开得更好',
sourceTypeLabel: '视频',
onlineStatus: '昨天',
unread: 0,
messages: [
{
id: 401,
sender: 'other',
text: '我发现自己总在说结论,带新人时对方不太能跟上。',
images: [],
createdAt: '2026-04-08 17:20',
},
{
id: 402,
sender: 'self',
text: '可以先从为什么这么做开始讲,再拆成两个最关键动作。',
images: [],
createdAt: '2026-04-08 17:35',
},
],
},
{
id: 13,
category: 'realname',
name: '陈菲',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei',
badge: '实名',
subtitle: '你上次提到的培训案例已经补充好了',
sourceTitle: '实践 · 从复盘到落地的团队协作方法',
sourceTypeLabel: '实践',
onlineStatus: '29分钟前活跃',
unread: 2,
isPinned: true,
messages: [
{
id: 101,
sender: 'other',
text: '你好,这篇实践文章里提到的复盘模板能发我看一下吗?',
images: [],
createdAt: '2026-04-09 09:18',
},
{
id: 102,
sender: 'self',
text: '可以,我整理完私信给你。',
images: [],
createdAt: '2026-04-09 09:22',
},
{
id: 103,
sender: 'other',
text: '好的,我也很想看看你们怎么把共识拆成执行动作。',
images: [],
createdAt: '2026-04-09 09:24',
},
],
},
{
id: 2,
category: 'anonymous',
name: '树洞来信',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA',
badge: '匿名',
subtitle: '谢谢你愿意认真回复我的困惑',
sourceTitle: '案例库 · 一次被看见的小改变',
sourceTypeLabel: '案例',
onlineStatus: '刚刚',
unread: 0,
messages: [
{
id: 201,
sender: 'other',
text: '看到你的评论后想私信你,原来很多人也会经历类似阶段。',
images: [],
createdAt: '2026-04-09 08:31',
},
{
id: 202,
sender: 'self',
text: '会的,所以别急着否定自己,先把能做的小事做起来。',
images: [],
createdAt: '2026-04-09 08:36',
},
],
},
{
id: 3,
category: 'realname',
name: '王守勇',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong',
badge: '实名',
subtitle: '关于专访提到的组织演进,我补了两张图',
sourceTitle: '专访 · 从宏大叙事到实事求是',
sourceTypeLabel: '专访',
onlineStatus: '今天 08:45',
unread: 1,
messages: [
{
id: 301,
sender: 'other',
text: '这两张图是给你补充的版本,可以一起带给同事看。',
images: [
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80',
],
createdAt: '2026-04-09 08:45',
},
],
},
{
id: 4,
category: 'anonymous',
name: '未署名同事',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB',
badge: '匿名',
subtitle: '想继续请教你怎么带新人',
sourceTitle: '视频 · 一次普通晨会为什么能开得更好',
sourceTypeLabel: '视频',
onlineStatus: '昨天',
unread: 0,
messages: [
{
id: 401,
sender: 'other',
text: '我发现自己总在说结论,带新人时对方不太能跟上。',
images: [],
createdAt: '2026-04-08 17:20',
},
{
id: 402,
sender: 'self',
text: '可以先从为什么这么做开始讲,再拆成两个最关键动作。',
images: [],
createdAt: '2026-04-08 17:35',
},
],
},
{
id: 1,
category: 'realname',
name: '陈菲',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei',
badge: '实名',
subtitle: '你上次提到的培训案例已经补充好了',
sourceTitle: '实践 · 从复盘到落地的团队协作方法',
sourceTypeLabel: '实践',
onlineStatus: '29分钟前活跃',
unread: 2,
isPinned: true,
messages: [
{
id: 101,
sender: 'other',
text: '你好,这篇实践文章里提到的复盘模板能发我看一下吗?',
images: [],
createdAt: '2026-04-09 09:18',
},
{
id: 102,
sender: 'self',
text: '可以,我整理完私信给你。',
images: [],
createdAt: '2026-04-09 09:22',
},
{
id: 103,
sender: 'other',
text: '好的,我也很想看看你们怎么把共识拆成执行动作。',
images: [],
createdAt: '2026-04-09 09:24',
},
],
},
{
id: 23,
category: 'anonymous',
name: '树洞来信',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA',
badge: '匿名',
subtitle: '谢谢你愿意认真回复我的困惑',
sourceTitle: '案例库 · 一次被看见的小改变',
sourceTypeLabel: '案例',
onlineStatus: '刚刚',
unread: 0,
messages: [
{
id: 201,
sender: 'other',
text: '看到你的评论后想私信你,原来很多人也会经历类似阶段。',
images: [],
createdAt: '2026-04-09 08:31',
},
{
id: 202,
sender: 'self',
text: '会的,所以别急着否定自己,先把能做的小事做起来。',
images: [],
createdAt: '2026-04-09 08:36',
},
],
},
{
id: 33,
category: 'realname',
name: '王守勇',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong',
badge: '实名',
subtitle: '关于专访提到的组织演进,我补了两张图',
sourceTitle: '专访 · 从宏大叙事到实事求是',
sourceTypeLabel: '专访',
onlineStatus: '今天 08:45',
unread: 1,
messages: [
{
id: 301,
sender: 'other',
text: '这两张图是给你补充的版本,可以一起带给同事看。',
images: [
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80',
],
createdAt: '2026-04-09 08:45',
},
],
},
{
id: 42,
category: 'anonymous',
name: '未署名同事',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB',
badge: '匿名',
subtitle: '想继续请教你怎么带新人',
sourceTitle: '视频 · 一次普通晨会为什么能开得更好',
sourceTypeLabel: '视频',
onlineStatus: '昨天',
unread: 0,
messages: [
{
id: 401,
sender: 'other',
text: '我发现自己总在说结论,带新人时对方不太能跟上。',
images: [],
createdAt: '2026-04-08 17:20',
},
{
id: 402,
sender: 'self',
text: '可以先从为什么这么做开始讲,再拆成两个最关键动作。',
images: [],
createdAt: '2026-04-08 17:35',
},
],
},
])
const activeCategory = ref<MessageCategory>('anonymous')
const activeConversationId = ref<number>(2)
const keyword = ref('')
const draftText = ref('')
const draftImages = ref('')
const sending = ref(false)
const messageListRef = useTemplateRef<HTMLDivElement>('messageListRef')
const conversationBoxRef = useTemplateRef<ScrollbarInstance>('conversationBoxRef')
const filteredConversationList = computed(() => {
const lowerKeyword = keyword.value.trim().toLowerCase()
return conversationList.value
.filter((item) => item.category === activeCategory.value)
.filter((item) => {
if (!lowerKeyword) return true
return [item.name, item.subtitle, item.sourceTitle].some((field) =>
field.toLowerCase().includes(lowerKeyword),
)
})
.sort((a, b) => {
if (!!a.isPinned !== !!b.isPinned) return a.isPinned ? -1 : 1
return (
dayjs(b.messages.at(-1)?.createdAt).valueOf() -
dayjs(a.messages.at(-1)?.createdAt).valueOf()
)
})
})
const activeConversation = computed(() => {
const current = filteredConversationList.value.find(
(item) => item.id === activeConversationId.value,
)
return current || filteredConversationList.value[0] || null
})
const groupedMessages = computed(() => {
if (!activeConversation.value) return []
const groups: Array<{ label: string; items: ChatMessage[] }> = []
activeConversation.value.messages.forEach((message) => {
const label = dayjs(message.createdAt).format('YYYY-MM-DD HH:mm')
const lastGroup = groups.at(-1)
if (!lastGroup || lastGroup.label !== label) {
groups.push({ label, items: [message] })
return
}
lastGroup.items.push(message)
})
return groups
})
const currentUserName = computed(() => userInfo.value?.hiddenName || userInfo.value?.name || '我')
const currentUserAvatar = computed(
() =>
userInfo.value?.hiddenAvatar ||
userInfo.value?.avatar ||
'https://api.dicebear.com/7.x/adventurer/svg?seed=Me',
)
const conversationStats = computed(() => ({
anonymous: conversationList.value.filter((item) => item.category === 'anonymous').length,
realname: conversationList.value.filter((item) => item.category === 'realname').length,
}))
const canSend = computed(() => {
return (
!!activeConversation.value &&
(!!draftText.value.trim() || !!draftImages.value.trim()) &&
!sending.value
)
})
watch(
filteredConversationList,
(list) => {
if (!list.length) {
activeConversationId.value = 0
return
}
if (!list.some((item) => item.id === activeConversationId.value) && list[0]) {
activeConversationId.value = list[0].id
}
},
{ immediate: true },
)
watch(
() => activeConversation.value?.id,
async (id) => {
if (!id) return
const current = conversationList.value.find((item) => item.id === id)
if (current) current.unread = 0
await nextTick()
scrollToBottom()
},
{ immediate: true },
)
const getConversationPreviewTime = (conversation: ConversationItem) => {
const latest = conversation.messages.at(-1)?.createdAt
if (!latest) return '--'
const now = dayjs()
const target = dayjs(latest)
if (target.isSame(now, 'day')) return target.format('HH:mm')
if (target.isSame(now.subtract(1, 'day'), 'day')) return '昨天'
return target.format('MM-DD')
}
const getMessageText = (message: ChatMessage) => {
if (message.text.trim()) return message.text
if (message.images.length) return '[图片]'
return '[新消息]'
}
const selectConversation = (id: number) => {
activeConversationId.value = id
}
const switchCategory = (category: MessageCategory) => {
activeCategory.value = category
keyword.value = ''
}
const scrollToBottom = () => {
const wrapper = conversationBoxRef.value
console.log(wrapper, 'wrapper')
if (!wrapper) return
console.log(wrapper.$el.scrollHeight, 'wrapper.$el.scrollHeight')
wrapper.scrollTo({
top: wrapper.$el.scrollHeight,
behavior: 'smooth',
})
}
const handleSend = async () => {
if (!canSend.value || !activeConversation.value) return
sending.value = true
try {
const newMessage: ChatMessage = {
id: Date.now(),
sender: 'self',
text: draftText.value.trim(),
images: draftImages.value
.split(',')
.map((item) => item.trim())
.filter(Boolean),
createdAt: dayjs().format('YYYY-MM-DD HH:mm'),
}
activeConversation.value.messages.push(newMessage)
activeConversation.value.subtitle = getMessageText(newMessage)
draftText.value = ''
draftImages.value = ''
await nextTick()
scrollToBottom()
push.success('私信已发送')
} finally {
sending.value = false
}
}
const handleInputKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSend()
}
}
</script>
<template>
<div class="rounded-lg relative">
<BackButton />
<section class="flex h-[calc(100vh-6.25rem)] gap-4 overflow-hidden">
<aside
class="flex-1/4 rounded-lg border border-[#d8e5f2] bg-[linear-gradient(180deg,#ffffff_0%,#f8fbff_100%)] p-4 shadow-[0_0.5rem_1.5rem_rgba(134,167,207,0.12)]"
>
<div class="flex h-full flex-col">
<div>
<div class="mb-4 flex items-start justify-between gap-3">
<div>
<h2 class="m-0 text-4.5 font-700 text-[#17305d]">私信列表</h2>
</div>
</div>
<div class="mb-4 grid grid-cols-2 gap-2">
<button
v-for="item in categoryTabs"
:key="item.key"
class="cursor-pointer flex items-center justify-between rounded-lg border px-4 py-3 text-3.5 transition-all"
:class="
activeCategory === item.key
? 'border-[#8eadff] bg-[linear-gradient(180deg,#f4f8ff_0%,#eaf1ff_100%)] text-[#2f5ab7] shadow-[0_0.25rem_0.625rem_rgba(130,160,220,0.12)]'
: 'border-[#dfebf5] bg-[#f9fbfe] text-[#667b95]'
"
@click="switchCategory(item.key)"
>
<span>{{ item.label }}</span>
<span
class="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-[#e8eff8] px-1.5 text-3 font-700"
>
{{
item.key === 'anonymous'
? conversationStats.anonymous
: conversationStats.realname
}}
</span>
</button>
</div>
<el-input
v-model="keyword"
size="large"
clearable
placeholder="搜索用户名"
class="mb-4"
>
<template #prefix>
<el-icon><IEpSearch /></el-icon>
</template>
</el-input>
</div>
<div class="min-h-0 flex-1">
<el-scrollbar>
<div v-if="filteredConversationList.length" class="space-y-3">
<button
v-for="item in filteredConversationList"
:key="item.id"
class="w-full rounded-lg border p-3 text-left transition-all"
:class="
activeConversation?.id === item.id
? 'border-[#cbdcff] bg-[linear-gradient(180deg,#f6faff_0%,#edf5ff_100%)] shadow-[0_0.5rem_1.125rem_rgba(134,167,207,0.15)]'
: 'border-transparent bg-transparent hover:border-[#dde8f3] hover:bg-[#f9fbfe]'
"
@click="selectConversation(item.id)"
>
<div class="flex gap-3">
<div class="relative shrink-0">
<el-avatar :size="46" :src="item.avatar" />
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
<span class="max-w-[6.875rem] truncate text-4 font-700 text-[#17305d]">
{{ item.name }}
</span>
</div>
<span class="shrink-0 text-3 text-[#8ca0b8]">{{
getConversationPreviewTime(item)
}}</span>
</div>
<div class="mt-2 flex min-w-0 items-center gap-2">
<span
class="inline-flex h-5 shrink-0 items-center rounded-full bg-[#fff3dc] px-2 text-3 text-[#9a6817]"
>
{{ item.sourceTypeLabel }}
</span>
<span class="truncate text-3 text-[#7890aa]">{{ item.sourceTitle }}</span>
</div>
</div>
</div>
</button>
</div>
<el-empty v-else description="没有找到匹配的会话" :image-size="90" />
</el-scrollbar>
</div>
</div>
</aside>
<main
class="overflow-hidden h-full flex-3/4 rounded-lg border border-[#d8e5f2] bg-[linear-gradient(180deg,#ffffff_0%,#f9fbff_100%)] shadow-[0_0.5rem_1.5rem_rgba(134,167,207,0.12)]"
>
<template v-if="activeConversation">
<div class="flex h-full min-h-0 flex-col">
<header
class="flex flex-col gap-3 border-b border-[#e6eef6] bg-[linear-gradient(180deg,#f6faff_0%,#ffffff_100%)] px-5 py-4 md:flex-row md:items-center md:justify-between"
>
<div class="flex min-w-0 items-center gap-3">
<el-avatar :size="50" :src="activeConversation.avatar" />
<div class="min-w-0">
<div class="flex items-center gap-2">
<strong class="text-4.5 font-700 text-[#18305d]">{{
activeConversation.name
}}</strong>
<span
class="inline-flex h-6 items-center rounded-full px-2.5 text-3 font-700"
:class="
activeConversation.category === 'anonymous'
? 'bg-[#e7f7ed] text-[#2a7f61]'
: 'bg-[#e8f0ff] text-[#3762c7]'
"
>
{{ activeConversation.badge }}
</span>
</div>
<p class="mt-1 truncate text-[0.8125rem] leading-[1.375rem] text-[#7588a3]">
来源:{{ activeConversation.sourceTitle }} ·
{{ activeConversation.onlineStatus }}
</p>
</div>
</div>
<div class="text-[0.8125rem] text-[#6f84a4]">
当前对话按
<span class="mx-1 font-700 text-[#4a6fd2]">{{ activeConversation.badge }}</span>
方式展示
</div>
</header>
<div class="min-h-0 flex-1 overflow-hidden">
<el-scrollbar ref="conversationBoxRef">
<div class="bg-[linear-gradient(180deg,#fcfdff_0%,#f8fbff_100%)] px-5 py-5">
<div v-for="group in groupedMessages" :key="group.label" class="pb-3">
<div class="mb-4 text-center text-3 text-[#96a6bb]">
<span class="rounded-full bg-[#eef4fb] px-3 py-1">{{ group.label }}</span>
</div>
<div
v-for="message in group.items"
:key="message.id"
class="mb-4 flex items-start gap-3"
:class="{ 'flex-row-reverse': message.sender === 'self' }"
>
<el-avatar
:size="38"
:src="
message.sender === 'self' ? currentUserAvatar : activeConversation.avatar
"
class="shrink-0"
/>
<div
class="flex max-w-[min(72%,38.75rem)] flex-col gap-1"
:class="{ 'items-end': message.sender === 'self' }"
>
<div class="text-3 text-[#8c9bb0]">
{{
message.sender === 'self' ? currentUserName : activeConversation.name
}}
</div>
<div
class="rounded-lg border px-4 py-3 shadow-[0_0.125rem_0.5rem_rgba(39,74,120,0.04)]"
:class="
message.sender === 'self'
? 'rounded-tr-[0.375rem] border-[#d4e4ff] bg-[#dbeaff] text-[#1a3f73]'
: 'rounded-tl-[0.375rem] border-[#e7edf5] bg-white text-[#23364c]'
"
>
<p
v-if="message.text"
class="m-0 whitespace-pre-wrap break-words text-3.5 leading-[1.625rem]"
>
{{ message.text }}
</p>
<div v-if="message.images.length" class="mt-2 grid gap-2 sm:grid-cols-2">
<el-image
v-for="image in message.images"
:key="image"
:src="image"
fit="cover"
:preview-src-list="message.images"
preview-teleported
class="h-30 w-full overflow-hidden rounded-[0.875rem]"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
<footer class="border-t border-[#e6eef6] bg-white px-5 py-4 h-auto">
<div
class="mb-3 flex items-center justify-between gap-3 text-[0.8125rem] text-[#7690ac]"
>
<div class="flex items-center gap-1.5">
<el-icon><IEpInfoFilled /></el-icon>
<span>按 Enter 发送,Shift + Enter 换行</span>
</div>
<span>{{ draftText.length }} / 250</span>
</div>
<div class="self-message-commentbox" @keydown="handleInputKeydown">
<CommentBox
v-model:inputText="draftText"
v-model:inputImg="draftImages"
:textAreaHeight="72"
placeholder="请输入回复内容,回车发送"
>
<template #submit>
<button
type="button"
class="comment-publish-btn cursor-pointer disabled:opacity-50 px-6 py-2 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!draftText?.trim() || sending"
@click="handleSend"
>
<div v-show="sending" class="flex items-center gap-2">
<el-icon><IEpLoading /></el-icon>
<span>发表中...</span>
</div>
<div v-show="!sending">发表</div>
</button>
</template>
</CommentBox>
</div>
</footer>
</div>
</template>
<div v-else class="flex h-full items-center justify-center">
<el-empty description="选择一个会话开始查看私信内容" :image-size="120" />
</div>
</main>
</section>
</div>
</template>
<style scoped>
:deep(.self-message-commentbox textarea) {
min-height: 4.25rem !important;
}
:deep(.self-message-commentbox .el-button--primary) {
border-radius: 0.875rem;
border-color: #85a7ff;
background: linear-gradient(90deg, #71b6ff 0%, #7f92ff 100%);
box-shadow: 0 0.375rem 1rem rgba(125, 146, 255, 0.2);
}
:deep(.self-message-commentbox .el-button--large) {
min-width: 5.75rem;
}
@media (max-width: 64rem) {
:deep(.self-message-commentbox .el-button--large) {
width: 100%;
}
}
/**
* 发表按钮:不用纯 Uno 渐变工具类。
* 旧版内核 / 部分企业微信 WebView 对「依赖 CSS 变量的 linear-gradient」解析失败时,
* 背景会变成透明或无效,原生 button 呈白底,易出现「只剩阴影、几乎看不见按钮」。
* 这里用实色 background-color 作兜底,并写死 hex + -webkit- 前缀渐变。
*/
.comment-publish-btn {
-webkit-appearance: none;
appearance: none;
margin: 0;
border: 1px solid rgba(255, 255, 255, 0.35);
/* 与 Tailwind blue-500 / purple-500 接近的实色兜底 */
background-color: #6366f1;
background-image: -webkit-linear-gradient(left, #3b82f6, #a855f7);
background-image: linear-gradient(to right, #3b82f6, #a855f7);
color: #ffffff;
}
.comment-publish-btn:disabled {
cursor: not-allowed;
}
</style>
...@@ -168,7 +168,6 @@ import { generateLoginKey, hasOfficialAccount } from '@/api' ...@@ -168,7 +168,6 @@ import { generateLoginKey, hasOfficialAccount } from '@/api'
import type { OfficialAccountItemDto } from '@/api/user/types' import type { OfficialAccountItemDto } from '@/api/user/types'
import { wxLogin } from '@/utils/wxUtil' import { wxLogin } from '@/utils/wxUtil'
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router' import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
import type { TabPaneName } from 'element-plus'
import { IS_REAL_KEY } from '@/constants/symbolKey' import { IS_REAL_KEY } from '@/constants/symbolKey'
import { useOnlineTimeStore, useUserStore } from '@/stores' import { useOnlineTimeStore, useUserStore } from '@/stores'
import BackButton from '@/components/common/BackButton/index.vue' import BackButton from '@/components/common/BackButton/index.vue'
...@@ -182,10 +181,11 @@ const route = useRoute() ...@@ -182,10 +181,11 @@ const route = useRoute()
// 当前激活的菜单 用计算属性 好办法! // 当前激活的菜单 用计算属性 好办法!
const activeMenu = computed(() => { const activeMenu = computed(() => {
const path = route.path const path = route.path
console.log(path, 'path')
if (path.includes('userPage')) { if (path.includes('userPage')) {
return path.split('/').at(-1) || 'selfPublish' return path || '/userPage/selfPublish'
} }
return 'selfPublish' return '/userPage/selfPublish'
}) })
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => { const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
...@@ -202,71 +202,77 @@ const { userInfo } = storeToRefs(userStore) ...@@ -202,71 +202,77 @@ const { userInfo } = storeToRefs(userStore)
// 左侧普通用户菜单 // 左侧普通用户菜单
const menuUserItems = [ const menuUserItems = [
{ {
path: 'selfPublish', path: '/userPage/selfPublish',
label: '我的帖子', label: '我的帖子',
icon: () => <IEpUser />, icon: () => <IEpUser />,
tab: '发布', tab: '发布',
}, },
{ {
path: 'selfDraft', path: '/userPage/selfDraft',
label: '我的草稿', label: '我的草稿',
icon: () => <IEpDocument />, icon: () => <IEpDocument />,
tab: '草稿', tab: '草稿',
}, },
{ {
path: 'selfCollect', path: '/userPage/selfCollect',
label: '我的收藏', label: '我的收藏',
icon: () => <IEpStar />, icon: () => <IEpStar />,
tab: '收藏', tab: '收藏',
}, },
{ {
path: 'selfPraise', path: '/userPage/selfPraise',
label: '我的点赞', label: '我的点赞',
icon: () => <IEpPointer />, icon: () => <IEpPointer />,
tab: '点赞', tab: '点赞',
}, },
{ {
path: 'selfCase', path: '/userPage/selfCase',
label: '我的案例库', label: '我的案例库',
icon: () => <IEpCollection />, icon: () => <IEpCollection />,
tab: '案例库', tab: '案例库',
}, },
{ {
path: 'selfTask', path: '/userPage/selfTask',
label: '我的任务', label: '我的任务',
icon: () => <IEpFinished />, icon: () => <IEpFinished />,
tab: '任务', tab: '任务',
}, },
{ {
path: 'selfActivity', path: '/userPage/selfActivity',
label: '参与活动', label: '参与活动',
icon: () => <IEpTrophy />, icon: () => <IEpTrophy />,
tab: '活动', tab: '活动',
}, },
{ {
path: 'selfComment', path: '/userPage/selfComment',
label: '评论回复', label: '评论回复',
icon: () => <IEpChatDotRound />, icon: () => <IEpChatDotRound />,
tab: '评论回复', tab: '评论回复',
}, },
{ {
path: 'selfAnswer', path: '/userPage/selfAnswer',
label: '回答问题(问吧)', label: '回答问题(问吧)',
icon: () => <IEpChatLineSquare />, icon: () => <IEpChatLineSquare />,
tab: '回答问题', tab: '回答问题',
}, },
{
path: '/selfMessage',
label: '我的私信',
icon: () => <IEpMessage />,
tab: '我的私信',
},
] ]
// 左侧官方账号菜单 // 左侧官方账号菜单
const menuOfficialItems = [ const menuOfficialItems = [
{ {
path: 'selfAudit', path: '/userPage/selfAudit',
label: '审核列表', label: '审核列表',
icon: () => <IEpUser />, icon: () => <IEpUser />,
tab: '审核列表', tab: '审核列表',
}, },
{ {
path: 'selfComplaint', path: '/userPage/selfComplaint',
label: '举报列表', label: '举报列表',
icon: () => <IEpWarning />, icon: () => <IEpWarning />,
tab: '举报列表', tab: '举报列表',
...@@ -296,8 +302,8 @@ const currentUserInfo = computed(() => ...@@ -296,8 +302,8 @@ const currentUserInfo = computed(() =>
}, },
) )
const changeMenu = (key: TabPaneName) => { const changeMenu = (key: string) => {
router.push(`/userPage/${key}`) router.push(key)
} }
const handleEdit = () => { const handleEdit = () => {
......
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