index2.vue 26 KB

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