Commit 31150849 by 王立鹏

Merge branch 'feature/22051-企业文化平台-YAYA文化岛私信功能需求' into 'master'

Feature/22051 企业文化平台 yaya文化岛私信功能需求

See merge request !29
parents f0a877f5 68a799ed
......@@ -10,6 +10,7 @@
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test": "vitest",
"type-check": "vue-tsc --build",
"lint": "oxlint --fix && eslint . --fix --cache",
"lint:oxlint": "oxlint --fix",
......@@ -56,11 +57,13 @@
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.10",
"@vue/tsconfig": "^0.8.1",
"baseline-browser-mapping": "^2.9.14",
"eslint": "^9.37.0",
"eslint-plugin-vue": "~10.5.0",
"jiti": "^2.6.1",
"jsdom": "^29.1.1",
"npm-run-all2": "^8.0.4",
"oxfmt": "^0.44.0",
"oxlint": "^1.59.0",
......@@ -74,6 +77,7 @@
"vite": "^8.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "^8.0.3",
"vitest": "^4.1.6",
"vue-tsc": "^3.1.1"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
// @vitest-environment jsdom
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import BackButton from '@/components/common/BackButton/index.vue'
console.log(BackButton)
const { push, back } = vi.hoisted(() => ({
push: vi.fn(),
back: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRouter: () => ({
push,
back,
}),
}))
describe('component: BackButton', () => {
beforeEach(() => {
vi.clearAllMocks()
})
test('backHomePage 为 true 时,点击后跳转首页', async () => {
const wrapper = mount(BackButton, {
props: {
backHomePage: true,
},
})
await wrapper.trigger('click')
expect(push).toHaveBeenCalledWith('/')
expect(back).not.toHaveBeenCalled()
})
test('backHomePage 为 false 且存在历史记录时,点击后返回上一页', async () => {
Object.defineProperty(window.history, 'length', {
configurable: true,
value: 2,
})
const wrapper = mount(BackButton)
await wrapper.trigger('click')
expect(back).toHaveBeenCalled()
expect(push).not.toHaveBeenCalled()
})
test('backHomePage 为 false 且不存在历史记录时,点击后跳转首页', async () => {
Object.defineProperty(window.history, 'length', {
configurable: true,
value: 1,
})
const wrapper = mount(BackButton)
await wrapper.trigger('click')
expect(push).toHaveBeenCalledWith('/')
expect(back).not.toHaveBeenCalled()
})
})
import { describe, expect, test } from 'vitest'
import { useResetData } from '@/hooks/useResetData'
describe('hook: useResetData', () => {
test('初始化时返回 initialValue 的深拷贝', () => {
const rawObj = {
age: 1,
arr: [],
}
const [data] = useResetData(rawObj)
expect(data.value).not.toBe(rawObj)
expect(data.value).toEqual(rawObj)
data.value.age = 2
expect(data.value).not.toEqual(rawObj)
})
test('reset仍然是深拷贝', () => {
const rawObj = {
age: 1,
arr: [],
}
const [data, reset] = useResetData(rawObj)
data.value.age = 2
reset()
expect(data.value).not.toBe(rawObj)
expect(data.value).toEqual(rawObj)
})
test('reset 会替换 state.value,旧 toRef 不再跟随后续状态变化', () => {
const rawObj = {
age: 1,
arr: [],
}
const [data, reset] = useResetData(rawObj)
const ageRef = toRef(data.value, 'age')
data.value.age = 2
expect(ageRef.value).toBe(2)
reset()
data.value.age = 3
expect(ageRef.value).toBe(2)
})
test('forReset 会保留 state.value 引用,旧 toRef 仍会跟随后续状态变化', () => {
const rawObj = {
age: 1,
arr: [],
}
const [data, , forReset] = useResetData(rawObj)
const ageRef = toRef(data.value, 'age')
data.value.age = 2
expect(ageRef.value).toBe(2)
forReset()
expect(ageRef.value).toBe(1)
data.value.age = 3
expect(ageRef.value).toBe(3)
})
})
import { describe, expect, test, vi } from 'vitest'
vi.mock('@/api', () => ({
uploadFile: vi.fn(),
}))
vi.mock('notivue', () => ({
push: {
error: vi.fn(),
},
}))
import { appendImageUrl, removeImageUrl, toImageList } from '@/hooks/useUploadImg'
describe('hook 辅助函数:useUploadImg', () => {
test('toImageList 能统一处理字符串和数组格式的图片值', () => {
expect(toImageList('a,,b,')).toEqual(['a', 'b'])
expect(toImageList(['a', 'b'])).toEqual(['a', 'b'])
})
test('appendImageUrl 追加图片后保持原有数据格式', () => {
expect(appendImageUrl('a,b', 'c')).toBe('a,b,c')
expect(appendImageUrl(['a', 'b'], 'c')).toEqual(['a', 'b', 'c'])
})
test('removeImageUrl 删除图片后保持原有数据格式', () => {
expect(removeImageUrl('a,b,c', 'b')).toBe('a,c')
expect(removeImageUrl(['a', 'b', 'c'], 'b')).toEqual(['a', 'c'])
})
})
// @vitest-environment jsdom
import { describe, expect, test } from 'vitest'
import { changeAppTitle } from '@/utils/app'
describe('function: changeAppTitle', () => {
test('传入标题时,修改 document.title', () => {
changeAppTitle('企业文化')
expect(document.title).toBe('企业文化')
})
})
import { describe, expect, test } from 'vitest'
import { formatSeconds, formatDuration } from '@/utils/app'
describe('function: formatSeconds', () => {
test('传入 number 时,返回当天 23:59:59 的秒级时间戳 number', () => {
const result = formatSeconds(1705285230)
expect(result).toBe(1705334399)
})
test('传入 string 时,返回当天 23:59:59 的秒级时间戳 string', () => {
const result = formatSeconds('1705285230')
expect(result).toBe('1705334399')
})
})
describe('function: formatDuration', () => {
test('把秒数格式化为 m:ss,并对秒数补 0', () => {
const result1 = formatDuration(120)
expect(result1).toBe('2:00')
const result2 = formatDuration(8)
expect(result2).toBe('0:08')
const result3 = formatDuration(61)
expect(result3).toBe('1:01')
})
test.each([
[120, '2:00'],
[8, '0:08'],
[61, '1:01'],
])('把秒数格式化为 m:ss,并对秒数补 0', (seconds, expected) => {
expect(formatDuration(seconds)).toBe(expected)
})
})
import { describe, expect, test } from 'vitest'
import { parseEmoji } from '@/utils/emoji'
describe('function: parseEmoji', () => {
test('传入空字符串时,返回空字符串', () => {
const result = parseEmoji('')
expect(result).toBe('')
})
test('传入不包含表情标记的文本时,返回原文本', () => {
const result = parseEmoji('这是一段普通评论')
expect(result).toBe('这是一段普通评论')
})
test('传入 YAYA 表情标记时,替换为 img 标签', () => {
const result = parseEmoji('收到[YAYA_OK]')
expect(result).toContain('<img')
expect(result).toContain('alt="[YAYA_OK]"')
expect(result).toContain('class="w-8 h-8"')
})
test('传入 普通 表情标记时,替换为 img 标签', () => {
const result = parseEmoji('开心[face_哈哈]')
expect(result).toContain('<img')
expect(result).toContain('alt="[face_哈哈]"')
expect(result).toContain('class="w-6 h-6"')
})
})
......@@ -157,6 +157,7 @@ export interface ArticleItemDto {
hasPraised: boolean
hasCollect: boolean
imgUrl: string
imgUrlList?: string[]
createUserAvatar: string
createUserName: string
showAvatar: string
......@@ -377,6 +378,7 @@ export interface CommentItemDto {
isExpand: boolean
childNum: number
imgUrl: string
imgUrlList?: string[]
floorNumber: number
}
......@@ -444,6 +446,7 @@ export interface SearchMoreVideoItemDto {
type: ArticleTypeEnum.VIDEO
videoDuration: string
viewCount: number
publishTime: number
}
/**
......
// 企业文化接口
export * from "./task";
export * from "./sign";
export * from "./shop";
export * from "./tag";
export * from "./user";
export * from "./case";
export * from "./home";
export * from "./practice";
export * from "./common";
export * from "./login";
export * from "./article";
export * from "./online";
export * from "./otherUserPage";
export * from "./auction";
export * from "./dailyLottery";
export * from "./launchCampaign";
export * from "./selfMessage";
export * from "./luckyWheel";
export * from './task'
export * from './sign'
export * from './shop'
export * from './tag'
export * from './user'
export * from './case'
export * from './home'
export * from './practice'
export * from './common'
export * from './login'
export * from './article'
export * from './online'
export * from './otherUserPage'
export * from './auction'
export * from './dailyLottery'
export * from './launchCampaign'
export * from './selfMessage'
export * from './luckyWheel'
// 导出类型
export * from "./task/types";
export * from "./shop/types";
export * from "./tag/types";
export * from "./article/types";
export * from "./user/types";
export * from "./case/types";
export * from "./home/types";
export * from "./practice/types";
export * from "./common/types";
export * from "./login/types";
export * from "./article/types";
export * from "./online/types";
export * from "./otherUserPage/types";
export * from "./auction/types";
export * from "./dailyLottery/types";
export * from "./launchCampaign/types";
export * from "./selfMessage/types";
export * from "./luckyWheel/types";
export * from './task/types'
export * from './shop/types'
export * from './tag/types'
export * from './article/types'
export * from './user/types'
export * from './case/types'
export * from './home/types'
export * from './practice/types'
export * from './common/types'
export * from './login/types'
export * from './article/types'
export * from './online/types'
export * from './otherUserPage/types'
export * from './auction/types'
export * from './dailyLottery/types'
export * from './launchCampaign/types'
export * from './selfMessage/types'
export * from './luckyWheel/types'
<template>
<div
class="absolute -left-12 top-2 z-10 flex flex-col items-center bg-white rounded-lg shadow-md shadow-black/6 border border-gray-100 cursor-pointer hover:shadow-lg hover:border-indigo-200 transition-all duration-300 group w-9 h-9 hover:h-18 overflow-hidden"
<button
type="button"
aria-label="返回"
class="absolute -left-12 top-2 z-10 flex flex-col items-center bg-white rounded-lg shadow-md shadow-black/6 border border-gray-100 cursor-pointer hover:shadow-lg hover:border-indigo-200 transition-all duration-300 group w-9 h-9 hover:h-18 overflow-hidden p-0"
@click="handleBack"
>
<svg
......@@ -19,7 +21,7 @@
>
返回
</span>
</div>
</button>
</template>
<script setup lang="ts">
......
......@@ -6,66 +6,18 @@
<span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span>
<div class="flex items-center gap-2">
<button
v-for="sortOption in commentSortOptions"
:key="sortOption.value"
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = CommentSortTypeEnum.MOST_LIKE), refresh())"
@click="handleChangeSort(sortOption.value)"
:class="{
'text-indigo-600 font-medium':
searchParams.sortType === CommentSortTypeEnum.MOST_LIKE,
'text-gray-600 hover:text-gray-900':
searchParams.sortType !== CommentSortTypeEnum.MOST_LIKE,
'text-indigo-600 font-medium': searchParams.sortType === sortOption.value,
'text-gray-600 hover:text-gray-900': searchParams.sortType !== sortOption.value,
}"
>
最高点赞
{{ sortOption.label }}
<span
v-if="searchParams.sortType === CommentSortTypeEnum.MOST_LIKE"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button>
<button
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = CommentSortTypeEnum.MOST_COMMENT), refresh())"
:class="{
'text-indigo-600 font-medium':
searchParams.sortType === CommentSortTypeEnum.MOST_COMMENT,
'text-gray-600 hover:text-gray-900':
searchParams.sortType !== CommentSortTypeEnum.MOST_COMMENT,
}"
>
最多评论
<span
v-if="searchParams.sortType === CommentSortTypeEnum.MOST_COMMENT"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button>
<button
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = CommentSortTypeEnum.EARLIEST_PUBLISH), refresh())"
:class="{
'text-indigo-600 font-medium':
searchParams.sortType === CommentSortTypeEnum.EARLIEST_PUBLISH,
'text-gray-600 hover:text-gray-900':
searchParams.sortType !== CommentSortTypeEnum.EARLIEST_PUBLISH,
}"
>
最早发布
<span
v-if="searchParams.sortType === CommentSortTypeEnum.EARLIEST_PUBLISH"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button>
<button
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="((searchParams.sortType = CommentSortTypeEnum.NEWEST_PUBLISH), refresh())"
:class="{
'text-indigo-600 font-medium':
searchParams.sortType === CommentSortTypeEnum.NEWEST_PUBLISH,
'text-gray-600 hover:text-gray-900':
searchParams.sortType !== CommentSortTypeEnum.NEWEST_PUBLISH,
}"
>
最新发布
<span
v-if="searchParams.sortType === CommentSortTypeEnum.NEWEST_PUBLISH"
v-if="searchParams.sortType === sortOption.value"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span>
</button>
......@@ -192,7 +144,7 @@
<!-- 评论图片列表 -->
<div class="flex flex-wrap gap-2">
<div
v-for="(img, imgIndex) in item.imgUrl.split(',').filter(Boolean)"
v-for="(img, imgIndex) in item.imgUrlList"
:key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2 cursor-pointer"
>
......@@ -200,7 +152,7 @@
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="item.imgUrl.split(',').filter(Boolean)"
:preview-src-list="item.imgUrlList"
:initial-index="imgIndex"
fit="cover"
/>
......@@ -276,7 +228,7 @@
<!-- 评论图片列表 -->
<div class="flex flex-wrap gap-2">
<div
v-for="(img, imgIndex) in child.imgUrl.split(',').filter(Boolean)"
v-for="(img, imgIndex) in child.imgUrlList"
:key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2"
>
......@@ -284,7 +236,7 @@
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="child.imgUrl.split(',').filter(Boolean)"
:preview-src-list="child.imgUrlList"
:initial-index="imgIndex"
fit="cover"
/>
......@@ -535,7 +487,11 @@ const {
return list.map((item) => {
// 初始化的时候 添加新的字段 是否展示分页子评论 默认是false 当前子评论分页current 以及子评论分页列表 loading效果
const obj: CommentItemDto = { ...item }
const obj: CommentItemDto = {
...item,
imgUrlList: getCommentImageList(item.imgUrl),
children: item.children?.map(formatCommentImageList) ?? [],
}
// if (obj.showChildrenPage == undefined) {
obj.showChildrenPage = false
// }
......@@ -554,11 +510,33 @@ const {
},
immediate,
})
const commentSortOptions = [
{ label: '最高点赞', value: CommentSortTypeEnum.MOST_LIKE },
{ label: '最多评论', value: CommentSortTypeEnum.MOST_COMMENT },
{ label: '最早发布', value: CommentSortTypeEnum.EARLIEST_PUBLISH },
{ label: '最新发布', value: CommentSortTypeEnum.NEWEST_PUBLISH },
]
const handleChangeSort = (sortType: CommentSortTypeEnum) => {
searchParams.value.sortType = sortType
refresh()
}
const handleCurrentChange = async (e: number) => {
await goToPage(e)
handleBackTop()
}
const getCommentImageList = (imgUrl?: string) => {
return imgUrl?.split(',').filter(Boolean) ?? []
}
const formatCommentImageList = (item: CommentItemDto): CommentItemDto => ({
...item,
imgUrlList: getCommentImageList(item.imgUrl),
})
// 自己发出的评论
const myComment = ref('')
const myCommentLoading = ref(false)
......@@ -740,7 +718,7 @@ const getCommentChildrenList = async (item: CommentItemDto) => {
current: item.childrenPageCurrent,
size: 10,
})
item.childrenPageList = data.list
item.childrenPageList = data.list.map(formatCommentImageList)
}
// 获取当前要渲染的子列表
......
<template>
<Teleport to="body">
<Transition
enter-active-class="transition-opacity duration-300 ease-out"
leave-active-class="transition-opacity duration-300 ease-in"
enter-from-class="opacity-0"
leave-to-class="opacity-0"
>
<div
v-if="visible"
class="fixed inset-0 z-9999 flex items-center justify-center p-4 isolate backdrop-blur-20px"
@click.stop
@keydown.stop
>
<div class="absolute inset-0 bg-black/45 backdrop-saturate-150" aria-hidden="true" />
<div
class="relative bg-white rounded-2xl max-w-full w-90 shadow-[0_8px_30px_rgba(100,116,180,0.16),0_2px_6px_rgba(0,0,0,0.04)]"
>
<div class="px-6 py-6">
<div
class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-red-50"
>
<svg
class="w-6 h-6 text-red-500"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
</div>
<h3 class="text-center text-base font-600 text-gray-800 leading-6">
{{ title }}
</h3>
<p class="mt-2 text-center text-13px text-gray-500 leading-5">
{{ messgae }}
</p>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
const { title = '登录异常' } = defineProps<{
title?: string
}>()
const messgae =
import.meta.env.MODE === '检测到未登录或者登录错误,' + 'production'
? '请关闭标签页,从企微工作台重新打开应用'
: '请关闭网页或重新打开网页携带code'
const visible = defineModel<boolean>({ default: false })
onMounted(() => {
visible.value = true
})
</script>
......@@ -32,7 +32,6 @@ export default defineComponent({
ref="fileInputRef"
onChange={handleFileChange}
accept="image/*"
multiple
/>
<div onClick={() => fileInputRef?.value?.click()}>{UploadImgIcon}</div>
</div>
......
......@@ -152,14 +152,15 @@ let cancelUploadController = () => {}
const startUpload = async () => {
if (!currentFile.value) return
let localVideoUrl = ''
try {
uploading.value = true
uploadError.value = ''
uploadProgress.value = 0
// 先提案获取视频的原信息 用本地blob
const blob = URL.createObjectURL(currentFile.value)
const metadataPromise = getVideoMetadata(blob)
localVideoUrl = URL.createObjectURL(currentFile.value)
const metadataPromise = getVideoMetadata(localVideoUrl)
const { promise, cancel } = uploadFileApi(currentFile.value, {
onProgress: (progress) => {
uploadProgress.value = progress
......@@ -198,6 +199,10 @@ const startUpload = async () => {
uploading.value = false
uploadError.value = error instanceof Error ? error.message : '上传失败,请重试'
push.error(uploadError.value)
} finally {
if (localVideoUrl) {
URL.revokeObjectURL(localVideoUrl)
}
}
}
......@@ -248,7 +253,13 @@ const replaceVideo = () => {
// 预览播放
const previewVideo = () => {
window.open(videoInfo.value?.url, '_blank')
const url = videoInfo.value?.url
if (!url) {
push.warning('暂无可预览的视频')
return
}
window.open(url, '_blank')
}
// 重试上传
......
......@@ -35,6 +35,8 @@ export interface PageSearchConfig<T extends PageSearchParams = PageSearchParams>
formatList?: (list: any[]) => any[]
/** 成功回调 */
success?: (list: Ref<any[]>) => void
/** 失败回调 */
error?: (error: unknown) => void
}
/**
......@@ -60,6 +62,7 @@ export function usePageSearch<
pageSizeField = 'size' as keyof TParams,
formatList = (list: any[]) => list,
success = () => {},
error: onError = () => {},
} = config
const loading = shallowRef(false)
......@@ -84,9 +87,10 @@ export function usePageSearch<
total.value = data.total || 0
success?.(list)
} catch (error) {
console.log('分页搜索失败:', error)
list.value = []
total.value = 0
console.log(error,'分页查询失败')
onError(error)
} finally {
loading.value = false
}
......
......@@ -15,6 +15,26 @@ type UseUploadImgReturnString = BaseReturn & {
// 传字符串数组时只返回基础
type UseUploadImgReturnArray = BaseReturn
type ImageValue = string | string[]
export const toImageList = (value: ImageValue): string[] => {
return Array.isArray(value) ? value.filter(Boolean) : value.split(',').filter(Boolean)
}
export function appendImageUrl(value: string, url: string): string
export function appendImageUrl(value: string[], url: string): string[]
export function appendImageUrl(value: ImageValue, url: string): ImageValue {
const nextList = [...toImageList(value), url].filter(Boolean)
return Array.isArray(value) ? nextList : nextList.join(',')
}
export function removeImageUrl(value: string, url: string): string
export function removeImageUrl(value: string[], url: string): string[]
export function removeImageUrl(value: ImageValue, url: string): ImageValue {
const nextList = toImageList(value).filter((item) => item !== url)
return Array.isArray(value) ? nextList : nextList.join(',')
}
// 直接传ref('imgs1,imgs2') 或者 ref(['img1','img2]) 传字符串的时候 会多返回一个imgList数组 便于模板使用遍历等
export function useUploadImg(imgs: Ref<string>): UseUploadImgReturnString
export function useUploadImg(imgs: Ref<string[]>): UseUploadImgReturnArray
......@@ -33,9 +53,9 @@ export function useUploadImg(imgs: Ref<string> | Ref<string[]>) {
})
const data = await promise
if (Array.isArray(imgs.value)) {
imgs.value = [...imgs.value, data.filePath]
imgs.value = appendImageUrl(imgs.value, data.filePath)
} else {
imgs.value = [...imgs.value.split(',').filter(Boolean), data.filePath].join(',')
imgs.value = appendImageUrl(imgs.value, data.filePath)
}
} catch (error) {
console.error('上传失败:', error)
......@@ -50,22 +70,14 @@ export function useUploadImg(imgs: Ref<string> | Ref<string[]>) {
// 删除图片
const handleDeleteImg = (urlStr: string) => {
if (Array.isArray(imgs.value)) {
imgs.value = imgs.value.filter((item) => item !== urlStr)
imgs.value = removeImageUrl(imgs.value, urlStr)
} else {
imgs.value =
imgs.value
.split(',')
.filter((item) => item !== urlStr)
.join(',') || ''
imgs.value = removeImageUrl(imgs.value, urlStr)
}
}
const imgList = computed(() => {
if (Array.isArray(imgs.value)) {
return imgs.value
} else {
return imgs.value.split(',').filter(Boolean)
}
return toImageList(imgs.value)
})
if (Array.isArray(imgs.value)) {
......
......@@ -47,7 +47,7 @@ import { useWindowSize } from '@vueuse/core'
import { getTodayOnlineSeconds, heartbeat } from '@/api'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { useOnlineTimeStore } from '@/stores'
import { useOnlineTimeStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
dayjs.extend(utc)
......@@ -55,6 +55,9 @@ dayjs.extend(utc)
const onlineTimeStore = useOnlineTimeStore()
const { showOnlineTime } = storeToRefs(onlineTimeStore)
const userStore = useUserStore()
const { userInfo, token } = storeToRefs(userStore)
const { height } = useWindowSize()
const CONTAINER_HEIGHT = 170,
GAP = 30
......@@ -81,7 +84,12 @@ const timer1 = setInterval(() => {
currentSeconds.value++
}, 1000)
const timer2 = setInterval(async () => {
heartbeat()
// 如果没有登录就不调用
if (!userInfo.value.id || !token.value) {
clearInterval(timer2)
} else {
heartbeat()
}
}, 1000 * 30)
onUnmounted(() => {
clearInterval(timer1)
......
......@@ -176,7 +176,7 @@ const userStore = useUserStore()
const activityStore = useActivityStore()
const { userInfo } = storeToRefs(userStore)
const isWareHouse = userInfo.value.address.includes('仓库')
const isWareHouse = userInfo.value.address?.includes('仓库')
const router = useRouter()
const route = useRoute()
......
......@@ -14,6 +14,9 @@ import service from './index'
import { useUserStore } from '@/stores/user'
import { push } from 'notivue'
import { generateLoginKey } from '@/api'
import LoginErrorModal from '@/components/common/LoginErrorModal/index.vue'
import { render, h } from 'vue'
/**
* 后端逻辑code报错处理
* @param backendServiceResult 后端传来错误信息
......@@ -118,6 +121,7 @@ function useService404() {
// 处理401的
let promiseFlashing: Promise<void> | null = null
let hasCatch = false
async function handleUnAuthorized(axiosError: AxiosError) {
const userStore = useUserStore()
if (!promiseFlashing) {
......@@ -132,10 +136,15 @@ async function handleUnAuthorized(axiosError: AxiosError) {
// 获取refreshToken接口报错
// 直接重新登陆
// 获取登陆的key
if (hasCatch) return
hasCatch = true
if (!userStore.userInfo.userId) {
return render(h(LoginErrorModal), document.body)
}
const { data } = await generateLoginKey({
timestamp: Date.now(),
type: 1,
userId: userStore.userInfo?.userId || '',
userId: userStore.userInfo?.userId,
})
// 清理sessionStorage
localStorage.clear()
......@@ -150,6 +159,7 @@ async function handleUnAuthorized(axiosError: AxiosError) {
// userStore.clearAllUserInfo()
// return Promise.reject(e)
} finally {
Promise.resolve().then(() => (hasCatch = false))
promiseFlashing = null
}
}
......
......@@ -72,7 +72,7 @@
</div>
<div class="flex items-center gap-1 bg-black/50 px-3 py-1.5 rounded-lg">
<el-icon class="text-sm"><IEpStar /></el-icon>
<span>{{ list[0]?.replyCount }}</span>
<span>{{ list[0]?.collectionCount }}</span>
</div>
</div>
</div>
......@@ -149,7 +149,7 @@
</div>
<div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg">
<el-icon class="text-sm"><IEpStar /></el-icon>
<span>{{ item?.replyCount }}</span>
<span>{{ item?.collectionCount }}</span>
</div>
</div>
</div>
......@@ -238,7 +238,7 @@
<el-icon class="text-sm">
<IEpStar />
</el-icon>
<span>{{ item.replyCount }}</span>
<span>{{ item.collectionCount }}</span>
</div>
</div>
<!-- 播放按钮 -->
......@@ -332,7 +332,7 @@
<el-icon class="text-sm">
<IEpStar />
</el-icon>
<span>{{ item.replyCount }}</span>
<span>{{ item.collectionCount }}</span>
</div>
</div>
<!-- 播放按钮 -->
......
......@@ -428,7 +428,7 @@ import { useTourStore, useUserStore } from '@/stores'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const isWareHouse = userInfo.value.address.includes('仓库')
const isWareHouse = userInfo.value.address?.includes('仓库')
const { confirm } = useMessageBox()
const tourStore = useTourStore()
const { shouldShowAskTabTour } = storeToRefs(tourStore)
......
......@@ -63,7 +63,6 @@
>
{{ item?.videoDuration }}
</div>
<!-- 数据 -->
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs">
<div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg">
......@@ -76,7 +75,7 @@
</div>
<div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg">
<el-icon class="text-sm"><IEpStar /></el-icon>
<span>{{ item.replyCount }}</span>
<span>{{ item.collectCount }}</span>
</div>
</div>
</div>
......@@ -94,9 +93,9 @@
</el-tooltip>
</div>
<span v-if="smallerThanXl">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD')
dayjs(item.publishTime * 1000).format('YYYY-MM-DD')
}}</span>
<span v-else>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
<span v-else>{{ dayjs(item.publishTime * 1000).format('YYYY-MM-DD HH:mm') }}</span>
</div>
</div>
</div>
......
......@@ -634,7 +634,7 @@ const handleVideoChange = ({
url: string
videoDuration: string
}) => {
locationVideoBlolUrl.value = form.value.videoUrl
locationVideoBlolUrl.value = parseUrl(form.value.videoUrl)
form.value.videoDuration = videoDuration
}
......@@ -647,6 +647,12 @@ const handleFileChange = async (e: Event) => {
const data = await promise
form.value.faceUrl = data.filePath
}
// 防止跨域
const parseUrl = (url: string) => {
return 'https://vikijin.site:8088' + '/oa/nfs' + new URL(url).pathname.replace('/database', '')
}
onDeactivated(() => {
// 清空页面的数据
resetPageData()
......@@ -658,7 +664,7 @@ onActivated(async () => {
if (route.query.id) {
const { data } = await getArticleDetail(route.query.id as string)
locationVideoBlolUrl.value = data.videoUrl
locationVideoBlolUrl.value = parseUrl(data.videoUrl)
form.value = {
...form.value,
......
......@@ -168,16 +168,16 @@
</button>
</div>
<div
v-if="questionDetail.imgUrl"
v-if="questionDetail.imgUrlList?.length"
class="mt-3 flex gap-2 flex-wrap items-center justify-start"
>
<el-image
v-for="(item, i) in questionDetail.imgUrl.split(',')"
v-for="(item, i) in questionDetail.imgUrlList"
:key="item"
:src="item"
fit="cover"
class="rounded-lg w-24 h-24 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list="questionDetail.imgUrl.split(',')"
:preview-src-list="questionDetail.imgUrlList"
:initial-index="i"
:preview-teleported="true"
/>
......@@ -237,13 +237,15 @@
<el-radio-group
size="small"
v-model="searchParams.sortType"
@change="(val) => changeSortType(val as number)"
@change="(val) => changeSortType(val as CommentSortTypeEnum)"
fill="#3b82f6"
>
<el-radio-button label="最多点赞" :value="CommentSortTypeEnum.MOST_LIKE" />
<el-radio-button label="最多评论" :value="CommentSortTypeEnum.MOST_COMMENT" />
<el-radio-button label="最早发布" :value="CommentSortTypeEnum.EARLIEST_PUBLISH" />
<el-radio-button label="最新发布" :value="CommentSortTypeEnum.NEWEST_PUBLISH" />
<el-radio-button
v-for="sortOption in answerSortOptions"
:key="sortOption.value"
:label="sortOption.label"
:value="sortOption.value"
/>
</el-radio-group>
<!-- <span
@click="changeSortType(2)"
......@@ -344,7 +346,7 @@
<!-- 评论图片列表 -->
<div class="flex flex-wrap gap-3 mb-3">
<div
v-for="(img, imgIndex) in answer.imgUrl.split(',').filter(Boolean)"
v-for="(img, imgIndex) in answer.imgUrlList"
:key="imgIndex"
class="w-24 h-24 rounded-lg overflow-hidden mb-2"
>
......@@ -352,7 +354,7 @@
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="answer.imgUrl.split(',').filter(Boolean)"
:preview-src-list="answer.imgUrlList"
:initial-index="imgIndex"
fit="cover"
/>
......@@ -501,7 +503,10 @@ const isOverThreeLine = computed(() => {
})
const getQuestionDetail = async () => {
const { data } = await getArticleDetail(questionId)
questionDetail.value = data
questionDetail.value = {
...data,
imgUrlList: getImageUrlList(data.imgUrl),
}
}
const {
......@@ -520,12 +525,24 @@ const {
formatList: (list) =>
list.map((item) => ({
...item,
imgUrlList: getImageUrlList(item.imgUrl),
showComment: false,
isExpand: false,
})),
})
const changeSortType = (type: number) => {
const getImageUrlList = (imgUrl?: string) => {
return imgUrl?.split(',').filter(Boolean) ?? []
}
const answerSortOptions = [
{ label: '最多点赞', value: CommentSortTypeEnum.MOST_LIKE },
{ label: '最多评论', value: CommentSortTypeEnum.MOST_COMMENT },
{ label: '最早发布', value: CommentSortTypeEnum.EARLIEST_PUBLISH },
{ label: '最新发布', value: CommentSortTypeEnum.NEWEST_PUBLISH },
]
const changeSortType = (type: CommentSortTypeEnum) => {
searchParams.value.sortType = type
refresh()
}
......
......@@ -126,6 +126,15 @@ const selectConversation = async (item: ConversationItem, shouldScrollToBottom =
await nextTick()
scrollToBottom()
}
// 刷新active的content
const lastItem = messageDetailList.value.at(-1)
if (lastItem) {
const index = conversationList.value.findIndex((i) => i.id === activeConversationId.value)
if (index !== -1) {
conversationList.value[index]!.last_message = lastItem.content
}
}
}
watch(
......@@ -218,9 +227,9 @@ const onMessageMenu = async (cmd: string | number, message: MessageDetailListIte
if (cmd !== 'delete') return
if (!message.is_self) return
await confirm({
title: '删除消息',
message: '确定删除这条消息?',
confirmText: '删除',
title: '撤回消息',
message: '确定撤回这条消息?',
confirmText: '撤回',
cancelText: '取消',
type: 'warning',
})
......@@ -228,7 +237,7 @@ const onMessageMenu = async (cmd: string | number, message: MessageDetailListIte
await deleteMessage({ type: 'message', idList: [message.message_id] })
// await refreshConversationList()
if (activeConversation.value) await selectConversation(activeConversation.value, false)
push.success('已删除消息')
push.success('已撤回消息')
}
onActivated(() => {
......@@ -427,9 +436,7 @@ onActivated(() => {
:class="{ 'items-end': message.is_self }"
>
<div class="flex items-center gap-2 text-3 text-[#8c9bb0]">
<span>{{
message.is_self ? userInfo.name : activeConversation.other_user_name
}}</span>
<span>{{ message.sender_name }}</span>
<span class="text-[#9eb0c6]">{{ message.create_time_str }}</span>
</div>
......@@ -454,7 +461,7 @@ onActivated(() => {
></p>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="delete"> 删除消息 </el-dropdown-item>
<el-dropdown-item command="delete"> 撤回消息 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
......
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