<!--
 * @Author: ysh
 * @Date: 2025-03-06 14:38:35
 * @LastEditors: Please set LastEditors
 * @LastEditTime: 2025-03-12 11:01:28
-->
<template>
  <div class="pdf-study-root">
    <div
      v-if="boxShow"
      v-loading="loading"
      class="pdf-study"
      element-loading-text="正在加载文档..."
    >
      <aside class="study-sidebar">
        <div class="sidebar-top">
          <el-button
            type="text"
            icon="el-icon-arrow-left"
            class="back-btn"
            @click="$router.push({ path: '/oa/study/myStudy' })"
          >返回我的学习</el-button>
        </div>

        <div v-if="resource.title" class="resource-title" :title="resource.title">
          {{ resource.title }}
        </div>

        <div class="progress-card">
          <div class="progress-header">
            <span class="progress-label">学习进度</span>
            <span class="progress-percent" :class="progressPercentClass">{{ percentage }}%</span>
          </div>
          <el-progress
            :percentage="percentage"
            :status="formatStatus(percentage)"
            :stroke-width="10"
            :show-text="false"
          />
          <div v-if="totalPages" class="page-stats">
            <span>已读 <strong>{{ readCount }}</strong> / {{ totalPages }} 页</span>
            <el-tag v-if="isComplete" type="success" size="mini">已完成</el-tag>
          </div>
          <div v-if="resource.hours" class="hours-info">
            可获得学时：<span>{{ resource.hours }}</span> 小时
          </div>
        </div>

        <div class="tips-card">
          <i class="el-icon-info" />
          <p>每页累计可见约 2 秒计入进度，请逐页阅读至文档末尾；滚到底部后稍停片刻即可完成最后一页。</p>
        </div>
      </aside>

      <main class="pdf-main">
        <div ref="pdfContainer" class="pdf-container">
          <div ref="pdfViewerEl" class="pdfViewer"></div>
        </div>
      </main>
    </div>

    <div v-else class="mobile-tip">
      <i class="el-icon-monitor" />
      <p>抱歉，暂不支持手机端，请前往电脑端观看学习</p>
    </div>
  </div>
</template>

<script>
import { getStudy, updateStudy } from '@/api/oa/study/myStudy'
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/build/pdf'
import { PDFViewer } from 'pdfjs-dist/web/pdf_viewer'

/** 页面累计可见多久视为已读（毫秒） */
const PAGE_DWELL_MS = 2000
/** 进度保存防抖（毫秒） */
const SAVE_DEBOUNCE_MS = 2000
/** 页面可见比例阈值（末页往往达不到 50%，故降低） */
const VISIBILITY_RATIO = 0.15
/** 判定滚动到底部的容差（像素） */
const SCROLL_BOTTOM_OFFSET = 80

GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min')

export default {
  name: 'PdfStudy',
  data() {
    return {
      baseUrl: process.env.VUE_APP_BASE_API,
      boxShow: false,
      loading: false,
      studyId: '',
      pdfPath: '',
      resource: {},
      lastPoint: 0,
      percentage: 0,
      totalPages: 0,
      readPages: [],
      isComplete: false,
      pdfViewer: null,
      pdfLoadingTask: null,
      pageObservers: [],
      pageVisibleSince: new Map(),
      pageDwellAccum: new Map(),
      dwellCheckTimer: null,
      saveTimer: null,
      hasRestoredScroll: false,
      onPagesLoaded: null,
      onResize: null,
      onScroll: null
    }
  },
  computed: {
    fullPdfPath() {
      if (!this.pdfPath) return ''
      const path = String(this.pdfPath).trim()
      if (/^https?:\/\//i.test(path)) return path
      if (path.indexOf('/profile/upload') === 0) {
        return `${this.baseUrl}${path}`
      }
      return `${this.baseUrl}/profile/upload${path.startsWith('/') ? path : '/' + path}`
    },
    readCount() {
      return this.readPages.filter(Boolean).length
    },
    progressPercentClass() {
      if (this.percentage >= 100) return 'is-complete'
      if (this.percentage >= 80) return 'is-high'
      if (this.percentage >= 50) return 'is-mid'
      return 'is-low'
    }
  },
  created() {
    this.boxShow = this.$store.getters.device !== 'mobile'
  },
  async mounted() {
    if (!this.boxShow) return
    try {
      await this.loadStudyData()
      await this.initPdfViewer()
    } catch (err) {
      console.error('初始化学习页失败:', err)
      this.$message.error('加载学习资料失败，请稍后重试')
    }
  },
  beforeDestroy() {
    this.teardown()
  },
  methods: {
    async loadStudyData() {
      this.studyId = this.$route.params.id
      const { data } = await getStudy(this.studyId)
      this.pdfPath = data.resource.sourcePath
      this.resource = data.resource
      this.lastPoint = Number(data.lastPoint) || 0
      this.percentage = Math.min(100, Math.round(this.lastPoint))
      this.isComplete = this.percentage >= 100
      if (this.isComplete) {
        this.$message.success('文档已阅读完毕')
      }
    },

    async initPdfViewer() {
      if (!this.fullPdfPath) {
        this.$message.error('未找到 PDF 文件路径')
        return
      }

      this.loading = true
      const container = this.$refs.pdfContainer
      const viewer = this.$refs.pdfViewerEl

      this.pdfViewer = new PDFViewer({
        container,
        viewer,
        enhanceTextSelection: true,
        textLayerMode: 1
      })

      this.bindViewerEvents()

      this.pdfLoadingTask = getDocument({
        url: this.fullPdfPath,
        cMapPacked: true
      })

      try {
        const pdfDocument = await this.pdfLoadingTask.promise
        this.pdfViewer.setDocument(pdfDocument)
        this.totalPages = pdfDocument.numPages
        this.restoreReadProgress()
      } catch (err) {
        console.error('PDF 加载失败:', err)
        this.$message.error('PDF 加载失败，请稍后重试或联系管理员')
        throw err
      } finally {
        this.loading = false
      }
    },

    bindViewerEvents() {
      const { eventBus } = this.pdfViewer
      this.onPagesLoaded = () => {
        this.scrollToLastPosition()
        this.setupPageObservers()
        this.startDwellCheckLoop()
      }
      eventBus.on('pagesloaded', this.onPagesLoaded)

      this.onResize = () => {
        if (this.pdfViewer) {
          this.pdfViewer.update()
        }
      }
      window.addEventListener('resize', this.onResize)

      const container = this.$refs.pdfContainer
      this.onScroll = () => {
        this.checkScrollBottom()
        this.flushVisibleDwell()
      }
      container.addEventListener('scroll', this.onScroll, { passive: true })
    },

    /** 根据上次保存的百分比恢复已读页 */
    restoreReadProgress() {
      this.readPages = new Array(this.totalPages).fill(false)
      if (!this.totalPages) return

      const readCount = Math.min(
        this.totalPages,
        Math.floor((this.percentage / 100) * this.totalPages)
      )
      for (let i = 0; i < readCount; i++) {
        this.readPages[i] = true
      }
      this.syncPercentageFromPages(false)
    },

    scrollToLastPosition() {
      if (this.hasRestoredScroll || !this.lastPoint || !this.totalPages) return

      const pageNumber = Math.min(
        Math.ceil((this.lastPoint / 100) * this.totalPages),
        this.totalPages
      )
      const pageElement = this.$refs.pdfContainer.querySelector(
        `.page[data-page-number="${pageNumber}"]`
      )
      if (pageElement) {
        this.$refs.pdfContainer.scrollTo({
          top: pageElement.offsetTop - 20,
          behavior: 'auto'
        })
      }
      this.hasRestoredScroll = true
    },

    setupPageObservers() {
      this.clearPageObservers()
      const container = this.$refs.pdfContainer
      const pages = container.querySelectorAll('.page')

      pages.forEach(pageEl => {
        const observer = new IntersectionObserver(
          entries => {
            entries.forEach(entry => this.handlePageIntersection(pageEl, entry))
          },
          { threshold: [0, 0.1, 0.15, 0.25, 0.5, 0.75, 1], root: container }
        )
        observer.observe(pageEl)
        this.pageObservers.push({ pageEl, observer })
      })
    },

    handlePageIntersection(pageEl, entry) {
      const pageNumber = parseInt(pageEl.dataset.pageNumber, 10)
      if (!pageNumber || this.readPages[pageNumber - 1]) return

      const ratio = entry.intersectionRatio
      const visible = ratio >= VISIBILITY_RATIO

      if (visible) {
        if (!this.pageVisibleSince.has(pageEl)) {
          this.pageVisibleSince.set(pageEl, Date.now())
        }
      } else if (this.pageVisibleSince.has(pageEl)) {
        this.accumulateDwell(pageEl, pageNumber)
      }
    },

    accumulateDwell(pageEl, pageNumber) {
      const since = this.pageVisibleSince.get(pageEl)
      if (!since) return

      const elapsed = Date.now() - since
      const total = (this.pageDwellAccum.get(pageNumber) || 0) + elapsed
      this.pageDwellAccum.set(pageNumber, total)
      this.pageVisibleSince.delete(pageEl)

      if (total >= PAGE_DWELL_MS) {
        this.markPageRead(pageNumber)
      }
    },

    flushVisibleDwell() {
      this.pageVisibleSince.forEach((since, pageEl) => {
        const pageNumber = parseInt(pageEl.dataset.pageNumber, 10)
        if (!pageNumber || this.readPages[pageNumber - 1]) return

        const elapsed = Date.now() - since
        const total = (this.pageDwellAccum.get(pageNumber) || 0) + elapsed
        this.pageDwellAccum.set(pageNumber, total)
        this.pageVisibleSince.set(pageEl, Date.now())

        if (total >= PAGE_DWELL_MS) {
          this.markPageRead(pageNumber)
        }
      })
    },

    startDwellCheckLoop() {
      this.stopDwellCheckLoop()
      this.dwellCheckTimer = setInterval(() => {
        if (this.isComplete) {
          this.stopDwellCheckLoop()
          return
        }
        this.flushVisibleDwell()
        this.checkScrollBottom()
      }, 500)
    },

    stopDwellCheckLoop() {
      if (this.dwellCheckTimer) {
        clearInterval(this.dwellCheckTimer)
        this.dwellCheckTimer = null
      }
    },

    checkScrollBottom() {
      if (!this.totalPages || this.isComplete) return

      const container = this.$refs.pdfContainer
      const atBottom =
        container.scrollTop + container.clientHeight >=
        container.scrollHeight - SCROLL_BOTTOM_OFFSET

      if (!atBottom) return

      const lastPageNumber = this.totalPages
      if (this.readPages[lastPageNumber - 1]) return

      const lastPageEl = container.querySelector(
        `.page[data-page-number="${lastPageNumber}"]`
      )
      if (!lastPageEl) return

      const rect = lastPageEl.getBoundingClientRect()
      const containerRect = container.getBoundingClientRect()
      const visibleHeight =
        Math.min(rect.bottom, containerRect.bottom) -
        Math.max(rect.top, containerRect.top)

      if (visibleHeight > 0) {
        if (!this.pageVisibleSince.has(lastPageEl)) {
          this.pageVisibleSince.set(lastPageEl, Date.now())
        }
        const accum = this.pageDwellAccum.get(lastPageNumber) || 0
        const since = this.pageVisibleSince.get(lastPageEl)
        const current = accum + (since ? Date.now() - since : 0)
        if (current >= PAGE_DWELL_MS) {
          this.markPageRead(lastPageNumber)
        }
      }
    },

    markPageRead(pageNumber) {
      const index = pageNumber - 1
      if (index < 0 || index >= this.totalPages || this.readPages[index]) return
      this.$set(this.readPages, index, true)
      this.onReadProgressChanged()
    },

    onReadProgressChanged() {
      if (this.isComplete) return

      const progress = this.calcProgressFromPages()
      if (progress < this.percentage) return

      this.percentage = progress
      this.scheduleSave(progress)

      if (progress >= 100) {
        this.tryCompleteStudy()
      }
    },

    calcProgressFromPages() {
      if (!this.totalPages) return 0
      if (this.readCount >= this.totalPages) return 100
      return Math.min(99, Math.floor((this.readCount / this.totalPages) * 100))
    },

    syncPercentageFromPages(allowComplete = true) {
      const fromPages = this.calcProgressFromPages()
      this.percentage = Math.max(this.percentage, fromPages)
      if (allowComplete && fromPages >= 100) {
        this.tryCompleteStudy()
      }
    },

    tryCompleteStudy() {
      if (this.isComplete || this.readCount < this.totalPages) return
      this.percentage = 100
      this.saveProgress(100)
    },

    scheduleSave(progress) {
      if (this.saveTimer) {
        clearTimeout(this.saveTimer)
      }
      this.saveTimer = setTimeout(() => {
        this.saveTimer = null
        this.saveProgress(progress)
      }, SAVE_DEBOUNCE_MS)
    },

    /** 按进度计算已获得学时，保留一位小数，避免 0.5 被 Math.round 成 1 */
    calcGetHours(progress) {
      const hours = Number(this.resource.hours) || 0
      const point = Math.min(100, Math.max(0, Number(progress) || 0))
      return Math.round((hours * point / 100) * 10) / 10
    },

    async saveProgress(progress) {
      if (this.isComplete && progress < 100) return

      const lastPoint = Math.min(100, Math.round(progress))
      const payload = {
        studyId: this.studyId,
        lastPoint,
        getHours: this.calcGetHours(lastPoint),
        lastTime: this.parseTime(new Date(), '{y}-{m}-{d}')
      }

      try {
        await updateStudy(payload)
        this.lastPoint = lastPoint

        if (lastPoint >= 100 && !this.isComplete) {
          this.isComplete = true
          this.$message.success(`文档已阅读完毕！学时+${this.calcGetHours(100)}`)
        }
      } catch (error) {
        console.error('保存进度失败:', error)
        this.$message.error('保存学习进度失败')
      }
    },

    clearPageObservers() {
      this.pageVisibleSince.clear()
      this.pageDwellAccum.clear()
      this.pageObservers.forEach(({ observer }) => observer.disconnect())
      this.pageObservers = []
    },

    teardown() {
      this.stopDwellCheckLoop()

      if (this.saveTimer) {
        clearTimeout(this.saveTimer)
        this.saveTimer = null
      }

      if (!this.isComplete && this.readCount > 0) {
        const progress = this.calcProgressFromPages()
        if (progress > this.lastPoint) {
          this.saveProgress(progress)
        }
      }

      this.clearPageObservers()

      if (this.pdfViewer && this.onPagesLoaded) {
        this.pdfViewer.eventBus.off('pagesloaded', this.onPagesLoaded)
      }

      if (this.onResize) {
        window.removeEventListener('resize', this.onResize)
      }

      if (this.onScroll && this.$refs.pdfContainer) {
        this.$refs.pdfContainer.removeEventListener('scroll', this.onScroll)
      }

      if (this.pdfLoadingTask) {
        this.pdfLoadingTask.destroy()
        this.pdfLoadingTask = null
      }

      this.pdfViewer = null
    },

    formatStatus(value) {
      const row = value || 0
      if (row <= 20) return 'exception'
      if (row <= 50) return 'warning'
      if (row <= 80) return null
      return 'success'
    }
  }
}
</script>

<style lang="scss" scoped>
@import '~pdfjs-dist/web/pdf_viewer.css';

.pdf-study-root {
  height: calc(100vh - 84px);
  min-height: 520px;
  background: #f0f2f5;
}

.pdf-study {
  display: flex;
  height: 100%;
  overflow: hidden;
}

.study-sidebar {
  flex-shrink: 0;
  width: 280px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 20px 18px;
  background: #fff;
  border-right: 1px solid #e4e7ed;
  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04);
  overflow-y: auto;
}

.sidebar-top {
  .back-btn {
    padding: 0;
    font-size: 14px;
    color: #409eff;

    &:hover {
      color: #66b1ff;
    }
  }
}

.resource-title {
  font-size: 15px;
  font-weight: 600;
  color: #303133;
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-all;
}

.progress-card {
  padding: 16px;
  background: linear-gradient(135deg, #f5f9ff 0%, #f0f7ff 100%);
  border: 1px solid #d9ecff;
  border-radius: 8px;
}

.progress-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.progress-label {
  font-size: 13px;
  color: #606266;
}

.progress-percent {
  font-size: 22px;
  font-weight: 700;
  line-height: 1;

  &.is-low { color: #f56c6c; }
  &.is-mid { color: #e6a23c; }
  &.is-high { color: #409eff; }
  &.is-complete { color: #67c23a; }
}

.page-stats {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 12px;
  font-size: 13px;
  color: #606266;

  strong {
    color: #409eff;
    font-size: 15px;
  }
}

.hours-info {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed #d9ecff;
  font-size: 12px;
  color: #909399;

  span {
    color: #409eff;
    font-weight: 600;
  }
}

.tips-card {
  display: flex;
  gap: 8px;
  padding: 12px;
  font-size: 12px;
  line-height: 1.6;
  color: #909399;
  background: #fafafa;
  border-radius: 6px;
  border: 1px solid #ebeef5;

  i {
    flex-shrink: 0;
    margin-top: 2px;
    font-size: 14px;
    color: #c0c4cc;
  }

  p {
    margin: 0;
  }
}

.pdf-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: #525659;
}

.pdf-container {
  flex: 1;
  width: 100%;
  overflow: auto;
  background: #525659;
}

.pdf-main ::v-deep .pdfViewer .page {
  margin: 16px auto;
  border: none;
  border-radius: 2px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}

.mobile-tip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #909399;

  i {
    font-size: 48px;
    margin-bottom: 16px;
  }

  p {
    margin: 0;
    font-size: 14px;
  }
}
</style>
