mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-12-02 03:37:53 +08:00
feat(view): ✨ 新增搜索页面功能
美化动态页面内容
This commit is contained in:
parent
85b6cad03f
commit
866ba89b93
@ -16,6 +16,7 @@
|
||||
:class="{ 'right-1px': activeItem.type === RoomTypeEnum.SINGLE }"
|
||||
class="relative h-100vh"
|
||||
:ignore-item-resize="true"
|
||||
:item-resizable="true"
|
||||
:padding-top="10"
|
||||
:item-size="itemSize"
|
||||
:items="chatMessageList">
|
||||
@ -50,7 +51,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<!-- 好友或者群聊的信息 -->
|
||||
<article
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col w-full"
|
||||
:class="[{ 'items-end': item.fromUser.uid === userUid }, chatStore.isGroup ? 'gap-18px' : 'gap-2px']">
|
||||
@ -275,7 +276,7 @@
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
|
@ -24,8 +24,8 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
const chatKey = ref(chat.value.sendKey)
|
||||
const msgInput = ref('')
|
||||
const ait = ref(false)
|
||||
/** 临时消息id */
|
||||
const tempMessageId = ref(0)
|
||||
// /** 临时消息id */
|
||||
// const tempMessageId = ref(0)
|
||||
/** 艾特后的关键字的key */
|
||||
const aitKey = ref('')
|
||||
/** 是否正在输入拼音 */
|
||||
@ -199,16 +199,13 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgType: msg.type,
|
||||
body: { content: msg.content, replyMsgId: msg.reply !== 0 ? msg.reply : undefined }
|
||||
})
|
||||
.then((res) => {
|
||||
.then(async (res) => {
|
||||
if (res.data.message.type === MsgEnum.TEXT) {
|
||||
chatStore.pushMsg(res.data)
|
||||
// 发完消息就要刷新会话列表,
|
||||
// FIXME 如果当前会话已经置顶了,可以不用刷新
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
} else {
|
||||
// 更新上传状态下的消息
|
||||
chatStore.updateMsg(tempMessageId.value, res.data)
|
||||
await chatStore.pushMsg(res.data)
|
||||
}
|
||||
// 发完消息就要刷新会话列表,
|
||||
// FIXME 如果当前会话已经置顶了,可以不用刷新
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
})
|
||||
msgInput.value = ''
|
||||
messageInputDom.value.innerHTML = ''
|
||||
|
220
src/hooks/useUpload.ts
Normal file
220
src/hooks/useUpload.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { ref } from 'vue'
|
||||
import { createEventHook } from '@vueuse/core'
|
||||
import apis from '@/services/apis'
|
||||
|
||||
/** 文件信息类型 */
|
||||
export type FileInfoType = {
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
suffix: string
|
||||
width?: number
|
||||
height?: number
|
||||
downloadUrl?: string
|
||||
second?: number
|
||||
thumbWidth?: number
|
||||
thumbHeight?: number
|
||||
thumbUrl?: string
|
||||
}
|
||||
|
||||
const Max = 100 // 单位M
|
||||
const MAX_FILE_SIZE = Max * 1024 * 1024 // 最大上传限制
|
||||
|
||||
/**
|
||||
* 文件上传Hook
|
||||
*/
|
||||
export const useUpload = () => {
|
||||
const isUploading = ref(false) // 是否正在上传
|
||||
const progress = ref(0) // 进度
|
||||
const fileInfo = ref<FileInfoType | null>(null) // 文件信息
|
||||
|
||||
const { on: onChange, trigger } = createEventHook()
|
||||
const onStart = createEventHook()
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param url 上传链接
|
||||
* @param file 文件
|
||||
* @param inner 是否内部调用
|
||||
*/
|
||||
const upload = async (url: string, file: File, inner?: boolean) => {
|
||||
isUploading.value = true
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', url, true)
|
||||
xhr.setRequestHeader('Content-Type', file.type)
|
||||
xhr.upload.onprogress = function (e) {
|
||||
if (!inner) {
|
||||
progress.value = Math.round((e.loaded / e.total) * 100)
|
||||
}
|
||||
}
|
||||
xhr.onload = function () {
|
||||
isUploading.value = false
|
||||
if (inner) return
|
||||
if (xhr.status === 200) {
|
||||
trigger('success')
|
||||
} else {
|
||||
trigger('fail')
|
||||
}
|
||||
}
|
||||
xhr.send(file)
|
||||
}
|
||||
/**
|
||||
* 获取视频第一帧
|
||||
*/
|
||||
const getVideoCover = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video')
|
||||
const tempUrl = URL.createObjectURL(file)
|
||||
video.src = tempUrl
|
||||
video.crossOrigin = 'anonymous' // 视频跨域
|
||||
video.currentTime = 2 // 第2帧
|
||||
video.oncanplay = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
canvas.getContext('2d')?.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 将canvas转为图片file
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return
|
||||
// 时间戳生成唯一文件名
|
||||
const name = Date.now() + 'thumb.jpg'
|
||||
const thumbFile = new File([blob], name, { type: 'image/jpeg' })
|
||||
// 转成File对象 并上传
|
||||
apis.getUploadUrl({ fileName: name, scene: '1' }).then(async (res) => {
|
||||
if (res.data.uploadUrl && res.data.downloadUrl) {
|
||||
await upload(res.data.uploadUrl, thumbFile, true)
|
||||
// 等待上传完成
|
||||
const timer = setInterval(() => {
|
||||
if (!isUploading.value) {
|
||||
clearInterval(timer)
|
||||
resolve({
|
||||
thumbWidth: canvas.width,
|
||||
thumbHeight: canvas.height,
|
||||
thumbUrl: res.data.downloadUrl,
|
||||
thumbSize: thumbFile.size,
|
||||
tempUrl
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
video.onerror = function () {
|
||||
URL.revokeObjectURL(tempUrl) // 释放临时URL资源
|
||||
reject({ width: 0, height: 0, url: null })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片宽高
|
||||
*/
|
||||
const getImgWH = (file: File) => {
|
||||
const img = new Image()
|
||||
const tempUrl = URL.createObjectURL(file)
|
||||
img.src = tempUrl
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = function () {
|
||||
resolve({ width: img.width, height: img.height, tempUrl })
|
||||
}
|
||||
img.onerror = function () {
|
||||
URL.revokeObjectURL(tempUrl) // 释放临时URL资源
|
||||
reject({ width: 0, height: 0, url: null })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频时长
|
||||
*/
|
||||
const getAudioDuration = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const audio = new Audio()
|
||||
const tempUrl = URL.createObjectURL(file)
|
||||
audio.src = tempUrl
|
||||
// 计算音频的时长
|
||||
const countAudioTime = async () => {
|
||||
while (isNaN(audio.duration) || audio.duration === Infinity) {
|
||||
// 防止浏览器卡死
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// 随机进度条位置
|
||||
audio.currentTime = 100000 * Math.random()
|
||||
}
|
||||
// 取整
|
||||
const second = Math.round(audio.duration || 0)
|
||||
resolve({ second, tempUrl })
|
||||
}
|
||||
countAudioTime()
|
||||
audio.onerror = function () {
|
||||
reject({ second: 0, tempUrl })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件
|
||||
* @param file 文件
|
||||
* @param addParams 参数
|
||||
* @returns 文件大小、文件类型、文件名、文件后缀...
|
||||
*/
|
||||
const parseFile = async (file: File, addParams: Record<string, any> = {}) => {
|
||||
const { name, size, type } = file
|
||||
const suffix = name.split('.').pop()?.trim().toLowerCase() || ''
|
||||
const baseInfo = { name, size, type, suffix, ...addParams }
|
||||
|
||||
if (type.includes('image')) {
|
||||
const { width, height, tempUrl } = (await getImgWH(file)) as any
|
||||
return { ...baseInfo, width, height, tempUrl }
|
||||
}
|
||||
|
||||
if (type.includes('audio')) {
|
||||
const { second, tempUrl } = (await getAudioDuration(file)) as any
|
||||
return { second, tempUrl, ...baseInfo }
|
||||
}
|
||||
// 如果是视频
|
||||
if (type.includes('video')) {
|
||||
const { thumbWidth, thumbHeight, tempUrl, thumbTempUrl, thumbUrl, thumbSize } = (await getVideoCover(file)) as any
|
||||
return { ...baseInfo, thumbWidth, thumbHeight, tempUrl, thumbTempUrl, thumbUrl, thumbSize }
|
||||
}
|
||||
|
||||
return baseInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param file 文件
|
||||
* @param addParams 额外参数
|
||||
*/
|
||||
const uploadFile = async (file: File, addParams?: Record<string, any>) => {
|
||||
if (isUploading.value || !file) return
|
||||
const info = await parseFile(file, addParams)
|
||||
|
||||
// 限制文件大小
|
||||
if (info.size > MAX_FILE_SIZE) {
|
||||
window.$message.error(`文件大小不能超过 ${Max}MB`)
|
||||
return
|
||||
}
|
||||
|
||||
const { downloadUrl, uploadUrl } = (await apis.getUploadUrl({ fileName: info.name, scene: '1' })).data
|
||||
|
||||
if (uploadUrl && downloadUrl) {
|
||||
fileInfo.value = { ...info, downloadUrl }
|
||||
await onStart.trigger(fileInfo)
|
||||
await upload(uploadUrl, file)
|
||||
} else {
|
||||
await trigger('fail')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fileInfo,
|
||||
isUploading,
|
||||
progress,
|
||||
onStart: onStart.on,
|
||||
onChange,
|
||||
uploadFile
|
||||
}
|
||||
}
|
@ -2,6 +2,10 @@ import { WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
|
||||
/**
|
||||
* 监听窗口状态变化
|
||||
* @param label 窗口标签
|
||||
*/
|
||||
export const useWindowState = (label: string) => {
|
||||
const win = WebviewWindow.getByLabel(label)
|
||||
|
||||
|
@ -1,19 +1,13 @@
|
||||
<template>
|
||||
<main
|
||||
data-tauri-drag-region
|
||||
id="center"
|
||||
class="resizable select-none flex flex-col shadow-inner"
|
||||
:style="{ width: `${initWidth}px` }">
|
||||
<main id="center" class="resizable select-none flex flex-col shadow-inner" :style="{ width: `${initWidth}px` }">
|
||||
<div class="resize-handle" @mousedown="initDrag"></div>
|
||||
<ActionBar
|
||||
class="absolute right-0"
|
||||
class="absolute right-0 w-full"
|
||||
v-if="shrinkStatus"
|
||||
:shrink-status="!shrinkStatus"
|
||||
:max-w="false"
|
||||
:current-label="appWindow.label" />
|
||||
|
||||
<!-- <div class="resize-handle" @mousedown="initDrag"></div>-->
|
||||
|
||||
<!-- 顶部搜索栏 -->
|
||||
<header
|
||||
style="box-shadow: 0 2px 4px var(--box-shadow-color)"
|
||||
@ -22,7 +16,7 @@
|
||||
<n-input
|
||||
id="search"
|
||||
@focus="() => router.push('/searchDetails')"
|
||||
class="rounded-6px w-full"
|
||||
class="rounded-6px w-full relative"
|
||||
style="background: var(--search-bg-color)"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
@ -32,16 +26,28 @@
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-button size="small" secondary style="padding: 0 5px">
|
||||
<n-button @click="addPanels.show = !addPanels.show" size="small" secondary style="padding: 0 5px">
|
||||
<template #icon>
|
||||
<svg class="w-24px h-24px"><use href="#plus"></use></svg>
|
||||
</template>
|
||||
</n-button>
|
||||
|
||||
<!-- 添加面板 -->
|
||||
<div v-if="addPanels.show" class="add-item">
|
||||
<div class="menu-list">
|
||||
<div v-for="(item, index) in addPanels.list" :key="index">
|
||||
<div class="menu-item" @click="() => item.click()">
|
||||
<svg><use :href="`#${item.icon}`"></use></svg>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div id="centerList">
|
||||
<div id="centerList" class="h-full">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
@ -66,6 +72,26 @@ const { width } = useWindowSize()
|
||||
const isDrag = ref(true)
|
||||
/** 当前消息 */
|
||||
const currentMsg = ref()
|
||||
/** 添加面板是否显示 */
|
||||
const addPanels = ref({
|
||||
show: false,
|
||||
list: [
|
||||
{
|
||||
label: '发起群聊',
|
||||
icon: 'launch',
|
||||
click: () => {
|
||||
console.log('发起群聊')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '加好友/群',
|
||||
icon: 'people-plus',
|
||||
click: () => {
|
||||
console.log('加好友/群')
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const startX = ref()
|
||||
const startWidth = ref()
|
||||
@ -96,6 +122,9 @@ const closeMenu = (event: Event) => {
|
||||
if (!e.matches('#search, #search *, #centerList *, #centerList') && route === '/searchDetails') {
|
||||
router.go(-1)
|
||||
}
|
||||
if (!e.matches('.add-item')) {
|
||||
addPanels.value.show = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 定义一个函数,在鼠标拖动时调用 */
|
||||
|
@ -15,3 +15,10 @@
|
||||
z-index: 9999;
|
||||
background-color: var(--split-color);
|
||||
}
|
||||
|
||||
.add-item {
|
||||
@include menu-item-style(absolute);
|
||||
top: 60px;
|
||||
right: 30px;
|
||||
@include menu-list();
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
<!-- 该选项有提示时展示 -->
|
||||
<n-popover style="padding: 12px" v-else-if="item.tip" trigger="manual" v-model:show="tipShow" placement="right">
|
||||
<template #trigger>
|
||||
<n-badge :max="99" :value="item.badge" :dot="dotShow">
|
||||
<n-badge :max="99" :value="item.badge" dot :show="dotShow">
|
||||
<svg class="size-22px" @click="handleTipShow">
|
||||
<use
|
||||
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction : item.icon}`"></use>
|
||||
|
@ -47,3 +47,13 @@ export const dynamicList = Array.from({ length: 10 }, (_, i) => {
|
||||
isAuth: i % 2 === 0
|
||||
}
|
||||
})
|
||||
|
||||
/** 动态评论 */
|
||||
export const dynamicCommentList = Array.from({ length: 50 }, (_, i) => {
|
||||
return {
|
||||
id: i,
|
||||
avatar: `${avatars}?${i}`,
|
||||
user: `泰勒斯威夫特${i}`,
|
||||
content: '点赞了你的动态'
|
||||
}
|
||||
})
|
||||
|
@ -99,9 +99,9 @@
|
||||
</n-flex>
|
||||
|
||||
<n-flex v-if="!isLogining && !isWrongPassword" justify="space-around" align="center" :size="0" class="options">
|
||||
<p class="text-(14px #f1f1f1)" @click="isUnlockPage = false">返回</p>
|
||||
<p class="text-(14px #e3e3e3)" @click="logout">退出登录</p>
|
||||
<p class="text-(14px #e3e3e3)">忘记密码</p>
|
||||
<p class="text-(14px #fefefe)" @click="isUnlockPage = false">返回</p>
|
||||
<p class="text-(14px #fefefe)" @click="logout">退出登录</p>
|
||||
<p class="text-(14px #fefefe)">忘记密码</p>
|
||||
<p class="text-(14px #fff)" @click="unlock">进入系统</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
@ -1,9 +1,38 @@
|
||||
<template>
|
||||
<main class="size-full bg-[--right-bg-color]">
|
||||
<ActionBar :shrink="false" :max-w="false" :top-win-label="appWindow.label" :current-label="appWindow.label" />
|
||||
<article class="flex flex-col items-center text-[--text-color] size-full bg-[--right-bg-color]">
|
||||
|
||||
<!-- 头部用户信息栏 -->
|
||||
<n-flex
|
||||
align="center"
|
||||
justify="center"
|
||||
:size="20"
|
||||
class="relative bg-[--left-active-color] h-160px w-full select-none">
|
||||
<n-avatar :size="120" round bordered :src="login.accountInfo.avatar" />
|
||||
<n-flex vertical justify="center" :size="20">
|
||||
<p class="text-(24px [--chat-text-color]) font-bold">{{ login.accountInfo.name }}</p>
|
||||
|
||||
<n-flex align="center" justify="space-between" :size="30" class="mt-5px">
|
||||
<template v-for="item in titleList" :key="item.label">
|
||||
<n-flex vertical align="center" class="cursor-pointer">
|
||||
<p class="text-[--text-color]">{{ item.total }}</p>
|
||||
<p class="text-(16px #808080)">{{ item.label }}</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<div class="absolute top-30px right-30px cursor-pointer" @click="handleInfoTip">
|
||||
<n-badge :value="infoTip.value" :max="100" :show="infoTip.show">
|
||||
<svg class="size-24px color-[--text-color]"><use href="#remind"></use></svg>
|
||||
</n-badge>
|
||||
</div>
|
||||
</n-flex>
|
||||
|
||||
<!-- 动态列表 -->
|
||||
<div class="flex flex-col items-center text-[--text-color] size-full bg-[--right-bg-color]">
|
||||
<n-scrollbar
|
||||
style="max-height: calc(100vh - 20px)"
|
||||
style="max-height: calc(100vh - 180px)"
|
||||
class="w-full bg-[--center-bg-color] border-(solid 1px [--line-color]) h-full p-[10px_0] box-border rounded-4px">
|
||||
<n-flex justify="center">
|
||||
<!-- 动态内容框 -->
|
||||
@ -58,13 +87,67 @@
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-scrollbar>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 弹出框 -->
|
||||
<n-modal v-model:show="infoTip.modalShow" class="w-450px border-rd-8px">
|
||||
<div class="bg-[--bg-popover] h-full p-6px box-border flex flex-col">
|
||||
<svg @click="infoTip.modalShow = false" class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<n-virtual-list
|
||||
:items="dynamicCommentList"
|
||||
:item-size="40"
|
||||
class="max-h-500px w-full p-10px box-border select-none">
|
||||
<template #default="{ item }">
|
||||
<n-flex align="center" justify="space-between" class="mt-18px">
|
||||
<n-flex align="center">
|
||||
<n-avatar :size="36" round bordered :src="item.avatar" />
|
||||
<p>{{ item.user }}</p>
|
||||
<p class="text-(12px #707070)">{{ item.content }}</p>
|
||||
</n-flex>
|
||||
|
||||
<p class="text-(12px #707070)">2021-01-01</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
</n-modal>
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { dynamicList } from '@/mock'
|
||||
import { dynamicList, dynamicCommentList } from '@/mock'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { useWindowState } from '@/hooks/useWindowState.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
useWindowState(appWindow.label)
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
const infoTip = ref({
|
||||
value: dynamicCommentList.length,
|
||||
show: true,
|
||||
modalShow: false
|
||||
})
|
||||
const titleList = [
|
||||
{
|
||||
label: '动态',
|
||||
total: 43
|
||||
},
|
||||
{
|
||||
label: '关注',
|
||||
total: 443
|
||||
},
|
||||
{
|
||||
label: '点赞',
|
||||
total: 99
|
||||
}
|
||||
]
|
||||
|
||||
/** 处理信息提示 */
|
||||
const handleInfoTip = () => {
|
||||
infoTip.value.show = false
|
||||
infoTip.value.modalShow = true
|
||||
}
|
||||
</script>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<ContextMenu @contextmenu="showMenu($event)" @select="handleSelect($event.label)" :menu="menuList">
|
||||
<n-collapse-item title="我的好友" name="1">
|
||||
<template #header-extra>
|
||||
<span class="text-(10px #707070)">1/1</span>
|
||||
<span class="text-(10px #707070)">0/0</span>
|
||||
</template>
|
||||
|
||||
<!-- 用户框 多套一层div来移除默认的右键事件然后覆盖掉因为margin空隙而导致右键可用 -->
|
||||
|
@ -1,6 +1,42 @@
|
||||
<template>
|
||||
<div>查找用户</div>
|
||||
<n-flex :size="14" vertical justify="center" class="p-14px text-(12px #909090)">
|
||||
<p>搜索建议</p>
|
||||
|
||||
<n-flex align="center" class="text-(12px #909090)">
|
||||
<p class="p-6px bg-#eee rounded-8px cursor-pointer">@我</p>
|
||||
<p class="p-6px bg-#eee rounded-8px cursor-pointer">特别关心</p>
|
||||
</n-flex>
|
||||
|
||||
<span class="w-full h-1px bg-[--line-color]"></span>
|
||||
|
||||
<n-flex align="center" justify="space-between">
|
||||
<p class="text-(12px #909090)">历史记录</p>
|
||||
<p class="cursor-pointer text-(12px #13987f)">清除</p>
|
||||
</n-flex>
|
||||
|
||||
<template v-for="(item, _index) in historyList" :key="_index">
|
||||
<n-flex align="center" :size="14" class="p-6px cursor-pointer rounded-8px hover:bg-[--bg-group-hover]">
|
||||
<n-avatar :size="38" round bordered :src="item.avatar" />
|
||||
<p class="text-(16px [--text-color])">{{ item.name }}</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
const historyList = [
|
||||
{
|
||||
avatar: 'https://picsum.photos/140?1',
|
||||
name: '小瘪三'
|
||||
},
|
||||
{
|
||||
avatar: 'https://picsum.photos/140?2',
|
||||
name: '号啊玉'
|
||||
},
|
||||
{
|
||||
avatar: 'https://picsum.photos/140?3',
|
||||
name: '张三'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
Loading…
Reference in New Issue
Block a user