Commit 31150849 by 王立鹏

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

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

See merge request !29
parents f0a877f5 68a799ed
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint": "oxlint --fix && eslint . --fix --cache", "lint": "oxlint --fix && eslint . --fix --cache",
"lint:oxlint": "oxlint --fix", "lint:oxlint": "oxlint --fix",
...@@ -56,11 +57,13 @@ ...@@ -56,11 +57,13 @@
"@vitejs/plugin-vue-jsx": "^5.1.5", "@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.10",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"baseline-browser-mapping": "^2.9.14", "baseline-browser-mapping": "^2.9.14",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-plugin-vue": "~10.5.0", "eslint-plugin-vue": "~10.5.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"jsdom": "^29.1.1",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"oxfmt": "^0.44.0", "oxfmt": "^0.44.0",
"oxlint": "^1.59.0", "oxlint": "^1.59.0",
...@@ -74,6 +77,7 @@ ...@@ -74,6 +77,7 @@
"vite": "^8.0.0", "vite": "^8.0.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "^8.0.3", "vite-plugin-vue-devtools": "^8.0.3",
"vitest": "^4.1.6",
"vue-tsc": "^3.1.1" "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 { ...@@ -157,6 +157,7 @@ export interface ArticleItemDto {
hasPraised: boolean hasPraised: boolean
hasCollect: boolean hasCollect: boolean
imgUrl: string imgUrl: string
imgUrlList?: string[]
createUserAvatar: string createUserAvatar: string
createUserName: string createUserName: string
showAvatar: string showAvatar: string
...@@ -377,6 +378,7 @@ export interface CommentItemDto { ...@@ -377,6 +378,7 @@ export interface CommentItemDto {
isExpand: boolean isExpand: boolean
childNum: number childNum: number
imgUrl: string imgUrl: string
imgUrlList?: string[]
floorNumber: number floorNumber: number
} }
...@@ -444,6 +446,7 @@ export interface SearchMoreVideoItemDto { ...@@ -444,6 +446,7 @@ export interface SearchMoreVideoItemDto {
type: ArticleTypeEnum.VIDEO type: ArticleTypeEnum.VIDEO
videoDuration: string videoDuration: string
viewCount: number viewCount: number
publishTime: number
} }
/** /**
......
// 企业文化接口 // 企业文化接口
export * from "./task"; export * from './task'
export * from "./sign"; export * from './sign'
export * from "./shop"; export * from './shop'
export * from "./tag"; export * from './tag'
export * from "./user"; export * from './user'
export * from "./case"; export * from './case'
export * from "./home"; export * from './home'
export * from "./practice"; export * from './practice'
export * from "./common"; export * from './common'
export * from "./login"; export * from './login'
export * from "./article"; export * from './article'
export * from "./online"; export * from './online'
export * from "./otherUserPage"; export * from './otherUserPage'
export * from "./auction"; export * from './auction'
export * from "./dailyLottery"; export * from './dailyLottery'
export * from "./launchCampaign"; export * from './launchCampaign'
export * from "./selfMessage"; export * from './selfMessage'
export * from "./luckyWheel"; export * from './luckyWheel'
// 导出类型 // 导出类型
export * from "./task/types"; export * from './task/types'
export * from "./shop/types"; export * from './shop/types'
export * from "./tag/types"; export * from './tag/types'
export * from "./article/types"; export * from './article/types'
export * from "./user/types"; export * from './user/types'
export * from "./case/types"; export * from './case/types'
export * from "./home/types"; export * from './home/types'
export * from "./practice/types"; export * from './practice/types'
export * from "./common/types"; export * from './common/types'
export * from "./login/types"; export * from './login/types'
export * from "./article/types"; export * from './article/types'
export * from "./online/types"; export * from './online/types'
export * from "./otherUserPage/types"; export * from './otherUserPage/types'
export * from "./auction/types"; export * from './auction/types'
export * from "./dailyLottery/types"; export * from './dailyLottery/types'
export * from "./launchCampaign/types"; export * from './launchCampaign/types'
export * from "./selfMessage/types"; export * from './selfMessage/types'
export * from "./luckyWheel/types"; export * from './luckyWheel/types'
<template> <template>
<div <button
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" 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" @click="handleBack"
> >
<svg <svg
...@@ -19,7 +21,7 @@ ...@@ -19,7 +21,7 @@
> >
返回 返回
</span> </span>
</div> </button>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
......
...@@ -6,66 +6,18 @@ ...@@ -6,66 +6,18 @@
<span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span> <span class="text-lg font-semibold text-gray-800">评论 ({{ total }})</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button
v-for="sortOption in commentSortOptions"
:key="sortOption.value"
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative" 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="{ :class="{
'text-indigo-600 font-medium': 'text-indigo-600 font-medium': searchParams.sortType === sortOption.value,
searchParams.sortType === CommentSortTypeEnum.MOST_LIKE, 'text-gray-600 hover:text-gray-900': searchParams.sortType !== sortOption.value,
'text-gray-600 hover:text-gray-900':
searchParams.sortType !== CommentSortTypeEnum.MOST_LIKE,
}" }"
> >
最高点赞 {{ sortOption.label }}
<span <span
v-if="searchParams.sortType === CommentSortTypeEnum.MOST_LIKE" v-if="searchParams.sortType === sortOption.value"
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"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600" class="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600"
></span> ></span>
</button> </button>
...@@ -192,7 +144,7 @@ ...@@ -192,7 +144,7 @@
<!-- 评论图片列表 --> <!-- 评论图片列表 -->
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div <div
v-for="(img, imgIndex) in item.imgUrl.split(',').filter(Boolean)" v-for="(img, imgIndex) in item.imgUrlList"
:key="imgIndex" :key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2 cursor-pointer" class="w-20 h-20 rounded-lg overflow-hidden mb-2 cursor-pointer"
> >
...@@ -200,7 +152,7 @@ ...@@ -200,7 +152,7 @@
:src="img" :src="img"
:preview-teleported="true" :preview-teleported="true"
class="w-full h-full object-cover" class="w-full h-full object-cover"
:preview-src-list="item.imgUrl.split(',').filter(Boolean)" :preview-src-list="item.imgUrlList"
:initial-index="imgIndex" :initial-index="imgIndex"
fit="cover" fit="cover"
/> />
...@@ -276,7 +228,7 @@ ...@@ -276,7 +228,7 @@
<!-- 评论图片列表 --> <!-- 评论图片列表 -->
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<div <div
v-for="(img, imgIndex) in child.imgUrl.split(',').filter(Boolean)" v-for="(img, imgIndex) in child.imgUrlList"
:key="imgIndex" :key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2" class="w-20 h-20 rounded-lg overflow-hidden mb-2"
> >
...@@ -284,7 +236,7 @@ ...@@ -284,7 +236,7 @@
:src="img" :src="img"
:preview-teleported="true" :preview-teleported="true"
class="w-full h-full object-cover" class="w-full h-full object-cover"
:preview-src-list="child.imgUrl.split(',').filter(Boolean)" :preview-src-list="child.imgUrlList"
:initial-index="imgIndex" :initial-index="imgIndex"
fit="cover" fit="cover"
/> />
...@@ -535,7 +487,11 @@ const { ...@@ -535,7 +487,11 @@ const {
return list.map((item) => { return list.map((item) => {
// 初始化的时候 添加新的字段 是否展示分页子评论 默认是false 当前子评论分页current 以及子评论分页列表 loading效果 // 初始化的时候 添加新的字段 是否展示分页子评论 默认是false 当前子评论分页current 以及子评论分页列表 loading效果
const obj: CommentItemDto = { ...item } const obj: CommentItemDto = {
...item,
imgUrlList: getCommentImageList(item.imgUrl),
children: item.children?.map(formatCommentImageList) ?? [],
}
// if (obj.showChildrenPage == undefined) { // if (obj.showChildrenPage == undefined) {
obj.showChildrenPage = false obj.showChildrenPage = false
// } // }
...@@ -554,11 +510,33 @@ const { ...@@ -554,11 +510,33 @@ const {
}, },
immediate, 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) => { const handleCurrentChange = async (e: number) => {
await goToPage(e) await goToPage(e)
handleBackTop() handleBackTop()
} }
const getCommentImageList = (imgUrl?: string) => {
return imgUrl?.split(',').filter(Boolean) ?? []
}
const formatCommentImageList = (item: CommentItemDto): CommentItemDto => ({
...item,
imgUrlList: getCommentImageList(item.imgUrl),
})
// 自己发出的评论 // 自己发出的评论
const myComment = ref('') const myComment = ref('')
const myCommentLoading = ref(false) const myCommentLoading = ref(false)
...@@ -740,7 +718,7 @@ const getCommentChildrenList = async (item: CommentItemDto) => { ...@@ -740,7 +718,7 @@ const getCommentChildrenList = async (item: CommentItemDto) => {
current: item.childrenPageCurrent, current: item.childrenPageCurrent,
size: 10, 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({ ...@@ -32,7 +32,6 @@ export default defineComponent({
ref="fileInputRef" ref="fileInputRef"
onChange={handleFileChange} onChange={handleFileChange}
accept="image/*" accept="image/*"
multiple
/> />
<div onClick={() => fileInputRef?.value?.click()}>{UploadImgIcon}</div> <div onClick={() => fileInputRef?.value?.click()}>{UploadImgIcon}</div>
</div> </div>
......
...@@ -152,14 +152,15 @@ let cancelUploadController = () => {} ...@@ -152,14 +152,15 @@ let cancelUploadController = () => {}
const startUpload = async () => { const startUpload = async () => {
if (!currentFile.value) return if (!currentFile.value) return
let localVideoUrl = ''
try { try {
uploading.value = true uploading.value = true
uploadError.value = '' uploadError.value = ''
uploadProgress.value = 0 uploadProgress.value = 0
// 先提案获取视频的原信息 用本地blob // 先提案获取视频的原信息 用本地blob
const blob = URL.createObjectURL(currentFile.value) localVideoUrl = URL.createObjectURL(currentFile.value)
const metadataPromise = getVideoMetadata(blob) const metadataPromise = getVideoMetadata(localVideoUrl)
const { promise, cancel } = uploadFileApi(currentFile.value, { const { promise, cancel } = uploadFileApi(currentFile.value, {
onProgress: (progress) => { onProgress: (progress) => {
uploadProgress.value = progress uploadProgress.value = progress
...@@ -198,6 +199,10 @@ const startUpload = async () => { ...@@ -198,6 +199,10 @@ const startUpload = async () => {
uploading.value = false uploading.value = false
uploadError.value = error instanceof Error ? error.message : '上传失败,请重试' uploadError.value = error instanceof Error ? error.message : '上传失败,请重试'
push.error(uploadError.value) push.error(uploadError.value)
} finally {
if (localVideoUrl) {
URL.revokeObjectURL(localVideoUrl)
}
} }
} }
...@@ -248,7 +253,13 @@ const replaceVideo = () => { ...@@ -248,7 +253,13 @@ const replaceVideo = () => {
// 预览播放 // 预览播放
const previewVideo = () => { 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> ...@@ -35,6 +35,8 @@ export interface PageSearchConfig<T extends PageSearchParams = PageSearchParams>
formatList?: (list: any[]) => any[] formatList?: (list: any[]) => any[]
/** 成功回调 */ /** 成功回调 */
success?: (list: Ref<any[]>) => void success?: (list: Ref<any[]>) => void
/** 失败回调 */
error?: (error: unknown) => void
} }
/** /**
...@@ -60,6 +62,7 @@ export function usePageSearch< ...@@ -60,6 +62,7 @@ export function usePageSearch<
pageSizeField = 'size' as keyof TParams, pageSizeField = 'size' as keyof TParams,
formatList = (list: any[]) => list, formatList = (list: any[]) => list,
success = () => {}, success = () => {},
error: onError = () => {},
} = config } = config
const loading = shallowRef(false) const loading = shallowRef(false)
...@@ -84,9 +87,10 @@ export function usePageSearch< ...@@ -84,9 +87,10 @@ export function usePageSearch<
total.value = data.total || 0 total.value = data.total || 0
success?.(list) success?.(list)
} catch (error) { } catch (error) {
console.log('分页搜索失败:', error)
list.value = [] list.value = []
total.value = 0 total.value = 0
console.log(error,'分页查询失败')
onError(error)
} finally { } finally {
loading.value = false loading.value = false
} }
......
...@@ -15,6 +15,26 @@ type UseUploadImgReturnString = BaseReturn & { ...@@ -15,6 +15,26 @@ type UseUploadImgReturnString = BaseReturn & {
// 传字符串数组时只返回基础 // 传字符串数组时只返回基础
type UseUploadImgReturnArray = 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数组 便于模板使用遍历等 // 直接传ref('imgs1,imgs2') 或者 ref(['img1','img2]) 传字符串的时候 会多返回一个imgList数组 便于模板使用遍历等
export function useUploadImg(imgs: Ref<string>): UseUploadImgReturnString export function useUploadImg(imgs: Ref<string>): UseUploadImgReturnString
export function useUploadImg(imgs: Ref<string[]>): UseUploadImgReturnArray export function useUploadImg(imgs: Ref<string[]>): UseUploadImgReturnArray
...@@ -33,9 +53,9 @@ export function useUploadImg(imgs: Ref<string> | Ref<string[]>) { ...@@ -33,9 +53,9 @@ export function useUploadImg(imgs: Ref<string> | Ref<string[]>) {
}) })
const data = await promise const data = await promise
if (Array.isArray(imgs.value)) { if (Array.isArray(imgs.value)) {
imgs.value = [...imgs.value, data.filePath] imgs.value = appendImageUrl(imgs.value, data.filePath)
} else { } else {
imgs.value = [...imgs.value.split(',').filter(Boolean), data.filePath].join(',') imgs.value = appendImageUrl(imgs.value, data.filePath)
} }
} catch (error) { } catch (error) {
console.error('上传失败:', error) console.error('上传失败:', error)
...@@ -50,22 +70,14 @@ export function useUploadImg(imgs: Ref<string> | Ref<string[]>) { ...@@ -50,22 +70,14 @@ export function useUploadImg(imgs: Ref<string> | Ref<string[]>) {
// 删除图片 // 删除图片
const handleDeleteImg = (urlStr: string) => { const handleDeleteImg = (urlStr: string) => {
if (Array.isArray(imgs.value)) { if (Array.isArray(imgs.value)) {
imgs.value = imgs.value.filter((item) => item !== urlStr) imgs.value = removeImageUrl(imgs.value, urlStr)
} else { } else {
imgs.value = imgs.value = removeImageUrl(imgs.value, urlStr)
imgs.value
.split(',')
.filter((item) => item !== urlStr)
.join(',') || ''
} }
} }
const imgList = computed(() => { const imgList = computed(() => {
if (Array.isArray(imgs.value)) { return toImageList(imgs.value)
return imgs.value
} else {
return imgs.value.split(',').filter(Boolean)
}
}) })
if (Array.isArray(imgs.value)) { if (Array.isArray(imgs.value)) {
......
...@@ -47,7 +47,7 @@ import { useWindowSize } from '@vueuse/core' ...@@ -47,7 +47,7 @@ import { useWindowSize } from '@vueuse/core'
import { getTodayOnlineSeconds, heartbeat } from '@/api' import { getTodayOnlineSeconds, heartbeat } from '@/api'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import { useOnlineTimeStore } from '@/stores' import { useOnlineTimeStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
dayjs.extend(utc) dayjs.extend(utc)
...@@ -55,6 +55,9 @@ dayjs.extend(utc) ...@@ -55,6 +55,9 @@ dayjs.extend(utc)
const onlineTimeStore = useOnlineTimeStore() const onlineTimeStore = useOnlineTimeStore()
const { showOnlineTime } = storeToRefs(onlineTimeStore) const { showOnlineTime } = storeToRefs(onlineTimeStore)
const userStore = useUserStore()
const { userInfo, token } = storeToRefs(userStore)
const { height } = useWindowSize() const { height } = useWindowSize()
const CONTAINER_HEIGHT = 170, const CONTAINER_HEIGHT = 170,
GAP = 30 GAP = 30
...@@ -81,7 +84,12 @@ const timer1 = setInterval(() => { ...@@ -81,7 +84,12 @@ const timer1 = setInterval(() => {
currentSeconds.value++ currentSeconds.value++
}, 1000) }, 1000)
const timer2 = setInterval(async () => { const timer2 = setInterval(async () => {
heartbeat() // 如果没有登录就不调用
if (!userInfo.value.id || !token.value) {
clearInterval(timer2)
} else {
heartbeat()
}
}, 1000 * 30) }, 1000 * 30)
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer1) clearInterval(timer1)
......
...@@ -176,7 +176,7 @@ const userStore = useUserStore() ...@@ -176,7 +176,7 @@ const userStore = useUserStore()
const activityStore = useActivityStore() const activityStore = useActivityStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const isWareHouse = userInfo.value.address.includes('仓库') const isWareHouse = userInfo.value.address?.includes('仓库')
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
......
...@@ -14,6 +14,9 @@ import service from './index' ...@@ -14,6 +14,9 @@ import service from './index'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { push } from 'notivue' import { push } from 'notivue'
import { generateLoginKey } from '@/api' import { generateLoginKey } from '@/api'
import LoginErrorModal from '@/components/common/LoginErrorModal/index.vue'
import { render, h } from 'vue'
/** /**
* 后端逻辑code报错处理 * 后端逻辑code报错处理
* @param backendServiceResult 后端传来错误信息 * @param backendServiceResult 后端传来错误信息
...@@ -118,6 +121,7 @@ function useService404() { ...@@ -118,6 +121,7 @@ function useService404() {
// 处理401的 // 处理401的
let promiseFlashing: Promise<void> | null = null let promiseFlashing: Promise<void> | null = null
let hasCatch = false
async function handleUnAuthorized(axiosError: AxiosError) { async function handleUnAuthorized(axiosError: AxiosError) {
const userStore = useUserStore() const userStore = useUserStore()
if (!promiseFlashing) { if (!promiseFlashing) {
...@@ -132,10 +136,15 @@ async function handleUnAuthorized(axiosError: AxiosError) { ...@@ -132,10 +136,15 @@ async function handleUnAuthorized(axiosError: AxiosError) {
// 获取refreshToken接口报错 // 获取refreshToken接口报错
// 直接重新登陆 // 直接重新登陆
// 获取登陆的key // 获取登陆的key
if (hasCatch) return
hasCatch = true
if (!userStore.userInfo.userId) {
return render(h(LoginErrorModal), document.body)
}
const { data } = await generateLoginKey({ const { data } = await generateLoginKey({
timestamp: Date.now(), timestamp: Date.now(),
type: 1, type: 1,
userId: userStore.userInfo?.userId || '', userId: userStore.userInfo?.userId,
}) })
// 清理sessionStorage // 清理sessionStorage
localStorage.clear() localStorage.clear()
...@@ -150,6 +159,7 @@ async function handleUnAuthorized(axiosError: AxiosError) { ...@@ -150,6 +159,7 @@ async function handleUnAuthorized(axiosError: AxiosError) {
// userStore.clearAllUserInfo() // userStore.clearAllUserInfo()
// return Promise.reject(e) // return Promise.reject(e)
} finally { } finally {
Promise.resolve().then(() => (hasCatch = false))
promiseFlashing = null promiseFlashing = null
} }
} }
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
</div> </div>
<div class="flex items-center gap-1 bg-black/50 px-3 py-1.5 rounded-lg"> <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> <el-icon class="text-sm"><IEpStar /></el-icon>
<span>{{ list[0]?.replyCount }}</span> <span>{{ list[0]?.collectionCount }}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
</div> </div>
<div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg"> <div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg">
<el-icon class="text-sm"><IEpStar /></el-icon> <el-icon class="text-sm"><IEpStar /></el-icon>
<span>{{ item?.replyCount }}</span> <span>{{ item?.collectionCount }}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -238,7 +238,7 @@ ...@@ -238,7 +238,7 @@
<el-icon class="text-sm"> <el-icon class="text-sm">
<IEpStar /> <IEpStar />
</el-icon> </el-icon>
<span>{{ item.replyCount }}</span> <span>{{ item.collectionCount }}</span>
</div> </div>
</div> </div>
<!-- 播放按钮 --> <!-- 播放按钮 -->
...@@ -332,7 +332,7 @@ ...@@ -332,7 +332,7 @@
<el-icon class="text-sm"> <el-icon class="text-sm">
<IEpStar /> <IEpStar />
</el-icon> </el-icon>
<span>{{ item.replyCount }}</span> <span>{{ item.collectionCount }}</span>
</div> </div>
</div> </div>
<!-- 播放按钮 --> <!-- 播放按钮 -->
......
...@@ -428,7 +428,7 @@ import { useTourStore, useUserStore } from '@/stores' ...@@ -428,7 +428,7 @@ import { useTourStore, useUserStore } from '@/stores'
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const isWareHouse = userInfo.value.address.includes('仓库') const isWareHouse = userInfo.value.address?.includes('仓库')
const { confirm } = useMessageBox() const { confirm } = useMessageBox()
const tourStore = useTourStore() const tourStore = useTourStore()
const { shouldShowAskTabTour } = storeToRefs(tourStore) const { shouldShowAskTabTour } = storeToRefs(tourStore)
......
...@@ -63,7 +63,6 @@ ...@@ -63,7 +63,6 @@
> >
{{ item?.videoDuration }} {{ item?.videoDuration }}
</div> </div>
<!-- 数据 --> <!-- 数据 -->
<div class="absolute bottom-3 left-3 flex gap-3 text-white text-xs"> <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"> <div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg">
...@@ -76,7 +75,7 @@ ...@@ -76,7 +75,7 @@
</div> </div>
<div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg"> <div class="flex items-center gap-1 bg-black/50 px-2 py-1 rounded-lg">
<el-icon class="text-sm"><IEpStar /></el-icon> <el-icon class="text-sm"><IEpStar /></el-icon>
<span>{{ item.replyCount }}</span> <span>{{ item.collectCount }}</span>
</div> </div>
</div> </div>
</div> </div>
...@@ -94,9 +93,9 @@ ...@@ -94,9 +93,9 @@
</el-tooltip> </el-tooltip>
</div> </div>
<span v-if="smallerThanXl">{{ <span v-if="smallerThanXl">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD') dayjs(item.publishTime * 1000).format('YYYY-MM-DD')
}}</span> }}</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> </div>
</div> </div>
......
...@@ -634,7 +634,7 @@ const handleVideoChange = ({ ...@@ -634,7 +634,7 @@ const handleVideoChange = ({
url: string url: string
videoDuration: string videoDuration: string
}) => { }) => {
locationVideoBlolUrl.value = form.value.videoUrl locationVideoBlolUrl.value = parseUrl(form.value.videoUrl)
form.value.videoDuration = videoDuration form.value.videoDuration = videoDuration
} }
...@@ -647,6 +647,12 @@ const handleFileChange = async (e: Event) => { ...@@ -647,6 +647,12 @@ const handleFileChange = async (e: Event) => {
const data = await promise const data = await promise
form.value.faceUrl = data.filePath form.value.faceUrl = data.filePath
} }
// 防止跨域
const parseUrl = (url: string) => {
return 'https://vikijin.site:8088' + '/oa/nfs' + new URL(url).pathname.replace('/database', '')
}
onDeactivated(() => { onDeactivated(() => {
// 清空页面的数据 // 清空页面的数据
resetPageData() resetPageData()
...@@ -658,7 +664,7 @@ onActivated(async () => { ...@@ -658,7 +664,7 @@ onActivated(async () => {
if (route.query.id) { if (route.query.id) {
const { data } = await getArticleDetail(route.query.id as string) const { data } = await getArticleDetail(route.query.id as string)
locationVideoBlolUrl.value = data.videoUrl locationVideoBlolUrl.value = parseUrl(data.videoUrl)
form.value = { form.value = {
...form.value, ...form.value,
......
...@@ -168,16 +168,16 @@ ...@@ -168,16 +168,16 @@
</button> </button>
</div> </div>
<div <div
v-if="questionDetail.imgUrl" v-if="questionDetail.imgUrlList?.length"
class="mt-3 flex gap-2 flex-wrap items-center justify-start" class="mt-3 flex gap-2 flex-wrap items-center justify-start"
> >
<el-image <el-image
v-for="(item, i) in questionDetail.imgUrl.split(',')" v-for="(item, i) in questionDetail.imgUrlList"
:key="item" :key="item"
:src="item" :src="item"
fit="cover" fit="cover"
class="rounded-lg w-24 h-24 hover:scale-105 transition-transform cursor-pointer" 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" :initial-index="i"
:preview-teleported="true" :preview-teleported="true"
/> />
...@@ -237,13 +237,15 @@ ...@@ -237,13 +237,15 @@
<el-radio-group <el-radio-group
size="small" size="small"
v-model="searchParams.sortType" v-model="searchParams.sortType"
@change="(val) => changeSortType(val as number)" @change="(val) => changeSortType(val as CommentSortTypeEnum)"
fill="#3b82f6" fill="#3b82f6"
> >
<el-radio-button label="最多点赞" :value="CommentSortTypeEnum.MOST_LIKE" /> <el-radio-button
<el-radio-button label="最多评论" :value="CommentSortTypeEnum.MOST_COMMENT" /> v-for="sortOption in answerSortOptions"
<el-radio-button label="最早发布" :value="CommentSortTypeEnum.EARLIEST_PUBLISH" /> :key="sortOption.value"
<el-radio-button label="最新发布" :value="CommentSortTypeEnum.NEWEST_PUBLISH" /> :label="sortOption.label"
:value="sortOption.value"
/>
</el-radio-group> </el-radio-group>
<!-- <span <!-- <span
@click="changeSortType(2)" @click="changeSortType(2)"
...@@ -344,7 +346,7 @@ ...@@ -344,7 +346,7 @@
<!-- 评论图片列表 --> <!-- 评论图片列表 -->
<div class="flex flex-wrap gap-3 mb-3"> <div class="flex flex-wrap gap-3 mb-3">
<div <div
v-for="(img, imgIndex) in answer.imgUrl.split(',').filter(Boolean)" v-for="(img, imgIndex) in answer.imgUrlList"
:key="imgIndex" :key="imgIndex"
class="w-24 h-24 rounded-lg overflow-hidden mb-2" class="w-24 h-24 rounded-lg overflow-hidden mb-2"
> >
...@@ -352,7 +354,7 @@ ...@@ -352,7 +354,7 @@
:src="img" :src="img"
:preview-teleported="true" :preview-teleported="true"
class="w-full h-full object-cover" class="w-full h-full object-cover"
:preview-src-list="answer.imgUrl.split(',').filter(Boolean)" :preview-src-list="answer.imgUrlList"
:initial-index="imgIndex" :initial-index="imgIndex"
fit="cover" fit="cover"
/> />
...@@ -501,7 +503,10 @@ const isOverThreeLine = computed(() => { ...@@ -501,7 +503,10 @@ const isOverThreeLine = computed(() => {
}) })
const getQuestionDetail = async () => { const getQuestionDetail = async () => {
const { data } = await getArticleDetail(questionId) const { data } = await getArticleDetail(questionId)
questionDetail.value = data questionDetail.value = {
...data,
imgUrlList: getImageUrlList(data.imgUrl),
}
} }
const { const {
...@@ -520,12 +525,24 @@ const { ...@@ -520,12 +525,24 @@ const {
formatList: (list) => formatList: (list) =>
list.map((item) => ({ list.map((item) => ({
...item, ...item,
imgUrlList: getImageUrlList(item.imgUrl),
showComment: false, showComment: false,
isExpand: 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 searchParams.value.sortType = type
refresh() refresh()
} }
......
...@@ -126,6 +126,15 @@ const selectConversation = async (item: ConversationItem, shouldScrollToBottom = ...@@ -126,6 +126,15 @@ const selectConversation = async (item: ConversationItem, shouldScrollToBottom =
await nextTick() await nextTick()
scrollToBottom() 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( watch(
...@@ -218,9 +227,9 @@ const onMessageMenu = async (cmd: string | number, message: MessageDetailListIte ...@@ -218,9 +227,9 @@ const onMessageMenu = async (cmd: string | number, message: MessageDetailListIte
if (cmd !== 'delete') return if (cmd !== 'delete') return
if (!message.is_self) return if (!message.is_self) return
await confirm({ await confirm({
title: '删除消息', title: '撤回消息',
message: '确定删除这条消息?', message: '确定撤回这条消息?',
confirmText: '删除', confirmText: '撤回',
cancelText: '取消', cancelText: '取消',
type: 'warning', type: 'warning',
}) })
...@@ -228,7 +237,7 @@ const onMessageMenu = async (cmd: string | number, message: MessageDetailListIte ...@@ -228,7 +237,7 @@ const onMessageMenu = async (cmd: string | number, message: MessageDetailListIte
await deleteMessage({ type: 'message', idList: [message.message_id] }) await deleteMessage({ type: 'message', idList: [message.message_id] })
// await refreshConversationList() // await refreshConversationList()
if (activeConversation.value) await selectConversation(activeConversation.value, false) if (activeConversation.value) await selectConversation(activeConversation.value, false)
push.success('已删除消息') push.success('已撤回消息')
} }
onActivated(() => { onActivated(() => {
...@@ -427,9 +436,7 @@ onActivated(() => { ...@@ -427,9 +436,7 @@ onActivated(() => {
:class="{ 'items-end': message.is_self }" :class="{ 'items-end': message.is_self }"
> >
<div class="flex items-center gap-2 text-3 text-[#8c9bb0]"> <div class="flex items-center gap-2 text-3 text-[#8c9bb0]">
<span>{{ <span>{{ message.sender_name }}</span>
message.is_self ? userInfo.name : activeConversation.other_user_name
}}</span>
<span class="text-[#9eb0c6]">{{ message.create_time_str }}</span> <span class="text-[#9eb0c6]">{{ message.create_time_str }}</span>
</div> </div>
...@@ -454,7 +461,7 @@ onActivated(() => { ...@@ -454,7 +461,7 @@ onActivated(() => {
></p> ></p>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="delete"> 删除消息 </el-dropdown-item> <el-dropdown-item command="delete"> 撤回消息 </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </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