index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <template>
  2. <div class="canvas-tool">
  3. <div class="left-handler">
  4. <div class="leftHandler-item" :class="{ disable: !canUndo }" v-tooltip="'撤销(Ctrl + Z)'" @click="undo()">
  5. <img src="./imgs/cx.png" alt="" />
  6. <div>撤销</div>
  7. </div>
  8. <div class="leftHandler-item" :class="{ disable: !canRedo }" v-tooltip="'恢复(Ctrl + Y)'" @click="redo()">
  9. <img src="./imgs/hf.png" alt="" />
  10. <div>恢复</div>
  11. </div>
  12. <div class="line"></div>
  13. <div class="leftHandler-item" :class="{ active: showNotesPanel }" v-tooltip="'批注'" @click="toggleNotesPanel()">
  14. <img src="./imgs/pz.png" alt="" />
  15. <div>批注</div>
  16. </div>
  17. <div class="leftHandler-item" :class="{ active: showSelectPanel }" v-tooltip="'选择'" @click="toggleSelectPanel()">
  18. <img src="./imgs/xz.png" alt="" />
  19. <div>选择</div>
  20. </div>
  21. <div class="leftHandler-item" :class="{ active: showSearchPanel }" v-tooltip="'查找/替换(Ctrl + F)'" @click="toggleSraechPanel()">
  22. <img src="./imgs/cz.png" alt="" />
  23. <div>查找</div>
  24. </div>
  25. <div class="line"></div>
  26. <Popover trigger="click" center>
  27. <template #content>
  28. <PopoverMenuItem @click="enterScreeningFromStart()">从头开始</PopoverMenuItem>
  29. <PopoverMenuItem @click="enterScreening()">从当前页开始</PopoverMenuItem>
  30. </template>
  31. <div class="arrow-btn">
  32. <img src="./imgs/bf.png" alt="" />
  33. <div>播放</div>
  34. </div>
  35. </Popover>
  36. </div>
  37. <div class="add-element-handler">
  38. <FileInput @change="files => insertImageElement(files)">
  39. <div class="handler-item">
  40. <img class="itemImg" src="./imgs/sctp.png" alt="" />
  41. <div class="tit">上传图片</div>
  42. </div>
  43. </FileInput>
  44. <ElUpload
  45. action=""
  46. :show-file-list="false"
  47. accept=".mp4,.avi,.flv,.mp3,.wav,.m4a"
  48. :http-request="
  49. (fileData: any) => {
  50. handleUpload(fileData)
  51. return undefined
  52. }
  53. "
  54. >
  55. <div class="handler-item">
  56. <img class="itemImg" src="./imgs/scysp.png" alt="" />
  57. <div class="tit">上传音视频</div>
  58. </div>
  59. </ElUpload>
  60. <div class="handler-item" @click="cloudCoachVisible = true">
  61. <img class="itemImg" src="./imgs/yp.png" alt="" />
  62. <div class="tit">乐谱</div>
  63. </div>
  64. <!-- <div class="handler-item">
  65. <img class="itemImg" src="./imgs/jzlx.png" alt="" />
  66. <div class="tit">节奏练习</div>
  67. </div>
  68. <div class="handler-item">
  69. <img class="itemImg" src="./imgs/tylx.png" alt="" />
  70. <div class="tit">听音练习</div>
  71. </div>
  72. <div class="handler-item">
  73. <img class="itemImg" src="./imgs/zyk.png" alt="" />
  74. <div class="tit">资源库</div>
  75. </div> -->
  76. <div class="handler-item" @click="drawText()" :class="{ active: creatingElement?.type === 'text' }">
  77. <img class="itemImg" src="./imgs/wz.png" alt="" />
  78. <Popover trigger="click" v-model:value="textTypeSelectVisible" :offset="10">
  79. <template #content>
  80. <PopoverMenuItem
  81. center
  82. @click="
  83. () => {
  84. drawText()
  85. textTypeSelectVisible = false
  86. }
  87. "
  88. ><IconTextRotationNone /> 横向文本框</PopoverMenuItem
  89. >
  90. <PopoverMenuItem
  91. center
  92. @click="
  93. () => {
  94. drawText(true)
  95. textTypeSelectVisible = false
  96. }
  97. "
  98. ><IconTextRotationDown /> 竖向文本框</PopoverMenuItem
  99. >
  100. </template>
  101. <div class="charTit tit">
  102. <div>文字</div>
  103. <img src="./imgs/jiantou.png" alt="" />
  104. </div>
  105. </Popover>
  106. </div>
  107. <div class="handler-item" :class="{ active: creatingCustomShape || creatingElement?.type === 'shape' }" @click="shapePoolVisible = true">
  108. <Popover trigger="click" v-model:value="shapePoolVisible" :offset="10">
  109. <template #content>
  110. <ShapePool @select="shape => drawShape(shape)" />
  111. </template>
  112. <img class="itemImg" src="./imgs/xz1.png" alt="" />
  113. </Popover>
  114. <Popover trigger="click" v-model:value="shapeMenuVisible" :offset="10" @click.stop>
  115. <template #content>
  116. <PopoverMenuItem
  117. center
  118. @click="
  119. () => {
  120. drawCustomShape()
  121. shapeMenuVisible = false
  122. }
  123. "
  124. >自由绘制</PopoverMenuItem
  125. >
  126. </template>
  127. <div class="charTit tit">
  128. <div>形状</div>
  129. <img src="./imgs/jiantou.png" alt="" />
  130. </div>
  131. </Popover>
  132. </div>
  133. <div class="handler-item" :class="{ active: creatingElement?.type === 'line' }" @click="linePoolVisible = true">
  134. <img class="itemImg" src="./imgs/xt.png" alt="" />
  135. <Popover trigger="click" v-model:value="linePoolVisible" :offset="10" @click.stop>
  136. <template #content>
  137. <LinePool @select="line => drawLine(line)" />
  138. </template>
  139. <div class="tit">线条</div>
  140. </Popover>
  141. </div>
  142. <div class="handler-item" @click="moreToolsVisible = true">
  143. <img class="itemImg" src="./imgs/gdgj.png" alt="" />
  144. <Popover trigger="click" v-model:value="moreToolsVisible" :offset="10" @click.stop>
  145. <template #content>
  146. <PopoverMenuItem @click="chartPoolVisible = true">
  147. <Popover trigger="click" v-model:value="chartPoolVisible" placement="right" :offsetOne="50" :offset="36">
  148. <template #content>
  149. <ChartPool
  150. @select="
  151. chart => {
  152. createChartElement(chart)
  153. chartPoolVisible = false
  154. }
  155. "
  156. />
  157. </template>
  158. <div class="menuItem">
  159. <img src="./imgs/tb.png" alt="" />
  160. <div class="tit">图表</div>
  161. </div>
  162. </Popover>
  163. </PopoverMenuItem>
  164. <PopoverMenuItem @click="tableGeneratorVisible = true">
  165. <div class="menuItem">
  166. <img src="./imgs/bg.png" alt="" />
  167. <div class="tit">表格</div>
  168. </div>
  169. </PopoverMenuItem>
  170. <PopoverMenuItem
  171. @click="
  172. () => {
  173. moreToolsVisible = false
  174. latexEditorVisible = true
  175. }
  176. "
  177. >
  178. <div class="menuItem">
  179. <img src="./imgs/gs.png" alt="" />
  180. <div class="tit">公式</div>
  181. </div>
  182. </PopoverMenuItem>
  183. </template>
  184. <div class="tit">更多工具</div>
  185. </Popover>
  186. <Popover trigger="click" v-model:value="tableGeneratorVisible" placement="right" :offsetOne="200" :offset="70">
  187. <template #content>
  188. <TableGenerator
  189. @close="tableGeneratorVisible = false"
  190. @insert="
  191. ({ row, col }) => {
  192. createTableElement(row, col)
  193. tableGeneratorVisible = false
  194. }
  195. "
  196. />
  197. </template>
  198. </Popover>
  199. </div>
  200. </div>
  201. <div class="right-handler">
  202. <IconMinus class="rightHandler-item" v-tooltip="'画布缩小(Ctrl + -)'" @click="scaleCanvas('-')" />
  203. <Popover trigger="click" v-model:value="canvasScaleVisible">
  204. <template #content>
  205. <PopoverMenuItem center v-for="item in canvasScalePresetList" :key="item" @click="applyCanvasPresetScale(item)"
  206. >{{ item }}%</PopoverMenuItem
  207. >
  208. <PopoverMenuItem center @click="resetCanvas()">适应屏幕</PopoverMenuItem>
  209. </template>
  210. <div class="text" :class="{ canvasScaleVisible: canvasScaleVisible }">{{ canvasScalePercentage }}</div>
  211. </Popover>
  212. <IconPlus class="rightHandler-item" v-tooltip="'画布放大(Ctrl + =)'" @click="scaleCanvas('+')" />
  213. <IconFullScreen class="rightHandler-item resetCanvas" v-tooltip="'适应屏幕(Ctrl + 0)'" @click="resetCanvas()" />
  214. </div>
  215. <Modal v-model:visible="latexEditorVisible" :width="880">
  216. <LaTeXEditor
  217. @close="latexEditorVisible = false"
  218. @update="
  219. data => {
  220. createLatexElement(data)
  221. latexEditorVisible = false
  222. }
  223. "
  224. />
  225. </Modal>
  226. <!--<Modal
  227. :contentStyle="{
  228. width: '70%',
  229. minWidth: '1200px',
  230. height: '86%',
  231. boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
  232. borderRadius: '16px',
  233. border: '1px solid #DEDEDE',
  234. padding: '0'
  235. }"
  236. v-model:visible="cloudCoachVisible"
  237. >
  238. <cloudCoachList
  239. @update="handleCloudCoach"
  240. @close="
  241. () => {
  242. cloudCoachVisible = false
  243. }
  244. "
  245. />
  246. </Modal> -->
  247. <Modal
  248. :contentStyle="{
  249. width: '800px',
  250. height: '600px',
  251. boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
  252. borderRadius: '16px',
  253. border: '1px solid #DEDEDE',
  254. padding: '0'
  255. }"
  256. v-model:visible="cloudCoachVisible"
  257. >
  258. <cloudCoachList
  259. @update="handleCloudCoach"
  260. @close="
  261. () => {
  262. cloudCoachVisible = false
  263. }
  264. "
  265. />
  266. </Modal>
  267. </div>
  268. </template>
  269. <script lang="ts" setup>
  270. import { ref } from "vue"
  271. import { storeToRefs } from "pinia"
  272. import { useMainStore, useSnapshotStore } from "@/store"
  273. import { getImageDataURL } from "@/utils/image"
  274. import type { ShapePoolItem } from "@/configs/shapes"
  275. import type { LinePoolItem } from "@/configs/lines"
  276. import useScaleCanvas from "@/hooks/useScaleCanvas"
  277. import useHistorySnapshot from "@/hooks/useHistorySnapshot"
  278. import useCreateElement from "@/hooks/useCreateElement"
  279. import useScreening from "@/hooks/useScreening"
  280. import ShapePool from "./ShapePool.vue"
  281. import LinePool from "./LinePool.vue"
  282. import ChartPool from "./ChartPool.vue"
  283. import TableGenerator from "./TableGenerator.vue"
  284. import LaTeXEditor from "@/components/LaTeXEditor/index.vue"
  285. import FileInput from "@/components/FileInput.vue"
  286. import Modal from "@/components/Modal.vue"
  287. import Popover from "@/components/Popover.vue"
  288. import PopoverMenuItem from "@/components/PopoverMenuItem.vue"
  289. import { ElUpload, ElMessage, type UploadRequestOptions } from "element-plus"
  290. import cloudCoachList from "@/views/components/element/cloudCoachElement/cloudCoachList"
  291. import fileUpload from "@/utils/oss-file-upload"
  292. const mainStore = useMainStore()
  293. const { creatingElement, creatingCustomShape, showSelectPanel, showSearchPanel, showNotesPanel } = storeToRefs(mainStore)
  294. const { canUndo, canRedo } = storeToRefs(useSnapshotStore())
  295. const { redo, undo } = useHistorySnapshot()
  296. const { scaleCanvas, setCanvasScalePercentage, resetCanvas, canvasScalePercentage } = useScaleCanvas()
  297. const canvasScalePresetList = [200, 150, 125, 100, 75, 50]
  298. const canvasScaleVisible = ref(false)
  299. const { enterScreening, enterScreeningFromStart } = useScreening()
  300. const applyCanvasPresetScale = (value: number) => {
  301. setCanvasScalePercentage(value)
  302. canvasScaleVisible.value = false
  303. }
  304. const {
  305. createImageElement,
  306. createChartElement,
  307. createTableElement,
  308. createLatexElement,
  309. createVideoElement,
  310. createAudioElement,
  311. createCloudCoachElement
  312. } = useCreateElement()
  313. const insertImageElement = (files: FileList) => {
  314. const imageFile = files[0]
  315. if (!imageFile) return
  316. getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
  317. }
  318. const shapePoolVisible = ref(false)
  319. const linePoolVisible = ref(false)
  320. const chartPoolVisible = ref(false)
  321. const tableGeneratorVisible = ref(false)
  322. const latexEditorVisible = ref(false)
  323. const textTypeSelectVisible = ref(false)
  324. const shapeMenuVisible = ref(false)
  325. const cloudCoachVisible = ref(false)
  326. const moreToolsVisible = ref(false)
  327. // 音视频
  328. function handleUpload(fileData: UploadRequestOptions) {
  329. const type = /\.(mp3|wav|m4a)$/i.test(fileData.file.name) ? "audio" : "video"
  330. fileUpload(fileData.file.name, fileData.file)
  331. .then(res => {
  332. if (type === "audio") {
  333. createAudioElement(res)
  334. } else {
  335. createVideoElement(res)
  336. }
  337. })
  338. .catch(() => {
  339. ElMessage({
  340. showClose: true,
  341. message: "上传失败!",
  342. type: "error"
  343. })
  344. })
  345. }
  346. // 处理云教练创建
  347. function handleCloudCoach(id: string) {
  348. createCloudCoachElement(id)
  349. cloudCoachVisible.value = false
  350. }
  351. // 绘制文字范围
  352. const drawText = (vertical = false) => {
  353. mainStore.setCreatingElement({
  354. type: "text",
  355. vertical
  356. })
  357. }
  358. // 绘制形状范围
  359. const drawShape = (shape: ShapePoolItem) => {
  360. mainStore.setCreatingElement({
  361. type: "shape",
  362. data: shape
  363. })
  364. shapePoolVisible.value = false
  365. }
  366. // 绘制自定义任意多边形
  367. const drawCustomShape = () => {
  368. mainStore.setCreatingCustomShapeState(true)
  369. shapePoolVisible.value = false
  370. }
  371. // 绘制线条路径
  372. const drawLine = (line: LinePoolItem) => {
  373. mainStore.setCreatingElement({
  374. type: "line",
  375. data: line
  376. })
  377. linePoolVisible.value = false
  378. }
  379. // 打开选择面板
  380. const toggleSelectPanel = () => {
  381. mainStore.setSelectPanelState(!showSelectPanel.value)
  382. }
  383. // 打开搜索替换面板
  384. const toggleSraechPanel = () => {
  385. mainStore.setSearchPanelState(!showSearchPanel.value)
  386. }
  387. // 打开批注面板
  388. const toggleNotesPanel = () => {
  389. mainStore.setNotesPanelState(!showNotesPanel.value)
  390. }
  391. </script>
  392. <style lang="scss" scoped>
  393. .canvas-tool {
  394. position: relative;
  395. border-bottom: 1px solid $borderColor;
  396. background-color: #fff;
  397. display: flex;
  398. justify-content: space-between;
  399. padding: 0 24px;
  400. user-select: none;
  401. }
  402. .left-handler {
  403. margin-left: -6px;
  404. display: flex;
  405. align-items: center;
  406. .leftHandler-item {
  407. display: flex;
  408. flex-direction: column;
  409. justify-content: center;
  410. align-items: center;
  411. margin-right: 8px;
  412. padding: 4px 6px;
  413. cursor: pointer;
  414. &.disable {
  415. opacity: 0.5;
  416. cursor: not-allowed;
  417. background-color: transparent !important;
  418. }
  419. &:hover,
  420. &.active {
  421. background: rgba(34, 71, 133, 0.08);
  422. border-radius: 6px;
  423. }
  424. & > img {
  425. width: 20px;
  426. height: 20px;
  427. }
  428. & > div {
  429. margin-top: 4px;
  430. font-weight: 400;
  431. font-size: 12px;
  432. color: #131415;
  433. line-height: 17px;
  434. }
  435. }
  436. .line {
  437. margin-left: 6px;
  438. margin-right: 14px;
  439. width: 1px;
  440. height: calc(100% - 20px);
  441. background-color: $borderColor;
  442. }
  443. .arrow-btn {
  444. margin-left: 6px;
  445. display: flex;
  446. align-items: center;
  447. justify-content: center;
  448. width: 88px;
  449. height: 32px;
  450. background: linear-gradient(312deg, #1b7af8 0%, #3cbbff 100%);
  451. border-radius: 6px;
  452. font-weight: 600;
  453. font-size: 14px;
  454. color: #ffffff;
  455. line-height: 20px;
  456. cursor: pointer;
  457. &:hover {
  458. opacity: 0.8;
  459. }
  460. & > img {
  461. margin-right: 8px;
  462. width: 12px;
  463. height: 14px;
  464. }
  465. }
  466. }
  467. .add-element-handler {
  468. position: absolute;
  469. top: 50%;
  470. left: 50%;
  471. transform: translate(-50%, -50%);
  472. display: flex;
  473. .handler-item {
  474. display: flex;
  475. flex-direction: column;
  476. align-items: center;
  477. justify-content: center;
  478. cursor: pointer;
  479. width: 68px;
  480. height: 53px;
  481. &:hover,
  482. &.active {
  483. background: rgba(34, 71, 133, 0.08);
  484. border-radius: 6px;
  485. }
  486. .itemImg {
  487. width: 20px;
  488. height: 20px;
  489. }
  490. .tit {
  491. margin-top: 4px;
  492. font-weight: 400;
  493. font-size: 12px;
  494. color: #131415;
  495. line-height: 17px;
  496. }
  497. .charTit {
  498. display: flex;
  499. align-items: center;
  500. padding: 0 6px;
  501. border-radius: 4px;
  502. &:hover {
  503. background: rgba(34, 71, 133, 0.1);
  504. }
  505. > img {
  506. margin-left: 4px;
  507. width: 7px;
  508. height: 4px;
  509. }
  510. }
  511. }
  512. }
  513. .menuItem {
  514. display: flex;
  515. align-items: center;
  516. & > img {
  517. width: 20px;
  518. height: 20px;
  519. }
  520. & > .tit {
  521. margin-left: 16px;
  522. font-weight: 400;
  523. font-size: 14px;
  524. color: #333333;
  525. }
  526. }
  527. .right-handler {
  528. display: flex;
  529. align-items: center;
  530. .text {
  531. margin: 0 8px;
  532. width: 57px;
  533. height: 32px;
  534. line-height: 32px;
  535. text-align: center;
  536. cursor: pointer;
  537. &:hover,
  538. &.canvasScaleVisible {
  539. border-radius: 6px;
  540. background-color: rgba(34, 71, 133, 0.08);
  541. }
  542. }
  543. .rightHandler-item {
  544. font-size: 20px;
  545. color: #131415;
  546. cursor: pointer;
  547. &:hover {
  548. opacity: 0.5;
  549. }
  550. }
  551. .resetCanvas {
  552. margin-left: 20px;
  553. }
  554. }
  555. </style>