index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <template>
  2. <div class="canvas-tool">
  3. <div class="left-handler">
  4. <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="'撤销(Ctrl + Z)'" @click="undo()" />
  5. <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="'重做(Ctrl + Y)'" @click="redo()" />
  6. <div class="more">
  7. <Divider type="vertical" style="height: 20px;" />
  8. <Popover class="more-icon" trigger="click" v-model:value="moreVisible" :offset="10">
  9. <template #content>
  10. <PopoverMenuItem center @click="toggleNotesPanel(); moreVisible = false">批注面板</PopoverMenuItem>
  11. <PopoverMenuItem center @click="toggleSelectPanel(); moreVisible = false">选择窗格</PopoverMenuItem>
  12. <PopoverMenuItem center @click="toggleSraechPanel(); moreVisible = false">查找替换</PopoverMenuItem>
  13. </template>
  14. <IconMore class="handler-item" />
  15. </Popover>
  16. <IconComment class="handler-item" :class="{ 'active': showNotesPanel }" v-tooltip="'批注面板'" @click="toggleNotesPanel()" />
  17. <IconMoveOne class="handler-item" :class="{ 'active': showSelectPanel }" v-tooltip="'选择窗格'" @click="toggleSelectPanel()" />
  18. <IconSearch class="handler-item" :class="{ 'active': showSearchPanel }" v-tooltip="'查找/替换(Ctrl + F)'" @click="toggleSraechPanel()" />
  19. </div>
  20. </div>
  21. <div class="add-element-handler">
  22. <div class="handler-item group-btn" v-tooltip="'插入文字'">
  23. <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
  24. <Popover trigger="click" v-model:value="textTypeSelectVisible" style="height: 100%;" :offset="10">
  25. <template #content>
  26. <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> 横向文本框</PopoverMenuItem>
  27. <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> 竖向文本框</PopoverMenuItem>
  28. </template>
  29. <IconDown class="arrow" />
  30. </Popover>
  31. </div>
  32. <div class="handler-item group-btn" v-tooltip="'插入形状'" :offset="10">
  33. <Popover trigger="click" style="height: 100%;" v-model:value="shapePoolVisible" :offset="10">
  34. <template #content>
  35. <ShapePool @select="shape => drawShape(shape)" />
  36. </template>
  37. <IconGraphicDesign class="icon" :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
  38. </Popover>
  39. <Popover trigger="click" v-model:value="shapeMenuVisible" style="height: 100%;" :offset="10">
  40. <template #content>
  41. <PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">自由绘制</PopoverMenuItem>
  42. </template>
  43. <IconDown class="arrow" />
  44. </Popover>
  45. </div>
  46. <FileInput @change="files => insertImageElement(files)">
  47. <IconPicture class="handler-item" v-tooltip="'插入图片'" />
  48. </FileInput>
  49. <Popover trigger="click" v-model:value="linePoolVisible" :offset="10">
  50. <template #content>
  51. <LinePool @select="line => drawLine(line)" />
  52. </template>
  53. <IconConnection class="handler-item" :class="{ 'active': creatingElement?.type === 'line' }" v-tooltip="'插入线条'" />
  54. </Popover>
  55. <Popover trigger="click" v-model:value="chartPoolVisible" :offset="10">
  56. <template #content>
  57. <ChartPool @select="chart => { createChartElement(chart); chartPoolVisible = false }" />
  58. </template>
  59. <IconChartProportion class="handler-item" v-tooltip="'插入图表'" />
  60. </Popover>
  61. <Popover trigger="click" v-model:value="tableGeneratorVisible" :offset="10">
  62. <template #content>
  63. <TableGenerator
  64. @close="tableGeneratorVisible = false"
  65. @insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }"
  66. />
  67. </template>
  68. <IconInsertTable class="handler-item" v-tooltip="'插入表格'" />
  69. </Popover>
  70. <IconFormula class="handler-item" v-tooltip="'插入公式'" @click="latexEditorVisible = true" />
  71. <Popover trigger="manual" v-model:value="webpageInputVisible" :offset="10">
  72. <template #content>
  73. <WebpageInput
  74. :webpageList="webpageList"
  75. @close="webpageInputVisible = false"
  76. @insertWebpage="({ url, type }) => { createFrameElement(url, type); webpageInputVisible = false }"
  77. />
  78. </template>
  79. <IconLinkOne class="handler-item" v-tooltip="'插入学习内容'" @click="handleInsertLearningContent" />
  80. </Popover>
  81. <Popover trigger="click" v-model:value="mediaInputVisible" :offset="10">
  82. <template #content>
  83. <MediaInput
  84. @close="mediaInputVisible = false"
  85. @insertVideo="src => { createVideoElement(src); mediaInputVisible = false }"
  86. @insertAudio="src => { createAudioElement(src); mediaInputVisible = false }"
  87. />
  88. </template>
  89. <IconVideoTwo class="handler-item" v-tooltip="'插入音视频'" />
  90. </Popover>
  91. </div>
  92. <div class="right-handler">
  93. <IconMinus class="handler-item viewport-size" v-tooltip="'画布缩小(Ctrl + -)'" @click="scaleCanvas('-')" />
  94. <Popover trigger="click" v-model:value="canvasScaleVisible">
  95. <template #content>
  96. <PopoverMenuItem
  97. center
  98. v-for="item in canvasScalePresetList"
  99. :key="item"
  100. @click="applyCanvasPresetScale(item)"
  101. >{{item}}%</PopoverMenuItem>
  102. <PopoverMenuItem center @click="resetCanvas(); canvasScaleVisible = false">适应屏幕</PopoverMenuItem>
  103. </template>
  104. <span class="text">{{canvasScalePercentage}}</span>
  105. </Popover>
  106. <IconPlus class="handler-item viewport-size" v-tooltip="'画布放大(Ctrl + =)'" @click="scaleCanvas('+')" />
  107. <IconFullScreen class="handler-item viewport-size-adaptation" v-tooltip="'适应屏幕(Ctrl + 0)'" @click="resetCanvas()" />
  108. </div>
  109. <Modal
  110. v-model:visible="latexEditorVisible"
  111. :width="880"
  112. >
  113. <LaTeXEditor
  114. @close="latexEditorVisible = false"
  115. @update="data => { createLatexElement(data); latexEditorVisible = false }"
  116. />
  117. </Modal>
  118. </div>
  119. </template>
  120. <script lang="ts" setup>
  121. import { ref } from 'vue'
  122. import { storeToRefs } from 'pinia'
  123. import { useMainStore, useSnapshotStore } from '@/store'
  124. import { getImageDataURL } from '@/utils/image'
  125. import type { ShapePoolItem } from '@/configs/shapes'
  126. import type { LinePoolItem } from '@/configs/lines'
  127. import useScaleCanvas from '@/hooks/useScaleCanvas'
  128. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  129. import useCreateElement from '@/hooks/useCreateElement'
  130. import ShapePool from './ShapePool.vue'
  131. import LinePool from './LinePool.vue'
  132. import ChartPool from './ChartPool.vue'
  133. import TableGenerator from './TableGenerator.vue'
  134. import MediaInput from './MediaInput.vue'
  135. import WebpageInput from './WebpageInput.vue'
  136. import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
  137. import FileInput from '@/components/FileInput.vue'
  138. import Modal from '@/components/Modal.vue'
  139. import Divider from '@/components/Divider.vue'
  140. import Popover from '@/components/Popover.vue'
  141. import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
  142. const mainStore = useMainStore()
  143. const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
  144. const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
  145. const { redo, undo } = useHistorySnapshot()
  146. const {
  147. scaleCanvas,
  148. setCanvasScalePercentage,
  149. resetCanvas,
  150. canvasScalePercentage,
  151. } = useScaleCanvas()
  152. const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
  153. const canvasScaleVisible = ref(false)
  154. const applyCanvasPresetScale = (value: number) => {
  155. setCanvasScalePercentage(value)
  156. canvasScaleVisible.value = false
  157. }
  158. const {
  159. createImageElement,
  160. createChartElement,
  161. createTableElement,
  162. createLatexElement,
  163. createVideoElement,
  164. createAudioElement,
  165. createFrameElement,
  166. } = useCreateElement()
  167. const insertImageElement = (files: FileList) => {
  168. const imageFile = files[0]
  169. if (!imageFile) return
  170. getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
  171. }
  172. const shapePoolVisible = ref(false)
  173. const linePoolVisible = ref(false)
  174. const chartPoolVisible = ref(false)
  175. const tableGeneratorVisible = ref(false)
  176. const mediaInputVisible = ref(false)
  177. const webpageInputVisible = ref(false)
  178. // 预设的网页列表
  179. const webpageList = ref<Array<{type: number, title: string, url: string}>>([])
  180. // 点击插入学习内容时获取数据
  181. const handleInsertLearningContent = () => {
  182. try {
  183. // 获取父窗口的工具列表,如果没有则使用空数组
  184. // 定义父窗口的类型,添加pptToolList属性
  185. interface ParentWindowWithToolList extends Window {
  186. pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
  187. }
  188. const parentWindow = window.parent as ParentWindowWithToolList
  189. const pptToolList = parentWindow?.pptToolList || []
  190. // 转换父窗口的工具列表格式
  191. webpageList.value = pptToolList.map((item: any) => ({
  192. type: item.tool || 0,
  193. title: item.title || '未知工具',
  194. url: item.url || '#'
  195. })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
  196. console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
  197. // 显示弹窗
  198. webpageInputVisible.value = true
  199. }
  200. catch (error) {
  201. console.error('加载学习内容失败:', error)
  202. // 发生错误时使用空数组
  203. webpageList.value = []
  204. // 显示弹窗
  205. webpageInputVisible.value = true
  206. }
  207. }
  208. const latexEditorVisible = ref(false)
  209. const textTypeSelectVisible = ref(false)
  210. const shapeMenuVisible = ref(false)
  211. const moreVisible = ref(false)
  212. // 绘制文字范围
  213. const drawText = (vertical = false) => {
  214. mainStore.setCreatingElement({
  215. type: 'text',
  216. vertical,
  217. })
  218. }
  219. // 绘制形状范围
  220. const drawShape = (shape: ShapePoolItem) => {
  221. mainStore.setCreatingElement({
  222. type: 'shape',
  223. data: shape,
  224. })
  225. shapePoolVisible.value = false
  226. }
  227. // 绘制自定义任意多边形
  228. const drawCustomShape = () => {
  229. mainStore.setCreatingCustomShapeState(true)
  230. shapePoolVisible.value = false
  231. }
  232. // 绘制线条路径
  233. const drawLine = (line: LinePoolItem) => {
  234. mainStore.setCreatingElement({
  235. type: 'line',
  236. data: line,
  237. })
  238. linePoolVisible.value = false
  239. }
  240. // 打开选择面板
  241. const toggleSelectPanel = () => {
  242. mainStore.setSelectPanelState(!showSelectPanel.value)
  243. }
  244. // 打开搜索替换面板
  245. const toggleSraechPanel = () => {
  246. mainStore.setSearchPanelState(!showSearchPanel.value)
  247. }
  248. // 打开批注面板
  249. const toggleNotesPanel = () => {
  250. mainStore.setNotesPanelState(!showNotesPanel.value)
  251. }
  252. </script>
  253. <style lang="scss" scoped>
  254. .canvas-tool {
  255. position: relative;
  256. border-bottom: 1px solid $borderColor;
  257. background-color: #fff;
  258. display: flex;
  259. justify-content: space-between;
  260. padding: 0 10px;
  261. font-size: 13px;
  262. user-select: none;
  263. }
  264. .left-handler, .more {
  265. display: flex;
  266. align-items: center;
  267. }
  268. .more-icon {
  269. display: none;
  270. }
  271. .add-element-handler {
  272. position: absolute;
  273. top: 50%;
  274. left: 50%;
  275. transform: translate(-50%, -50%);
  276. display: flex;
  277. .handler-item {
  278. width: 32px;
  279. &:not(.group-btn):hover {
  280. background-color: #f1f1f1;
  281. }
  282. &.active {
  283. color: $themeColor;
  284. }
  285. &.group-btn {
  286. width: auto;
  287. margin-right: 5px;
  288. &:hover {
  289. background-color: #f3f3f3;
  290. }
  291. .icon, .arrow {
  292. height: 100%;
  293. display: flex;
  294. justify-content: center;
  295. align-items: center;
  296. }
  297. .icon {
  298. width: 26px;
  299. padding: 0 2px;
  300. &:hover {
  301. background-color: #e9e9e9;
  302. }
  303. &.active {
  304. color: $themeColor;
  305. }
  306. }
  307. .arrow {
  308. font-size: 12px;
  309. &:hover {
  310. background-color: #e9e9e9;
  311. }
  312. }
  313. }
  314. }
  315. }
  316. .handler-item {
  317. height: 30px;
  318. font-size: 14px;
  319. margin: 0 2px;
  320. display: flex;
  321. justify-content: center;
  322. align-items: center;
  323. border-radius: $borderRadius;
  324. overflow: hidden;
  325. cursor: pointer;
  326. &.disable {
  327. opacity: .5;
  328. }
  329. }
  330. .left-handler, .right-handler {
  331. .handler-item {
  332. padding: 0 8px;
  333. &.active,
  334. &:not(.disable):hover {
  335. background-color: #f1f1f1;
  336. }
  337. }
  338. }
  339. .right-handler {
  340. display: flex;
  341. align-items: center;
  342. .text {
  343. display: inline-block;
  344. width: 40px;
  345. text-align: center;
  346. cursor: pointer;
  347. }
  348. .viewport-size {
  349. font-size: 13px;
  350. }
  351. }
  352. @media screen and (width <= 1200px) {
  353. .right-handler .text {
  354. display: none;
  355. }
  356. .more > .handler-item {
  357. display: none;
  358. }
  359. .more-icon {
  360. display: block;
  361. }
  362. }
  363. @media screen and (width <= 1000px) {
  364. .left-handler, .right-handler {
  365. display: none;
  366. }
  367. }
  368. </style>