mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加批注功能
This commit is contained in:
parent
aeee2b25d2
commit
0d0e729944
@ -67,6 +67,7 @@ npm run dev
|
|||||||
- 元素动画(入场、退场、强调)
|
- 元素动画(入场、退场、强调)
|
||||||
- 选择面板(隐藏元素、层级排序、元素命名)
|
- 选择面板(隐藏元素、层级排序、元素命名)
|
||||||
- 查找/替换
|
- 查找/替换
|
||||||
|
- 批注
|
||||||
### 幻灯片元素编辑
|
### 幻灯片元素编辑
|
||||||
- 元素添加、删除
|
- 元素添加、删除
|
||||||
- 元素复制粘贴
|
- 元素复制粘贴
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
class="moveable-panel"
|
class="moveable-panel"
|
||||||
ref="moveablePanelRef"
|
ref="moveablePanelRef"
|
||||||
:style="{
|
:style="{
|
||||||
width: width + 'px',
|
width: w + 'px',
|
||||||
height: height ? height + 'px' : 'auto',
|
height: h ? h + 'px' : 'auto',
|
||||||
left: x + 'px',
|
left: x + 'px',
|
||||||
top: y + 'px',
|
top: y + 'px',
|
||||||
}"
|
}"
|
||||||
@ -23,6 +23,8 @@
|
|||||||
<div v-else class="content" @mousedown="$event => startMove($event)">
|
<div v-else class="content" @mousedown="$event => startMove($event)">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="resizer" v-if="resizeable" @mousedown="$event => startResize($event)"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -32,15 +34,25 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
minWidth?: number
|
||||||
|
minHeight?: number
|
||||||
|
maxWidth?: number
|
||||||
|
maxHeight?: number
|
||||||
left?: number
|
left?: number
|
||||||
top?: number
|
top?: number
|
||||||
title?: string
|
title?: string
|
||||||
moveable?: boolean
|
moveable?: boolean
|
||||||
|
resizeable?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
|
minWidth: 20,
|
||||||
|
minHeight: 20,
|
||||||
|
maxWidth: 500,
|
||||||
|
maxHeight: 500,
|
||||||
left: 10,
|
left: 10,
|
||||||
top: 10,
|
top: 10,
|
||||||
title: '',
|
title: '',
|
||||||
moveable: true,
|
moveable: true,
|
||||||
|
resizeable: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -49,12 +61,14 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
|
const w = ref(0)
|
||||||
|
const h = ref(0)
|
||||||
const moveablePanelRef = ref<HTMLElement>()
|
const moveablePanelRef = ref<HTMLElement>()
|
||||||
const realHeight = computed(() => {
|
const realHeight = computed(() => {
|
||||||
if (!props.height) {
|
if (!h.value) {
|
||||||
return moveablePanelRef.value?.clientHeight || 0
|
return moveablePanelRef.value?.clientHeight || 0
|
||||||
}
|
}
|
||||||
return props.height
|
return h.value
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -63,6 +77,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
if (props.top >= 0) y.value = props.top
|
if (props.top >= 0) y.value = props.top
|
||||||
else y.value = document.body.clientHeight + props.top - realHeight.value
|
else y.value = document.body.clientHeight + props.top - realHeight.value
|
||||||
|
|
||||||
|
w.value = props.width
|
||||||
|
h.value = props.height
|
||||||
})
|
})
|
||||||
|
|
||||||
const startMove = (e: MouseEvent) => {
|
const startMove = (e: MouseEvent) => {
|
||||||
@ -90,7 +107,7 @@ const startMove = (e: MouseEvent) => {
|
|||||||
|
|
||||||
if (left < 0) left = 0
|
if (left < 0) left = 0
|
||||||
if (top < 0) top = 0
|
if (top < 0) top = 0
|
||||||
if (left + props.width > windowWidth) left = windowWidth - props.width
|
if (left + w.value > windowWidth) left = windowWidth - w.value
|
||||||
if (top + realHeight.value > clientHeight) top = clientHeight - realHeight.value
|
if (top + realHeight.value > clientHeight) top = clientHeight - realHeight.value
|
||||||
|
|
||||||
x.value = left
|
x.value = left
|
||||||
@ -103,6 +120,42 @@ const startMove = (e: MouseEvent) => {
|
|||||||
document.onmouseup = null
|
document.onmouseup = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startResize = (e: MouseEvent) => {
|
||||||
|
if (!props.resizeable) return
|
||||||
|
|
||||||
|
let isMouseDown = true
|
||||||
|
|
||||||
|
const startPageX = e.pageX
|
||||||
|
const startPageY = e.pageY
|
||||||
|
|
||||||
|
const originWidth = w.value
|
||||||
|
const originHeight = h.value
|
||||||
|
|
||||||
|
document.onmousemove = e => {
|
||||||
|
if (!isMouseDown) return
|
||||||
|
|
||||||
|
const moveX = e.pageX - startPageX
|
||||||
|
const moveY = e.pageY - startPageY
|
||||||
|
|
||||||
|
let width = originWidth + moveX
|
||||||
|
let height = originHeight + moveY
|
||||||
|
|
||||||
|
if (width < props.minWidth) width = props.minWidth
|
||||||
|
if (height < props.minHeight) height = props.minHeight
|
||||||
|
if (width > props.maxWidth) width = props.maxWidth
|
||||||
|
if (height > props.maxHeight) height = props.maxHeight
|
||||||
|
|
||||||
|
w.value = width
|
||||||
|
h.value = height
|
||||||
|
}
|
||||||
|
document.onmouseup = () => {
|
||||||
|
isMouseDown = false
|
||||||
|
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -116,6 +169,27 @@ const startMove = (e: MouseEvent) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
.resizer {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: se-resize;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -4px;
|
||||||
|
right: -4px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transform-origin: center;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
border-left-color: #e1e1e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
.header {
|
.header {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -10,7 +10,12 @@
|
|||||||
:value="value"
|
:value="value"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
:style="{
|
||||||
|
padding: padding ? `${padding}px` : '10px',
|
||||||
|
}"
|
||||||
@input="$event => handleInput($event)"
|
@input="$event => handleInput($event)"
|
||||||
|
@focus="$event => emit('focus', $event)"
|
||||||
|
@blur="$event => emit('blur', $event)"
|
||||||
></textarea>
|
></textarea>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,6 +25,7 @@ import { ref } from 'vue'
|
|||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
value: string
|
value: string
|
||||||
rows?: number
|
rows?: number
|
||||||
|
padding?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
resizable?: boolean
|
resizable?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@ -32,6 +38,8 @@ withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'update:value', payload: string): void
|
(event: 'update:value', payload: string): void
|
||||||
|
(event: 'focus', payload: FocusEvent): void
|
||||||
|
(event: 'blur', payload: FocusEvent): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const handleInput = (e: Event) => {
|
const handleInput = (e: Event) => {
|
||||||
|
@ -120,6 +120,8 @@ import {
|
|||||||
CheckOne,
|
CheckOne,
|
||||||
CloseOne,
|
CloseOne,
|
||||||
Info,
|
Info,
|
||||||
|
Comment,
|
||||||
|
User,
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
|
||||||
export interface Icons {
|
export interface Icons {
|
||||||
@ -245,6 +247,8 @@ export const icons: Icons = {
|
|||||||
IconCheckOne: CheckOne,
|
IconCheckOne: CheckOne,
|
||||||
IconCloseOne: CloseOne,
|
IconCloseOne: CloseOne,
|
||||||
IconInfo: Info,
|
IconInfo: Info,
|
||||||
|
IconComment: Comment,
|
||||||
|
IconUser: User,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -37,6 +37,7 @@ export interface MainState {
|
|||||||
shapeFormatPainter: ShapeFormatPainter | null
|
shapeFormatPainter: ShapeFormatPainter | null
|
||||||
showSelectPanel: boolean
|
showSelectPanel: boolean
|
||||||
showSearchPanel: boolean
|
showSearchPanel: boolean
|
||||||
|
showNotesPanel: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
||||||
@ -71,6 +72,7 @@ export const useMainStore = defineStore('main', {
|
|||||||
shapeFormatPainter: null, // 形状格式刷
|
shapeFormatPainter: null, // 形状格式刷
|
||||||
showSelectPanel: false, // 打开选择面板
|
showSelectPanel: false, // 打开选择面板
|
||||||
showSearchPanel: false, // 打开查找替换面板
|
showSearchPanel: false, // 打开查找替换面板
|
||||||
|
showNotesPanel: false, // 打开批注面板
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -196,5 +198,9 @@ export const useMainStore = defineStore('main', {
|
|||||||
setSearchPanelState(show: boolean) {
|
setSearchPanelState(show: boolean) {
|
||||||
this.showSearchPanel = show
|
this.showSearchPanel = show
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setNotesPanelState(show: boolean) {
|
||||||
|
this.showNotesPanel = show
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
@ -656,6 +656,22 @@ export interface SlideBackground {
|
|||||||
|
|
||||||
export type TurningMode = 'no' | 'fade' | 'slideX' | 'slideY' | 'random' | 'slideX3D' | 'slideY3D' | 'rotate' | 'scaleY' | 'scaleX' | 'scale' | 'scaleReverse'
|
export type TurningMode = 'no' | 'fade' | 'slideX' | 'slideY' | 'random' | 'slideX3D' | 'slideY3D' | 'rotate' | 'scaleY' | 'scaleX' | 'scale' | 'scaleReverse'
|
||||||
|
|
||||||
|
export interface NoteReply {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
time: number
|
||||||
|
user: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Note {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
time: number
|
||||||
|
user: string
|
||||||
|
elId?: string
|
||||||
|
replies?: NoteReply[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 幻灯片页面
|
* 幻灯片页面
|
||||||
*
|
*
|
||||||
@ -663,6 +679,8 @@ export type TurningMode = 'no' | 'fade' | 'slideX' | 'slideY' | 'random' | 'slid
|
|||||||
*
|
*
|
||||||
* elements: 元素集合
|
* elements: 元素集合
|
||||||
*
|
*
|
||||||
|
* notes: 批注
|
||||||
|
*
|
||||||
* remark?: 备注
|
* remark?: 备注
|
||||||
*
|
*
|
||||||
* background?: 页面背景
|
* background?: 页面背景
|
||||||
@ -674,6 +692,7 @@ export type TurningMode = 'no' | 'fade' | 'slideX' | 'slideY' | 'random' | 'slid
|
|||||||
export interface Slide {
|
export interface Slide {
|
||||||
id: string
|
id: string
|
||||||
elements: PPTElement[]
|
elements: PPTElement[]
|
||||||
|
notes: Note[]
|
||||||
remark?: string
|
remark?: string
|
||||||
background?: SlideBackground
|
background?: SlideBackground
|
||||||
animations?: PPTAnimation[]
|
animations?: PPTAnimation[]
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
<div class="left-handler">
|
<div class="left-handler">
|
||||||
<IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="'撤销'" @click="undo()" />
|
<IconBack class="handler-item" :class="{ 'disable': !canUndo }" v-tooltip="'撤销'" @click="undo()" />
|
||||||
<IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="'重做'" @click="redo()" />
|
<IconNext class="handler-item" :class="{ 'disable': !canRedo }" v-tooltip="'重做'" @click="redo()" />
|
||||||
<Divider type="vertical" style="height: 20px;" />
|
<div class="more">
|
||||||
<IconMoveOne class="handler-item" :class="{ 'active': showSelectPanel }" v-tooltip="'选择窗格'" @click="toggleSelectPanel()" />
|
<Divider type="vertical" style="height: 20px;" />
|
||||||
<IconSearch class="handler-item" :class="{ 'active': showSearchPanel }" v-tooltip="'查找/替换'" @click="toggleSraechPanel()" />
|
<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="'查找/替换'" @click="toggleSraechPanel()" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add-element-handler">
|
<div class="add-element-handler">
|
||||||
@ -116,7 +119,7 @@ import Popover from '@/components/Popover.vue'
|
|||||||
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
|
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel } = storeToRefs(mainStore)
|
const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
|
||||||
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
|
const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
|
||||||
|
|
||||||
const { redo, undo } = useHistorySnapshot()
|
const { redo, undo } = useHistorySnapshot()
|
||||||
@ -199,6 +202,11 @@ const toggleSelectPanel = () => {
|
|||||||
const toggleSraechPanel = () => {
|
const toggleSraechPanel = () => {
|
||||||
mainStore.setSearchPanelState(!showSearchPanel.value)
|
mainStore.setSearchPanelState(!showSearchPanel.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开批注面板
|
||||||
|
const toggleNotesPanel = () => {
|
||||||
|
mainStore.setNotesPanelState(!showNotesPanel.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -212,7 +220,7 @@ const toggleSraechPanel = () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.left-handler {
|
.left-handler, .more {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -311,8 +319,11 @@ const toggleSraechPanel = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 1024px) {
|
@media screen and (width <= 1200px) {
|
||||||
.text {
|
.right-handler .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.more {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
323
src/views/Editor/NotesPanel.vue
Normal file
323
src/views/Editor/NotesPanel.vue
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
<template>
|
||||||
|
<MoveablePanel
|
||||||
|
class="notes-panel"
|
||||||
|
:width="300"
|
||||||
|
:height="560"
|
||||||
|
:title="`幻灯片${slideIndex + 1}的批注`"
|
||||||
|
:left="-270"
|
||||||
|
:top="90"
|
||||||
|
:minWidth="300"
|
||||||
|
:minHeight="400"
|
||||||
|
:maxWidth="480"
|
||||||
|
:maxHeight="780"
|
||||||
|
resizeable
|
||||||
|
@close="close()"
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<div class="notes">
|
||||||
|
<div class="note" :class="{ 'active': activeNoteId === note.id }" v-for="note in notes" :key="note.id" @click="handleClickNote(note)">
|
||||||
|
<div class="header note-header">
|
||||||
|
<div class="user">
|
||||||
|
<div class="avatar"><IconUser /></div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="username">{{ note.user }}</div>
|
||||||
|
<div class="time">{{ new Date(note.time).toLocaleString() }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<div class="btn reply" @click="replyNoteId = note.id">回复</div>
|
||||||
|
<div class="btn delete" @click.stop="deleteNote(note.id)">删除</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">{{ note.content }}</div>
|
||||||
|
<div class="replies" v-if="note.replies?.length">
|
||||||
|
<div class="reply-item" v-for="reply in note.replies" :key="reply.id">
|
||||||
|
<div class="header reply-header">
|
||||||
|
<div class="user">
|
||||||
|
<div class="avatar"><IconUser /></div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="username">{{ reply.user }}</div>
|
||||||
|
<div class="time">{{ new Date(reply.time).toLocaleString() }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<div class="btn delete" @click.stop="deleteReply(note.id, reply.id)">删除</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">{{ reply.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="note-reply" v-if="replyNoteId === note.id">
|
||||||
|
<TextArea :padding="6" v-model:value="replyContent" placeholder="输入回复内容" :rows="1" />
|
||||||
|
<div class="reply-btns">
|
||||||
|
<Button class="btn" size="small" @click="replyNoteId = ''">取消</Button>
|
||||||
|
<Button class="btn" size="small" type="primary" @click="createNoteReply()">回复</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty" v-if="!notes.length">本页暂无批注</div>
|
||||||
|
</div>
|
||||||
|
<div class="send">
|
||||||
|
<TextArea
|
||||||
|
ref="textAreaRef"
|
||||||
|
v-model:value="content"
|
||||||
|
:padding="6"
|
||||||
|
:placeholder="`输入批注(为${handleElementId ? '选中元素' : '当前页幻灯片' })`"
|
||||||
|
:rows="2"
|
||||||
|
@focus="replyNoteId = ''; activeNoteId = ''"
|
||||||
|
/>
|
||||||
|
<div class="footer">
|
||||||
|
<Button class="btn" v-tooltip="'清空本页批注'" style="flex: 1" @click="clear()"><IconDelete /></Button>
|
||||||
|
<Button type="primary" class="btn" style="flex: 12" @click="createNote()">添加批注</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MoveablePanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { useMainStore, useSlidesStore } from '@/store'
|
||||||
|
import type { Note } from '@/types/slides'
|
||||||
|
|
||||||
|
import MoveablePanel from '@/components/MoveablePanel.vue'
|
||||||
|
import TextArea from '@/components/TextArea.vue'
|
||||||
|
import Button from '@/components/Button.vue'
|
||||||
|
|
||||||
|
const slidesStore = useSlidesStore()
|
||||||
|
const mainStore = useMainStore()
|
||||||
|
const { slideIndex, currentSlide } = storeToRefs(slidesStore)
|
||||||
|
const { handleElementId } = storeToRefs(mainStore)
|
||||||
|
|
||||||
|
const content = ref('')
|
||||||
|
const replyContent = ref('')
|
||||||
|
const notes = computed(() => currentSlide.value?.notes || [])
|
||||||
|
const activeNoteId = ref('')
|
||||||
|
const replyNoteId = ref('')
|
||||||
|
const textAreaRef = ref<InstanceType<typeof TextArea>>()
|
||||||
|
|
||||||
|
const createNote = () => {
|
||||||
|
if (!content.value) {
|
||||||
|
if (textAreaRef.value) textAreaRef.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNote: Note = {
|
||||||
|
id: nanoid(),
|
||||||
|
content: content.value,
|
||||||
|
time: new Date().getTime(),
|
||||||
|
user: '测试用户',
|
||||||
|
}
|
||||||
|
if (handleElementId.value) newNote.elId = handleElementId.value
|
||||||
|
|
||||||
|
const newNotes = [
|
||||||
|
...notes.value,
|
||||||
|
newNote,
|
||||||
|
]
|
||||||
|
slidesStore.updateSlide({ notes: newNotes })
|
||||||
|
|
||||||
|
content.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNote = (id: string) => {
|
||||||
|
const newNotes = notes.value.filter(note => note.id !== id)
|
||||||
|
slidesStore.updateSlide({ notes: newNotes })
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNoteReply = () => {
|
||||||
|
if (!replyContent.value) return
|
||||||
|
|
||||||
|
const currentNote = notes.value.find(note => note.id === replyNoteId.value)
|
||||||
|
if (!currentNote) return
|
||||||
|
|
||||||
|
const newReplies = [
|
||||||
|
...currentNote.replies || [],
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
content: replyContent.value,
|
||||||
|
time: new Date().getTime(),
|
||||||
|
user: '测试用户',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const newNote: Note = {
|
||||||
|
...currentNote,
|
||||||
|
replies: newReplies,
|
||||||
|
}
|
||||||
|
const newNotes = notes.value.map(note => note.id === replyNoteId.value ? newNote : note)
|
||||||
|
slidesStore.updateSlide({ notes: newNotes })
|
||||||
|
|
||||||
|
replyContent.value = ''
|
||||||
|
replyNoteId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteReply = (noteId: string, replyId: string) => {
|
||||||
|
const currentNote = notes.value.find(note => note.id === noteId)
|
||||||
|
if (!currentNote || !currentNote.replies) return
|
||||||
|
|
||||||
|
const newReplies = currentNote.replies.filter(reply => reply.id !== replyId)
|
||||||
|
const newNote: Note = {
|
||||||
|
...currentNote,
|
||||||
|
replies: newReplies,
|
||||||
|
}
|
||||||
|
const newNotes = notes.value.map(note => note.id === noteId ? newNote : note)
|
||||||
|
slidesStore.updateSlide({ notes: newNotes })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickNote = (note: Note) => {
|
||||||
|
activeNoteId.value = note.id
|
||||||
|
|
||||||
|
if (note.elId) {
|
||||||
|
const elIds = currentSlide.value.elements.map(item => item.id)
|
||||||
|
if (elIds.includes(note.elId)) {
|
||||||
|
mainStore.setActiveElementIdList([note.elId])
|
||||||
|
}
|
||||||
|
else mainStore.setActiveElementIdList([])
|
||||||
|
}
|
||||||
|
else mainStore.setActiveElementIdList([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
slidesStore.updateSlide({ notes: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
mainStore.setNotesPanelState(false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.notes-panel {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.notes {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0 -10px;
|
||||||
|
padding: 2px 12px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
& + .note {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.btns {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #42ba97;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btns {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-left: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.replies {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.reply-item {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.note-reply {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.reply-btns {
|
||||||
|
margin-top: 5px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.send {
|
||||||
|
height: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.btn + .btn {
|
||||||
|
margin-left: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<SelectPanel v-if="showSelectPanel" />
|
<SelectPanel v-if="showSelectPanel" />
|
||||||
<SearchPanel v-if="showSearchPanel" />
|
<SearchPanel v-if="showSearchPanel" />
|
||||||
|
<NotesPanel v-if="showNotesPanel" />
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
:visible="!!dialogForExport"
|
:visible="!!dialogForExport"
|
||||||
@ -44,10 +45,11 @@ import Remark from './Remark/index.vue'
|
|||||||
import ExportDialog from './ExportDialog/index.vue'
|
import ExportDialog from './ExportDialog/index.vue'
|
||||||
import SelectPanel from './SelectPanel.vue'
|
import SelectPanel from './SelectPanel.vue'
|
||||||
import SearchPanel from './SearchPanel.vue'
|
import SearchPanel from './SearchPanel.vue'
|
||||||
|
import NotesPanel from './NotesPanel.vue'
|
||||||
import Modal from '@/components/Modal.vue'
|
import Modal from '@/components/Modal.vue'
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
const { dialogForExport, showSelectPanel, showSearchPanel } = storeToRefs(mainStore)
|
const { dialogForExport, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
|
||||||
const closeExportDialog = () => mainStore.setDialogForExport('')
|
const closeExportDialog = () => mainStore.setDialogForExport('')
|
||||||
|
|
||||||
const remarkHeight = ref(40)
|
const remarkHeight = ref(40)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user