DiskPhoto.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <template>
  2. <div class="photos-container">
  3. <van-nav-bar fixed placeholder :title="isEditMode ? `已选中 ${selectedIds.length} 项` : '相册'">
  4. <template #right>
  5. <span @click="toggleEditMode" class="nav-action">
  6. {{ isEditMode ? '取消' : '管理' }}
  7. </span>
  8. </template>
  9. </van-nav-bar>
  10. <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
  11. <van-list v-model="loading" :finished="finished" @load="onLoad">
  12. <div v-for="group in groupedPhotos" :key="group.date" class="photo-group">
  13. <div class="group-header">
  14. <span class="date-title">{{ formatGroupDate(group.date) }}</span>
  15. <span v-if="isEditMode" class="select-all" @click="selectGroup(group)">
  16. {{ isGroupSelected(group) ? '取消全选' : '全选' }}
  17. </span>
  18. </div>
  19. <van-grid :column-num="3" :gutter="2" :border="false" square>
  20. <van-grid-item
  21. v-for="item in group.children"
  22. :key="item.fileId"
  23. @click="onItemClick(item)"
  24. >
  25. <van-image width="100%" height="100%" fit="cover" lazy-load :src="item.url" />
  26. <div v-if="item.type === 'video'" class="video-info-tag">
  27. <van-icon name="play-circle-o" class="play-icon" />
  28. <span class="duration">{{ item.duration }}</span>
  29. </div>
  30. <div
  31. v-if="isEditMode"
  32. class="select-mask"
  33. :class="{ active: selectedIds.includes(item.fileId) }"
  34. >
  35. <van-icon :name="selectedIds.includes(item.fileId) ? 'checked' : 'circle'" />
  36. </div>
  37. </van-grid-item>
  38. </van-grid>
  39. </div>
  40. </van-list>
  41. </van-pull-refresh>
  42. <transition name="van-slide-up">
  43. <div v-if="isEditMode" class="bottom-tab-bar">
  44. <div class="action-item" @click="handleBatchDownload">
  45. <van-icon name="down" />
  46. <span>下载</span>
  47. </div>
  48. <div class="action-item delete" @click="handleBatchDelete">
  49. <van-icon name="delete-o" />
  50. <span>删除</span>
  51. </div>
  52. </div>
  53. </transition>
  54. <van-popup
  55. v-model="showPlayerModal"
  56. position="bottom"
  57. :style="{ height: '100%' }"
  58. closeable
  59. @closed="onVideoPopupClosed"
  60. >
  61. <div v-if="currentVideoItem" class="player-container">
  62. <div class="player-header van-ellipsis">{{ currentVideoItem.filename }}</div>
  63. <div class="video-wrapper">
  64. <video
  65. ref="videoPlayer"
  66. :src="currentVideoItem.videoUrl"
  67. controls
  68. autoplay
  69. playsinline
  70. webkit-playsinline
  71. class="native-video"
  72. >
  73. 您的浏览器不支持视频播放。
  74. </video>
  75. </div>
  76. <div class="player-info">
  77. <van-cell title="文件大小" :value="currentVideoItem.size" />
  78. <van-cell title="拍摄时间" :value="currentVideoItem.updateTime" />
  79. </div>
  80. </div>
  81. </van-popup>
  82. </div>
  83. </template>
  84. <script>
  85. import { getPhotoItems } from '@/api/disk'
  86. export default {
  87. data() {
  88. return {
  89. queryParams: {
  90. pn: 1
  91. },
  92. showPlayerModal: false, // 控制弹窗
  93. currentVideoItem: null, // 当前播放的视频对象
  94. photoList: [],
  95. loading: false, // 是否正在加载
  96. finished: false, // 是否完成
  97. refreshing: false, // 是否刷新
  98. page: 1,
  99. isEditMode: false, // 是否处于多选模式
  100. selectedIds: [] // 已选中的照片 ID 列表
  101. }
  102. },
  103. computed: {
  104. groupedPhotos() {
  105. return this.groupDataByDate(this.photoList)
  106. }
  107. },
  108. methods: {
  109. async onLoad() {
  110. // 1. 如果正在下拉刷新中,不执行滚动加载逻辑
  111. if (this.refreshing) return
  112. try {
  113. // 2. 调用接口(确保传参是当前的 pn)
  114. const resp = await getPhotoItems(this.queryParams)
  115. if (resp.code === 0) {
  116. const respData = resp.data
  117. const newData = respData.list || []
  118. // 3. 如果是第一页,直接赋值;否则追加
  119. if (this.queryParams.pn === 1) {
  120. this.photoList = newData
  121. } else {
  122. this.photoList.push(...newData)
  123. }
  124. // 4. 加载状态结束
  125. this.loading = false
  126. // 5. 判断是否还有下一页
  127. // 注意:这里建议优先判断 newData 是否为空,或者使用后端返回的 hasNext
  128. if (!respData.hasNext || newData.length === 0) {
  129. this.finished = true
  130. } else {
  131. this.queryParams.pn++ // 准备加载下一页
  132. }
  133. } else {
  134. this.$toast(resp.msg)
  135. this.finished = true
  136. }
  137. } catch (error) {
  138. this.loading = false
  139. this.finished = true
  140. this.$toast('加载失败')
  141. }
  142. },
  143. onRefresh() {
  144. // 下拉刷新的重置逻辑
  145. this.finished = false
  146. this.loading = true // 展示加载转圈
  147. this.refreshing = true
  148. // 重置参数
  149. this.queryParams.pn = 1
  150. // 重新发起请求
  151. this.onLoad()
  152. },
  153. // 转换扁平数据为分组数据
  154. groupDataByDate(flatList) {
  155. const groups = {}
  156. flatList.forEach((item) => {
  157. // 提取日期部分 (2024-05-09)
  158. const date = item.updateTime.split(' ')[0]
  159. if (!groups[date]) {
  160. groups[date] = []
  161. }
  162. groups[date].push(item)
  163. })
  164. // 转换为数组格式以便 v-for 渲染
  165. return Object.keys(groups)
  166. .map((date) => ({
  167. date,
  168. children: groups[date]
  169. }))
  170. .sort((a, b) => new Date(b.date) - new Date(a.date)) // 按日期倒序
  171. },
  172. formatGroupDate(dateStr) {
  173. const today = new Date().toISOString().split('T')[0]
  174. const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]
  175. if (dateStr === today) return '今天'
  176. if (dateStr === yesterday) return '昨天'
  177. // 返回 MM-DD 格式
  178. return dateStr.split('-').slice(1).join('-')
  179. },
  180. // 调用 Vant 的全屏预览
  181. handlePreview(currentImg) {
  182. // 获取所有照片的 URL 列表
  183. const allUrls = this.photoList.map((p) => p.url)
  184. // 找到当前点击图片在原始扁平数组中的位置
  185. const startIndex = this.photoList.findIndex((p) => p.id === currentImg.id)
  186. this.$imagePreview({
  187. images: allUrls,
  188. startPosition: startIndex,
  189. closeable: true
  190. })
  191. },
  192. toggleEditMode() {
  193. this.isEditMode = !this.isEditMode
  194. this.selectedIds = [] // 切换模式时清空选中
  195. },
  196. onItemClick(item) {
  197. if (this.isEditMode) {
  198. this.handleSelect(item.fileId)
  199. } else {
  200. if (item.type === 'video') {
  201. // 唤起视频播放弹窗 (复用之前 Home.vue 里的视频预览逻辑)
  202. this.previewVideo(item)
  203. } else {
  204. // 唤起图片预览
  205. this.handlePreview(item)
  206. }
  207. }
  208. },
  209. handleSelect(id) {
  210. const index = this.selectedIds.indexOf(id)
  211. if (index > -1) {
  212. // 如果已经在数组里,说明是要取消选中
  213. this.selectedIds.splice(index, 1)
  214. } else {
  215. // 否则,加入选中列表
  216. this.selectedIds.push(id)
  217. }
  218. },
  219. // 别忘了补齐 previewVideo 方法 (如果还没从 Home.vue 挪过来的话)
  220. previewVideo(item) {
  221. this.currentVideoItem = item
  222. this.showPlayerModal = true
  223. },
  224. // 弹窗关闭后的清理逻辑
  225. onVideoPopupClosed() {
  226. if (this.$refs.videoPlayer) {
  227. this.$refs.videoPlayer.pause() // 停止播放防止后台有声音
  228. }
  229. this.currentVideoItem = null
  230. },
  231. // 组全选逻辑
  232. selectGroup(group) {
  233. const groupIds = group.children.map((img) => img.id)
  234. if (this.isGroupSelected(group)) {
  235. // 如果已全选,则该组全部取消
  236. this.selectedIds = this.selectedIds.filter((id) => !groupIds.includes(id))
  237. } else {
  238. // 否则将该组未选中的 ID 加入
  239. groupIds.forEach((id) => {
  240. if (!this.selectedIds.includes(id)) this.selectedIds.push(id)
  241. })
  242. }
  243. },
  244. isGroupSelected(group) {
  245. return group.children.every((img) => this.selectedIds.includes(img.id))
  246. },
  247. handleBatchDownload() {
  248. console.log('handleBatchDownload')
  249. },
  250. handleBatchDelete() {
  251. if (this.selectedIds.length === 0) return this.$toast('请先选择照片')
  252. this.$dialog
  253. .confirm({
  254. title: '确认删除',
  255. message: `确定要删除这 ${this.selectedIds.length} 张照片吗?`,
  256. confirmButtonColor: '#ee0a24'
  257. })
  258. .then(() => {
  259. // 模拟删除
  260. this.photoList = this.photoList.filter((img) => !this.selectedIds.includes(img.id))
  261. this.isEditMode = false
  262. this.selectedIds = []
  263. this.$toast.success('删除成功')
  264. })
  265. }
  266. }
  267. }
  268. </script>
  269. <style lang="less" scoped>
  270. .nav-action {
  271. color: #1989fa;
  272. font-size: 14px;
  273. }
  274. .select-mask {
  275. position: absolute;
  276. top: 0;
  277. left: 0;
  278. right: 0;
  279. bottom: 0;
  280. background: rgba(0, 0, 0, 0.1);
  281. display: flex;
  282. justify-content: flex-end;
  283. padding: 5px;
  284. &.active {
  285. background: rgba(25, 137, 250, 0.2);
  286. border: 2px solid #1989fa;
  287. box-sizing: border-box;
  288. }
  289. .select-icon {
  290. font-size: 20px;
  291. color: #ebedf0;
  292. }
  293. &.active .select-icon {
  294. color: #1989fa;
  295. }
  296. }
  297. .bottom-tab-bar {
  298. position: fixed;
  299. bottom: 0;
  300. left: 0;
  301. right: 0;
  302. height: 50px;
  303. background: #fff;
  304. display: flex;
  305. box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
  306. z-index: 100;
  307. .action-item {
  308. flex: 1;
  309. display: flex;
  310. flex-direction: column;
  311. align-items: center;
  312. justify-content: center;
  313. font-size: 10px;
  314. color: #646566;
  315. .van-icon {
  316. font-size: 20px;
  317. margin-bottom: 2px;
  318. }
  319. &.delete {
  320. color: #ee0a24;
  321. }
  322. }
  323. }
  324. .select-all {
  325. font-size: 12px;
  326. color: #1989fa;
  327. }
  328. .video-info-tag {
  329. position: absolute;
  330. left: 0;
  331. right: 0;
  332. bottom: 0;
  333. padding: 4px 6px;
  334. background: linear-gradient(transparent, rgba(0, 0, 0, 0.5));
  335. display: flex;
  336. align-items: center;
  337. justify-content: space-between;
  338. pointer-events: none; /* 确保不影响点击事件 */
  339. .play-icon {
  340. color: #fff;
  341. font-size: 14px;
  342. }
  343. .duration {
  344. color: #fff;
  345. font-size: 11px;
  346. font-weight: 500;
  347. }
  348. }
  349. /* 视频播放器弹窗样式 */
  350. .player-container {
  351. background: #000;
  352. height: 100%;
  353. display: flex;
  354. flex-direction: column;
  355. .player-header {
  356. padding: 15px 45px; // 留出关闭按钮的空间
  357. text-align: center;
  358. font-size: 16px;
  359. color: #fff;
  360. background: rgba(0, 0, 0, 0.8);
  361. }
  362. .video-wrapper {
  363. flex: 1;
  364. display: flex;
  365. align-items: center;
  366. justify-content: center;
  367. background: #000;
  368. }
  369. .native-video {
  370. width: 100%;
  371. max-height: 80vh;
  372. object-fit: contain; // 保持视频比例,不拉伸
  373. }
  374. .player-info {
  375. padding-bottom: 20px;
  376. // 调整内部 Cell 为深色风格
  377. /deep/ .van-cell {
  378. background-color: #1a1a1a;
  379. color: #eee;
  380. &::after {
  381. border-bottom: 1px solid #333;
  382. }
  383. .van-cell__value {
  384. color: #aaa;
  385. }
  386. }
  387. }
  388. }
  389. /* 覆盖 Vant Popup 的关闭按钮颜色,使其在黑底上可见 */
  390. /deep/ .van-popup__close-icon {
  391. color: #fff;
  392. }
  393. </style>