Explorar el Código

添加 disk 移动端的页面和路由

reghao hace 2 semanas
padre
commit
6a6e0cc75a

+ 53 - 0
src/router/diskm.js

@@ -0,0 +1,53 @@
+const Disk = () => import('views/diskm/Disk')
+const DiskPhoto = () => import('views/diskm/DiskPhoto')
+const DiskMe = () => import('views/diskm/DiskMe')
+const DiskFile = () => import('views/diskm/DiskFile')
+const DiskShare = () => import('views/diskm/DiskShare')
+const CamList = () => import('views/diskm/CamList')
+const CamDetail = () => import('views/diskm/CamDetail')
+
+export default {
+  path: '/disk',
+  name: 'Disk',
+  component: Disk,
+  redirect: '/disk/file',
+  meta: { needAuth: true },
+  children: [
+    {
+      path: '/disk/file',
+      name: 'DiskFile',
+      component: DiskFile,
+      meta: { title: '我的文件', needAuth: true }
+    },
+    {
+      path: '/disk/photo',
+      name: 'DiskPhoto',
+      component: DiskPhoto,
+      meta: { title: '我的相册', needAuth: true }
+    },
+    {
+      path: '/disk/cam',
+      name: 'CamList',
+      component: CamList,
+      meta: { title: '我的监控', needAuth: true }
+    },
+    {
+      path: '/disk/cam/detail',
+      name: 'CamDetail',
+      component: CamDetail,
+      meta: { needAuth: true }
+    },
+    {
+      path: '/disk/share',
+      name: 'DiskShare',
+      component: DiskShare,
+      meta: { title: '我的分享', needAuth: true }
+    },
+    {
+      path: '/disk/me',
+      name: 'DiskMe',
+      component: DiskMe,
+      meta: { title: '我的', needAuth: true }
+    }
+  ]
+}

+ 1 - 0
src/router/index.js

@@ -2,6 +2,7 @@ import VueRouter from 'vue-router'
 import Vue from 'vue'
 
 import DiskRouter from './disk'
+import DiskMobileRouter from './diskm'
 import UserRouter from './user'
 import BlogRouter from './blog'
 import AiRouter from './ai'

+ 449 - 0
src/views/diskm/CamDetail.vue

@@ -0,0 +1,449 @@
+<template>
+  <div class="mobile-cam-detail">
+    <div class="mobile-player-viewport">
+      <div class="video-container">
+        <video
+          id="videoElement"
+          controls
+          autoplay
+          muted
+          playsinline
+          webkit-playsinline
+          class="mobile-video-element"
+        />
+        <div v-if="!camDetail" class="video-loading-mask">
+          <i class="el-icon-loading" />
+          <p>正在建立安全加密连接...</p>
+        </div>
+      </div>
+
+      <div class="video-status-bar">
+        <span v-if="camDetail" :class="['status-badge', camDetail.onLive ? 'is-live' : 'is-playback']">
+          <span class="pulse-dot" />
+          {{ camDetail.onLive ? 'LIVE 实时画面' : '历史回放' }}
+        </span>
+        <span class="device-id">ID: {{ query.camId }}</span>
+      </div>
+    </div>
+
+    <div class="device-info-section">
+      <div class="info-left">
+        <h2 class="device-title">{{ camDetail ? camDetail.camName : '摄像头监控' }}</h2>
+      </div>
+      <div class="info-right-actions">
+        <el-button size="mini" icon="el-icon-share" type="primary" round @click="onShareCam">授权分享</el-button>
+        <el-button size="mini" icon="el-icon-refresh" circle @click="refreshData" />
+      </div>
+    </div>
+
+    <div class="history-archive-section">
+      <div class="archive-header">
+        <span class="archive-title"><i class="el-icon-folder-opened" /> 监控回放档案</span>
+
+        <el-date-picker
+          v-model="calendarDate"
+          type="date"
+          placeholder="选择日期"
+          size="mini"
+          :clearable="false"
+          class="mobile-date-picker"
+          @change="handleDateChange"
+        />
+      </div>
+
+      <div class="current-date-display">
+        <span class="date-text">当前查看:{{ getYearMonthDay(calendarDate) }}</span>
+      </div>
+
+      <div class="mobile-record-list">
+        <div
+          v-for="item in dataList"
+          :key="item.recordId"
+          class="mobile-record-item"
+          @click="handlePlay(item.recordId)"
+        >
+          <div class="record-item-left">
+            <div class="play-icon-box">
+              <i class="el-icon-video-play" />
+            </div>
+            <div class="record-meta">
+              <div class="record-time">{{ item.startTime }}</div>
+              <div class="record-duration">时长: {{ item.duration }}秒</div>
+            </div>
+          </div>
+          <i class="el-icon-arrow-right arrow-indicator" />
+        </div>
+
+        <el-empty v-if="!dataList.length" description="该日期暂无监控录像" :image-size="80" />
+      </div>
+    </div>
+
+    <el-dialog
+      title="授权给好友"
+      :visible.sync="showCreateShareDialog"
+      width="90%"
+      custom-class="mobile-auth-dialog"
+      append-to-body
+    >
+      <div class="share-box">
+        <p class="share-tip">请选择有权查看此摄像头画面的好友:</p>
+        <div class="user-select-list">
+          <el-checkbox-group v-model="createShareForm.shareToList">
+            <el-checkbox
+              v-for="user in userContactList"
+              :key="user.userIdStr"
+              :label="user.username"
+              border
+              class="mobile-user-checkbox"
+            >
+              <div class="user-cell-wrapper">
+                <el-avatar :size="20" icon="el-icon-user" />
+                <span class="u-name">{{ user.username }}</span>
+              </div>
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="small" @click="showCreateShareDialog = false" style="width: 45%">取消</el-button>
+        <el-button type="primary" size="small" round style="width: 45%" @click="createShare">确认授权</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import flvjs from 'flv.js'
+import { createShare, getCamDetail, getRecordByMonth, getRecordUrl } from '@/api/disk'
+import { getUserContact } from '@/api/user'
+
+export default {
+  name: 'CamDetailMobile',
+  data() {
+    return {
+      calendarDate: new Date(),
+      query: {
+        camId: '',
+        yearMonth: '',
+        yearMonthDay: ''
+      },
+      dataList: [],
+      camDetail: null,
+      showCreateShareDialog: false,
+      userContactList: [],
+      createShareForm: {
+        albumType: 1,
+        albumId: null,
+        shareToList: []
+      },
+      flvPlayer: null
+    }
+  },
+  created() {
+    this.query.camId = this.$route.query.camId
+    if (this.query.camId) {
+      this.getData()
+    } else {
+      this.$message.error('参数错误')
+      this.$router.go(-1)
+    }
+  },
+  beforeDestroy() {
+    // 移动端路由切换时必须销毁直播流实例,释放手机内存与解码器
+    this.destroyFlvPlayer()
+  },
+  methods: {
+    getData() {
+      getCamDetail(this.query).then(resp => {
+        if (resp.code === 0) {
+          this.camDetail = resp.data
+          // 如果后端返回了当日录像,则直接渲染
+          if(resp.data.dayRecords) {
+            this.dataList = resp.data.dayRecords
+          }
+          this.$nextTick(() => {
+            if (this.camDetail.onLive) {
+              this.initVideoPlayer(this.camDetail.liveUrl, true)
+            } else {
+              this.initVideoPlayer(this.camDetail.url, false)
+            }
+          })
+        }
+      })
+    },
+    refreshData() {
+      this.destroyFlvPlayer()
+      this.getData()
+      this.$message.success('已刷新当前画面')
+    },
+    handleDateChange(date) {
+      this.query.yearMonthDay = this.getYearMonthDay(date)
+      getCamDetail(this.query).then(resp => {
+        if (resp.code === 0) {
+          this.camDetail = resp.data
+          this.dataList = resp.data.dayRecords || []
+          // 如果切换日期带有录像,自动载入新视频
+          if (this.camDetail.url) {
+            this.initVideoPlayer(this.camDetail.url, false)
+          }
+        }
+      })
+    },
+    handlePlay(recordId) {
+      getRecordUrl(recordId).then(resp => {
+        if (resp.code === 0) {
+          this.destroyFlvPlayer()
+          this.initVideoPlayer(resp.data.url, false)
+          // 手机端播放回放时,让屏幕平滑滚动回顶部视频区,提升操作连贯性
+          window.scrollTo({ top: 0, behavior: 'smooth' })
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    destroyFlvPlayer() {
+      if (this.flvPlayer) {
+        this.flvPlayer.pause()
+        this.flvPlayer.unload()
+        this.flvPlayer.detachMediaElement()
+        this.flvPlayer.destroy()
+        this.flvPlayer = null
+      }
+    },
+    initVideoPlayer(videoUrl, live) {
+      this.destroyFlvPlayer()
+      const videoElement = document.getElementById('videoElement')
+      if (!videoElement) return
+
+      if (!live) {
+        videoElement.src = videoUrl
+        videoElement.load()
+        return
+      }
+
+      if (!flvjs.isSupported()) {
+        this.$message.error('当前设备浏览器不支持 FLV 直播流解码')
+        return
+      }
+
+      this.flvPlayer = flvjs.createPlayer({
+        type: 'flv',
+        isLive: true,
+        url: videoUrl,
+        enableStashBuffer: false,
+        hasAudio: true,
+        hasVideo: true
+      })
+      this.flvPlayer.attachMediaElement(videoElement)
+      this.flvPlayer.load()
+      this.flvPlayer.play()
+
+      // 移动端低延时追帧策略
+      videoElement.addEventListener('progress', () => {
+        if(!this.flvPlayer || !this.flvPlayer.buffered.length) return
+        const end = this.flvPlayer.buffered.end(0)
+        const delta = end - this.flvPlayer.currentTime
+        if (delta > 8 || delta < 0) {
+          this.flvPlayer.currentTime = end - 1
+          return
+        }
+        videoElement.playbackRate = delta > 1.2 ? 1.15 : 1.0
+      })
+    },
+    getYearMonthDay(date) {
+      const year = date.getFullYear().toString().padStart(4, '0')
+      const month = (date.getMonth() + 1).toString().padStart(2, '0')
+      const day = date.getDate().toString().padStart(2, '0')
+      return `${year}-${month}-${day}`
+    },
+    onShareCam() {
+      this.createShareForm.albumId = this.camDetail.camId
+      getUserContact(1).then(resp => {
+        if (resp.code === 0) {
+          this.userContactList = resp.data
+          this.showCreateShareDialog = true
+        } else {
+          this.$message.error(resp.msg)
+        }
+      })
+    },
+    createShare() {
+      createShare(this.createShareForm).then(resp => {
+        this.$message.success('授权成功')
+        this.showCreateShareDialog = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mobile-cam-detail {
+  background-color: #f8f9fa;
+  min-height: 100vh;
+  padding-bottom: 40px;
+}
+
+/* 视频视口:严格的移动端 16:9 比例 */
+.mobile-player-viewport {
+  position: sticky;
+  top: 0;
+  width: 100%;
+  background: #000;
+  z-index: 100;
+  box-shadow: 0 4px 10px rgba(0,0,0,0.1);
+}
+
+.video-container {
+  position: relative;
+  width: 100%;
+  aspect-ratio: 16 / 9;
+}
+
+.mobile-video-element {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.video-loading-mask {
+  position: absolute;
+  inset: 0;
+  background: rgba(0,0,0,0.8);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: #fff;
+  font-size: 13px;
+}
+.video-loading-mask i { font-size: 24px; margin-bottom: 8px; }
+
+/* 视频内底部状态栏 */
+.video-status-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  background: #111;
+  color: #fff;
+}
+
+.status-badge {
+  display: flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: bold;
+}
+.is-live { background: #fef0f0; color: #f56c6c; }
+.is-playback { background: #333; color: #bbb; }
+
+.pulse-dot { width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; background: currentColor; }
+.is-live .pulse-dot { animation: pulse 1.5s infinite; }
+
+@keyframes pulse {
+  0% { transform: scale(1); opacity: 1; }
+  50% { transform: scale(1.4); opacity: 0.4; }
+  100% { transform: scale(1); opacity: 1; }
+}
+.device-id { font-size: 11px; color: #888; }
+
+/* 设备操作区块 */
+.device-info-section {
+  background: #fff;
+  padding: 12px 16px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #f0f0f0;
+}
+.device-title { font-size: 16px; font-weight: bold; color: #222; margin: 0; }
+.info-right-actions { display: flex; gap: 8px; }
+
+/* 下半部分:档案列表 */
+.history-archive-section {
+  margin-top: 10px;
+  background: #fff;
+  padding: 14px 16px;
+}
+
+.archive-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+.archive-title { font-size: 14px; font-weight: bold; color: #444; }
+.archive-title i { color: #1a73e8; margin-right: 4px; }
+
+/* 移动端轻量选择器重写 */
+::v-deep .mobile-date-picker.el-input {
+  width: 120px !important;
+}
+
+.current-date-display {
+  background: #f1f5f9;
+  padding: 6px 12px;
+  border-radius: 6px;
+  font-size: 12px;
+  color: #475569;
+  font-weight: 600;
+  margin-bottom: 12px;
+}
+
+/* 列表移动端单元格化 */
+.mobile-record-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.mobile-record-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px;
+  background: #f8fafc;
+  border-radius: 10px;
+  -webkit-tap-highlight-color: transparent;
+}
+.mobile-record-item:active {
+  background: #edf2f7;
+}
+
+.record-item-left { display: flex; align-items: center; gap: 12px; }
+
+.play-icon-box {
+  width: 32px;
+  height: 32px;
+  background: #fff;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  box-shadow: 0 2px 6px rgba(0,0,0,0.05);
+  color: #1a73e8;
+  font-size: 16px;
+}
+
+.record-time { font-size: 14px; font-weight: bold; color: #334155; }
+.record-duration { font-size: 11px; color: #64748b; margin-top: 2px; }
+.arrow-indicator { color: #cbd5e1; font-size: 14px; }
+
+/* 好友授权移动端弹窗微调 */
+::v-deep .mobile-auth-dialog {
+  border-radius: 16px !important;
+}
+.user-select-list { max-height: 240px; overflow-y: auto; }
+.mobile-user-checkbox {
+  width: 100%;
+  margin: 0 0 8px 0 !important;
+  border-radius: 8px;
+  padding: 8px 12px !important;
+  box-sizing: border-box;
+}
+.user-cell-wrapper { display: flex; align-items: center; gap: 8px; }
+.u-name { font-size: 13px; }
+</style>

+ 284 - 0
src/views/diskm/CamList.vue

@@ -0,0 +1,284 @@
+<template>
+  <div v-loading="loading" class="mobile-cam-container">
+    <div class="page-header">
+      <div class="title-section">
+        <h2 class="page-title">我的监控</h2>
+        <span class="online-status-text">
+          在线 <span class="count-highlight">{{ onlineCount }}</span> / {{ dataList.length }}
+        </span>
+      </div>
+      <i class="el-icon-plus-circle-outline add-icon-btn" @click="onAddDevice" />
+    </div>
+
+    <div class="cam-list">
+      <div
+        v-for="item in dataList"
+        :key="item.id"
+        class="mobile-cam-card"
+        @click="handlePlay(item)"
+      >
+        <div class="cam-preview-wrapper">
+          <div class="cam-placeholder">
+            <i class="el-icon-video-camera" />
+          </div>
+
+          <div class="play-btn-mask">
+            <i class="el-icon-video-play play-icon" />
+          </div>
+
+          <div class="status-indicator" :class="{ 'is-online': item.state }">
+            <span class="dot" />
+            {{ item.state ? '在线' : '离线' }}
+          </div>
+        </div>
+
+        <div class="cam-content">
+          <div class="cam-info-block">
+            <h3 class="cam-name">{{ item.camName }}</h3>
+            <p class="cam-id">ID: {{ item.camId }}</p>
+          </div>
+
+          <div class="cam-manage-block" @click.stop="handleSetting(item)">
+            <i class="el-icon-setting mobile-setting-icon" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <el-empty v-if="dataList.length === 0 && !loading" description="暂无绑定设备" :image-size="120" />
+
+    <div class="fab-add-btn" @click="onAddDevice">
+      <i class="el-icon-plus" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { getCamList } from '@/api/disk'
+
+export default {
+  name: 'CamListMobile',
+  data() {
+    return {
+      dataList: [],
+      loading: false
+    }
+  },
+  computed: {
+    onlineCount() {
+      return this.dataList.filter(i => i.state).length
+    }
+  },
+  created() {
+    this.getData()
+  },
+  methods: {
+    getData() {
+      this.loading = true
+      getCamList().then(resp => {
+        if (resp.code === 0) {
+          this.dataList = resp.data
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    handlePlay(item) {
+      if (!item.state) {
+        this.$message.warning('设备已离线,无法查看实时画面')
+        return
+      }
+      this.$router.push({
+        path: '/disk/cam/detail',
+        query: { camId: item.camId }
+      })
+    },
+    handleSetting(item) {
+      this.$message.info(`打开设备 [${item.camName}] 的设置`)
+      // 实际开发跳转至设置路由或打开 Dialog
+    },
+    onAddDevice() {
+      this.$message.info('去往添加设备页面')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mobile-cam-container {
+  padding: 12px 16px 80px 16px; /* 为底部导航栏和悬浮按钮预留空间 */
+  background: #f8f9fa;
+  min-height: calc(100vh - 100px);
+}
+
+/* 顶部轻量头 */
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.page-title {
+  font-size: 20px;
+  font-weight: bold;
+  color: #1a1a1a;
+  margin: 0;
+}
+
+.online-status-text {
+  font-size: 12px;
+  color: #7d7e80;
+  margin-left: 8px;
+}
+
+.count-highlight {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.add-icon-btn {
+  font-size: 24px;
+  color: #1a73e8;
+  padding: 4px;
+}
+
+/* 移动端专用单列流布局卡片 */
+.cam-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.mobile-cam-card {
+  background: #ffffff;
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
+  border: 1px solid rgba(0, 0, 0, 0.02);
+  -webkit-tap-highlight-color: transparent; /* 清除点击高亮 */
+}
+
+/* 触发点击反馈效果 */
+.mobile-cam-card:active {
+  background-color: #f5f5f5;
+  transform: scale(0.99);
+  transition: all 0.1s ease;
+}
+
+/* 16:9 标准比例视频容器 */
+.cam-preview-wrapper {
+  position: relative;
+  width: 100%;
+  aspect-ratio: 16 / 9; /* 完美符合现代摄像头画面 */
+  background: linear-gradient(135deg, #2c3e50 0%, #111 100%);
+}
+
+.cam-placeholder {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.cam-placeholder i {
+  font-size: 40px;
+  color: rgba(255, 255, 255, 0.15);
+}
+
+/* 常驻轻量中心播放徽标 */
+.play-btn-mask {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: rgba(0, 0, 0, 0.1);
+}
+.play-icon {
+  font-size: 36px;
+  color: rgba(255, 255, 255, 0.8);
+  text-shadow: 0 2px 4px rgba(0,0,0,0.3);
+}
+
+/* 状态指示器样式轻量化 */
+.status-indicator {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  padding: 2px 8px;
+  background: rgba(0, 0, 0, 0.55);
+  backdrop-filter: blur(4px);
+  border-radius: 12px;
+  color: #fff;
+  font-size: 10px;
+  display: flex;
+  align-items: center;
+}
+
+.dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  margin-right: 4px;
+  background: #f56c6c;
+}
+
+.is-online .dot {
+  background: #67c23a;
+  box-shadow: 0 0 6px #67c23a;
+}
+
+/* 信息控制栏区 */
+.cam-content {
+  padding: 12px 14px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.cam-name {
+  font-size: 15px;
+  font-weight: bold;
+  color: #333333;
+  margin: 0 0 2px 0;
+}
+
+.cam-id {
+  font-size: 11px;
+  color: #999999;
+  margin: 0;
+}
+
+/* 移动端加大点击半径的控制块 */
+.cam-manage-block {
+  padding: 10px; /* 增加手指触控面积 */
+  margin-right: -4px;
+}
+
+.mobile-setting-icon {
+  font-size: 20px;
+  color: #606266;
+}
+
+/* 右下角悬浮动作添加按钮(FAB) */
+.fab-add-btn {
+  position: fixed;
+  bottom: 80px; /* 避开底部的 Tabbar */
+  right: 20px;
+  width: 50px;
+  height: 50px;
+  background: #1a73e8;
+  border-radius: 50%;
+  color: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 24px;
+  box-shadow: 0 4px 12px rgba(26, 115, 232, 0.4);
+  z-index: 99;
+}
+.fab-add-btn:active {
+  background: #155cb8;
+}
+</style>

+ 268 - 0
src/views/diskm/Disk.vue

@@ -0,0 +1,268 @@
+<template>
+  <div class="mobile-layout">
+    <header class="mobile-header">
+      <div class="header-left">
+        <img src="@/assets/img/logo.png" class="logo" alt="logo">
+        <span class="logo-text">Disk</span>
+      </div>
+      <div class="header-right">
+        <i class="el-icon-setting setting-icon" @click="showSettingDialog = true" />
+      </div>
+    </header>
+
+    <main class="mobile-main">
+      <div class="scroll-container">
+        <transition name="page-fade" mode="out-in">
+          <router-view :key="$route.fullPath" />
+        </transition>
+      </div>
+    </main>
+
+    <footer class="mobile-tabbar">
+      <div
+        v-for="item in menuItems"
+        :key="item.path"
+        :class="['tabbar-item', { 'is-active': isTabActive(item.path) }]"
+        @click="handleTabClick(item.path)"
+      >
+        <i :class="item.icon" class="tabbar-icon"></i>
+        <span class="tabbar-text">{{ item.title }}</span>
+      </div>
+    </footer>
+
+    <el-dialog
+      title="系统设置"
+      :visible.sync="showSettingDialog"
+      width="100%"
+      fullscreen
+      append-to-body
+      custom-class="mobile-setting-dialog"
+    >
+      <div class="setting-content">
+        <el-form label-position="left" label-width="100px">
+          <el-form-item label="活动通知">
+            <el-switch
+              v-model="settingForm.enableNotify"
+              active-text="开启"
+              inactive-text="关闭"
+              @change="handleNotifyChange"
+            />
+          </el-form-item>
+          <el-form-item label="外观设置">
+            <el-radio-group v-model="settingForm.theme" size="mini">
+              <el-radio-button label="light">浅色</el-radio-button>
+              <el-radio-button label="dark">深色</el-radio-button>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="默认视图">
+            <el-select v-model="settingForm.defaultView" placeholder="请选择" style="width: 100%">
+              <el-option label="列表视图" value="list" />
+              <el-option label="网格视图" value="grid" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="语言">
+            <el-select v-model="settingForm.lang" style="width: 100%">
+              <el-option label="简体中文" value="zh" />
+              <el-option label="English" value="en" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="存储空间" class="quota-form-item">
+            <div class="quota-text">已用 1.2GB / 6TB</div>
+            <el-progress :percentage="2" :show-text="false" :stroke-width="4" />
+          </el-form-item>
+        </el-form>
+
+        <div class="logout-box">
+          <el-button type="danger" plain style="width: 100%" @click="goToLogout">退出登录</el-button>
+        </div>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="showSettingDialog = false" style="width: 45%">取 消</el-button>
+        <el-button type="primary" @click="saveSettings" style="width: 45%">保 存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { userMixin } from 'assets/js/mixin'
+import { getAuthedUser } from '@/utils/auth'
+import { submitActivity } from '@/api/disk'
+
+export default {
+  name: 'DiskMobile',
+  mixins: [userMixin],
+  data() {
+    return {
+      user: null,
+      showSettingDialog: false,
+      settingForm: {
+        enableNotify: false,
+        theme: 'light',
+        defaultView: 'list',
+        lang: 'zh'
+      },
+      menuItems: [
+        { path: '/disk/file', title: '文件', icon: 'el-icon-folder' },
+        { path: '/disk/photo', title: '相册', icon: 'el-icon-picture-outline' },
+        { path: '/disk/cam', title: '监控', icon: 'el-icon-video-camera' },
+        { path: '/disk/share', title: '分享', icon: 'el-icon-share' },
+        { path: '/disk/me', title: '我的', icon: 'el-icon-user' }
+      ]
+    }
+  },
+  created() {
+    this.user = getAuthedUser()
+  },
+  methods: {
+    handleTabClick(path) {
+      if (this.$route.path !== path) {
+        this.$router.push(path)
+      }
+    },
+    // 精准匹配当前路由高亮,防止多级子路径导致高亮失效
+    isTabActive(path) {
+      return this.$route.path.startsWith(path)
+    },
+    handleNotifyChange(val) {
+      submitActivity({ enabled: val }).then(resp => {
+        if(resp && resp.msg) this.$message.info(resp.msg)
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    saveSettings() {
+      this.$message.success('设置已保存')
+      this.showSettingDialog = false
+    },
+    goToLogout() {
+      this.$message.warning('正在退出...')
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* 核心移动端容器 */
+.mobile-layout {
+  display: flex;
+  flex-direction: column;
+  /* 解决移动端浏览器 100vh 包含工具栏导致底部被遮挡的问题 */
+  height: 100vh;
+  height: 100dvh; /* 现代浏览器动态视口高度支持 */
+  width: 100vw;
+  overflow: hidden;
+  background-color: #f6f8fa;
+  position: relative;
+}
+
+/* 顶部 Header */
+.mobile-header {
+  height: 48px;
+  background-color: #fff;
+  border-bottom: 1px solid #edf2f7;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 14px;
+  flex-shrink: 0; /* 坚决不被挤压 */
+  z-index: 10;
+}
+
+.header-left { display: flex; align-items: center; }
+.logo { width: 26px; height: 26px; border-radius: 6px; }
+.logo-text { margin-left: 8px; font-size: 16px; font-weight: 800; color: #0f172a; }
+.setting-icon { font-size: 20px; color: #64748b; padding: 4px; }
+
+/* 核心滚动解耦控制区 */
+.mobile-main {
+  flex: 1; /* 自动吃满剩下的中间高度 */
+  min-height: 0; /* 极关键:防止子层弹性内容把外层撑开 */
+  display: flex;
+  flex-direction: column;
+  position: relative;
+}
+
+/* 子页面的独立滚动容器:将滚动条留给各页面内部,而不是大框架 */
+.scroll-container {
+  flex: 1;
+  overflow-y: auto;
+  overflow-x: hidden;
+  -webkit-overflow-scrolling: touch; /* iOS 弹性流畅回弹 */
+}
+
+/* 底部标签栏 Tabbar */
+.mobile-tabbar {
+  background-color: #fff;
+  border-top: 1px solid #edf2f7;
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  flex-shrink: 0; /* 坚决不缩水 */
+  box-sizing: border-box;
+
+  /* 核心防挤压计算高度:基础高度 52px + 苹果底栏安全区域 */
+  height: calc(52px + env(safe-area-inset-bottom));
+  padding-bottom: env(safe-area-inset-bottom);
+  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.02);
+}
+
+.tabbar-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #64748b;
+  flex: 1;
+  height: 52px; /* 固定触控高度 */
+  cursor: pointer;
+  -webkit-tap-highlight-color: transparent;
+}
+
+.tabbar-icon { font-size: 20px; margin-bottom: 2px; }
+.tabbar-text { font-size: 10px; font-weight: 500; }
+.tabbar-item.is-active { color: #1a73e8; font-weight: 700; }
+
+/* 移动端全屏弹窗优化 */
+::v-deep .mobile-setting-dialog {
+  display: flex;
+  flex-direction: column;
+  margin: 0 !important;
+  height: 100% !important;
+}
+
+::v-deep .mobile-setting-dialog .el-dialog__header {
+  padding: 16px;
+  border-bottom: 1px solid #edf2f7;
+  flex-shrink: 0;
+}
+
+::v-deep .mobile-setting-dialog .el-dialog__body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+  -webkit-overflow-scrolling: touch;
+}
+
+.setting-content .el-form-item {
+  margin-bottom: 16px;
+  border-bottom: 1px solid #f1f5f9;
+  padding-bottom: 12px;
+}
+
+.quota-form-item { margin-top: 24px; border-bottom: none !important; }
+.quota-text { font-size: 12px; color: #64748b; margin-bottom: 6px; }
+.logout-box { margin-top: 32px; }
+
+::v-deep .mobile-setting-dialog .el-dialog__footer {
+  border-top: 1px solid #edf2f7;
+  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
+  flex-shrink: 0;
+  display: flex;
+  justify-content: space-between;
+}
+
+/* 动效过渡 */
+.page-fade-enter-active, .page-fade-leave-active { transition: opacity 0.12s ease; }
+.page-fade-enter, .page-fade-leave-to { opacity: 0; }
+</style>

+ 462 - 0
src/views/diskm/DiskFile.vue

@@ -0,0 +1,462 @@
+<template>
+  <div class="mobile-file-center">
+    <div class="mobile-top-bar">
+      <div class="search-row">
+        <el-input
+          v-model="inputData"
+          placeholder="搜索我的文件"
+          prefix-icon="el-icon-search"
+          size="small"
+          class="mobile-search-input"
+          clearable
+          @keyup.enter.native="onSearchFile"
+        />
+        <i class="el-icon-folder-add quick-action-icon" @click="onCreateFolder" />
+        <i class="el-icon-upload2 quick-action-icon" @click="onClickUpload" />
+      </div>
+
+      <div class="mobile-breadcrumb-wrapper">
+        <div class="breadcrumb-inner-scroll">
+          <span
+            v-for="(item, index) in pathList"
+            :key="index"
+            class="crumb-item"
+            @click="jumpToPath(item.path)"
+          >
+            {{ item.name }}
+            <i v-if="index < pathList.length - 1" class="el-icon-arrow-right arrow" />
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <div
+      v-infinite-scroll="loadMore"
+      class="mobile-list-wrapper"
+      :infinite-scroll-disabled="infiniteDisabled"
+      infinite-scroll-distance="30"
+    >
+      <div class="file-list">
+        <div
+          v-for="item in dataList"
+          :key="item.fileId"
+          class="mobile-file-row"
+          :class="{ 'is-checked': isSelected(item) }"
+          @click="onRowClick(item)"
+        >
+          <div class="row-left-checkbox" @click.stop="toggleItemSelection(item)">
+            <div class="custom-checkbox" :class="{ 'checked': isSelected(item) }">
+              <i v-if="isSelected(item)" class="el-icon-check" />
+            </div>
+          </div>
+
+          <div class="row-middle-content">
+            <div class="file-icon-box">
+              <i :class="getFileIcon(item.fileType)" :style="getIconStyle(item.fileType)" />
+            </div>
+            <div class="file-info-text">
+              <span class="filename-title">{{ item.filename }}</span>
+              <div class="file-meta-sub">
+                <span class="m-size">{{ item.size || '-' }}</span>
+                <span class="m-dot">•</span>
+                <span class="m-time">{{ formatSimpleTime(item.updateTime) }}</span>
+              </div>
+            </div>
+          </div>
+
+          <div class="row-right-more" @click.stop="showSingleActionMenu(item)">
+            <i class="el-icon-more" />
+          </div>
+        </div>
+      </div>
+
+      <div class="load-status">
+        <p v-if="loading"><i class="el-icon-loading" /> 载入中...</p>
+        <p v-if="noMore && dataList.length > 0">已加载全部文件</p>
+        <el-empty v-if="dataList.length === 0 && !loading" :image-size="80" description="空空如也" />
+      </div>
+    </div>
+
+    <transition name="slide-up">
+      <div v-if="selectedTable.length > 0" class="mobile-batch-bar">
+        <span class="batch-count">已选 {{ selectedTable.length }} 项</span>
+        <div class="batch-buttons">
+          <div class="b-btn" @click="addToAlbum"><i class="el-icon-collection" /><span>存合集</span></div>
+          <div class="b-btn" @click="moveToFolder"><i class="el-icon-rank" /><span>移动</span></div>
+          <div class="b-btn text-danger" @click="onDeleteFile"><i class="el-icon-delete" /><span>删除</span></div>
+          <div class="b-btn text-muted" @click="clearSelections"><span>取消</span></div>
+        </div>
+      </div>
+    </transition>
+
+    <el-dialog
+      :visible.sync="showPreviewDialog"
+      :before-close="handlePreviewClose"
+      width="100%"
+      fullscreen
+      custom-class="mobile-fullscreen-preview"
+      append-to-body
+    >
+      <div slot="title" class="mobile-preview-header">
+        <i class="el-icon-arrow-left back-icon" @click="handlePreviewClose" />
+        <span class="p-title">{{ fileDetail ? fileDetail.filename : '预览' }}</span>
+      </div>
+
+      <div v-if="fileDetail" class="mobile-preview-body">
+        <template v-if="fileType === 1001">
+          <el-image :src="fileDetail.url" fit="contain" class="m-img" :preview-src-list="[fileDetail.url]" />
+        </template>
+        <template v-else-if="fileType === 1002">
+          <video ref="videoPlayer" :src="fileDetail.url" class="m-video" controls autoplay playsinline />
+        </template>
+        <template v-else-if="fileType === 1003">
+          <div class="m-audio-box">
+            <i class="el-icon-headset" />
+            <audio :src="fileDetail.url" controls autoplay style="width:90%" />
+          </div>
+        </template>
+        <template v-else-if="fileType === 1006">
+          <iframe :src="getPdfUrl(fileDetail.url)" width="100%" height="100%" frameborder="0" />
+        </template>
+        <template v-else>
+          <el-empty description="无法直接在手机查看">
+            <el-button type="primary" size="small" round @click="handleDownload(fileDetail)">下载到本地</el-button>
+          </el-empty>
+        </template>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="新建文件夹" :visible.sync="showCreateFolderDialog" width="85%" custom-class="mobile-center-dialog" append-to-body>
+      <el-form ref="folderForm" :model="createFolderForm" @submit.native.prevent="createFolder">
+        <el-input v-model="createFolderForm.folderName" placeholder="文件夹名称" clearable />
+      </el-form>
+      <div slot="footer" class="m-dialog-footer">
+        <el-button size="small" @click="showCreateFolderDialog = false">取消</el-button>
+        <el-button size="small" type="primary" :loading="loading" @click="createFolder">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="上传文件" :visible.sync="showUploadDialog" width="90%" custom-class="mobile-center-dialog" append-to-body>
+      <uploader v-if="options" :options="options" :auto-start="true" @file-added="onFileAdded" @file-success="onFileSuccess" @complete="onUploadComplete">
+        <uploader-drop class="m-upload-drop">
+          <i class="el-icon-cloudy" />
+          <p><uploader-btn :attrs="attrs" class="m-select-link">点击选择手机文件上传</uploader-btn></p>
+        </uploader-drop>
+        <uploader-list class="m-upload-list" />
+      </uploader>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { addFile, createFolder, deleteFile, getDiskChannelInfo, getDiskFile, getFileDetail } from '@/api/disk'
+import { hashFile } from '@/utils/functions'
+
+export default {
+  name: 'DiskFileMobile',
+  data() {
+    return {
+      dataList: [],
+      selectedTable: [], // 存储选中的行对象
+      pathList: [],
+      inputData: '',
+      showPreviewDialog: false,
+      fileDetail: null,
+      fileType: 0,
+      currentPid: '0',
+      loading: false,
+      noMore: false,
+      queryForm: { pn: 1, path: '/', fileType: null },
+      createFolderForm: { pid: '0', folderName: '' },
+      showCreateFolderDialog: false,
+      showUploadDialog: false,
+      options: null,
+      attrs: { accept: '*' },
+      uploadForm: { channelCode: null, pid: null, uploadId: null }
+    }
+  },
+  computed: {
+    infiniteDisabled() { return this.loading || this.noMore }
+  },
+  created() {
+    if (this.$route.query.path) this.queryForm.path = this.$route.query.path
+  },
+  methods: {
+    loadMore() {
+      if (this.infiniteDisabled) return
+      this.loading = true
+      getDiskFile(this.queryForm).then(resp => {
+        if (resp.code === 0) {
+          const { list, hasNext } = resp.data.pageList
+          this.dataList = [...this.dataList, ...list]
+          if (this.queryForm.pn === 1) this.updatePathInfo(resp.data)
+          if (!hasNext) this.noMore = true; else this.queryForm.pn++
+        } else {
+          this.noMore = true
+          this.$message.error(resp.msg)
+        }
+      }).finally(() => { this.loading = false })
+    },
+    updatePathInfo(respData) {
+      this.currentPid = respData.currentPid
+      const { namePathList } = respData
+      this.pathList = []
+      if (!namePathList || namePathList.length === 0) {
+        this.pathList.push({ path: '/', name: '全部文件' })
+      } else {
+        namePathList.forEach(item => {
+          this.pathList.push({ path: item.path, name: item.filename })
+        })
+      }
+    },
+    changeDirectory(newPath) {
+      this.queryForm.path = newPath
+      this.queryForm.pn = 1
+      this.dataList = []
+      this.noMore = false
+      this.loadMore()
+    },
+    jumpToPath(pathStr) {
+      this.$router.push({ query: { path: pathStr }})
+      this.changeDirectory(pathStr)
+    },
+    onRowClick(item) {
+      if (this.selectedTable.length > 0) {
+        this.toggleItemSelection(item)
+        return
+      }
+      this.fileType = item.fileType
+      if (this.fileType === 1000) {
+        const newPath = this.queryForm.path === '/' ? `/${item.filename}` : `${this.queryForm.path}/${item.filename}`
+        this.jumpToPath(newPath)
+      } else {
+        getFileDetail(item.fileId).then(resp => {
+          if (resp.code === 0) {
+            this.fileDetail = resp.data
+            this.showPreviewDialog = true
+          }
+        })
+      }
+    },
+    isSelected(item) {
+      return this.selectedTable.some(i => i.fileId === item.fileId)
+    },
+    toggleItemSelection(item) {
+      const idx = this.selectedTable.findIndex(i => i.fileId === item.fileId)
+      if (idx > -1) this.selectedTable.splice(idx, 1); else this.selectedTable.push(item)
+    },
+    clearSelections() { this.selectedTable = [] },
+    formatSimpleTime(timeStr) {
+      if (!timeStr) return ''
+      return timeStr.split(' ')[0].substring(5) // 精简只显示 "MM-DD"
+    },
+    getFileIcon(type) {
+      const iconMap = { 1000: 'el-icon-folder', 1001: 'el-icon-picture', 1002: 'el-icon-video-camera', 1003: 'el-icon-headset', 1004: 'el-icon-document' }
+      return iconMap[type] || 'el-icon-files'
+    },
+    getIconStyle(type) {
+      const colorMap = { 1000: '#f59e0b', 1001: '#10b981', 1002: '#ef4444', 1003: '#8b5cf6' }
+      return { color: colorMap[type] || '#64748b', fontSize: '24px' }
+    },
+    getPdfUrl(url) { return '/pdfjs/web/viewer.html?file=' + encodeURIComponent(url) },
+    handlePreviewClose() {
+      if (this.$refs.videoPlayer) this.$refs.videoPlayer.pause()
+      this.showPreviewDialog = false
+      this.fileDetail = null
+    },
+    onCreateFolder() {
+      this.createFolderForm.folderName = ''
+      this.showCreateFolderDialog = true
+    },
+    createFolder() {
+      if(!this.createFolderForm.folderName.trim()) return
+      this.createFolderForm.pid = this.currentPid
+      this.loading = true
+      createFolder(this.createFolderForm).then(resp => {
+        if (resp.code === 0) {
+          this.showCreateFolderDialog = false
+          this.changeDirectory(this.queryForm.path)
+        }
+      }).finally(() => { this.loading = false })
+    },
+    onDeleteFile() {
+      this.$confirm(`确定删除选中的 ${this.selectedTable.length} 个文件吗?`, '提示', { type: 'warning', width: '80%' }).then(() => {
+        const fileIds = this.selectedTable.map(i => i.fileId)
+        deleteFile({ fileIds }).then(resp => {
+          if (resp.code === 0) {
+            this.$message.success('删除成功')
+            this.clearSelections()
+            this.changeDirectory(this.queryForm.path) // 局部重载目录,绝不触发全页刷新 router.go
+          }
+        })
+      }).catch(() => {})
+    },
+    // ***************** Simple Uploader 移动桥接 *****************
+    onClickUpload() {
+      this.uploadForm.pid = this.currentPid
+      getDiskChannelInfo().then(resp => {
+        if (resp.code === 0) {
+          this.uploadForm.channelCode = resp.data.channelCode
+          this.options = {
+            target: resp.data.ossUrl,
+            chunkSize: 1024 * 1024 * 5, // 手机端改为 5M 切片更稳定
+            maxChunkRetries: 2,
+            fileParameterName: 'file',
+            testChunks: true,
+            checkChunkUploadedByResponse: (chunk, msg) => {
+              const res = JSON.parse(msg).data
+              return res.skipUpload || (res.uploaded || []).indexOf(chunk.offset + 1) >= 0
+            },
+            query: () => ({ channelCode: resp.data.channelCode, multiparts: '' }),
+            headers: { Authorization: 'Bearer ' + resp.data.token }
+          }
+          this.showUploadDialog = true
+        }
+      })
+    },
+    onFileAdded(file) {
+      file.pause()
+      hashFile(file.file).then(res => { file.uniqueIdentifier = res.sha256sum; file.resume() })
+    },
+    onFileSuccess(rootFile, file, response) {
+      const resp = JSON.parse(response)
+      if (resp.code === 0) {
+        this.uploadForm.uploadId = resp.data.uploadId
+        addFile(this.uploadForm)
+      }
+    },
+    onUploadComplete() {
+      this.$message.success('上传处理完成')
+      setTimeout(() => { this.showUploadDialog = false; this.changeDirectory(this.queryForm.path) }, 800)
+    },
+    handleDownload(file) { if(file && file.url) window.open(file.url, '_blank') },
+    onSearchFile() { this.$message.info('搜索触发') },
+    addToAlbum() {}, moveToFolder() {} // 后续逻辑类似
+  }
+}
+</script>
+
+<style scoped>
+.mobile-file-center {
+  background-color: #f6f8fa;
+  min-height: 100vh;
+}
+
+/* 顶部固定栏 */
+.mobile-top-bar {
+  position: sticky;
+  top: 0;
+  background: #fff;
+  padding: 10px 14px 4px 14px;
+  z-index: 90;
+  border-bottom: 1px solid #edf2f7;
+}
+.search-row { display: flex; align-items: center; gap: 12px; }
+.mobile-search-input { flex: 1; }
+::v-deep .mobile-search-input .el-input__inner { border-radius: 18px; background: #f1f5f9; border: none; }
+
+.quick-action-icon { font-size: 22px; color: #1a73e8; padding: 4px; }
+
+/* 核心面包屑优化:打通横向单行滑动 */
+.mobile-breadcrumb-wrapper {
+  margin-top: 8px;
+  overflow-x: auto;
+  white-space: nowrap;
+  padding-bottom: 4px;
+  -webkit-overflow-scrolling: touch;
+}
+.mobile-breadcrumb-wrapper::-webkit-scrollbar { display: none; } /* 藏起滚动条 */
+
+.breadcrumb-inner-scroll { display: flex; align-items: center; gap: 6px; }
+.crumb-item { font-size: 13px; color: #475569; display: flex; align-items: center; }
+.crumb-item:last-child { color: #1a73e8; font-weight: bold; }
+.crumb-item .arrow { font-size: 11px; margin-left: 6px; color: #94a3b8; }
+
+/* 触屏大列表流布局 */
+.mobile-list-wrapper { padding: 8px 12px 100px 12px; }
+.file-list { display: flex; flex-direction: column; gap: 6px; }
+
+.mobile-file-row {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  padding: 12px 10px;
+  border-radius: 12px;
+  box-shadow: 0 2px 6px rgba(0,0,0,0.01);
+  transition: background 0.15s;
+  -webkit-tap-highlight-color: transparent;
+}
+.mobile-file-row:active { background: #f1f5f9; }
+.mobile-file-row.is-checked { background: #f0f7ff; border: 1px solid #bfdbfe; }
+
+/* 独立设计的加大选框热区 */
+.row-left-checkbox { padding: 8px 10px 8px 4px; }
+.custom-checkbox {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  border: 1.5px solid #cbd5e1;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.custom-checkbox.checked { background: #1a73e8; border-color: #1a73e8; }
+.custom-checkbox i { color: #fff; font-size: 11px; font-weight: bold; }
+
+.row-middle-content { flex: 1; display: flex; align-items: center; gap: 12px; overflow: hidden; }
+.file-info-text { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
+.filename-title { font-size: 14px; font-weight: 600; color: #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.file-meta-sub { font-size: 11px; color: #94a3b8; margin-top: 2px; display: flex; align-items: center; gap: 4px; }
+
+.row-right-more { padding: 8px 6px; color: #94a3b8; font-size: 16px; }
+
+/* 批量底部面板吸底 */
+.mobile-batch-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  background: #1e293b;
+  color: #fff;
+  padding: 12px 16px env(safe-area-inset-bottom) 16px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  z-index: 100;
+  border-top-left-radius: 16px;
+  border-top-right-radius: 16px;
+}
+.batch-count { font-size: 12px; color: #94a3b8; text-align: center; }
+.batch-buttons { display: flex; justify-content: space-around; align-items: center; }
+.b-btn { display: flex; flex-direction: column; align-items: center; font-size: 11px; gap: 4px; color: #f1f5f9; cursor: pointer; }
+.b-btn i { font-size: 20px; }
+.text-danger { color: #f87171; }
+.text-muted { color: #94a3b8; }
+
+/* 全屏预览层重组 */
+::v-deep .mobile-fullscreen-preview { background: #000 !important; margin:0!important; }
+::v-deep .mobile-fullscreen-preview .el-dialog__header { padding: 0; }
+::v-deep .mobile-fullscreen-preview .el-dialog__body { padding: 0; height: calc(100vh - 50px); background: #000; }
+
+.mobile-preview-header { height: 50px; background: #111; display: flex; align-items: center; padding: 0 16px; color: #fff; gap: 14px; }
+.mobile-preview-header .back-icon { font-size: 20px; }
+.mobile-preview-header .p-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.mobile-preview-body { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; }
+.m-img { max-width: 100%; max-height: 90vh; }
+.m-video { width: 100%; max-height: 80vh; }
+.m-audio-box { text-align: center; color: #1a73e8; font-size: 48px; width: 100%; }
+
+/* 弹窗小改 */
+::v-deep .mobile-center-dialog { border-radius: 14px !important; }
+.m-upload-drop { border: 2px dashed #cbd5e1; padding: 30px 10px; text-align: center; background: #f8fafc; border-radius: 10px; }
+.m-select-link { color: #1a73e8; font-weight: bold; text-decoration: underline; background: none; border: none; }
+.m-upload-list { max-height: 180px; overflow-y: auto; margin-top: 10px; }
+.m-dialog-footer { display: flex; justify-content: flex-end; gap: 10px; }
+.load-status { text-align: center; padding: 16px 0; font-size: 12px; color: #94a3b8; }
+
+.slide-up-enter-active, .slide-up-leave-active { transition: transform 0.25s ease; }
+.slide-up-enter, .slide-up-leave-to { transform: translateY(100%); }
+</style>

+ 268 - 0
src/views/diskm/DiskMe.vue

@@ -0,0 +1,268 @@
+<template>
+  <div class="mobile-me-center">
+    <div class="mobile-profile-hero">
+      <div class="profile-inner">
+        <el-avatar :size="64" :src="user ? user.avatarUrl : ''" class="m-avatar" />
+        <div class="m-user-meta">
+          <div class="m-username-row">
+            <span class="m-name">{{ user ? user.name : '未登录用户' }}</span>
+            <span class="m-vip-badge">黄金VIP</span>
+          </div>
+          <span class="m-uid">ID: {{ user ? user.userId : '-' }}</span>
+        </div>
+      </div>
+      <el-button type="text" size="mini" class="m-edit-profile-btn" icon="el-icon-edit">修改资料</el-button>
+    </div>
+
+    <div class="mobile-storage-card">
+      <div class="s-header">
+        <div class="s-title-box">
+          <i class="el-icon-cloudy" />
+          <span class="s-title">云存储空间</span>
+        </div>
+        <span class="s-action" @click="$message.info('前往扩容')">扩容套餐</span>
+      </div>
+
+      <div class="s-usage-display">
+        <span class="num-used">{{ usedSpaceGB }} GB</span>
+        <span class="num-total">/ {{ totalSpaceGB }} GB</span>
+      </div>
+
+      <div class="m-multi-progress">
+        <div class="m-seg video" :style="{ width: videoPercent + '%' }" />
+        <div class="m-seg photo" :style="{ width: photoPercent + '%' }" />
+        <div class="m-seg other" :style="{ width: otherPercent + '%' }" />
+      </div>
+
+      <div class="m-legend-grid">
+        <div class="l-item"><i class="dot-indicator is-video" />视频 ({{ videoPercent }}%)</div>
+        <div class="l-item"><i class="dot-indicator is-photo" />照片 ({{ photoPercent }}%)</div>
+        <div class="l-item"><i class="dot-indicator is-other" />其他 ({{ otherPercent }}%)</div>
+      </div>
+    </div>
+
+    <div class="mobile-menu-group">
+      <div
+        v-for="item in actionList"
+        :key="item.title"
+        class="mobile-menu-item"
+        @click="handleAction(item)"
+      >
+        <div class="item-left-content">
+          <div class="item-icon-wrapper" :style="{ background: item.color + '15' }">
+            <i :class="item.icon" :style="{ color: item.color }" />
+          </div>
+          <div class="item-text-block">
+            <span class="item-title">{{ item.title }}</span>
+            <span class="item-desc">{{ item.desc }}</span>
+          </div>
+        </div>
+        <i class="el-icon-arrow-right item-right-arrow" />
+      </div>
+    </div>
+
+    <el-dialog
+      title="账号设置"
+      :visible.sync="showSettings"
+      width="90%"
+      custom-class="mobile-popup-settings"
+      append-to-body
+    >
+      <div class="mobile-form-list">
+        <div class="form-row">
+          <span class="f-label">离线下载通知</span>
+          <el-switch v-model="switchEnabled" @change="onSwitchChange" />
+        </div>
+        <el-divider class="m-divider" />
+        <div class="form-row">
+          <span class="f-label">隐私保护模式</span>
+          <el-switch v-model="privacyEnabled" />
+        </div>
+        <el-divider class="m-divider" />
+        <div class="form-row">
+          <span class="f-label">客户端版本</span>
+          <span class="v-tag">v1.2.0 (Stable)</span>
+        </div>
+      </div>
+      <div slot="footer" class="dialog-footer-full">
+        <el-button type="primary" round style="width: 100%" @click="showSettings = false">关闭窗口</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getAuthedUser } from '@/utils/auth'
+
+export default {
+  name: 'DiskMeMobile',
+  data() {
+    return {
+      user: null,
+      usedSpaceGB: 120,
+      totalSpaceGB: 6144,
+      videoPercent: 45,
+      photoPercent: 15,
+      otherPercent: 10,
+      showSettings: false,
+      switchEnabled: false,
+      privacyEnabled: true,
+      actionList: [
+        { title: '离线下载', desc: '支持磁力链接与 BT 种子', icon: 'el-icon-link', color: '#1989fa' },
+        { title: '回收站', desc: '已删除文件保留 30 天', icon: 'el-icon-delete', color: '#f56c6c' },
+        { title: '传输中心', desc: '查看当前上传与下载任务', icon: 'el-icon-sort', color: '#67c23a' },
+        { title: '个人设置', desc: '修改头像、密码与偏好', icon: 'el-icon-setting', color: '#64748b', action: 'settings' },
+        { title: '帮助反馈', desc: '遇到问题?联系客服', icon: 'el-icon-chat-dot-round', color: '#e6a23c' }
+      ]
+    }
+  },
+  created() {
+    this.user = getAuthedUser()
+  },
+  methods: {
+    handleAction(item) {
+      if (item.action === 'settings') {
+        this.showSettings = true
+      } else {
+        this.$message.info(`导航至: ${item.title}`)
+      }
+    },
+    onSwitchChange(val) {
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mobile-me-center {
+  background-color: #f6f8fa;
+  min-height: 100vh;
+  padding: 12px 14px 40px 14px;
+  box-sizing: border-box;
+}
+
+/* 沉浸式顶部个人信息名片 */
+.mobile-profile-hero {
+  position: relative;
+  background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
+  padding: 20px 16px;
+  border-radius: 16px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  box-shadow: 0 4px 14px rgba(0,0,0,0.02);
+}
+
+.profile-inner { display: flex; align-items: center; gap: 14px; }
+.m-avatar { border: 2px solid #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
+
+.m-user-meta { display: flex; flex-direction: column; gap: 4px; }
+.m-username-row { display: flex; align-items: center; gap: 8px; }
+.m-name { font-size: 18px; font-weight: bold; color: #1e293b; }
+
+.m-vip-badge {
+  background: linear-gradient(90deg, #f59e0b, #d97706);
+  color: #fff;
+  font-size: 10px;
+  padding: 1px 6px;
+  border-radius: 10px;
+  font-weight: bold;
+}
+.m-uid { font-size: 12px; color: #64748b; }
+.m-edit-profile-btn { font-size: 12px; padding: 4px 0; }
+
+/* 存储卡片全面松绑 */
+.mobile-storage-card {
+  background: #fff;
+  border-radius: 16px;
+  margin-top: 12px;
+  padding: 16px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.01);
+}
+
+.s-header { display: flex; justify-content: space-between; align-items: center; }
+.s-title-box { display: flex; align-items: center; gap: 6px; font-weight: bold; color: #334155; font-size: 14px; }
+.s-title-box i { color: #1a73e8; font-size: 16px; }
+.s-action { font-size: 12px; color: #1a73e8; font-weight: 600; }
+
+.s-usage-display { margin: 14px 0 10px 0; }
+.num-used { font-size: 26px; font-weight: 800; color: #1a73e8; }
+.num-total { font-size: 14px; color: #94a3b8; margin-left: 4px; }
+
+.m-multi-progress {
+  height: 8px;
+  background: #f1f5f9;
+  border-radius: 4px;
+  display: flex;
+  overflow: hidden;
+}
+.m-seg { height: 100%; }
+.m-seg.video { background: #1989fa; }
+.m-seg.photo { background: #67c23a; }
+.m-seg.other { background: #ff976a; }
+
+.m-legend-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 8px;
+  margin-top: 14px;
+}
+.l-item { display: flex; align-items: center; font-size: 11px; color: #64748b; gap: 4px; }
+.dot-indicator { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
+.dot-indicator.is-video { background: #1989fa; }
+.dot-indicator.is-photo { background: #67c23a; }
+.dot-indicator.is-other { background: #ff976a; }
+
+/* 微信式一列菜单项流 */
+.mobile-menu-group {
+  margin-top: 14px;
+  background: #fff;
+  border-radius: 16px;
+  padding: 4px 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.mobile-menu-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 14px 16px;
+  transition: background 0.1s;
+  -webkit-tap-highlight-color: transparent;
+}
+.mobile-menu-item:not(:last-child) { border-bottom: 1px solid #f1f5f9; }
+.mobile-menu-item:active { background: #f8fafc; }
+
+.item-left-content { display: flex; align-items: center; gap: 14px; flex: 1; overflow: hidden; }
+
+.item-icon-wrapper {
+  width: 36px;
+  height: 36px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 18px;
+  flex-shrink: 0;
+}
+
+.item-text-block { display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
+.item-title { font-size: 14px; font-weight: 600; color: #1e293b; }
+.item-desc { font-size: 11px; color: #94a3b8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.item-right-arrow { color: #cbd5e1; font-size: 14px; }
+
+/* 移动端设置弹窗列表重塑 */
+::v-deep .mobile-popup-settings {
+  border-radius: 16px !important;
+}
+::v-deep .mobile-popup-settings .el-dialog__body {
+  padding: 10px 20px 20px 20px;
+}
+.mobile-form-list { display: flex; flex-direction: column; }
+.form-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; }
+.f-label { font-size: 14px; font-weight: 600; color: #334155; }
+.v-tag { font-family: monospace; font-size: 13px; color: #64748b; }
+.m-divider { margin: 4px 0 !important; }
+.dialog-footer-full { padding-top: 10px; }
+</style>

+ 437 - 0
src/views/diskm/DiskPhoto.vue

@@ -0,0 +1,437 @@
+<template>
+  <div v-loading="refreshing" class="mobile-photos-container">
+    <div class="photo-header">
+      <div class="header-left">
+        <span class="title">我的相册</span>
+        <span v-if="isEditMode" class="selection-info">已选择 {{ selectedIds.length }} 项</span>
+      </div>
+      <div class="header-right">
+        <el-button
+          :type="isEditMode ? 'default' : 'primary'"
+          size="mini"
+          round
+          @click="toggleEditMode"
+        >
+          {{ isEditMode ? '取消' : '选择' }}
+        </el-button>
+      </div>
+    </div>
+
+    <div
+      v-infinite-scroll="onLoad"
+      class="scroll-wrapper"
+      :infinite-scroll-disabled="disabled"
+      infinite-scroll-distance="50"
+    >
+      <div v-for="group in groupedPhotos" :key="group.date" class="photo-group">
+        <div class="group-header">
+          <span class="date-title">{{ formatGroupDate(group.date) }}</span>
+          <div v-if="isEditMode" class="group-select-btn" @click="selectGroup(group)">
+            {{ isGroupSelected(group) ? '取消全选' : '全选' }}
+          </div>
+        </div>
+
+        <div class="photo-grid">
+          <div
+            v-for="item in group.children"
+            :key="item.fileId"
+            class="photo-item"
+            @click="onItemClick(item)"
+          >
+            <el-image
+              class="image-content"
+              :src="item.url"
+              fit="cover"
+              lazy
+              :preview-src-list="isEditMode || item.fileType === 1002 ? [] : [item.url]"
+            />
+
+            <div v-if="item.fileType === 1002" class="video-badge">
+              <i class="el-icon-video-play" />
+              <span>{{ item.duration || '视频' }}</span>
+            </div>
+
+            <div v-if="isEditMode" class="select-mask" :class="{ 'is-active': selectedIds.includes(item.fileId) }">
+              <div class="check-icon-wrapper">
+                <i class="el-icon-check" />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="load-status">
+        <p v-if="loading" class="status-text"><i class="el-icon-loading" /> 努力加载中...</p>
+        <p v-if="finished && photoList.length > 0" class="status-text">已经看完全部照片啦</p>
+        <el-empty v-if="finished && photoList.length === 0" :image-size="80" description="暂无照片" />
+      </div>
+    </div>
+
+    <transition name="slide-up">
+      <div v-if="isEditMode" class="batch-footer-action">
+        <div class="action-btn download" :class="{ disabled: selectedIds.length === 0 }" @click="handleBatchDownload">
+          <i class="el-icon-download" />
+          <span>下载</span>
+        </div>
+        <div class="action-btn delete" :class="{ disabled: selectedIds.length === 0 }" @click="handleBatchDelete">
+          <i class="el-icon-delete" />
+          <span>删除</span>
+        </div>
+      </div>
+    </transition>
+
+    <el-dialog
+      :visible.sync="showPlayerModal"
+      width="100%"
+      fullscreen
+      custom-class="mobile-video-dialog"
+      append-to-body
+      @closed="onVideoPopupClosed"
+    >
+      <div v-if="currentVideoItem" class="video-fullscreen-wrapper">
+        <div class="video-header-bar">
+          <i class="el-icon-arrow-left close-btn" @click="showPlayerModal = false" />
+          <span class="video-title">{{ currentVideoItem.filename }}</span>
+        </div>
+        <div class="video-body">
+          <video
+            ref="videoPlayer"
+            :src="currentVideoItem.url"
+            controls
+            playsinline
+            webkit-playsinline
+            class="mobile-video-player"
+          />
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getPhotoItems } from '@/api/disk'
+
+export default {
+  name: 'DiskPhotoMobile',
+  data() {
+    return {
+      photoList: [],
+      loading: false,
+      finished: false,
+      refreshing: false,
+      isEditMode: false,
+      selectedIds: [],
+      queryParams: {
+        pn: 1
+      },
+      showPlayerModal: false,
+      currentVideoItem: null
+    }
+  },
+  computed: {
+    groupedPhotos() {
+      return this.groupDataByDate(this.photoList)
+    },
+    disabled() {
+      return this.loading || this.finished
+    }
+  },
+  methods: {
+    async onLoad() {
+      if (this.finished || this.loading) return
+      this.loading = true
+
+      try {
+        const resp = await getPhotoItems(this.queryParams)
+        if (resp.code === 0) {
+          const list = resp.data.list || []
+          this.photoList = [...this.photoList, ...list]
+
+          if (!resp.data.hasNext) {
+            this.finished = true
+          } else {
+            this.queryParams.pn++
+          }
+        } else {
+          this.finished = true
+          this.$message.error(resp.msg)
+        }
+      } catch (e) {
+        this.finished = true
+      } finally {
+        this.loading = false
+      }
+    },
+
+    groupDataByDate(flatList) {
+      const groups = {}
+      flatList.forEach(item => {
+        const date = item.updateTime.split(' ')[0]
+        if (!groups[date]) groups[date] = []
+        groups[date].push(item)
+      })
+      return Object.keys(groups).map(date => ({
+        date,
+        children: groups[date]
+      })).sort((a, b) => new Date(b.date) - new Date(a.date))
+    },
+
+    formatGroupDate(dateStr) {
+      const d = new Date(dateStr)
+      const now = new Date()
+      const today = now.toISOString().split('T')[0]
+      if (dateStr === today) return '今天'
+
+      // 昨天的简单计算
+      const yesterday = new Date(now.setDate(now.getDate() - 1)).toISOString().split('T')[0]
+      if (dateStr === yesterday) return '昨天'
+
+      return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
+    },
+
+    onItemClick(item) {
+      if (this.isEditMode) {
+        this.handleSelect(item.fileId)
+      } else {
+        if (item.fileType === 1002) {
+          this.currentVideoItem = item
+          this.showPlayerModal = true
+        }
+        // 如果是普通图片,el-image 上的 preview-src-list 会自动接管实现原生全屏缩放预览
+      }
+    },
+
+    handleSelect(id) {
+      const idx = this.selectedIds.indexOf(id)
+      if (idx > -1) {
+        this.selectedIds.splice(idx, 1)
+      } else {
+        this.selectedIds.push(id)
+      }
+    },
+
+    toggleEditMode() {
+      this.isEditMode = !this.isEditMode
+      this.selectedIds = []
+    },
+
+    selectGroup(group) {
+      const groupIds = group.children.map(i => i.fileId)
+      if (this.isGroupSelected(group)) {
+        this.selectedIds = this.selectedIds.filter(id => !groupIds.includes(id))
+      } else {
+        groupIds.forEach(id => {
+          if (!this.selectedIds.includes(id)) this.selectedIds.push(id)
+        })
+      }
+    },
+
+    isGroupSelected(group) {
+      return group.children.every(i => this.selectedIds.includes(i.fileId))
+    },
+
+    handleBatchDownload() {
+      if (this.selectedIds.length === 0) return
+      this.$message.success(`开始下载 ${this.selectedIds.length} 项`)
+    },
+
+    handleBatchDelete() {
+      if (this.selectedIds.length === 0) return
+      this.$confirm(`确定删除选中的 ${this.selectedIds.length} 张照片吗?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        customClass: 'mobile-message-box', // 需配合全局样式微调弹窗大小
+        center: true
+      }).then(() => {
+        this.$message.success('删除成功')
+        // 实际开发中在此处调用接口更新列表
+      }).catch(() => {})
+    },
+
+    onVideoPopupClosed() {
+      if (this.$refs.videoPlayer) this.$refs.videoPlayer.pause()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mobile-photos-container {
+  padding: 12px 4px 60px 4px; /* 底部预留操作栏空间 */
+  background: #f8f9fa;
+  min-height: calc(100vh - 100px);
+}
+
+/* 顶部轻量级 Title 栏 */
+.photo-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  background: #f8f9fa;
+}
+.photo-header .title { font-size: 18px; font-weight: bold; color: #1a1a1a; }
+.photo-header .selection-info { margin-left: 10px; color: #1a73e8; font-size: 13px; font-weight: bold; }
+
+/* 日期分组头 */
+.photo-group {
+  margin-bottom: 16px;
+}
+.group-header {
+  padding: 10px 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.group-header .date-title { font-size: 14px; font-weight: bold; color: #333; }
+.group-header .group-select-btn { font-size: 13px; color: #1a73e8; cursor: pointer; }
+
+/* 标准移动端相册:无缝隙 4 列矩阵 */
+.photo-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr); /* 牢牢固定 4 列 */
+  gap: 2px; /* 类似系统相册的极微小拼缝 */
+}
+
+.photo-item {
+  position: relative;
+  width: 100%;
+  aspect-ratio: 1 / 1; /* 严格正方形 */
+  overflow: hidden;
+  -webkit-tap-highlight-color: transparent;
+}
+
+.image-content {
+  width: 100%;
+  height: 100%;
+}
+
+/* 视频标记适配小格 */
+.video-badge {
+  position: absolute;
+  bottom: 4px;
+  right: 4px;
+  background: rgba(0, 0, 0, 0.5);
+  color: #fff;
+  padding: 1px 4px;
+  border-radius: 3px;
+  font-size: 10px;
+  display: flex;
+  align-items: center;
+}
+.video-badge i { margin-right: 2px; font-size: 11px; }
+
+/* 移动端多选高亮蒙层:不再是一个小圆圈,而是全屏高亮+右上角打勾 */
+.select-mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(255, 255, 255, 0.2);
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-start;
+  padding: 6px;
+  box-sizing: border-box;
+}
+
+.check-icon-wrapper {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.2);
+  border: 1px solid #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.check-icon-wrapper i { color: transparent; font-size: 12px; font-weight: bold; }
+
+/* 被选中时的多选状态 */
+.select-mask.is-active {
+  background: rgba(0, 0, 0, 0.1);
+  border: 2px solid #1a73e8; /* 蓝色外框 */
+}
+.select-mask.is-active .check-icon-wrapper {
+  background: #1a73e8;
+  border-color: #1a73e8;
+}
+.select-mask.is-active .check-icon-wrapper i {
+  color: #fff;
+}
+
+/* 底部状态 */
+.load-status { text-align: center; padding: 20px 0; }
+.status-text { font-size: 12px; color: #999; }
+
+/* 移动端底部原生操作面板 */
+.batch-footer-action {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  height: 54px;
+  background: #ffffff;
+  border-top: 1px solid #eee;
+  display: flex;
+  z-index: 100;
+  padding-bottom: env(safe-area-inset-bottom); /* 兼容全面屏 */
+}
+
+.action-btn {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #333;
+  font-size: 11px;
+}
+.action-btn i { font-size: 20px; margin-bottom: 2px; }
+.action-btn.delete { color: #f56c6c; }
+.action-btn.disabled { opacity: 0.3; pointer-events: none; }
+
+/* 全屏视频播放器样式 */
+::v-deep .mobile-video-dialog {
+  background: #000 !important; /* 手机播放视频必须全黑背景 */
+  margin: 0 !important;
+}
+::v-deep .mobile-video-dialog .el-dialog__header { display: none; } /* 隐去官方头 */
+::v-deep .mobile-video-dialog .el-dialog__body { padding: 0; height: 100vh; }
+
+.video-fullscreen-wrapper {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  color: #fff;
+}
+
+.video-header-bar {
+  height: 48px;
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+  background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);
+  z-index: 10;
+}
+.video-header-bar .close-btn { font-size: 22px; margin-right: 15px; }
+.video-header-bar .video-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.video-body {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #000;
+}
+.mobile-video-player {
+  width: 100%;
+  max-height: 80vh;
+}
+
+/* 底部滑出动画 */
+.slide-up-enter-active, .slide-up-leave-active { transition: transform 0.25s ease; }
+.slide-up-enter, .slide-up-leave-to { transform: translateY(100%); }
+</style>

+ 339 - 0
src/views/diskm/DiskShare.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="mobile-share-center">
+    <div class="mobile-share-header">
+      <h2 class="m-page-title">我的分享</h2>
+      <span class="m-sub-title">管理你已分享给好友的合集内容</span>
+    </div>
+
+    <div
+      v-infinite-scroll="loadMore"
+      class="mobile-share-wrapper"
+      :infinite-scroll-disabled="infiniteDisabled"
+      infinite-scroll-distance="30"
+    >
+      <div class="share-card-list">
+        <div
+          v-for="item in dataList"
+          :key="item.shareId"
+          class="share-item-card"
+        >
+          <div class="card-main-row">
+            <div class="album-title-box">
+              <i class="el-icon-folder-opened m-album-icon" />
+              <span class="m-album-name">{{ item.albumName }}</span>
+            </div>
+            <el-tag :type="getTagType(item.albumType)" size="mini" effect="light" round>
+              {{ item.albumType }}
+            </el-tag>
+          </div>
+
+          <div class="card-meta-block">
+            <div class="meta-line">
+              <i class="el-icon-time" />
+              <span>分享于: {{ item.shareAt }}</span>
+            </div>
+            <div class="meta-line" @click="getShareToList(item)">
+              <i class="el-icon-user" />
+              <span>被分享人: <strong class="danger-highlight">{{ item.num }} 位用户</strong></span>
+              <i class="el-icon-arrow-right view-arrow-inline" />
+            </div>
+          </div>
+
+          <el-divider class="card-inner-divider" />
+
+          <div class="card-action-bar">
+            <el-button
+              size="small"
+              type="danger"
+              plain
+              round
+              icon="el-icon-share"
+              class="m-action-btn"
+              @click="deleteShare(item)"
+            >
+              取消分享
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+      <div class="load-status">
+        <p v-if="loading"><i class="el-icon-loading" /> 正在加载中...</p>
+        <p v-if="noMore && dataList.length > 0">已加载所有分享</p>
+        <el-empty v-if="dataList.length === 0 && !loading" :image-size="80" description="暂无任何分享" />
+      </div>
+    </div>
+
+    <el-dialog
+      title="分享名单"
+      :visible.sync="showShareToDialog"
+      width="88%"
+      custom-class="mobile-share-dialog"
+      append-to-body
+    >
+      <div class="m-share-user-list">
+        <div v-if="shareToList.length === 0" class="m-empty-list">暂无分享用户</div>
+        <div v-for="(user, index) in shareToList" :key="index" class="m-user-item">
+          <div class="u-info-left">
+            <el-avatar :size="32" icon="el-icon-user" class="u-avatar" />
+            <span class="u-name">{{ user.username }}</span>
+          </div>
+          <el-tag size="mini" type="success" plain>已查看</el-tag>
+        </div>
+      </div>
+      <div slot="footer" class="m-popup-footer">
+        <el-button type="primary" round style="width: 100%" @click="showShareToDialog = false">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { deleteShare, getShareList, getShareToList } from '@/api/disk'
+
+export default {
+  name: 'DiskShareMobile',
+  data() {
+    return {
+      currentPage: 1,
+      dataList: [],
+      loading: false,
+      noMore: false,
+      showShareToDialog: false,
+      shareToList: []
+    }
+  },
+  computed: {
+    infiniteDisabled() {
+      return this.loading || this.noMore
+    }
+  },
+  created() {
+    // 初始由 infinite-scroll 自动触发首屏加载
+  },
+  methods: {
+    loadMore() {
+      if (this.infiniteDisabled) return
+      this.loading = true
+
+      getShareList(this.currentPage).then(resp => {
+        if (resp.code === 0) {
+          const { list, totalSize } = resp.data
+          // 增量追加列表
+          this.dataList = [...this.dataList, ...list]
+
+          // 如果列表长度达到总数,停止滚动加载
+          if (this.dataList.length >= totalSize || list.length === 0) {
+            this.noMore = true
+          } else {
+            this.currentPage++
+          }
+        } else {
+          this.noMore = true
+          this.$message.error(resp.msg)
+        }
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    getTagType(type) {
+      const map = { '视频': 'danger', '图片': 'success', '文档': 'warning' }
+      return map[type] || 'info'
+    },
+    getShareToList(row) {
+      getShareToList(row.shareId).then(resp => {
+        if (resp.code === 0) {
+          this.shareToList = resp.data
+          this.showShareToDialog = true
+        }
+      })
+    },
+    deleteShare(row) {
+      this.$confirm(`确定要取消针对合集 "${row.albumName}" 的分享吗?`, '取消分享', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        confirmButtonClass: 'el-button--danger',
+        type: 'warning',
+        width: '85%', // 限制确认框宽度适配手机
+        roundButton: true
+      }).then(() => {
+        deleteShare(row.shareId).then(resp => {
+          if (resp.code === 0) {
+            this.$message.success('已取消分享')
+            // 重置状态、局部重载
+            this.currentPage = 1
+            this.dataList = []
+            this.noMore = false
+            this.loadMore()
+          } else {
+            this.$message.error(resp.msg)
+          }
+        })
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mobile-share-center {
+  background-color: #f6f8fa;
+  min-height: 100vh;
+}
+
+/* 顶部文案区 */
+.mobile-share-header {
+  padding: 16px 14px 4px 14px;
+}
+.m-page-title {
+  font-size: 18px;
+  font-weight: bold;
+  color: #0f172a;
+  margin: 0;
+}
+.m-sub-title {
+  font-size: 12px;
+  color: #94a3b8;
+  margin-top: 2px;
+  display: block;
+}
+
+/* 独立流卡片容器 */
+.mobile-share-wrapper {
+  padding: 10px 12px 60px 12px;
+}
+.share-card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+/* 分享单体卡片设计 */
+.share-item-card {
+  background: #fff;
+  border-radius: 14px;
+  padding: 14px;
+  box-shadow: 0 4px 10px rgba(0,0,0,0.01);
+}
+
+.card-main-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 10px;
+}
+.album-title-box {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  overflow: hidden;
+}
+.m-album-icon {
+  font-size: 20px;
+  color: #f59e0b;
+  flex-shrink: 0;
+}
+.m-album-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1e293b;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+/* 元数据组:垂直列表 */
+.card-meta-block {
+  margin-top: 12px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.meta-line {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #64748b;
+  -webkit-tap-highlight-color: transparent;
+}
+.meta-line:active {
+  color: #1a73e8; /* 点击时给被分享人一栏反馈 */
+}
+.meta-line i {
+  font-size: 14px;
+  color: #94a3b8;
+}
+.danger-highlight {
+  color: #ef4444;
+  font-weight: 600;
+}
+.view-arrow-inline {
+  font-size: 11px !important;
+  margin-left: auto; /* 将小箭头右推 */
+  color: #cbd5e1 !important;
+}
+
+.card-inner-divider {
+  margin: 12px 0 10px 0 !important;
+}
+
+/* 底部功能区 */
+.card-action-bar {
+  display: flex;
+  justify-content: flex-end;
+}
+.m-action-btn {
+  font-weight: 500;
+  padding: 6px 14px;
+}
+
+/* 无限加载提示 */
+.load-status {
+  text-align: center;
+  padding: 20px 0;
+  font-size: 12px;
+  color: #94a3b8;
+}
+
+/* 分享名单弹窗优化 */
+::v-deep .mobile-share-dialog {
+  border-radius: 16px !important;
+}
+::v-deep .mobile-share-dialog .el-dialog__body {
+  padding: 10px 16px 20px 16px;
+}
+.m-share-user-list {
+  max-height: 240px;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+.m-user-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 0;
+}
+.m-user-item:not(:last-child) {
+  border-bottom: 1px solid #f1f5f9;
+}
+.u-info-left {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+.u-name {
+  font-size: 13px;
+  color: #334155;
+  font-weight: 500;
+}
+.m-empty-list {
+  text-align: center;
+  padding: 24px;
+  color: #94a3b8;
+  font-size: 13px;
+}
+.m-popup-footer {
+  padding-top: 4px;
+}
+</style>