index.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <div class="editor-header">
  3. <div class="left">
  4. <Popover trigger="click" placement="bottom-start" v-model:value="mainMenuVisible">
  5. <template #content>
  6. <FileInput
  7. accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
  8. @change="
  9. files => {
  10. spinType = 'import'
  11. importPPTXFile(files)
  12. mainMenuVisible = false
  13. }
  14. "
  15. >
  16. <PopoverMenuItem>导入pptx</PopoverMenuItem>
  17. </FileInput>
  18. <PopoverMenuItem v-if="userStoreHook.userInfo.phone === '13720176797'" @click="handleExport">导出pptx</PopoverMenuItem>
  19. <PopoverMenuItem
  20. @click="
  21. () => {
  22. mainMenuVisible = false
  23. aiCoursewareVisible = true
  24. }
  25. "
  26. >AI生成课件</PopoverMenuItem
  27. >
  28. <PopoverMenuItem
  29. @click="
  30. () => {
  31. resetSlides()
  32. mainMenuVisible = false
  33. }
  34. "
  35. >重置幻灯片</PopoverMenuItem
  36. >
  37. <PopoverMenuItem
  38. @click="
  39. () => {
  40. mainMenuVisible = false
  41. hotkeyDrawerVisible = true
  42. }
  43. "
  44. >快捷操作</PopoverMenuItem
  45. >
  46. </template>
  47. <div class="menuCon" :class="{ menuVisible: mainMenuVisible }" role="button" tabindex="0" aria-label="课件菜单" @keydown.enter="mainMenuVisible = !mainMenuVisible" @keydown.space.prevent="mainMenuVisible = !mainMenuVisible">
  48. <img class="icon" src="./imgs/file.png" />
  49. </div>
  50. </Popover>
  51. <div class="line"></div>
  52. <div class="title">
  53. <div class="title-text">{{ title }}</div>
  54. </div>
  55. </div>
  56. <div class="right">
  57. <span v-if="aiEditing" class="ai-editing-status" role="status">{{ usePptWorkHook.isSave ? "✓ 课件已保存" : "④ 编辑课件 → ⑤ 保存课件" }}</span>
  58. <Popover
  59. v-if="generationIssues.length"
  60. trigger="click"
  61. placement="bottom-end"
  62. :content-style="{ width: '360px', maxWidth: 'calc(100vw - 40px)' }"
  63. >
  64. <template #content>
  65. <div class="generation-issues">
  66. <strong>上课前请核对以下内容</strong>
  67. <p>可在编辑器中补充或调整,完成后保存课件。</p>
  68. <ul>
  69. <li v-for="(issue, index) in generationIssues" :key="index">
  70. <button v-if="issue.pageNo" @click="slidesStore.updateSlideIndex(Math.min(issue.pageNo - 1, slidesStore.slides.length - 1))">
  71. 第 {{ issue.pageNo }} 页</button
  72. >{{ issue.message }}
  73. </li>
  74. </ul>
  75. </div>
  76. </template>
  77. <button class="generation-notice">需核对 {{ generationIssues.length }} 项</button>
  78. </Popover>
  79. <div class="cancelBtn" @click="handleClose">退出</div>
  80. <div class="saveBtn" @click="handleSave">保存课件</div>
  81. </div>
  82. <Drawer :width="320" v-model:visible="hotkeyDrawerVisible" placement="right">
  83. <HotkeyDoc />
  84. <template v-slot:title>快捷操作</template>
  85. </Drawer>
  86. <AiCoursewareDialog
  87. v-model:visible="aiCoursewareVisible"
  88. @generated="onGenerated"
  89. />
  90. <FullscreenSpin :loading="fullscreenSpinData.loading" :progress="fullscreenSpinData.progress" :tip="fullscreenSpinData.tip" />
  91. </div>
  92. </template>
  93. <script lang="ts" setup>
  94. import { storeToRefs } from "pinia"
  95. import { useSlidesStore } from "@/store"
  96. import useImport from "@/hooks/useImport"
  97. import useExport from "@/hooks/useExport"
  98. import useSlideHandler from "@/hooks/useSlideHandler"
  99. import HotkeyDoc from "./HotkeyDoc.vue"
  100. import FileInput from "@/components/FileInput.vue"
  101. import FullscreenSpin from "@/components/FullscreenSpin.vue"
  102. import Drawer from "@/components/Drawer.vue"
  103. import Popover from "@/components/Popover.vue"
  104. import PopoverMenuItem from "@/components/PopoverMenuItem.vue"
  105. import AiCoursewareDialog from "./AiCoursewareDialog.vue"
  106. import type { AiPptQualityIssue } from "@/utils/aiPptQuality"
  107. import { ref, computed, watch } from "vue"
  108. import { ElMessageBox } from "element-plus"
  109. import usePptWork from "@/store/pptWork"
  110. import { iframeExitMes } from "@/messageHooks/closePage"
  111. import userStore from "@/store/user"
  112. const userStoreHook = userStore()
  113. const slidesStore = useSlidesStore()
  114. const { title } = storeToRefs(slidesStore)
  115. const { resetSlides } = useSlideHandler()
  116. const mainMenuVisible = ref(false)
  117. const hotkeyDrawerVisible = ref(false)
  118. const aiCoursewareVisible = ref(false)
  119. const aiEditing = ref(false)
  120. const generationIssues = ref<AiPptQualityIssue[]>([])
  121. function onGenerated(issues: AiPptQualityIssue[]) {
  122. generationIssues.value = issues
  123. aiEditing.value = true
  124. }
  125. const spinType = ref<"import" | "export">("import")
  126. /* 导入 */
  127. const { importPPTXFile, importing, importProgress } = useImport()
  128. /* 导出 */
  129. const { exportPPTX, exporting, exportProgress } = useExport()
  130. function handleExport() {
  131. mainMenuVisible.value = false
  132. ElMessageBox.confirm("导出pptx可能会丢失一些内容,确认导出?", "提示", {
  133. confirmButtonText: "确认",
  134. cancelButtonText: "取消",
  135. type: "warning"
  136. })
  137. .then(() => {
  138. spinType.value = "export"
  139. exportPPTX()
  140. })
  141. .catch(() => {})
  142. }
  143. const fullscreenSpinData = computed(() => {
  144. if (spinType.value === "export") {
  145. return {
  146. loading: exporting.value,
  147. progress: exportProgress.value,
  148. tip: "正在导出中,请稍等…"
  149. }
  150. } else {
  151. return {
  152. loading: importing.value,
  153. progress: importProgress.value,
  154. tip: "正在导入中,请稍等…"
  155. }
  156. }
  157. })
  158. const usePptWorkHook = usePptWork()
  159. watch(
  160. () => [slidesStore.slides, slidesStore.title, slidesStore.theme, slidesStore.viewportSize, slidesStore.viewportRatio],
  161. () => { if (aiEditing.value) usePptWorkHook.isSave = false },
  162. { deep: true, flush: 'sync' }
  163. )
  164. watch(() => usePptWorkHook.id, () => { aiEditing.value = false; generationIssues.value = [] })
  165. /* 保存课件 */
  166. function handleSave() {
  167. usePptWorkHook.updatePPT()
  168. }
  169. /* 关闭页面 */
  170. function handleClose() {
  171. window.close()
  172. iframeExitMes()
  173. }
  174. </script>
  175. <style lang="scss" scoped>
  176. .editor-header {
  177. background-color: #fff;
  178. user-select: none;
  179. border-bottom: 1px solid $borderColor;
  180. display: flex;
  181. justify-content: space-between;
  182. padding: 0 24px;
  183. }
  184. .left,
  185. .right {
  186. display: flex;
  187. justify-content: center;
  188. align-items: center;
  189. }
  190. .menuCon {
  191. display: flex;
  192. justify-content: center;
  193. align-items: center;
  194. cursor: pointer;
  195. margin-right: 8px;
  196. width: 34px;
  197. height: 34px;
  198. border-radius: 6px;
  199. margin-left: -8px;
  200. &:hover,
  201. &.menuVisible {
  202. background: rgba(34, 71, 133, 0.08);
  203. }
  204. & > img {
  205. width: 20px;
  206. height: 20px;
  207. }
  208. }
  209. .line {
  210. width: 1px;
  211. height: calc(100% - 26px);
  212. background-color: $borderColor;
  213. }
  214. .title {
  215. margin-left: 16px;
  216. .title-text {
  217. font-weight: 600;
  218. font-size: 15px;
  219. color: #131415;
  220. }
  221. }
  222. .cancelBtn {
  223. width: 88px;
  224. height: 32px;
  225. background: #f1f2f6;
  226. border-radius: 6px;
  227. line-height: 32px;
  228. font-weight: 600;
  229. font-size: 14px;
  230. color: #1e2022;
  231. text-align: center;
  232. cursor: pointer;
  233. &:hover {
  234. opacity: 0.8;
  235. }
  236. }
  237. .saveBtn {
  238. margin-left: 16px;
  239. width: 88px;
  240. height: 32px;
  241. background: linear-gradient(312deg, #1b7af8 0%, #3cbbff 100%);
  242. border-radius: 6px;
  243. line-height: 32px;
  244. font-weight: 600;
  245. font-size: 14px;
  246. color: #ffffff;
  247. text-align: center;
  248. cursor: pointer;
  249. &:hover {
  250. opacity: 0.8;
  251. }
  252. }
  253. .generation-notice {
  254. margin-right: 16px;
  255. border: 1px solid #ecd8b7;
  256. border-radius: 5px;
  257. background: #fff9ef;
  258. color: #a76b20;
  259. padding: 7px 12px;
  260. cursor: pointer;
  261. }
  262. .generation-issues {
  263. padding: 16px;
  264. line-height: 1.6;
  265. max-height: 50vh;
  266. overflow: auto;
  267. }
  268. .generation-issues p {
  269. color: #7b899a;
  270. margin: 8px 0;
  271. }
  272. .generation-issues ul {
  273. padding-left: 18px;
  274. }
  275. .generation-issues li {
  276. margin: 10px 0;
  277. }
  278. .generation-issues button {
  279. border: 0;
  280. background: transparent;
  281. color: #1989fa;
  282. cursor: pointer;
  283. margin-right: 6px;
  284. }
  285. .ai-editing-status {
  286. color: #70849b;
  287. font-size: 12px;
  288. margin-right: 16px;
  289. white-space: nowrap;
  290. }
  291. @media (max-width: 1100px) {
  292. .ai-editing-status {
  293. display: none;
  294. }
  295. }
  296. </style>