index2.vue 27 KB

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