Pārlūkot izejas kodu

调试音频播放,还有问题:上传文件夹不显示;分页切换数据不准确;在线解压没调完;

tanshanming 8 mēneši atpakaļ
vecāks
revīzija
93d8c98fa5

BIN
src/assets/images/myResource/audio/wave.gif


+ 0 - 10
src/views/myResource/common/FileTable.vue

@@ -164,14 +164,6 @@
 				showOverflowTooltip: true
 			}
 		]
-		console.log(
-			'selectedColumnList: ',
-			selectedColumnList.value,
-			' screenWidth ',
-			screenWidth.value,
-			' screenWidth.value > 768 ',
-			screenWidth.value > 768
-		)
 		// if (![0, 8].includes(Number(route.query.fileType)) && routeName.value !== 'Share' && screenWidth.value > 768) {
 		if (
 			![0, 8].includes(Number(myResourceStore.getQuery.fileType ? myResourceStore.getQuery.fileType : 0)) &&
@@ -362,10 +354,8 @@
 	}
 
 	const onSelectChange = (changableRowKeys) => {
-		console.log('selectedRowKeys changed: ', changableRowKeys)
 		selectedRowKeys.value = changableRowKeys
 		const list = changableRowKeys.map((id) => props.fileList.find((file) => file.userFileId === id)).filter(Boolean)
-		console.log('list', list)
 		myResourceStore.changeSelectedFiles(list)
 		myResourceStore.changeIsBatchOperation(changableRowKeys.length !== 0)
 	}

+ 312 - 281
src/views/myResource/file/box/audioPreview/BoxMask.vue

@@ -1,222 +1,241 @@
 <template>
-	<a-modal
-		class="audio-preview-wrapper"
-		v-model:visible="visible"
-		:footer="null"
-		:closable="false"
-		:maskClosable="false"
-		:width="'100%'"
-		:bodyStyle="{ padding: 0 }"
-		:centered="true"
-	>
-		<img class="audio-background" :src="musicImgUrl" alt="背景图" />
-
-		<!-- 右上角操作 -->
-		<div class="operate-box">
-			<a-tooltip placement="bottom">
-				<template #title>
-					<div style="line-height: 2">
-						操作提示: <br />
-						1. 按 Esc 键可退出查看;<br />
-						2. 支持键盘控制:<br />
-						&nbsp;&nbsp;空格 - 暂停/播放<br />
-						&nbsp;&nbsp;左方向键 - 播放上一个<br />
-						&nbsp;&nbsp;右方向键 - 播放下一个<br />
-						&nbsp;&nbsp;上方向键 - 音量调大<br />
-						&nbsp;&nbsp;下方向键 - 音量减小<br />
+	<transition name="zoom-in-top">
+		<div class="audio-preview-wrapper" v-show="visible">
+			<img class="audio-background" :src="musicImgUrl" alt="背景图" />
+			<!-- 右上角操作 -->
+			<div class="operate-box">
+				<a-tooltip placement="bottom">
+					<template #title>
+						<div style="line-height: 2">
+							操作提示: <br />
+							1. 按 Esc 键可退出查看;<br />
+							2. 支持键盘控制:<br />
+							&nbsp;&nbsp;空格 - 暂停/播放<br />
+							&nbsp;&nbsp;左方向键 - 播放上一个<br />
+							&nbsp;&nbsp;右方向键 - 播放下一个<br />
+							&nbsp;&nbsp;上方向键 - 音量调大<br />
+							&nbsp;&nbsp;下方向键 - 音量减小<br />
+						</div>
+					</template>
+					<i class="tip-icon anticon anticon-bulb"></i>
+				</a-tooltip>
+				<i class="close-icon anticon anticon-close" title="关闭(Escape)" @click="handleClosePreview"></i>
+			</div>
+			<audio
+				ref="audioRef"
+				:src="activeFileObj.fileUrl"
+				controls
+				style="position: fixed; top: 0; left: 0; display: none"
+				@loadedmetadata="handleLoadedmetadata"
+				@timeupdate="handleTimeUpdate"
+				@ended="handleChangeAudioIndex('next')"
+			></audio>
+			<div class="audio-list-wrapper">
+				<!-- 音频列表 -->
+				<ul class="audio-list">
+					<li class="audio-list-header">
+						<span class="name">音频名称</span>
+						<span class="audio-size">大小</span>
+						<span class="path">路径</span>
+					</li>
+					<div class="audio-list-body">
+						<li
+							class="audio-item"
+							v-for="(item, index) in audioList"
+							:key="index"
+							:class="[activeIndex === index ? 'active' : '']"
+							:title="isPlay ? '暂停' : '播放'"
+							@click="handleChangeAudioIndex('manual', index)"
+						>
+							<span class="name">
+								<span class="sequence" v-show="activeIndex !== index">
+									{{ index + 1 }}
+								</span>
+								<img class="wave" :src="activePlayIcon" alt="波浪动图" v-show="activeIndex === index && isPlay" />
+								<i class="no-wave anticon anticon-bar-chart" v-show="activeIndex === index && !isPlay"></i>
+								<span class="text">{{ item.fileName }}.{{ item.extendName }}</span>
+							</span>
+							<i class="play-icon iconfont icon-icon-7" v-show="activeIndex === index && !isPlay"></i>
+							<i class="pause-icon iconfont icon-icon-3" v-show="activeIndex === index && isPlay"></i>
+							<a class="download" :href="$file.getDownloadFilePath(item)" target="_blank" title="下载">
+								<i class="download-icon anticon anticon-download"></i>
+							</a>
+							<i
+								class="share-icon anticon anticon-share-alt"
+								title="分享"
+								@click.stop="
+									$openDialog.shareFile({
+										fileInfo: [
+											{
+												userFileId: item.userFileId
+											}
+										]
+									})
+								"
+							></i>
+							<span class="audio-size">{{ $file.calculateFileSize(item.fileSize) }}</span>
+							<span class="path">{{ item.filePath }}</span>
+						</li>
 					</div>
-				</template>
-				<question-circle-outlined class="tip-icon" />
-			</a-tooltip>
-			<close-outlined class="close-icon" title="关闭(Escape)" @click="handleClosePreview" />
-		</div>
-
-		<audio
-			ref="audioRef"
-			:src="activeFileObj.fileUrl"
-			controls
-			style="position: fixed; top: 0; left: 0; display: none"
-			@loadedmetadata="handleLoadedmetadata"
-			@timeupdate="handleTimeUpdate"
-			@ended="handleChangeAudioIndex('next')"
-		/>
-
-		<div class="audio-list-wrapper">
-			<!-- 音频列表 -->
-			<ul class="audio-list">
-				<li class="audio-list-header">
-					<span class="name">音频名称</span>
-					<span class="audio-size">大小</span>
-					<span class="path">路径</span>
-				</li>
-				<div class="audio-list-body">
-					<li
-						v-for="(item, index) in audioList"
-						:key="index"
-						class="audio-item"
-						:class="{ active: activeIndex === index }"
-						:title="isPlay ? '暂停' : '播放'"
-						@click="handleChangeAudioIndex('manual', index)"
+				</ul>
+				<!-- 歌曲图片和歌词 -->
+				<div class="img-and-lyrics">
+					<img class="audio-img" :src="musicImgUrl" alt="歌曲图片" />
+					<div class="audio-name">{{ activeFileObj.fileName }}.{{ activeFileObj.extendName }}</div>
+					<div class="album-artist" v-show="audioInfo.artist">歌手:{{ audioInfo.artist }}</div>
+					<div class="album-name" v-if="audioInfo.album">专辑:{{ audioInfo.album }}</div>
+					<ul
+						class="lyrics-list"
+						ref="lyricsListRef"
+						:class="{ one: lyricsList.length === 1 }"
+						v-if="lyricsList.length"
 					>
-						<span class="name">
-							<span class="sequence" v-show="activeIndex !== index">
-								{{ index + 1 }}
-							</span>
-							<img class="wave" :src="activePlayIcon" alt="波浪动图" v-show="activeIndex === index && isPlay" />
-							<i class="no-wave el-icon-s-data" v-show="activeIndex === index && !isPlay"></i>
-							<span class="text">{{ item.fileName }}.{{ item.extendName }}</span>
-						</span>
-						<i class="play-icon iconfont icon-icon-7" v-show="activeIndex === index && !isPlay"></i>
-						<i class="pause-icon iconfont icon-icon-3" v-show="activeIndex === index && isPlay"></i>
-						<a class="download" :href="$file.getDownloadFilePath(item)" target="_blank" title="下载">
-							<i class="download-icon el-icon-download"></i>
-						</a>
-						<i
-							class="share-icon el-icon-share"
-							title="分享"
-							@click.stop="
-								$openDialog.shareFile({
-									fileInfo: [
-										{
-											userFileId: item.userFileId
-										}
-									]
-								})
-							"
-						></i>
-						<span class="audio-size">{{ $file.calculateFileSize(item.fileSize) }}</span>
-						<span class="path">{{ item.filePath }}</span>
-					</li>
+						<li
+							class="lyrics-item"
+							ref="lyricsLineRef"
+							v-for="(item, index) in lyricsList"
+							:key="index"
+							:class="{
+								active: currentLyricsLineIndex === index
+							}"
+							@click="handleChangeProgress(transferTimeToSeconds(item.time))"
+						>
+							{{ item.text }}
+						</li>
+					</ul>
 				</div>
-			</ul>
-
-			<!-- 歌曲图片和歌词 -->
-			<div class="img-and-lyrics">
-				<img class="audio-img" :src="musicImgUrl" alt="歌曲图片" />
-				<div class="audio-name">{{ activeFileObj.fileName }}.{{ activeFileObj.extendName }}</div>
-				<div class="album-artist" v-show="audioInfo.artist">歌手:{{ audioInfo.artist }}</div>
-				<div class="album-name" v-if="audioInfo.album">专辑:{{ audioInfo.album }}</div>
-				<ul class="lyrics-list" ref="lyricsListRef" :class="{ one: lyricsList.length === 1 }" v-if="lyricsList.length">
-					<li
-						class="lyrics-item"
-						ref="lyricsLineRef"
-						v-for="(item, index) in lyricsList"
-						:key="index"
-						:class="{
-							active: currentLyricsLineIndex === index
-						}"
-						@click="handleChangeProgress(transferTimeToSeconds(item.time))"
-					>
-						{{ item.text }}
-					</li>
-				</ul>
 			</div>
-		</div>
-
-		<!-- 底部音乐控件 -->
-		<div class="control-wrapper">
-			<div class="control-left">
-				<step-backward-outlined
-					class="operate-icon"
-					title="上一个(按左方向键)"
-					@click="handleChangeAudioIndex('pre')"
-				/>
-				<play-circle-outlined
-					v-if="!isPlay"
-					class="operate-icon"
-					title="播放(按空格键)"
-					@click="handleClickPlayIcon"
-				/>
-				<pause-circle-outlined v-else class="operate-icon" title="暂停(按空格键)" @click="handleClickPauseIcon" />
-				<step-forward-outlined
-					class="operate-icon"
-					title="下一个(按右方向键)"
-					@click="handleChangeAudioIndex('next')"
-				/>
-				<a-slider
-					class="progress-bar control-item"
-					v-model:value="currentTime"
-					:step="progressStep"
-					:max="audioInfo.duration"
-					:tooltip-visible="true"
-					:tip-formatter="(val) => transferSecondsToTime(val)"
-					@mousedown="isDrop = true"
-					@mouseup="isDrop = false"
-					@change="handleChangeProgress"
-				/>
-				<span class="time control-item">
-					{{ transferSecondsToTime(currentTime) }} / {{ transferSecondsToTime(audioInfo.duration) }}
-				</span>
-			</div>
-
-			<div class="control-right">
-				<i
-					class="operate-icon cycle-type iconfont"
-					:class="cycleTypeMap[String(cycleType)].icon"
-					:title="cycleTypeMap[String(cycleType)].text"
-					@click="handleChangeCycleType"
-				></i>
-				<a
-					class="operate-icon download-link"
-					:href="$file.getDownloadFilePath(activeFileObj)"
-					target="_blank"
-					title="下载"
-				>
-					<i class="download-icon el-icon-download"></i>
-				</a>
-				<i
-					class="operate-icon share-icon el-icon-share"
-					title="分享"
-					@click.stop="
-						$openDialog.shareFile({
-							fileInfo: [
-								{
-									userFileId: item.userFileId
-								}
-							]
-						})
-					"
-				></i>
-				<i
-					class="operate-icon volume-icon control-item iconfont"
-					:class="volume === 0 ? 'icon-jingyin01' : 'icon-yinliang101'"
-					@click="handleClickVolumeIcon"
-				></i>
-				<el-slider
-					class="volume-bar control-item"
-					v-model="volume"
-					:step="0.01"
-					:max="1"
-					:format-tooltip="(val) => Math.floor(val * 100)"
-					height="100px"
-					title="可按上下方向键调节音量"
-					@input="handleChangeVolumeBar"
-				></el-slider>
+			<!-- 底部音乐控件 -->
+			<div class="control-wrapper">
+				<div class="control-left">
+					<i
+						class="operate-icon iconfont icon-shangyishou"
+						title="上一个(按左方向键)"
+						@click="handleChangeAudioIndex('pre')"
+					></i>
+					<i
+						class="operate-icon play-icon iconfont icon-icon-7"
+						v-show="!isPlay"
+						title="播放(按空格键)"
+						@click="handleClickPlayIcon"
+					></i>
+					<i
+						class="operate-icon pause-icon iconfont icon-icon-3"
+						v-show="isPlay"
+						title="暂停(按空格键)"
+						@click="handleClickPauseIcon"
+					></i>
+					<i
+						class="operate-icon iconfont icon-xiayishou"
+						title="下一个(按右方向键)"
+						@click="handleChangeAudioIndex('next')"
+					></i>
+					<a-slider
+						class="progress-bar control-item"
+						v-model:value="currentTime"
+						:step="progressStep"
+						:max="audioInfo.duration"
+						:tip-formatter="(val) => transferSecondsToTime(val)"
+						@mousedown="isDrop = true"
+						@mouseup="isDrop = false"
+						@change="handleChangeProgress"
+					/>
+					<span class="time control-item"
+						>{{ transferSecondsToTime(currentTime) }} / {{ transferSecondsToTime(audioInfo.duration) }}</span
+					>
+				</div>
+				<div class="control-right">
+					<i
+						class="operate-icon cycle-type iconfont"
+						:class="cycleTypeMap[String(cycleType)].icon"
+						:title="cycleTypeMap[String(cycleType)].text"
+						@click="handleChangeCycleType"
+					></i>
+					<a
+						class="operate-icon download-link"
+						:href="$file.getDownloadFilePath(activeFileObj)"
+						target="_blank"
+						title="下载"
+					>
+						<i class="download-icon anticon anticon-download"></i>
+					</a>
+					<i
+						class="operate-icon share-icon anticon anticon-share-alt"
+						title="分享"
+						@click.stop="
+							$openDialog.shareFile({
+								fileInfo: [
+									{
+										userFileId: activeFileObj.userFileId
+									}
+								]
+							})
+						"
+					></i>
+					<i
+						class="operate-icon volume-icon control-item iconfont"
+						:class="volume === 0 ? 'icon-jingyin01' : 'icon-yinliang101'"
+						@click="handleClickVolumeIcon"
+					></i>
+					<a-slider
+						class="volume-bar control-item"
+						v-model:value="volume"
+						:step="0.01"
+						:max="1"
+						:tip-formatter="(val) => Math.floor(val * 100)"
+						title="可按上下方向键调节音量"
+						@change="handleChangeVolumeBar"
+					/>
+				</div>
 			</div>
 		</div>
-	</a-modal>
+	</transition>
 </template>
 
 <script setup>
-	import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
-	import musicPng from '@/assets/images/myResource/file/file_music.png'
-	import { message } from 'ant-design-vue'
-	import {
-		QuestionCircleOutlined,
-		CloseOutlined,
-		StepBackwardOutlined,
-		StepForwardOutlined,
-		PlayCircleOutlined,
-		PauseCircleOutlined
-	} from '@ant-design/icons-vue'
-	import { getFileDetail } from '@/api/myResource/file'
+	import { ref, computed, watch, onUnmounted, getCurrentInstance } from 'vue'
+	import { getFileDetail } from '@/api/myResource/file.js'
 	import { Base64 } from 'js-base64'
-
-	const props = defineProps({
-		visible: {
-			type: Boolean,
-			default: false
+	import waveGif from '@/assets/images/myResource/audio/wave.gif'
+	import musicImg from '@/assets/images/myResource/file/file_music.png'
+
+	const { proxy } = getCurrentInstance()
+
+	// 定义响应式数据
+	const visible = ref(false) // 音频预览组件是否可见
+	const activeIndex = ref(0) // 当前打开的音频索引
+	const activePlayIcon = ref(waveGif)
+	const cycleType = ref(1) // 音频播放的循环模式
+	const isPlay = ref(false) // 是否正在播放
+	const currentTime = ref(0) // 当前播放的秒
+	const isDrop = ref(false) // 是否正在拖拽播放进度滑块
+	const volume = ref(0) // 音量
+	const audioInfo = ref({}) // 音频信息
+	const lyricsList = ref([]) // 歌词列表
+	const currentLyricsLineIndex = ref(0) // 当前高亮的歌词行索引,从 0 开始
+	const loading = ref(false) // 加载状态
+	const audioRef = ref(null) // 音频元素引用
+	const lyricsListRef = ref(null) // 歌词列表引用
+	const lyricsLineRef = ref([]) // 歌词行引用
+	const defaultIndex = ref(0) // 默认索引
+
+	// 音频循环模式和图标对应的 Map
+	const cycleTypeMap = {
+		1: {
+			icon: 'icon-xunhuanbofang',
+			text: '列表循环'
+		},
+		2: {
+			icon: 'icon-danquxunhuan1',
+			text: '单曲循环'
 		},
+		3: {
+			icon: 'icon-suijibofang1',
+			text: '随机播放'
+		}
+	}
+	// 定义 props 接收父组件传递的参数
+	const props = defineProps({
 		audioList: {
 			type: Array,
 			default: () => []
@@ -231,53 +250,55 @@
 		}
 	})
 
-	const emit = defineEmits(['update:visible'])
-
-	const visible = ref(props.visible)
-	const activeIndex = ref(0)
-	const isPlay = ref(false)
-	const currentTime = ref(0)
-	const isDrop = ref(false)
-	const volume = ref(0)
-	const audioInfo = ref({})
-	const lyricsList = ref([])
-	const currentLyricsLineIndex = ref(0)
-
+	// 计算属性
+	// 当前显示的文件信息
 	const activeFileObj = computed(() => {
 		const res = props.audioList.length ? props.audioList[activeIndex.value] : {}
 		return res
 	})
 
+	// 隐藏的 audio 标签
 	const audioElement = computed(() => {
-		return refs.audioRef
+		return audioRef.value
 	})
 
+	// 歌曲封面
 	const musicImgUrl = computed(() => {
-		return audioInfo.value.albumImage ? `data:image/jpeg;base64,${audioInfo.value.albumImage}` : musicPng
+		return audioInfo.value.albumImage ? `data:image/jpeg;base64,${audioInfo.value.albumImage}` : musicImg
 	})
 
+	// 播放进度条步长
 	const progressStep = computed(() => {
 		return audioInfo.value.duration / 100
 	})
 
-	watch(
-		() => props.visible,
-		(newValue) => {
-			visible.value = newValue
-			if (newValue) {
-				activeIndex.value = props.defaultIndex
-				getFileDetailData()
-				document.addEventListener('keyup', handleAddKeyupEvent)
-			} else {
-				document.removeEventListener('keyup', handleAddKeyupEvent)
-			}
+	// 监听音频预览组件状态
+	watch(visible, (newValue) => {
+		if (newValue) {
+			activeIndex.value = defaultIndex.value
+			getFileDetailData()
+			// 添加键盘相关事件
+			document.addEventListener('keyup', handleAddKeyupEvent)
+		} else {
+			// 移除键盘相关事件
+			document.removeEventListener('keyup', handleAddKeyupEvent)
 		}
-	)
+	})
 
+	// 监听当前索引变化
 	watch(activeIndex, () => {
 		getFileDetailData()
 	})
 
+	// 组件卸载时移除事件监听
+	onUnmounted(() => {
+		document.removeEventListener('keyup', handleAddKeyupEvent)
+	})
+
+	/**
+	 * DOM 绑定 Esc 键、左方向键、右方向键的键盘按下事件
+	 * @param {event} event 事件
+	 */
 	function handleAddKeyupEvent(event) {
 		switch (event.code) {
 			// 关闭预览
@@ -317,6 +338,9 @@
 		}
 	}
 
+	/**
+	 * 获取文件信息
+	 */
 	function getFileDetailData() {
 		handleClickPauseIcon()
 		loading.value = true
@@ -357,7 +381,7 @@
 						}
 					})
 					// 当切换完歌曲时,歌词重新滚动到顶部
-					refs.lyricsListRef.scrollTo({
+					lyricsListRef.value.scrollTo({
 						top: 0,
 						behavior: 'smooth'
 					})
@@ -369,6 +393,9 @@
 			})
 	}
 
+	/**
+	 * 获取播放器参数
+	 */
 	function handleLoadedmetadata(event) {
 		const audioDom = event.target
 		volume.value = audioDom.volume || 0.5
@@ -376,6 +403,10 @@
 		handleClickPlayIcon()
 	}
 
+	/**
+	 * 将秒转化为时分秒
+	 * @param {number} duration 总秒数
+	 */
 	function transferSecondsToTime(duration) {
 		const hour = Math.floor(duration / 3600)
 		const minutes = Math.floor(duration / 60)
@@ -385,11 +416,18 @@
 		}`
 	}
 
+	/**
+	 * 将分秒转化为秒
+	 * @param {string} time 分秒,格式 00:00
+	 */
 	function transferTimeToSeconds(time) {
 		const timeList = time.split('.')[0].split(':')
 		return Number(timeList[1]) + Number(timeList[0]) * 60
 	}
 
+	/**
+	 * 当前播放时间改变时触发
+	 */
 	function handleTimeUpdate(event) {
 		// 如果正在拖拽进度滑块,函数结束,不计算当前时间
 		if (isDrop.value) return
@@ -407,8 +445,8 @@
 					// 使高亮歌词行永远保持在第二行
 					if (currentLyricsLineIndex.value > 2) {
 						// 平滑滚动
-						refs.lyricsListRef.scrollTo({
-							top: refs.lyricsLineRef[index].clientHeight * (index - 2),
+						lyricsListRef.value.scrollTo({
+							top: lyricsLineRef.value[index].clientHeight * (index - 2),
 							behavior: 'smooth'
 						})
 					}
@@ -417,11 +455,17 @@
 		}
 	}
 
+	/**
+	 * 拖动播放进度滑块触发
+	 */
 	function handleChangeProgress(progress) {
 		audioElement.value.currentTime = progress
 		isDrop.value = false
 	}
 
+	/**
+	 * 切换循环播放类型
+	 */
 	function handleChangeCycleType() {
 		if (cycleType.value === 3) {
 			cycleType.value = 1
@@ -430,16 +474,29 @@
 		}
 	}
 
+	/**
+	 * 点击播放图标触发
+	 * @description 开始播放音频
+	 */
 	function handleClickPlayIcon() {
 		isPlay.value = true
 		audioElement.value.play()
 	}
 
+	/**
+	 * 点击暂停图标触发
+	 * @description 暂停音频
+	 */
 	function handleClickPauseIcon() {
 		isPlay.value = false
 		audioElement.value.pause()
 	}
 
+	/**
+	 * 切换、暂停或播放歌曲
+	 * @param {string} type pre - 上一首 | next - 下一首 | manual 手动切换
+	 * @param {number} index 手动切换的音频索引,从 0 开始
+	 */
 	function handleChangeAudioIndex(type, index) {
 		// 如果当前手动切换
 		if (type === 'manual') {
@@ -457,22 +514,22 @@
 			// 判断当前循环播放类型
 			switch (cycleType.value) {
 				case 3: {
-					let activeIndex = 0
+					let newActiveIndex = 0
 					do {
-						activeIndex = Math.floor(Math.random() * (props.audioList.length - 1)) + 1
-					} while (activeIndex === activeIndex.value)
-					activeIndex.value = activeIndex
+						newActiveIndex = Math.floor(Math.random() * (props.audioList.value.length - 1)) + 1
+					} while (activeIndex.value === newActiveIndex)
+					activeIndex.value = newActiveIndex
 					break
 				}
 				default: {
 					if (type === 'pre') {
 						if (activeIndex.value === 0) {
-							activeIndex.value = props.audioList.length - 1
+							activeIndex.value = props.audioList.value.length - 1
 						} else {
 							activeIndex.value--
 						}
 					} else if (type === 'next') {
-						if (activeIndex.value === props.audioList.length - 1) {
+						if (activeIndex.value === props.audioList.value.length - 1) {
 							activeIndex.value = 0
 						} else {
 							activeIndex.value++
@@ -484,30 +541,30 @@
 		}
 	}
 
+	/**
+	 * 点击音量图标
+	 */
 	function handleClickVolumeIcon() {
 		volume.value = volume.value === 0 ? 0.5 : 0
 		handleChangeVolumeBar(volume.value)
 	}
 
-	function handleChangeVolumeBar(volume) {
-		audioElement.value.volume = Number(volume.toFixed(1))
+	/**
+	 * 音量滑块改变时触发
+	 */
+	function handleChangeVolumeBar(value) {
+		audioElement.value.volume = Number(value.toFixed(1))
 	}
 
-	// 关闭音频预览
+	/**
+	 * 关闭音频预览
+	 */
 	function handleClosePreview() {
 		visible.value = false
-		props.callback('cancel')
+		callback('cancel')
 	}
 
-	onMounted(() => {
-		visible.value = props.visible
-		activeIndex.value = props.defaultIndex
-	})
-
-	onUnmounted(() => {
-		document.removeEventListener('keyup', handleAddKeyupEvent)
-	})
-
+	// 暴露给父组件的方法和属性
 	defineExpose({
 		visible
 	})
@@ -756,19 +813,6 @@
 				.progress-bar {
 					margin-right: 16px;
 					flex: 1;
-					// >>> .el-slider__runway {
-					//   height: 2px;
-					//   .el-slider__button-wrapper {
-					//     top: -17px;
-					//     .el-slider__button {
-					//       border: none;
-					//     }
-					//   }
-					//   .el-slider__bar {
-					//     height: 100%;
-					//     background: @Warning;
-					//   }
-					// }
 				}
 			}
 
@@ -810,19 +854,6 @@
 
 				.volume-bar {
 					width: 100px;
-					// >>> .el-slider__runway {
-					//   height: 2px;
-					//   .el-slider__button-wrapper {
-					//     top: -19px;
-					//     .el-slider__button {
-					//       border: none;
-					//     }
-					//   }
-					//   .el-slider__bar {
-					//     height: 100%;
-					//     background: @Warning;
-					//   }
-					// }
 				}
 			}
 		}

+ 2 - 1
src/views/myResource/file/box/audioPreview/index.js

@@ -2,7 +2,7 @@ import { createApp } from 'vue'
 import Antd from 'ant-design-vue'
 // 导入组件
 import AudioPreview from './BoxMask.vue'
-
+import snowy from '@/snowy'
 let audioPreviewInstance = null
 let audioPreviewApp = null
 
@@ -21,6 +21,7 @@ const initInstanceAudioPreview = (audioList, defaultIndex, callback) => {
 	})
 	// 注册 Ant Design Vue 组件
 	audioPreviewApp.use(Antd)
+	audioPreviewApp.use(snowy)
 	audioPreviewInstance = audioPreviewApp.mount(mountNode)
 	return mountNode
 }

+ 0 - 443
src/views/myResource/file/box/codePreview/BoxMask copy.vue

@@ -1,443 +0,0 @@
-<template>
-	<transition
-		name="fade"
-		enter-active-class="ant-fade-enter ant-fade-enter-active"
-		leave-active-class="ant-fade-leave ant-fade-leave-active"
-	>
-		<div class="code-preview-wrapper" v-show="visible" @keydown.ctrl.s.prevent="handleModifyFileContent">
-			<!-- 顶部信息栏 -->
-			<div class="tip-wrapper" v-if="visible">
-				<div class="name" :title="$getFileNameComplete(fileInfo)">
-					{{ $getFileNameComplete(fileInfo) }}
-					<span class="un-save" v-show="isModify && !codeMirrorOptions.readOnly">(未保存)</span>
-				</div>
-				<div class="editor-preview">在线预览{{ codeMirrorOptions.readOnly ? '' : ' & 编辑' }}</div>
-				<div class="tool-wrapper">
-					<a
-						class="item download-link"
-						target="_blank"
-						:href="$getDownloadFilePath(fileInfo)"
-						:download="$getFileNameComplete(fileInfo)"
-					>
-						<download-outlined title="下载" />
-					</a>
-					<a-tooltip placement="bottom">
-						<template #title>
-							<div>
-								操作提示:<br />
-								1. 按 Esc 键可退出查看;<br />
-								2. 支持在线编辑、保存、下载
-							</div>
-						</template>
-						<div class="item text-wrapper">
-							<span class="text">操作提示</span>
-							<bulb-outlined />
-						</div>
-					</a-tooltip>
-					<close-outlined class="item" title="关闭预览" @click="closeCodePreview" />
-				</div>
-			</div>
-
-			<!-- 代码编辑区域 -->
-			<div class="code-editor-wrapper">
-				<div class="operate-wrapper">
-					<save-outlined
-						v-show="isModify && !codeMirrorOptions.readOnly"
-						class="save-icon"
-						title="保存(ctrl+s)"
-						@click="handleModifyFileContent"
-					/>
-					<a-form class="editor-set-form" :model="codeMirrorOptions" layout="inline" size="small">
-						<a-form-item>
-							<a-checkbox v-model:checked="codeMirrorOptions.lineWrapping" @change="handleChangeCodeMirrorOption">
-								自动换行
-							</a-checkbox>
-						</a-form-item>
-
-						<a-form-item>
-							<a-select v-model:value="codeMirrorCustomOptions.fontSize" style="width: 96px" show-search>
-								<a-select-option v-for="size in fontSizeList" :key="size" :value="size">
-									{{ size }} px
-								</a-select-option>
-							</a-select>
-						</a-form-item>
-
-						<a-form-item label="代码语言">
-							<a-select
-								v-model:value="codeMirrorOptions.mode"
-								style="width: 120px"
-								show-search
-								@change="handleChangeCodeMirrorOption"
-							>
-								<a-select-option
-									v-for="[key, value] in Object.entries(fileSuffixCodeModeMap)"
-									:key="key"
-									:value="value[1].mime"
-								>
-									{{ value[1].language }}
-								</a-select-option>
-							</a-select>
-						</a-form-item>
-
-						<a-form-item label="主题">
-							<a-select
-								v-model:value="codeMirrorOptions.theme"
-								style="width: 190px"
-								show-search
-								@change="handleChangeCodeMirrorOption"
-							>
-								<a-select-option value="default">default</a-select-option>
-								<a-select-option v-for="theme in codeMirrorThemeList" :key="theme" :value="theme">
-									{{ theme }}
-								</a-select-option>
-							</a-select>
-						</a-form-item>
-					</a-form>
-				</div>
-
-				<a-spin :spinning="codeMirrorLoading">
-					<!-- <Codemirror
-						v-if="isShow"
-						ref="codemirrorRef"
-						v-model:value="codeMirrorText"
-						:options="codeMirrorOptions"
-						class="code-editor"
-						:style="{ fontSize: `${codeMirrorCustomOptions.fontSize}px` }"
-					/> -->
-				</a-spin>
-			</div>
-		</div>
-	</transition>
-</template>
-
-<script setup>
-	import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
-	import { message } from 'ant-design-vue'
-	// import { Codemirror } from 'codemirror-editor-vue3'
-	// 启用基础样式
-	// import 'codemirror-editor-vue3/dist/style.css'
-	// // 启用编程语言
-	// import 'codemirror/mode/javascript/javascript.js'
-	// import 'codemirror/mode/xml/xml.js'
-	// import 'codemirror/mode/css/css.js'
-	// // 启用主题
-	// import 'codemirror/lib/codemirror.css'
-	// import 'codemirror/theme/monokai.css'
-	// import 'codemirror/theme/material.css'
-	import { DownloadOutlined, BulbOutlined, CloseOutlined, SaveOutlined } from '@ant-design/icons-vue'
-	import { useMyResourceStore } from '@/store/myResource'
-	import { getFilePreview, modifyFileContent } from '@/api/myResource/file'
-	import { fontSizeList, fileSuffixCodeModeMap, codeMirrorThemeList } from '@/libs/map'
-
-	// import 'codemirror/lib/codemirror.css'
-	// import './theme.js'
-	// import './mode.js'
-	// import './fold.js'
-
-	// 状态管理
-	const store = useMyResourceStore()
-
-	const visible = ref(false)
-	const originalCodeText = ref('')
-	const codeMirrorText = ref('')
-	const codeMirrorLoading = ref(false)
-	const codeMirrorOptions = ref({
-		tabSize: 4,
-		mode: 'text/html',
-		theme: 'default',
-		readOnly: true,
-		lineNumbers: true,
-		line: true,
-		autoCloseBrackets: true,
-		foldGutter: true,
-		lineWrapping: true,
-		gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers']
-	})
-	const codeMirrorCustomOptions = ref({
-		fontSize: 14
-	})
-	const isShow = ref(true)
-
-	// 计算属性
-	const screenWidth = computed(() => store.screenWidth)
-	const isModify = computed(() => originalCodeText.value !== codeMirrorText.value)
-
-	// 方法
-	function getCodeText() {
-		codeMirrorLoading.value = true
-		getFilePreview({
-			userFileId: fileInfo.userFileId,
-			isMin: false,
-			shareBatchNum: fileInfo.shareBatchNum,
-			extractionCode: fileInfo.extractionCode,
-			token: $common.getCookies($config.tokenKeyName)
-		}).then((res) => {
-			codeMirrorLoading.value = false
-			originalCodeText.value = typeof res === 'object' ? JSON.stringify(res) : res
-			codeMirrorText.value = originalCodeText.value + ''
-		})
-	}
-
-	function handleModifyFileContent() {
-		if (!isModify.value || codeMirrorOptions.value.readOnly) {
-			return false
-		}
-		codeMirrorLoading.value = true
-		modifyFileContent({
-			userFileId: fileInfo.userFileId,
-			fileContent: codeMirrorText.value
-		})
-			.then((res) => {
-				codeMirrorLoading.value = false
-				if (res.success) {
-					message.success('已保存')
-					getCodeText()
-				} else {
-					message.error(res.message)
-				}
-			})
-			.catch((err) => {
-				codeMirrorLoading.value = false
-				message.error(err.message)
-			})
-	}
-
-	function handleChangeCodeMirrorOption() {
-		isShow.value = false
-		isShow.value = true
-	}
-
-	function closeCodePreview() {
-		visible.value = false
-		callback('cancel')
-	}
-
-	// 监听
-	watch(visible, (val) => {
-		if (val) {
-			let fileSuffix = fileInfo.extendName.toLowerCase()
-			if (fileSuffix === 'yaml') {
-				fileSuffix = 'yml'
-			}
-			if (fileSuffixCodeModeMap.has(fileSuffix)) {
-				codeMirrorOptions.value.mode = fileSuffixCodeModeMap.get(fileSuffix).mime
-			}
-			codeMirrorOptions.value.readOnly = !isEdit //  设置编辑器是否只读
-			codeMirrorOptions.value.theme = localStorage.getItem('qiwen_file_codemirror_theme') || 'default'
-			getCodeText()
-			// 添加键盘 Esc 事件
-			nextTick(() => {
-				document.addEventListener('keyup', (e) => {
-					if (e.keyCode === 27) {
-						closeCodePreview()
-					}
-				})
-			})
-		} else {
-			document.removeEventListener('keyup', (e) => {
-				if (e.keyCode === 27) {
-					closeCodePreview()
-				}
-			})
-		}
-	})
-
-	watch(
-		() => codeMirrorOptions.value.theme,
-		(val) => {
-			localStorage.setItem('qiwen_file_codemirror_theme', val)
-		}
-	)
-
-	// 生命周期钩子
-	onMounted(() => {})
-
-	onUnmounted(() => {})
-
-	defineExpose({
-		visible
-	})
-</script>
-
-<style lang="less" scoped>
-	@import '@/style/myResource/varibles.less';
-
-	.code-preview-wrapper {
-		position: fixed;
-		top: 0;
-		right: 0;
-		bottom: 0;
-		left: 0;
-		overflow: auto;
-		width: 100%;
-		height: 100vh;
-		z-index: 2;
-		display: flex;
-		align-items: center;
-		animation: imgPreviewAnimation 0.3s;
-		-webkit-animation: imgPreviewAnimation 0.3s; /* Safari and Chrome */
-		animation-iteration-count: 0.3;
-		-webkit-animation-iteration-count: 0.3;
-		animation-fill-mode: forwards;
-		-webkit-animation-fill-mode: forwards; /* Safari 和 Chrome */
-		@keyframes imgPreviewAnimation {
-			0% {
-				background: transparent;
-			}
-			100% {
-				background: rgba(0, 0, 0, 0.8);
-			}
-		}
-		@keyframes imgPreviewAnimation {
-			0% {
-				background: transparent;
-			}
-			100% {
-				background: rgba(0, 0, 0, 0.8);
-			}
-		}
-		.tip-wrapper {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 2;
-			background: rgba(0, 0, 0, 0.5);
-			padding: 0 48px;
-			width: 100%;
-			height: 48px;
-			line-height: 48px;
-			color: #fff;
-			font-size: 16px;
-			display: flex;
-			justify-content: space-between;
-			.name {
-				flex: 1;
-				padding-right: 16px;
-				text-align: left;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				white-space: nowrap;
-				.un-save {
-					color: @Warning;
-					font-size: 14px;
-				}
-			}
-			.tool-wrapper {
-				flex: 1;
-				display: flex;
-				justify-content: flex-end;
-				.item {
-					margin-left: 16px;
-					height: 48px;
-					line-height: 48px;
-					cursor: pointer;
-					&:hover {
-						opacity: 0.7;
-					}
-				}
-				.download-link {
-					color: inherit;
-					font-size: 18px;
-				}
-				.text-wrapper {
-					.text {
-						margin-right: 8px;
-					}
-				}
-			}
-		}
-		.code-editor-wrapper {
-			margin: 56px auto 0 auto;
-			width: 90vw;
-			height: calc(100vh - 80px);
-			.operate-wrapper {
-				display: flex;
-				justify-content: space-between;
-				align-items: center;
-				border-radius: 8px 8px 0 0;
-				border-bottom: 1px solid @BorderBase;
-				padding: 8px 16px;
-				background: #fff;
-				.save-icon {
-					font-size: 20px;
-					cursor: pointer;
-					color: @Info;
-					font-weight: 550;
-					&:hover {
-						opacity: 0.5;
-					}
-				}
-				.editor-set-form {
-					flex: 1;
-					text-align: right;
-					// >>> .el-form-item {
-					//   margin-bottom: 0;
-					//   &.font-size {
-					//     .el-form-item__content {
-					//       .el-select {
-					//         width: 96px;
-					//       }
-					//     }
-					//   }
-					//   &.lanaguage {
-					//     .el-form-item__content {
-					//       .el-select {
-					//         width: 120px;
-					//       }
-					//     }
-					//   }
-					//   &.theme {
-					//     .el-form-item__content {
-					//       .el-select {
-					//         width: 190px;
-					//       }
-					//     }
-					//   }
-					// }
-				}
-			}
-			.code-editor {
-				height: calc(100vh - 129px);
-				//   >>> .CodeMirror {
-				//     border-radius: 0 0 8px 8px;
-				//     height: inherit;
-				//     font-size: inherit;
-				//     * {
-				//       font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace !important;
-				//     }
-				//     .CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
-				//       display: none !important;
-				//     }
-				//     .CodeMirror-scroll {
-				//       width: 100%;
-				//       padding: 8px 0 0 0;
-				//       line-height: 1.5;
-				//       font-size: inherit;
-				//       setScrollbar(12px, transparent, #C0C4CC);
-				//     }
-				//   }
-			}
-		}
-	}
-	.editor-preveiw {
-		margin-left: -102px;
-	}
-	.editor-preveiw[data-v-8897ab08] {
-		margin-left: -102px;
-	}
-	/deep/ .el-checkbox__input.is-checked + .el-checkbox__label {
-		color: #29175b;
-	}
-	/deep/ .el-checkbox__input.is-checked .el-checkbox__inner,
-	.el-checkbox__input.is-indeterminate .el-checkbox__inner {
-		background-color: #29175b;
-		border-color: #29175b;
-	}
-	/deep/ .el-select .el-input.is-focus .el-input__inner {
-		border-color: #29175b;
-	}
-	/deep/ .el-select-dropdown__item.selected {
-		color: #29175b !important;
-		font-weight: 700;
-	}
-</style>

+ 0 - 2
src/views/myResource/file/box/codePreview/BoxMask.vue

@@ -112,8 +112,6 @@
 		isEdit: Boolean,
 		callback: Function
 	})
-
-	const { proxy } = getCurrentInstance()
 	const myResource = useMyResourceStore()
 
 	const visible = ref(false)

+ 0 - 7
src/views/myResource/file/box/codePreview/fold.js

@@ -1,7 +0,0 @@
-// 折叠
-import 'codemirror/addon/fold/foldgutter.css'
-import 'codemirror/addon/fold/foldcode'
-import 'codemirror/addon/fold/foldgutter'
-import 'codemirror/addon/fold/brace-fold'
-import 'codemirror/addon/fold/comment-fold'
-import 'codemirror/addon/fold/indent-fold'

+ 0 - 24
src/views/myResource/file/box/codePreview/mode.js

@@ -1,24 +0,0 @@
-/**
- * 这里引入几个常用的语言解析模式
- * 全量语言模式参考,https://codemirror.net/mode/
- */
-import 'codemirror/mode/clike/clike.js' //  C | C++ | Objective-C | Scala | Ceylon | Java 语言
-import 'codemirror/mode/javascript/javascript.js' //  JavaScript 语言
-import 'codemirror/mode/css/css.js' //  css 语言 | less 预编译器 | scss 预编译器
-import 'codemirror/mode/go/go.js' //  Go 语言
-import 'codemirror/mode/nginx/nginx.js' //  Nginx 语言
-import 'codemirror/mode/php/php.js' //  PHP 语言
-import 'codemirror/mode/powershell/powershell.js' //  Bat 文件
-import 'codemirror/mode/properties/properties.js' //  properties 文件
-import 'codemirror/mode/python/python.js' //  Python 语言
-import 'codemirror/mode/r/r.js' //  R 语言
-import 'codemirror/mode/rust/rust.js' //  R 语言
-import 'codemirror/mode/sass/sass.js' //  sass 预编译器
-import 'codemirror/mode/shell/shell.js' //  sass 预编译器
-import 'codemirror/mode/stylus/stylus.js' //  stylus 预编译器
-import 'codemirror/mode/swift/swift.js' //  Swift 语言
-import 'codemirror/mode/vue/vue.js' //  vue.js 框架
-import 'codemirror/mode/sql/sql.js' //  SQL 语言
-import 'codemirror/mode/xml/xml.js' //  xml 语言
-import 'codemirror/mode/yaml/yaml' //  YAML 语言
-import 'codemirror/mode/htmlmixed/htmlmixed.js' //  html 标记语言

+ 0 - 69
src/views/myResource/file/box/codePreview/theme.js

@@ -1,69 +0,0 @@
-/**
- * codemirror 所有的高亮代码主题
- */
-import 'codemirror/theme/3024-day.css'
-import 'codemirror/theme/3024-night.css'
-import 'codemirror/theme/abbott.css'
-import 'codemirror/theme/abcdef.css'
-import 'codemirror/theme/ambiance-mobile.css'
-import 'codemirror/theme/ambiance.css'
-import 'codemirror/theme/ayu-dark.css'
-import 'codemirror/theme/ayu-mirage.css'
-import 'codemirror/theme/base16-dark.css'
-import 'codemirror/theme/base16-light.css'
-import 'codemirror/theme/bespin.css'
-import 'codemirror/theme/blackboard.css'
-import 'codemirror/theme/cobalt.css'
-import 'codemirror/theme/colorforth.css'
-import 'codemirror/theme/darcula.css'
-import 'codemirror/theme/dracula.css'
-import 'codemirror/theme/duotone-dark.css'
-import 'codemirror/theme/duotone-light.css'
-import 'codemirror/theme/eclipse.css'
-import 'codemirror/theme/elegant.css'
-import 'codemirror/theme/erlang-dark.css'
-import 'codemirror/theme/gruvbox-dark.css'
-import 'codemirror/theme/hopscotch.css'
-import 'codemirror/theme/icecoder.css'
-import 'codemirror/theme/idea.css'
-import 'codemirror/theme/isotope.css'
-import 'codemirror/theme/juejin.css'
-import 'codemirror/theme/lesser-dark.css'
-import 'codemirror/theme/liquibyte.css'
-import 'codemirror/theme/lucario.css'
-import 'codemirror/theme/material-darker.css'
-import 'codemirror/theme/material-ocean.css'
-import 'codemirror/theme/material-palenight.css'
-import 'codemirror/theme/material.css'
-import 'codemirror/theme/mbo.css'
-import 'codemirror/theme/mdn-like.css'
-import 'codemirror/theme/midnight.css'
-import 'codemirror/theme/monokai.css'
-import 'codemirror/theme/moxer.css'
-import 'codemirror/theme/neat.css'
-import 'codemirror/theme/neo.css'
-import 'codemirror/theme/night.css'
-import 'codemirror/theme/nord.css'
-import 'codemirror/theme/oceanic-next.css'
-import 'codemirror/theme/panda-syntax.css'
-import 'codemirror/theme/paraiso-dark.css'
-import 'codemirror/theme/paraiso-light.css'
-import 'codemirror/theme/pastel-on-dark.css'
-import 'codemirror/theme/railscasts.css'
-import 'codemirror/theme/rubyblue.css'
-import 'codemirror/theme/seti.css'
-import 'codemirror/theme/shadowfox.css'
-import 'codemirror/theme/solarized.css'
-import 'codemirror/theme/ssms.css'
-import 'codemirror/theme/the-matrix.css'
-import '_public/codemirror/css/tomorrow-night.css' //  自定义高亮样式文件
-import 'codemirror/theme/tomorrow-night-bright.css'
-import 'codemirror/theme/tomorrow-night-eighties.css'
-import 'codemirror/theme/ttcn.css'
-import 'codemirror/theme/twilight.css'
-import 'codemirror/theme/vibrant-ink.css'
-import 'codemirror/theme/xq-dark.css'
-import 'codemirror/theme/xq-light.css'
-import 'codemirror/theme/yeti.css'
-import 'codemirror/theme/yonce.css'
-import 'codemirror/theme/zenburn.css'

+ 1 - 0
src/views/myResource/file/box/contextMenu/Box.vue

@@ -613,6 +613,7 @@
 				position: absolute;
 				display: none;
 				list-style: none;
+				margin-left: -15px;
 				.unzip-item {
 					width: 200px;
 					.setEllipsis(1);

+ 203 - 0
src/views/myResource/file/dialog/unzipFile/Dialog copy.vue

@@ -0,0 +1,203 @@
+<template>
+	<a-modal
+		title="解压缩文件"
+		v-model:visible="visible"
+		:maskClosable="false"
+		:closable="false"
+		@afterVisibleChange="handleVisibleChange"
+		@cancel="handleDialogClose"
+	>
+		<div class="unzip-tree-wrapper" v-if="unzipMode === 2">
+			<div class="target-path">
+				<span class="label">目标路径:</span>
+				<a-input v-model:value="targetPath" readonly size="small" />
+			</div>
+
+			<a-spin :spinning="loading">
+				<a-tree
+					:tree-data="fileTree"
+					:field-names="{ children: 'children', title: 'label', key: 'id' }"
+					highlight-current
+					:expandedKeys="defaultExpandedKeys"
+					@update:expandedKeys="(val) => (defaultExpandedKeys = val)"
+					node-key="id"
+					@select="handleNodeClick"
+					:showLine="true"
+				>
+					<template #title="{ dataRef }">
+						<span class="custom-tree-node">
+							<span class="label">{{ dataRef.label }}</span>
+							<a-button type="link" size="small" class="add-folder-btn" @click.stop="handleAddFolderBtnClick(dataRef)">
+								新建文件夹
+							</a-button>
+						</span>
+					</template>
+				</a-tree>
+			</a-spin>
+		</div>
+		<div class="unzip-text" v-else>
+			<a-spin />
+			正在解压缩,请稍等片刻...
+		</div>
+
+		<template #footer>
+			<template v-if="unzipMode === 2">
+				<a-button @click="handleDialogClose">取 消</a-button>
+				<a-button type="primary" :loading="sureBtnLoading" @click="handleDialogSure"> 确 定 </a-button>
+			</template>
+		</template>
+	</a-modal>
+</template>
+
+<script setup>
+	import { ref } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { getFoldTree, unzipFile } from '@/api/myResource/file'
+	const { proxy } = getCurrentInstance()
+
+	const props = defineProps({
+		unzipMode: Number,
+		userFileId: String,
+		callback: Function
+	})
+
+	const visible = ref(false)
+	const targetPath = ref('/')
+	const fileTree = ref([])
+	const loading = ref(false)
+	const defaultExpandedKeys = ref([])
+	const sureBtnLoading = ref(false)
+
+	const handleDialogClose = () => {
+		visible.value = false
+		props.callback('cancel')
+	}
+
+	const handleVisibleChange = (val) => {
+		if (val) {
+			if (props.unzipMode === 2) {
+				initFileTree()
+			} else {
+				handleUnzipFile()
+			}
+		}
+	}
+
+	const initFileTree = async (id) => {
+		try {
+			loading.value = true
+			const res = await getFoldTree()
+			if (res.success) {
+				fileTree.value = [res.data]
+				defaultExpandedKeys.value = id ? [id] : [fileTree.value[0].id]
+			} else {
+				message.error(res.message)
+			}
+		} finally {
+			loading.value = false
+		}
+	}
+
+	const handleNodeClick = (selectedKeys, { node }) => {
+		targetPath.value = node.filePath || '/'
+	}
+
+	const handleAddFolderBtnClick = async (data) => {
+		const result = await proxy.$openDialog.addFolder({
+			filePath: data.filePath || '/'
+		})
+		if (result === 'confirm') {
+			initFileTree(data.id)
+		}
+	}
+
+	const handleUnzipFile = async () => {
+		try {
+			sureBtnLoading.value = true
+			const reqData = {
+				unzipMode: props.unzipMode,
+				userFileId: props.userFileId,
+				...(props.unzipMode === 2 ? { filePath: targetPath.value } : {})
+			}
+
+			const res = await unzipFile(reqData)
+			if (res.success) {
+				message.success('解压成功')
+				visible.value = false
+				props.callback('confirm')
+			} else {
+				message.error(res.message)
+			}
+		} finally {
+			sureBtnLoading.value = false
+		}
+	}
+
+	const handleDialogSure = () => {
+		handleUnzipFile()
+	}
+
+	defineExpose({
+		visible
+	})
+</script>
+
+<style lang="less" scoped>
+	@import '@/style/myResource/varibles.less';
+
+	.unzip-tree-wrapper {
+		height: 300px;
+		overflow: auto;
+
+		.target-path {
+			display: flex;
+			align-items: center;
+			margin-bottom: 16px;
+
+			.label {
+				width: 80px;
+			}
+
+			.ant-input {
+				flex: 1;
+			}
+		}
+
+		.custom-tree-node {
+			width: 100%;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			.add-folder-btn {
+				display: none;
+			}
+
+			&:hover .add-folder-btn {
+				display: inline-block;
+			}
+		}
+	}
+
+	.unzip-text {
+		padding: 40px 0 64px 0;
+		text-align: center;
+
+		.ant-spin {
+			margin-right: 8px;
+		}
+	}
+
+	.ant-btn-primary {
+		&,
+		&:hover,
+		&:focus {
+			background-color: @primary-color;
+			border-color: @primary-color;
+		}
+	}
+
+	.ant-tree-node-selected {
+		background-color: @primary-color;
+	}
+</style>

+ 225 - 103
src/views/myResource/file/dialog/unzipFile/Dialog.vue

@@ -1,202 +1,324 @@
 <template>
+	<!-- 解压缩文件 -->
 	<a-modal
 		title="解压缩文件"
-		v-model:visible="visible"
+		:visible="internalVisible"
 		:maskClosable="false"
-		:closable="false"
-		@afterVisibleChange="handleVisibleChange"
+		@ok="handleDialogSure"
 		@cancel="handleDialogClose"
+		:afterClose="handleDialogClose"
 	>
 		<div class="unzip-tree-wrapper" v-if="unzipMode === 2">
+			<!-- 选择的目标路径 -->
 			<div class="target-path">
 				<span class="label">目标路径:</span>
-				<a-input v-model:value="targetPath" readonly size="small" />
+				<a-input class="content" v-model:value="targetPath" readonly size="small"></a-input>
 			</div>
-
+			<!-- 文件目录树 -->
 			<a-spin :spinning="loading">
-				<a-directory-tree
-					:treeData="fileTree"
-					:fieldNames="{
-						children: 'children',
-						title: 'label'
-					}"
-					:defaultExpandedKeys="defaultExpandedKeys"
+				<a-tree
+					:tree-data="fileTree"
+					:field-names="{ children: 'children', title: 'label', key: 'id' }"
+					highlight-current
+					:expandedKeys="defaultExpandedKeys"
+					@update:expandedKeys="(val) => (defaultExpandedKeys = val)"
 					@select="handleNodeClick"
+					:showLine="true"
 				>
-					<template #title="{ node, data }">
-						<div class="custom-tree-node">
-							<span class="label">{{ node.title }}</span>
-							<a-button type="link" size="small" class="add-folder-btn" @click.stop="handleAddFolderBtnClick(data)">
+					<template #title="{ dataRef }">
+						<span class="custom-tree-node">
+							<span class="label">{{ dataRef.label }}</span>
+							<a-button class="add-folder-btn" type="link" size="small" @click.stop="handleAddFolderBtnClick(dataRef)">
 								新建文件夹
 							</a-button>
-						</div>
+						</span>
 					</template>
-				</a-directory-tree>
+				</a-tree>
 			</a-spin>
 		</div>
-		<div class="unzip-text" v-else>
-			<a-spin />
-			正在解压缩,请稍等片刻...
-		</div>
-
+		<div class="unzip-text" v-else><a-spin /> 正在解压缩,请稍等片刻...</div>
 		<template #footer>
-			<template v-if="unzipMode === 2">
+			<div v-if="unzipMode === 2">
 				<a-button @click="handleDialogClose">取 消</a-button>
-				<a-button type="primary" :loading="sureBtnLoading" @click="handleDialogSure"> 确 定 </a-button>
-			</template>
+				<a-button type="primary" :loading="sureBtnLoading" @click="handleDialogSure">确 定</a-button>
+			</div>
 		</template>
 	</a-modal>
 </template>
 
 <script setup>
-	import { ref } from 'vue'
-	import { message } from 'ant-design-vue'
+	import { ref, getCurrentInstance, watch } from 'vue'
 	import { getFoldTree, unzipFile } from '@/api/myResource/file'
+	import { message } from 'ant-design-vue'
+
 	const { proxy } = getCurrentInstance()
 
 	const props = defineProps({
-		unzipMode: Number,
-		userFileId: String,
+		visible: {
+			type: Boolean,
+			default: false
+		},
+		unzipMode: {
+			type: Number,
+			default: 0
+		},
+		userFileId: {
+			type: String,
+			default: ''
+		},
 		callback: Function
 	})
 
-	const visible = ref(false)
-	const targetPath = ref('/')
-	const fileTree = ref([])
-	const loading = ref(false)
+	const emit = defineEmits(['update:visible'])
+	const internalVisible = ref(props.visible) // 使用 internalVisible 作为主要的响应式状态
+	const targetPath = ref('/') //  目标路径
+	const fileTree = ref([]) //  文件夹目录树
+	const loading = ref(false) //  文件夹目录树 loading 状态
 	const defaultExpandedKeys = ref([])
-	const sureBtnLoading = ref(false)
+	const sureBtnLoading = ref(false) //  确定按钮 loading 状态
+
+	watch(
+		internalVisible, // 监听内部的 internalVisible ref
+		(newVal, oldVal) => {
+			if (newVal && !oldVal) {
+				// 仅在从 false 变为 true 时调用,即对话框打开时
+				handleDialogOpen()
+			}
+		}
+	)
+
+	// 如果父组件通过 prop 更新 visible,也需要同步到 internalVisible
+	watch(
+		() => props.visible,
+		(newVal) => {
+			internalVisible.value = newVal
+		}
+	)
 
+	/**
+	 * 取消按钮点击事件 & 对话框关闭的回调
+	 */
 	const handleDialogClose = () => {
-		visible.value = false
+		internalVisible.value = false
+		emit('update:visible', false) // 更新父组件的 visible 值
 		props.callback('cancel')
 	}
 
-	const handleVisibleChange = (val) => {
-		if (val) {
-			if (props.unzipMode === 2) {
-				initFileTree()
-			} else {
-				handleUnzipFile()
-			}
+	/**
+	 * 对话框打开的回调
+	 */
+	const handleDialogOpen = () => {
+		if (props.unzipMode === 2) {
+			initFileTree()
+		} else {
+			handleUnzipFile()
 		}
 	}
 
+	/**
+	 * 初始化文件目录树
+	 */
 	const initFileTree = async (id) => {
+		loading.value = true
 		try {
-			loading.value = true
 			const res = await getFoldTree()
 			if (res.success) {
 				fileTree.value = [res.data]
-				defaultExpandedKeys.value = id ? [id] : [fileTree.value[0].id]
+				defaultExpandedKeys.value = id ? [id] : fileTree.value.length > 0 ? [fileTree.value[0].id] : []
 			} else {
 				message.error(res.message)
 			}
+		} catch (error) {
+			message.error('获取文件夹树失败')
+			console.error(error)
 		} finally {
 			loading.value = false
 		}
 	}
 
-	const handleNodeClick = (selectedKeys, { node }) => {
-		targetPath.value = node.filePath || '/'
+	/**
+	 * 目录树节点点击回调函数
+	 * @param {Array} selectedKeys 当前选中的节点的 key
+	 * @param {object} e event 对象,包含 {selected: bool, selectedNodes, node, event}
+	 */
+	const handleNodeClick = (selectedKeys, e) => {
+		if (e.node && e.node.dataRef) {
+			targetPath.value = e.node.dataRef.filePath ? e.node.dataRef.filePath : '/'
+		}
 	}
 
+	/**
+	 * 新建文件夹按钮点击事件
+	 * @description 调用新建文件夹服务,并在弹窗确认回调事件中刷新文件夹树
+	 */
 	const handleAddFolderBtnClick = async (data) => {
-		const result = await proxy.$openDialog.addFolder({
-			filePath: data.filePath || '/'
-		})
-		if (result === 'confirm') {
+		try {
+			await proxy.$openDialog.addFolder({
+				filePath: data.filePath || '/'
+			})
 			initFileTree(data.id)
+		} catch (error) {
+			console.log('Add folder cancelled or failed', error)
 		}
 	}
 
+	/**
+	 * 确定按钮点击事件
+	 * @description 调用移动文件接口
+	 */
+	const handleDialogSure = () => {
+		sureBtnLoading.value = true
+		handleUnzipFile()
+	}
+
+	/**
+	 * 解压文件
+	 */
 	const handleUnzipFile = async () => {
-		try {
-			sureBtnLoading.value = true
-			const reqData = {
-				unzipMode: props.unzipMode,
-				userFileId: props.userFileId,
-				...(props.unzipMode === 2 ? { filePath: targetPath.value } : {})
+		// unzipMode 解压模式 0-解压到当前文件夹, 1-自动创建该文件名目录,并解压到目录里, 2-手动选择解压目录
+		let reqData = {
+			unzipMode: props.unzipMode,
+			userFileId: props.userFileId
+		}
+		if (props.unzipMode === 2) {
+			reqData = {
+				...reqData,
+				filePath: targetPath.value //  解压的目标路径
 			}
+		}
 
+		try {
 			const res = await unzipFile(reqData)
+			sureBtnLoading.value = false
 			if (res.success) {
 				message.success('解压成功')
-				visible.value = false
+				internalVisible.value = false
+				emit('update:visible', false)
 				props.callback('confirm')
 			} else {
 				message.error(res.message)
 			}
-		} finally {
+		} catch (error) {
 			sureBtnLoading.value = false
+			message.error('解压失败')
+			console.error(error)
 		}
 	}
 
-	const handleDialogSure = () => {
-		handleUnzipFile()
-	}
-
+	// 暴露给模板或者父组件(如果需要ref访问)
 	defineExpose({
-		visible
+		visible: internalVisible // 暴露 internalVisible
 	})
 </script>
 
 <style lang="less" scoped>
 	@import '@/style/myResource/varibles.less';
+	@import '@/style/myResource/mixins.less';
 
-	.unzip-tree-wrapper {
-		height: 300px;
-		overflow: auto;
-
-		.target-path {
+	// 使用 :deep 替代 >>>
+	:deep(.ant-modal) {
+		.ant-modal-header {
 			display: flex;
-			align-items: center;
-			margin-bottom: 16px;
+		}
 
-			.label {
-				width: 80px;
-			}
+		.ant-modal-body {
+			padding: 10px 30px;
 
-			.ant-input {
-				flex: 1;
-			}
-		}
+			.unzip-tree-wrapper {
+				height: 300px;
+				overflow: auto;
+				&::-webkit-scrollbar {
+					width: 6px;
+				}
+				&::-webkit-scrollbar-thumb {
+					background: #ccc; // 滚动条颜色
+					border-radius: 3px;
+				}
+				&::-webkit-scrollbar-track {
+					background: #f1f1f1; // 轨道颜色
+				}
 
-		.custom-tree-node {
-			width: 100%;
-			display: flex;
-			align-items: center;
-			justify-content: space-between;
+				.target-path {
+					display: flex;
+					align-items: center;
+					margin-bottom: 10px;
 
-			.add-folder-btn {
-				display: none;
-			}
+					.label {
+						width: 80px;
+						flex-shrink: 0;
+					}
+
+					.content {
+						flex: 1;
+					}
+				}
+
+				.ant-tree {
+					.ant-tree-treenode {
+						width: 100%;
+						// antd v4+ 使用 .ant-tree-treenode
+						.ant-tree-node-content-wrapper {
+							height: 34px;
+							font-size: 16px;
+							display: flex;
+							align-items: center;
+						}
+						&:hover {
+							.add-folder-btn {
+								display: inline-block;
+							}
+						}
+
+						.ant-tree-switcher .ant-tree-switcher-icon svg {
+							font-size: 18px;
+						}
+
+						.custom-tree-node {
+							flex: 1;
+							display: flex;
+							align-items: center;
+							justify-content: space-between;
+							font-size: 14px;
+							padding-right: 8px;
 
-			&:hover .add-folder-btn {
-				display: inline-block;
+							.add-folder-btn {
+								color: #000000;
+								display: none;
+								margin-left: 8px;
+							}
+						}
+					}
+				}
+			}
+			.unzip-text {
+				padding: 40px 0 64px 0;
+				text-align: center;
 			}
 		}
 	}
 
-	.unzip-text {
-		padding: 40px 0 64px 0;
-		text-align: center;
+	:deep(.ant-tree .ant-tree-treenode .custom-tree-node) {
+		display: flex;
+		justify-content: space-between;
+		width: 100%;
+	}
 
-		.ant-spin {
-			margin-right: 8px;
-		}
+	:deep(.ant-tree .ant-tree-treenode:hover .custom-tree-node .add-folder-btn) {
+		color: @primary-color !important;
+		display: inline-block !important;
 	}
 
-	.ant-btn-primary {
-		&,
-		&:hover,
-		&:focus {
-			background-color: @primary-color;
-			border-color: @primary-color;
-		}
+	:deep(.ant-tree .ant-tree-treenode .custom-tree-node .add-folder-btn) {
+		color: #000000;
+		display: none !important;
+		margin-left: 8px;
 	}
 
-	.ant-tree-node-selected {
-		background-color: @primary-color;
+	:deep(.ant-tree) {
+		.ant-tree-treenode,
+		.ant-tree-node-content-wrapper,
+		.ant-tree-title {
+			width: 100%;
+		}
 	}
 </style>