From 1d552a3bad95caae4af15322df28e6240b797693 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Thu, 13 Aug 2026 15:32:06 +0800
Subject: [PATCH] feat: app视频封面图

---
 uniapps/work-app/src/pages/work/index.vue |  876 +++++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 595 insertions(+), 281 deletions(-)

diff --git a/uniapps/work-app/src/pages/work/index.vue b/uniapps/work-app/src/pages/work/index.vue
index a0f981e..f9dea90 100644
--- a/uniapps/work-app/src/pages/work/index.vue
+++ b/uniapps/work-app/src/pages/work/index.vue
@@ -1,334 +1,648 @@
 <template>
-  <div class="eventTickets" :style="{ paddingTop: topMargin + 'px' }">
+	<div class="eventTickets" :style="{ paddingTop: topMargin + 'px' }">
+		<div class="searchTop">
+			<up-search
+				placeholder="请输入关键字搜索"
+				:animation="true"
+				v-model="listParams.keyword"
+				:show-action="false"
+				@search="handleSearch"
+				@clear="handleClear"
+			></up-search>
+		</div>
+		<div class="listBox">
+			<div class="tabs-container">
+				<up-tabs :scrollable="true" :list="tabList" @click="handleClick"></up-tabs>
+			</div>
+			<scroll-view class="eventBox" scroll-y :lower-threshold="80" @scrolltolower="loadMore">
+				<div class="eventList">
+					<div class="eventItem" v-for="(item, index) in dataList" :key="index">
+						<image v-if="item.isImage" :src="item.thumbnail" mode="aspectFill" @click="detailHandle(item)" />
+						<div
+							v-else-if="item.isVideo"
+							class="videoBox"
+							:style="item.thumbnailUrl ? { backgroundImage: `url(${item.thumbnailUrl})`, backgroundSize: 'cover', backgroundPosition: 'center' } : {}"
+							@click="detailHandle(item)"
+						>
+							<div class="playIcon"></div>
+						</div>
+						<div class="informationDisplay">
+							<div class="itemTitle">{{ item.eventName }}</div>
+							<div class="itemContent">{{ formatDate(item.createTime) }}</div>
+						</div>
+					</div>
+				</div>
 
-    <div class="searchTop">
-      <up-search placeholder="请输入关键字搜索"  :animation="true"  v-model="listParams.keyword" :show-action="false" @search="handleSearch" @clear="handleClear"></up-search>
-    </div>
-    <div class="listBox">
-      <div class="tabs-container">
-        <up-tabs :list="tabList" @click="handleClick"></up-tabs>
-      </div>
-<!-- @scrolltolower="loadMore" -->
-      <scroll-view class="eventBox" scroll-y :lower-threshold="80" >
-        <div class="eventList">
-          <div class="eventItem"  v-for="(item,index) in dataList" :key="index">
-            <img :src="item.eventImageUrl" alt="" @click="detailHandle(item)" />
-            <div class="informationDisplay">
-              <!--              <div class="itemTitle">{{item.event_name}}</div>-->
-              <div class="itemContent">{{formatDate(item.createTime) }}</div>
-            </div>
-          </div>
-        </div>
+				<!-- 暂无数据提示 -->
+				<div class="noData" v-if="!loading && dataList.length === 0">
+					<text>暂无数据</text>
+				</div>
 
-        <!-- 暂无数据提示 -->
-        <div class="noData" v-if="!loading && dataList.length === 0">
-          <text>暂无数据</text>
-        </div>
-
-        <!-- 加载提示 -->
-        <div class="loadingMore">
-          <text v-if="loading">加载中...</text>
-
-        </div>
-      </scroll-view>
-    </div>
-
-  </div>
-
+				<!-- 加载提示 -->
+				<div class="loadingMore">
+					<text v-if="loading">加载中...</text>
+					<text v-else-if="!hasMore">没有更多了</text>
+				</div>
+			</scroll-view>
+		</div>
+		<canvas
+			canvas-id="ai-image-canvas"
+			id="ai-image-canvas"
+			class="ai-image-canvas"
+			:width="canvasSize.width"
+			:height="canvasSize.height"
+			:style="{ width: canvasSize.width + 'px', height: canvasSize.height + 'px' }"
+		></canvas>
+	</div>
 </template>
 
 <script setup>
-
+import { getCurrentInstance } from 'vue'
 import { useUserStore } from '@/store/index.js'
-import {getGdList,getstatusCount} from '/src/api/work/index.js'
-import dayjs from 'dayjs';
-import { getStatusBarHeight } from '@/utils/common';
+import { getGdList, getstatusCount } from '@/api/work/index.js'
+import dayjs from 'dayjs'
+import { getStatusBarHeight } from '@/utils/common'
+
+const canvasOwner = getCurrentInstance()?.proxy
 const userStore = useUserStore()
 const userInfo = userStore.userInfo
 const dataList = ref([])
-const currentTab=ref('all')
+const currentTab = ref('all')
 const loading = ref(false)
 const hasMore = ref(true)
+const PREVIEW_IMG_CACHE_KEY_PREFIX = 'work-detail-ai-img-'
+const imageTaskMap = new Map()
+let imageRenderQueue = Promise.resolve()
 const tabList = ref([
-  {
-    name: '全部工单',
-    key: 'all',
-    badge: {
-      value: 0,
-      showZero: true
-    },
-
-  },
-  {
-    name: '我的工单',
-    key: 'myTickets',
-    badge: {
-      value: 0,
-      showZero: true
-    }
-  }
-
-
+	{
+		name: '全部工单',
+		key: 'all',
+		badge: {
+			value: 0,
+			showZero: true,
+		},
+	},
+	{
+		name: '我的工单',
+		key: 'myTickets',
+		badge: {
+			value: 0,
+			showZero: true,
+		},
+	},
+	{
+		name: '处置中',
+		key: 'disposing',
+		badge: {
+			value: 0,
+			showZero: true,
+		},
+	},
+	{
+		name: '已处置',
+		key: 'handled',
+		badge: {
+			value: 0,
+			showZero: true,
+		},
+	},
+	{
+		name: '已完成',
+		key: 'completed',
+		badge: {
+			value: 0,
+			showZero: true,
+		},
+	},
 ])
-const formatDate = (dateString) => {
-  return dayjs(dateString).format('MM/DD HH:mm');
-};
-const listParams = ref({
-
-  current: 1,
-  size: 12,
-  source: 1,
-  department:'',
-  keyword:''
-
-})
-const getDataList = () => {
-  if (loading.value || !hasMore.value) return
-
-  loading.value = true
-  const params = {
-    current: listParams.value.current,
-    size: listParams.value.size,
-    keyword:listParams.value.keyword,
-    onlyMine:currentTab.value=== 'myTickets' ? 1 : 0
-  }
-  getGdList(params).then(res => {
-    const response = res.data.data
-    dataList.value = [
-      {
-        "id": "2018864760937254914",
-        "eventImageUrl": "https://wrj.shuixiongit.com/minio/cloud-bucket/78611c83-8fb3-47c9-a18c-0349137bb30c/DJI_202601171733_001_78611c83-8fb3-47c9-a18c-0349137bb30c/DJI_20260117173534_0004_V.jpeg",
-        "createTime": "2026-02-04 09:50:42"
-      },
-      {
-        "id": "2018864819590402049",
-        "eventImageUrl": "https://wrj.shuixiongit.com/minio/cloud-bucket/78611c83-8fb3-47c9-a18c-0349137bb30c/DJI_202601171733_001_78611c83-8fb3-47c9-a18c-0349137bb30c/DJI_20260117173534_0004_V.jpeg",
-        "createTime": "2026-02-04 09:50:56"
-      }
-    ]
-    // 根据当前页码决定是替换还是追加数据
-    // if (listParams.value.current === 1) {
-    //   dataList.value = response
-    // } else {
-    //   dataList.value = [...dataList.value, ...response]
-    // }
-
-    // // 判断是否还有更多数据
-    // if (response.length < listParams.value.size || response.current >= response.pages) {
-    //   hasMore.value = false
-    // } else {
-    //   hasMore.value = true
-    // }
-  }).finally(() => {
-    loading.value = false
-  })
+// tab key 与事件状态码映射
+const tabStatusMap = {
+	disposing: 1,
+	handled: 3,
+	completed: 4,
 }
-const getstatusCountData=()=>{
-  const params={
-    keyword:listParams.value.keyword,
-  }
-	getstatusCount(params).then(res=>{
-		const response = res.data.data
-		 const {  totalCount,myCount } = response
-		tabList.value.forEach(tab=>{
+const formatDate = dateString => {
+	return dayjs(dateString).format('MM/DD HH:mm')
+}
+const listParams = ref({
+	current: 1,
+	size: 12,
+	source: 1,
+	department: '',
+	keyword: '',
+	eventStatus: '',
+})
+let requestSeq = 0 // 请求序列号,用于丢弃快速切换tab时的过期响应
+let imageGeneration = 0 // 图片任务批次,用于取消已离开列表的绘制任务
+const MAX_CANVAS_EDGE = 2048
+const canvasSize = ref({
+	width: MAX_CANVAS_EDGE,
+	height: MAX_CANVAS_EDGE,
+})
 
-			if(tab.key === 'all'){
-        tab.badge.value = (totalCount || 0).toString()
-        tab.badge.showZero = true
+// 解析算法框数据
+const parseAiFrame = aiFrameSource => {
+	if (!aiFrameSource) return []
+	if (Array.isArray(aiFrameSource)) return aiFrameSource
 
-			}else if(tab.key === 'myTickets'){
-				tab.badge.value = (myCount || 0).toString()
-        tab.badge.showZero = true
-			}
+	try {
+		return JSON.parse(aiFrameSource)
+	} catch (error) {
+		console.log(error)
+		return []
+	}
+}
+
+// 获取图片信息
+const getImageInfo = src =>
+	new Promise(resolve => {
+		uni.getImageInfo({
+			src,
+			success: resolve,
+			fail: error => {
+				console.log(error)
+				resolve(null)
+			},
 		})
+	})
 
+// 将画布导出为临时图片
+const canvasToTempFilePath = (width, height) =>
+	new Promise(resolve => {
+		uni.canvasToTempFilePath(
+			{
+				canvasId: 'ai-image-canvas',
+				width,
+				height,
+				destWidth: width,
+				destHeight: height,
+				fileType: 'jpg',
+				quality: 0.92,
+				success: res => resolve(res.tempFilePath),
+				fail: error => {
+					console.log(error)
+					resolve('')
+				},
+			},
+			canvasOwner
+		)
+	})
+
+// 根据图片信息绘制算法框
+const drawAiImage = async (imageInfo, aiFrame, scaleX = 1, scaleY = 1) => {
+	if (!imageInfo?.path || !imageInfo.width || !imageInfo.height || !aiFrame.length) return ''
+	const canvasScale = Math.min(1, MAX_CANVAS_EDGE / imageInfo.width, MAX_CANVAS_EDGE / imageInfo.height)
+	const width = Math.round(imageInfo.width * canvasScale)
+	const height = Math.round(imageInfo.height * canvasScale)
+	const boxScaleX = scaleX * canvasScale
+	const boxScaleY = scaleY * canvasScale
+
+	const ctx = uni.createCanvasContext('ai-image-canvas', canvasOwner)
+	ctx.drawImage(imageInfo.path, 0, 0, width, height)
+
+	aiFrame.forEach(item => {
+		const { x_cen, y_cen, width: boxWidth, height: boxHeight } = item.bbox || {}
+		if ([x_cen, y_cen, boxWidth, boxHeight].some(value => typeof value !== 'number')) return
+
+		const scaledWidth = boxWidth * boxScaleX
+		const scaledHeight = boxHeight * boxScaleY
+		const x = x_cen * boxScaleX - scaledWidth / 2
+		const y = y_cen * boxScaleY - scaledHeight / 2
+		const label = item.class_name || ''
+		const fontSize = Math.max(18, Math.round(width / 80))
+		const labelHeight = fontSize + 10
+		const labelY = y - labelHeight >= 0 ? y - labelHeight : y
+		const lineWidth = Math.max(3, Math.round(width / 640))
+
+		ctx.setStrokeStyle('#FF3B30')
+		ctx.setLineWidth(lineWidth)
+		ctx.strokeRect(x, y, scaledWidth, scaledHeight)
+
+		if (label) {
+			ctx.setFontSize(fontSize)
+			const labelWidth = label.length * fontSize + 16
+			ctx.setFillStyle('#FF3B30')
+			ctx.fillRect(x, labelY, labelWidth, labelHeight)
+			ctx.setFillStyle('#FFFFFF')
+			ctx.setTextBaseline('middle')
+			ctx.fillText(label, x + 8, labelY + labelHeight / 2)
+		}
+	})
+
+	return new Promise(resolve => {
+		ctx.draw(false, async () => {
+			const tempFilePath = await canvasToTempFilePath(width, height)
+			resolve(tempFilePath)
+		})
 	})
 }
 
-const handleClick = (item) => {
-  currentTab.value = item.key
-
-  listParams.value.current = 1
-  hasMore.value = true
-  dataList.value = []
-  getDataList()
+// 生成缩略图地址
+const getThumbnailUrl = url => {
+	if (!url) return ''
+	return url.replace(/(\.[^./?]+)(\?.*)?$/, '_thumbnail$1$2')
 }
+
+// 前置整理列表媒体字段
+const normalizeWorkItem = item => {
+	const isImage = [1, 2].includes(item.attachmentType)
+	const aiFrame = item.attachmentType === 2 ? parseAiFrame(item.geojson) : []
+	return {
+		...item,
+		isImage,
+		isVideo: item.attachmentType === 3,
+		thumbnail: item.eventImageUrl,
+		previewUrl: item.eventImageUrl,
+		thumbnailSource: isImage ? getThumbnailUrl(item.eventImageUrl) : '',
+		aiFrame,
+		imageReady: !aiFrame.length,
+	}
+}
+
+// 串行绘制单条 AI 原图和缩略图
+const enqueueAiImages = (item, originalImageInfo, thumbnailImageInfo, generation) => {
+	const task = imageRenderQueue.then(async () => {
+		if (generation !== imageGeneration) return
+		const previewUrl = (await drawAiImage(originalImageInfo, item.aiFrame)) || item.eventImageUrl
+		if (generation !== imageGeneration) return
+		item.previewUrl = previewUrl
+		if (!thumbnailImageInfo) {
+			item.thumbnail = previewUrl
+			return
+		}
+		const scaleX = thumbnailImageInfo.width / originalImageInfo.width
+		const scaleY = thumbnailImageInfo.height / originalImageInfo.height
+		item.thumbnail = (await drawAiImage(thumbnailImageInfo, item.aiFrame, scaleX, scaleY)) || previewUrl
+	})
+	imageRenderQueue = task.catch(() => {})
+	return task
+}
+
+// 异步准备单条记录的缩略图和 AI 图片
+const prepareItemImages = async (item, generation) => {
+	if (!item.isImage || !item.eventImageUrl) return
+	const thumbnailImagePromise = getImageInfo(item.thumbnailSource)
+	if (!item.aiFrame.length) {
+		const thumbnailImageInfo = await thumbnailImagePromise
+		if (generation === imageGeneration && thumbnailImageInfo) item.thumbnail = item.thumbnailSource
+		return
+	}
+
+	const [originalImageInfo, thumbnailImageInfo] = await Promise.all([
+		getImageInfo(item.eventImageUrl),
+		thumbnailImagePromise,
+	])
+	if (generation !== imageGeneration) return
+	if (thumbnailImageInfo) item.thumbnail = item.thumbnailSource
+	if (!originalImageInfo) return
+	return enqueueAiImages(item, originalImageInfo, thumbnailImageInfo, generation)
+}
+
+// 启动当前页图片后台处理任务
+const startImageTasks = (list, generation) => {
+	list.forEach(item => {
+		const task = prepareItemImages(item, generation)
+			.catch(() => {})
+			.finally(() => {
+				item.imageReady = true
+			})
+		if (item.id) imageTaskMap.set(item.id, task)
+	})
+}
+
+// 获取工单列表
+const getDataList = () => {
+	if (!hasMore.value) return
+
+	// 每次请求自增序列号,响应回来时校验是否过期
+	const seq = ++requestSeq
+	const generation = imageGeneration
+	loading.value = true
+	const params = {
+		current: listParams.value.current,
+		size: listParams.value.size,
+		keyword: listParams.value.keyword,
+		onlyMine: currentTab.value === 'myTickets' ? 1 : 0,
+		eventStatus: listParams.value.eventStatus,
+	}
+	getGdList(params)
+		.then(res => {
+			// 已有新请求发出,丢弃过期响应
+			if (seq !== requestSeq) return
+			const resData = res.data.data
+			const response = resData.records
+			const list = response.map(normalizeWorkItem)
+			let currentPageList = []
+			// 根据当前页码决定是替换还是追加数据
+			if (listParams.value.current === 1) {
+				dataList.value = list
+				currentPageList = dataList.value
+			} else {
+				const startIndex = dataList.value.length
+				dataList.value = [...dataList.value, ...list]
+				currentPageList = dataList.value.slice(startIndex)
+			}
+			// 判断是否还有更多数据
+			if (list.length < listParams.value.size || resData.current >= resData.pages) {
+				hasMore.value = false
+			} else {
+				hasMore.value = true
+			}
+			startImageTasks(currentPageList, generation)
+		})
+		.finally(() => {
+			// 仅在最新一次请求结束时关闭loading,避免过早关闭
+			if (seq === requestSeq) loading.value = false
+		})
+}
+// 获取工单状态数量
+const getstatusCountData = () => {
+	const params = {
+		keyword: listParams.value.keyword,
+	}
+	getstatusCount(params).then(res => {
+		const response = res.data.data
+		const { totalCount, myCount, disposingCount, disposedCount, completedCount } = response
+		tabList.value.forEach(tab => {
+			if (tab.key === 'all') {
+				tab.badge.value = (totalCount || 0).toString()
+				tab.badge.showZero = true
+			} else if (tab.key === 'myTickets') {
+				tab.badge.value = (myCount || 0).toString()
+				tab.badge.showZero = true
+			} else if (tab.key === 'disposing') {
+				tab.badge.value = (disposingCount || 0).toString()
+				tab.badge.showZero = true
+			} else if (tab.key === 'handled') {
+				tab.badge.value = (disposedCount || 0).toString()
+				tab.badge.showZero = true
+			} else if (tab.key === 'completed') {
+				tab.badge.value = (completedCount || 0).toString()
+				tab.badge.showZero = true
+			}
+		})
+	})
+}
+
+// 切换工单状态
+const handleClick = item => {
+	imageGeneration++
+	imageTaskMap.clear()
+	currentTab.value = item.key
+	listParams.value.eventStatus = tabStatusMap[item.key] ?? ''
+
+	listParams.value.current = 1
+	hasMore.value = true
+	dataList.value = []
+	getDataList()
+}
+// 加载更多工单
 const loadMore = () => {
-  if (loading.value || !hasMore.value) return
-  listParams.value.current++
-  getDataList()
+	if (loading.value || !hasMore.value) return
+	listParams.value.current++
+	getDataList()
 }
 
-const detailHandle = (val) => {
-  uni.navigateTo({
-    url: `/subPackages/workDetail/index?id=${val.id}`,
-  })
+// 缓存预览原图后跳转详情页
+const detailHandle = async val => {
+	const imageTask = imageTaskMap.get(val.id)
+	if (val.aiFrame.length && !val.imageReady && imageTask) {
+		uni.showLoading({ title: '图片处理中', mask: true })
+		try {
+			await imageTask
+		} finally {
+			uni.hideLoading()
+		}
+	}
+	if (val.aiFrame.length) {
+		uni.setStorageSync(`${PREVIEW_IMG_CACHE_KEY_PREFIX}${val.id}`, val.previewUrl)
+	}
+	uni.navigateTo({ url: `/subPackages/workDetail/index?id=${val.id}` })
 }
 
 // 搜索功能
 const handleSearch = () => {
-  listParams.value.current = 1
-  hasMore.value = true
-  dataList.value = []
-  getDataList()
-  getstatusCountData()
+	imageGeneration++
+	imageTaskMap.clear()
+	listParams.value.current = 1
+	hasMore.value = true
+	dataList.value = []
+	getDataList()
+	getstatusCountData()
 }
 // 清除搜索
 const handleClear = () => {
-  listParams.value.keyword = ''
-  listParams.value.current = 1
-  hasMore.value = true
-  dataList.value = []
-  getDataList()
-  getstatusCountData()
+	imageGeneration++
+	imageTaskMap.clear()
+	listParams.value.keyword = ''
+	listParams.value.current = 1
+	hasMore.value = true
+	dataList.value = []
+	getDataList()
+	getstatusCountData()
 }
 const topMargin = getStatusBarHeight()
+// 页面显示时刷新工单列表
 onShow(() => {
-  listParams.value.current = 1
-  hasMore.value = true
-  dataList.value = []
-  getDataList()
-  getstatusCountData()
+	imageGeneration++
+	imageTaskMap.clear()
+	listParams.value.current = 1
+	hasMore.value = true
+	dataList.value = []
+	getDataList()
+	getstatusCountData()
 })
 </script>
 
 <style scoped lang="scss">
 .eventTickets {
-  padding: 0 20rpx;
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  box-sizing: border-box;
-  width: 100%;
-  overflow-x: hidden;
-  .searchTop {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    padding-right: 24rpx;
-    width: 100%;
-    height: 108rpx;
-    :deep(){
-      .u-search__content{
-        background: #fff !important;
-      }
-      .u-search__content__input{
-        background: #fff !important;
-      }
-    }
-  }
-  :deep(){
+	padding: 0 20rpx;
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+	box-sizing: border-box;
+	width: 100%;
+	overflow-x: hidden;
+	.searchTop {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding-right: 24rpx;
+		width: 100%;
+		height: 108rpx;
+		:deep() {
+			.u-search__content {
+				background: #fff !important;
+			}
+			.u-search__content__input {
+				background: #fff !important;
+			}
+		}
+	}
+	:deep() {
+		.u-tabs__wrapper__nav__line {
+			// width: 80rpx !important;
+			background: #1d6fe9 !important;
+		}
+		.u-badge {
+			background-color: #1d6fe9 !important;
+			margin-top: -14px !important;
+			margin-left: -5px !important;
+		}
+	}
+	tabs-container {
+		display: flex;
+		align-items: center;
+		height: 98rpx;
+		width: 100%;
+		overflow-x: auto; // 横向滚动
+		overflow-y: hidden;
+		white-space: nowrap; // 防止换行
+		-webkit-overflow-scrolling: touch; // 移动端平滑滚动
 
-    .u-tabs__wrapper__nav__line{
-      // width: 80rpx !important;
-      background: #1D6FE9 !important;
-    }
-    .u-badge {
-      background-color: #1d6fe9 !important;
-      margin-top: -14px !important;
-      margin-left: -5px !important;
-    }
-  }
+		// 隐藏滚动条(可选)
+		&::-webkit-scrollbar {
+			height: 0;
+			display: none;
+		}
+	}
 
+	// 子元素不压缩
+	.tabs-container > * {
+		flex-shrink: 0;
+	}
+	.listBox {
+		position: relative;
+		display: flex;
+		flex-direction: column;
+		flex: 1;
+		width: 100%;
+		box-sizing: border-box;
+	}
+	.eventBox {
+		padding: 20rpx 0;
+		overflow-y: auto;
+		overflow-x: hidden;
+		height: 0;
+		flex-grow: 1;
+		align-content: flex-start;
+		width: 100%;
+		box-sizing: border-box;
+	}
 
-  .tabs-container{
-    display: flex;
-    justify-content: center;
-    height: 98rpx;
-  }
-  .listBox {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    flex: 1;
-    width: 100%;
-    box-sizing: border-box;
-  }
-  .eventBox {
-    padding: 20rpx 0;
-    overflow-y: auto;
-    overflow-x: hidden;
-    height: 0;
-    flex-grow: 1;
-    align-content: flex-start;
-    width: 100%;
-    box-sizing: border-box;
-  }
+	.eventList {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 20rpx;
+		width: 100%;
+		box-sizing: border-box;
+		justify-content: space-between;
 
-  .eventList {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 20rpx;
-    width: 100%;
-    box-sizing: border-box;
-    justify-content: space-between;
+		.eventItem {
+			width: calc(50% - 10rpx);
+			background-color: #fff;
+			border-radius: 12rpx;
+			overflow: hidden;
+			position: relative;
+			box-sizing: border-box;
+			max-width: 100%;
 
-    .eventItem {
-      width: calc(50% - 10rpx);
-      background-color: #fff;
-      border-radius: 12rpx;
-      overflow: hidden;
-      position: relative;
-      box-sizing: border-box;
-      max-width: 100%;
+			image,
+			.videoBox {
+				width: 100%;
+				height: 208rpx;
+				border-radius: 12rpx;
+				overflow: hidden;
+				display: block;
+			}
+			.videoBox {
+				background: linear-gradient(135deg, #eef4ff 0%, #dfe8f8 100%);
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				padding-bottom: 48rpx;
+				box-sizing: border-box;
+			}
+			.playIcon {
+				width: 72rpx;
+				height: 72rpx;
+				border-radius: 50%;
+				background: rgba(29, 111, 233, 0.9);
+				position: relative;
+				box-shadow: 0 8rpx 18rpx rgba(29, 111, 233, 0.24);
 
-      img {
-        width: 100%;
-        height: 208rpx;
-        border-radius: 12rpx;
-        overflow: hidden;
-      }
-      .informationDisplay{
-        width: 100%;
-        position: absolute;
-        bottom: 3px;
-        background: rgba(11,11,11,0.35);
-        border-radius: 0rpx 0rpx 12rpx 12rpx;
-        display: flex;
-        align-items: center;
-        justify-content: flex-end;
-        padding: 10rpx 0rpx 10rpx 0rpx;
-        .itemTitle {
-          width: 144rpx;
-          font-family: Source Han Sans CN, Source Han Sans CN;
-          font-weight: 500;
-          font-size: 28rpx;
-          color: #000000;
-          white-space: nowrap;
-          overflow: hidden;
-          text-overflow: ellipsis;
-        }
+				&::after {
+					content: '';
+					position: absolute;
+					left: 29rpx;
+					top: 20rpx;
+					width: 0;
+					height: 0;
+					border-top: 16rpx solid transparent;
+					border-bottom: 16rpx solid transparent;
+					border-left: 22rpx solid #fff;
+				}
+			}
+			.informationDisplay {
+				width: 100%;
+				position: absolute;
+				bottom: 3px;
+				background: rgba(11, 11, 11, 0.35);
+				border-radius: 0rpx 0rpx 12rpx 12rpx;
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+				padding: 10rpx 14rpx;
+				box-sizing: border-box;
+				.itemTitle {
+					flex: 1;
+					min-width: 0;
+					font-family: Source Han Sans CN, Source Han Sans CN;
+					font-weight: 500;
+					font-size: 24rpx;
+					color: #ffffff;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+				}
 
-        .itemContent {
-          font-family: Source Han Sans CN, Source Han Sans CN;
-          font-weight: 400;
-          font-size: 24rpx;
-          color: #FFFFFF;
-          padding-right: 14rpx;
+				.itemContent {
+					font-family: Source Han Sans CN, Source Han Sans CN;
+					font-weight: 400;
+					font-size: 24rpx;
+					color: #ffffff;
+					margin-left: 12rpx;
+					white-space: nowrap;
+				}
+			}
+		}
+	}
 
-        }
-      }
-    }
-  }
+	.loadingMore {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		height: 80rpx;
+		font-size: 28rpx;
+		color: #999;
+		width: 100%;
+	}
 
-  .loadingMore {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 80rpx;
-    font-size: 28rpx;
-    color: #999;
-    width: 100%;
-  }
+	.noData {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		height: 300rpx;
+		font-size: 28rpx;
+		color: #999;
+		width: 100%;
+	}
 
-  .noData {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 300rpx;
-    font-size: 28rpx;
-    color: #999;
-    width: 100%;
-  }
+	.ai-image-canvas {
+		position: fixed;
+		left: -9999px;
+		top: -9999px;
+		pointer-events: none;
+	}
 }
 </style>

--
Gitblit v1.9.3