index2.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. <template>
  2. <div class="canvas-tool">
  3. <div class="left-handler">
  4. <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="lang.ssUndoTip" @click="undo()" v-if="!isFrame" />
  5. <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="lang.ssRedoTip" @click="redo()" v-if="!isFrame" />
  6. <Popover trigger="click" v-model:value="toolVisible" style="height: 100%;display: flex;align-items: center;"
  7. :offset="10" v-if="hasInteractiveTool">
  8. <template #content>
  9. <div class="popover-item" @click="editContent(45)" v-if="frametype != 45">
  10. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  11. <circle cx="12" cy="12" r="10" />
  12. <path d="M12 16v-4m0-4h.01" />
  13. </svg>
  14. <span>{{ lang.ssChoiceQ }}</span>
  15. </div>
  16. <div class="popover-item" @click="editContent(15)" v-if="frametype != 15">
  17. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  18. <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
  19. </svg>
  20. <span>{{ lang.ssQandA }}</span>
  21. </div>
  22. <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
  23. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  24. <polyline points="9 11 12 14 22 4"></polyline>
  25. <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
  26. </svg>
  27. <span>{{ lang.ssVote }}</span>
  28. </div>
  29. <div class="popover-item" @click="editContent(79)" v-if="frametype != 79">
  30. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  31. <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
  32. <circle cx="12" cy="13" r="4"></circle>
  33. </svg>
  34. <span>{{ lang.ssPhoto }}</span>
  35. </div>
  36. <div class="popover-item" @click="editContent(84)" v-if="frametype != 84">
  37. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  38. <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
  39. <circle cx="12" cy="13" r="4"></circle>
  40. </svg>
  41. <span>{{ lang.ssFile }}</span>
  42. </div>
  43. </template>
  44. <div class="handler-item" :class="{ active: toolVisible }">
  45. <span class="svg-icon">
  46. <svg v-if="frametype == 45" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  47. stroke-width="2">
  48. <circle cx="12" cy="12" r="10" />
  49. <path d="M12 16v-4m0-4h.01" />
  50. </svg>
  51. <svg v-if="frametype == 15" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  52. stroke-width="2">
  53. <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
  54. </svg>
  55. <svg v-if="frametype == 78" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  56. <polyline points="9 11 12 14 22 4"></polyline>
  57. <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
  58. </svg>
  59. <svg v-if="frametype == 79" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  60. <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
  61. <circle cx="12" cy="13" r="4"></circle>
  62. </svg>
  63. </span>
  64. <span>{{ iframeLabel }}</span>
  65. <svg t="1776672009773" class="xia-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
  66. p-id="4735" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12">
  67. <path
  68. d="M562.5 771c-14.3 14.3-33.7 27.5-52 23.5-18.4 3.1-35.7-11.2-50-23.5L18.8 327.3c-22.4-22.4-22.4-59.2 0-81.6s59.2-22.4 81.6 0L511.5 668l412.1-422.3c22.4-22.4 59.2-22.4 81.6 0s22.4 59.2 0 81.6L562.5 771z"
  69. p-id="4736" fill="currentColor"></path>
  70. </svg>
  71. </div>
  72. </Popover>
  73. <Popover trigger="click" v-model:value="textTypeSelectVisible"
  74. style="height: 100%;display: flex;align-items: center;" :offset="10" v-if="!isFrame">
  75. <template #content>
  76. <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }">
  77. <IconTextRotationNone /> {{ lang.ssTextHorizontal }}
  78. </PopoverMenuItem>
  79. <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }">
  80. <IconTextRotationDown /> {{ lang.ssTextVertical }}
  81. </PopoverMenuItem>
  82. </template>
  83. <div class="handler-item">
  84. <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
  85. <span>{{ lang.ssText }}</span>
  86. </div>
  87. </Popover>
  88. <Popover trigger="click" style="height: 100%;display: flex;align-items: center;"
  89. v-model:value="picturePoolVisible" :offset="10" v-if="!isFrame">
  90. <template #content>
  91. <FileInput @change="files => insertImageElement(files)">
  92. <div class="popover-item">
  93. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  94. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  95. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
  96. <polyline points="17 8 12 3 7 8" />
  97. <line x1="12" y1="3" x2="12" y2="15" />
  98. </svg>
  99. <span>{{ lang.ssUploadFromLocal }}</span>
  100. </div>
  101. </FileInput>
  102. <!-- <div class="popover-item">
  103. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  104. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  105. <circle cx="11" cy="11" r="8" />
  106. <path d="M21 21l-4.35-4.35" />
  107. </svg>
  108. <span>{{ lang.ssSearchFromWeb }}</span>
  109. </div>
  110. <div class="popover-item">
  111. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  112. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  113. <path d="M12 2L2 7l10 5 10-5-10-5z" />
  114. <path d="M2 17l10 5 10-5" />
  115. <path d="M2 12l10 5 10-5" />
  116. </svg>
  117. <span>{{ lang.ssGenerateFromAI }}</span>
  118. </div> -->
  119. </template>
  120. <div class="handler-item">
  121. <IconPicture class="icon" v-tooltip="lang.ssInsertImage" />
  122. <span>{{ lang.ssImage }}</span>
  123. </div>
  124. <!-- <FileInput @change="files => insertImageElement(files)">
  125. </FileInput> -->
  126. </Popover>
  127. <Popover trigger="click" style="height: 100%;display: flex;align-items: center;" v-model:value="shapePoolVisible"
  128. :offset="10" v-if="!isFrame">
  129. <template #content>
  130. <ShapePool @select="shape => drawShape(shape)" />
  131. </template>
  132. <div class="handler-item">
  133. <IconGraphicDesign class="icon"
  134. :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
  135. <span>{{ lang.ssShape }}</span>
  136. </div>
  137. </Popover>
  138. <RichTextBase v-if="handleElement?.type == 'text'"/>
  139. <div style="display: flex; align-items: center;" v-if="handleElement?.type == 'shape'">
  140. <Select
  141. style="flex: 1;"
  142. :value="fillType"
  143. @update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
  144. :options="[
  145. { label: lang.ssSolidFill, value: 'fill' },
  146. { label: lang.ssGradFill, value: 'gradient' },
  147. // { label: lang.ssImgFill, value: 'pattern' },
  148. ]"
  149. />
  150. <Popover trigger="click" v-if="fillType === 'fill'" style="flex: 1;">
  151. <template #content>
  152. <ColorPicker
  153. :modelValue="fill"
  154. @update:modelValue="value => updateFill(value)"
  155. />
  156. </template>
  157. <ColorButton style="width: 100px;" :color="fill" />
  158. </Popover>
  159. <Select
  160. style="flex: 1;"
  161. :value="gradient.type"
  162. @update:value="value => updateGradient({ type: value as GradientType })"
  163. v-else-if="fillType === 'gradient'"
  164. :options="[
  165. { label: lang.ssLinearGrad, value: 'linear' },
  166. { label: lang.ssRadialGrad, value: 'radial' },
  167. ]"
  168. />
  169. </div>
  170. <template v-if="fillType === 'gradient' && handleElement?.type == 'shape'">
  171. <div class="row">
  172. <GradientBar
  173. :value="gradient.colors"
  174. :index="currentGradientIndex"
  175. @update:value="value => updateGradient({ colors: value })"
  176. @update:index="index => currentGradientIndex = index"
  177. />
  178. </div>
  179. <div class="row">
  180. <div style="width: 40%;">{{ lang.ssCurColorBlock }}</div>
  181. <Popover trigger="click" style="width: 60%;">
  182. <template #content>
  183. <ColorPicker
  184. :modelValue="gradient.colors[currentGradientIndex].color"
  185. @update:modelValue="value => updateGradientColors(value)"
  186. />
  187. </template>
  188. <ColorButton :color="gradient.colors[currentGradientIndex].color" />
  189. </Popover>
  190. </div>
  191. <div class="row" v-if="gradient.type === 'linear'">
  192. <div style="width: 40%;">{{ lang.ssGradAngle }}</div>
  193. <Slider
  194. style="width: 60%;"
  195. :min="0"
  196. :max="360"
  197. :step="15"
  198. :value="gradient.rotate"
  199. @update:value="value => updateGradient({ rotate: value as number })"
  200. />
  201. </div>
  202. </template>
  203. <ElementFlip v-if="handleElement?.type == 'shape'"/>
  204. <!-- 英语口语工具:重置预览 -->
  205. <div
  206. v-if="canResetSpeakingPreview"
  207. class="handler-item reset-preview-btn"
  208. @click="handleResetSpeakingPreview"
  209. >
  210. <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor"
  211. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  212. <polyline points="23 4 23 10 17 10" />
  213. <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
  214. </svg>
  215. <span>重置预览</span>
  216. </div>
  217. </div>
  218. <Modal v-model:visible="latexEditorVisible" :width="880">
  219. <LaTeXEditor @close="latexEditorVisible = false"
  220. @update="data => { createLatexElement(data); latexEditorVisible = false }" />
  221. </Modal>
  222. <!-- 确认对话框 -->
  223. <Modal :visible="confirmDialogVisible" :width="400" :contentStyle="{ borderRadius: '15px' }" :closeButton="false"
  224. :closeOnClickMask="false" :closeOnEsc="false" @update:visible="val => confirmDialogVisible = val">
  225. <div class="clear-confirm">
  226. <div class="clear-confirm__title">{{ lang.ssConfirmOperation }}</div>
  227. <div class="clear-confirm__content">
  228. {{ lang.ssClearToolContent }}
  229. </div>
  230. <div class="clear-confirm__footer">
  231. <div class="btn-c" @click="handleCancel">{{ lang.ssCancel }}</div>
  232. <div class="btn-c confirm" @click="handleConfirm">{{ lang.ssApply }}</div>
  233. </div>
  234. </div>
  235. </Modal>
  236. </div>
  237. </template>
  238. <script lang="ts" setup>
  239. import { ref, computed, watch } from 'vue'
  240. import { storeToRefs } from 'pinia'
  241. import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
  242. import { useSpeakingStore } from '@/store/speaking'
  243. import { useArticleReadingStore } from '@/store/articleReading'
  244. import {
  245. ARTICLE_READING_TOOL_TYPE,
  246. isArticleReadingTool,
  247. isTopicDiscussionTool,
  248. } from '@/configs/englishSpeakingTools'
  249. import { getImageDataURL } from '@/utils/image'
  250. import useImport from '@/hooks/useImport'
  251. import type { ShapePoolItem } from '@/configs/shapes'
  252. import type { LinePoolItem } from '@/configs/lines'
  253. import useScaleCanvas from '@/hooks/useScaleCanvas'
  254. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  255. import useCreateElement from '@/hooks/useCreateElement'
  256. import { lang } from '@/main'
  257. import ShapePool from './ShapePool.vue'
  258. import LinePool from './LinePool.vue'
  259. import ChartPool from './ChartPool.vue'
  260. import TableGenerator from './TableGenerator.vue'
  261. import MediaInput from './MediaInput.vue'
  262. import WebpageInput from './WebpageInput.vue'
  263. import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
  264. import FileInput from '@/components/FileInput.vue'
  265. import Modal from '@/components/Modal.vue'
  266. import Divider from '@/components/Divider.vue'
  267. import Popover from '@/components/Popover.vue'
  268. import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
  269. import Button from '@/components/Button.vue'
  270. import RichTextBase from '@/views/Editor/Toolbar/common/RichTextBase2.vue'
  271. import Select from '@/components/Select.vue'
  272. import ColorPicker from '@/components/ColorPicker/index.vue'
  273. import GradientBar from '@/components/GradientBar.vue'
  274. import Slider from '@/components/Slider.vue'
  275. import ColorButton from '@/components/ColorButton.vue'
  276. import ElementFlip from '@/views/Editor/Toolbar/common/ElementFlip2.vue'
  277. const mainStore = useMainStore()
  278. const slidesStore = useSlidesStore()
  279. const speakingStore = useSpeakingStore()
  280. const articleStore = useArticleReadingStore()
  281. const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
  282. const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
  283. const { currentSlide } = storeToRefs(slidesStore)
  284. const { previewState } = storeToRefs(speakingStore)
  285. const { previewStage: articlePreviewStage } = storeToRefs(articleStore)
  286. // 当前 slide 是否包含英语口语工具(toolType=77)
  287. const hasEnglishSpeakingTool = computed(() => {
  288. const elements = currentSlide.value?.elements || []
  289. return elements.some((el: any) => el.type === 'frame' && isTopicDiscussionTool(el.toolType))
  290. })
  291. // 文章朗读(toolType=772)
  292. const hasArticleReadingTool = computed(() => {
  293. const elements = currentSlide.value?.elements || []
  294. return elements.some((el: any) => el.type === 'frame' && isArticleReadingTool(el.toolType))
  295. })
  296. // 「重置预览」两种题型同一条规则:元素在当前页 **且** 预览已经离开起始页。
  297. // 文章朗读原来是「元素在就一直显示」,与话题讨论不一致 —— 停在起始页时那个按钮没有任何
  298. // 可重置的东西,按下去只是原地重载一次。两边的 ready 都由各自的预览组件同步进 store。
  299. const canResetSpeakingPreview = computed(
  300. () => (hasEnglishSpeakingTool.value && previewState.value !== 'ready')
  301. || (hasArticleReadingTool.value && articlePreviewStage.value !== 'ready')
  302. )
  303. const handleResetSpeakingPreview = () => {
  304. if (hasArticleReadingTool.value) articleStore.requestResetPreview()
  305. if (hasEnglishSpeakingTool.value) speakingStore.requestResetPreview()
  306. }
  307. const getInitialViewMode = () => {
  308. const urlParams = new URLSearchParams(window.location.search)
  309. const modeFromUrl = urlParams.get('mode')
  310. if (modeFromUrl === 'editor2') {
  311. return 'editor2'
  312. }
  313. const modeFromStorage = localStorage.getItem('viewMode')
  314. if (modeFromStorage) {
  315. return modeFromStorage
  316. }
  317. return 'editor'
  318. }
  319. const viewMode = computed(() => getInitialViewMode())
  320. const toolTypeArray = ref([45, 15, 78, 79, 84])
  321. const hasInteractiveTool = computed(() => {
  322. const elements = currentSlide.value?.elements || []
  323. return elements.some((el: any) => el.type === 'frame' && toolTypeArray.value.includes(el.toolType))
  324. })
  325. const iframeLabel = computed(() => {
  326. return getTypeLabel(frametype.value)
  327. })
  328. const frametype = computed(() => {
  329. const elements = currentSlide.value?.elements || []
  330. return hasInteractiveTool.value ? elements[0].type === 'frame' ? elements[0]?.toolType || 0 : 0 : 0
  331. })
  332. // 获取类型标签
  333. const getTypeLabel = (type: number) => {
  334. const typeMap: Record<number, string> = {
  335. 45: lang.ssChoiceQ,
  336. 15: lang.ssQATest,
  337. 72: lang.ssAiApp,
  338. 73: lang.ssHPage,
  339. 81: lang.ssHPage,
  340. 74: lang.ssVideo,
  341. 75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
  342. 76: lang.ssCreative,
  343. 77: lang.ssEnglishSpeakingTool,
  344. [ARTICLE_READING_TOOL_TYPE]: lang.ssArticleReadingTool,
  345. 78: lang.ssVote,
  346. 79: lang.ssPhoto,
  347. 84: lang.ssFile,
  348. 831: lang.ssKwlTitle,
  349. }
  350. return typeMap[type] || lang.ssUnknown
  351. }
  352. const isFrame = computed(() => {
  353. const elements = currentSlide.value?.elements || []
  354. return elements.some((el: any) => el.type === 'frame')
  355. })
  356. const editTool = () => {
  357. const elements = currentSlide.value?.elements || []
  358. const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15))
  359. if (frameElement) {
  360. const url = frameElement.url || ''
  361. interface ParentWindowWithToolList extends Window {
  362. toolBtn?: (action: number, id: string) => void;
  363. }
  364. const parentWindow = window.parent as ParentWindowWithToolList
  365. parentWindow?.toolBtn?.(0, url)
  366. }
  367. }
  368. const { redo, undo } = useHistorySnapshot()
  369. const {
  370. scaleCanvas,
  371. setCanvasScalePercentage,
  372. resetCanvas,
  373. canvasScalePercentage,
  374. } = useScaleCanvas()
  375. const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
  376. const canvasScaleVisible = ref(false)
  377. const applyCanvasPresetScale = (value: number) => {
  378. setCanvasScalePercentage(value)
  379. canvasScaleVisible.value = false
  380. }
  381. const {
  382. createImageElement,
  383. createChartElement,
  384. createTableElement,
  385. createLatexElement,
  386. createVideoElement,
  387. createAudioElement,
  388. createFrameElement,
  389. } = useCreateElement()
  390. const { uploadFileToS3 } = useImport()
  391. const insertImageElement = async (files: FileList) => {
  392. const imageFile = files[0]
  393. if (!imageFile) return
  394. const url = await uploadFileToS3(imageFile)
  395. console.log(url)
  396. // getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
  397. createImageElement(url)
  398. picturePoolVisible.value = false
  399. }
  400. const shapePoolVisible = ref(false)
  401. const picturePoolVisible = ref(false)
  402. const linePoolVisible = ref(false)
  403. const chartPoolVisible = ref(false)
  404. const tableGeneratorVisible = ref(false)
  405. const mediaInputVisible = ref(false)
  406. const webpageInputVisible = ref(false)
  407. // 预设的网页列表
  408. const webpageList = ref<Array<{ type: number, title: string, url: string }>>([])
  409. // 点击插入学习内容时获取数据
  410. const handleInsertLearningContent = () => {
  411. try {
  412. // 获取父窗口的工具列表,如果没有则使用空数组
  413. // 定义父窗口的类型,添加pptToolList属性
  414. interface ParentWindowWithToolList extends Window {
  415. pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
  416. }
  417. const parentWindow = window.parent as ParentWindowWithToolList
  418. const pptToolList = parentWindow?.pptToolList || []
  419. // 转换父窗口的工具列表格式
  420. webpageList.value = pptToolList.map((item: any) => ({
  421. type: item.tool || 0,
  422. title: item.title || lang.ssUnknownTool,
  423. url: item.url || '#'
  424. })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
  425. console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
  426. // 显示弹窗
  427. webpageInputVisible.value = true
  428. }
  429. catch (error) {
  430. console.error('加载学习内容失败:', error)
  431. // 发生错误时使用空数组
  432. webpageList.value = []
  433. // 显示弹窗
  434. webpageInputVisible.value = true
  435. }
  436. }
  437. const latexEditorVisible = ref(false)
  438. const textTypeSelectVisible = ref(false)
  439. const toolVisible = ref(false)
  440. const shapeMenuVisible = ref(false)
  441. const moreVisible = ref(false)
  442. // 绘制文字范围
  443. const drawText = (vertical = false) => {
  444. mainStore.setCreatingElement({
  445. type: 'text',
  446. vertical,
  447. })
  448. }
  449. // 绘制形状范围
  450. const drawShape = (shape: ShapePoolItem) => {
  451. mainStore.setCreatingElement({
  452. type: 'shape',
  453. data: shape,
  454. })
  455. shapePoolVisible.value = false
  456. }
  457. // 绘制自定义任意多边形
  458. const drawCustomShape = () => {
  459. mainStore.setCreatingCustomShapeState(true)
  460. shapePoolVisible.value = false
  461. }
  462. // 绘制线条路径
  463. const drawLine = (line: LinePoolItem) => {
  464. mainStore.setCreatingElement({
  465. type: 'line',
  466. data: line,
  467. })
  468. linePoolVisible.value = false
  469. }
  470. // 打开选择面板
  471. const toggleSelectPanel = () => {
  472. mainStore.setSelectPanelState(!showSelectPanel.value)
  473. }
  474. // 打开搜索替换面板
  475. const toggleSraechPanel = () => {
  476. mainStore.setSearchPanelState(!showSearchPanel.value)
  477. }
  478. // 打开批注面板
  479. const toggleNotesPanel = () => {
  480. mainStore.setNotesPanelState(!showNotesPanel.value)
  481. }
  482. interface ContentItem {
  483. tool?: number
  484. title?: string
  485. url?: string
  486. id?: string
  487. }
  488. // 确认对话框状态
  489. const confirmDialogVisible = ref(false)
  490. const confirmDialogCallback = ref<(() => void) | null>(null)
  491. const showConfirmDialog = (callback: () => void) => {
  492. confirmDialogCallback.value = callback
  493. confirmDialogVisible.value = true
  494. }
  495. const handleConfirm = () => {
  496. confirmDialogCallback.value?.()
  497. confirmDialogVisible.value = false
  498. confirmDialogCallback.value = null
  499. }
  500. const handleCancel = () => {
  501. confirmDialogVisible.value = false
  502. confirmDialogCallback.value = null
  503. }
  504. const editContent = (toolType: number) => {
  505. showConfirmDialog(() => {
  506. interface ParentWindowWithToolList extends Window {
  507. toolBtn2?: (action: number, id: string, toolType: number) => void;
  508. }
  509. const parentWindow = window.parent as ParentWindowWithToolList
  510. const elements = currentSlide.value?.elements || []
  511. const frameElement = elements.find((el: any) => el.type === 'frame' && toolTypeArray.value.includes(el.toolType))
  512. parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
  513. toolVisible.value = false
  514. })
  515. }
  516. import type { GradientType, PPTShapeElement, Gradient, ShapeText } from '@/types/slides'
  517. const { handleElement, handleElementId } = storeToRefs(useMainStore())
  518. const fillType = ref('fill')
  519. const fill = ref<string>('#000')
  520. const textAlign = ref('middle')
  521. const pattern = ref('')
  522. import emitter, { EmitterEvents } from '@/utils/emitter'
  523. const { addHistorySnapshot } = useHistorySnapshot()
  524. const currentGradientIndex = ref(0)
  525. watch(handleElementId, () => {
  526. currentGradientIndex.value = 0
  527. })
  528. const updateElement = (props: Partial<PPTShapeElement>) => {
  529. slidesStore.updateElement({ id: handleElementId.value, props })
  530. addHistorySnapshot()
  531. }
  532. const gradient = ref<Gradient>({
  533. type: 'linear',
  534. rotate: 0,
  535. colors: [
  536. { pos: 0, color: '#fff' },
  537. { pos: 100, color: '#fff' },
  538. ],
  539. })
  540. watch(handleElement, () => {
  541. if (!handleElement.value || handleElement.value.type !== 'shape') return
  542. fill.value = handleElement.value.fill || '#fff'
  543. const defaultGradientColor = [
  544. { pos: 0, color: fill.value },
  545. { pos: 100, color: '#fff' },
  546. ]
  547. gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, colors: defaultGradientColor }
  548. pattern.value = handleElement.value.pattern || ''
  549. fillType.value = (handleElement.value.pattern !== undefined) ? 'pattern' : (handleElement.value.gradient ? 'gradient' : 'fill')
  550. textAlign.value = handleElement.value?.text?.align || 'middle'
  551. if (handleElement.value.text?.content) {
  552. emitter.emit(EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE)
  553. }
  554. }, { deep: true, immediate: true })
  555. // 设置填充类型:渐变、纯色
  556. const updateFillType = (type: 'gradient' | 'fill' | 'pattern') => {
  557. console.log('设置填充类型:', type)
  558. if (type === 'fill') {
  559. slidesStore.removeElementProps({ id: handleElementId.value, propName: ['gradient', 'pattern'] })
  560. addHistorySnapshot()
  561. }
  562. else if (type === 'gradient') {
  563. currentGradientIndex.value = 0
  564. slidesStore.removeElementProps({ id: handleElementId.value, propName: 'pattern' })
  565. updateElement({ gradient: gradient.value })
  566. }
  567. // else if (type === 'pattern') {
  568. // slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
  569. // updateElement({ pattern: '' })
  570. // }
  571. }
  572. // 设置填充色
  573. const updateFill = (value: string) => {
  574. updateElement({ fill: value })
  575. }
  576. // 设置渐变填充
  577. const updateGradient = (gradientProps: Partial<Gradient>) => {
  578. if (!gradient.value) return
  579. const _gradient = { ...gradient.value, ...gradientProps }
  580. updateElement({ gradient: _gradient })
  581. }
  582. const updateGradientColors = (color: string) => {
  583. const colors = gradient.value.colors.map((item, index) => {
  584. if (index === currentGradientIndex.value) return { ...item, color }
  585. return item
  586. })
  587. updateGradient({ colors })
  588. }
  589. </script>
  590. <style lang="scss" scoped>
  591. .confirm-dialog-content {
  592. padding: 20px;
  593. text-align: center;
  594. h3 {
  595. margin: 0 0 16px;
  596. font-size: 18px;
  597. font-weight: 600;
  598. color: #333;
  599. }
  600. p {
  601. margin: 0 0 24px;
  602. font-size: 14px;
  603. color: #666;
  604. line-height: 1.5;
  605. }
  606. .confirm-dialog-buttons {
  607. display: flex;
  608. justify-content: center;
  609. gap: 16px;
  610. button {
  611. padding: 8px 24px;
  612. border-radius: 6px;
  613. font-size: 14px;
  614. font-weight: 500;
  615. cursor: pointer;
  616. transition: all 0.3s;
  617. &.cancel-btn {
  618. background: #f5f5f5;
  619. color: #666;
  620. border: 1px solid #e0e0e0;
  621. &:hover {
  622. background: #e8e8e8;
  623. }
  624. }
  625. &.confirm-btn {
  626. background: #FF9300;
  627. color: white;
  628. border: 1px solid #FF9300;
  629. &:hover {
  630. background: #e68a00;
  631. }
  632. }
  633. }
  634. }
  635. }
  636. .canvas-tool {
  637. position: relative;
  638. border-bottom: 1px solid $borderColor;
  639. background-color: #fff;
  640. display: flex;
  641. padding: 0 10px;
  642. font-size: 13px;
  643. user-select: none;
  644. }
  645. .left-handler,
  646. .right-handler {
  647. .handler-item {
  648. padding: 0 15px;
  649. &:not(.disable):hover,
  650. &:not(.active):hover {
  651. background-color: #f1f1f1;
  652. }
  653. }
  654. }
  655. .reset-preview-btn {
  656. gap: 5px;
  657. color: #6b7280;
  658. svg {
  659. width: 14px;
  660. height: 14px;
  661. }
  662. &:hover {
  663. color: #f97316;
  664. }
  665. }
  666. .left-handler {
  667. display: flex;
  668. align-items: center;
  669. gap: 10px;
  670. .handler-item {
  671. // width: 32px;
  672. background-color: #f9fafb;
  673. border-radius: 5px;
  674. transition: all 0.3s ease;
  675. &:not(.group-btn):hover {
  676. background-color: #f1f1f1;
  677. }
  678. &.active {
  679. // color: $themeColor;
  680. color: #000;
  681. border: 1px solid #f59f4a;
  682. background: #fef7ef !important;
  683. font-weight: 600;
  684. }
  685. .icon {
  686. margin-right: 5px;
  687. }
  688. .svg-icon {
  689. margin-right: 5px;
  690. display: flex;
  691. }
  692. .xia-icon {
  693. margin-left: 5px;
  694. width: 10px;
  695. height: 10px;
  696. }
  697. }
  698. }
  699. .handler-item {
  700. height: 35px;
  701. font-size: 14px;
  702. // margin: 0 2px;
  703. display: flex;
  704. justify-content: center;
  705. align-items: center;
  706. border-radius: $borderRadius;
  707. overflow: hidden;
  708. cursor: pointer;
  709. &.disable {
  710. opacity: .5;
  711. }
  712. }
  713. .right-handler {
  714. display: flex;
  715. align-items: center;
  716. .text {
  717. display: inline-block;
  718. width: 40px;
  719. text-align: center;
  720. cursor: pointer;
  721. }
  722. .viewport-size {
  723. font-size: 13px;
  724. }
  725. }
  726. .edit-tool-btn {
  727. color: #285cf5;
  728. cursor: pointer;
  729. }
  730. .popover-item {
  731. min-width: 80px;
  732. padding: 10px 10px;
  733. // border-radius: $borderRadius;
  734. border-radius: 5px;
  735. font-size: 13px;
  736. cursor: pointer;
  737. transition: all 0.3s ease;
  738. display: flex;
  739. align-items: center;
  740. svg {
  741. margin-right: 5px;
  742. width: 1em;
  743. height: 1em;
  744. }
  745. &.center {
  746. text-align: center;
  747. }
  748. &:hover {
  749. // background-color: #f3f4f6;
  750. background-color: #fff5e5;
  751. color: #f89c46;
  752. }
  753. &+.popover-menu-item {
  754. margin-top: 2px;
  755. }
  756. }
  757. .btn-c {
  758. display: inline-block;
  759. line-height: 1;
  760. white-space: nowrap;
  761. cursor: pointer;
  762. background: #FFF;
  763. border: 1px solid #DCDFE6;
  764. color: #606266;
  765. text-align: center;
  766. box-sizing: border-box;
  767. outline: 0;
  768. margin: 0;
  769. transition: .1s;
  770. font-weight: 500;
  771. padding: 12px 20px;
  772. font-size: 14px;
  773. border-radius: 10px;
  774. +.btn-c {
  775. margin-left: 10px;
  776. }
  777. &.confirm {
  778. background: #FF9400;
  779. color: white;
  780. border: 1px solid #FF9400;
  781. &:hover {
  782. background: #FFA500;
  783. }
  784. }
  785. }
  786. .row {
  787. display: flex;
  788. align-items: center;
  789. gap: 10px;
  790. width: 180px;
  791. }
  792. </style>