123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- <template>
- <div class="canvas-tool">
- <div class="left-handler">
- <IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="'撤销(Ctrl + Z)'" @click="undo()" />
- <IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="'重做(Ctrl + Y)'" @click="redo()" />
- <div class="more">
- <Divider type="vertical" style="height: 20px;" />
- <Popover class="more-icon" trigger="click" v-model:value="moreVisible" :offset="10">
- <template #content>
- <PopoverMenuItem center @click="toggleNotesPanel(); moreVisible = false">批注面板</PopoverMenuItem>
- <PopoverMenuItem center @click="toggleSelectPanel(); moreVisible = false">选择窗格</PopoverMenuItem>
- <PopoverMenuItem center @click="toggleSraechPanel(); moreVisible = false">查找替换</PopoverMenuItem>
- </template>
- <IconMore class="handler-item" />
- </Popover>
- <IconComment class="handler-item" :class="{ 'active': showNotesPanel }" v-tooltip="'批注面板'" @click="toggleNotesPanel()" />
- <IconMoveOne class="handler-item" :class="{ 'active': showSelectPanel }" v-tooltip="'选择窗格'" @click="toggleSelectPanel()" />
- <IconSearch class="handler-item" :class="{ 'active': showSearchPanel }" v-tooltip="'查找/替换(Ctrl + F)'" @click="toggleSraechPanel()" />
- </div>
- </div>
- <div class="add-element-handler">
- <div class="handler-item group-btn" v-tooltip="'插入文字'">
- <IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
-
- <Popover trigger="click" v-model:value="textTypeSelectVisible" style="height: 100%;" :offset="10">
- <template #content>
- <PopoverMenuItem center @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> 横向文本框</PopoverMenuItem>
- <PopoverMenuItem center @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> 竖向文本框</PopoverMenuItem>
- </template>
- <IconDown class="arrow" />
- </Popover>
- </div>
- <div class="handler-item group-btn" v-tooltip="'插入形状'" :offset="10">
- <Popover trigger="click" style="height: 100%;" v-model:value="shapePoolVisible" :offset="10">
- <template #content>
- <ShapePool @select="shape => drawShape(shape)" />
- </template>
- <IconGraphicDesign class="icon" :class="{ 'active': creatingCustomShape || creatingElement?.type === 'shape' }" />
- </Popover>
-
- <Popover trigger="click" v-model:value="shapeMenuVisible" style="height: 100%;" :offset="10">
- <template #content>
- <PopoverMenuItem center @click="() => { drawCustomShape(); shapeMenuVisible = false }">自由绘制</PopoverMenuItem>
- </template>
- <IconDown class="arrow" />
- </Popover>
- </div>
- <FileInput @change="files => insertImageElement(files)">
- <IconPicture class="handler-item" v-tooltip="'插入图片'" />
- </FileInput>
- <Popover trigger="click" v-model:value="linePoolVisible" :offset="10">
- <template #content>
- <LinePool @select="line => drawLine(line)" />
- </template>
- <IconConnection class="handler-item" :class="{ 'active': creatingElement?.type === 'line' }" v-tooltip="'插入线条'" />
- </Popover>
- <Popover trigger="click" v-model:value="chartPoolVisible" :offset="10">
- <template #content>
- <ChartPool @select="chart => { createChartElement(chart); chartPoolVisible = false }" />
- </template>
- <IconChartProportion class="handler-item" v-tooltip="'插入图表'" />
- </Popover>
- <Popover trigger="click" v-model:value="tableGeneratorVisible" :offset="10">
- <template #content>
- <TableGenerator
- @close="tableGeneratorVisible = false"
- @insert="({ row, col }) => { createTableElement(row, col); tableGeneratorVisible = false }"
- />
- </template>
- <IconInsertTable class="handler-item" v-tooltip="'插入表格'" />
- </Popover>
- <IconFormula class="handler-item" v-tooltip="'插入公式'" @click="latexEditorVisible = true" />
- <Popover trigger="manual" v-model:value="webpageInputVisible" :offset="10">
- <template #content>
- <WebpageInput
- :webpageList="webpageList"
- @close="webpageInputVisible = false"
- @insertWebpage="({ url, type }) => { createFrameElement(url, type); webpageInputVisible = false }"
- />
- </template>
- <IconLinkOne class="handler-item" v-tooltip="'插入学习内容'" @click="handleInsertLearningContent" />
- </Popover>
- <Popover trigger="click" v-model:value="mediaInputVisible" :offset="10">
- <template #content>
- <MediaInput
- @close="mediaInputVisible = false"
- @insertVideo="src => { createVideoElement(src); mediaInputVisible = false }"
- @insertAudio="src => { createAudioElement(src); mediaInputVisible = false }"
- />
- </template>
- <IconVideoTwo class="handler-item" v-tooltip="'插入音视频'" />
- </Popover>
- </div>
- <div class="right-handler">
- <IconMinus class="handler-item viewport-size" v-tooltip="'画布缩小(Ctrl + -)'" @click="scaleCanvas('-')" />
- <Popover trigger="click" v-model:value="canvasScaleVisible">
- <template #content>
- <PopoverMenuItem
- center
- v-for="item in canvasScalePresetList"
- :key="item"
- @click="applyCanvasPresetScale(item)"
- >{{item}}%</PopoverMenuItem>
- <PopoverMenuItem center @click="resetCanvas(); canvasScaleVisible = false">适应屏幕</PopoverMenuItem>
- </template>
- <span class="text">{{canvasScalePercentage}}</span>
- </Popover>
- <IconPlus class="handler-item viewport-size" v-tooltip="'画布放大(Ctrl + =)'" @click="scaleCanvas('+')" />
- <IconFullScreen class="handler-item viewport-size-adaptation" v-tooltip="'适应屏幕(Ctrl + 0)'" @click="resetCanvas()" />
- </div>
- <Modal
- v-model:visible="latexEditorVisible"
- :width="880"
- >
- <LaTeXEditor
- @close="latexEditorVisible = false"
- @update="data => { createLatexElement(data); latexEditorVisible = false }"
- />
- </Modal>
- </div>
- </template>
- <script lang="ts" setup>
- import { ref } from 'vue'
- import { storeToRefs } from 'pinia'
- import { useMainStore, useSnapshotStore } from '@/store'
- import { getImageDataURL } from '@/utils/image'
- import type { ShapePoolItem } from '@/configs/shapes'
- import type { LinePoolItem } from '@/configs/lines'
- import useScaleCanvas from '@/hooks/useScaleCanvas'
- import useHistorySnapshot from '@/hooks/useHistorySnapshot'
- import useCreateElement from '@/hooks/useCreateElement'
- import ShapePool from './ShapePool.vue'
- import LinePool from './LinePool.vue'
- import ChartPool from './ChartPool.vue'
- import TableGenerator from './TableGenerator.vue'
- import MediaInput from './MediaInput.vue'
- import WebpageInput from './WebpageInput.vue'
- import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
- import FileInput from '@/components/FileInput.vue'
- import Modal from '@/components/Modal.vue'
- import Divider from '@/components/Divider.vue'
- import Popover from '@/components/Popover.vue'
- import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
- const mainStore = useMainStore()
- const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
- const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
- const { redo, undo } = useHistorySnapshot()
- const {
- scaleCanvas,
- setCanvasScalePercentage,
- resetCanvas,
- canvasScalePercentage,
- } = useScaleCanvas()
- const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
- const canvasScaleVisible = ref(false)
- const applyCanvasPresetScale = (value: number) => {
- setCanvasScalePercentage(value)
- canvasScaleVisible.value = false
- }
- const {
- createImageElement,
- createChartElement,
- createTableElement,
- createLatexElement,
- createVideoElement,
- createAudioElement,
- createFrameElement,
- } = useCreateElement()
- const insertImageElement = (files: FileList) => {
- const imageFile = files[0]
- if (!imageFile) return
- getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
- }
- const shapePoolVisible = ref(false)
- const linePoolVisible = ref(false)
- const chartPoolVisible = ref(false)
- const tableGeneratorVisible = ref(false)
- const mediaInputVisible = ref(false)
- const webpageInputVisible = ref(false)
- // 预设的网页列表
- const webpageList = ref<Array<{type: number, title: string, url: string}>>([])
- // 点击插入学习内容时获取数据
- const handleInsertLearningContent = () => {
- try {
- // 获取父窗口的工具列表,如果没有则使用空数组
- // 定义父窗口的类型,添加pptToolList属性
- interface ParentWindowWithToolList extends Window {
- pptToolList?: Array<{ tool?: number; title?: string; url?: string }>
- }
- const parentWindow = window.parent as ParentWindowWithToolList
- const pptToolList = parentWindow?.pptToolList || []
- // 转换父窗口的工具列表格式
- webpageList.value = pptToolList.map((item: any) => ({
- type: item.tool || 0,
- title: item.title || '未知工具',
- url: item.url || '#'
- })).filter((item: any) => item.url !== '#') // 过滤掉无效的URL
-
- console.log('学习内容列表加载完成:', webpageList.value.length, '个项目')
-
- // 显示弹窗
- webpageInputVisible.value = true
-
- }
- catch (error) {
- console.error('加载学习内容失败:', error)
-
- // 发生错误时使用空数组
- webpageList.value = []
-
- // 显示弹窗
- webpageInputVisible.value = true
- }
- }
- const latexEditorVisible = ref(false)
- const textTypeSelectVisible = ref(false)
- const shapeMenuVisible = ref(false)
- const moreVisible = ref(false)
- // 绘制文字范围
- const drawText = (vertical = false) => {
- mainStore.setCreatingElement({
- type: 'text',
- vertical,
- })
- }
- // 绘制形状范围
- const drawShape = (shape: ShapePoolItem) => {
- mainStore.setCreatingElement({
- type: 'shape',
- data: shape,
- })
- shapePoolVisible.value = false
- }
- // 绘制自定义任意多边形
- const drawCustomShape = () => {
- mainStore.setCreatingCustomShapeState(true)
- shapePoolVisible.value = false
- }
- // 绘制线条路径
- const drawLine = (line: LinePoolItem) => {
- mainStore.setCreatingElement({
- type: 'line',
- data: line,
- })
- linePoolVisible.value = false
- }
- // 打开选择面板
- const toggleSelectPanel = () => {
- mainStore.setSelectPanelState(!showSelectPanel.value)
- }
- // 打开搜索替换面板
- const toggleSraechPanel = () => {
- mainStore.setSearchPanelState(!showSearchPanel.value)
- }
- // 打开批注面板
- const toggleNotesPanel = () => {
- mainStore.setNotesPanelState(!showNotesPanel.value)
- }
- </script>
- <style lang="scss" scoped>
- .canvas-tool {
- position: relative;
- border-bottom: 1px solid $borderColor;
- background-color: #fff;
- display: flex;
- justify-content: space-between;
- padding: 0 10px;
- font-size: 13px;
- user-select: none;
- }
- .left-handler, .more {
- display: flex;
- align-items: center;
- }
- .more-icon {
- display: none;
- }
- .add-element-handler {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- display: flex;
- .handler-item {
- width: 32px;
- &:not(.group-btn):hover {
- background-color: #f1f1f1;
- }
- &.active {
- color: $themeColor;
- }
- &.group-btn {
- width: auto;
- margin-right: 5px;
- &:hover {
- background-color: #f3f3f3;
- }
- .icon, .arrow {
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .icon {
- width: 26px;
- padding: 0 2px;
- &:hover {
- background-color: #e9e9e9;
- }
- &.active {
- color: $themeColor;
- }
- }
- .arrow {
- font-size: 12px;
- &:hover {
- background-color: #e9e9e9;
- }
- }
- }
- }
- }
- .handler-item {
- height: 30px;
- font-size: 14px;
- margin: 0 2px;
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: $borderRadius;
- overflow: hidden;
- cursor: pointer;
- &.disable {
- opacity: .5;
- }
- }
- .left-handler, .right-handler {
- .handler-item {
- padding: 0 8px;
- &.active,
- &:not(.disable):hover {
- background-color: #f1f1f1;
- }
- }
- }
- .right-handler {
- display: flex;
- align-items: center;
- .text {
- display: inline-block;
- width: 40px;
- text-align: center;
- cursor: pointer;
- }
- .viewport-size {
- font-size: 13px;
- }
- }
- @media screen and (width <= 1200px) {
- .right-handler .text {
- display: none;
- }
- .more > .handler-item {
- display: none;
- }
- .more-icon {
- display: block;
- }
- }
- @media screen and (width <= 1000px) {
- .left-handler, .right-handler {
- display: none;
- }
- }
- </style>
|