index2.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <template>
  2. <div class="canvas-tool">
  3. <div class="left-handler">
  4. <Popover trigger="click" v-model:value="toolVisible"
  5. style="height: 100%;display: flex;align-items: center;" :offset="10" v-if="hasInteractiveTool">
  6. <template #content>
  7. <div class="popover-item" @click="editContent(45)" v-if="frametype != 45">
  8. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  9. <circle cx="12" cy="12" r="10" />
  10. <path d="M12 16v-4m0-4h.01" />
  11. </svg>
  12. <span>{{ lang.ssChoiceQ }}</span>
  13. </div>
  14. <div class="popover-item" @click="editContent(15)" v-if="frametype != 15">
  15. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  16. <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
  17. </svg>
  18. <span>{{ lang.ssQandA }}</span>
  19. </div>
  20. </template>
  21. <div class="handler-item">
  22. <span class="svg-icon">
  23. <svg v-if="frametype == 45" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  24. <circle cx="12" cy="12" r="10" />
  25. <path d="M12 16v-4m0-4h.01" />
  26. </svg>
  27. <svg v-if="frametype == 15" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  28. <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
  29. </svg>
  30. </span>
  31. <span>{{ iframeLabel }}</span>
  32. </div>
  33. </Popover>
  34. <Popover trigger="click" v-model:value="textTypeSelectVisible"
  35. style="height: 100%;display: flex;align-items: center;" :offset="10" v-if="!isFrame">
  36. <template #content>
  37. <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }">
  38. <IconTextRotationNone /> {{ lang.ssTextHorizontal }}
  39. </PopoverMenuItem>
  40. <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }">
  41. <IconTextRotationDown /> {{ lang.ssTextVertical }}
  42. </PopoverMenuItem>
  43. </template>
  44. <div class="handler-item">
  45. <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
  46. <span>{{ lang.ssText }}</span>
  47. </div>
  48. </Popover>
  49. <Popover trigger="click" style="height: 100%;display: flex;align-items: center;"
  50. v-model:value="picturePoolVisible" :offset="10" v-if="!isFrame">
  51. <template #content>
  52. <FileInput @change="files => insertImageElement(files)">
  53. <div class="popover-item">
  54. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  55. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  56. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
  57. <polyline points="17 8 12 3 7 8" />
  58. <line x1="12" y1="3" x2="12" y2="15" />
  59. </svg>
  60. <span>{{ lang.ssUploadFromLocal }}</span>
  61. </div>
  62. </FileInput>
  63. <!-- <div class="popover-item">
  64. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  65. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  66. <circle cx="11" cy="11" r="8" />
  67. <path d="M21 21l-4.35-4.35" />
  68. </svg>
  69. <span>{{ lang.ssSearchFromWeb }}</span>
  70. </div>
  71. <div class="popover-item">
  72. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  73. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  74. <path d="M12 2L2 7l10 5 10-5-10-5z" />
  75. <path d="M2 17l10 5 10-5" />
  76. <path d="M2 12l10 5 10-5" />
  77. </svg>
  78. <span>{{ lang.ssGenerateFromAI }}</span>
  79. </div> -->
  80. </template>
  81. <div class="handler-item">
  82. <IconPicture class="icon" v-tooltip="lang.ssInsertImage" />
  83. <span>{{ lang.ssImage }}</span>
  84. </div>
  85. <!-- <FileInput @change="files => insertImageElement(files)">
  86. </FileInput> -->
  87. </Popover>
  88. <Popover trigger="click" style="height: 100%;display: flex;align-items: center;" v-model:value="shapePoolVisible"
  89. :offset="10" v-if="!isFrame">
  90. <template #content>
  91. <ShapePool @select="shape => drawShape(shape)" />
  92. </template>
  93. <div class="handler-item">
  94. <IconGraphicDesign class="icon"
  95. :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
  96. <span>{{ lang.ssShape }}</span>
  97. </div>
  98. </Popover>
  99. </div>
  100. <Modal v-model:visible="latexEditorVisible" :width="880">
  101. <LaTeXEditor @close="latexEditorVisible = false"
  102. @update="data => { createLatexElement(data); latexEditorVisible = false }" />
  103. </Modal>
  104. <!-- 确认对话框 -->
  105. <Modal
  106. :visible="confirmDialogVisible"
  107. :width="420"
  108. :closeButton="true"
  109. :closeOnClickMask="false"
  110. :closeOnEsc="false"
  111. @update:visible="val => confirmDialogVisible = val"
  112. >
  113. <div class="clear-confirm">
  114. <div class="clear-confirm__title">{{ lang.ssConfirmOperation }}</div>
  115. <div class="clear-confirm__content">
  116. {{ lang.ssClearToolContent }}
  117. </div>
  118. <div class="clear-confirm__footer">
  119. <Button type="default" @click="handleCancel">{{ lang.ssCancel }}</Button>
  120. <Button type="primary" @click="handleConfirm">{{ lang.ssApply }}</Button>
  121. </div>
  122. </div>
  123. </Modal>
  124. </div>
  125. </template>
  126. <script lang="ts" setup>
  127. import { ref, computed } from 'vue'
  128. import { storeToRefs } from 'pinia'
  129. import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
  130. import { getImageDataURL } from '@/utils/image'
  131. import useImport from '@/hooks/useImport'
  132. import type { ShapePoolItem } from '@/configs/shapes'
  133. import type { LinePoolItem } from '@/configs/lines'
  134. import useScaleCanvas from '@/hooks/useScaleCanvas'
  135. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  136. import useCreateElement from '@/hooks/useCreateElement'
  137. import { lang } from '@/main'
  138. import ShapePool from './ShapePool.vue'
  139. import LinePool from './LinePool.vue'
  140. import ChartPool from './ChartPool.vue'
  141. import TableGenerator from './TableGenerator.vue'
  142. import MediaInput from './MediaInput.vue'
  143. import WebpageInput from './WebpageInput.vue'
  144. import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
  145. import FileInput from '@/components/FileInput.vue'
  146. import Modal from '@/components/Modal.vue'
  147. import Divider from '@/components/Divider.vue'
  148. import Popover from '@/components/Popover.vue'
  149. import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
  150. import Button from '@/components/Button.vue'
  151. const mainStore = useMainStore()
  152. const slidesStore = useSlidesStore()
  153. const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
  154. const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
  155. const { currentSlide } = storeToRefs(slidesStore)
  156. const getInitialViewMode = () => {
  157. const urlParams = new URLSearchParams(window.location.search)
  158. const modeFromUrl = urlParams.get('mode')
  159. if (modeFromUrl === 'editor2') {
  160. return 'editor2'
  161. }
  162. const modeFromStorage = localStorage.getItem('viewMode')
  163. if (modeFromStorage) {
  164. return modeFromStorage
  165. }
  166. return 'editor'
  167. }
  168. const viewMode = computed(() => getInitialViewMode())
  169. const hasInteractiveTool = computed(() => {
  170. const elements = currentSlide.value?.elements || []
  171. return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
  172. })
  173. const iframeLabel = computed(() => {
  174. return getTypeLabel(frametype.value)
  175. })
  176. const frametype = computed(() => {
  177. const elements = currentSlide.value?.elements || []
  178. return hasInteractiveTool.value ? elements[0].type === 'frame' ? elements[0]?.toolType || 0 : 0 : 0
  179. })
  180. // 获取类型标签
  181. const getTypeLabel = (type: number) => {
  182. const typeMap: Record<number, string> = {
  183. 45: lang.ssChoiceQ,
  184. 15: lang.ssQATest,
  185. 72: lang.ssAiApp,
  186. 73: lang.ssHPage,
  187. 74: lang.ssVideo,
  188. 75: lang.ssBiliVideo,
  189. 76: lang.ssCreative
  190. }
  191. return typeMap[type] || lang.ssUnknown
  192. }
  193. const isFrame = computed(() => {
  194. const elements = currentSlide.value?.elements || []
  195. return elements.some((el: any) => el.type === 'frame')
  196. })
  197. const editTool = () => {
  198. const elements = currentSlide.value?.elements || []
  199. const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
  200. if (frameElement) {
  201. const url = frameElement.url || ''
  202. interface ParentWindowWithToolList extends Window {
  203. toolBtn?: (action: number, id: string) => void;
  204. }
  205. const parentWindow = window.parent as ParentWindowWithToolList
  206. parentWindow?.toolBtn?.(0, url)
  207. }
  208. }
  209. const { redo, undo } = useHistorySnapshot()
  210. const {
  211. scaleCanvas,
  212. setCanvasScalePercentage,
  213. resetCanvas,
  214. canvasScalePercentage,
  215. } = useScaleCanvas()
  216. const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
  217. const canvasScaleVisible = ref(false)
  218. const applyCanvasPresetScale = (value: number) => {
  219. setCanvasScalePercentage(value)
  220. canvasScaleVisible.value = false
  221. }
  222. const {
  223. createImageElement,
  224. createChartElement,
  225. createTableElement,
  226. createLatexElement,
  227. createVideoElement,
  228. createAudioElement,
  229. createFrameElement,
  230. } = useCreateElement()
  231. const { uploadFileToS3 } = useImport()
  232. const insertImageElement = async (files: FileList) => {
  233. const imageFile = files[0]
  234. if (!imageFile) return
  235. const url = await uploadFileToS3(imageFile)
  236. console.log(url)
  237. // getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
  238. createImageElement(url)
  239. picturePoolVisible.value = false
  240. }
  241. const shapePoolVisible = ref(false)
  242. const picturePoolVisible = ref(false)
  243. const linePoolVisible = ref(false)
  244. const chartPoolVisible = ref(false)
  245. const tableGeneratorVisible = ref(false)
  246. const mediaInputVisible = ref(false)
  247. const webpageInputVisible = ref(false)
  248. // 预设的网页列表
  249. const webpageList = ref<Array<{ type: number, title: string, url: string }>>([])
  250. // 点击插入学习内容时获取数据
  251. const handleInsertLearningContent = () => {
  252. try {
  253. // 获取父窗口的工具列表,如果没有则使用空数组
  254. // 定义父窗口的类型,添加pptToolList属性
  255. interface ParentWindowWithToolList extends Window {
  256. pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
  257. }
  258. const parentWindow = window.parent as ParentWindowWithToolList
  259. const pptToolList = parentWindow?.pptToolList || []
  260. // 转换父窗口的工具列表格式
  261. webpageList.value = pptToolList.map((item: any) => ({
  262. type: item.tool || 0,
  263. title: item.title || lang.ssUnknownTool,
  264. url: item.url || '#'
  265. })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
  266. console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
  267. // 显示弹窗
  268. webpageInputVisible.value = true
  269. }
  270. catch (error) {
  271. console.error('加载学习内容失败:', error)
  272. // 发生错误时使用空数组
  273. webpageList.value = []
  274. // 显示弹窗
  275. webpageInputVisible.value = true
  276. }
  277. }
  278. const latexEditorVisible = ref(false)
  279. const textTypeSelectVisible = ref(false)
  280. const toolVisible = ref(false)
  281. const shapeMenuVisible = ref(false)
  282. const moreVisible = ref(false)
  283. // 绘制文字范围
  284. const drawText = (vertical = false) => {
  285. mainStore.setCreatingElement({
  286. type: 'text',
  287. vertical,
  288. })
  289. }
  290. // 绘制形状范围
  291. const drawShape = (shape: ShapePoolItem) => {
  292. mainStore.setCreatingElement({
  293. type: 'shape',
  294. data: shape,
  295. })
  296. shapePoolVisible.value = false
  297. }
  298. // 绘制自定义任意多边形
  299. const drawCustomShape = () => {
  300. mainStore.setCreatingCustomShapeState(true)
  301. shapePoolVisible.value = false
  302. }
  303. // 绘制线条路径
  304. const drawLine = (line: LinePoolItem) => {
  305. mainStore.setCreatingElement({
  306. type: 'line',
  307. data: line,
  308. })
  309. linePoolVisible.value = false
  310. }
  311. // 打开选择面板
  312. const toggleSelectPanel = () => {
  313. mainStore.setSelectPanelState(!showSelectPanel.value)
  314. }
  315. // 打开搜索替换面板
  316. const toggleSraechPanel = () => {
  317. mainStore.setSearchPanelState(!showSearchPanel.value)
  318. }
  319. // 打开批注面板
  320. const toggleNotesPanel = () => {
  321. mainStore.setNotesPanelState(!showNotesPanel.value)
  322. }
  323. interface ContentItem {
  324. tool?: number
  325. title?: string
  326. url?: string
  327. id?: string
  328. }
  329. // 确认对话框状态
  330. const confirmDialogVisible = ref(false)
  331. const confirmDialogCallback = ref<(() => void) | null>(null)
  332. const showConfirmDialog = (callback: () => void) => {
  333. confirmDialogCallback.value = callback
  334. confirmDialogVisible.value = true
  335. }
  336. const handleConfirm = () => {
  337. confirmDialogCallback.value?.()
  338. confirmDialogVisible.value = false
  339. confirmDialogCallback.value = null
  340. }
  341. const handleCancel = () => {
  342. confirmDialogVisible.value = false
  343. confirmDialogCallback.value = null
  344. }
  345. const editContent = (toolType: number) => {
  346. showConfirmDialog(() => {
  347. interface ParentWindowWithToolList extends Window {
  348. toolBtn2?: (action: number, id: string, toolType: number) => void;
  349. }
  350. const parentWindow = window.parent as ParentWindowWithToolList
  351. const elements = currentSlide.value?.elements || []
  352. const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
  353. parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
  354. toolVisible.value = false
  355. })
  356. }
  357. </script>
  358. <style lang="scss" scoped>
  359. .confirm-dialog-content {
  360. padding: 20px;
  361. text-align: center;
  362. h3 {
  363. margin: 0 0 16px;
  364. font-size: 18px;
  365. font-weight: 600;
  366. color: #333;
  367. }
  368. p {
  369. margin: 0 0 24px;
  370. font-size: 14px;
  371. color: #666;
  372. line-height: 1.5;
  373. }
  374. .confirm-dialog-buttons {
  375. display: flex;
  376. justify-content: center;
  377. gap: 16px;
  378. button {
  379. padding: 8px 24px;
  380. border-radius: 6px;
  381. font-size: 14px;
  382. font-weight: 500;
  383. cursor: pointer;
  384. transition: all 0.3s;
  385. &.cancel-btn {
  386. background: #f5f5f5;
  387. color: #666;
  388. border: 1px solid #e0e0e0;
  389. &:hover {
  390. background: #e8e8e8;
  391. }
  392. }
  393. &.confirm-btn {
  394. background: #FF9300;
  395. color: white;
  396. border: 1px solid #FF9300;
  397. &:hover {
  398. background: #e68a00;
  399. }
  400. }
  401. }
  402. }
  403. }
  404. .canvas-tool {
  405. position: relative;
  406. border-bottom: 1px solid $borderColor;
  407. background-color: #fff;
  408. display: flex;
  409. padding: 0 10px;
  410. font-size: 13px;
  411. user-select: none;
  412. }
  413. .left-handler {
  414. display: flex;
  415. align-items: center;
  416. gap: 10px;
  417. .handler-item {
  418. // width: 32px;
  419. background-color: #f9fafb;
  420. border-radius: 5px;
  421. transition: all 0.3s ease;
  422. &:not(.group-btn):hover {
  423. background-color: #f1f1f1;
  424. }
  425. &.active {
  426. color: $themeColor;
  427. }
  428. .icon {
  429. margin-right: 5px;
  430. }
  431. .svg-icon {
  432. margin-right: 5px;
  433. display: flex;
  434. }
  435. }
  436. }
  437. .handler-item {
  438. height: 35px;
  439. font-size: 14px;
  440. // margin: 0 2px;
  441. display: flex;
  442. justify-content: center;
  443. align-items: center;
  444. border-radius: $borderRadius;
  445. overflow: hidden;
  446. cursor: pointer;
  447. &.disable {
  448. opacity: .5;
  449. }
  450. }
  451. .left-handler,
  452. .right-handler {
  453. .handler-item {
  454. padding: 0 15px;
  455. &.active,
  456. &:not(.disable):hover {
  457. background-color: #f1f1f1;
  458. }
  459. }
  460. }
  461. .right-handler {
  462. display: flex;
  463. align-items: center;
  464. .text {
  465. display: inline-block;
  466. width: 40px;
  467. text-align: center;
  468. cursor: pointer;
  469. }
  470. .viewport-size {
  471. font-size: 13px;
  472. }
  473. }
  474. .edit-tool-btn {
  475. color: #285cf5;
  476. cursor: pointer;
  477. }
  478. .popover-item {
  479. min-width: 80px;
  480. padding: 10px 10px;
  481. // border-radius: $borderRadius;
  482. border-radius: 5px;
  483. font-size: 13px;
  484. cursor: pointer;
  485. transition: all 0.3s ease;
  486. display: flex;
  487. align-items: center;
  488. svg {
  489. margin-right: 5px;
  490. width: 1em;
  491. height: 1em;
  492. }
  493. &.center {
  494. text-align: center;
  495. }
  496. &:hover {
  497. background-color: #f3f4f6;
  498. }
  499. &+.popover-menu-item {
  500. margin-top: 2px;
  501. }
  502. }
  503. </style>