Compare commits
No commits in common. "6e3efe78438990656c7eb551b77989dbd4ba8ac2" and "2f3a2b7aaeae6e513ae8aa09ab92c2eaf6a0e1aa" have entirely different histories.
6e3efe7843
...
2f3a2b7aae
64
package.json
|
@ -21,6 +21,11 @@
|
||||||
"build:linux": "npm run build && electron-builder --linux"
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"@electron/remote": "^2.1.2",
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||||
"@antv/x6": "^2.18.1",
|
"@antv/x6": "^2.18.1",
|
||||||
"@antv/x6-plugin-clipboard": "^2.1.6",
|
"@antv/x6-plugin-clipboard": "^2.1.6",
|
||||||
"@antv/x6-plugin-dnd": "^2.1.1",
|
"@antv/x6-plugin-dnd": "^2.1.1",
|
||||||
|
@ -29,11 +34,6 @@
|
||||||
"@antv/x6-plugin-selection": "^2.2.2",
|
"@antv/x6-plugin-selection": "^2.2.2",
|
||||||
"@antv/x6-plugin-snapline": "^2.1.7",
|
"@antv/x6-plugin-snapline": "^2.1.7",
|
||||||
"@antv/x6-plugin-transform": "^2.1.8",
|
"@antv/x6-plugin-transform": "^2.1.8",
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
|
||||||
"@electron/remote": "^2.1.2",
|
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
|
||||||
"@vue-office/docx": "^1.6.2",
|
"@vue-office/docx": "^1.6.2",
|
||||||
"@vue-office/excel": "^1.7.11",
|
"@vue-office/excel": "^1.7.11",
|
||||||
"@vue-office/pdf": "^2.0.2",
|
"@vue-office/pdf": "^2.0.2",
|
||||||
|
@ -53,8 +53,6 @@
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jsondiffpatch": "0.6.0",
|
"jsondiffpatch": "0.6.0",
|
||||||
"less": "^4.2.0",
|
|
||||||
"less-loader": "^7.3.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-addon-api": "^8.1.0",
|
"node-addon-api": "^8.1.0",
|
||||||
"pdfjs-dist": "4.4.168",
|
"pdfjs-dist": "4.4.168",
|
||||||
|
@ -64,39 +62,11 @@
|
||||||
"vite-plugin-electron": "^0.28.8",
|
"vite-plugin-electron": "^0.28.8",
|
||||||
"vue-qr": "^4.0.9",
|
"vue-qr": "^4.0.9",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0",
|
||||||
"whiteboard_lyc": "^0.1.3",
|
|
||||||
"xgplayer": "^3.0.19",
|
"xgplayer": "^3.0.19",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
|
"less": "^4.2.0",
|
||||||
"@icon-park/vue-next": "^1.4.2",
|
"less-loader": "^7.3.0",
|
||||||
"animate.css": "^4.1.1",
|
"whiteboard_lyc": "^0.1.3"
|
||||||
"clipboard": "^2.0.11",
|
|
||||||
"dexie": "3.0.3",
|
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"hfmath": "^0.0.2",
|
|
||||||
"html-to-image": "^1.11.11",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"nanoid": "^5.0.7",
|
|
||||||
"number-precision": "^1.6.0",
|
|
||||||
"pptxgenjs": "^3.12.0",
|
|
||||||
"pptxtojson": "^1.0.3",
|
|
||||||
"prosemirror-commands": "^1.6.0",
|
|
||||||
"prosemirror-dropcursor": "^1.8.1",
|
|
||||||
"prosemirror-gapcursor": "^1.3.2",
|
|
||||||
"prosemirror-history": "^1.3.2",
|
|
||||||
"prosemirror-inputrules": "^1.4.0",
|
|
||||||
"prosemirror-keymap": "^1.2.2",
|
|
||||||
"prosemirror-model": "^1.22.2",
|
|
||||||
"prosemirror-schema-basic": "^1.2.3",
|
|
||||||
"prosemirror-schema-list": "^1.4.1",
|
|
||||||
"prosemirror-state": "^1.4.3",
|
|
||||||
"prosemirror-view": "^1.33.9",
|
|
||||||
"svg-arc-to-cubic-bezier": "^3.2.0",
|
|
||||||
"svg-pathdata": "^7.1.0",
|
|
||||||
"tinycolor2": "^1.6.0",
|
|
||||||
"tippy.js": "^6.3.7",
|
|
||||||
"vue": "^3.4.34",
|
|
||||||
"vuedraggable": "^4.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
|
@ -114,22 +84,6 @@
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.1",
|
||||||
"vite-plugin-windicss": "^1.9.3",
|
"vite-plugin-windicss": "^1.9.3",
|
||||||
"vue": "^3.4.30",
|
"vue": "^3.4.30",
|
||||||
"windicss": "^3.5.6",
|
"windicss": "^3.5.6"
|
||||||
|
|
||||||
"@commitlint/cli": "^18.4.3",
|
|
||||||
"@commitlint/config-conventional": "^18.4.3",
|
|
||||||
"@tsconfig/node18": "^18.2.2",
|
|
||||||
"@types/crypto-js": "^4.2.1",
|
|
||||||
"@types/file-saver": "^2.0.7",
|
|
||||||
"@types/lodash": "^4.14.202",
|
|
||||||
"@types/node": "^18.19.3",
|
|
||||||
"@types/svg-arc-to-cubic-bezier": "^3.2.2",
|
|
||||||
"@types/tinycolor2": "^1.4.6",
|
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
|
||||||
"@vue/tsconfig": "^0.5.0",
|
|
||||||
"husky": "^8.0.3",
|
|
||||||
"npm-run-all2": "^6.1.1",
|
|
||||||
"typescript": "~5.3.0",
|
|
||||||
"vue-tsc": "^1.8.25"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,53 +0,0 @@
|
||||||
<template>
|
|
||||||
<Screen v-if="screening" />
|
|
||||||
<Editor v-else-if="_isPC" />
|
|
||||||
<Mobile v-else />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useScreenStore, useMainStore, useSnapshotStore } from './store'
|
|
||||||
import { LOCALSTORAGE_KEY_DISCARDED_DB } from './configs/storage'
|
|
||||||
import { deleteDiscardedDB } from './utils/database'
|
|
||||||
import { isPC } from './utils/common'
|
|
||||||
import Editor from './views/Editor/index.vue'
|
|
||||||
import Screen from './views/Screen/index.vue'
|
|
||||||
import Mobile from './views/Mobile/index.vue'
|
|
||||||
|
|
||||||
const _isPC = isPC()
|
|
||||||
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const snapshotStore = useSnapshotStore()
|
|
||||||
const { databaseId } = storeToRefs(mainStore)
|
|
||||||
const { screening } = storeToRefs(useScreenStore())
|
|
||||||
|
|
||||||
if (import.meta.env.MODE !== 'development') {
|
|
||||||
window.onbeforeunload = () => false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await deleteDiscardedDB()
|
|
||||||
snapshotStore.initSnapshotDatabase()
|
|
||||||
mainStore.setAvailableFonts()
|
|
||||||
})
|
|
||||||
|
|
||||||
// 应用注销时向 localStorage 中记录下本次 indexedDB 的数据库ID,用于之后清除数据库
|
|
||||||
window.addEventListener('unload', () => {
|
|
||||||
const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
|
|
||||||
const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
|
|
||||||
|
|
||||||
discardedDBList.push(databaseId.value)
|
|
||||||
|
|
||||||
const newDiscardedDB = JSON.stringify(discardedDBList)
|
|
||||||
localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +0,0 @@
|
||||||
$fontList: '仓耳小丸子', '优设标题黑', '字制区喜脉体', '峰广明锐体', '得意黑', '摄图摩登小方体', '站酷快乐体', '素材集市康康体', '素材集市酷方体', '途牛类圆体', '锐字真言体';
|
|
||||||
|
|
||||||
@each $font in $fontList {
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: $font;
|
|
||||||
src: url('../fonts/#{$font}.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
@import "variable";
|
|
||||||
html, body, div, span, applet, object, iframe,
|
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
|
||||||
a, abbr, acronym, address, big, cite, code,
|
|
||||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
|
||||||
small, strike, strong, sub, sup, tt, var,
|
|
||||||
b, u, i, center,
|
|
||||||
dl, dt, dd, ol, ul, li,
|
|
||||||
fieldset, form, label, legend,
|
|
||||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
|
||||||
article, aside, canvas, details, embed,
|
|
||||||
figure, figcaption, footer, header, hgroup,
|
|
||||||
menu, nav, output, ruby, section, summary,
|
|
||||||
time, mark, audio, video {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
font-size: 100%;
|
|
||||||
vertical-align: baseline;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
article,
|
|
||||||
aside,
|
|
||||||
details,
|
|
||||||
figcaption,
|
|
||||||
figure,
|
|
||||||
footer,
|
|
||||||
header,
|
|
||||||
hgroup,
|
|
||||||
menu,
|
|
||||||
nav,
|
|
||||||
section {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #fff;
|
|
||||||
color: $textColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
||||||
}
|
|
||||||
|
|
||||||
ol,
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote, q {
|
|
||||||
quotes: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote::before,
|
|
||||||
blockquote::after,
|
|
||||||
q::before,
|
|
||||||
q::after {
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
vertical-align: middle;
|
|
||||||
border-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
box-sizing: content-box;
|
|
||||||
height: 0;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
mark.active {
|
|
||||||
background-color: #ff9632;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button,
|
|
||||||
select,
|
|
||||||
optgroup,
|
|
||||||
textarea {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
input {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
select {
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
overflow: auto;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
area,
|
|
||||||
button,
|
|
||||||
[role='button'],
|
|
||||||
input:not([type='range']),
|
|
||||||
label,
|
|
||||||
select,
|
|
||||||
summary,
|
|
||||||
textarea {
|
|
||||||
touch-action: manipulation;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #e1e1e1;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
@mixin ellipsis-oneline() {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin ellipsis-multiline($line: 2) {
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: $line;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin flex-grid-layout() {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin flex-grid-layout-children($col, $colWidth) {
|
|
||||||
width: $colWidth;
|
|
||||||
margin-bottom: calc(#{100 - $col * $colWidth} / #{$col - 1});
|
|
||||||
|
|
||||||
&:not(:nth-child(#{$col}n)) {
|
|
||||||
margin-right: calc(#{100 - $col * $colWidth} / #{$col - 1});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin overflow-overlay() {
|
|
||||||
overflow: auto;
|
|
||||||
overflow: overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin absolute-0() {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
@import "variable";
|
|
||||||
.ProseMirror, .ProseMirror-static {
|
|
||||||
outline: 0;
|
|
||||||
border: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: normal;
|
|
||||||
|
|
||||||
&:not(.ProseMirror-static) {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background-color: rgba($themeColor, 0.25);
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-top: var(--paragraphSpace);
|
|
||||||
}
|
|
||||||
p:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
padding-inline-start: 1.25em;
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: inherit;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
padding-inline-start: 1.25em;
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: inherit;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: $borderColor;
|
|
||||||
padding: 2px 6px;
|
|
||||||
margin: 0 1px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup {
|
|
||||||
vertical-align: super;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
sub {
|
|
||||||
vertical-align: sub;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
overflow: hidden;
|
|
||||||
padding-right: 1.2em;
|
|
||||||
padding-left: 1.2em;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
font-style: italic;
|
|
||||||
border-left: 4px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-indent='1'] {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
[data-indent='2'] {
|
|
||||||
padding-left: 40px;
|
|
||||||
}
|
|
||||||
[data-indent='3'] {
|
|
||||||
padding-left: 60px;
|
|
||||||
}
|
|
||||||
[data-indent='4'] {
|
|
||||||
padding-left: 80px;
|
|
||||||
}
|
|
||||||
[data-indent='5'] {
|
|
||||||
padding-left: 100px;
|
|
||||||
}
|
|
||||||
[data-indent='6'] {
|
|
||||||
padding-left: 120px;
|
|
||||||
}
|
|
||||||
[data-indent='7'] {
|
|
||||||
padding-left: 140px;
|
|
||||||
}
|
|
||||||
[data-indent='8'] {
|
|
||||||
padding-left: 160px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror-selectednode {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
$themeColor: #d14424;
|
|
||||||
$themeHoverColor: #de6949;
|
|
||||||
$textColor: #41464b;
|
|
||||||
$borderColor: #e5e7eb;
|
|
||||||
$lightGray: #f9f9f9;
|
|
||||||
|
|
||||||
$boxShadow: 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -2px rgba(0, 0, 0, .1);
|
|
||||||
|
|
||||||
$transitionDelay: .2s;
|
|
||||||
$transitionDelayFast: .1s;
|
|
||||||
$transitionDelaySlow: .3s;
|
|
||||||
|
|
||||||
$borderRadius: 2px;
|
|
||||||
|
|
||||||
:root{
|
|
||||||
--zhuhao-theme-color: #e5e7eb;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import type { Icons } from '../plugins/icon'
|
|
||||||
|
|
||||||
declare module 'vue' {
|
|
||||||
export type GlobalComponents = Icons
|
|
||||||
}
|
|
||||||
|
|
||||||
export {}
|
|
|
@ -1,118 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
class="button"
|
|
||||||
:class="{
|
|
||||||
'disabled': disabled,
|
|
||||||
'checked': !disabled && checked,
|
|
||||||
'default': !disabled && type === 'default',
|
|
||||||
'primary': !disabled && type === 'primary',
|
|
||||||
'checkbox': !disabled && type === 'checkbox',
|
|
||||||
'radio': !disabled && type === 'radio',
|
|
||||||
'small': size === 'small',
|
|
||||||
'first': first,
|
|
||||||
'last': last,
|
|
||||||
}"
|
|
||||||
@click="handleClick()"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
checked?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
type?: 'default' | 'primary' | 'checkbox' | 'radio'
|
|
||||||
size?: 'small' | 'normal'
|
|
||||||
first?: boolean
|
|
||||||
last?: boolean
|
|
||||||
}>(), {
|
|
||||||
checked: false,
|
|
||||||
disabled: false,
|
|
||||||
type: 'default',
|
|
||||||
size: 'normal',
|
|
||||||
first: false,
|
|
||||||
last: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'click'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
if (props.disabled) return
|
|
||||||
emit('click')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.button {
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
outline: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 0 15px;
|
|
||||||
text-align: center;
|
|
||||||
color: $textColor;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
user-select: none;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.small {
|
|
||||||
height: 24px;
|
|
||||||
line-height: 24px;
|
|
||||||
padding: 0 7px;
|
|
||||||
letter-spacing: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.default {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
color: $textColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $themeColor;
|
|
||||||
border-color: $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.primary {
|
|
||||||
background-color: $themeColor;
|
|
||||||
border: 1px solid $themeColor;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $themeHoverColor;
|
|
||||||
border-color: $themeHoverColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.checkbox, &.radio {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
color: $textColor;
|
|
||||||
|
|
||||||
&:not(.checked):hover {
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.checked {
|
|
||||||
color: #fff;
|
|
||||||
background-color: $themeColor;
|
|
||||||
border-color: $themeColor;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $themeHoverColor;
|
|
||||||
border-color: $themeHoverColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.disabled {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
color: #b7b7b7;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="button-group" :class="{ 'passive': passive }" ref="groupRef">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
passive?: boolean
|
|
||||||
}>(), {
|
|
||||||
passive: false,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
::v-deep(button.button) {
|
|
||||||
border-radius: 0;
|
|
||||||
border-left-width: 1px;
|
|
||||||
border-right-width: 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.passive) {
|
|
||||||
::v-deep(button.button) {
|
|
||||||
&:not(:last-child, .radio, .checkbox):hover {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
width: 1px;
|
|
||||||
height: calc(100% + 2px);
|
|
||||||
background-color: $themeColor;
|
|
||||||
position: absolute;
|
|
||||||
top: -1px;
|
|
||||||
right: -1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top-left-radius: $borderRadius;
|
|
||||||
border-bottom-left-radius: $borderRadius;
|
|
||||||
border-left-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-top-right-radius: $borderRadius;
|
|
||||||
border-bottom-right-radius: $borderRadius;
|
|
||||||
border-right-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.passive {
|
|
||||||
::v-deep(button.button) {
|
|
||||||
&:not(.last, .radio, .checkbox):hover {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
width: 1px;
|
|
||||||
height: calc(100% + 2px);
|
|
||||||
background-color: $themeColor;
|
|
||||||
position: absolute;
|
|
||||||
top: -1px;
|
|
||||||
right: -1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.first {
|
|
||||||
border-top-left-radius: $borderRadius;
|
|
||||||
border-bottom-left-radius: $borderRadius;
|
|
||||||
border-left-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.last {
|
|
||||||
border-top-right-radius: $borderRadius;
|
|
||||||
border-bottom-right-radius: $borderRadius;
|
|
||||||
border-right-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,111 +0,0 @@
|
||||||
<template>
|
|
||||||
<label
|
|
||||||
class="checkbox"
|
|
||||||
:class="{
|
|
||||||
'checked': value,
|
|
||||||
'disabled': disabled,
|
|
||||||
}"
|
|
||||||
@change="$event => handleChange($event)"
|
|
||||||
>
|
|
||||||
<span class="checkbox-input"></span>
|
|
||||||
<input class="checkbox-original" type="checkbox" :checked="value">
|
|
||||||
<span class="checkbox-label">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleChange = (e: Event) => {
|
|
||||||
if (props.disabled) return
|
|
||||||
emit('update:value', (e.target as HTMLInputElement).checked)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.checkbox {
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:not(.disabled).checked {
|
|
||||||
.checkbox-input {
|
|
||||||
background-color: $themeColor;
|
|
||||||
border-color: $themeColor;
|
|
||||||
}
|
|
||||||
.checkbox-input::after {
|
|
||||||
transform: rotate(45deg) scaleY(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
color: #b7b7b7;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
.checkbox-input {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-input {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-color: #fff;
|
|
||||||
vertical-align: middle;
|
|
||||||
transition: border-color .15s cubic-bezier(.71, -.46, .29, 1.46), background-color .15s cubic-bezier(.71, -.46, .29, 1.46);
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
border: 2px solid #fff;
|
|
||||||
border-left: 0;
|
|
||||||
border-top: 0;
|
|
||||||
height: 9px;
|
|
||||||
left: 4px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
transform: rotate(45deg) scaleY(0);
|
|
||||||
width: 6px;
|
|
||||||
transition: transform .15s ease-in .05s;
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.checkbox-original {
|
|
||||||
opacity: 0;
|
|
||||||
outline: 0;
|
|
||||||
position: absolute;
|
|
||||||
margin: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
.checkbox-label {
|
|
||||||
margin-left: 5px;
|
|
||||||
line-height: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<template>
|
|
||||||
<Button
|
|
||||||
:checked="checked"
|
|
||||||
:disabled="disabled"
|
|
||||||
type="checkbox"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import Button from './Button.vue'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
checked?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
}>(), {
|
|
||||||
checked: false,
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,44 +0,0 @@
|
||||||
<template>
|
|
||||||
<Button class="color-btn">
|
|
||||||
<div class="color-block">
|
|
||||||
<div class="content" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
<IconPlatte class="color-btn-icon" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import Button from './Button.vue'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
color: string
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.color-btn {
|
|
||||||
width: 100%;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.color-block {
|
|
||||||
height: 20px;
|
|
||||||
margin-left: 8px;
|
|
||||||
flex: 1;
|
|
||||||
outline: 1px dashed rgba($color: #666, $alpha: .12);
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAEBJREFUOE9jfPbs2X8GIoCkpCQRqhgYGEcNxBlOo2GIM2iGQLL5//8/UTnl+fPnxOWUUQNxhtNoGOLOKYM+2QAAh2Nq10DwkukAAAAASUVORK5CYII=);
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.color-btn-icon {
|
|
||||||
width: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #bfbfbf;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,109 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="alpha">
|
|
||||||
<div class="alpha-checkboard-wrap">
|
|
||||||
<Checkboard />
|
|
||||||
</div>
|
|
||||||
<div class="alpha-gradient" :style="{ background: gradientColor }"></div>
|
|
||||||
<div
|
|
||||||
class="alpha-container"
|
|
||||||
ref="alphaRef"
|
|
||||||
@mousedown="$event => handleMouseDown($event)"
|
|
||||||
>
|
|
||||||
<div class="alpha-pointer" :style="{ left: color.a * 100 + '%' }">
|
|
||||||
<div class="alpha-picker"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
import Checkboard from './Checkboard.vue'
|
|
||||||
import type { ColorFormats } from 'tinycolor2'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
value: ColorFormats.RGBA
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'colorChange', payload: ColorFormats.RGBA): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const color = computed(() => props.value)
|
|
||||||
|
|
||||||
const gradientColor = computed(() => {
|
|
||||||
const rgbaStr = [color.value.r, color.value.g, color.value.b].join(',')
|
|
||||||
return `linear-gradient(to right, rgba(${rgbaStr}, 0) 0%, rgba(${rgbaStr}, 1) 100%)`
|
|
||||||
})
|
|
||||||
|
|
||||||
const alphaRef = ref<HTMLElement>()
|
|
||||||
const handleChange = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!alphaRef.value) return
|
|
||||||
const containerWidth = alphaRef.value.clientWidth
|
|
||||||
const xOffset = alphaRef.value.getBoundingClientRect().left + window.pageXOffset
|
|
||||||
const left = e.pageX - xOffset
|
|
||||||
let a
|
|
||||||
|
|
||||||
if (left < 0) a = 0
|
|
||||||
else if (left > containerWidth) a = 1
|
|
||||||
else a = Math.round(left * 100 / containerWidth) / 100
|
|
||||||
|
|
||||||
if (color.value.a !== a) {
|
|
||||||
emit('colorChange', {
|
|
||||||
r: color.value.r,
|
|
||||||
g: color.value.g,
|
|
||||||
b: color.value.b,
|
|
||||||
a: a,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbindEventListeners = () => {
|
|
||||||
window.removeEventListener('mousemove', handleChange)
|
|
||||||
window.removeEventListener('mouseup', unbindEventListeners)
|
|
||||||
}
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
handleChange(e)
|
|
||||||
window.addEventListener('mousemove', handleChange)
|
|
||||||
window.addEventListener('mouseup', unbindEventListeners)
|
|
||||||
}
|
|
||||||
onUnmounted(unbindEventListeners)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.alpha {
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
.alpha-checkboard-wrap {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
.alpha-gradient {
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
.alpha-container {
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0 3px;
|
|
||||||
}
|
|
||||||
.alpha-pointer {
|
|
||||||
z-index: 2;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.alpha-picker {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 4px;
|
|
||||||
height: 8px;
|
|
||||||
box-shadow: 0 0 2px rgba(0, 0, 0, .6);
|
|
||||||
background: #fff;
|
|
||||||
margin-top: 1px;
|
|
||||||
transform: translateX(-2px);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,62 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="checkerboard" :style="bgStyle"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
size?: number
|
|
||||||
white?: string
|
|
||||||
grey?: string
|
|
||||||
}>(), {
|
|
||||||
size: 8,
|
|
||||||
white: '#fff',
|
|
||||||
grey: '#e6e6e6',
|
|
||||||
})
|
|
||||||
|
|
||||||
interface CheckboardCache {
|
|
||||||
[key: string]: string | null
|
|
||||||
}
|
|
||||||
const checkboardCache: CheckboardCache = {}
|
|
||||||
|
|
||||||
const renderCheckboard = (white: string, grey: string, size: number) => {
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = canvas.height = size * 2
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
|
|
||||||
if (!ctx) return null
|
|
||||||
|
|
||||||
ctx.fillStyle = white
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
||||||
ctx.fillStyle = grey
|
|
||||||
ctx.fillRect(0, 0, size, size)
|
|
||||||
ctx.translate(size, size)
|
|
||||||
ctx.fillRect(0, 0, size, size)
|
|
||||||
return canvas.toDataURL()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCheckboard = (white: string, grey: string, size: number) => {
|
|
||||||
const key = white + ',' + grey + ',' + size
|
|
||||||
if (checkboardCache[key]) return checkboardCache[key]
|
|
||||||
|
|
||||||
const checkboard = renderCheckboard(white, grey, size)
|
|
||||||
checkboardCache[key] = checkboard
|
|
||||||
return checkboard
|
|
||||||
}
|
|
||||||
|
|
||||||
const bgStyle = computed(() => {
|
|
||||||
const checkboard = getCheckboard(props.white, props.grey, props.size)
|
|
||||||
return { backgroundImage: `url(${checkboard})` }
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.checkerboard {
|
|
||||||
background-size: contain;
|
|
||||||
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,71 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="editable-input">
|
|
||||||
<input
|
|
||||||
class="input-content"
|
|
||||||
:value="val"
|
|
||||||
@input="$event => handleInput($event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import tinycolor, { type ColorFormats } from 'tinycolor2'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
value: ColorFormats.RGBA
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'colorChange', payload: ColorFormats.RGBA): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const val = computed(() => {
|
|
||||||
let _hex = ''
|
|
||||||
if (props.value.a < 1) _hex = tinycolor(props.value).toHex8String().toUpperCase()
|
|
||||||
else _hex = tinycolor(props.value).toHexString().toUpperCase()
|
|
||||||
return _hex.replace('#', '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleInput = (e: Event) => {
|
|
||||||
const value = (e.target as HTMLInputElement).value
|
|
||||||
if (value.length >= 6) {
|
|
||||||
const color = tinycolor(value)
|
|
||||||
if (color.isValid()) {
|
|
||||||
emit('colorChange', color.toRgb())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.editable-input {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '#';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.input-content {
|
|
||||||
width: 100%;
|
|
||||||
padding: 3px;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
outline: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.input-label {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,119 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hue">
|
|
||||||
<div
|
|
||||||
class="hue-container"
|
|
||||||
ref="hueRef"
|
|
||||||
@mousedown="$event => handleMouseDown($event)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="hue-pointer"
|
|
||||||
:style="{ left: pointerLeft }"
|
|
||||||
>
|
|
||||||
<div class="hue-picker"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import tinycolor, { type ColorFormats } from 'tinycolor2'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
value: ColorFormats.RGBA
|
|
||||||
hue: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'colorChange', payload: ColorFormats.HSLA): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const oldHue = ref(0)
|
|
||||||
const pullDirection = ref('')
|
|
||||||
|
|
||||||
const color = computed(() => {
|
|
||||||
const hsla = tinycolor(props.value).toHsl()
|
|
||||||
if (props.hue !== -1) hsla.h = props.hue
|
|
||||||
return hsla
|
|
||||||
})
|
|
||||||
|
|
||||||
const pointerLeft = computed(() => {
|
|
||||||
if (color.value.h === 0 && pullDirection.value === 'right') return '100%'
|
|
||||||
return color.value.h * 100 / 360 + '%'
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.value, () => {
|
|
||||||
const hsla = tinycolor(props.value).toHsl()
|
|
||||||
const h = hsla.s === 0 ? props.hue : hsla.h
|
|
||||||
if (h !== 0 && h - oldHue.value > 0) pullDirection.value = 'right'
|
|
||||||
if (h !== 0 && h - oldHue.value < 0) pullDirection.value = 'left'
|
|
||||||
oldHue.value = h
|
|
||||||
})
|
|
||||||
|
|
||||||
const hueRef = ref<HTMLElement>()
|
|
||||||
const handleChange = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!hueRef.value) return
|
|
||||||
|
|
||||||
const containerWidth = hueRef.value.clientWidth
|
|
||||||
const xOffset = hueRef.value.getBoundingClientRect().left + window.pageXOffset
|
|
||||||
const left = e.pageX - xOffset
|
|
||||||
let h, percent
|
|
||||||
|
|
||||||
if (left < 0) h = 0
|
|
||||||
else if (left > containerWidth) h = 360
|
|
||||||
else {
|
|
||||||
percent = left * 100 / containerWidth
|
|
||||||
h = 360 * percent / 100
|
|
||||||
}
|
|
||||||
if (props.hue === -1 || color.value.h !== h) {
|
|
||||||
emit('colorChange', {
|
|
||||||
h,
|
|
||||||
l: color.value.l,
|
|
||||||
s: color.value.s,
|
|
||||||
a: color.value.a,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbindEventListeners = () => {
|
|
||||||
window.removeEventListener('mousemove', handleChange)
|
|
||||||
window.removeEventListener('mouseup', unbindEventListeners)
|
|
||||||
}
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
handleChange(e)
|
|
||||||
window.addEventListener('mousemove', handleChange)
|
|
||||||
window.addEventListener('mouseup', unbindEventListeners)
|
|
||||||
}
|
|
||||||
onUnmounted(unbindEventListeners)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.hue {
|
|
||||||
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
|
||||||
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
.hue-container {
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 0 2px;
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.hue-pointer {
|
|
||||||
z-index: 2;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.hue-picker {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 1px;
|
|
||||||
width: 4px;
|
|
||||||
height: 8px;
|
|
||||||
box-shadow: 0 0 2px rgba(0, 0, 0, .6);
|
|
||||||
background: #fff;
|
|
||||||
transform: translateX(-2px);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,110 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="saturation"
|
|
||||||
ref="saturationRef"
|
|
||||||
:style="{ background: bgColor }"
|
|
||||||
@mousedown="$event => handleMouseDown($event)"
|
|
||||||
>
|
|
||||||
<div class="saturation-white"></div>
|
|
||||||
<div class="saturation-black"></div>
|
|
||||||
<div class="saturation-pointer"
|
|
||||||
:style="{
|
|
||||||
top: pointerTop,
|
|
||||||
left: pointerLeft,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="saturation-circle"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onUnmounted, ref } from 'vue'
|
|
||||||
import tinycolor, { type ColorFormats } from 'tinycolor2'
|
|
||||||
import { throttle, clamp } from 'lodash'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
value: ColorFormats.RGBA
|
|
||||||
hue: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'colorChange', payload: ColorFormats.HSVA): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const color = computed(() => {
|
|
||||||
const hsva = tinycolor(props.value).toHsv()
|
|
||||||
if (props.hue !== -1) hsva.h = props.hue
|
|
||||||
return hsva
|
|
||||||
})
|
|
||||||
|
|
||||||
const bgColor = computed(() => `hsl(${color.value.h}, 100%, 50%)`)
|
|
||||||
const pointerTop = computed(() => (-(color.value.v * 100) + 1) + 100 + '%')
|
|
||||||
const pointerLeft = computed(() => color.value.s * 100 + '%')
|
|
||||||
|
|
||||||
const emitChangeEvent = throttle(function(param: ColorFormats.HSVA) {
|
|
||||||
emit('colorChange', param)
|
|
||||||
}, 20, { leading: true, trailing: false })
|
|
||||||
|
|
||||||
const saturationRef = ref<HTMLElement>()
|
|
||||||
const handleChange = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!saturationRef.value) return
|
|
||||||
|
|
||||||
const containerWidth = saturationRef.value.clientWidth
|
|
||||||
const containerHeight = saturationRef.value.clientHeight
|
|
||||||
const xOffset = saturationRef.value.getBoundingClientRect().left + window.pageXOffset
|
|
||||||
const yOffset = saturationRef.value.getBoundingClientRect().top + window.pageYOffset
|
|
||||||
const left = clamp(e.pageX - xOffset, 0, containerWidth)
|
|
||||||
const top = clamp(e.pageY - yOffset, 0, containerHeight)
|
|
||||||
const saturation = left / containerWidth
|
|
||||||
const bright = clamp(-(top / containerHeight) + 1, 0, 1)
|
|
||||||
|
|
||||||
emitChangeEvent({
|
|
||||||
h: color.value.h,
|
|
||||||
s: saturation,
|
|
||||||
v: bright,
|
|
||||||
a: color.value.a,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbindEventListeners = () => {
|
|
||||||
window.removeEventListener('mousemove', handleChange)
|
|
||||||
window.removeEventListener('mouseup', unbindEventListeners)
|
|
||||||
}
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
handleChange(e)
|
|
||||||
window.addEventListener('mousemove', handleChange)
|
|
||||||
window.addEventListener('mouseup', unbindEventListeners)
|
|
||||||
}
|
|
||||||
onUnmounted(unbindEventListeners)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.saturation,
|
|
||||||
.saturation-white,
|
|
||||||
.saturation-black {
|
|
||||||
@include absolute-0();
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.saturation-white {
|
|
||||||
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
|
|
||||||
}
|
|
||||||
.saturation-black {
|
|
||||||
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
|
|
||||||
}
|
|
||||||
.saturation-pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.saturation-circle {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, .3), 0 0 1px 2px rgba(0, 0, 0, .4);
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: translate(-2px, -2px);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,445 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="color-picker">
|
|
||||||
<div class="picker-saturation-wrap">
|
|
||||||
<Saturation :value="color" :hue="hue" @colorChange="value => changeColor(value)" />
|
|
||||||
</div>
|
|
||||||
<div class="picker-controls">
|
|
||||||
<div class="picker-color-wrap">
|
|
||||||
<div class="picker-current-color" :style="{ background: currentColor }"></div>
|
|
||||||
<Checkboard />
|
|
||||||
</div>
|
|
||||||
<div class="picker-sliders">
|
|
||||||
<div class="picker-hue-wrap">
|
|
||||||
<Hue :value="color" :hue="hue" @colorChange="value => changeColor(value)" />
|
|
||||||
</div>
|
|
||||||
<div class="picker-alpha-wrap">
|
|
||||||
<Alpha :value="color" @colorChange="value => changeColor(value)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="picker-field">
|
|
||||||
<EditableInput class="input" :value="color" @colorChange="value => changeColor(value)" />
|
|
||||||
<div class="straw" @click="openEyeDropper()"><IconNeedle /></div>
|
|
||||||
<div class="transparent" @click="selectPresetColor('#00000000')">
|
|
||||||
<Checkboard />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="picker-presets">
|
|
||||||
<div
|
|
||||||
class="picker-presets-color"
|
|
||||||
v-for="c in themeColors"
|
|
||||||
:key="c"
|
|
||||||
:style="{ background: c }"
|
|
||||||
@click="selectPresetColor(c)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="picker-gradient-presets">
|
|
||||||
<div
|
|
||||||
class="picker-gradient-col"
|
|
||||||
v-for="(col, index) in presetColors"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<div class="picker-gradient-color"
|
|
||||||
v-for="c in col"
|
|
||||||
:key="c"
|
|
||||||
:style="{ background: c }"
|
|
||||||
@click="selectPresetColor(c)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="picker-presets">
|
|
||||||
<div
|
|
||||||
v-for="c in standardColors"
|
|
||||||
:key="c"
|
|
||||||
class="picker-presets-color"
|
|
||||||
:style="{ background: c }"
|
|
||||||
@click="selectPresetColor(c)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="recent-colors-title" v-if="recentColors.length">最近使用:</div>
|
|
||||||
<div class="picker-presets">
|
|
||||||
<div
|
|
||||||
v-for="c in recentColors"
|
|
||||||
:key="c"
|
|
||||||
class="picker-presets-color alpha"
|
|
||||||
@click="selectPresetColor(c)"
|
|
||||||
>
|
|
||||||
<div class="picker-presets-color-content" :style="{ background: c }"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
|
||||||
import tinycolor, { type ColorFormats } from 'tinycolor2'
|
|
||||||
import { debounce } from 'lodash'
|
|
||||||
import { toCanvas } from 'html-to-image'
|
|
||||||
import message from '../../utils/message'
|
|
||||||
|
|
||||||
import Alpha from './Alpha.vue'
|
|
||||||
import Checkboard from './Checkboard.vue'
|
|
||||||
import Hue from './Hue.vue'
|
|
||||||
import Saturation from './Saturation.vue'
|
|
||||||
import EditableInput from './EditableInput.vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue?: string
|
|
||||||
}>(), {
|
|
||||||
modelValue: '#e86b99',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', payload: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const RECENT_COLORS = 'RECENT_COLORS'
|
|
||||||
|
|
||||||
const presetColorConfig = [
|
|
||||||
['#7f7f7f', '#f2f2f2'],
|
|
||||||
['#0d0d0d', '#808080'],
|
|
||||||
['#1c1a10', '#ddd8c3'],
|
|
||||||
['#0e243d', '#c6d9f0'],
|
|
||||||
['#233f5e', '#dae5f0'],
|
|
||||||
['#632623', '#f2dbdb'],
|
|
||||||
['#4d602c', '#eaf1de'],
|
|
||||||
['#3f3150', '#e6e0ec'],
|
|
||||||
['#1e5867', '#d9eef3'],
|
|
||||||
['#99490f', '#fee9da'],
|
|
||||||
]
|
|
||||||
|
|
||||||
const gradient = (startColor: string, endColor: string, step: number) => {
|
|
||||||
const _startColor = tinycolor(startColor).toRgb()
|
|
||||||
const _endColor = tinycolor(endColor).toRgb()
|
|
||||||
|
|
||||||
const rStep = (_endColor.r - _startColor.r) / step
|
|
||||||
const gStep = (_endColor.g - _startColor.g) / step
|
|
||||||
const bStep = (_endColor.b - _startColor.b) / step
|
|
||||||
const gradientColorArr = []
|
|
||||||
|
|
||||||
for (let i = 0; i < step; i++) {
|
|
||||||
const gradientColor = tinycolor({
|
|
||||||
r: _startColor.r + rStep * i,
|
|
||||||
g: _startColor.g + gStep * i,
|
|
||||||
b: _startColor.b + bStep * i,
|
|
||||||
}).toRgbString()
|
|
||||||
gradientColorArr.push(gradientColor)
|
|
||||||
}
|
|
||||||
return gradientColorArr
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPresetColors = () => {
|
|
||||||
const presetColors = []
|
|
||||||
for (const color of presetColorConfig) {
|
|
||||||
presetColors.push(gradient(color[1], color[0], 5))
|
|
||||||
}
|
|
||||||
return presetColors
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeColors = ['#000000', '#ffffff', '#eeece1', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c']
|
|
||||||
const standardColors = ['#c21401', '#ff1e02', '#ffc12a', '#ffff3a', '#90cf5b', '#00af57', '#00afee', '#0071be', '#00215f', '#72349d']
|
|
||||||
|
|
||||||
const hue = ref(-1)
|
|
||||||
const recentColors = ref<string[]>([])
|
|
||||||
|
|
||||||
const color = computed({
|
|
||||||
get() {
|
|
||||||
return tinycolor(props.modelValue).toRgb()
|
|
||||||
},
|
|
||||||
set(rgba: ColorFormats.RGBA) {
|
|
||||||
const rgbaString = `rgba(${[rgba.r, rgba.g, rgba.b, rgba.a].join(',')})`
|
|
||||||
emit('update:modelValue', rgbaString)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const presetColors = getPresetColors()
|
|
||||||
|
|
||||||
const currentColor = computed(() => {
|
|
||||||
return `rgba(${[color.value.r, color.value.g, color.value.b, color.value.a].join(',')})`
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectPresetColor = (colorString: string) => {
|
|
||||||
hue.value = tinycolor(colorString).toHsl().h
|
|
||||||
emit('update:modelValue', colorString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每次选择非预设颜色时,需要将该颜色加入到最近使用列表中
|
|
||||||
const updateRecentColorsCache = debounce(function() {
|
|
||||||
const _color = tinycolor(color.value).toRgbString()
|
|
||||||
if (!recentColors.value.includes(_color)) {
|
|
||||||
recentColors.value = [_color, ...recentColors.value]
|
|
||||||
|
|
||||||
const maxLength = 10
|
|
||||||
if (recentColors.value.length > maxLength) {
|
|
||||||
recentColors.value = recentColors.value.slice(0, maxLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 300, { trailing: true })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const recentColorsCache = localStorage.getItem(RECENT_COLORS)
|
|
||||||
if (recentColorsCache) recentColors.value = JSON.parse(recentColorsCache)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(recentColors, () => {
|
|
||||||
const recentColorsCache = JSON.stringify(recentColors.value)
|
|
||||||
localStorage.setItem(RECENT_COLORS, recentColorsCache)
|
|
||||||
})
|
|
||||||
|
|
||||||
const changeColor = (value: ColorFormats.RGBA | ColorFormats.HSLA | ColorFormats.HSVA) => {
|
|
||||||
if ('h' in value) {
|
|
||||||
hue.value = value.h
|
|
||||||
color.value = tinycolor(value).toRgb()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
hue.value = tinycolor(value).toHsl().h
|
|
||||||
color.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRecentColorsCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开取色吸管
|
|
||||||
// 检查环境是否支持原生取色吸管,支持则使用原生吸管,否则使用自定义吸管
|
|
||||||
const openEyeDropper = () => {
|
|
||||||
const isSupportedEyeDropper = 'EyeDropper' in window
|
|
||||||
|
|
||||||
if (isSupportedEyeDropper) browserEyeDropper()
|
|
||||||
else customEyeDropper()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 原生取色吸管
|
|
||||||
const browserEyeDropper = () => {
|
|
||||||
message.success('按 ESC 键关闭取色吸管', { duration: 0 })
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const eyeDropper = new (window as any).EyeDropper()
|
|
||||||
eyeDropper.open().then((result: { sRGBHex: string }) => {
|
|
||||||
const tColor = tinycolor(result.sRGBHex)
|
|
||||||
hue.value = tColor.toHsl().h
|
|
||||||
color.value = tColor.toRgb()
|
|
||||||
|
|
||||||
message.closeAll()
|
|
||||||
updateRecentColorsCache()
|
|
||||||
}).catch(() => {
|
|
||||||
message.closeAll()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基于 Canvas 的自定义取色吸管
|
|
||||||
const customEyeDropper = () => {
|
|
||||||
const targetRef: HTMLElement | null = document.querySelector('.canvas')
|
|
||||||
if (!targetRef) return
|
|
||||||
|
|
||||||
const maskRef = document.createElement('div')
|
|
||||||
maskRef.style.cssText = 'position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 9999; cursor: wait;'
|
|
||||||
document.body.appendChild(maskRef)
|
|
||||||
|
|
||||||
const colorBlockRef = document.createElement('div')
|
|
||||||
colorBlockRef.style.cssText = 'position: absolute; top: -100px; left: -100px; width: 16px; height: 16px; border: 1px solid #000; z-index: 999'
|
|
||||||
maskRef.appendChild(colorBlockRef)
|
|
||||||
|
|
||||||
const { left, top, width, height } = targetRef.getBoundingClientRect()
|
|
||||||
|
|
||||||
const filter = (node: HTMLElement) => {
|
|
||||||
if (node.tagName && node.tagName.toUpperCase() === 'FOREIGNOBJECT') return false
|
|
||||||
if (node.classList && node.classList.contains('operate')) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
toCanvas(targetRef, { filter, fontEmbedCSS: '', width, height, canvasWidth: width, canvasHeight: height, pixelRatio: 1 }).then(canvasRef => {
|
|
||||||
canvasRef.style.cssText = `position: absolute; top: ${top}px; left: ${left}px; cursor: crosshair;`
|
|
||||||
maskRef.style.cursor = 'default'
|
|
||||||
maskRef.appendChild(canvasRef)
|
|
||||||
|
|
||||||
const ctx = canvasRef.getContext('2d')
|
|
||||||
if (!ctx) return
|
|
||||||
|
|
||||||
let currentColor = ''
|
|
||||||
const handleMousemove = (e: MouseEvent) => {
|
|
||||||
const x = e.x
|
|
||||||
const y = e.y
|
|
||||||
|
|
||||||
const mouseX = x - left
|
|
||||||
const mouseY = y - top
|
|
||||||
|
|
||||||
const [r, g, b, a] = ctx.getImageData(mouseX, mouseY, 1, 1).data
|
|
||||||
currentColor = `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(2)})`
|
|
||||||
|
|
||||||
colorBlockRef.style.left = x + 10 + 'px'
|
|
||||||
colorBlockRef.style.top = y + 10 + 'px'
|
|
||||||
colorBlockRef.style.backgroundColor = currentColor
|
|
||||||
}
|
|
||||||
const handleMouseleave = () => {
|
|
||||||
currentColor = ''
|
|
||||||
colorBlockRef.style.left = '-100px'
|
|
||||||
colorBlockRef.style.top = '-100px'
|
|
||||||
colorBlockRef.style.backgroundColor = ''
|
|
||||||
}
|
|
||||||
const handleMousedown = (e: MouseEvent) => {
|
|
||||||
if (currentColor && e.button === 0) {
|
|
||||||
const tColor = tinycolor(currentColor)
|
|
||||||
hue.value = tColor.toHsl().h
|
|
||||||
color.value = tColor.toRgb()
|
|
||||||
|
|
||||||
updateRecentColorsCache()
|
|
||||||
}
|
|
||||||
document.body.removeChild(maskRef)
|
|
||||||
|
|
||||||
canvasRef.removeEventListener('mousemove', handleMousemove)
|
|
||||||
canvasRef.removeEventListener('mouseleave', handleMouseleave)
|
|
||||||
window.removeEventListener('mousedown', handleMousedown)
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasRef.addEventListener('mousemove', handleMousemove)
|
|
||||||
canvasRef.addEventListener('mouseleave', handleMouseleave)
|
|
||||||
window.addEventListener('mousedown', handleMousedown)
|
|
||||||
}).catch(() => {
|
|
||||||
message.error('取色吸管初始化失败')
|
|
||||||
document.body.removeChild(maskRef)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.color-picker {
|
|
||||||
position: relative;
|
|
||||||
width: 240px;
|
|
||||||
background: #fff;
|
|
||||||
user-select: none;
|
|
||||||
margin-bottom: -10px;
|
|
||||||
}
|
|
||||||
.picker-saturation-wrap {
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 50%;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.picker-controls {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.picker-sliders {
|
|
||||||
padding: 4px 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.picker-hue-wrap {
|
|
||||||
position: relative;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
.picker-alpha-wrap {
|
|
||||||
position: relative;
|
|
||||||
height: 10px;
|
|
||||||
margin-top: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.picker-color-wrap {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-right: 4px;
|
|
||||||
outline: 1px dashed rgba($color: #666, $alpha: .12);
|
|
||||||
|
|
||||||
.checkerboard {
|
|
||||||
background-size: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.picker-current-color {
|
|
||||||
@include absolute-0();
|
|
||||||
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-field {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-left: 8px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
width: 26px;
|
|
||||||
height: 2px;
|
|
||||||
position: absolute;
|
|
||||||
top: 11px;
|
|
||||||
left: -1px;
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
background-color: #f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkerboard {
|
|
||||||
background-size: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.straw {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-left: 8px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
outline: 1px solid #f1f1f1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-presets {
|
|
||||||
@include flex-grid-layout();
|
|
||||||
}
|
|
||||||
.picker-presets-color {
|
|
||||||
@include flex-grid-layout-children(10, 7%);
|
|
||||||
|
|
||||||
height: 0;
|
|
||||||
padding-bottom: 7%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.alpha {
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADBJREFUOE9jfPbs2X8GPEBSUhKfNAPjqAHDIgz+//+PNx08f/4cfzoYNYCBceiHAQC5flV5JzgrxQAAAABJRU5ErkJggg==);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.picker-presets-color-content {
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
.picker-gradient-presets {
|
|
||||||
@include flex-grid-layout();
|
|
||||||
}
|
|
||||||
.picker-gradient-col {
|
|
||||||
@include flex-grid-layout-children(10, 7%);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.picker-gradient-color {
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-colors-title {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,139 +0,0 @@
|
||||||
<template>
|
|
||||||
<ul class="menu-content">
|
|
||||||
<template v-for="(menu, index) in menus" :key="menu.text || index">
|
|
||||||
<li
|
|
||||||
v-if="!menu.hide"
|
|
||||||
class="menu-item"
|
|
||||||
@click.stop="handleClickMenuItem(menu)"
|
|
||||||
:class="{'divider': menu.divider, 'disable': menu.disable}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="menu-item-content"
|
|
||||||
:class="{
|
|
||||||
'has-children': menu.children,
|
|
||||||
'has-handler': menu.handler,
|
|
||||||
}"
|
|
||||||
v-if="!menu.divider"
|
|
||||||
>
|
|
||||||
<span class="text">{{menu.text}}</span>
|
|
||||||
<span class="sub-text" v-if="menu.subText && !menu.children">{{menu.subText}}</span>
|
|
||||||
|
|
||||||
<menu-content
|
|
||||||
class="sub-menu"
|
|
||||||
:menus="menu.children"
|
|
||||||
v-if="menu.children && menu.children.length"
|
|
||||||
:handleClickMenuItem="handleClickMenuItem"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { ContextmenuItem } from './types'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
menus: ContextmenuItem[]
|
|
||||||
handleClickMenuItem: (item: ContextmenuItem) => void
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
$menuWidth: 180px;
|
|
||||||
$menuHeight: 30px;
|
|
||||||
$subMenuWidth: 120px;
|
|
||||||
|
|
||||||
.menu-content {
|
|
||||||
width: $menuWidth;
|
|
||||||
padding: 5px 0;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid $borderColor;
|
|
||||||
box-shadow: $boxShadow;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.menu-item {
|
|
||||||
padding: 0 20px;
|
|
||||||
color: #555;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: all $transitionDelayFast;
|
|
||||||
white-space: nowrap;
|
|
||||||
height: $menuHeight;
|
|
||||||
line-height: $menuHeight;
|
|
||||||
background-color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:not(.disable):hover > .menu-item-content > .sub-menu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.disable):hover > .has-children.has-handler::after {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(.disable) {
|
|
||||||
background-color: rgba($color: $themeColor, $alpha: .2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.divider {
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 5px;
|
|
||||||
background-color: #e5e5e5;
|
|
||||||
line-height: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disable {
|
|
||||||
color: #b1b1b1;
|
|
||||||
cursor: no-drop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.menu-item-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.has-children::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #666 #666 transparent transparent;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%) rotate(45deg);
|
|
||||||
}
|
|
||||||
&.has-children.has-handler::after {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 1px;
|
|
||||||
height: 24px;
|
|
||||||
background-color: rgba($color: #fff, $alpha: .3);
|
|
||||||
position: absolute;
|
|
||||||
right: 18px;
|
|
||||||
top: 3px;
|
|
||||||
transform: scale(0);
|
|
||||||
transition: transform $transitionDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-text {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.sub-menu {
|
|
||||||
width: $subMenuWidth;
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
left: 112%;
|
|
||||||
top: -6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,80 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="mask"
|
|
||||||
@contextmenu.prevent="removeContextmenu()"
|
|
||||||
@mousedown.left="removeContextmenu()"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="contextmenu"
|
|
||||||
:style="{
|
|
||||||
left: style.left + 'px',
|
|
||||||
top: style.top + 'px',
|
|
||||||
}"
|
|
||||||
@contextmenu.prevent
|
|
||||||
>
|
|
||||||
<MenuContent
|
|
||||||
:menus="menus"
|
|
||||||
:handleClickMenuItem="handleClickMenuItem"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import type { ContextmenuItem, Axis } from './types'
|
|
||||||
|
|
||||||
import MenuContent from './MenuContent.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
axis: Axis
|
|
||||||
el: HTMLElement
|
|
||||||
menus: ContextmenuItem[]
|
|
||||||
removeContextmenu: () => void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const style = computed(() => {
|
|
||||||
const MENU_WIDTH = 180
|
|
||||||
const MENU_HEIGHT = 30
|
|
||||||
const DIVIDER_HEIGHT = 11
|
|
||||||
const PADDING = 5
|
|
||||||
|
|
||||||
const { x, y } = props.axis
|
|
||||||
const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
|
|
||||||
const dividerCount = props.menus.filter(menu => menu.divider).length
|
|
||||||
|
|
||||||
const menuWidth = MENU_WIDTH
|
|
||||||
const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
|
|
||||||
|
|
||||||
const screenWidth = document.body.clientWidth
|
|
||||||
const screenHeight = document.body.clientHeight
|
|
||||||
|
|
||||||
return {
|
|
||||||
left: screenWidth <= x + menuWidth ? x - menuWidth : x,
|
|
||||||
top: screenHeight <= y + menuHeight ? y - menuHeight : y,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClickMenuItem = (item: ContextmenuItem) => {
|
|
||||||
if (item.disable) return
|
|
||||||
if (item.children && !item.handler) return
|
|
||||||
if (item.handler) item.handler(props.el)
|
|
||||||
props.removeContextmenu()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.mask {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 9998;
|
|
||||||
}
|
|
||||||
.contextmenu {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 9999;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,14 +0,0 @@
|
||||||
export interface ContextmenuItem {
|
|
||||||
text?: string
|
|
||||||
subText?: string
|
|
||||||
divider?: boolean
|
|
||||||
disable?: boolean
|
|
||||||
hide?: boolean
|
|
||||||
children?: ContextmenuItem[]
|
|
||||||
handler?: (el: HTMLElement) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Axis {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="['divider', type]"
|
|
||||||
:style="{
|
|
||||||
margin: type === 'horizontal' ? `${margin >= 0 ? margin : 24}px 0` : `0 ${margin >= 0 ? margin : 8}px`
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
type?: 'horizontal' | 'vertical'
|
|
||||||
margin?: number
|
|
||||||
}>(), {
|
|
||||||
type: 'horizontal',
|
|
||||||
margin: -1,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.divider {
|
|
||||||
&.horizontal {
|
|
||||||
width: 100%;
|
|
||||||
margin: 24px 0;
|
|
||||||
border-block-start: 1px solid rgba(5, 5, 5, .06);
|
|
||||||
}
|
|
||||||
&.vertical {
|
|
||||||
position: relative;
|
|
||||||
height: 1em;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 8px;
|
|
||||||
border-inline-start: 1px solid rgba(5, 5, 5, .06);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,128 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition :name="`drawer-slide-${placement}`"
|
|
||||||
@afterLeave="contentVisible = false"
|
|
||||||
@before-enter="contentVisible = true"
|
|
||||||
>
|
|
||||||
<div :class="['drawer', placement]" v-show="visible" :style="{ width: props.width + 'px' }">
|
|
||||||
<div class="header">
|
|
||||||
<slot name="title"></slot>
|
|
||||||
<span class="close-btn" @click="emit('update:visible', false)"><IconClose /></span>
|
|
||||||
</div>
|
|
||||||
<div class="content" v-if="contentVisible" :style="contentStyle">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, type CSSProperties } from 'vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
visible: boolean
|
|
||||||
width?: number
|
|
||||||
contentStyle?: CSSProperties
|
|
||||||
placement?: 'left' | 'right'
|
|
||||||
}>(), {
|
|
||||||
width: 320,
|
|
||||||
placement: 'right',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:visible', payload: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const contentVisible = ref(false)
|
|
||||||
|
|
||||||
const contentStyle = computed(() => {
|
|
||||||
return {
|
|
||||||
width: props.width + 'px',
|
|
||||||
...(props.contentStyle || {})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.drawer {
|
|
||||||
height: 100%;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 5000;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&.left {
|
|
||||||
left: 0;
|
|
||||||
box-shadow: 3px 0 6px -4px rgba(0, 0, 0, 0.12), 9px 0 28px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
&.right {
|
|
||||||
right: 0;
|
|
||||||
box-shadow: -3px 0 6px -4px rgba(0, 0, 0, 0.12), -9px 0 28px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
height: 50px;
|
|
||||||
padding: 0 15px;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
right: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: 0 15px;
|
|
||||||
overflow: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-slide-right-enter-active {
|
|
||||||
animation: drawer-slide-right-enter .25s both ease;
|
|
||||||
}
|
|
||||||
.drawer-slide-right-leave-active {
|
|
||||||
animation: drawer-slide-right-leave .25s both ease;
|
|
||||||
}
|
|
||||||
.drawer-slide-left-enter-active {
|
|
||||||
animation: drawer-slide-left-enter .25s both ease;
|
|
||||||
}
|
|
||||||
.drawer-slide-left-leave-active {
|
|
||||||
animation: drawer-slide-left-leave .25s both ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes drawer-slide-right-enter {
|
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes drawer-slide-right-leave {
|
|
||||||
to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes drawer-slide-left-enter {
|
|
||||||
from {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes drawer-slide-left-leave {
|
|
||||||
to {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,47 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="file-input" @click="handleClick()">
|
|
||||||
<slot></slot>
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="file"
|
|
||||||
name="upload"
|
|
||||||
ref="inputRef"
|
|
||||||
:accept="accept"
|
|
||||||
@change="$event => handleChange($event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
accept?: string
|
|
||||||
}>(), {
|
|
||||||
accept: 'image/*',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'change', payload: FileList): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement>()
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
if (!inputRef.value) return
|
|
||||||
inputRef.value.value = ''
|
|
||||||
inputRef.value.click()
|
|
||||||
}
|
|
||||||
const handleChange = (e: Event) => {
|
|
||||||
const files = (e.target as HTMLInputElement).files
|
|
||||||
if (files) emit('change', files)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,68 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="fullscreen-spin" v-if="loading">
|
|
||||||
<div class="spin">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div class="text">{{tip}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
loading?: boolean
|
|
||||||
tip?: string
|
|
||||||
}>(), {
|
|
||||||
loading: false,
|
|
||||||
tip: '',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.fullscreen-spin {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba($color: #f1f1f1, $alpha: .7);
|
|
||||||
}
|
|
||||||
.spin {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -100px;
|
|
||||||
margin-left: -100px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.spinner {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border: 3px solid $themeColor;
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spinner .8s linear infinite;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
margin-top: 20px;
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
@keyframes spinner {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,152 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="gradient-bar">
|
|
||||||
<div class="bar" ref="barRef" :style="{ backgroundImage: gradientStyle }" @click="$event => addPoint($event)"></div>
|
|
||||||
<div class="point"
|
|
||||||
:class="{ 'active': activeIndex === index }"
|
|
||||||
v-for="(item, index) in points"
|
|
||||||
:key="item.pos + '-' + index"
|
|
||||||
:style="{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
left: `calc(${item.pos}% - 5px)`,
|
|
||||||
}"
|
|
||||||
@mousedown.left="movePoint(index)"
|
|
||||||
@click.right="removePoint(index)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { GradientColor } from '../types/slides'
|
|
||||||
import { ref, computed, watchEffect, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
value: GradientColor[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: GradientColor[]): void
|
|
||||||
(event: 'update:index', payload: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const points = ref<GradientColor[]>([])
|
|
||||||
|
|
||||||
const barRef = ref<HTMLElement>()
|
|
||||||
const activeIndex = ref(0)
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
points.value = props.value
|
|
||||||
if (activeIndex.value > props.value.length - 1) activeIndex.value = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(activeIndex, () => {
|
|
||||||
emit('update:index', activeIndex.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const gradientStyle = computed(() => {
|
|
||||||
const list = points.value.map(item => `${item.color} ${item.pos}%`)
|
|
||||||
return `linear-gradient(to right, ${list.join(',')})`
|
|
||||||
})
|
|
||||||
|
|
||||||
const removePoint = (index: number) => {
|
|
||||||
if (props.value.length <= 2) return
|
|
||||||
|
|
||||||
if (index === activeIndex.value) {
|
|
||||||
activeIndex.value = (index - 1 < 0) ? 0 : index - 1
|
|
||||||
}
|
|
||||||
else if (activeIndex.value === props.value.length - 1) {
|
|
||||||
activeIndex.value = props.value.length - 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = props.value.filter((item, _index) => _index !== index)
|
|
||||||
emit('update:value', values)
|
|
||||||
}
|
|
||||||
|
|
||||||
const movePoint = (index: number) => {
|
|
||||||
let isMouseDown = true
|
|
||||||
|
|
||||||
document.onmousemove = e => {
|
|
||||||
if (!isMouseDown) return
|
|
||||||
if (!barRef.value) return
|
|
||||||
|
|
||||||
let pos = Math.round((e.clientX - barRef.value.getBoundingClientRect().left) / barRef.value.clientWidth * 100)
|
|
||||||
if (pos > 100) pos = 100
|
|
||||||
if (pos < 0) pos = 0
|
|
||||||
|
|
||||||
points.value = points.value.map((item, _index) => {
|
|
||||||
if (_index === index) return { ...item, pos }
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
document.onmouseup = () => {
|
|
||||||
isMouseDown = false
|
|
||||||
|
|
||||||
const point = points.value[index]
|
|
||||||
const _points = [...points.value]
|
|
||||||
_points.splice(index, 1)
|
|
||||||
|
|
||||||
let targetIndex = 0
|
|
||||||
for (let i = 0; i < _points.length; i++) {
|
|
||||||
if (point.pos > _points[i].pos) targetIndex = i + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
activeIndex.value = targetIndex
|
|
||||||
_points.splice(targetIndex, 0, point)
|
|
||||||
|
|
||||||
emit('update:value', _points)
|
|
||||||
|
|
||||||
document.onmousemove = null
|
|
||||||
document.onmouseup = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPoint = (e: MouseEvent) => {
|
|
||||||
if (props.value.length >= 6) return
|
|
||||||
if (!barRef.value) return
|
|
||||||
const pos = Math.round((e.clientX - barRef.value.getBoundingClientRect().left) / barRef.value.clientWidth * 100)
|
|
||||||
|
|
||||||
let targetIndex = 0
|
|
||||||
for (let i = 0; i < props.value.length; i++) {
|
|
||||||
if (pos > props.value[i].pos) targetIndex = i + 1
|
|
||||||
}
|
|
||||||
const color = props.value[targetIndex - 1] ? props.value[targetIndex - 1].color : props.value[targetIndex].color
|
|
||||||
const values = [...props.value]
|
|
||||||
values.splice(targetIndex, 0, { pos, color })
|
|
||||||
activeIndex.value = targetIndex
|
|
||||||
emit('update:value', values)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.gradient-bar {
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
height: 18px;
|
|
||||||
padding: 1px 0;
|
|
||||||
margin: 3px 0;
|
|
||||||
position: relative;
|
|
||||||
left: 5px;
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
height: 16px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
}
|
|
||||||
.point {
|
|
||||||
width: 10px;
|
|
||||||
height: 18px;
|
|
||||||
background-color: #fff;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
outline: 1px solid #d9d9d9;
|
|
||||||
box-shadow: 0 0 2px 2px #d9d9d9;
|
|
||||||
border-radius: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
outline: 1px solid $themeColor;
|
|
||||||
box-shadow: 0 0 2px 2px $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,134 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="input"
|
|
||||||
:class="{
|
|
||||||
'disabled': disabled,
|
|
||||||
'focused': focused,
|
|
||||||
'simple': simple,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="prefix">
|
|
||||||
<slot name="prefix"></slot>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
ref="inputRef"
|
|
||||||
:disabled="disabled"
|
|
||||||
:value="value"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
@input="$event => handleInput($event)"
|
|
||||||
@focus="$event => handleFocus($event)"
|
|
||||||
@blur="$event => handleBlur($event)"
|
|
||||||
@change="$event => emit('change', $event)"
|
|
||||||
@keydown.enter="$event => emit('enter', $event)"
|
|
||||||
/>
|
|
||||||
<span class="suffix">
|
|
||||||
<slot name="suffix"></slot>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
value: string
|
|
||||||
disabled?: boolean
|
|
||||||
placeholder?: string
|
|
||||||
simple?: boolean
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
placeholder: '',
|
|
||||||
simple: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: string): void
|
|
||||||
(event: 'input', payload: Event): void
|
|
||||||
(event: 'change', payload: Event): void
|
|
||||||
(event: 'blur', payload: Event): void
|
|
||||||
(event: 'focus', payload: Event): void
|
|
||||||
(event: 'enter', payload: Event): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const focused = ref(false)
|
|
||||||
|
|
||||||
const handleInput = (e: Event) => {
|
|
||||||
emit('update:value', (e.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
const handleBlur = (e: Event) => {
|
|
||||||
focused.value = false
|
|
||||||
emit('blur', e)
|
|
||||||
}
|
|
||||||
const handleFocus = (e: Event) => {
|
|
||||||
focused.value = true
|
|
||||||
emit('focus', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement>()
|
|
||||||
const focus = () => {
|
|
||||||
if (inputRef.value) inputRef.value.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
focus,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.input {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
padding: 0 5px;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
transition: border-color .25s;
|
|
||||||
font-size: 13px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
input {
|
|
||||||
min-width: 0;
|
|
||||||
height: 30px;
|
|
||||||
outline: 0;
|
|
||||||
border: 0;
|
|
||||||
line-height: 30px;
|
|
||||||
vertical-align: top;
|
|
||||||
color: $textColor;
|
|
||||||
padding: 0 5px;
|
|
||||||
flex: 1;
|
|
||||||
font-size: 13px;
|
|
||||||
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #bfbfbf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.disabled):hover, &.focused {
|
|
||||||
border-color: $themeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-color: #dcdcdc;
|
|
||||||
color: #b7b7b7;
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: #b7b7b7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.simple {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prefix, .suffix {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 30px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,59 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg
|
|
||||||
class="formula-content"
|
|
||||||
overflow="visible"
|
|
||||||
:width="box.w + 32"
|
|
||||||
:height="box.h + 32"
|
|
||||||
stroke="#000"
|
|
||||||
stroke-width="1"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
:transform="`scale(${scale}, ${scale}) translate(0,0) matrix(1,0,0,1,0,0)`"
|
|
||||||
transform-origin="0 50%"
|
|
||||||
>
|
|
||||||
<path :d="pathd"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { hfmath } from './hfmath'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
latex: string
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const box = ref({ x: 0, y: 0, w: 0, h: 0 })
|
|
||||||
const pathd = ref('')
|
|
||||||
|
|
||||||
watch(() => props.latex, () => {
|
|
||||||
const eq = new hfmath(props.latex)
|
|
||||||
pathd.value = eq.pathd({})
|
|
||||||
box.value = eq.box({})
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
const scale = computed(() => {
|
|
||||||
const boxW = box.value.w + 32
|
|
||||||
const boxH = box.value.h + 32
|
|
||||||
|
|
||||||
if (boxW > props.width || boxH > props.height) {
|
|
||||||
if (boxW / boxH > props.width / props.height) return props.width / boxW
|
|
||||||
return props.height / boxH
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
svg {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="symbol-content" v-html="svg"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { hfmath } from './hfmath'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
latex: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const svg = computed(() => {
|
|
||||||
const eq = new hfmath(props.latex)
|
|
||||||
return eq.svg({
|
|
||||||
SCALE_X: 10,
|
|
||||||
SCALE_Y: 10,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { hfmath, CONFIG as hfmathConfig } from 'hfmath'
|
|
||||||
|
|
||||||
hfmathConfig.SUB_SUP_SCALE = 0.5
|
|
||||||
|
|
||||||
export { hfmath }
|
|
|
@ -1,267 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="latex-editor">
|
|
||||||
<div class="container">
|
|
||||||
<div class="left">
|
|
||||||
<div class="input-area">
|
|
||||||
<TextArea v-model:value="latex" placeholder="输入 LaTeX 公式" ref="textAreaRef" />
|
|
||||||
</div>
|
|
||||||
<div class="preview">
|
|
||||||
<div class="placeholder" v-if="!latex">公式预览</div>
|
|
||||||
<div class="preview-content" v-else>
|
|
||||||
<FormulaContent
|
|
||||||
:width="518"
|
|
||||||
:height="138"
|
|
||||||
:latex="latex"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<Tabs
|
|
||||||
:tabs="tabs"
|
|
||||||
v-model:value="toolbarState"
|
|
||||||
card
|
|
||||||
/>
|
|
||||||
<div class="content">
|
|
||||||
<div class="symbol" v-if="toolbarState === 'symbol'">
|
|
||||||
<Tabs
|
|
||||||
:tabs="symbolTabs"
|
|
||||||
v-model:value="selectedSymbolKey"
|
|
||||||
spaceBetween
|
|
||||||
:tabsStyle="{ margin: '10px 10px 0' }"
|
|
||||||
/>
|
|
||||||
<div class="symbol-pool">
|
|
||||||
<div class="symbol-item" v-for="item in symbolPool" :key="item.latex" @click="insertSymbol(item.latex)">
|
|
||||||
<SymbolContent :latex="item.latex" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="formula" v-else>
|
|
||||||
<div class="formula-item" v-for="item in formulaList" :key="item.label">
|
|
||||||
<div class="formula-title">{{item.label}}</div>
|
|
||||||
<div class="formula-item-content" @click="latex = item.latex">
|
|
||||||
<FormulaContent
|
|
||||||
:width="236"
|
|
||||||
:height="60"
|
|
||||||
:latex="item.latex"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<Button class="btn" @click="emit('close')">取消</Button>
|
|
||||||
<Button class="btn" type="primary" @click="update()">确定</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
import { hfmath } from './hfmath'
|
|
||||||
import { FORMULA_LIST, SYMBOL_LIST } from '../../configs/latex'
|
|
||||||
import message from '../../utils/message'
|
|
||||||
|
|
||||||
import FormulaContent from './FormulaContent.vue'
|
|
||||||
import SymbolContent from './SymbolContent.vue'
|
|
||||||
import Button from '../Button.vue'
|
|
||||||
import TextArea from '../TextArea.vue'
|
|
||||||
import Tabs from '../Tabs.vue'
|
|
||||||
|
|
||||||
interface TabItem {
|
|
||||||
key: 'symbol' | 'formula'
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: TabItem[] = [
|
|
||||||
{ label: '常用符号', key: 'symbol' },
|
|
||||||
{ label: '预置公式', key: 'formula' },
|
|
||||||
]
|
|
||||||
|
|
||||||
interface LatexResult {
|
|
||||||
latex: string
|
|
||||||
path: string
|
|
||||||
w: number
|
|
||||||
h: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value?: string
|
|
||||||
}>(), {
|
|
||||||
value: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update', payload: LatexResult): void
|
|
||||||
(event: 'close'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const formulaList = FORMULA_LIST
|
|
||||||
|
|
||||||
const symbolTabs = SYMBOL_LIST.map(item => ({
|
|
||||||
label: item.label,
|
|
||||||
key: item.type,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const latex = ref('')
|
|
||||||
const toolbarState = ref<'symbol' | 'formula'>('symbol')
|
|
||||||
const textAreaRef = ref<InstanceType<typeof TextArea>>()
|
|
||||||
|
|
||||||
const selectedSymbolKey = ref(SYMBOL_LIST[0].type)
|
|
||||||
const symbolPool = computed(() => {
|
|
||||||
const selectedSymbol = SYMBOL_LIST.find(item => item.type === selectedSymbolKey.value)
|
|
||||||
return selectedSymbol?.children || []
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.value) latex.value = props.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
if (!latex.value) return message.error('公式不能为空')
|
|
||||||
|
|
||||||
const eq = new hfmath(latex.value)
|
|
||||||
const pathd = eq.pathd({})
|
|
||||||
const box = eq.box({})
|
|
||||||
|
|
||||||
emit('update', {
|
|
||||||
latex: latex.value,
|
|
||||||
path: pathd,
|
|
||||||
w: box.w + 32,
|
|
||||||
h: box.h + 32,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertSymbol = (latex: string) => {
|
|
||||||
if (!textAreaRef.value) return
|
|
||||||
textAreaRef.value.focus()
|
|
||||||
document.execCommand('insertText', false, latex)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../../assets/styles/variable.scss";
|
|
||||||
@import "../../assets/styles/mixin.scss";
|
|
||||||
.latex-editor {
|
|
||||||
height: 560px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
height: calc(100% - 50px);
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.left {
|
|
||||||
width: 540px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.input-area {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
height: 100% !important;
|
|
||||||
border-color: $borderColor !important;
|
|
||||||
padding: 10px !important;
|
|
||||||
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preview {
|
|
||||||
height: 160px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
border: 1px solid $borderColor;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
color: #888;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.preview-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.right {
|
|
||||||
width: 280px;
|
|
||||||
height: 100%;
|
|
||||||
margin-left: 20px;
|
|
||||||
border: solid 1px $borderColor;
|
|
||||||
background-color: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
height: calc(100% - 40px);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.formula {
|
|
||||||
height: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
@include overflow-overlay();
|
|
||||||
}
|
|
||||||
.formula-item {
|
|
||||||
& + .formula-item {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formula-title {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.formula-item-content {
|
|
||||||
height: 60px;
|
|
||||||
padding: 5px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: $lightGray;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.symbol {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.symbol-pool {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
@include overflow-overlay();
|
|
||||||
}
|
|
||||||
.symbol-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $lightGray;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,184 +0,0 @@
|
||||||
<template>
|
|
||||||
<Transition
|
|
||||||
name="message-fade"
|
|
||||||
appear
|
|
||||||
mode="in-out"
|
|
||||||
@beforeLeave="emit('close')"
|
|
||||||
@afterLeave="emit('destroy')"
|
|
||||||
>
|
|
||||||
<div class="message" :id="id" v-if="visible">
|
|
||||||
<div class="message-container"
|
|
||||||
@mouseenter="clearTimer()"
|
|
||||||
@mouseleave="startTimer()"
|
|
||||||
>
|
|
||||||
<div class="icons">
|
|
||||||
<IconAttention theme="filled" size="18" fill="#faad14" v-if="type === 'warning'" />
|
|
||||||
<IconCheckOne theme="filled" size="18" fill="#52c41a" v-if="type === 'success'" />
|
|
||||||
<IconCloseOne theme="filled" size="18" fill="#ff4d4f" v-if="type === 'error'" />
|
|
||||||
<IconInfo theme="filled" size="18" fill="#1677ff" v-if="type === 'info'" />
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="title" v-if="title">{{ title }}</div>
|
|
||||||
<div class="description">{{ message }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="control" v-if="closable">
|
|
||||||
<span
|
|
||||||
class="close-btn"
|
|
||||||
@click="close()"
|
|
||||||
>
|
|
||||||
<IconCloseSmall />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, ref, onBeforeMount } from 'vue'
|
|
||||||
import { icons } from '../plugins/icon'
|
|
||||||
|
|
||||||
const {
|
|
||||||
IconAttention,
|
|
||||||
IconCheckOne,
|
|
||||||
IconCloseOne,
|
|
||||||
IconInfo,
|
|
||||||
IconCloseSmall,
|
|
||||||
} = icons
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
id: string
|
|
||||||
message: string
|
|
||||||
type?: string
|
|
||||||
title?: string
|
|
||||||
duration?: number
|
|
||||||
closable?: boolean
|
|
||||||
}>(), {
|
|
||||||
type: 'success',
|
|
||||||
title: '',
|
|
||||||
duration: 3000,
|
|
||||||
closable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'close'): void
|
|
||||||
(event: 'destroy'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const visible = ref(true)
|
|
||||||
const timer = ref<number | null>(null)
|
|
||||||
|
|
||||||
const startTimer = () => {
|
|
||||||
if (props.duration <= 0) return
|
|
||||||
timer.value = setTimeout(close, props.duration)
|
|
||||||
}
|
|
||||||
const clearTimer = () => {
|
|
||||||
if (timer.value) clearTimeout(timer.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => visible.value = false
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
clearTimer()
|
|
||||||
})
|
|
||||||
onMounted(() => {
|
|
||||||
startTimer()
|
|
||||||
})
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
close,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.message {
|
|
||||||
max-width: 600px;
|
|
||||||
|
|
||||||
& + & {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.message-container {
|
|
||||||
min-width: 50px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
box-shadow: 0 1px 8px rgba(0, 0, 0, .15);
|
|
||||||
background: #fff;
|
|
||||||
pointer-events: all;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
line-height: 1.5;
|
|
||||||
color: $textColor;
|
|
||||||
}
|
|
||||||
.title + .description {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.control {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.close-btn {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #666;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-fade-enter-active {
|
|
||||||
animation: message-fade-in-down .3s;
|
|
||||||
}
|
|
||||||
.message-fade-leave-active {
|
|
||||||
animation: message-fade-out .3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes message-fade-in-down {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes message-fade-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
margin-top: -45px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,156 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="modal-fade">
|
|
||||||
<div class="modal" ref="modalRef" v-show="visible" tabindex="-1" @keyup.esc="onEsc()">
|
|
||||||
<div class="mask" @click="onClickMask()"></div>
|
|
||||||
<Transition name="modal-zoom"
|
|
||||||
@afterLeave="contentVisible = false"
|
|
||||||
@before-enter="contentVisible = true"
|
|
||||||
>
|
|
||||||
<div class="modal-content" v-show="visible" :style="contentStyle">
|
|
||||||
<span class="close-btn" v-if="closeButton" @click="close()"><IconClose /></span>
|
|
||||||
<slot v-if="contentVisible"></slot>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, nextTick, ref, watch, type CSSProperties } from 'vue'
|
|
||||||
import { icons } from '../plugins/icon'
|
|
||||||
|
|
||||||
const { IconClose } = icons
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
visible: boolean
|
|
||||||
width?: number
|
|
||||||
closeButton?: boolean
|
|
||||||
closeOnClickMask?: boolean
|
|
||||||
closeOnEsc?: boolean
|
|
||||||
contentStyle?: CSSProperties
|
|
||||||
}>(), {
|
|
||||||
width: 480,
|
|
||||||
closeButton: false,
|
|
||||||
closeOnClickMask: true,
|
|
||||||
closeOnEsc: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const modalRef = ref<HTMLDivElement>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:visible', payload: boolean): void
|
|
||||||
(event: 'closed'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const contentVisible = ref(false)
|
|
||||||
|
|
||||||
const contentStyle = computed(() => {
|
|
||||||
return {
|
|
||||||
width: props.width + 'px',
|
|
||||||
...(props.contentStyle || {})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.visible, () => {
|
|
||||||
if (props.visible) {
|
|
||||||
nextTick(() => modalRef.value!.focus())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
emit('update:visible', false)
|
|
||||||
emit('closed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEsc = () => {
|
|
||||||
if (props.visible && props.closeOnEsc) close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickMask = () => {
|
|
||||||
if (props.closeOnClickMask) close()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.modal, .mask {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
position: fixed;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
outline: 0;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mask {
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(0, 0, 0, .25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
z-index: 5001;
|
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .2);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-fade-enter-active {
|
|
||||||
animation: modal-fade-enter .25s both ease-in;
|
|
||||||
}
|
|
||||||
.modal-fade-leave-active {
|
|
||||||
animation: modal-fade-leave .25s both ease-out;
|
|
||||||
}
|
|
||||||
.modal-zoom-enter-active {
|
|
||||||
animation: modal-zoom-enter .25s both cubic-bezier(.4, 0, 0, 1.5);
|
|
||||||
}
|
|
||||||
.modal-zoom-leave-active {
|
|
||||||
animation: modal-zoom-leave .25s both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modal-fade-enter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes modal-fade-leave {
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes modal-zoom-enter {
|
|
||||||
from {
|
|
||||||
transform: scale3d(.3, .3, .3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes modal-zoom-leave {
|
|
||||||
to {
|
|
||||||
transform: scale3d(.3, .3, .3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,222 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="moveable-panel"
|
|
||||||
ref="moveablePanelRef"
|
|
||||||
:style="{
|
|
||||||
width: w + 'px',
|
|
||||||
height: h ? h + 'px' : 'auto',
|
|
||||||
left: x + 'px',
|
|
||||||
top: y + 'px',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template v-if="title">
|
|
||||||
<div class="header" @mousedown="$event => startMove($event)">
|
|
||||||
<div class="title">{{title}}</div>
|
|
||||||
<div class="close-btn" @click="emit('close')"><IconClose /></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-else class="content" @mousedown="$event => startMove($event)">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="resizer" v-if="resizeable" @mousedown="$event => startResize($event)"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
minWidth?: number
|
|
||||||
minHeight?: number
|
|
||||||
maxWidth?: number
|
|
||||||
maxHeight?: number
|
|
||||||
left?: number
|
|
||||||
top?: number
|
|
||||||
title?: string
|
|
||||||
moveable?: boolean
|
|
||||||
resizeable?: boolean
|
|
||||||
}>(), {
|
|
||||||
minWidth: 20,
|
|
||||||
minHeight: 20,
|
|
||||||
maxWidth: 500,
|
|
||||||
maxHeight: 500,
|
|
||||||
left: 10,
|
|
||||||
top: 10,
|
|
||||||
title: '',
|
|
||||||
moveable: true,
|
|
||||||
resizeable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'close'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const x = ref(0)
|
|
||||||
const y = ref(0)
|
|
||||||
const w = ref(0)
|
|
||||||
const h = ref(0)
|
|
||||||
const moveablePanelRef = ref<HTMLElement>()
|
|
||||||
const realHeight = computed(() => {
|
|
||||||
if (!h.value) {
|
|
||||||
return moveablePanelRef.value?.clientHeight || 0
|
|
||||||
}
|
|
||||||
return h.value
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.left >= 0) x.value = props.left
|
|
||||||
else x.value = document.body.clientWidth + props.left - props.width
|
|
||||||
|
|
||||||
if (props.top >= 0) y.value = props.top
|
|
||||||
else y.value = document.body.clientHeight + props.top - realHeight.value
|
|
||||||
|
|
||||||
w.value = props.width
|
|
||||||
h.value = props.height
|
|
||||||
})
|
|
||||||
|
|
||||||
const startMove = (e: MouseEvent) => {
|
|
||||||
if (!props.moveable) return
|
|
||||||
|
|
||||||
let isMouseDown = true
|
|
||||||
|
|
||||||
const windowWidth = document.body.clientWidth
|
|
||||||
const clientHeight = document.body.clientHeight
|
|
||||||
|
|
||||||
const startPageX = e.pageX
|
|
||||||
const startPageY = e.pageY
|
|
||||||
|
|
||||||
const originLeft = x.value
|
|
||||||
const originTop = y.value
|
|
||||||
|
|
||||||
document.onmousemove = e => {
|
|
||||||
if (!isMouseDown) return
|
|
||||||
|
|
||||||
const moveX = e.pageX - startPageX
|
|
||||||
const moveY = e.pageY - startPageY
|
|
||||||
|
|
||||||
let left = originLeft + moveX
|
|
||||||
let top = originTop + moveY
|
|
||||||
|
|
||||||
if (left < 0) left = 0
|
|
||||||
if (top < 0) top = 0
|
|
||||||
if (left + w.value > windowWidth) left = windowWidth - w.value
|
|
||||||
if (top + realHeight.value > clientHeight) top = clientHeight - realHeight.value
|
|
||||||
|
|
||||||
x.value = left
|
|
||||||
y.value = top
|
|
||||||
}
|
|
||||||
document.onmouseup = () => {
|
|
||||||
isMouseDown = false
|
|
||||||
|
|
||||||
document.onmousemove = 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>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.moveable-panel {
|
|
||||||
position: fixed;
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: $boxShadow;
|
|
||||||
border: 1px solid $borderColor;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
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 {
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 13px;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
.close-btn {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: #666;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,203 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="number-input"
|
|
||||||
:class="{
|
|
||||||
'disabled': disabled,
|
|
||||||
'focused': focused,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="prefix">
|
|
||||||
<slot name="prefix"></slot>
|
|
||||||
</span>
|
|
||||||
<div class="input-wrap">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
:disabled="disabled"
|
|
||||||
v-model="number"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
@input="$event => emit('input', $event)"
|
|
||||||
@focus="$event => handleFocus($event)"
|
|
||||||
@blur="$event => handleBlur($event)"
|
|
||||||
@change="$event => emit('change', $event)"
|
|
||||||
@keydown.enter="$event => handleEnter($event)"
|
|
||||||
/>
|
|
||||||
<div class="handlers">
|
|
||||||
<span class="handler" @click="number += step">
|
|
||||||
<svg fill="currentColor" width="1em" height="1em" viewBox="64 64 896 896"><path d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"></path></svg>
|
|
||||||
</span>
|
|
||||||
<span class="handler" @click="number -= step">
|
|
||||||
<svg fill="currentColor" width="1em" height="1em" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="suffix">
|
|
||||||
<slot name="suffix"></slot>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: number
|
|
||||||
disabled?: boolean
|
|
||||||
placeholder?: string
|
|
||||||
min?: number
|
|
||||||
max?: number
|
|
||||||
step?: number
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
placeholder: '',
|
|
||||||
min: 0,
|
|
||||||
max: Infinity,
|
|
||||||
step: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: number): void
|
|
||||||
(event: 'input', payload: Event): void
|
|
||||||
(event: 'change', payload: Event): void
|
|
||||||
(event: 'blur', payload: Event): void
|
|
||||||
(event: 'focus', payload: Event): void
|
|
||||||
(event: 'enter', payload: Event): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const number = ref(0)
|
|
||||||
const focused = ref(false)
|
|
||||||
|
|
||||||
watch(() => props.value, () => {
|
|
||||||
if (props.value !== number.value) {
|
|
||||||
number.value = props.value
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
immediate: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(number, () => {
|
|
||||||
const value = +number.value
|
|
||||||
if (isNaN(value)) return
|
|
||||||
else if (value > props.max) return
|
|
||||||
else if (value < props.min) return
|
|
||||||
|
|
||||||
number.value = value
|
|
||||||
emit('update:value', number.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const checkAndEmitValue = () => {
|
|
||||||
let value = +number.value
|
|
||||||
if (isNaN(value)) value = props.min
|
|
||||||
else if (value > props.max) value = props.max
|
|
||||||
else if (value < props.min) value = props.min
|
|
||||||
|
|
||||||
number.value = value
|
|
||||||
emit('update:value', number.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEnter = (e: Event) => {
|
|
||||||
checkAndEmitValue()
|
|
||||||
emit('enter', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBlur = (e: Event) => {
|
|
||||||
checkAndEmitValue()
|
|
||||||
focused.value = false
|
|
||||||
emit('blur', e)
|
|
||||||
}
|
|
||||||
const handleFocus = (e: Event) => {
|
|
||||||
focused.value = true
|
|
||||||
emit('focus', e)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.number-input {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
padding: 0 0 0 5px;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
transition: border-color .25s;
|
|
||||||
font-size: 13px;
|
|
||||||
display: inline-flex;
|
|
||||||
|
|
||||||
.input-wrap {
|
|
||||||
flex: 1;
|
|
||||||
color: $textColor;
|
|
||||||
padding: 0 0 0 5px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
&:not(.disabled) .input-wrap:hover .handlers {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.handlers {
|
|
||||||
width: 20px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: 6px;
|
|
||||||
color: #999;
|
|
||||||
opacity: 0;
|
|
||||||
user-select: none;
|
|
||||||
transition: opacity .25s;
|
|
||||||
|
|
||||||
.handler {
|
|
||||||
width: 100%;
|
|
||||||
height: 50%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-left: 1px solid #d9d9d9;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
& + .handler {
|
|
||||||
border-top: 1px solid #d9d9d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
outline: 0;
|
|
||||||
border: 0;
|
|
||||||
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #bfbfbf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.disabled):hover, &.focused {
|
|
||||||
border-color: $themeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-color: #dcdcdc;
|
|
||||||
color: #b7b7b7;
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: #b7b7b7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prefix, .suffix {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 30px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,105 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="popover" :class="{ 'center': center }" ref="triggerRef">
|
|
||||||
<div class="popover-content" :style="contentStyle" ref="contentRef">
|
|
||||||
<slot name="content" v-if="contentVisible"></slot>
|
|
||||||
</div>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { type CSSProperties, onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
|
||||||
import tippy, { type Instance, type Placement } from 'tippy.js'
|
|
||||||
|
|
||||||
import 'tippy.js/animations/scale.css'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value?: boolean
|
|
||||||
trigger?: 'click' | 'mouseenter' | 'manual'
|
|
||||||
placement?: Placement
|
|
||||||
appendTo?: HTMLElement | 'parent'
|
|
||||||
contentStyle?: CSSProperties
|
|
||||||
center?: boolean
|
|
||||||
offset?: number
|
|
||||||
}>(), {
|
|
||||||
value: false,
|
|
||||||
trigger: 'click',
|
|
||||||
placement: 'bottom',
|
|
||||||
center: false,
|
|
||||||
offset: 8,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const instance = ref<Instance>()
|
|
||||||
const triggerRef = ref<HTMLElement>()
|
|
||||||
const contentRef = ref<HTMLElement>()
|
|
||||||
const contentVisible = ref(false)
|
|
||||||
|
|
||||||
const contentStyle = computed(() => {
|
|
||||||
return props.contentStyle || {}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.value, () => {
|
|
||||||
if (!instance.value) return
|
|
||||||
if (props.value) instance.value.show()
|
|
||||||
else instance.value.hide()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (instance.value) instance.value.destroy()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
instance.value = tippy(triggerRef.value!, {
|
|
||||||
content: contentRef.value!,
|
|
||||||
allowHTML: true,
|
|
||||||
trigger: props.trigger,
|
|
||||||
placement: props.placement,
|
|
||||||
interactive: true,
|
|
||||||
appendTo: props.appendTo || document.body,
|
|
||||||
maxWidth: 'none',
|
|
||||||
offset: [0, props.offset],
|
|
||||||
duration: 200,
|
|
||||||
animation: 'scale',
|
|
||||||
theme: 'popover',
|
|
||||||
onShow() {
|
|
||||||
contentVisible.value = true
|
|
||||||
},
|
|
||||||
onShown() {
|
|
||||||
if (!props.value) emit('update:value', true)
|
|
||||||
},
|
|
||||||
onHidden() {
|
|
||||||
if (props.value) emit('update:value', false)
|
|
||||||
contentVisible.value = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.popover.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.popover-content {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid $borderColor;
|
|
||||||
box-shadow: $boxShadow;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.tippy-box[data-theme~='popover'] {
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="popover-menu-item" :class="{ 'center': center }" @click="emit('click')">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
center?: boolean
|
|
||||||
}>(), {
|
|
||||||
center: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'click'): void
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.popover-menu-item {
|
|
||||||
min-width: 80px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
}
|
|
||||||
& + .popover-menu-item {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,26 +0,0 @@
|
||||||
<template>
|
|
||||||
<Button
|
|
||||||
:checked="!disabled && _value === value"
|
|
||||||
:disabled="disabled"
|
|
||||||
type="radio"
|
|
||||||
@click="!disabled && updateValue(value)"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { inject } from 'vue'
|
|
||||||
import { injectKeyRadioGroupValue, type RadioGroupValue } from '../types/injectKey'
|
|
||||||
|
|
||||||
import Button from './Button.vue'
|
|
||||||
|
|
||||||
const { value: _value, updateValue } = inject(injectKeyRadioGroupValue) as RadioGroupValue
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
value: string
|
|
||||||
disabled?: boolean
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,35 +0,0 @@
|
||||||
<template>
|
|
||||||
<ButtonGroup class="radio-group">
|
|
||||||
<slot></slot>
|
|
||||||
</ButtonGroup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, provide } from 'vue'
|
|
||||||
import { injectKeyRadioGroupValue } from '../types/injectKey'
|
|
||||||
|
|
||||||
import ButtonGroup from './ButtonGroup.vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: string
|
|
||||||
disabled?: boolean
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const updateValue = (value: string) => {
|
|
||||||
if (props.disabled) return
|
|
||||||
emit('update:value', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = computed(() => props.value)
|
|
||||||
|
|
||||||
provide(injectKeyRadioGroupValue, {
|
|
||||||
value,
|
|
||||||
updateValue,
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,206 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="select-wrap" v-if="disabled">
|
|
||||||
<div class="select disabled" ref="selectRef">
|
|
||||||
<div class="selector">{{ value }}</div>
|
|
||||||
<div class="icon">
|
|
||||||
<slot name="icon">
|
|
||||||
<IconDown :size="14" />
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Popover
|
|
||||||
class="select-wrap"
|
|
||||||
trigger="click"
|
|
||||||
v-model:value="popoverVisible"
|
|
||||||
placement="bottom"
|
|
||||||
:contentStyle="{
|
|
||||||
padding: 0,
|
|
||||||
boxShadow: '0 6px 16px 0 rgba(0, 0, 0, 0.08)',
|
|
||||||
}"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
<template #content>
|
|
||||||
<template v-if="search">
|
|
||||||
<Input ref="searchInputRef" simple :placeholder="searchLabel" v-model:value="searchKey" :style="{ width: width + 2 + 'px' }" />
|
|
||||||
<Divider :margin="0" />
|
|
||||||
</template>
|
|
||||||
<div class="options" :style="{ width: width + 2 + 'px' }">
|
|
||||||
<div class="option"
|
|
||||||
:class="{
|
|
||||||
'disabled': option.disabled,
|
|
||||||
'selected': option.value === value,
|
|
||||||
}"
|
|
||||||
v-for="option in showOptions"
|
|
||||||
:key="option.value"
|
|
||||||
@click="handleSelect(option)"
|
|
||||||
>{{ option.label }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="select" ref="selectRef">
|
|
||||||
<div class="selector">{{ showLabel }}</div>
|
|
||||||
<div class="icon">
|
|
||||||
<slot name="icon">
|
|
||||||
<IconDown :size="14" />
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch, nextTick, onBeforeUnmount } from 'vue'
|
|
||||||
import Popover from './Popover.vue'
|
|
||||||
import Input from './Input.vue'
|
|
||||||
import Divider from './Divider.vue'
|
|
||||||
|
|
||||||
interface SelectOption {
|
|
||||||
label: string
|
|
||||||
value: string | number
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: string | number
|
|
||||||
options: SelectOption[]
|
|
||||||
disabled?: boolean
|
|
||||||
search?: boolean
|
|
||||||
searchLabel?: string
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
search: false,
|
|
||||||
searchLabel: '搜索',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: string | number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const popoverVisible = ref(false)
|
|
||||||
const selectRef = ref<HTMLElement>()
|
|
||||||
const searchInputRef = ref<InstanceType<typeof Input>>()
|
|
||||||
const width = ref(0)
|
|
||||||
const searchKey = ref('')
|
|
||||||
|
|
||||||
const showLabel = computed(() => {
|
|
||||||
return props.options.find(item => item.value === props.value)?.label || props.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const showOptions = computed(() => {
|
|
||||||
if (!props.search) return props.options
|
|
||||||
if (!searchKey.value.trim()) return props.options
|
|
||||||
const opts = props.options.filter(item => {
|
|
||||||
return item.label.toLowerCase().indexOf(searchKey.value.toLowerCase()) !== -1
|
|
||||||
})
|
|
||||||
return opts.length ? opts : props.options
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(popoverVisible, () => {
|
|
||||||
if (popoverVisible.value) {
|
|
||||||
nextTick(() => {
|
|
||||||
if (searchInputRef.value) searchInputRef.value.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else searchKey.value = ''
|
|
||||||
})
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
searchKey.value = ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateWidth = () => {
|
|
||||||
if (!selectRef.value) return
|
|
||||||
width.value = selectRef.value.clientWidth
|
|
||||||
}
|
|
||||||
const resizeObserver = new ResizeObserver(updateWidth)
|
|
||||||
onMounted(() => {
|
|
||||||
if (!selectRef.value) return
|
|
||||||
resizeObserver.observe(selectRef.value)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (!selectRef.value) return
|
|
||||||
resizeObserver.unobserve(selectRef.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSelect = (option: SelectOption) => {
|
|
||||||
if (option.disabled) return
|
|
||||||
|
|
||||||
emit('update:value', option.value)
|
|
||||||
popoverVisible.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.select {
|
|
||||||
width: 100%;
|
|
||||||
height: 32px;
|
|
||||||
padding-right: 32px;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
transition: border-color .25s;
|
|
||||||
font-size: 13px;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:not(.disabled):hover {
|
|
||||||
border-color: $themeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-color: #dcdcdc;
|
|
||||||
color: #b7b7b7;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
min-width: 50px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
padding-left: 10px;
|
|
||||||
@include ellipsis-oneline();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.options {
|
|
||||||
max-height: 260px;
|
|
||||||
padding: 5px;
|
|
||||||
overflow: auto;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 13px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.option {
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
padding: 0 5px;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
@include ellipsis-oneline();
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
color: #b7b7b7;
|
|
||||||
}
|
|
||||||
&:not(.disabled, .selected):hover {
|
|
||||||
background-color: rgba($color: $themeColor, $alpha: .05);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
color: $themeColor;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 30px;
|
|
||||||
color: #bfbfbf;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,56 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="select-group">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.select-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
::v-deep(.select-wrap) {
|
|
||||||
.select {
|
|
||||||
border-radius: 0;
|
|
||||||
border-left-width: 0;
|
|
||||||
border-right-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + .select-wrap {
|
|
||||||
.select {
|
|
||||||
border-left-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
& + .select-wrap {
|
|
||||||
.select {
|
|
||||||
border-left-color: $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
.select {
|
|
||||||
border-top-left-radius: $borderRadius;
|
|
||||||
border-bottom-left-radius: $borderRadius;
|
|
||||||
border-left-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
.select {
|
|
||||||
border-top-right-radius: $borderRadius;
|
|
||||||
border-bottom-right-radius: $borderRadius;
|
|
||||||
border-right-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,283 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="slider" :class="{ 'disabled': disabled }" ref="sliderRef" @mousedown="$event => handleMousedown($event)">
|
|
||||||
<div class="bar">
|
|
||||||
<template v-if="!range">
|
|
||||||
<div class="track" :style="{ width: `${percentage}%` }"></div>
|
|
||||||
<div class="thumb" :style="{ left: `${percentage}%` }" :data-tooltip="tooltipValue"></div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="track" :style="{ width: `${end - start}%`, left: `${start}%` }"></div>
|
|
||||||
<div class="thumb" :style="{ left: `${start}%` }" :data-tooltip="tooltipRangeStartValue"></div>
|
|
||||||
<div class="thumb" :style="{ left: `${end}%` }" :data-tooltip="tooltipRangeEndValue"></div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import NP from 'number-precision'
|
|
||||||
|
|
||||||
const getBoundingClientRectViewLeft = (element: HTMLElement) => {
|
|
||||||
return element.getBoundingClientRect().left
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: number | [number, number]
|
|
||||||
disabled?: boolean
|
|
||||||
min?: number
|
|
||||||
max?: number
|
|
||||||
step?: number
|
|
||||||
range?: boolean
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
step: 1,
|
|
||||||
range: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: number | [number, number]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const sliderRef = ref<HTMLElement>()
|
|
||||||
const percentage = ref(0)
|
|
||||||
const start = ref(0)
|
|
||||||
const end = ref(0)
|
|
||||||
const handler = ref<'start' | 'end'>('end')
|
|
||||||
|
|
||||||
const getNewValue = (percentage: number) => {
|
|
||||||
let diff = percentage / 100 * (props.max - props.min)
|
|
||||||
if (props.step >= 1) diff = Math.fround(diff)
|
|
||||||
else {
|
|
||||||
const str = props.step.toString()
|
|
||||||
const match = str.match(/^[0.]*([1-9])/)
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const targetNumber = match[1]
|
|
||||||
const position = str.indexOf(targetNumber) - 1
|
|
||||||
if (position > 0) {
|
|
||||||
const accuracy = Math.pow(10, position)
|
|
||||||
diff = Math.fround(diff * accuracy) / accuracy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NP.plus(diff, props.min)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tooltipValue = computed(() => {
|
|
||||||
return getNewValue(percentage.value)
|
|
||||||
})
|
|
||||||
const tooltipRangeStartValue = computed(() => {
|
|
||||||
return getNewValue(start.value)
|
|
||||||
})
|
|
||||||
const tooltipRangeEndValue = computed(() => {
|
|
||||||
return getNewValue(end.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.value, () => {
|
|
||||||
if (props.max === props.min) return
|
|
||||||
if (typeof props.value === 'number') {
|
|
||||||
percentage.value = (props.value - props.min) / (props.max - props.min) * 100
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
start.value = (props.value[0] - props.min) / (props.max - props.min) * 100
|
|
||||||
end.value = (props.value[1] - props.min) / (props.max - props.min) * 100
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
immediate: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const getPercentage = (e: MouseEvent | TouchEvent) => {
|
|
||||||
if (!sliderRef.value) return 0
|
|
||||||
const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX
|
|
||||||
let progress = (clientX - getBoundingClientRectViewLeft(sliderRef.value)) / sliderRef.value.clientWidth
|
|
||||||
progress = Math.max(progress, 0)
|
|
||||||
progress = Math.min(progress, 1)
|
|
||||||
|
|
||||||
let _percentage = progress * 100
|
|
||||||
const step = props.step / (props.max - props.min) * 100
|
|
||||||
const remainder = _percentage % step
|
|
||||||
|
|
||||||
if (remainder > 0) {
|
|
||||||
if (remainder <= step / 2) _percentage = _percentage - remainder
|
|
||||||
else _percentage = _percentage - remainder + step
|
|
||||||
}
|
|
||||||
return _percentage
|
|
||||||
}
|
|
||||||
|
|
||||||
// 双滑块(范围)模式
|
|
||||||
const updateRange = (e: MouseEvent | TouchEvent) => {
|
|
||||||
const value = getPercentage(e)
|
|
||||||
|
|
||||||
if (handler.value === 'start') start.value = value
|
|
||||||
else end.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRangeEnd = (e: MouseEvent | TouchEvent) => {
|
|
||||||
updatePercentage(e)
|
|
||||||
const newValue = getNewValue(percentage.value)
|
|
||||||
const oldValueArr = props.value as [number, number]
|
|
||||||
const newValueArr: [number, number] = handler.value === 'start' ? [newValue, oldValueArr[1]] : [oldValueArr[0], newValue]
|
|
||||||
if (newValueArr[0] > newValueArr[1]) {
|
|
||||||
[newValueArr[0], newValueArr[1]] = [newValueArr[1], newValueArr[0]]
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('update:value', newValueArr)
|
|
||||||
|
|
||||||
document.removeEventListener('mousemove', updateRange)
|
|
||||||
document.removeEventListener('touchmove', updateRange)
|
|
||||||
document.removeEventListener('mouseup', updateRangeEnd)
|
|
||||||
document.removeEventListener('touchend', updateRangeEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 单滑块模式
|
|
||||||
const updatePercentage = (e: MouseEvent | TouchEvent) => {
|
|
||||||
percentage.value = getPercentage(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePercentageEnd = (e: MouseEvent | TouchEvent) => {
|
|
||||||
updatePercentage(e)
|
|
||||||
const newValue = getNewValue(percentage.value)
|
|
||||||
|
|
||||||
emit('update:value', newValue)
|
|
||||||
|
|
||||||
document.removeEventListener('mousemove', updatePercentage)
|
|
||||||
document.removeEventListener('touchmove', updatePercentage)
|
|
||||||
document.removeEventListener('mouseup', updatePercentageEnd)
|
|
||||||
document.removeEventListener('touchend', updatePercentageEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMousedown = (e: MouseEvent | TouchEvent) => {
|
|
||||||
if (props.disabled) return
|
|
||||||
|
|
||||||
if (props.range) {
|
|
||||||
const _percentage = getPercentage(e)
|
|
||||||
|
|
||||||
if (Math.abs(_percentage - start.value) < Math.abs(_percentage - end.value)) {
|
|
||||||
handler.value = 'start'
|
|
||||||
}
|
|
||||||
else handler.value = 'end'
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', updateRange)
|
|
||||||
document.addEventListener('touchmove', updateRange)
|
|
||||||
document.addEventListener('mouseup', updateRangeEnd)
|
|
||||||
document.addEventListener('touchend', updateRangeEnd)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.addEventListener('mousemove', updatePercentage)
|
|
||||||
document.addEventListener('touchmove', updatePercentage)
|
|
||||||
document.addEventListener('mouseup', updatePercentageEnd)
|
|
||||||
document.addEventListener('touchend', updatePercentageEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.slider {
|
|
||||||
width: 100%;
|
|
||||||
height: 12px;
|
|
||||||
padding: 4px 0;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
.track {
|
|
||||||
background-color: #b4b4b4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb {
|
|
||||||
outline: 2px solid #b4b4b4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.slider:not(.disabled) {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
&:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.track {
|
|
||||||
&:hover {
|
|
||||||
background-color: $themeHoverColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb {
|
|
||||||
&:hover, &:active {
|
|
||||||
outline: 4px solid $themeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
margin-left: 5px;
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
position: relative;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
user-select: none;
|
|
||||||
transition: background-color .2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
background-color: $themeColor;
|
|
||||||
transition: background-color .2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumb {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background-color: #fff;
|
|
||||||
outline: 2px solid $themeColor;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
border-radius: 50%;
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
&::before, &::after {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: attr(data-tooltip);
|
|
||||||
min-width: 28px;
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 24px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background-color: #262626;
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
padding: 6px 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 15px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border: 5px solid transparent;
|
|
||||||
border-top-color: #262626;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,86 +0,0 @@
|
||||||
<template>
|
|
||||||
<span
|
|
||||||
class="switch"
|
|
||||||
:class="{
|
|
||||||
'active': value,
|
|
||||||
'disabled': disabled,
|
|
||||||
}"
|
|
||||||
@click="handleChange()"
|
|
||||||
>
|
|
||||||
<span class="switch-core"></span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleChange = () => {
|
|
||||||
if (props.disabled) return
|
|
||||||
emit('update:value', !props.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.switch {
|
|
||||||
height: 20px;
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:not(.disabled).active {
|
|
||||||
.switch-core {
|
|
||||||
border-color: $themeColor;
|
|
||||||
background-color: $themeColor;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
left: 100%;
|
|
||||||
margin-left: -17px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
.switch-core::after {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.switch-core {
|
|
||||||
margin: 0;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 40px;
|
|
||||||
height: 20px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #d9d9d9;
|
|
||||||
transition: border-color .3s, background-color .3s;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
left: 1px;
|
|
||||||
border-radius: 100%;
|
|
||||||
transition: all .3s;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,110 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="tabs"
|
|
||||||
:class="{
|
|
||||||
'card': card,
|
|
||||||
'space-around': spaceAround,
|
|
||||||
'space-between': spaceBetween,
|
|
||||||
}"
|
|
||||||
:style="tabsStyle || {}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="tab"
|
|
||||||
:class="{ 'active': tab.key === value }"
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.key"
|
|
||||||
:style="{
|
|
||||||
...(tabStyle || {}),
|
|
||||||
'--color': tab.color,
|
|
||||||
}"
|
|
||||||
@click="emit('update:value', tab.key)"
|
|
||||||
>{{tab.label}}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { type CSSProperties } from 'vue'
|
|
||||||
|
|
||||||
interface TabItem {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
color?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
value: string
|
|
||||||
tabs: TabItem[]
|
|
||||||
card?: boolean
|
|
||||||
tabsStyle?: CSSProperties
|
|
||||||
tabStyle?: CSSProperties
|
|
||||||
spaceAround?: boolean
|
|
||||||
spaceBetween?: boolean
|
|
||||||
}>(), {
|
|
||||||
card: false,
|
|
||||||
spaceAround: false,
|
|
||||||
spaceBetween: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: string): void
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
user-select: none;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&:not(.card) {
|
|
||||||
font-size: 13px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
border-bottom: 1px solid $borderColor;
|
|
||||||
|
|
||||||
&.space-around {
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
&.space-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
text-align: center;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
padding: 8px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-bottom: 2px solid var(--color, $themeColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.card {
|
|
||||||
height: 40px;
|
|
||||||
font-size: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: $lightGray;
|
|
||||||
border-bottom: 1px solid $borderColor;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: transparent;
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + .tab {
|
|
||||||
border-left: 1px solid $borderColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,94 +0,0 @@
|
||||||
<template>
|
|
||||||
<textarea
|
|
||||||
class="textarea"
|
|
||||||
:class="{
|
|
||||||
'disabled': disabled,
|
|
||||||
'resizable': resizable,
|
|
||||||
}"
|
|
||||||
ref="textareaRef"
|
|
||||||
:disabled="disabled"
|
|
||||||
:value="value"
|
|
||||||
:rows="rows"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:style="{
|
|
||||||
padding: padding ? `${padding}px` : '10px',
|
|
||||||
}"
|
|
||||||
@input="$event => handleInput($event)"
|
|
||||||
@focus="$event => emit('focus', $event)"
|
|
||||||
@blur="$event => emit('blur', $event)"
|
|
||||||
></textarea>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
value: string
|
|
||||||
rows?: number
|
|
||||||
padding?: number
|
|
||||||
disabled?: boolean
|
|
||||||
resizable?: boolean
|
|
||||||
placeholder?: string
|
|
||||||
}>(), {
|
|
||||||
rows: 4,
|
|
||||||
disabled: false,
|
|
||||||
resizable: false,
|
|
||||||
placeholder: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:value', payload: string): void
|
|
||||||
(event: 'focus', payload: FocusEvent): void
|
|
||||||
(event: 'blur', payload: FocusEvent): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const handleInput = (e: Event) => {
|
|
||||||
emit('update:value', (e.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const textareaRef = ref<HTMLTextAreaElement>()
|
|
||||||
const focus = () => {
|
|
||||||
if (textareaRef.value) textareaRef.value.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
focus,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.textarea {
|
|
||||||
outline: 0;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
padding: 10px;
|
|
||||||
transition: border-color .25s;
|
|
||||||
box-sizing: border-box;
|
|
||||||
line-height: 1.675;
|
|
||||||
resize: none;
|
|
||||||
font-family: -apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: $themeColor;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.resizable {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-color: #dcdcdc;
|
|
||||||
color: #b7b7b7;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #bfbfbf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<template>
|
|
||||||
<Button class="text-color-btn">
|
|
||||||
<slot></slot>
|
|
||||||
<div class="text-color-block">
|
|
||||||
<div class="text-color-block-content" :style="{ backgroundColor: color }"></div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import Button from './Button.vue'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
color: string
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.text-color-btn {
|
|
||||||
width: 100%;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.text-color-block {
|
|
||||||
width: 17px;
|
|
||||||
height: 4px;
|
|
||||||
margin-top: 1px;
|
|
||||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACdJREFUGFdjfPbs2X8GBgYGSUlJEMXAiCHw//9/sIrnz59DVKALAADNxxVfaiODNQAAAABJRU5ErkJggg==);
|
|
||||||
|
|
||||||
.text-color-block-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,361 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="writing-board" ref="writingBoardRef">
|
|
||||||
<div class="blackboard" v-if="blackboard"></div>
|
|
||||||
|
|
||||||
<canvas class="canvas" ref="canvasRef"
|
|
||||||
:style="{
|
|
||||||
width: canvasWidth + 'px',
|
|
||||||
height: canvasHeight + 'px',
|
|
||||||
}"
|
|
||||||
@mousedown="$event => handleMousedown($event)"
|
|
||||||
@mousemove="$event => handleMousemove($event)"
|
|
||||||
@mouseup="handleMouseup()"
|
|
||||||
@touchstart="$event => handleMousedown($event)"
|
|
||||||
@touchmove="$event => handleMousemove($event)"
|
|
||||||
@touchend="handleMouseup(); mouseInCanvas = false"
|
|
||||||
@mouseleave="handleMouseup(); mouseInCanvas = false"
|
|
||||||
@mouseenter="mouseInCanvas = true"
|
|
||||||
></canvas>
|
|
||||||
|
|
||||||
<template v-if="mouseInCanvas">
|
|
||||||
<div
|
|
||||||
class="eraser"
|
|
||||||
:style="{
|
|
||||||
left: mouse.x - rubberSize / 2 + 'px',
|
|
||||||
top: mouse.y - rubberSize / 2 + 'px',
|
|
||||||
width: rubberSize + 'px',
|
|
||||||
height: rubberSize + 'px',
|
|
||||||
}"
|
|
||||||
v-if="model === 'eraser'"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="pen"
|
|
||||||
:style="{
|
|
||||||
left: mouse.x - penSize / 2 + 'px',
|
|
||||||
top: mouse.y - penSize * 6 + penSize / 2 + 'px',
|
|
||||||
color: color,
|
|
||||||
}"
|
|
||||||
v-if="model === 'pen'"
|
|
||||||
>
|
|
||||||
<IconWrite class="icon" :size="penSize * 6" v-if="model === 'pen'" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="pen"
|
|
||||||
:style="{
|
|
||||||
left: mouse.x - markSize / 2 + 'px',
|
|
||||||
top: mouse.y + 'px',
|
|
||||||
color: color,
|
|
||||||
}"
|
|
||||||
v-if="model === 'mark'"
|
|
||||||
>
|
|
||||||
<IconHighLight class="icon" :size="markSize * 1.5" v-if="model === 'mark'" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
color?: string
|
|
||||||
model?: 'pen' | 'eraser' | 'mark'
|
|
||||||
blackboard?: boolean
|
|
||||||
penSize?: number
|
|
||||||
markSize?: number
|
|
||||||
rubberSize?: number
|
|
||||||
}>(), {
|
|
||||||
color: '#ffcc00',
|
|
||||||
model: 'pen',
|
|
||||||
blackboard: false,
|
|
||||||
penSize: 6,
|
|
||||||
markSize: 24,
|
|
||||||
rubberSize: 80,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'end'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
let ctx: CanvasRenderingContext2D | null = null
|
|
||||||
const writingBoardRef = ref<HTMLElement>()
|
|
||||||
const canvasRef = ref<HTMLCanvasElement>()
|
|
||||||
|
|
||||||
let lastPos = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
}
|
|
||||||
let isMouseDown = false
|
|
||||||
let lastTime = 0
|
|
||||||
let lastLineWidth = -1
|
|
||||||
|
|
||||||
// 鼠标位置坐标:用于画笔或橡皮位置跟随
|
|
||||||
const mouse = ref({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 鼠标是否处在画布范围内:处在范围内才会显示画笔或橡皮
|
|
||||||
const mouseInCanvas = ref(false)
|
|
||||||
|
|
||||||
// 监听更新canvas尺寸
|
|
||||||
const canvasWidth = ref(0)
|
|
||||||
const canvasHeight = ref(0)
|
|
||||||
|
|
||||||
const widthScale = computed(() => canvasRef.value ? canvasWidth.value / canvasRef.value.width : 1)
|
|
||||||
const heightScale = computed(() => canvasRef.value ? canvasHeight.value / canvasRef.value.height : 1)
|
|
||||||
|
|
||||||
const updateCanvasSize = () => {
|
|
||||||
if (!writingBoardRef.value) return
|
|
||||||
canvasWidth.value = writingBoardRef.value.clientWidth
|
|
||||||
canvasHeight.value = writingBoardRef.value.clientHeight
|
|
||||||
}
|
|
||||||
const resizeObserver = new ResizeObserver(updateCanvasSize)
|
|
||||||
onMounted(() => {
|
|
||||||
if (writingBoardRef.value) resizeObserver.observe(writingBoardRef.value)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (writingBoardRef.value) resizeObserver.unobserve(writingBoardRef.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化画布
|
|
||||||
const initCanvas = () => {
|
|
||||||
if (!canvasRef.value || !writingBoardRef.value) return
|
|
||||||
|
|
||||||
ctx = canvasRef.value.getContext('2d')
|
|
||||||
if (!ctx) return
|
|
||||||
|
|
||||||
canvasRef.value.width = writingBoardRef.value.clientWidth
|
|
||||||
canvasRef.value.height = writingBoardRef.value.clientHeight
|
|
||||||
|
|
||||||
ctx.lineCap = 'round'
|
|
||||||
ctx.lineJoin = 'round'
|
|
||||||
}
|
|
||||||
onMounted(initCanvas)
|
|
||||||
|
|
||||||
// 切换画笔模式时,更新 canvas ctx 配置
|
|
||||||
const updateCtx = () => {
|
|
||||||
if (!ctx) return
|
|
||||||
if (props.model === 'mark') {
|
|
||||||
ctx.globalCompositeOperation = 'xor'
|
|
||||||
ctx.globalAlpha = 0.5
|
|
||||||
}
|
|
||||||
else if (props.model === 'pen') {
|
|
||||||
ctx.globalCompositeOperation = 'source-over'
|
|
||||||
ctx.globalAlpha = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
watch(() => props.model, updateCtx)
|
|
||||||
|
|
||||||
// 绘制画笔墨迹方法
|
|
||||||
const draw = (posX: number, posY: number, lineWidth: number) => {
|
|
||||||
if (!ctx) return
|
|
||||||
|
|
||||||
const lastPosX = lastPos.x
|
|
||||||
const lastPosY = lastPos.y
|
|
||||||
|
|
||||||
ctx.lineWidth = lineWidth
|
|
||||||
ctx.strokeStyle = props.color
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(lastPosX, lastPosY)
|
|
||||||
ctx.lineTo(posX, posY)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.closePath()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 擦除墨迹方法
|
|
||||||
const erase = (posX: number, posY: number) => {
|
|
||||||
if (!ctx || !canvasRef.value) return
|
|
||||||
const lastPosX = lastPos.x
|
|
||||||
const lastPosY = lastPos.y
|
|
||||||
|
|
||||||
const radius = props.rubberSize / 2
|
|
||||||
|
|
||||||
const sinRadius = radius * Math.sin(Math.atan((posY - lastPosY) / (posX - lastPosX)))
|
|
||||||
const cosRadius = radius * Math.cos(Math.atan((posY - lastPosY) / (posX - lastPosX)))
|
|
||||||
const rectPoint1: [number, number] = [lastPosX + sinRadius, lastPosY - cosRadius]
|
|
||||||
const rectPoint2: [number, number] = [lastPosX - sinRadius, lastPosY + cosRadius]
|
|
||||||
const rectPoint3: [number, number] = [posX + sinRadius, posY - cosRadius]
|
|
||||||
const rectPoint4: [number, number] = [posX - sinRadius, posY + cosRadius]
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(posX, posY, radius, 0, Math.PI * 2)
|
|
||||||
ctx.clip()
|
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(...rectPoint1)
|
|
||||||
ctx.lineTo(...rectPoint3)
|
|
||||||
ctx.lineTo(...rectPoint4)
|
|
||||||
ctx.lineTo(...rectPoint2)
|
|
||||||
ctx.closePath()
|
|
||||||
ctx.clip()
|
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
|
||||||
ctx.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算鼠标两次移动之间的距离
|
|
||||||
const getDistance = (posX: number, posY: number) => {
|
|
||||||
const lastPosX = lastPos.x
|
|
||||||
const lastPosY = lastPos.y
|
|
||||||
return Math.sqrt((posX - lastPosX) * (posX - lastPosX) + (posY - lastPosY) * (posY - lastPosY))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据鼠标两次移动之间的距离s和时间t计算绘制速度,速度越快,墨迹越细
|
|
||||||
const getLineWidth = (s: number, t: number) => {
|
|
||||||
const maxV = 10
|
|
||||||
const minV = 0.1
|
|
||||||
const maxWidth = props.penSize
|
|
||||||
const minWidth = 3
|
|
||||||
const v = s / t
|
|
||||||
let lineWidth
|
|
||||||
|
|
||||||
if (v <= minV) lineWidth = maxWidth
|
|
||||||
else if (v >= maxV) lineWidth = minWidth
|
|
||||||
else lineWidth = maxWidth - v / maxV * maxWidth
|
|
||||||
|
|
||||||
if (lastLineWidth === -1) return lineWidth
|
|
||||||
return lineWidth * 1 / 3 + lastLineWidth * 2 / 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// 路径操作
|
|
||||||
const handleMove = (x: number, y: number) => {
|
|
||||||
const time = new Date().getTime()
|
|
||||||
|
|
||||||
if (props.model === 'pen') {
|
|
||||||
const s = getDistance(x, y)
|
|
||||||
const t = time - lastTime
|
|
||||||
const lineWidth = getLineWidth(s, t)
|
|
||||||
|
|
||||||
draw(x, y, lineWidth)
|
|
||||||
lastLineWidth = lineWidth
|
|
||||||
}
|
|
||||||
else if (props.model === 'mark') draw(x, y, props.markSize)
|
|
||||||
else erase(x, y)
|
|
||||||
|
|
||||||
lastPos = { x, y }
|
|
||||||
lastTime = new Date().getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取鼠标在canvas中的相对位置
|
|
||||||
const getMouseOffsetPosition = (e: MouseEvent | TouchEvent) => {
|
|
||||||
if (!canvasRef.value) return [0, 0]
|
|
||||||
const event = e instanceof MouseEvent ? e : e.changedTouches[0]
|
|
||||||
const canvasRect = canvasRef.value.getBoundingClientRect()
|
|
||||||
const x = event.pageX - canvasRect.x
|
|
||||||
const y = event.pageY - canvasRect.y
|
|
||||||
return [x, y]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理鼠标(触摸)事件
|
|
||||||
// 准备开始绘制/擦除墨迹(落笔)
|
|
||||||
const handleMousedown = (e: MouseEvent | TouchEvent) => {
|
|
||||||
const [mouseX, mouseY] = getMouseOffsetPosition(e)
|
|
||||||
const x = mouseX / widthScale.value
|
|
||||||
const y = mouseY / heightScale.value
|
|
||||||
|
|
||||||
isMouseDown = true
|
|
||||||
lastPos = { x, y }
|
|
||||||
lastTime = new Date().getTime()
|
|
||||||
|
|
||||||
if (!(e instanceof MouseEvent)) {
|
|
||||||
mouse.value = { x: mouseX, y: mouseY }
|
|
||||||
mouseInCanvas.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始绘制/擦除墨迹(移动)
|
|
||||||
const handleMousemove = (e: MouseEvent | TouchEvent) => {
|
|
||||||
const [mouseX, mouseY] = getMouseOffsetPosition(e)
|
|
||||||
const x = mouseX / widthScale.value
|
|
||||||
const y = mouseY / heightScale.value
|
|
||||||
|
|
||||||
mouse.value = { x: mouseX, y: mouseY }
|
|
||||||
|
|
||||||
if (isMouseDown) handleMove(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 结束绘制/擦除墨迹(停笔)
|
|
||||||
const handleMouseup = () => {
|
|
||||||
if (!isMouseDown) return
|
|
||||||
isMouseDown = false
|
|
||||||
emit('end')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空画布
|
|
||||||
const clearCanvas = () => {
|
|
||||||
if (!ctx || !canvasRef.value) return
|
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
|
||||||
emit('end')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 DataURL
|
|
||||||
const getImageDataURL = () => {
|
|
||||||
return canvasRef.value?.toDataURL()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 DataURL(绘制图片到 canvas)
|
|
||||||
const setImageDataURL = (imageDataURL: string) => {
|
|
||||||
if (!ctx || !canvasRef.value) return
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
|
||||||
|
|
||||||
if (imageDataURL) {
|
|
||||||
ctx.globalCompositeOperation = 'source-over'
|
|
||||||
ctx.globalAlpha = 1
|
|
||||||
|
|
||||||
const img = new Image()
|
|
||||||
img.src = imageDataURL
|
|
||||||
img.onload = () => {
|
|
||||||
ctx!.drawImage(img, 0, 0)
|
|
||||||
updateCtx()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
clearCanvas,
|
|
||||||
getImageDataURL,
|
|
||||||
setImageDataURL,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../assets/styles/variable.scss";
|
|
||||||
@import "../assets/styles/mixin.scss";
|
|
||||||
.writing-board {
|
|
||||||
z-index: 8;
|
|
||||||
cursor: none;
|
|
||||||
@include absolute-0();
|
|
||||||
}
|
|
||||||
.blackboard {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #0f392b;
|
|
||||||
}
|
|
||||||
.canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.eraser, .pen {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 9;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
filter: drop-shadow(2px 2px 2px #555);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.eraser {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 4px solid rgba($color: #555, $alpha: .15);
|
|
||||||
color: rgba($color: #555, $alpha: .75);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,234 +0,0 @@
|
||||||
import type { TurningMode } from '../types/slides'
|
|
||||||
|
|
||||||
export const ANIMATION_DEFAULT_DURATION = 1000
|
|
||||||
export const ANIMATION_DEFAULT_TRIGGER = 'click'
|
|
||||||
export const ANIMATION_CLASS_PREFIX = 'animate__'
|
|
||||||
|
|
||||||
export const ENTER_ANIMATIONS = [
|
|
||||||
{
|
|
||||||
type: 'bounce',
|
|
||||||
name: '弹跳',
|
|
||||||
children: [
|
|
||||||
{ name: '弹入', value: 'bounceIn' },
|
|
||||||
{ name: '向右弹入', value: 'bounceInLeft' },
|
|
||||||
{ name: '向左弹入', value: 'bounceInRight' },
|
|
||||||
{ name: '向上弹入', value: 'bounceInUp' },
|
|
||||||
{ name: '向下弹入', value: 'bounceInDown' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'fade',
|
|
||||||
name: '浮现',
|
|
||||||
children: [
|
|
||||||
{ name: '浮入', value: 'fadeIn' },
|
|
||||||
{ name: '向下浮入', value: 'fadeInDown' },
|
|
||||||
{ name: '向下长距浮入', value: 'fadeInDownBig' },
|
|
||||||
{ name: '向右浮入', value: 'fadeInLeft' },
|
|
||||||
{ name: '向右长距浮入', value: 'fadeInLeftBig' },
|
|
||||||
{ name: '向左浮入', value: 'fadeInRight' },
|
|
||||||
{ name: '向左长距浮入', value: 'fadeInRightBig' },
|
|
||||||
{ name: '向上浮入', value: 'fadeInUp' },
|
|
||||||
{ name: '向上长距浮入', value: 'fadeInUpBig' },
|
|
||||||
{ name: '从左上浮入', value: 'fadeInTopLeft' },
|
|
||||||
{ name: '从右上浮入', value: 'fadeInTopRight' },
|
|
||||||
{ name: '从左下浮入', value: 'fadeInBottomLeft' },
|
|
||||||
{ name: '从右下浮入', value: 'fadeInBottomRight' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'rotate',
|
|
||||||
name: '旋转',
|
|
||||||
children: [
|
|
||||||
{ name: '旋转进入', value: 'rotateIn' },
|
|
||||||
{ name: '绕左下进入', value: 'rotateInDownLeft' },
|
|
||||||
{ name: '绕右下进入', value: 'rotateInDownRight' },
|
|
||||||
{ name: '绕左上进入', value: 'rotateInUpLeft' },
|
|
||||||
{ name: '绕右上进入', value: 'rotateInUpRight' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'zoom',
|
|
||||||
name: '缩放',
|
|
||||||
children: [
|
|
||||||
{ name: '放大进入', value: 'zoomIn' },
|
|
||||||
{ name: '向下放大进入', value: 'zoomInDown' },
|
|
||||||
{ name: '从左放大进入', value: 'zoomInLeft' },
|
|
||||||
{ name: '从右放大进入', value: 'zoomInRight' },
|
|
||||||
{ name: '向上放大进入', value: 'zoomInUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'slide',
|
|
||||||
name: '滑入',
|
|
||||||
children: [
|
|
||||||
{ name: '向下滑入', value: 'slideInDown' },
|
|
||||||
{ name: '从右滑入', value: 'slideInLeft' },
|
|
||||||
{ name: '从左滑入', value: 'slideInRight' },
|
|
||||||
{ name: '向上滑入', value: 'slideInUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'flip',
|
|
||||||
name: '翻转',
|
|
||||||
children: [
|
|
||||||
{ name: 'X轴翻转进入', value: 'flipInX' },
|
|
||||||
{ name: 'Y轴翻转进入', value: 'flipInY' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'back',
|
|
||||||
name: '放大滑入',
|
|
||||||
children: [
|
|
||||||
{ name: '向下放大滑入', value: 'backInDown' },
|
|
||||||
{ name: '从左放大滑入', value: 'backInLeft' },
|
|
||||||
{ name: '从右放大滑入', value: 'backInRight' },
|
|
||||||
{ name: '向上放大滑入', value: 'backInUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lightSpeed',
|
|
||||||
name: '飞入',
|
|
||||||
children: [
|
|
||||||
{ name: '从右飞入', value: 'lightSpeedInRight' },
|
|
||||||
{ name: '从左飞入', value: 'lightSpeedInLeft' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const EXIT_ANIMATIONS = [
|
|
||||||
{
|
|
||||||
type: 'bounce',
|
|
||||||
name: '弹跳',
|
|
||||||
children: [
|
|
||||||
{ name: '弹出', value: 'bounceOut' },
|
|
||||||
{ name: '向左弹出', value: 'bounceOutLeft' },
|
|
||||||
{ name: '向右弹出', value: 'bounceOutRight' },
|
|
||||||
{ name: '向上弹出', value: 'bounceOutUp' },
|
|
||||||
{ name: '向下弹出', value: 'bounceOutDown' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'fade',
|
|
||||||
name: '浮现',
|
|
||||||
children: [
|
|
||||||
{ name: '浮出', value: 'fadeOut' },
|
|
||||||
{ name: '向下浮出', value: 'fadeOutDown' },
|
|
||||||
{ name: '向下长距浮出', value: 'fadeOutDownBig' },
|
|
||||||
{ name: '向左浮出', value: 'fadeOutLeft' },
|
|
||||||
{ name: '向左长距浮出', value: 'fadeOutLeftBig' },
|
|
||||||
{ name: '向右浮出', value: 'fadeOutRight' },
|
|
||||||
{ name: '向右长距浮出', value: 'fadeOutRightBig' },
|
|
||||||
{ name: '向上浮出', value: 'fadeOutUp' },
|
|
||||||
{ name: '向上长距浮出', value: 'fadeOutUpBig' },
|
|
||||||
{ name: '从左上浮出', value: 'fadeOutTopLeft' },
|
|
||||||
{ name: '从右上浮出', value: 'fadeOutTopRight' },
|
|
||||||
{ name: '从左下浮出', value: 'fadeOutBottomLeft' },
|
|
||||||
{ name: '从右下浮出', value: 'fadeOutBottomRight' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'rotate',
|
|
||||||
name: '旋转',
|
|
||||||
children: [
|
|
||||||
{ name: '旋转退出', value: 'rotateOut' },
|
|
||||||
{ name: '绕左下退出', value: 'rotateOutDownLeft' },
|
|
||||||
{ name: '绕右下退出', value: 'rotateOutDownRight' },
|
|
||||||
{ name: '绕左上退出', value: 'rotateOutUpLeft' },
|
|
||||||
{ name: '绕右上退出', value: 'rotateOutUpRight' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'zoom',
|
|
||||||
name: '缩放',
|
|
||||||
children: [
|
|
||||||
{ name: '缩小退出', value: 'zoomOut' },
|
|
||||||
{ name: '向下缩小退出', value: 'zoomOutDown' },
|
|
||||||
{ name: '从左缩小退出', value: 'zoomOutLeft' },
|
|
||||||
{ name: '从右缩小退出', value: 'zoomOutRight' },
|
|
||||||
{ name: '向上缩小退出', value: 'zoomOutUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'slide',
|
|
||||||
name: '滑出',
|
|
||||||
children: [
|
|
||||||
{ name: '向下滑出', value: 'slideOutDown' },
|
|
||||||
{ name: '从左滑出', value: 'slideOutLeft' },
|
|
||||||
{ name: '从右滑出', value: 'slideOutRight' },
|
|
||||||
{ name: '向上滑出', value: 'slideOutUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'flip',
|
|
||||||
name: '翻转',
|
|
||||||
children: [
|
|
||||||
{ name: 'X轴翻转退出', value: 'flipOutX' },
|
|
||||||
{ name: 'Y轴翻转退出', value: 'flipOutY' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'back',
|
|
||||||
name: '缩小滑出',
|
|
||||||
children: [
|
|
||||||
{ name: '向下缩小滑出', value: 'backOutDown' },
|
|
||||||
{ name: '从左缩小滑出', value: 'backOutLeft' },
|
|
||||||
{ name: '从右缩小滑出', value: 'backOutRight' },
|
|
||||||
{ name: '向上缩小滑出', value: 'backOutUp' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lightSpeed',
|
|
||||||
name: '飞出',
|
|
||||||
children: [
|
|
||||||
{ name: '从右飞出', value: 'lightSpeedOutRight' },
|
|
||||||
{ name: '从左飞出', value: 'lightSpeedOutLeft' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const ATTENTION_ANIMATIONS = [
|
|
||||||
{
|
|
||||||
type: 'shake',
|
|
||||||
name: '晃动',
|
|
||||||
children: [
|
|
||||||
{ name: '左右摇晃', value: 'shakeX' },
|
|
||||||
{ name: '上下摇晃', value: 'shakeY' },
|
|
||||||
{ name: '摇头', value: 'headShake' },
|
|
||||||
{ name: '摆动', value: 'swing' },
|
|
||||||
{ name: '晃动', value: 'wobble' },
|
|
||||||
{ name: '惊恐', value: 'tada' },
|
|
||||||
{ name: '果冻', value: 'jello' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'other',
|
|
||||||
name: '其他',
|
|
||||||
children: [
|
|
||||||
{ name: '弹跳', value: 'bounce' },
|
|
||||||
{ name: '闪烁', value: 'flash' },
|
|
||||||
{ name: '脉搏', value: 'pulse' },
|
|
||||||
{ name: '橡皮筋', value: 'rubberBand' },
|
|
||||||
{ name: '心跳(快)', value: 'heartBeat' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SlideAnimation {
|
|
||||||
label: string
|
|
||||||
value: TurningMode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SLIDE_ANIMATIONS: SlideAnimation[] = [
|
|
||||||
{ label: '无', value: 'no' },
|
|
||||||
{ label: '随机', value: 'random' },
|
|
||||||
{ label: '左右推移', value: 'slideX' },
|
|
||||||
{ label: '上下推移', value: 'slideY' },
|
|
||||||
{ label: '左右推移(3D)', value: 'slideX3D' },
|
|
||||||
{ label: '上下推移(3D)', value: 'slideY3D' },
|
|
||||||
{ label: '淡入淡出', value: 'fade' },
|
|
||||||
{ label: '旋转', value: 'rotate' },
|
|
||||||
{ label: '上下展开', value: 'scaleY' },
|
|
||||||
{ label: '左右展开', value: 'scaleX' },
|
|
||||||
{ label: '放大', value: 'scale' },
|
|
||||||
{ label: '缩小', value: 'scaleReverse' },
|
|
||||||
]
|
|
|
@ -1,70 +0,0 @@
|
||||||
import type { ChartData } from '../types/slides'
|
|
||||||
|
|
||||||
export const CHART_TYPE_MAP: { [key: string]: string } = {
|
|
||||||
'bar': '柱状图',
|
|
||||||
'column': '条形图',
|
|
||||||
'line': '折线图',
|
|
||||||
'area': '面积图',
|
|
||||||
'scatter': '散点图',
|
|
||||||
'pie': '饼图',
|
|
||||||
'ring': '环形图',
|
|
||||||
'radar': '雷达图',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHART_DEFAULT_DATA: { [key: string]: ChartData } = {
|
|
||||||
'bar': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['系列1', '系列2'],
|
|
||||||
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
|
||||||
},
|
|
||||||
'column': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['系列1', '系列2'],
|
|
||||||
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
|
||||||
},
|
|
||||||
'line': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['系列1', '系列2'],
|
|
||||||
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
|
||||||
},
|
|
||||||
'pie': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['值'],
|
|
||||||
series: [[12, 19, 5, 2, 18]],
|
|
||||||
},
|
|
||||||
'ring': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['值'],
|
|
||||||
series: [[12, 19, 5, 2, 18]],
|
|
||||||
},
|
|
||||||
'area': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['系列1', '系列2'],
|
|
||||||
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
|
||||||
},
|
|
||||||
'radar': {
|
|
||||||
labels: ['类别1', '类别2', '类别3', '类别4', '类别5'],
|
|
||||||
legends: ['系列1', '系列2'],
|
|
||||||
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
|
||||||
},
|
|
||||||
'scatter': {
|
|
||||||
labels: ['坐标1', '坐标2', '坐标3', '坐标4', '坐标5'],
|
|
||||||
legends: ['X', 'Y'],
|
|
||||||
series: [[12, 19, 5, 2, 18], [7, 11, 13, 21, 9]],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHART_PRESET_THEMES = [
|
|
||||||
['#d87c7c', '#919e8b', '#d7ab82', '#6e7074', '#61a0a8', '#efa18d'],
|
|
||||||
['#dd6b66', '#759aa0', '#e69d87', '#8dc1a9', '#ea7e53', '#eedd78'],
|
|
||||||
['#516b91', '#59c4e6', '#edafda', '#93b7e3', '#a5e7f0', '#cbb0e3'],
|
|
||||||
['#893448', '#d95850', '#eb8146', '#ffb248', '#f2d643', '#ebdba4'],
|
|
||||||
['#4ea397', '#22c3aa', '#7bd9a5', '#d0648a', '#f58db2', '#f2b3c9'],
|
|
||||||
['#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6', '#c4ebad', '#96dee8'],
|
|
||||||
['#fc97af', '#87f7cf', '#f7f494', '#72ccff', '#f7c5a0', '#d4a4eb'],
|
|
||||||
['#c1232b', '#27727b', '#fcce10', '#e87c25', '#b5c334', '#fe8463'],
|
|
||||||
['#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80', '#8d98b3'],
|
|
||||||
['#e01f54', '#001852', '#f5e8c8', '#b8d2c7', '#c6b38e', '#a4d8c2'],
|
|
||||||
['#c12e34', '#e6b600', '#0098d9', '#2b821d', '#005eaa', '#339ca8'],
|
|
||||||
['#8a7ca8', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', '#7cb4cc'],
|
|
||||||
]
|
|
|
@ -1,22 +0,0 @@
|
||||||
export const ELEMENT_TYPE_ZH: { [key: string]: string } = {
|
|
||||||
text: '文本',
|
|
||||||
image: '图片',
|
|
||||||
shape: '形状',
|
|
||||||
line: '线条',
|
|
||||||
chart: '图表',
|
|
||||||
table: '表格',
|
|
||||||
video: '视频',
|
|
||||||
audio: '音频',
|
|
||||||
latex: '公式',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MIN_SIZE: { [key: string]: number } = {
|
|
||||||
text: 20,
|
|
||||||
image: 20,
|
|
||||||
shape: 20,
|
|
||||||
chart: 200,
|
|
||||||
table: 20,
|
|
||||||
video: 250,
|
|
||||||
audio: 20,
|
|
||||||
latex: 20,
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
export const SYS_FONTS = [
|
|
||||||
{ label: 'Arial', value: 'Arial' },
|
|
||||||
{ label: '微软雅黑', value: 'Microsoft Yahei' },
|
|
||||||
{ label: '宋体', value: 'SimSun' },
|
|
||||||
{ label: '黑体', value: 'SimHei' },
|
|
||||||
{ label: '楷体', value: 'KaiTi' },
|
|
||||||
{ label: '新宋体', value: 'NSimSun' },
|
|
||||||
{ label: '仿宋', value: 'FangSong' },
|
|
||||||
{ label: '苹方', value: 'PingFang SC' },
|
|
||||||
{ label: '华文黑体', value: 'STHeiti' },
|
|
||||||
{ label: '华文楷体', value: 'STKaiti' },
|
|
||||||
{ label: '华文宋体', value: 'STSong' },
|
|
||||||
{ label: '华文仿宋', value: 'STFangSong' },
|
|
||||||
{ label: '华文中宋', value: 'STZhongSong' },
|
|
||||||
{ label: '华文琥珀', value: 'STHupo' },
|
|
||||||
{ label: '华文新魏', value: 'STXinwei' },
|
|
||||||
{ label: '华文隶书', value: 'STLiti' },
|
|
||||||
{ label: '华文行楷', value: 'STXingkai' },
|
|
||||||
{ label: '冬青黑体', value: 'Hiragino Sans GB' },
|
|
||||||
{ label: '兰亭黑', value: 'Lantinghei SC' },
|
|
||||||
{ label: '偏偏体', value: 'Hanzipen SC' },
|
|
||||||
{ label: '手札体', value: 'Hannotate SC' },
|
|
||||||
{ label: '宋体', value: 'Songti SC' },
|
|
||||||
{ label: '娃娃体', value: 'Wawati SC' },
|
|
||||||
{ label: '行楷', value: 'Xingkai SC' },
|
|
||||||
{ label: '圆体', value: 'Yuanti SC' },
|
|
||||||
{ label: '华文细黑', value: 'STXihei' },
|
|
||||||
{ label: '幼圆', value: 'YouYuan' },
|
|
||||||
{ label: '隶书', value: 'LiSu' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const WEB_FONTS = [
|
|
||||||
{ label: '得意黑', value: '得意黑' },
|
|
||||||
{ label: '仓耳小丸子', value: '仓耳小丸子' },
|
|
||||||
{ label: '优设标题黑', value: '优设标题黑' },
|
|
||||||
{ label: '峰广明锐体', value: '峰广明锐体' },
|
|
||||||
{ label: '摄图摩登小方体', value: '摄图摩登小方体' },
|
|
||||||
{ label: '站酷快乐体', value: '站酷快乐体' },
|
|
||||||
{ label: '字制区喜脉体', value: '字制区喜脉体' },
|
|
||||||
{ label: '素材集市康康体', value: '素材集市康康体' },
|
|
||||||
{ label: '素材集市酷方体', value: '素材集市酷方体' },
|
|
||||||
{ label: '途牛类圆体', value: '途牛类圆体' },
|
|
||||||
{ label: '锐字真言体', value: '锐字真言体' },
|
|
||||||
]
|
|
|
@ -1,129 +0,0 @@
|
||||||
export const enum KEYS {
|
|
||||||
C = 'C',
|
|
||||||
X = 'X',
|
|
||||||
Z = 'Z',
|
|
||||||
Y = 'Y',
|
|
||||||
A = 'A',
|
|
||||||
G = 'G',
|
|
||||||
L = 'L',
|
|
||||||
F = 'F',
|
|
||||||
D = 'D',
|
|
||||||
B = 'B',
|
|
||||||
P = 'P',
|
|
||||||
O = 'O',
|
|
||||||
R = 'R',
|
|
||||||
T = 'T',
|
|
||||||
MINUS = '-',
|
|
||||||
EQUAL = '=',
|
|
||||||
DIGIT_0 = '0',
|
|
||||||
DELETE = 'DELETE',
|
|
||||||
UP = 'ARROWUP',
|
|
||||||
DOWN = 'ARROWDOWN',
|
|
||||||
LEFT = 'ARROWLEFT',
|
|
||||||
RIGHT = 'ARROWRIGHT',
|
|
||||||
ENTER = 'ENTER',
|
|
||||||
SPACE = ' ',
|
|
||||||
TAB = 'TAB',
|
|
||||||
BACKSPACE = 'BACKSPACE',
|
|
||||||
ESC = 'ESCAPE',
|
|
||||||
PAGEUP = 'PAGEUP',
|
|
||||||
PAGEDOWN = 'PAGEDOWN',
|
|
||||||
F5 = 'F5',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HOTKEY_DOC = [
|
|
||||||
{
|
|
||||||
type: '通用',
|
|
||||||
children: [
|
|
||||||
{ label: '剪切', value: 'Ctrl + X' },
|
|
||||||
{ label: '复制', value: 'Ctrl + C' },
|
|
||||||
{ label: '粘贴', value: 'Ctrl + V' },
|
|
||||||
{ label: '粘贴为纯文本', value: 'Ctrl + Shift + V' },
|
|
||||||
{ label: '快速复制粘贴', value: 'Ctrl + D' },
|
|
||||||
{ label: '全选', value: 'Ctrl + A' },
|
|
||||||
{ label: '撤销', value: 'Ctrl + Z' },
|
|
||||||
{ label: '恢复', value: 'Ctrl + Y' },
|
|
||||||
{ label: '删除', value: 'Delete / Backspace' },
|
|
||||||
{ label: '多选', value: '按住 Ctrl 或 Shift' },
|
|
||||||
{ label: '打开搜索替换', value: 'Ctrl + F' },
|
|
||||||
{ label: '打印', value: 'Ctrl + P' },
|
|
||||||
{ label: '关闭弹窗', value: 'ESC' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '幻灯片放映',
|
|
||||||
children: [
|
|
||||||
{ label: '从头开始放映幻灯片', value: 'F5' },
|
|
||||||
{ label: '从当前开始放映幻灯片', value: 'Shift + F5' },
|
|
||||||
{ label: '切换上一页', value: '↑ / ← / PgUp' },
|
|
||||||
{ label: '切换下一页', value: '↓ / → / PgDown' },
|
|
||||||
{ label: '切换下一页', value: 'Enter / Space' },
|
|
||||||
{ label: '退出放映', value: 'ESC' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '幻灯片编辑',
|
|
||||||
children: [
|
|
||||||
{ label: '新建幻灯片', value: 'Enter' },
|
|
||||||
{ label: '移动画布', value: 'Space + 鼠标拖拽' },
|
|
||||||
{ label: '缩放画布', value: 'Ctrl + 鼠标滚轮' },
|
|
||||||
{ label: '放大画布', value: 'Ctrl + =' },
|
|
||||||
{ label: '缩小画布', value: 'Ctrl + -' },
|
|
||||||
{ label: '使画布适应当前屏幕', value: 'Ctrl + 0' },
|
|
||||||
{ label: '上一页(未选中元素)', value: '↑' },
|
|
||||||
{ label: '下一页(未选中元素)', value: '↓' },
|
|
||||||
{ label: '上一页', value: '鼠标上滚 / PgUp' },
|
|
||||||
{ label: '下一页', value: '鼠标下滚 / PgDown' },
|
|
||||||
{ label: '快速创建文本', value: '双击空白处 / T' },
|
|
||||||
{ label: '快速创建矩形', value: 'R' },
|
|
||||||
{ label: '快速创建圆形', value: 'O' },
|
|
||||||
{ label: '快速创建线条', value: 'L' },
|
|
||||||
{ label: '退出绘制状态', value: '鼠标右键' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '元素操作',
|
|
||||||
children: [
|
|
||||||
{ label: '移动', value: '↑ / ← / ↓ / →' },
|
|
||||||
{ label: '锁定', value: 'Ctrl + L' },
|
|
||||||
{ label: '组合', value: 'Ctrl + G' },
|
|
||||||
{ label: '取消组合', value: 'Ctrl + Shift + G' },
|
|
||||||
{ label: '置顶层', value: 'Alt + F' },
|
|
||||||
{ label: '置底层', value: 'Alt + B' },
|
|
||||||
{ label: '锁定宽高比例', value: '按住 Ctrl 或 Shift' },
|
|
||||||
{ label: '创建水平 / 垂直线条', value: '按住 Ctrl 或 Shift' },
|
|
||||||
{ label: '切换焦点元素', value: 'Tab' },
|
|
||||||
{ label: '确认图片裁剪', value: 'Enter' },
|
|
||||||
{ label: '完成自定义形状绘制', value: 'Enter' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '表格编辑',
|
|
||||||
children: [
|
|
||||||
{ label: '聚焦到下一个单元格', value: 'Tab' },
|
|
||||||
{ label: '移动焦点单元格', value: '↑ / ← / ↓ / →' },
|
|
||||||
{ label: '在上方插入一行', value: 'Ctrl + ↑' },
|
|
||||||
{ label: '在下方插入一行', value: 'Ctrl + ↓' },
|
|
||||||
{ label: '在左侧插入一列', value: 'Ctrl + ←' },
|
|
||||||
{ label: '在右侧插入一列', value: 'Ctrl + →' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '图表数据编辑',
|
|
||||||
children: [
|
|
||||||
{ label: '聚焦到下一行', value: 'Enter' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '文本编辑',
|
|
||||||
children: [
|
|
||||||
{ label: '加粗', value: 'Ctrl + B' },
|
|
||||||
{ label: '斜体', value: 'Ctrl + I' },
|
|
||||||
{ label: '下划线', value: 'Ctrl + U' },
|
|
||||||
{ label: '行内代码', value: 'Ctrl + E' },
|
|
||||||
{ label: '上角标', value: 'Ctrl + ;' },
|
|
||||||
{ label: '下角标', value: `Ctrl + '` },
|
|
||||||
{ label: '选中段落', value: `ESC` },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,181 +0,0 @@
|
||||||
export const enum ClipPathTypes {
|
|
||||||
RECT = 'rect',
|
|
||||||
ELLIPSE = 'ellipse',
|
|
||||||
POLYGON = 'polygon',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum ClipPaths {
|
|
||||||
RECT = 'rect',
|
|
||||||
ROUNDRECT = 'roundRect',
|
|
||||||
ELLIPSE = 'ellipse',
|
|
||||||
TRIANGLE = 'triangle',
|
|
||||||
PENTAGON = 'pentagon',
|
|
||||||
RHOMBUS = 'rhombus',
|
|
||||||
STAR = 'star',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClipPath {
|
|
||||||
[key: string]: {
|
|
||||||
name: string
|
|
||||||
type: ClipPathTypes
|
|
||||||
style: string
|
|
||||||
radius?: string
|
|
||||||
createPath?: (width: number, height: number) => string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CLIPPATHS: ClipPath = {
|
|
||||||
rect: {
|
|
||||||
name: '矩形',
|
|
||||||
type: ClipPathTypes.RECT,
|
|
||||||
radius: '0',
|
|
||||||
style: '',
|
|
||||||
},
|
|
||||||
rect2: {
|
|
||||||
name: '矩形2',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(0% 0%, 80% 0%, 100% 20%, 100% 100%, 0 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M 0 0 L ${width * 0.8} 0 L ${width} ${height * 0.2} L ${width} ${height} L 0 ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rect3: {
|
|
||||||
name: '矩形3',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(0% 0%, 80% 0%, 100% 20%, 100% 100%, 20% 100%, 0% 80%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M 0 0 L ${width * 0.8} 0 L ${width} ${height * 0.2} L ${width} ${height} L ${width * 0.2} ${height} L 0 ${height * 0.8} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roundRect: {
|
|
||||||
name: '圆角矩形',
|
|
||||||
type: ClipPathTypes.RECT,
|
|
||||||
radius: '10px',
|
|
||||||
style: 'inset(0 round 10px)',
|
|
||||||
},
|
|
||||||
ellipse: {
|
|
||||||
name: '圆形',
|
|
||||||
type: ClipPathTypes.ELLIPSE,
|
|
||||||
style: 'ellipse(50% 50% at 50% 50%)',
|
|
||||||
},
|
|
||||||
triangle: {
|
|
||||||
name: '三角形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(50% 0%, 0% 100%, 100% 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.5} 0 L 0 ${height} L ${width} ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
triangle2: {
|
|
||||||
name: '三角形2',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(50% 100%, 0% 0%, 100% 0%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.5} ${height} L 0 0 L ${width} 0 Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
triangle3: {
|
|
||||||
name: '三角形3',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(0% 0%, 0% 100%, 100% 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M 0 0 L 0 ${height} L ${width} ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rhombus: {
|
|
||||||
name: '菱形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.5} 0 L ${width} ${height * 0.5} L ${width * 0.5} ${height} L 0 ${height * 0.5} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pentagon: {
|
|
||||||
name: '五边形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.5} 0 L ${width} ${0.38 * height} L ${0.82 * width} ${height} L ${0.18 * width} ${height} L 0 ${0.38 * height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hexagon: {
|
|
||||||
name: '六边形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(20% 0%, 80% 0%, 100% 50%, 80% 100%, 20% 100%, 0% 50%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.2} 0 L ${width * 0.8} 0 L ${width} ${height * 0.5} L ${width * 0.8} ${height} L ${width * 0.2} ${height} L 0 ${height * 0.5} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
heptagon: {
|
|
||||||
name: '七边形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(50% 0%, 90% 20%, 100% 60%, 75% 100%, 25% 100%, 0% 60%, 10% 20%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.5} 0 L ${width * 0.9} ${height * 0.2} L ${width} ${height * 0.6} L ${width * 0.75} ${height} L ${width * 0.25} ${height} L 0 ${height * 0.6} L ${width * 0.1} ${height * 0.2} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
octagon: {
|
|
||||||
name: '八边形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.3} 0 L ${width * 0.7} 0 L ${width} ${height * 0.3} L ${width} ${height * 0.7} L ${width * 0.7} ${height} L ${width * 0.3} ${height} L 0 ${height * 0.7} L 0 ${height * 0.3} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
chevron: {
|
|
||||||
name: 'V形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(75% 0%, 100% 50%, 75% 100%, 0% 100%, 25% 50%, 0% 0%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.75} 0 L ${width} ${height * 0.5} L ${width * 0.75} ${height} L 0 ${height} L ${width * 0.25} ${height * 0.5} L 0 0 Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
point: {
|
|
||||||
name: '点',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(0% 0%, 75% 0%, 100% 50%, 75% 100%, 0% 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M 0 0 L ${width * 0.75} 0 L ${width} ${height * 0.5} L ${width * 0.75} ${height} L 0 ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
name: '箭头',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(0% 20%, 60% 20%, 60% 0%, 100% 50%, 60% 100%, 60% 80%, 0% 80%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M 0 ${height * 0.2} L ${width * 0.6} ${height * 0.2} L ${width * 0.6} 0 L ${width} ${height * 0.5} L ${width * 0.6} ${height} L ${width * 0.6} ${height * 0.8} L 0 ${height * 0.8} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
parallelogram: {
|
|
||||||
name: '平行四边形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(30% 0%, 100% 0%, 70% 100%, 0% 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.3} 0 L ${width} 0 L ${width * 0.7} ${height} L 0 ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
parallelogram2: {
|
|
||||||
name: '平行四边形2',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(30% 100%, 100% 100%, 70% 0%, 0% 0%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.3} ${height} L ${width} ${height} L ${width * 0.7} 0 L 0 0 Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trapezoid: {
|
|
||||||
name: '梯形',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(25% 0%, 75% 0%, 100% 100%, 0% 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M ${width * 0.25} 0 L ${width * 0.75} 0 L ${width} ${height} L 0 ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trapezoid2: {
|
|
||||||
name: '梯形2',
|
|
||||||
type: ClipPathTypes.POLYGON,
|
|
||||||
style: 'polygon(0% 0%, 100% 0%, 75% 100%, 25% 100%)',
|
|
||||||
createPath: (width: number, height: number) => {
|
|
||||||
return `M 0 0 L ${width} 0 L ${width * 0.75} ${height} L ${width * 0.25} ${height} Z`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,274 +0,0 @@
|
||||||
export const FORMULA_LIST = [
|
|
||||||
{
|
|
||||||
label: '高斯公式',
|
|
||||||
latex: `\\int\\int\\int _ { \\Omega } \\left( \\frac { \\partial {P} } { \\partial {x} } + \\frac { \\partial {Q} } { \\partial {y} } + \\frac { \\partial {R} }{ \\partial {z} } \\right) \\mathrm { d } V = \\oint _ { \\partial \\Omega } ( P \\cos \\alpha + Q \\cos \\beta + R \\cos \\gamma ) \\mathrm{ d} S`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '傅里叶级数',
|
|
||||||
latex: `f(x) = \\frac {a_0} 2 + \\sum_{n = 1}^\\infty {({a_n}\\cos {nx} + {b_n}\\sin {nx})}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '泰勒展开式',
|
|
||||||
latex: `e ^ { x } = 1 + \\frac { x } { 1 ! } + \\frac { x ^ { 2 } } { 2 ! } + \\frac { x ^ { 3 } } { 3 ! } + ... , \\quad - \\infty < x < \\infty`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '定积分',
|
|
||||||
latex: `\\lim_ { n \\rightarrow + \\infty } \\sum _ { i = 1 } ^ { n } f \\left[ a + \\frac { i } { n } ( b - a ) \\right] \\frac { b - a } { n } = \\int _ { a } ^ { b } f ( x ) dx`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '三角恒等式1',
|
|
||||||
latex: `\\sin \\alpha \\pm \\sin \\beta = 2 \\sin \\frac { 1 } { 2 } ( \\alpha \\pm \\beta ) \\cos \\frac { 1 } { 2 } ( \\alpha \\mp \\beta )`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '三角恒等式2',
|
|
||||||
latex: `\\cos \\alpha + \\cos \\beta = 2 \\cos \\frac { 1 } { 2 } ( \\alpha + \\beta ) \\cos \\frac { 1 } { 2 } ( \\alpha - \\beta )`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '和的展开式',
|
|
||||||
latex: `( 1 + x ) ^ { n } = 1 + \\frac { n x } { 1 ! } + \\frac { n ( n - 1 ) x ^ { 2 } } { 2 ! } + ...`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '欧拉公式',
|
|
||||||
latex: ` e^{ix} = \\cos {x} + i\\sin {x}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '贝努利方程',
|
|
||||||
latex: `\\frac {dy} {dx} + P(x)y = Q(x) y^n ({n} \\not= {0,1})`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '全微分方程',
|
|
||||||
latex: `du(x,y) = P(x,y)dx + Q(x,y)dy = 0`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '非齐次方程',
|
|
||||||
latex: `y = (\\int Q(x) e^{\\int {P(x)dx}}dx + C)e^{-\\int {P(x)dx}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '柯西中值定理',
|
|
||||||
latex: `\\frac{{f(b) - f(a)}}{{F(b) - F(a)}} = \\frac{{f'(\\xi )}}{{F'(\\xi )}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '拉格朗日中值定理',
|
|
||||||
latex: `f(b) - f(a) = f'(\\xi )(b - a)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '导数公式',
|
|
||||||
latex: `(\\arcsin x)' = \\frac{1}{{\\sqrt {1 - x^2} }}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '三角函数积分',
|
|
||||||
latex: `\\int {tgxdx = - \\ln \\left| {\\cos x} \\right| + C}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '二次曲面',
|
|
||||||
latex: `\\frac{{{x^2}}}{{{a^2}}} + \\frac{{{y^2}}}{{{b^2}}} - \\frac{{{z^2}}}{{{c^2}}} = 1`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '二阶微分',
|
|
||||||
latex: `\\frac {{d^2}y} {dx^2} + P(x) \\frac {dy} {dx} + Q(x)y = f(x)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '方向导数',
|
|
||||||
latex: `\\frac{{\\partial f}}{{\\partial l}} = \\frac{{\\partial f}}{{\\partial x}}\\cos \\phi + \\frac{{\\partial f}}{{\\partial y}}\\sin \\phi`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const SYMBOL_LIST = [
|
|
||||||
{
|
|
||||||
type: 'operators',
|
|
||||||
label: '数学',
|
|
||||||
children: [
|
|
||||||
{ latex: '\\cdot' },
|
|
||||||
{ latex: '\\pm' },
|
|
||||||
{ latex: '\\mp' },
|
|
||||||
{ latex: '+' },
|
|
||||||
{ latex: '-' },
|
|
||||||
{ latex: '\\times' },
|
|
||||||
{ latex: '\\div' },
|
|
||||||
{ latex: '<' },
|
|
||||||
{ latex: '>' },
|
|
||||||
{ latex: '=' },
|
|
||||||
{ latex: '\\neq\\ne' },
|
|
||||||
{ latex: '\\leqq' },
|
|
||||||
{ latex: '\\geqq' },
|
|
||||||
{ latex: '\\leq' },
|
|
||||||
{ latex: '\\geq' },
|
|
||||||
{ latex: '\\propto' },
|
|
||||||
{ latex: '\\sim' },
|
|
||||||
{ latex: '\\equiv' },
|
|
||||||
{ latex: '\\dagger' },
|
|
||||||
{ latex: '\\ddagger' },
|
|
||||||
{ latex: '\\ell' },
|
|
||||||
{ latex: '\\#' },
|
|
||||||
{ latex: '\\$' },
|
|
||||||
{ latex: '\\&' },
|
|
||||||
{ latex: '\\%' },
|
|
||||||
{ latex: '\\langle\\rangle' },
|
|
||||||
{ latex: '()' },
|
|
||||||
{ latex: '[]' },
|
|
||||||
{ latex: '\\{\\}' },
|
|
||||||
{ latex: '||' },
|
|
||||||
{ latex: '\\|' },
|
|
||||||
{ latex: '\\exists' },
|
|
||||||
{ latex: '\\in' },
|
|
||||||
{ latex: '\\subset' },
|
|
||||||
{ latex: '\\supset' },
|
|
||||||
{ latex: '\\cup' },
|
|
||||||
{ latex: '\\cap' },
|
|
||||||
{ latex: '\\infty' },
|
|
||||||
{ latex: '\\partial' },
|
|
||||||
{ latex: '\\nabla' },
|
|
||||||
{ latex: '\\aleph' },
|
|
||||||
{ latex: '\\wp' },
|
|
||||||
{ latex: '\\therefore' },
|
|
||||||
{ latex: '\\mid' },
|
|
||||||
{ latex: '\\sum' },
|
|
||||||
{ latex: '\\prod' },
|
|
||||||
{ latex: '\\bigoplus' },
|
|
||||||
{ latex: '\\bigodot' },
|
|
||||||
{ latex: '\\int' },
|
|
||||||
{ latex: '\\oint' },
|
|
||||||
{ latex: '\\oplus' },
|
|
||||||
{ latex: '\\odot' },
|
|
||||||
{ latex: '\\perp' },
|
|
||||||
{ latex: '\\angle' },
|
|
||||||
{ latex: '\\triangle' },
|
|
||||||
{ latex: '\\Box' },
|
|
||||||
{ latex: '\\rightarrow' },
|
|
||||||
{ latex: '\\to' },
|
|
||||||
{ latex: '\\leftarrow' },
|
|
||||||
{ latex: '\\gets' },
|
|
||||||
{ latex: '\\circ' },
|
|
||||||
{ latex: '\\bigcirc' },
|
|
||||||
{ latex: '\\bullet' },
|
|
||||||
{ latex: '\\star' },
|
|
||||||
{ latex: '\\diamond' },
|
|
||||||
{ latex: '\\ast' },
|
|
||||||
{ latex: ',' },
|
|
||||||
{ latex: '.' },
|
|
||||||
{ latex: ';' },
|
|
||||||
{ latex: '!' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'group',
|
|
||||||
label: '组合',
|
|
||||||
children: [
|
|
||||||
{ latex: '\\frac{a}{b}' },
|
|
||||||
{ latex: '\\frac{dx}{dx}' },
|
|
||||||
{ latex: '\\frac{\\partial a}{\\partial b}' },
|
|
||||||
{ latex: '\\sqrt{x}' },
|
|
||||||
{ latex: '\\sqrt[n]{x}' },
|
|
||||||
{ latex: 'x^{n}' },
|
|
||||||
{ latex: 'x_{n}' },
|
|
||||||
{ latex: 'x_a^b' },
|
|
||||||
{ latex: '\\int_{a}^{b}' },
|
|
||||||
{ latex: '\\oint_a^b' },
|
|
||||||
{ latex: '\\lim_{a \\rightarrow b}' },
|
|
||||||
{ latex: '\\prod_a^b' },
|
|
||||||
{ latex: '\\sum_a^b' },
|
|
||||||
{ latex: '\\left(\\begin{array}a \\\\ b\\end{array}\\right)' },
|
|
||||||
{ latex: '\\begin{bmatrix}a & b \\\\ c & d \\end{bmatrix}' },
|
|
||||||
{ latex: '\\begin{cases}a & x = 0 \\\\ b & x > 0\\end{cases}' },
|
|
||||||
{ latex: '\\hat{a}' },
|
|
||||||
{ latex: '\\breve{a}' },
|
|
||||||
{ latex: '\\acute{a}' },
|
|
||||||
{ latex: '\\grave{a}' },
|
|
||||||
{ latex: '\\tilde{a}' },
|
|
||||||
{ latex: '\\bar{a}' },
|
|
||||||
{ latex: '\\vec{a}' },
|
|
||||||
{ latex: '\\underline{a}' },
|
|
||||||
{ latex: '\\overline{a}' },
|
|
||||||
{ latex: '\\widehat{ab}' },
|
|
||||||
{ latex: '\\overleftarrow{ab}' },
|
|
||||||
{ latex: '\\overrightarrow{ab}' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'verbatim',
|
|
||||||
label: '函数',
|
|
||||||
children: [
|
|
||||||
{ latex: '\\log' },
|
|
||||||
{ latex: '\\ln' },
|
|
||||||
{ latex: '\\exp' },
|
|
||||||
{ latex: '\\mod' },
|
|
||||||
{ latex: '\\lim' },
|
|
||||||
{ latex: '\\sin' },
|
|
||||||
{ latex: '\\cos' },
|
|
||||||
{ latex: '\\tan' },
|
|
||||||
{ latex: '\\csc' },
|
|
||||||
{ latex: '\\sec' },
|
|
||||||
{ latex: '\\cot' },
|
|
||||||
{ latex: '\\sinh' },
|
|
||||||
{ latex: '\\cosh' },
|
|
||||||
{ latex: '\\tanh' },
|
|
||||||
{ latex: '\\csch' },
|
|
||||||
{ latex: '\\sech' },
|
|
||||||
{ latex: '\\coth' },
|
|
||||||
{ latex: '\\arcsin' },
|
|
||||||
{ latex: '\\arccos' },
|
|
||||||
{ latex: '\\arctan' },
|
|
||||||
{ latex: '\\arccsc' },
|
|
||||||
{ latex: '\\arcsec' },
|
|
||||||
{ latex: '\\arccot' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'greek',
|
|
||||||
label: '希腊字母',
|
|
||||||
children: [
|
|
||||||
{ latex: '\\alpha' },
|
|
||||||
{ latex: '\\beta' },
|
|
||||||
{ latex: '\\gamma' },
|
|
||||||
{ latex: '\\delta' },
|
|
||||||
{ latex: '\\varepsilon' },
|
|
||||||
{ latex: '\\zeta' },
|
|
||||||
{ latex: '\\eta' },
|
|
||||||
{ latex: '\\vartheta' },
|
|
||||||
{ latex: '\\iota' },
|
|
||||||
{ latex: '\\kappa' },
|
|
||||||
{ latex: '\\lambda' },
|
|
||||||
{ latex: '\\mu' },
|
|
||||||
{ latex: '\\nu' },
|
|
||||||
{ latex: '\\xi' },
|
|
||||||
{ latex: '\\omicron' },
|
|
||||||
{ latex: '\\pi' },
|
|
||||||
{ latex: '\\rho' },
|
|
||||||
{ latex: '\\sigma' },
|
|
||||||
{ latex: '\\tau' },
|
|
||||||
{ latex: '\\upsilon' },
|
|
||||||
{ latex: '\\varphi' },
|
|
||||||
{ latex: '\\chi' },
|
|
||||||
{ latex: '\\psi' },
|
|
||||||
{ latex: '\\omega' },
|
|
||||||
{ latex: '\\epsilon' },
|
|
||||||
{ latex: '\\theta' },
|
|
||||||
{ latex: '\\phi' },
|
|
||||||
{ latex: '\\varsigma' },
|
|
||||||
{ latex: '\\Alpha' },
|
|
||||||
{ latex: '\\Beta' },
|
|
||||||
{ latex: '\\Gamma' },
|
|
||||||
{ latex: '\\Delta' },
|
|
||||||
{ latex: '\\Epsilon' },
|
|
||||||
{ latex: '\\Zeta' },
|
|
||||||
{ latex: '\\Eta' },
|
|
||||||
{ latex: '\\Theta' },
|
|
||||||
{ latex: '\\Iota' },
|
|
||||||
{ latex: '\\Kappa' },
|
|
||||||
{ latex: '\\Lambda' },
|
|
||||||
{ latex: '\\Mu' },
|
|
||||||
{ latex: '\\Nu' },
|
|
||||||
{ latex: '\\Xi' },
|
|
||||||
{ latex: '\\Omicron' },
|
|
||||||
{ latex: '\\Pi' },
|
|
||||||
{ latex: '\\Rho' },
|
|
||||||
{ latex: '\\Sigma' },
|
|
||||||
{ latex: '\\Tau' },
|
|
||||||
{ latex: '\\Upsilon' },
|
|
||||||
{ latex: '\\Phi' },
|
|
||||||
{ latex: '\\Chi' },
|
|
||||||
{ latex: '\\Psi' },
|
|
||||||
{ latex: '\\Omega' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { LinePoint } from '../types/slides'
|
|
||||||
|
|
||||||
|
|
||||||
export interface LinePoolItem {
|
|
||||||
path: string
|
|
||||||
style: 'solid' | 'dashed'
|
|
||||||
points: [LinePoint, LinePoint]
|
|
||||||
isBroken?: boolean
|
|
||||||
isBroken2?: boolean
|
|
||||||
isCurve?: boolean
|
|
||||||
isCubic?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PresetLine {
|
|
||||||
type: string
|
|
||||||
children: LinePoolItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LINE_LIST: PresetLine[] = [
|
|
||||||
{
|
|
||||||
type: '直线',
|
|
||||||
children: [
|
|
||||||
{ path: 'M 0 0 L 20 20', style: 'solid', points: ['', ''] },
|
|
||||||
{ path: 'M 0 0 L 20 20', style: 'dashed', points: ['', ''] },
|
|
||||||
{ path: 'M 0 0 L 20 20', style: 'solid', points: ['', 'arrow'] },
|
|
||||||
{ path: 'M 0 0 L 20 20', style: 'dashed', points: ['', 'arrow'] },
|
|
||||||
{ path: 'M 0 0 L 20 20', style: 'solid', points: ['', 'dot'] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: '折线、曲线',
|
|
||||||
children: [
|
|
||||||
{ path: 'M 0 0 L 0 20 L 20 20', style: 'solid', points: ['', 'arrow'], isBroken: true },
|
|
||||||
{ path: 'M 0 0 L 10 0 L 10 20 L 20 20', style: 'solid', points: ['', 'arrow'], isBroken2: true },
|
|
||||||
{ path: 'M 0 0 Q 0 20 20 20', style: 'solid', points: ['', 'arrow'], isCurve: true },
|
|
||||||
{ path: 'M 0 0 C 20 0 0 20 20 20', style: 'solid', points: ['', 'arrow'], isCubic: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1 +0,0 @@
|
||||||
export const LOCALSTORAGE_KEY_DISCARDED_DB = 'PPTIST_DISCARDED_DB'
|
|
|
@ -1,59 +0,0 @@
|
||||||
export const SYMBOL_LIST = [
|
|
||||||
{
|
|
||||||
key: 'letter',
|
|
||||||
label: '字母',
|
|
||||||
children: [
|
|
||||||
'α', 'β', 'γ', 'δ', 'ϵ', 'ε', 'ζ', 'η', 'θ', 'ϑ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ϖ', 'ρ', 'ϱ', 'σ', 'ς', 'τ', 'υ', 'ϕ', 'φ', 'χ', 'ψ', 'ω',
|
|
||||||
'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω',
|
|
||||||
'𝐀', '𝐁', '𝐂', '𝐃', '𝐄', '𝐅', '𝐆', '𝐇', '𝐈', '𝐉', '𝐊', '𝐋', '𝐌', '𝐍', '𝐎', '𝐏', '𝐐', '𝐑', '𝐒', '𝐓', '𝐔', '𝐕', '𝐖', '𝐗', '𝐘', '𝐙',
|
|
||||||
'𝐚', '𝐛', '𝐜', '𝐝', '𝐞', '𝐟', '𝐠', '𝐡', '𝐢', '𝐣', '𝐤', '𝐥', '𝐦', '𝐧', '𝐨', '𝐩', '𝐪', '𝐫', '𝐬', '𝐭', '𝐮', '𝐯', '𝐰', '𝐱', '𝐲', '𝐳',
|
|
||||||
'𝓐', '𝓑', '𝓒', '𝓓', '𝓔', '𝓕', '𝓖', '𝓗', '𝓘', '𝓙', '𝓚', '𝓛', '𝓜', '𝓝', '𝓞', '𝓟', '𝓠', '𝓡', '𝓢', '𝓣', '𝓤', '𝓥', '𝓦', '𝓧', '𝓨', '𝓩',
|
|
||||||
'𝓪', '𝓫', '𝓬', '𝓭', '𝓮', '𝓯', '𝓰', '𝓱', '𝓲', '𝓳', '𝓴', '𝓵', '𝓶', '𝓷', '𝓸', '𝓹', '𝓺', '𝓻', '𝓼', '𝓽', '𝓾', '𝓿', '𝔀', '𝔁', '𝔂', '𝔃',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'number',
|
|
||||||
label: '序号',
|
|
||||||
children: [
|
|
||||||
'①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳',
|
|
||||||
'⑴', '⑵', '⑶', '⑷', '⑸', '⑹', '⑺', '⑻', '⑼', '⑽', '⑾', '⑿', '⒀', '⒁', '⒂', '⒃', '⒄', '⒅', '⒆', '⒇',
|
|
||||||
'º', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹', '₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉',
|
|
||||||
'Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ', 'Ⅵ', 'Ⅶ', 'Ⅷ', 'Ⅸ', 'Ⅹ', 'Ⅺ', 'Ⅻ', 'Ⅼ', 'Ⅽ', 'Ⅾ', 'Ⅿ',
|
|
||||||
'ⅰ', 'ⅱ', 'ⅲ', 'ⅳ', 'ⅴ', 'ⅵ', 'ⅶ', 'ⅷ', 'ⅸ', 'ⅹ', 'ⅺ', 'ⅻ', 'ⅼ', 'ⅽ', 'ⅾ', 'ⅿ', 'ↀ', 'ↁ', 'ↂ',
|
|
||||||
'㊀', '㊁', '㊂', '㊃', '㊄', '㊅', '㊆', '㊇', '㊈', '㊉', '㈠', '㈡', '㈢', '㈣', '㈤', '㈥', '㈦', '㈧', '㈨', '㈩',
|
|
||||||
'𝟘', '𝟙', '𝟚', '𝟛', '𝟜', '𝟝', '𝟞', '𝟟', '𝟠', '𝟡',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'math',
|
|
||||||
label: '数学',
|
|
||||||
children: [
|
|
||||||
'+', '-', '×', '÷', '=', '~', '¬', '±', '%', '°', 'ǃ', '‰', '‱', '½', '⅓', '⅔', '¼', '¾',
|
|
||||||
'<', '>', 'l', 'o', 'g', 'l', 'g', 'l', 'n', '⨂', '⨁', '⨄', '⨃', '⨅', '⨆', '√', '∛', '∜', '∝', '∞',
|
|
||||||
'∟', '∠', '∡', '∢', '∧', '∨', '∩', '∪', '∫', '∬', '∭', '∮', '∯', '∰', '∱', '∲', '∳',
|
|
||||||
'∴', '∵', '∼', '∽', '∾', '∿', '≃', '≄', '≅', '≆', '≇', '≈', '≊', '≋', '≌', '≍', '≎', '≏', '≐', '≑', '≒', '≓', '≔', '≕',
|
|
||||||
'≤', '≥', '≦', '≧', '≨', '≩', '≪', '≫', '≺', '≻', '≼', '≽', '≾', '≿', '⊀', '⊁', '⊂', '⊃', '⊄', '⊅', '⊆', '⊇', '⊈', '⊉', '⊊', '⊋', '⊏', '⊐', '⊑', '⊒',
|
|
||||||
'⊓', '⊔', '⊢', '⊣', '⊤', '⊥', '⊦', '⊧', '⊨', '⊩', '⊪', '⊫', '⊬', '⊭', '⊮', '⊯', '⊲', '⊳', '⊴', '⊵', '⋀', '⋁', '⋂', '⋃', '⋉', '⋊',
|
|
||||||
'⋋', '⋌', '⟨', '⟩', '⟪', '⟫', '⟮', '⟯', '⧼', '⧽', '⦰',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'arrow',
|
|
||||||
label: '箭头',
|
|
||||||
children: [
|
|
||||||
'←', '↑', '→', '↓', '↔', '↕', '↖', '↗', '↘', '↙', '↚', '↛', '↜', '↝', '↞', '↟', '↠', '↡', '↢', '↣', '↤', '↥', '↦', '↧', '↨',
|
|
||||||
'↫', '↬', '↭', '↮', '↯', '↰', '↱', '↲', '↳', '↴', '↵', '↶', '↷', '↸', '↹', '↺', '↻', '↼', '↽', '↾', '↿', '⇀', '⇁', '⇂', '⇃',
|
|
||||||
'⇄', '⇅', '⇆', '⇇', '⇈', '⇉', '⇊', '⇋', '⇌', '⇍', '⇎', '⇏', '⇐', '⇑', '⇒', '⇓', '⇔', '⇕', '⇖', '⇗', '⇘', '⇙', '⇚', '⇛',
|
|
||||||
'⇜', '⇝', '⇞', '⇟', '⇠', '⇡', '⇢', '⇣', '⇤', '⇥', '⇦', '⇧', '⇨', '⇩', '⇪', '⇫', '⇬', '⇭', '⇮', '⇯', '⇰', '⇱', '⇲', '⇳', '⇴', '⇵',
|
|
||||||
'⇶', '⇷', '⇸', '⇹', '⇺', '⇻', '⇼', '⇽', '⇾', '⇿',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'graph',
|
|
||||||
label: '图形',
|
|
||||||
children: [
|
|
||||||
'▢', '▣', '▤', '▥', '▦', '▧', '▨', '▩', '▭', '▮', '▯', '▰', '▱', '▲', '▷', '▼', '◁',
|
|
||||||
'◈', '◉', '◍', '◐', '◑', '◒', '◓', '◔', '◕', '◧', '◨', '◩', '◪', '◫', '◬', '◭', '◮',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,93 +0,0 @@
|
||||||
export interface PresetTheme {
|
|
||||||
background: string
|
|
||||||
fontColor: string
|
|
||||||
fontname: string
|
|
||||||
colors: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PRESET_THEMES: PresetTheme[] = [
|
|
||||||
{
|
|
||||||
background: '#ffffff',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#ffffff',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#83992a', '#3c9670', '#44709d', '#a23b32', '#d87728', '#deb340'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#ffffff',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#e48312', '#bd582c', '#865640', '#9b8357', '#c2bc80', '#94a088'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#ffffff',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#ffffff',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#90c225', '#54a121', '#e6b91e', '#e86618', '#c42f19', '#918756'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#ffffff',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#1cade4', '#2683c6', '#27ced7', '#42ba97', '#3e8853', '#62a39f'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#e9efd6',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#a5300f', '#de7e18', '#9f8351', '#728653', '#92aa4c', '#6aac91'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#17444e',
|
|
||||||
fontColor: '#ffffff',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#b01513', '#ea6312', '#e6b729', '#6bab90', '#55839a', '#9e5d9d'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#36234d',
|
|
||||||
fontColor: '#ffffff',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#b31166', '#e33d6f', '#e45f3c', '#e9943a', '#9b6bf2', '#d63cd0'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#247fad',
|
|
||||||
fontColor: '#ffffff',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#052f61', '#a50e82', '#14967c', '#6a9e1f', '#e87d37', '#c62324'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#103f55',
|
|
||||||
fontColor: '#ffffff',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#40aebd', '#97e8d5', '#a1cf49', '#628f3e', '#f2df3a', '#fcb01c'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#242367',
|
|
||||||
fontColor: '#ffffff',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#ac3ec1', '#477bd1', '#46b298', '#90ba4c', '#dd9d31', '#e25345'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#e4b75e',
|
|
||||||
fontColor: '#333333',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#f0a22e', '#a5644e', '#b58b80', '#c3986d', '#a19574', '#c17529'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
background: '#333333',
|
|
||||||
fontColor: '#ffffff',
|
|
||||||
fontname: 'Microsoft Yahei',
|
|
||||||
colors: ['#bdc8df', '#003fa9', '#f5ba00', '#ff7567', '#7676d9', '#923ffc'],
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,16 +0,0 @@
|
||||||
interface HTMLElement {
|
|
||||||
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>
|
|
||||||
mozRequestFullScreen(options?: FullscreenOptions): Promise<void>
|
|
||||||
msRequestFullscreen(options?: FullscreenOptions): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Document {
|
|
||||||
webkitFullscreenElement: Element | null
|
|
||||||
mozFullScreenElement: Element | null
|
|
||||||
msFullscreenElement: Element | null
|
|
||||||
webkitCurrentFullScreenElement: Element | null
|
|
||||||
|
|
||||||
mozCancelFullScreen(): Promise<void>
|
|
||||||
webkitExitFullscreen(): Promise<void>
|
|
||||||
msExitFullscreen(): Promise<void>
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { nanoid } from 'nanoid'
|
|
||||||
import { useSlidesStore, useMainStore } from '../store'
|
|
||||||
import type { PPTElement, Slide } from '../types/slides'
|
|
||||||
import { createSlideIdMap, createElementIdMap, getElementRange } from '../utils/element'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加指定的元素数据(一组)
|
|
||||||
* @param elements 元素列表数据
|
|
||||||
*/
|
|
||||||
const addElementsFromData = (elements: PPTElement[]) => {
|
|
||||||
const { groupIdMap, elIdMap } = createElementIdMap(elements)
|
|
||||||
|
|
||||||
const firstElement = elements[0]
|
|
||||||
let offset = 0
|
|
||||||
let lastSameElement: PPTElement | undefined
|
|
||||||
|
|
||||||
do {
|
|
||||||
lastSameElement = currentSlide.value.elements.find(el => {
|
|
||||||
if (el.type !== firstElement.type) return false
|
|
||||||
|
|
||||||
const { minX: oMinX, maxX: oMaxX, minY: oMinY, maxY: oMaxY } = getElementRange(el)
|
|
||||||
const { minX: nMinX, maxX: nMaxX, minY: nMinY, maxY: nMaxY } = getElementRange({
|
|
||||||
...firstElement,
|
|
||||||
left: firstElement.left + offset,
|
|
||||||
top: firstElement.top + offset
|
|
||||||
})
|
|
||||||
if (
|
|
||||||
oMinX === nMinX &&
|
|
||||||
oMaxX === nMaxX &&
|
|
||||||
oMinY === nMinY &&
|
|
||||||
oMaxY === nMaxY
|
|
||||||
) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (lastSameElement) offset += 10
|
|
||||||
|
|
||||||
} while (lastSameElement)
|
|
||||||
|
|
||||||
for (const element of elements) {
|
|
||||||
element.id = elIdMap[element.id]
|
|
||||||
|
|
||||||
element.left = element.left + offset
|
|
||||||
element.top = element.top + offset
|
|
||||||
|
|
||||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
|
||||||
}
|
|
||||||
slidesStore.addElement(elements)
|
|
||||||
mainStore.setActiveElementIdList(Object.values(elIdMap))
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加指定的页面数据
|
|
||||||
* @param slide 页面数据
|
|
||||||
*/
|
|
||||||
const addSlidesFromData = (slides: Slide[]) => {
|
|
||||||
const slideIdMap = createSlideIdMap(slides)
|
|
||||||
const newSlides = slides.map(slide => {
|
|
||||||
const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
|
|
||||||
|
|
||||||
for (const element of slide.elements) {
|
|
||||||
element.id = elIdMap[element.id]
|
|
||||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
|
||||||
|
|
||||||
// 若元素绑定了页面跳转链接
|
|
||||||
if (element.link && element.link.type === 'slide') {
|
|
||||||
|
|
||||||
// 待添加页面中包含该页面,则替换相关绑定关系
|
|
||||||
if (slideIdMap[element.link.target]) {
|
|
||||||
element.link.target = slideIdMap[element.link.target]
|
|
||||||
}
|
|
||||||
// 待添加页面中不包含该页面,则删除该元素绑定的页面跳转
|
|
||||||
else delete element.link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 动画id替换
|
|
||||||
if (slide.animations) {
|
|
||||||
for (const animation of slide.animations) {
|
|
||||||
animation.id = nanoid(10)
|
|
||||||
animation.elId = elIdMap[animation.elId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...slide,
|
|
||||||
id: slideIdMap[slide.id],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
slidesStore.addSlide(newSlides)
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
addElementsFromData,
|
|
||||||
addSlidesFromData,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import { ElementAlignCommands } from '../types/edit'
|
|
||||||
import { getElementListRange, getRectRotatedOffset } from '../utils/element'
|
|
||||||
import useHistorySnapshot from './useHistorySnapshot'
|
|
||||||
|
|
||||||
interface RangeMap {
|
|
||||||
[id: string]: ReturnType<typeof getElementListRange>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对齐选中的元素
|
|
||||||
* @param command 对齐方向
|
|
||||||
*/
|
|
||||||
const alignActiveElement = (command: ElementAlignCommands) => {
|
|
||||||
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
|
|
||||||
const elementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
|
|
||||||
|
|
||||||
// 如果所选择的元素为组合元素的成员,需要计算该组合的整体范围
|
|
||||||
const groupElementRangeMap: RangeMap = {}
|
|
||||||
for (const activeElement of activeElementList.value) {
|
|
||||||
if (activeElement.groupId && !groupElementRangeMap[activeElement.groupId]) {
|
|
||||||
const groupElements = activeElementList.value.filter(item => item.groupId === activeElement.groupId)
|
|
||||||
groupElementRangeMap[activeElement.groupId] = getElementListRange(groupElements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据不同的命令,计算对齐的位置
|
|
||||||
if (command === ElementAlignCommands.LEFT) {
|
|
||||||
elementList.forEach(element => {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
if (!element.groupId) {
|
|
||||||
if ('rotate' in element && element.rotate) {
|
|
||||||
const { offsetX } = getRectRotatedOffset({
|
|
||||||
left: element.left,
|
|
||||||
top: element.top,
|
|
||||||
width: element.width,
|
|
||||||
height: element.height,
|
|
||||||
rotate: element.rotate,
|
|
||||||
})
|
|
||||||
element.left = minX - offsetX
|
|
||||||
}
|
|
||||||
else element.left = minX
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const range = groupElementRangeMap[element.groupId]
|
|
||||||
const offset = range.minX - minX
|
|
||||||
element.left = element.left - offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (command === ElementAlignCommands.RIGHT) {
|
|
||||||
elementList.forEach(element => {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
if (!element.groupId) {
|
|
||||||
const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
|
|
||||||
if ('rotate' in element && element.rotate) {
|
|
||||||
const { offsetX } = getRectRotatedOffset({
|
|
||||||
left: element.left,
|
|
||||||
top: element.top,
|
|
||||||
width: element.width,
|
|
||||||
height: element.height,
|
|
||||||
rotate: element.rotate,
|
|
||||||
})
|
|
||||||
element.left = maxX - elWidth + offsetX
|
|
||||||
}
|
|
||||||
else element.left = maxX - elWidth
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const range = groupElementRangeMap[element.groupId]
|
|
||||||
const offset = range.maxX - maxX
|
|
||||||
element.left = element.left - offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (command === ElementAlignCommands.TOP) {
|
|
||||||
elementList.forEach(element => {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
if (!element.groupId) {
|
|
||||||
if ('rotate' in element && element.rotate) {
|
|
||||||
const { offsetY } = getRectRotatedOffset({
|
|
||||||
left: element.left,
|
|
||||||
top: element.top,
|
|
||||||
width: element.width,
|
|
||||||
height: element.height,
|
|
||||||
rotate: element.rotate,
|
|
||||||
})
|
|
||||||
element.top = minY - offsetY
|
|
||||||
}
|
|
||||||
else element.top = minY
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const range = groupElementRangeMap[element.groupId]
|
|
||||||
const offset = range.minY - minY
|
|
||||||
element.top = element.top - offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (command === ElementAlignCommands.BOTTOM) {
|
|
||||||
elementList.forEach(element => {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
if (!element.groupId) {
|
|
||||||
const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
|
|
||||||
if ('rotate' in element && element.rotate) {
|
|
||||||
const { offsetY } = getRectRotatedOffset({
|
|
||||||
left: element.left,
|
|
||||||
top: element.top,
|
|
||||||
width: element.width,
|
|
||||||
height: element.height,
|
|
||||||
rotate: element.rotate,
|
|
||||||
})
|
|
||||||
element.top = maxY - elHeight + offsetY
|
|
||||||
}
|
|
||||||
else element.top = maxY - elHeight
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const range = groupElementRangeMap[element.groupId]
|
|
||||||
const offset = range.maxY - maxY
|
|
||||||
element.top = element.top - offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (command === ElementAlignCommands.HORIZONTAL) {
|
|
||||||
const horizontalCenter = (minX + maxX) / 2
|
|
||||||
elementList.forEach(element => {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
if (!element.groupId) {
|
|
||||||
const elWidth = element.type === 'line' ? Math.max(element.start[0], element.end[0]) : element.width
|
|
||||||
element.left = horizontalCenter - elWidth / 2
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const range = groupElementRangeMap[element.groupId]
|
|
||||||
const center = (range.maxX + range.minX) / 2
|
|
||||||
const offset = center - horizontalCenter
|
|
||||||
element.left = element.left - offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (command === ElementAlignCommands.VERTICAL) {
|
|
||||||
const verticalCenter = (minY + maxY) / 2
|
|
||||||
elementList.forEach(element => {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
if (!element.groupId) {
|
|
||||||
const elHeight = element.type === 'line' ? Math.max(element.start[1], element.end[1]) : element.height
|
|
||||||
element.top = verticalCenter - elHeight / 2
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const range = groupElementRangeMap[element.groupId]
|
|
||||||
const center = (range.maxY + range.minY) / 2
|
|
||||||
const offset = center - verticalCenter
|
|
||||||
element.top = element.top - offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
slidesStore.updateSlide({ elements: elementList })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
alignActiveElement,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import { ElementAlignCommands } from '../types/edit'
|
|
||||||
import { getElementListRange } from '../utils/element'
|
|
||||||
import useHistorySnapshot from './useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { activeElementIdList, activeElementList } = storeToRefs(useMainStore())
|
|
||||||
const { currentSlide, viewportRatio, viewportSize } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将所有选中的元素对齐到画布
|
|
||||||
* @param command 对齐方向
|
|
||||||
*/
|
|
||||||
const alignElementToCanvas = (command: ElementAlignCommands) => {
|
|
||||||
const viewportWidth = viewportSize.value
|
|
||||||
const viewportHeight = viewportSize.value * viewportRatio.value
|
|
||||||
const { minX, maxX, minY, maxY } = getElementListRange(activeElementList.value)
|
|
||||||
|
|
||||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
|
|
||||||
for (const element of newElementList) {
|
|
||||||
if (!activeElementIdList.value.includes(element.id)) continue
|
|
||||||
|
|
||||||
// 水平垂直居中
|
|
||||||
if (command === ElementAlignCommands.CENTER) {
|
|
||||||
const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
|
|
||||||
const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
|
|
||||||
element.top = element.top - offsetY
|
|
||||||
element.left = element.left - offsetX
|
|
||||||
}
|
|
||||||
|
|
||||||
// 顶部对齐
|
|
||||||
if (command === ElementAlignCommands.TOP) {
|
|
||||||
const offsetY = minY - 0
|
|
||||||
element.top = element.top - offsetY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 垂直居中
|
|
||||||
else if (command === ElementAlignCommands.VERTICAL) {
|
|
||||||
const offsetY = minY + (maxY - minY) / 2 - viewportHeight / 2
|
|
||||||
element.top = element.top - offsetY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 底部对齐
|
|
||||||
else if (command === ElementAlignCommands.BOTTOM) {
|
|
||||||
const offsetY = maxY - viewportHeight
|
|
||||||
element.top = element.top - offsetY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 左侧对齐
|
|
||||||
else if (command === ElementAlignCommands.LEFT) {
|
|
||||||
const offsetX = minX - 0
|
|
||||||
element.left = element.left - offsetX
|
|
||||||
}
|
|
||||||
|
|
||||||
// 水平居中
|
|
||||||
else if (command === ElementAlignCommands.HORIZONTAL) {
|
|
||||||
const offsetX = minX + (maxX - minX) / 2 - viewportWidth / 2
|
|
||||||
element.left = element.left - offsetX
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右侧对齐
|
|
||||||
else if (command === ElementAlignCommands.RIGHT) {
|
|
||||||
const offsetX = maxX - viewportWidth
|
|
||||||
element.left = element.left - offsetX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
alignElementToCanvas,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
import { computed } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { nanoid } from 'nanoid'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { activeElementIdList, activeElementList, handleElementId } = storeToRefs(mainStore)
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断当前选中的元素是否可以组合
|
|
||||||
*/
|
|
||||||
const canCombine = computed(() => {
|
|
||||||
if (activeElementList.value.length < 2) return false
|
|
||||||
|
|
||||||
const firstGroupId = activeElementList.value[0].groupId
|
|
||||||
if (!firstGroupId) return true
|
|
||||||
|
|
||||||
const inSameGroup = activeElementList.value.every(el => (el.groupId && el.groupId) === firstGroupId)
|
|
||||||
return !inSameGroup
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组合当前选中的元素:给当前选中的元素赋予一个相同的分组ID
|
|
||||||
*/
|
|
||||||
const combineElements = () => {
|
|
||||||
if (!activeElementList.value.length) return
|
|
||||||
|
|
||||||
// 生成一个新元素列表进行后续操作
|
|
||||||
let newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
|
|
||||||
|
|
||||||
// 生成分组ID
|
|
||||||
const groupId = nanoid(10)
|
|
||||||
|
|
||||||
// 收集需要组合的元素列表,并赋上唯一分组ID
|
|
||||||
const combineElementList: PPTElement[] = []
|
|
||||||
for (const element of newElementList) {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) {
|
|
||||||
element.groupId = groupId
|
|
||||||
combineElementList.push(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保该组合内所有元素成员的层级是连续的,具体操作方法为:
|
|
||||||
// 先获取到该组合内最上层元素的层级,将本次需要组合的元素从新元素列表中移除,
|
|
||||||
// 再根据最上层元素的层级位置,将上面收集到的需要组合的元素列表一起插入到新元素列表中合适的位置
|
|
||||||
const combineElementMaxLevel = newElementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id)
|
|
||||||
const combineElementIdList = combineElementList.map(_element => _element.id)
|
|
||||||
newElementList = newElementList.filter(_element => !combineElementIdList.includes(_element.id))
|
|
||||||
|
|
||||||
const insertLevel = combineElementMaxLevel - combineElementList.length + 1
|
|
||||||
newElementList.splice(insertLevel, 0, ...combineElementList)
|
|
||||||
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消组合元素:移除选中元素的分组ID
|
|
||||||
*/
|
|
||||||
const uncombineElements = () => {
|
|
||||||
if (!activeElementList.value.length) return
|
|
||||||
const hasElementInGroup = activeElementList.value.some(item => item.groupId)
|
|
||||||
if (!hasElementInGroup) return
|
|
||||||
|
|
||||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
|
|
||||||
for (const element of newElementList) {
|
|
||||||
if (activeElementIdList.value.includes(element.id) && element.groupId) delete element.groupId
|
|
||||||
}
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
|
|
||||||
// 取消组合后,需要重置激活元素状态
|
|
||||||
// 默认重置为当前正在操作的元素,如果不存在则重置为空
|
|
||||||
const handleElementIdList = handleElementId.value ? [handleElementId.value] : []
|
|
||||||
mainStore.setActiveElementIdList(handleElementIdList)
|
|
||||||
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
canCombine,
|
|
||||||
combineElements,
|
|
||||||
uncombineElements,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore } from '../store'
|
|
||||||
import { copyText, readClipboard } from '../utils/clipboard'
|
|
||||||
import { encrypt } from '../utils/crypto'
|
|
||||||
import message from '../utils/message'
|
|
||||||
import usePasteTextClipboardData from '../hooks/usePasteTextClipboardData'
|
|
||||||
import useDeleteElement from './useDeleteElement'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const { activeElementIdList, activeElementList } = storeToRefs(mainStore)
|
|
||||||
|
|
||||||
const { pasteTextClipboardData } = usePasteTextClipboardData()
|
|
||||||
const { deleteElement } = useDeleteElement()
|
|
||||||
|
|
||||||
// 将选中元素数据加密后复制到剪贴板
|
|
||||||
const copyElement = () => {
|
|
||||||
if (!activeElementIdList.value.length) return
|
|
||||||
|
|
||||||
const text = encrypt(JSON.stringify({
|
|
||||||
type: 'elements',
|
|
||||||
data: activeElementList.value,
|
|
||||||
}))
|
|
||||||
|
|
||||||
copyText(text).then(() => {
|
|
||||||
mainStore.setEditorareaFocus(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将选中元素复制后删除(剪切)
|
|
||||||
const cutElement = () => {
|
|
||||||
copyElement()
|
|
||||||
deleteElement()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试将剪贴板元素数据解密后进行粘贴
|
|
||||||
const pasteElement = () => {
|
|
||||||
readClipboard().then(text => {
|
|
||||||
pasteTextClipboardData(text)
|
|
||||||
}).catch(err => message.warning(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将选中元素复制后立刻粘贴
|
|
||||||
const quickCopyElement = () => {
|
|
||||||
copyElement()
|
|
||||||
pasteElement()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
copyElement,
|
|
||||||
cutElement,
|
|
||||||
pasteElement,
|
|
||||||
quickCopyElement,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,325 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { nanoid } from 'nanoid'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import { getImageSize } from '../utils/image'
|
|
||||||
import type { PPTLineElement, PPTElement, TableCell, TableCellStyle, PPTShapeElement, ChartType } from '../types/slides'
|
|
||||||
import { type ShapePoolItem, SHAPE_PATH_FORMULAS } from '../configs/shapes'
|
|
||||||
import type { LinePoolItem } from '../configs/lines'
|
|
||||||
import { CHART_DEFAULT_DATA } from '../configs/chart'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
interface CommonElementPosition {
|
|
||||||
top: number
|
|
||||||
left: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LineElementPosition {
|
|
||||||
top: number
|
|
||||||
left: number
|
|
||||||
start: [number, number]
|
|
||||||
end: [number, number]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateTextData {
|
|
||||||
content?: string
|
|
||||||
vertical?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { creatingElement } = storeToRefs(mainStore)
|
|
||||||
const { theme, viewportRatio, viewportSize } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
// 创建(插入)一个元素并将其设置为被选中元素
|
|
||||||
const createElement = (element: PPTElement, callback?: () => void) => {
|
|
||||||
slidesStore.addElement(element)
|
|
||||||
mainStore.setActiveElementIdList([element.id])
|
|
||||||
|
|
||||||
if (creatingElement.value) mainStore.setCreatingElement(null)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
mainStore.setEditorareaFocus(true)
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
if (callback) callback()
|
|
||||||
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建图片元素
|
|
||||||
* @param src 图片地址
|
|
||||||
*/
|
|
||||||
const createImageElement = (src: string) => {
|
|
||||||
getImageSize(src).then(({ width, height }) => {
|
|
||||||
const scale = height / width
|
|
||||||
|
|
||||||
if (scale < viewportRatio.value && width > viewportSize.value) {
|
|
||||||
width = viewportSize.value
|
|
||||||
height = width * scale
|
|
||||||
}
|
|
||||||
else if (height > viewportSize.value * viewportRatio.value) {
|
|
||||||
height = viewportSize.value * viewportRatio.value
|
|
||||||
width = height / scale
|
|
||||||
}
|
|
||||||
|
|
||||||
createElement({
|
|
||||||
type: 'image',
|
|
||||||
id: nanoid(10),
|
|
||||||
src,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
left: (viewportSize.value - width) / 2,
|
|
||||||
top: (viewportSize.value * viewportRatio.value - height) / 2,
|
|
||||||
fixedRatio: true,
|
|
||||||
rotate: 0,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建图表元素
|
|
||||||
* @param chartType 图表类型
|
|
||||||
*/
|
|
||||||
const createChartElement = (type: ChartType) => {
|
|
||||||
createElement({
|
|
||||||
type: 'chart',
|
|
||||||
id: nanoid(10),
|
|
||||||
chartType: type,
|
|
||||||
left: 300,
|
|
||||||
top: 81.25,
|
|
||||||
width: 400,
|
|
||||||
height: 400,
|
|
||||||
rotate: 0,
|
|
||||||
themeColors: [theme.value.themeColor],
|
|
||||||
textColor: theme.value.fontColor,
|
|
||||||
data: CHART_DEFAULT_DATA[type],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建表格元素
|
|
||||||
* @param row 行数
|
|
||||||
* @param col 列数
|
|
||||||
*/
|
|
||||||
const createTableElement = (row: number, col: number) => {
|
|
||||||
const style: TableCellStyle = {
|
|
||||||
fontname: theme.value.fontName,
|
|
||||||
color: theme.value.fontColor,
|
|
||||||
}
|
|
||||||
const data: TableCell[][] = []
|
|
||||||
for (let i = 0; i < row; i++) {
|
|
||||||
const rowCells: TableCell[] = []
|
|
||||||
for (let j = 0; j < col; j++) {
|
|
||||||
rowCells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '', style })
|
|
||||||
}
|
|
||||||
data.push(rowCells)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_CELL_WIDTH = 100
|
|
||||||
const DEFAULT_CELL_HEIGHT = 36
|
|
||||||
|
|
||||||
const colWidths: number[] = new Array(col).fill(1 / col)
|
|
||||||
|
|
||||||
const width = col * DEFAULT_CELL_WIDTH
|
|
||||||
const height = row * DEFAULT_CELL_HEIGHT
|
|
||||||
|
|
||||||
createElement({
|
|
||||||
type: 'table',
|
|
||||||
id: nanoid(10),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
colWidths,
|
|
||||||
rotate: 0,
|
|
||||||
data,
|
|
||||||
left: (viewportSize.value - width) / 2,
|
|
||||||
top: (viewportSize.value * viewportRatio.value - height) / 2,
|
|
||||||
outline: {
|
|
||||||
width: 2,
|
|
||||||
style: 'solid',
|
|
||||||
color: '#eeece1',
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
color: theme.value.themeColor,
|
|
||||||
rowHeader: true,
|
|
||||||
rowFooter: false,
|
|
||||||
colHeader: false,
|
|
||||||
colFooter: false,
|
|
||||||
},
|
|
||||||
cellMinHeight: 36,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建文本元素
|
|
||||||
* @param position 位置大小信息
|
|
||||||
* @param content 文本内容
|
|
||||||
*/
|
|
||||||
const createTextElement = (position: CommonElementPosition, data?: CreateTextData) => {
|
|
||||||
const { left, top, width, height } = position
|
|
||||||
const content = data?.content || ''
|
|
||||||
const vertical = data?.vertical || false
|
|
||||||
|
|
||||||
const id = nanoid(10)
|
|
||||||
createElement({
|
|
||||||
type: 'text',
|
|
||||||
id,
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
content,
|
|
||||||
rotate: 0,
|
|
||||||
defaultFontName: theme.value.fontName,
|
|
||||||
defaultColor: theme.value.fontColor,
|
|
||||||
vertical,
|
|
||||||
}, () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const editorRef: HTMLElement | null = document.querySelector(`#editable-element-${id} .ProseMirror`)
|
|
||||||
if (editorRef) editorRef.focus()
|
|
||||||
}, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建形状元素
|
|
||||||
* @param position 位置大小信息
|
|
||||||
* @param data 形状路径信息
|
|
||||||
*/
|
|
||||||
const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem, supplement: Partial<PPTShapeElement> = {}) => {
|
|
||||||
const { left, top, width, height } = position
|
|
||||||
const newElement: PPTShapeElement = {
|
|
||||||
type: 'shape',
|
|
||||||
id: nanoid(10),
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
viewBox: data.viewBox,
|
|
||||||
path: data.path,
|
|
||||||
fill: theme.value.themeColor,
|
|
||||||
fixedRatio: false,
|
|
||||||
rotate: 0,
|
|
||||||
...supplement,
|
|
||||||
}
|
|
||||||
if (data.withborder) newElement.outline = theme.value.outline
|
|
||||||
if (data.special) newElement.special = true
|
|
||||||
if (data.pathFormula) {
|
|
||||||
newElement.pathFormula = data.pathFormula
|
|
||||||
newElement.viewBox = [width, height]
|
|
||||||
|
|
||||||
const pathFormula = SHAPE_PATH_FORMULAS[data.pathFormula]
|
|
||||||
if ('editable' in pathFormula && pathFormula.editable) {
|
|
||||||
newElement.path = pathFormula.formula(width, height, pathFormula.defaultValue!)
|
|
||||||
newElement.keypoints = pathFormula.defaultValue
|
|
||||||
}
|
|
||||||
else newElement.path = pathFormula.formula(width, height)
|
|
||||||
}
|
|
||||||
createElement(newElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建线条元素
|
|
||||||
* @param position 位置大小信息
|
|
||||||
* @param data 线条的路径和样式
|
|
||||||
*/
|
|
||||||
const createLineElement = (position: LineElementPosition, data: LinePoolItem) => {
|
|
||||||
const { left, top, start, end } = position
|
|
||||||
|
|
||||||
const newElement: PPTLineElement = {
|
|
||||||
type: 'line',
|
|
||||||
id: nanoid(10),
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
points: data.points,
|
|
||||||
color: theme.value.themeColor,
|
|
||||||
style: data.style,
|
|
||||||
width: 2,
|
|
||||||
}
|
|
||||||
if (data.isBroken) newElement.broken = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
|
|
||||||
if (data.isBroken2) newElement.broken2 = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
|
|
||||||
if (data.isCurve) newElement.curve = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
|
|
||||||
if (data.isCubic) newElement.cubic = [[(start[0] + end[0]) / 2, (start[1] + end[1]) / 2], [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]]
|
|
||||||
createElement(newElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建LaTeX元素
|
|
||||||
* @param svg SVG代码
|
|
||||||
*/
|
|
||||||
const createLatexElement = (data: { path: string; latex: string; w: number; h: number; }) => {
|
|
||||||
createElement({
|
|
||||||
type: 'latex',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: data.w,
|
|
||||||
height: data.h,
|
|
||||||
rotate: 0,
|
|
||||||
left: (viewportSize.value - data.w) / 2,
|
|
||||||
top: (viewportSize.value * viewportRatio.value - data.h) / 2,
|
|
||||||
path: data.path,
|
|
||||||
latex: data.latex,
|
|
||||||
color: theme.value.fontColor,
|
|
||||||
strokeWidth: 2,
|
|
||||||
viewBox: [data.w, data.h],
|
|
||||||
fixedRatio: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建视频元素
|
|
||||||
* @param src 视频地址
|
|
||||||
*/
|
|
||||||
const createVideoElement = (src: string) => {
|
|
||||||
createElement({
|
|
||||||
type: 'video',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: 500,
|
|
||||||
height: 300,
|
|
||||||
rotate: 0,
|
|
||||||
left: (viewportSize.value - 500) / 2,
|
|
||||||
top: (viewportSize.value * viewportRatio.value - 300) / 2,
|
|
||||||
src,
|
|
||||||
autoplay: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建音频元素
|
|
||||||
* @param src 音频地址
|
|
||||||
*/
|
|
||||||
const createAudioElement = (src: string) => {
|
|
||||||
createElement({
|
|
||||||
type: 'audio',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
rotate: 0,
|
|
||||||
left: (viewportSize.value - 50) / 2,
|
|
||||||
top: (viewportSize.value * viewportRatio.value - 50) / 2,
|
|
||||||
loop: false,
|
|
||||||
autoplay: false,
|
|
||||||
fixedRatio: true,
|
|
||||||
color: theme.value.themeColor,
|
|
||||||
src,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
createImageElement,
|
|
||||||
createChartElement,
|
|
||||||
createTableElement,
|
|
||||||
createTextElement,
|
|
||||||
createShapeElement,
|
|
||||||
createLineElement,
|
|
||||||
createLatexElement,
|
|
||||||
createVideoElement,
|
|
||||||
createAudioElement,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { activeElementIdList, activeGroupElementId } = storeToRefs(mainStore)
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
// 删除全部选中元素
|
|
||||||
// 组合元素成员中,存在被选中可独立操作的元素时,优先删除该元素。否则默认删除所有被选中的元素
|
|
||||||
const deleteElement = () => {
|
|
||||||
if (!activeElementIdList.value.length) return
|
|
||||||
|
|
||||||
let newElementList: PPTElement[] = []
|
|
||||||
if (activeGroupElementId.value) {
|
|
||||||
newElementList = currentSlide.value.elements.filter(el => el.id !== activeGroupElementId.value)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newElementList = currentSlide.value.elements.filter(el => !activeElementIdList.value.includes(el.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
mainStore.setActiveElementIdList([])
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除内面内全部元素(无论是否选中)
|
|
||||||
const deleteAllElements = () => {
|
|
||||||
if (!currentSlide.value.elements.length) return
|
|
||||||
mainStore.setActiveElementIdList([])
|
|
||||||
slidesStore.updateSlide({ elements: [] })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
deleteElement,
|
|
||||||
deleteAllElements,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,855 +0,0 @@
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { trim } from 'lodash'
|
|
||||||
import { saveAs } from 'file-saver'
|
|
||||||
import pptxgen from 'pptxgenjs'
|
|
||||||
import tinycolor from 'tinycolor2'
|
|
||||||
import { toPng, toJpeg } from 'html-to-image'
|
|
||||||
import { useSlidesStore } from '../store'
|
|
||||||
import type { PPTElementOutline, PPTElementShadow, PPTElementLink, Slide } from '../types/slides'
|
|
||||||
import { getElementRange, getLineElementPath, getTableSubThemeColor } from '../utils/element'
|
|
||||||
import { type AST, toAST } from '../utils/htmlParser'
|
|
||||||
import { type SvgPoints, toPoints } from '../utils/svgPathParser'
|
|
||||||
import { encrypt } from '../utils/crypto'
|
|
||||||
import { svg2Base64 } from '../utils/svg2Base64'
|
|
||||||
import message from '../utils/message'
|
|
||||||
|
|
||||||
interface ExportImageConfig {
|
|
||||||
quality: number
|
|
||||||
width: number
|
|
||||||
fontEmbedCSS?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { slides, theme, viewportRatio, title, viewportSize } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const ratioPx2Inch = computed(() => {
|
|
||||||
return 96 * (viewportSize.value / 960)
|
|
||||||
})
|
|
||||||
const ratioPx2Pt = computed(() => {
|
|
||||||
return 96 / 72 * (viewportSize.value / 960)
|
|
||||||
})
|
|
||||||
|
|
||||||
const exporting = ref(false)
|
|
||||||
|
|
||||||
// 导出图片
|
|
||||||
const exportImage = (domRef: HTMLElement, format: string, quality: number, ignoreWebfont = true) => {
|
|
||||||
exporting.value = true
|
|
||||||
const toImage = format === 'png' ? toPng : toJpeg
|
|
||||||
|
|
||||||
const foreignObjectSpans = domRef.querySelectorAll('foreignObject [xmlns]')
|
|
||||||
foreignObjectSpans.forEach(spanRef => spanRef.removeAttribute('xmlns'))
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const config: ExportImageConfig = {
|
|
||||||
quality,
|
|
||||||
width: 1600,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ignoreWebfont) config.fontEmbedCSS = ''
|
|
||||||
|
|
||||||
toImage(domRef, config).then(dataUrl => {
|
|
||||||
exporting.value = false
|
|
||||||
saveAs(dataUrl, `${title.value}.${format}`)
|
|
||||||
}).catch(() => {
|
|
||||||
exporting.value = false
|
|
||||||
message.error('导出图片失败')
|
|
||||||
})
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出pptist文件(特有 .pptist 后缀文件)
|
|
||||||
const exportSpecificFile = (_slides: Slide[]) => {
|
|
||||||
const blob = new Blob([encrypt(JSON.stringify(_slides))], { type: '' })
|
|
||||||
saveAs(blob, `${title.value}.pptist`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出JSON文件
|
|
||||||
const exportJSON = () => {
|
|
||||||
const json = {
|
|
||||||
title: title.value,
|
|
||||||
width: viewportSize.value,
|
|
||||||
height: viewportSize.value * viewportRatio.value,
|
|
||||||
slides: slides.value,
|
|
||||||
}
|
|
||||||
const blob = new Blob([JSON.stringify(json)], { type: '' })
|
|
||||||
saveAs(blob, `${title.value}.json`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化颜色值为 透明度 + HexString,供pptxgenjs使用
|
|
||||||
const formatColor = (_color: string) => {
|
|
||||||
const c = tinycolor(_color)
|
|
||||||
const alpha = c.getAlpha()
|
|
||||||
const color = alpha === 0 ? '#ffffff' : c.setAlpha(1).toHexString()
|
|
||||||
return {
|
|
||||||
alpha,
|
|
||||||
color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormatColor = ReturnType<typeof formatColor>
|
|
||||||
|
|
||||||
// 将HTML字符串格式化为pptxgenjs所需的格式
|
|
||||||
// 核心思路:将HTML字符串按样式分片平铺,每个片段需要继承祖先元素的样式信息,遇到块级元素需要换行
|
|
||||||
const formatHTML = (html: string) => {
|
|
||||||
const ast = toAST(html)
|
|
||||||
let bulletFlag = false
|
|
||||||
let indent = 0
|
|
||||||
|
|
||||||
const slices: pptxgen.TextProps[] = []
|
|
||||||
const parse = (obj: AST[], baseStyleObj: { [key: string]: string } = {}) => {
|
|
||||||
|
|
||||||
for (const item of obj) {
|
|
||||||
const isBlockTag = 'tagName' in item && ['div', 'li', 'p'].includes(item.tagName)
|
|
||||||
|
|
||||||
if (isBlockTag && slices.length) {
|
|
||||||
const lastSlice = slices[slices.length - 1]
|
|
||||||
if (!lastSlice.options) lastSlice.options = {}
|
|
||||||
lastSlice.options.breakLine = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const styleObj = { ...baseStyleObj }
|
|
||||||
const styleAttr = 'attributes' in item ? item.attributes.find(attr => attr.key === 'style') : null
|
|
||||||
if (styleAttr && styleAttr.value) {
|
|
||||||
const styleArr = styleAttr.value.split(';')
|
|
||||||
for (const styleItem of styleArr) {
|
|
||||||
const [_key, _value] = styleItem.split(': ')
|
|
||||||
const [key, value] = [trim(_key), trim(_value)]
|
|
||||||
if (key && value) styleObj[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('tagName' in item) {
|
|
||||||
if (item.tagName === 'em') {
|
|
||||||
styleObj['font-style'] = 'italic'
|
|
||||||
}
|
|
||||||
if (item.tagName === 'strong') {
|
|
||||||
styleObj['font-weight'] = 'bold'
|
|
||||||
}
|
|
||||||
if (item.tagName === 'sup') {
|
|
||||||
styleObj['vertical-align'] = 'super'
|
|
||||||
}
|
|
||||||
if (item.tagName === 'sub') {
|
|
||||||
styleObj['vertical-align'] = 'sub'
|
|
||||||
}
|
|
||||||
if (item.tagName === 'a') {
|
|
||||||
const attr = item.attributes.find(attr => attr.key === 'href')
|
|
||||||
styleObj['href'] = attr?.value || ''
|
|
||||||
}
|
|
||||||
if (item.tagName === 'ul') {
|
|
||||||
styleObj['list-type'] = 'ul'
|
|
||||||
}
|
|
||||||
if (item.tagName === 'ol') {
|
|
||||||
styleObj['list-type'] = 'ol'
|
|
||||||
}
|
|
||||||
if (item.tagName === 'li') {
|
|
||||||
bulletFlag = true
|
|
||||||
}
|
|
||||||
if (item.tagName === 'p') {
|
|
||||||
if ('attributes' in item) {
|
|
||||||
const dataIndentAttr = item.attributes.find(attr => attr.key === 'data-indent')
|
|
||||||
if (dataIndentAttr && dataIndentAttr.value) indent = +dataIndentAttr.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('tagName' in item && item.tagName === 'br') {
|
|
||||||
slices.push({ text: '', options: { breakLine: true } })
|
|
||||||
}
|
|
||||||
else if ('content' in item) {
|
|
||||||
const text = item.content.replace(/ /g, ' ').replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&').replace(/\n/g, '')
|
|
||||||
const options: pptxgen.TextPropsOptions = {}
|
|
||||||
|
|
||||||
if (styleObj['font-size']) {
|
|
||||||
options.fontSize = parseInt(styleObj['font-size']) / ratioPx2Pt.value
|
|
||||||
}
|
|
||||||
if (styleObj['color']) {
|
|
||||||
options.color = formatColor(styleObj['color']).color
|
|
||||||
}
|
|
||||||
if (styleObj['background-color']) {
|
|
||||||
options.highlight = formatColor(styleObj['background-color']).color
|
|
||||||
}
|
|
||||||
if (styleObj['text-decoration-line']) {
|
|
||||||
if (styleObj['text-decoration-line'].indexOf('underline') !== -1) {
|
|
||||||
options.underline = {
|
|
||||||
color: options.color || '#000000',
|
|
||||||
style: 'sng',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (styleObj['text-decoration-line'].indexOf('line-through') !== -1) {
|
|
||||||
options.strike = 'sngStrike'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (styleObj['text-decoration']) {
|
|
||||||
if (styleObj['text-decoration'].indexOf('underline') !== -1) {
|
|
||||||
options.underline = {
|
|
||||||
color: options.color || '#000000',
|
|
||||||
style: 'sng',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (styleObj['text-decoration'].indexOf('line-through') !== -1) {
|
|
||||||
options.strike = 'sngStrike'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (styleObj['vertical-align']) {
|
|
||||||
if (styleObj['vertical-align'] === 'super') options.superscript = true
|
|
||||||
if (styleObj['vertical-align'] === 'sub') options.subscript = true
|
|
||||||
}
|
|
||||||
if (styleObj['text-align']) options.align = styleObj['text-align'] as pptxgen.HAlign
|
|
||||||
if (styleObj['font-weight']) options.bold = styleObj['font-weight'] === 'bold'
|
|
||||||
if (styleObj['font-style']) options.italic = styleObj['font-style'] === 'italic'
|
|
||||||
if (styleObj['font-family']) options.fontFace = styleObj['font-family']
|
|
||||||
if (styleObj['href']) options.hyperlink = { url: styleObj['href'] }
|
|
||||||
|
|
||||||
if (bulletFlag && styleObj['list-type'] === 'ol') {
|
|
||||||
options.bullet = { type: 'number', indent: (options.fontSize || 20) * 1.25 }
|
|
||||||
options.paraSpaceBefore = 0.1
|
|
||||||
bulletFlag = false
|
|
||||||
}
|
|
||||||
if (bulletFlag && styleObj['list-type'] === 'ul') {
|
|
||||||
options.bullet = { indent: (options.fontSize || 20) * 1.25 }
|
|
||||||
options.paraSpaceBefore = 0.1
|
|
||||||
bulletFlag = false
|
|
||||||
}
|
|
||||||
if (indent) {
|
|
||||||
options.indentLevel = indent
|
|
||||||
indent = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.push({ text, options })
|
|
||||||
}
|
|
||||||
else if ('children' in item) parse(item.children, styleObj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parse(ast)
|
|
||||||
return slices
|
|
||||||
}
|
|
||||||
|
|
||||||
type Points = Array<
|
|
||||||
| { x: number; y: number; moveTo?: boolean }
|
|
||||||
| { x: number; y: number; curve: { type: 'arc'; hR: number; wR: number; stAng: number; swAng: number } }
|
|
||||||
| { x: number; y: number; curve: { type: 'quadratic'; x1: number; y1: number } }
|
|
||||||
| { x: number; y: number; curve: { type: 'cubic'; x1: number; y1: number; x2: number; y2: number } }
|
|
||||||
| { close: true }
|
|
||||||
>
|
|
||||||
|
|
||||||
// 将SVG路径信息格式化为pptxgenjs所需要的格式
|
|
||||||
const formatPoints = (points: SvgPoints, scale = { x: 1, y: 1 }): Points => {
|
|
||||||
return points.map(point => {
|
|
||||||
if (point.close !== undefined) {
|
|
||||||
return { close: true }
|
|
||||||
}
|
|
||||||
else if (point.type === 'M') {
|
|
||||||
return {
|
|
||||||
x: point.x / ratioPx2Inch.value * scale.x,
|
|
||||||
y: point.y / ratioPx2Inch.value * scale.y,
|
|
||||||
moveTo: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (point.curve) {
|
|
||||||
if (point.curve.type === 'cubic') {
|
|
||||||
return {
|
|
||||||
x: point.x / ratioPx2Inch.value * scale.x,
|
|
||||||
y: point.y / ratioPx2Inch.value * scale.y,
|
|
||||||
curve: {
|
|
||||||
type: 'cubic',
|
|
||||||
x1: (point.curve.x1 as number) / ratioPx2Inch.value * scale.x,
|
|
||||||
y1: (point.curve.y1 as number) / ratioPx2Inch.value * scale.y,
|
|
||||||
x2: (point.curve.x2 as number) / ratioPx2Inch.value * scale.x,
|
|
||||||
y2: (point.curve.y2 as number) / ratioPx2Inch.value * scale.y,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (point.curve.type === 'quadratic') {
|
|
||||||
return {
|
|
||||||
x: point.x / ratioPx2Inch.value * scale.x,
|
|
||||||
y: point.y / ratioPx2Inch.value * scale.y,
|
|
||||||
curve: {
|
|
||||||
type: 'quadratic',
|
|
||||||
x1: (point.curve.x1 as number) / ratioPx2Inch.value * scale.x,
|
|
||||||
y1: (point.curve.y1 as number) / ratioPx2Inch.value * scale.y,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
x: point.x / ratioPx2Inch.value * scale.x,
|
|
||||||
y: point.y / ratioPx2Inch.value * scale.y,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取阴影配置
|
|
||||||
const getShadowOption = (shadow: PPTElementShadow): pptxgen.ShadowProps => {
|
|
||||||
const c = formatColor(shadow.color)
|
|
||||||
const { h, v } = shadow
|
|
||||||
|
|
||||||
let offset = 4
|
|
||||||
let angle = 45
|
|
||||||
|
|
||||||
if (h === 0 && v === 0) {
|
|
||||||
offset = 4
|
|
||||||
angle = 45
|
|
||||||
}
|
|
||||||
else if (h === 0) {
|
|
||||||
if (v > 0) {
|
|
||||||
offset = v
|
|
||||||
angle = 90
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
offset = -v
|
|
||||||
angle = 270
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (v === 0) {
|
|
||||||
if (h > 0) {
|
|
||||||
offset = h
|
|
||||||
angle = 1
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
offset = -h
|
|
||||||
angle = 180
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (h > 0 && v > 0) {
|
|
||||||
offset = Math.max(h, v)
|
|
||||||
angle = 45
|
|
||||||
}
|
|
||||||
else if (h > 0 && v < 0) {
|
|
||||||
offset = Math.max(h, -v)
|
|
||||||
angle = 315
|
|
||||||
}
|
|
||||||
else if (h < 0 && v > 0) {
|
|
||||||
offset = Math.max(-h, v)
|
|
||||||
angle = 135
|
|
||||||
}
|
|
||||||
else if (h < 0 && v < 0) {
|
|
||||||
offset = Math.max(-h, -v)
|
|
||||||
angle = 225
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'outer',
|
|
||||||
color: c.color.replace('#', ''),
|
|
||||||
opacity: c.alpha,
|
|
||||||
blur: shadow.blur / ratioPx2Pt.value,
|
|
||||||
offset,
|
|
||||||
angle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dashTypeMap = {
|
|
||||||
'solid': 'solid',
|
|
||||||
'dashed': 'dash',
|
|
||||||
'dotted': 'sysDot',
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取边框配置
|
|
||||||
const getOutlineOption = (outline: PPTElementOutline): pptxgen.ShapeLineProps => {
|
|
||||||
const c = formatColor(outline?.color || '#000000')
|
|
||||||
|
|
||||||
return {
|
|
||||||
color: c.color,
|
|
||||||
transparency: (1 - c.alpha) * 100,
|
|
||||||
width: (outline.width || 1) / ratioPx2Pt.value,
|
|
||||||
dashType: outline.style ? dashTypeMap[outline.style] as 'solid' | 'dash' | 'sysDot' : 'solid',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取超链接配置
|
|
||||||
const getLinkOption = (link: PPTElementLink): pptxgen.HyperlinkProps | null => {
|
|
||||||
const { type, target } = link
|
|
||||||
if (type === 'web') return { url: target }
|
|
||||||
if (type === 'slide') {
|
|
||||||
const index = slides.value.findIndex(slide => slide.id === target)
|
|
||||||
if (index !== -1) return { slide: index + 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否为Base64图片地址
|
|
||||||
const isBase64Image = (url: string) => {
|
|
||||||
const regex = /^data:image\/[^;]+;base64,/
|
|
||||||
return url.match(regex) !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出PPTX文件
|
|
||||||
const exportPPTX = (_slides: Slide[], masterOverwrite: boolean, ignoreMedia: boolean) => {
|
|
||||||
exporting.value = true
|
|
||||||
const pptx = new pptxgen()
|
|
||||||
|
|
||||||
if (viewportRatio.value === 0.625) pptx.layout = 'LAYOUT_16x10'
|
|
||||||
else if (viewportRatio.value === 0.75) pptx.layout = 'LAYOUT_4x3'
|
|
||||||
else if (viewportRatio.value === 0.70710678) {
|
|
||||||
pptx.defineLayout({ name: 'A3', width: 10, height: 7.0710678 })
|
|
||||||
pptx.layout = 'A3'
|
|
||||||
}
|
|
||||||
else if (viewportRatio.value === 1.41421356) {
|
|
||||||
pptx.defineLayout({ name: 'A3_V', width: 10, height: 14.1421356 })
|
|
||||||
pptx.layout = 'A3_V'
|
|
||||||
}
|
|
||||||
else pptx.layout = 'LAYOUT_16x9'
|
|
||||||
|
|
||||||
if (masterOverwrite) {
|
|
||||||
const { color: bgColor, alpha: bgAlpha } = formatColor(theme.value.backgroundColor)
|
|
||||||
pptx.defineSlideMaster({
|
|
||||||
title: 'PPTIST_MASTER',
|
|
||||||
background: { color: bgColor, transparency: (1 - bgAlpha) * 100 },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const slide of _slides) {
|
|
||||||
const pptxSlide = pptx.addSlide()
|
|
||||||
|
|
||||||
if (slide.background) {
|
|
||||||
const background = slide.background
|
|
||||||
if (background.type === 'image' && background.image) {
|
|
||||||
if (isBase64Image(background.image.src)) pptxSlide.background = { data: background.image.src }
|
|
||||||
else pptxSlide.background = { path: background.image.src }
|
|
||||||
}
|
|
||||||
else if (background.type === 'solid' && background.color) {
|
|
||||||
const c = formatColor(background.color)
|
|
||||||
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
|
||||||
}
|
|
||||||
else if (background.type === 'gradient' && background.gradient) {
|
|
||||||
const colors = background.gradient.colors
|
|
||||||
const color1 = colors[0].color
|
|
||||||
const color2 = colors[colors.length - 1].color
|
|
||||||
const color = tinycolor.mix(color1, color2).toHexString()
|
|
||||||
const c = formatColor(color)
|
|
||||||
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (slide.remark) pptxSlide.addNotes(slide.remark)
|
|
||||||
|
|
||||||
if (!slide.elements) continue
|
|
||||||
|
|
||||||
for (const el of slide.elements) {
|
|
||||||
if (el.type === 'text') {
|
|
||||||
const textProps = formatHTML(el.content)
|
|
||||||
|
|
||||||
const options: pptxgen.TextPropsOptions = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
fontSize: 20 / ratioPx2Pt.value,
|
|
||||||
fontFace: '微软雅黑',
|
|
||||||
color: '#000000',
|
|
||||||
valign: 'top',
|
|
||||||
margin: 10 / ratioPx2Pt.value,
|
|
||||||
paraSpaceBefore: 5 / ratioPx2Pt.value,
|
|
||||||
lineSpacingMultiple: 1.5 / 1.25,
|
|
||||||
autoFit: true,
|
|
||||||
}
|
|
||||||
if (el.rotate) options.rotate = el.rotate
|
|
||||||
if (el.wordSpace) options.charSpacing = el.wordSpace / ratioPx2Pt.value
|
|
||||||
if (el.lineHeight) options.lineSpacingMultiple = el.lineHeight / 1.25
|
|
||||||
if (el.fill) {
|
|
||||||
const c = formatColor(el.fill)
|
|
||||||
const opacity = el.opacity === undefined ? 1 : el.opacity
|
|
||||||
options.fill = { color: c.color, transparency: (1 - c.alpha * opacity) * 100 }
|
|
||||||
}
|
|
||||||
if (el.defaultColor) options.color = formatColor(el.defaultColor).color
|
|
||||||
if (el.defaultFontName) options.fontFace = el.defaultFontName
|
|
||||||
if (el.shadow) options.shadow = getShadowOption(el.shadow)
|
|
||||||
if (el.outline?.width) options.line = getOutlineOption(el.outline)
|
|
||||||
if (el.opacity !== undefined) options.transparency = (1 - el.opacity) * 100
|
|
||||||
if (el.paragraphSpace !== undefined) options.paraSpaceBefore = el.paragraphSpace / ratioPx2Pt.value
|
|
||||||
if (el.vertical) options.vert = 'eaVert'
|
|
||||||
|
|
||||||
pptxSlide.addText(textProps, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (el.type === 'image') {
|
|
||||||
const options: pptxgen.ImageProps = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
}
|
|
||||||
if (isBase64Image(el.src)) options.data = el.src
|
|
||||||
else options.path = el.src
|
|
||||||
|
|
||||||
if (el.flipH) options.flipH = el.flipH
|
|
||||||
if (el.flipV) options.flipV = el.flipV
|
|
||||||
if (el.rotate) options.rotate = el.rotate
|
|
||||||
if (el.link) {
|
|
||||||
const linkOption = getLinkOption(el.link)
|
|
||||||
if (linkOption) options.hyperlink = linkOption
|
|
||||||
}
|
|
||||||
if (el.filters?.opacity) options.transparency = 100 - parseInt(el.filters?.opacity)
|
|
||||||
if (el.clip) {
|
|
||||||
if (el.clip.shape === 'ellipse') options.rounding = true
|
|
||||||
|
|
||||||
const [start, end] = el.clip.range
|
|
||||||
const [startX, startY] = start
|
|
||||||
const [endX, endY] = end
|
|
||||||
|
|
||||||
const originW = el.width / ((endX - startX) / ratioPx2Inch.value)
|
|
||||||
const originH = el.height / ((endY - startY) / ratioPx2Inch.value)
|
|
||||||
|
|
||||||
options.w = originW / ratioPx2Inch.value
|
|
||||||
options.h = originH / ratioPx2Inch.value
|
|
||||||
|
|
||||||
options.sizing = {
|
|
||||||
type: 'crop',
|
|
||||||
x: startX / ratioPx2Inch.value * originW / ratioPx2Inch.value,
|
|
||||||
y: startY / ratioPx2Inch.value * originH / ratioPx2Inch.value,
|
|
||||||
w: (endX - startX) / ratioPx2Inch.value * originW / ratioPx2Inch.value,
|
|
||||||
h: (endY - startY) / ratioPx2Inch.value * originH / ratioPx2Inch.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pptxSlide.addImage(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (el.type === 'shape') {
|
|
||||||
if (el.special) {
|
|
||||||
const svgRef = document.querySelector(`.thumbnail-list .base-element-${el.id} svg`) as HTMLElement
|
|
||||||
if (svgRef.clientWidth < 1 || svgRef.clientHeight < 1) continue // 临时处理(导入PPTX文件带来的异常数据)
|
|
||||||
const base64SVG = svg2Base64(svgRef)
|
|
||||||
|
|
||||||
const options: pptxgen.ImageProps = {
|
|
||||||
data: base64SVG,
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
}
|
|
||||||
if (el.rotate) options.rotate = el.rotate
|
|
||||||
if (el.link) {
|
|
||||||
const linkOption = getLinkOption(el.link)
|
|
||||||
if (linkOption) options.hyperlink = linkOption
|
|
||||||
}
|
|
||||||
|
|
||||||
pptxSlide.addImage(options)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const scale = {
|
|
||||||
x: el.width / el.viewBox[0],
|
|
||||||
y: el.height / el.viewBox[1],
|
|
||||||
}
|
|
||||||
const points = formatPoints(toPoints(el.path), scale)
|
|
||||||
|
|
||||||
let fillColor = formatColor(el.fill)
|
|
||||||
if (el.gradient) {
|
|
||||||
const colors = el.gradient.colors
|
|
||||||
const color1 = colors[0].color
|
|
||||||
const color2 = colors[colors.length - 1].color
|
|
||||||
const color = tinycolor.mix(color1, color2).toHexString()
|
|
||||||
fillColor = formatColor(color)
|
|
||||||
}
|
|
||||||
const opacity = el.opacity === undefined ? 1 : el.opacity
|
|
||||||
|
|
||||||
const options: pptxgen.ShapeProps = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 },
|
|
||||||
points,
|
|
||||||
}
|
|
||||||
if (el.flipH) options.flipH = el.flipH
|
|
||||||
if (el.flipV) options.flipV = el.flipV
|
|
||||||
if (el.shadow) options.shadow = getShadowOption(el.shadow)
|
|
||||||
if (el.outline?.width) options.line = getOutlineOption(el.outline)
|
|
||||||
if (el.rotate) options.rotate = el.rotate
|
|
||||||
if (el.link) {
|
|
||||||
const linkOption = getLinkOption(el.link)
|
|
||||||
if (linkOption) options.hyperlink = linkOption
|
|
||||||
}
|
|
||||||
|
|
||||||
pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options)
|
|
||||||
}
|
|
||||||
if (el.text) {
|
|
||||||
const textProps = formatHTML(el.text.content)
|
|
||||||
|
|
||||||
const options: pptxgen.TextPropsOptions = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
fontSize: 20 / ratioPx2Pt.value,
|
|
||||||
fontFace: '微软雅黑',
|
|
||||||
color: '#000000',
|
|
||||||
paraSpaceBefore: 5 / ratioPx2Pt.value,
|
|
||||||
valign: el.text.align,
|
|
||||||
}
|
|
||||||
if (el.rotate) options.rotate = el.rotate
|
|
||||||
if (el.text.defaultColor) options.color = formatColor(el.text.defaultColor).color
|
|
||||||
if (el.text.defaultFontName) options.fontFace = el.text.defaultFontName
|
|
||||||
|
|
||||||
pptxSlide.addText(textProps, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (el.type === 'line') {
|
|
||||||
const path = getLineElementPath(el)
|
|
||||||
const points = formatPoints(toPoints(path))
|
|
||||||
const { minX, maxX, minY, maxY } = getElementRange(el)
|
|
||||||
const c = formatColor(el.color)
|
|
||||||
|
|
||||||
const options: pptxgen.ShapeProps = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: (maxX - minX) / ratioPx2Inch.value,
|
|
||||||
h: (maxY - minY) / ratioPx2Inch.value,
|
|
||||||
line: {
|
|
||||||
color: c.color,
|
|
||||||
transparency: (1 - c.alpha) * 100,
|
|
||||||
width: el.width / ratioPx2Pt.value,
|
|
||||||
dashType: dashTypeMap[el.style] as 'solid' | 'dash' | 'sysDot',
|
|
||||||
beginArrowType: el.points[0] ? 'arrow' : 'none',
|
|
||||||
endArrowType: el.points[1] ? 'arrow' : 'none',
|
|
||||||
},
|
|
||||||
points,
|
|
||||||
}
|
|
||||||
if (el.shadow) options.shadow = getShadowOption(el.shadow)
|
|
||||||
|
|
||||||
pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (el.type === 'chart') {
|
|
||||||
const chartData = []
|
|
||||||
for (let i = 0; i < el.data.series.length; i++) {
|
|
||||||
const item = el.data.series[i]
|
|
||||||
chartData.push({
|
|
||||||
name: `系列${i + 1}`,
|
|
||||||
labels: el.data.labels,
|
|
||||||
values: item,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let chartColors: string[] = []
|
|
||||||
if (el.themeColors.length === 10) chartColors = el.themeColors.map(color => formatColor(color).color)
|
|
||||||
else if (el.themeColors.length === 1) chartColors = tinycolor(el.themeColors[0]).analogous(10).map(color => formatColor(color.toHexString()).color)
|
|
||||||
else {
|
|
||||||
const len = el.themeColors.length
|
|
||||||
const supplement = tinycolor(el.themeColors[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString())
|
|
||||||
chartColors = [...el.themeColors.slice(0, len - 1), ...supplement].map(color => formatColor(color).color)
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: pptxgen.IChartOpts = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
chartColors: (el.chartType === 'pie' || el.chartType === 'ring') ? chartColors : chartColors.slice(0, el.data.series.length),
|
|
||||||
}
|
|
||||||
|
|
||||||
const textColor = formatColor(el.textColor || '#000000').color
|
|
||||||
options.catAxisLabelColor = textColor
|
|
||||||
options.valAxisLabelColor = textColor
|
|
||||||
|
|
||||||
const fontSize = 14 / ratioPx2Pt.value
|
|
||||||
options.catAxisLabelFontSize = fontSize
|
|
||||||
options.valAxisLabelFontSize = fontSize
|
|
||||||
|
|
||||||
if (el.fill || el.outline) {
|
|
||||||
const plotArea: pptxgen.IChartPropsFillLine = {}
|
|
||||||
if (el.fill) {
|
|
||||||
plotArea.fill = { color: formatColor(el.fill).color }
|
|
||||||
}
|
|
||||||
if (el.outline) {
|
|
||||||
plotArea.border = {
|
|
||||||
pt: el.outline.width! / ratioPx2Pt.value,
|
|
||||||
color: formatColor(el.outline.color!).color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options.plotArea = plotArea
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((el.data.series.length > 1 && el.chartType !== 'scatter') || el.chartType === 'pie' || el.chartType === 'ring') {
|
|
||||||
options.showLegend = true
|
|
||||||
options.legendPos = 'b'
|
|
||||||
options.legendColor = textColor
|
|
||||||
options.legendFontSize = fontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
let type = pptx.ChartType.bar
|
|
||||||
if (el.chartType === 'bar') {
|
|
||||||
type = pptx.ChartType.bar
|
|
||||||
options.barDir = 'col'
|
|
||||||
if (el.options?.stack) options.barGrouping = 'stacked'
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'column') {
|
|
||||||
type = pptx.ChartType.bar
|
|
||||||
options.barDir = 'bar'
|
|
||||||
if (el.options?.stack) options.barGrouping = 'stacked'
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'line') {
|
|
||||||
type = pptx.ChartType.line
|
|
||||||
if (el.options?.lineSmooth) options.lineSmooth = true
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'area') {
|
|
||||||
type = pptx.ChartType.area
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'radar') {
|
|
||||||
type = pptx.ChartType.radar
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'scatter') {
|
|
||||||
type = pptx.ChartType.scatter
|
|
||||||
options.lineSize = 0
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'pie') {
|
|
||||||
type = pptx.ChartType.pie
|
|
||||||
}
|
|
||||||
else if (el.chartType === 'ring') {
|
|
||||||
type = pptx.ChartType.doughnut
|
|
||||||
options.holeSize = 60
|
|
||||||
}
|
|
||||||
|
|
||||||
pptxSlide.addChart(type, chartData, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (el.type === 'table') {
|
|
||||||
const hiddenCells = []
|
|
||||||
for (let i = 0; i < el.data.length; i++) {
|
|
||||||
const rowData = el.data[i]
|
|
||||||
|
|
||||||
for (let j = 0; j < rowData.length; j++) {
|
|
||||||
const cell = rowData[j]
|
|
||||||
if (cell.colspan > 1 || cell.rowspan > 1) {
|
|
||||||
for (let row = i; row < i + cell.rowspan; row++) {
|
|
||||||
for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) hiddenCells.push(`${row}_${col}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableData = []
|
|
||||||
|
|
||||||
const theme = el.theme
|
|
||||||
let themeColor: FormatColor | null = null
|
|
||||||
let subThemeColors: FormatColor[] = []
|
|
||||||
if (theme) {
|
|
||||||
themeColor = formatColor(theme.color)
|
|
||||||
subThemeColors = getTableSubThemeColor(theme.color).map(item => formatColor(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < el.data.length; i++) {
|
|
||||||
const row = el.data[i]
|
|
||||||
const _row = []
|
|
||||||
|
|
||||||
for (let j = 0; j < row.length; j++) {
|
|
||||||
const cell = row[j]
|
|
||||||
const cellOptions: pptxgen.TableCellProps = {
|
|
||||||
colspan: cell.colspan,
|
|
||||||
rowspan: cell.rowspan,
|
|
||||||
bold: cell.style?.bold || false,
|
|
||||||
italic: cell.style?.em || false,
|
|
||||||
underline: { style: cell.style?.underline ? 'sng' : 'none' },
|
|
||||||
align: cell.style?.align || 'left',
|
|
||||||
valign: 'middle',
|
|
||||||
fontFace: cell.style?.fontname || '微软雅黑',
|
|
||||||
fontSize: (cell.style?.fontsize ? parseInt(cell.style?.fontsize) : 14) / ratioPx2Pt.value,
|
|
||||||
}
|
|
||||||
if (theme && themeColor) {
|
|
||||||
let c: FormatColor
|
|
||||||
if (i % 2 === 0) c = subThemeColors[1]
|
|
||||||
else c = subThemeColors[0]
|
|
||||||
|
|
||||||
if (theme.rowHeader && i === 0) c = themeColor
|
|
||||||
else if (theme.rowFooter && i === el.data.length - 1) c = themeColor
|
|
||||||
else if (theme.colHeader && j === 0) c = themeColor
|
|
||||||
else if (theme.colFooter && j === row.length - 1) c = themeColor
|
|
||||||
|
|
||||||
cellOptions.fill = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
|
||||||
}
|
|
||||||
if (cell.style?.backcolor) {
|
|
||||||
const c = formatColor(cell.style.backcolor)
|
|
||||||
cellOptions.fill = { color: c.color, transparency: (1 - c.alpha) * 100 }
|
|
||||||
}
|
|
||||||
if (cell.style?.color) cellOptions.color = formatColor(cell.style.color).color
|
|
||||||
|
|
||||||
if (!hiddenCells.includes(`${i}_${j}`)) {
|
|
||||||
_row.push({
|
|
||||||
text: cell.text,
|
|
||||||
options: cellOptions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_row.length) tableData.push(_row)
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: pptxgen.TableProps = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
colW: el.colWidths.map(item => el.width * item / ratioPx2Inch.value),
|
|
||||||
}
|
|
||||||
if (el.theme) options.fill = { color: '#ffffff' }
|
|
||||||
if (el.outline.width && el.outline.color) {
|
|
||||||
options.border = {
|
|
||||||
type: el.outline.style === 'solid' ? 'solid' : 'dash',
|
|
||||||
pt: el.outline.width / ratioPx2Pt.value,
|
|
||||||
color: formatColor(el.outline.color).color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pptxSlide.addTable(tableData, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (el.type === 'latex') {
|
|
||||||
const svgRef = document.querySelector(`.thumbnail-list .base-element-${el.id} svg`) as HTMLElement
|
|
||||||
const base64SVG = svg2Base64(svgRef)
|
|
||||||
|
|
||||||
const options: pptxgen.ImageProps = {
|
|
||||||
data: base64SVG,
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
}
|
|
||||||
if (el.link) {
|
|
||||||
const linkOption = getLinkOption(el.link)
|
|
||||||
if (linkOption) options.hyperlink = linkOption
|
|
||||||
}
|
|
||||||
|
|
||||||
pptxSlide.addImage(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (!ignoreMedia && (el.type === 'video' || el.type === 'audio')) {
|
|
||||||
const options: pptxgen.MediaProps = {
|
|
||||||
x: el.left / ratioPx2Inch.value,
|
|
||||||
y: el.top / ratioPx2Inch.value,
|
|
||||||
w: el.width / ratioPx2Inch.value,
|
|
||||||
h: el.height / ratioPx2Inch.value,
|
|
||||||
path: el.src,
|
|
||||||
type: el.type,
|
|
||||||
}
|
|
||||||
if (el.type === 'video' && el.poster) options.cover = el.poster
|
|
||||||
|
|
||||||
const extMatch = el.src.match(/\.([a-zA-Z0-9]+)(?:[\?#]|$)/)
|
|
||||||
if (extMatch && extMatch[1]) options.extn = extMatch[1]
|
|
||||||
else if (el.ext) options.extn = el.ext
|
|
||||||
|
|
||||||
const videoExts = ['avi', 'mp4', 'm4v', 'mov', 'wmv']
|
|
||||||
const audioExts = ['mp3', 'm4a', 'mp4', 'wav', 'wma']
|
|
||||||
if (options.extn && [...videoExts, ...audioExts].includes(options.extn)) {
|
|
||||||
pptxSlide.addMedia(options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
pptx.writeFile({ fileName: `${title.value}.pptx` }).then(() => exporting.value = false).catch(() => {
|
|
||||||
exporting.value = false
|
|
||||||
message.error('导出失败')
|
|
||||||
})
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
exporting,
|
|
||||||
exportImage,
|
|
||||||
exportJSON,
|
|
||||||
exportSpecificFile,
|
|
||||||
exportPPTX,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,320 +0,0 @@
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore, useSlidesStore, useKeyboardStore } from '../store'
|
|
||||||
import { ElementOrderCommands } from '../types/edit'
|
|
||||||
import { KEYS } from '../configs/hotkey'
|
|
||||||
|
|
||||||
import useSlideHandler from './useSlideHandler'
|
|
||||||
import useLockElement from './useLockElement'
|
|
||||||
import useDeleteElement from './useDeleteElement'
|
|
||||||
import useCombineElement from './useCombineElement'
|
|
||||||
import useCopyAndPasteElement from './useCopyAndPasteElement'
|
|
||||||
import useSelectElement from './useSelectElement'
|
|
||||||
import useMoveElement from './useMoveElement'
|
|
||||||
import useOrderElement from './useOrderElement'
|
|
||||||
import useHistorySnapshot from './useHistorySnapshot'
|
|
||||||
import useScreening from './useScreening'
|
|
||||||
import useScaleCanvas from './useScaleCanvas'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const keyboardStore = useKeyboardStore()
|
|
||||||
const {
|
|
||||||
activeElementIdList,
|
|
||||||
disableHotkeys,
|
|
||||||
handleElement,
|
|
||||||
handleElementId,
|
|
||||||
editorAreaFocus,
|
|
||||||
thumbnailsFocus,
|
|
||||||
showSearchPanel,
|
|
||||||
} = storeToRefs(mainStore)
|
|
||||||
const { currentSlide } = storeToRefs(useSlidesStore())
|
|
||||||
const { ctrlKeyState, shiftKeyState, spaceKeyState } = storeToRefs(keyboardStore)
|
|
||||||
|
|
||||||
const {
|
|
||||||
updateSlideIndex,
|
|
||||||
copySlide,
|
|
||||||
createSlide,
|
|
||||||
deleteSlide,
|
|
||||||
cutSlide,
|
|
||||||
copyAndPasteSlide,
|
|
||||||
selectAllSlide,
|
|
||||||
} = useSlideHandler()
|
|
||||||
|
|
||||||
const { combineElements, uncombineElements } = useCombineElement()
|
|
||||||
const { deleteElement } = useDeleteElement()
|
|
||||||
const { lockElement } = useLockElement()
|
|
||||||
const { copyElement, cutElement, quickCopyElement } = useCopyAndPasteElement()
|
|
||||||
const { selectAllElements } = useSelectElement()
|
|
||||||
const { moveElement } = useMoveElement()
|
|
||||||
const { orderElement } = useOrderElement()
|
|
||||||
const { redo, undo } = useHistorySnapshot()
|
|
||||||
const { enterScreening, enterScreeningFromStart } = useScreening()
|
|
||||||
const { scaleCanvas, resetCanvas } = useScaleCanvas()
|
|
||||||
|
|
||||||
const copy = () => {
|
|
||||||
if (activeElementIdList.value.length) copyElement()
|
|
||||||
else if (thumbnailsFocus.value) copySlide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const cut = () => {
|
|
||||||
if (activeElementIdList.value.length) cutElement()
|
|
||||||
else if (thumbnailsFocus.value) cutSlide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const quickCopy = () => {
|
|
||||||
if (activeElementIdList.value.length) quickCopyElement()
|
|
||||||
else if (thumbnailsFocus.value) copyAndPasteSlide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAll = () => {
|
|
||||||
if (editorAreaFocus.value) selectAllElements()
|
|
||||||
if (thumbnailsFocus.value) selectAllSlide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const lock = () => {
|
|
||||||
if (!editorAreaFocus.value) return
|
|
||||||
lockElement()
|
|
||||||
}
|
|
||||||
const combine = () => {
|
|
||||||
if (!editorAreaFocus.value) return
|
|
||||||
combineElements()
|
|
||||||
}
|
|
||||||
|
|
||||||
const uncombine = () => {
|
|
||||||
if (!editorAreaFocus.value) return
|
|
||||||
uncombineElements()
|
|
||||||
}
|
|
||||||
|
|
||||||
const remove = () => {
|
|
||||||
if (activeElementIdList.value.length) deleteElement()
|
|
||||||
else if (thumbnailsFocus.value) deleteSlide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = (key: string) => {
|
|
||||||
if (activeElementIdList.value.length) moveElement(key)
|
|
||||||
else if (key === KEYS.UP || key === KEYS.DOWN) updateSlideIndex(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveSlide = (key: string) => {
|
|
||||||
if (key === KEYS.PAGEUP) updateSlideIndex(KEYS.UP)
|
|
||||||
else if (key === KEYS.PAGEDOWN) updateSlideIndex(KEYS.DOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
const order = (command: ElementOrderCommands) => {
|
|
||||||
if (!handleElement.value) return
|
|
||||||
orderElement(handleElement.value, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
const create = () => {
|
|
||||||
if (!thumbnailsFocus.value) return
|
|
||||||
createSlide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabActiveElement = () => {
|
|
||||||
if (!currentSlide.value.elements.length) return
|
|
||||||
if (!handleElementId.value) {
|
|
||||||
const firstElement = currentSlide.value.elements[0]
|
|
||||||
mainStore.setActiveElementIdList([firstElement.id])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const currentIndex = currentSlide.value.elements.findIndex(el => el.id === handleElementId.value)
|
|
||||||
const nextIndex = currentIndex >= currentSlide.value.elements.length - 1 ? 0 : currentIndex + 1
|
|
||||||
const nextElementId = currentSlide.value.elements[nextIndex].id
|
|
||||||
|
|
||||||
mainStore.setActiveElementIdList([nextElementId])
|
|
||||||
}
|
|
||||||
|
|
||||||
const keydownListener = (e: KeyboardEvent) => {
|
|
||||||
const { ctrlKey, shiftKey, altKey, metaKey } = e
|
|
||||||
const ctrlOrMetaKeyActive = ctrlKey || metaKey
|
|
||||||
|
|
||||||
const key = e.key.toUpperCase()
|
|
||||||
|
|
||||||
if (ctrlOrMetaKeyActive && !ctrlKeyState.value) keyboardStore.setCtrlKeyState(true)
|
|
||||||
if (shiftKey && !shiftKeyState.value) keyboardStore.setShiftKeyState(true)
|
|
||||||
if (!disableHotkeys.value && key === KEYS.SPACE) keyboardStore.setSpaceKeyState(true)
|
|
||||||
|
|
||||||
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.P) {
|
|
||||||
e.preventDefault()
|
|
||||||
mainStore.setDialogForExport('pdf')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (shiftKey && key === KEYS.F5) {
|
|
||||||
e.preventDefault()
|
|
||||||
enterScreening()
|
|
||||||
keyboardStore.setShiftKeyState(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (key === KEYS.F5) {
|
|
||||||
e.preventDefault()
|
|
||||||
enterScreeningFromStart()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (ctrlKey && key === KEYS.F) {
|
|
||||||
e.preventDefault()
|
|
||||||
mainStore.setSearchPanelState(!showSearchPanel.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editorAreaFocus.value && !thumbnailsFocus.value) return
|
|
||||||
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.C) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
copy()
|
|
||||||
}
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.X) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
cut()
|
|
||||||
}
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.D) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
quickCopy()
|
|
||||||
}
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.Z) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
undo()
|
|
||||||
}
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.Y) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
redo()
|
|
||||||
}
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.A) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
selectAll()
|
|
||||||
}
|
|
||||||
if (ctrlOrMetaKeyActive && key === KEYS.L) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
lock()
|
|
||||||
}
|
|
||||||
if (!shiftKey && ctrlOrMetaKeyActive && key === KEYS.G) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
combine()
|
|
||||||
}
|
|
||||||
if (shiftKey && ctrlOrMetaKeyActive && key === KEYS.G) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
uncombine()
|
|
||||||
}
|
|
||||||
if (altKey && key === KEYS.F) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
order(ElementOrderCommands.TOP)
|
|
||||||
}
|
|
||||||
if (altKey && key === KEYS.B) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
order(ElementOrderCommands.BOTTOM)
|
|
||||||
}
|
|
||||||
if (key === KEYS.DELETE || key === KEYS.BACKSPACE) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
remove()
|
|
||||||
}
|
|
||||||
if (key === KEYS.UP) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
move(KEYS.UP)
|
|
||||||
}
|
|
||||||
if (key === KEYS.DOWN) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
move(KEYS.DOWN)
|
|
||||||
}
|
|
||||||
if (key === KEYS.LEFT) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
move(KEYS.LEFT)
|
|
||||||
}
|
|
||||||
if (key === KEYS.RIGHT) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
move(KEYS.RIGHT)
|
|
||||||
}
|
|
||||||
if (key === KEYS.PAGEUP) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
moveSlide(KEYS.PAGEUP)
|
|
||||||
}
|
|
||||||
if (key === KEYS.PAGEDOWN) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
moveSlide(KEYS.PAGEDOWN)
|
|
||||||
}
|
|
||||||
if (key === KEYS.ENTER) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
create()
|
|
||||||
}
|
|
||||||
if (key === KEYS.MINUS) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
scaleCanvas('-')
|
|
||||||
}
|
|
||||||
if (key === KEYS.EQUAL) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
scaleCanvas('+')
|
|
||||||
}
|
|
||||||
if (key === KEYS.DIGIT_0) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
resetCanvas()
|
|
||||||
}
|
|
||||||
if (key === KEYS.TAB) {
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
e.preventDefault()
|
|
||||||
tabActiveElement()
|
|
||||||
}
|
|
||||||
if (editorAreaFocus.value && !shiftKey && !ctrlOrMetaKeyActive && !disableHotkeys.value) {
|
|
||||||
if (key === KEYS.T) {
|
|
||||||
mainStore.setCreatingElement({ type: 'text' })
|
|
||||||
}
|
|
||||||
else if (key === KEYS.R) {
|
|
||||||
mainStore.setCreatingElement({ type: 'shape', data: {
|
|
||||||
viewBox: [200, 200],
|
|
||||||
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
else if (key === KEYS.O) {
|
|
||||||
mainStore.setCreatingElement({ type: 'shape', data: {
|
|
||||||
viewBox: [200, 200],
|
|
||||||
path: 'M 100 0 A 50 50 0 1 1 100 200 A 50 50 0 1 1 100 0 Z',
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
else if (key === KEYS.L) {
|
|
||||||
mainStore.setCreatingElement({ type: 'line', data: {
|
|
||||||
path: 'M 0 0 L 20 20',
|
|
||||||
style: 'solid',
|
|
||||||
points: ['', ''],
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyupListener = () => {
|
|
||||||
if (ctrlKeyState.value) keyboardStore.setCtrlKeyState(false)
|
|
||||||
if (shiftKeyState.value) keyboardStore.setShiftKeyState(false)
|
|
||||||
if (spaceKeyState.value) keyboardStore.setSpaceKeyState(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('keydown', keydownListener)
|
|
||||||
document.addEventListener('keyup', keyupListener)
|
|
||||||
window.addEventListener('blur', keyupListener)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', keydownListener)
|
|
||||||
document.removeEventListener('keyup', keyupListener)
|
|
||||||
window.removeEventListener('blur', keyupListener)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useSlidesStore, useMainStore } from '../store'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
const { activeElementIdList, hiddenElementIdList } = storeToRefs(mainStore)
|
|
||||||
|
|
||||||
const toggleHideElement = (id: string) => {
|
|
||||||
if (hiddenElementIdList.value.includes(id)) {
|
|
||||||
mainStore.setHiddenElementIdList(hiddenElementIdList.value.filter(item => item !== id))
|
|
||||||
}
|
|
||||||
else mainStore.setHiddenElementIdList([...hiddenElementIdList.value, id])
|
|
||||||
|
|
||||||
if (activeElementIdList.value.includes(id)) mainStore.setActiveElementIdList([])
|
|
||||||
}
|
|
||||||
|
|
||||||
const showAllElements = () => {
|
|
||||||
const currentSlideElIdList = currentSlide.value.elements.map(item => item.id)
|
|
||||||
const needHiddenElementIdList = hiddenElementIdList.value.filter(item => !currentSlideElIdList.includes(item))
|
|
||||||
mainStore.setHiddenElementIdList(needHiddenElementIdList)
|
|
||||||
}
|
|
||||||
const hideAllElements = () => {
|
|
||||||
const currentSlideElIdList = currentSlide.value.elements.map(item => item.id)
|
|
||||||
mainStore.setHiddenElementIdList([...hiddenElementIdList.value, ...currentSlideElIdList])
|
|
||||||
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
toggleHideElement,
|
|
||||||
showAllElements,
|
|
||||||
hideAllElements,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { debounce, throttle} from 'lodash'
|
|
||||||
import { useSnapshotStore } from '../store'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const snapshotStore = useSnapshotStore()
|
|
||||||
|
|
||||||
// 添加历史快照(历史记录)
|
|
||||||
const addHistorySnapshot = debounce(function() {
|
|
||||||
snapshotStore.addSnapshot()
|
|
||||||
}, 300, { trailing: true })
|
|
||||||
|
|
||||||
// 重做
|
|
||||||
const redo = throttle(function() {
|
|
||||||
snapshotStore.reDo()
|
|
||||||
}, 100, { leading: true, trailing: false })
|
|
||||||
|
|
||||||
// 撤销
|
|
||||||
const undo = throttle(function() {
|
|
||||||
snapshotStore.unDo()
|
|
||||||
}, 100, { leading: true, trailing: false })
|
|
||||||
|
|
||||||
return {
|
|
||||||
addHistorySnapshot,
|
|
||||||
redo,
|
|
||||||
undo,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,494 +0,0 @@
|
||||||
import { ref } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { parse, type Shape, type Element, type ChartItem } from 'pptxtojson'
|
|
||||||
import { nanoid } from 'nanoid'
|
|
||||||
import { useSlidesStore } from '../store'
|
|
||||||
import { decrypt } from '../utils/crypto'
|
|
||||||
import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '../configs/shapes'
|
|
||||||
import useAddSlidesOrElements from '../hooks/useAddSlidesOrElements'
|
|
||||||
import useSlideHandler from '../hooks/useSlideHandler'
|
|
||||||
import message from '../utils/message'
|
|
||||||
import { getSvgPathRange } from '../utils/svgPathParser'
|
|
||||||
import type {
|
|
||||||
Slide,
|
|
||||||
TableCellStyle,
|
|
||||||
TableCell,
|
|
||||||
ChartType,
|
|
||||||
SlideBackground,
|
|
||||||
PPTShapeElement,
|
|
||||||
PPTLineElement,
|
|
||||||
ShapeTextAlign,
|
|
||||||
PPTTextElement,
|
|
||||||
ChartOptions,
|
|
||||||
} from '../types/slides'
|
|
||||||
|
|
||||||
const convertFontSizePtToPx = (html: string, ratio: number) => {
|
|
||||||
return html.replace(/font-size:\s*([\d.]+)pt/g, (match, p1) => {
|
|
||||||
return `font-size: ${(parseFloat(p1) * ratio).toFixed(1)}px`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { theme } = storeToRefs(useSlidesStore())
|
|
||||||
|
|
||||||
const { addSlidesFromData } = useAddSlidesOrElements()
|
|
||||||
const { isEmptySlide } = useSlideHandler()
|
|
||||||
|
|
||||||
const exporting = ref(false)
|
|
||||||
|
|
||||||
// 导入pptist文件
|
|
||||||
const importSpecificFile = (files: FileList, cover = false) => {
|
|
||||||
const file = files[0]
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.addEventListener('load', () => {
|
|
||||||
try {
|
|
||||||
const slides = JSON.parse(decrypt(reader.result as string))
|
|
||||||
if (cover) {
|
|
||||||
slidesStore.updateSlideIndex(0)
|
|
||||||
slidesStore.setSlides(slides)
|
|
||||||
}
|
|
||||||
else if (isEmptySlide.value) slidesStore.setSlides(slides)
|
|
||||||
else addSlidesFromData(slides)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
message.error('无法正确读取 / 解析该文件')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
reader.readAsText(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseLineElement = (el: Shape) => {
|
|
||||||
let start: [number, number] = [0, 0]
|
|
||||||
let end: [number, number] = [0, 0]
|
|
||||||
|
|
||||||
if (!el.isFlipV && !el.isFlipH) { // 右下
|
|
||||||
start = [0, 0]
|
|
||||||
end = [el.width, el.height]
|
|
||||||
}
|
|
||||||
else if (el.isFlipV && el.isFlipH) { // 左上
|
|
||||||
start = [el.width, el.height]
|
|
||||||
end = [0, 0]
|
|
||||||
}
|
|
||||||
else if (el.isFlipV && !el.isFlipH) { // 右上
|
|
||||||
start = [0, el.height]
|
|
||||||
end = [el.width, 0]
|
|
||||||
}
|
|
||||||
else { // 左下
|
|
||||||
start = [el.width, 0]
|
|
||||||
end = [0, el.height]
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: PPTLineElement = {
|
|
||||||
type: 'line',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: el.borderWidth || 1,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
style: el.borderType,
|
|
||||||
color: el.borderColor,
|
|
||||||
points: ['', /straightConnector/.test(el.shapType) ? 'arrow' : '']
|
|
||||||
}
|
|
||||||
if (/bentConnector/.test(el.shapType)) {
|
|
||||||
data.broken2 = [
|
|
||||||
Math.abs(start[0] - end[0]) / 2,
|
|
||||||
Math.abs(start[1] - end[1]) / 2,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入PPTX文件
|
|
||||||
const importPPTXFile = (files: FileList) => {
|
|
||||||
const file = files[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
exporting.value = true
|
|
||||||
|
|
||||||
const shapeList: ShapePoolItem[] = []
|
|
||||||
for (const item of SHAPE_LIST) {
|
|
||||||
shapeList.push(...item.children)
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = async e => {
|
|
||||||
const json = await parse(e.target!.result as ArrayBuffer)
|
|
||||||
|
|
||||||
const ratio = 96 / 72
|
|
||||||
const width = json.size.width
|
|
||||||
|
|
||||||
slidesStore.setViewportSize(width * ratio)
|
|
||||||
|
|
||||||
const slides: Slide[] = []
|
|
||||||
for (const item of json.slides) {
|
|
||||||
const { type, value } = item.fill
|
|
||||||
let background: SlideBackground
|
|
||||||
if (type === 'image') {
|
|
||||||
background = {
|
|
||||||
type: 'image',
|
|
||||||
image: {
|
|
||||||
src: value.picBase64,
|
|
||||||
size: 'cover',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (type === 'gradient') {
|
|
||||||
background = {
|
|
||||||
type: 'gradient',
|
|
||||||
gradient: {
|
|
||||||
type: 'linear',
|
|
||||||
colors: value.colors.map(item => ({
|
|
||||||
...item,
|
|
||||||
pos: parseInt(item.pos),
|
|
||||||
})),
|
|
||||||
rotate: value.rot,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
background = {
|
|
||||||
type: 'solid',
|
|
||||||
color: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const slide: Slide = {
|
|
||||||
id: nanoid(10),
|
|
||||||
elements: [],
|
|
||||||
background,
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseElements = (elements: Element[]) => {
|
|
||||||
for (const el of elements) {
|
|
||||||
const originWidth = el.width || 1
|
|
||||||
const originHeight = el.height || 1
|
|
||||||
const originLeft = el.left
|
|
||||||
const originTop = el.top
|
|
||||||
|
|
||||||
el.width = el.width * ratio
|
|
||||||
el.height = el.height * ratio
|
|
||||||
el.left = el.left * ratio
|
|
||||||
el.top = el.top * ratio
|
|
||||||
|
|
||||||
if (el.type === 'text') {
|
|
||||||
const textEl: PPTTextElement = {
|
|
||||||
type: 'text',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
rotate: el.rotate,
|
|
||||||
defaultFontName: theme.value.fontName,
|
|
||||||
defaultColor: theme.value.fontColor,
|
|
||||||
content: convertFontSizePtToPx(el.content, ratio),
|
|
||||||
lineHeight: 1,
|
|
||||||
outline: {
|
|
||||||
color: el.borderColor,
|
|
||||||
width: el.borderWidth,
|
|
||||||
style: el.borderType,
|
|
||||||
},
|
|
||||||
fill: el.fillColor,
|
|
||||||
vertical: el.isVertical,
|
|
||||||
}
|
|
||||||
if (el.shadow) {
|
|
||||||
textEl.shadow = {
|
|
||||||
h: el.shadow.h * ratio,
|
|
||||||
v: el.shadow.v * ratio,
|
|
||||||
blur: el.shadow.blur * ratio,
|
|
||||||
color: el.shadow.color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slide.elements.push(textEl)
|
|
||||||
}
|
|
||||||
else if (el.type === 'image') {
|
|
||||||
slide.elements.push({
|
|
||||||
type: 'image',
|
|
||||||
id: nanoid(10),
|
|
||||||
src: el.src,
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
fixedRatio: true,
|
|
||||||
rotate: el.rotate,
|
|
||||||
flipH: el.isFlipH,
|
|
||||||
flipV: el.isFlipV,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (el.type === 'audio') {
|
|
||||||
slide.elements.push({
|
|
||||||
type: 'audio',
|
|
||||||
id: nanoid(10),
|
|
||||||
src: el.blob,
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
rotate: 0,
|
|
||||||
fixedRatio: false,
|
|
||||||
color: theme.value.themeColor,
|
|
||||||
loop: false,
|
|
||||||
autoplay: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (el.type === 'video') {
|
|
||||||
slide.elements.push({
|
|
||||||
type: 'video',
|
|
||||||
id: nanoid(10),
|
|
||||||
src: (el.blob || el.src)!,
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
rotate: 0,
|
|
||||||
autoplay: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (el.type === 'shape') {
|
|
||||||
if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
|
|
||||||
const lineElement = parseLineElement(el)
|
|
||||||
slide.elements.push(lineElement)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
|
|
||||||
|
|
||||||
const vAlignMap: { [key: string]: ShapeTextAlign } = {
|
|
||||||
'mid': 'middle',
|
|
||||||
'down': 'bottom',
|
|
||||||
'up': 'top',
|
|
||||||
}
|
|
||||||
|
|
||||||
const element: PPTShapeElement = {
|
|
||||||
type: 'shape',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
viewBox: [200, 200],
|
|
||||||
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
|
|
||||||
fill: el.fillColor || 'none',
|
|
||||||
fixedRatio: false,
|
|
||||||
rotate: el.rotate,
|
|
||||||
outline: {
|
|
||||||
color: el.borderColor,
|
|
||||||
width: el.borderWidth,
|
|
||||||
style: el.borderType,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
content: convertFontSizePtToPx(el.content, ratio),
|
|
||||||
defaultFontName: theme.value.fontName,
|
|
||||||
defaultColor: theme.value.fontColor,
|
|
||||||
align: vAlignMap[el.vAlign] || 'middle',
|
|
||||||
},
|
|
||||||
flipH: el.isFlipH,
|
|
||||||
flipV: el.isFlipV,
|
|
||||||
}
|
|
||||||
if (el.shadow) {
|
|
||||||
element.shadow = {
|
|
||||||
h: el.shadow.h * ratio,
|
|
||||||
v: el.shadow.v * ratio,
|
|
||||||
blur: el.shadow.blur * ratio,
|
|
||||||
color: el.shadow.color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shape) {
|
|
||||||
element.path = shape.path
|
|
||||||
element.viewBox = shape.viewBox
|
|
||||||
|
|
||||||
if (shape.pathFormula) {
|
|
||||||
element.pathFormula = shape.pathFormula
|
|
||||||
element.viewBox = [el.width, el.height]
|
|
||||||
|
|
||||||
const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
|
|
||||||
if ('editable' in pathFormula && pathFormula.editable) {
|
|
||||||
element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
|
|
||||||
element.keypoints = pathFormula.defaultValue
|
|
||||||
}
|
|
||||||
else element.path = pathFormula.formula(el.width, el.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (el.shapType === 'custom') {
|
|
||||||
if (el.path!.indexOf('NaN') !== -1) element.path = ''
|
|
||||||
else {
|
|
||||||
element.special = true
|
|
||||||
element.path = el.path!
|
|
||||||
|
|
||||||
const { maxX, maxY } = getSvgPathRange(element.path)
|
|
||||||
element.viewBox = [maxX || originWidth, maxY || originHeight]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.path) slide.elements.push(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (el.type === 'table') {
|
|
||||||
const row = el.data.length
|
|
||||||
const col = el.data[0].length
|
|
||||||
|
|
||||||
const style: TableCellStyle = {
|
|
||||||
fontname: theme.value.fontName,
|
|
||||||
color: theme.value.fontColor,
|
|
||||||
}
|
|
||||||
const data: TableCell[][] = []
|
|
||||||
for (let i = 0; i < row; i++) {
|
|
||||||
const rowCells: TableCell[] = []
|
|
||||||
for (let j = 0; j < col; j++) {
|
|
||||||
const cellData = el.data[i][j]
|
|
||||||
|
|
||||||
let textDiv: HTMLDivElement | null = document.createElement('div')
|
|
||||||
textDiv.innerHTML = cellData.text
|
|
||||||
const p = textDiv.querySelector('p')
|
|
||||||
const align = p?.style.textAlign || 'left'
|
|
||||||
|
|
||||||
const span = textDiv.querySelector('span')
|
|
||||||
const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
|
|
||||||
const fontname = span?.style.fontFamily || ''
|
|
||||||
const color = span?.style.color || cellData.fontColor
|
|
||||||
|
|
||||||
rowCells.push({
|
|
||||||
id: nanoid(10),
|
|
||||||
colspan: cellData.colSpan || 1,
|
|
||||||
rowspan: cellData.rowSpan || 1,
|
|
||||||
text: textDiv.innerText,
|
|
||||||
style: {
|
|
||||||
...style,
|
|
||||||
align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
|
|
||||||
fontsize,
|
|
||||||
fontname,
|
|
||||||
color,
|
|
||||||
bold: cellData.fontBold,
|
|
||||||
backcolor: cellData.fillColor,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
textDiv = null
|
|
||||||
}
|
|
||||||
data.push(rowCells)
|
|
||||||
}
|
|
||||||
|
|
||||||
const colWidths: number[] = new Array(col).fill(1 / col)
|
|
||||||
|
|
||||||
slide.elements.push({
|
|
||||||
type: 'table',
|
|
||||||
id: nanoid(10),
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
colWidths,
|
|
||||||
rotate: 0,
|
|
||||||
data,
|
|
||||||
outline: {
|
|
||||||
width: el.borderWidth || 2,
|
|
||||||
style: el.borderType,
|
|
||||||
color: el.borderColor || '#eeece1',
|
|
||||||
},
|
|
||||||
cellMinHeight: 36,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (el.type === 'chart') {
|
|
||||||
let labels: string[]
|
|
||||||
let legends: string[]
|
|
||||||
let series: number[][]
|
|
||||||
|
|
||||||
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
|
||||||
labels = el.data[0].map((item, index) => `坐标${index + 1}`)
|
|
||||||
legends = ['X', 'Y']
|
|
||||||
series = el.data
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const data = el.data as ChartItem[]
|
|
||||||
labels = Object.values(data[0].xlabels)
|
|
||||||
legends = data.map(item => item.key)
|
|
||||||
series = data.map(item => item.values.map(v => v.y))
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: ChartOptions = {}
|
|
||||||
|
|
||||||
let chartType: ChartType = 'bar'
|
|
||||||
|
|
||||||
switch (el.chartType) {
|
|
||||||
case 'barChart':
|
|
||||||
case 'bar3DChart':
|
|
||||||
chartType = 'bar'
|
|
||||||
if (el.barDir === 'bar') chartType = 'column'
|
|
||||||
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
|
|
||||||
break
|
|
||||||
case 'lineChart':
|
|
||||||
case 'line3DChart':
|
|
||||||
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
|
|
||||||
chartType = 'line'
|
|
||||||
break
|
|
||||||
case 'areaChart':
|
|
||||||
case 'area3DChart':
|
|
||||||
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
|
|
||||||
chartType = 'area'
|
|
||||||
break
|
|
||||||
case 'scatterChart':
|
|
||||||
case 'bubbleChart':
|
|
||||||
chartType = 'scatter'
|
|
||||||
break
|
|
||||||
case 'pieChart':
|
|
||||||
case 'pie3DChart':
|
|
||||||
chartType = 'pie'
|
|
||||||
break
|
|
||||||
case 'radarChart':
|
|
||||||
chartType = 'radar'
|
|
||||||
break
|
|
||||||
case 'doughnutChart':
|
|
||||||
chartType = 'ring'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
slide.elements.push({
|
|
||||||
type: 'chart',
|
|
||||||
id: nanoid(10),
|
|
||||||
chartType: chartType,
|
|
||||||
width: el.width,
|
|
||||||
height: el.height,
|
|
||||||
left: el.left,
|
|
||||||
top: el.top,
|
|
||||||
rotate: 0,
|
|
||||||
themeColors: [theme.value.themeColor],
|
|
||||||
textColor: theme.value.fontColor,
|
|
||||||
data: {
|
|
||||||
labels,
|
|
||||||
legends,
|
|
||||||
series,
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (el.type === 'group' || el.type === 'diagram') {
|
|
||||||
const elements = el.elements.map(_el => ({
|
|
||||||
..._el,
|
|
||||||
left: _el.left + originLeft,
|
|
||||||
top: _el.top + originTop,
|
|
||||||
}))
|
|
||||||
parseElements(elements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parseElements(item.elements)
|
|
||||||
slides.push(slide)
|
|
||||||
}
|
|
||||||
slidesStore.updateSlideIndex(0)
|
|
||||||
slidesStore.setSlides(slides)
|
|
||||||
exporting.value = false
|
|
||||||
}
|
|
||||||
reader.readAsArrayBuffer(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
importSpecificFile,
|
|
||||||
importPPTXFile,
|
|
||||||
exporting,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement, PPTElementLink } from '../types/slides'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
import message from '../utils/message'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
const setLink = (handleElement: PPTElement, link: PPTElementLink) => {
|
|
||||||
const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
|
|
||||||
if (link.type === 'web' && !linkRegExp.test(link.target)) {
|
|
||||||
message.error('不是正确的网页链接地址')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (link.type === 'slide' && !link.target) {
|
|
||||||
message.error('请先选择链接目标')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const props = { link }
|
|
||||||
slidesStore.updateElement({ id: handleElement.id, props })
|
|
||||||
addHistorySnapshot()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeLink = (handleElement: PPTElement) => {
|
|
||||||
slidesStore.removeElementProps({ id: handleElement.id, propName: 'link' })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
setLink,
|
|
||||||
removeLink,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useSlidesStore } from '../store'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { slides } = storeToRefs(useSlidesStore())
|
|
||||||
|
|
||||||
const timer = ref<number | null>(null)
|
|
||||||
const slidesLoadLimit = ref(50)
|
|
||||||
|
|
||||||
const loadSlide = () => {
|
|
||||||
if (slides.value.length > slidesLoadLimit.value) {
|
|
||||||
timer.value = setTimeout(() => {
|
|
||||||
slidesLoadLimit.value = slidesLoadLimit.value + 20
|
|
||||||
loadSlide()
|
|
||||||
}, 600)
|
|
||||||
}
|
|
||||||
else slidesLoadLimit.value = 9999
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadSlide)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (timer.value) clearTimeout(timer.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
slidesLoadLimit,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { activeElementIdList } = storeToRefs(mainStore)
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
// 锁定选中的元素,并清空选中元素状态
|
|
||||||
const lockElement = () => {
|
|
||||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
|
|
||||||
|
|
||||||
for (const element of newElementList) {
|
|
||||||
if (activeElementIdList.value.includes(element.id)) element.lock = true
|
|
||||||
}
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
mainStore.setActiveElementIdList([])
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解除元素的锁定状态,并将其设置为当前选择元素
|
|
||||||
* @param handleElement 需要解锁的元素
|
|
||||||
*/
|
|
||||||
const unlockElement = (handleElement: PPTElement) => {
|
|
||||||
const newElementList: PPTElement[] = JSON.parse(JSON.stringify(currentSlide.value.elements))
|
|
||||||
|
|
||||||
if (handleElement.groupId) {
|
|
||||||
const groupElementIdList = []
|
|
||||||
for (const element of newElementList) {
|
|
||||||
if (element.groupId === handleElement.groupId) {
|
|
||||||
element.lock = false
|
|
||||||
groupElementIdList.push(element.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
mainStore.setActiveElementIdList(groupElementIdList)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (const element of newElementList) {
|
|
||||||
if (element.id === handleElement.id) {
|
|
||||||
element.lock = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
mainStore.setActiveElementIdList([handleElement.id])
|
|
||||||
}
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lockElement,
|
|
||||||
unlockElement,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore, useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import { KEYS } from '../configs/hotkey'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { activeElementIdList, activeGroupElementId } = storeToRefs(useMainStore())
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将元素向指定方向移动指定的距离
|
|
||||||
* 组合元素成员中,存在被选中可独立操作的元素时,优先移动该元素。否则默认移动所有被选中的元素
|
|
||||||
* @param command 移动方向
|
|
||||||
* @param step 移动距离
|
|
||||||
*/
|
|
||||||
const moveElement = (command: string, step = 1) => {
|
|
||||||
let newElementList: PPTElement[] = []
|
|
||||||
|
|
||||||
const move = (el: PPTElement) => {
|
|
||||||
let { left, top } = el
|
|
||||||
switch (command) {
|
|
||||||
case KEYS.LEFT:
|
|
||||||
left = left - step
|
|
||||||
break
|
|
||||||
case KEYS.RIGHT:
|
|
||||||
left = left + step
|
|
||||||
break
|
|
||||||
case KEYS.UP:
|
|
||||||
top = top - step
|
|
||||||
break
|
|
||||||
case KEYS.DOWN:
|
|
||||||
top = top + step
|
|
||||||
break
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
return { ...el, left, top }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeGroupElementId.value) {
|
|
||||||
newElementList = currentSlide.value.elements.map(el => {
|
|
||||||
return activeGroupElementId.value === el.id ? move(el) : el
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newElementList = currentSlide.value.elements.map(el => {
|
|
||||||
return activeElementIdList.value.includes(el.id) ? move(el) : el
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
moveElement,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useSlidesStore } from '../store'
|
|
||||||
import type { PPTElement } from '../types/slides'
|
|
||||||
import { ElementOrderCommands } from '../types/edit'
|
|
||||||
import useHistorySnapshot from '../hooks/useHistorySnapshot'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const slidesStore = useSlidesStore()
|
|
||||||
const { currentSlide } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取组合元素层级范围
|
|
||||||
* @param elementList 本页所有元素列表
|
|
||||||
* @param combineElementList 组合元素列表
|
|
||||||
*/
|
|
||||||
const getCombineElementLevelRange = (elementList: PPTElement[], combineElementList: PPTElement[]) => {
|
|
||||||
return {
|
|
||||||
minLevel: elementList.findIndex(_element => _element.id === combineElementList[0].id),
|
|
||||||
maxLevel: elementList.findIndex(_element => _element.id === combineElementList[combineElementList.length - 1].id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上移一层
|
|
||||||
* @param elementList 本页所有元素列表
|
|
||||||
* @param element 当前操作的元素
|
|
||||||
*/
|
|
||||||
const moveUpElement = (elementList: PPTElement[], element: PPTElement) => {
|
|
||||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
|
||||||
|
|
||||||
// 如果被操作的元素是组合元素成员,需要将该组合全部成员一起进行移动
|
|
||||||
if (element.groupId) {
|
|
||||||
|
|
||||||
// 获取到该组合全部成员,以及所有成员的层级范围
|
|
||||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
|
||||||
const { minLevel, maxLevel } = getCombineElementLevelRange(elementList, combineElementList)
|
|
||||||
|
|
||||||
// 已经处在顶层,无法继续移动
|
|
||||||
if (maxLevel === elementList.length - 1) return
|
|
||||||
|
|
||||||
// 通过组合成员范围的最大值,获取到该组合上一层的元素,然后将该组合元素从元素列表中移除(并缓存被移除的元素列表)
|
|
||||||
// 若上层元素处在另一个组合中,则将上述被移除的组合元素插入到该上层组合上方
|
|
||||||
// 若上层元素不处于任何分组中,则将上述被移除的组合元素插入到该上层元素上方
|
|
||||||
const nextElement = copyOfElementList[maxLevel + 1]
|
|
||||||
const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length)
|
|
||||||
|
|
||||||
if (nextElement.groupId) {
|
|
||||||
const nextCombineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
|
|
||||||
copyOfElementList.splice(minLevel + nextCombineElementList.length, 0, ...movedElementList)
|
|
||||||
}
|
|
||||||
else copyOfElementList.splice(minLevel + 1, 0, ...movedElementList)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果被操作的元素不是组合元素成员
|
|
||||||
else {
|
|
||||||
|
|
||||||
// 获取该元素在列表中的层级
|
|
||||||
const level = elementList.findIndex(item => item.id === element.id)
|
|
||||||
|
|
||||||
// 已经处在顶层,无法继续移动
|
|
||||||
if (level === elementList.length - 1) return
|
|
||||||
|
|
||||||
// 获取到该组合上一层的元素,然后将该组合元素从元素列表中移除(并缓存被移除的元素列表)
|
|
||||||
const nextElement = copyOfElementList[level + 1]
|
|
||||||
const movedElement = copyOfElementList.splice(level, 1)[0]
|
|
||||||
|
|
||||||
// 通过组合成员范围的最大值,获取到该组合上一层的元素,然后将该组合元素从元素列表中移除(并缓存被移除的元素列表)
|
|
||||||
// 若上层元素处在另一个组合中,则将上述被移除的组合元素插入到该上层组合上方
|
|
||||||
// 若上层元素不处于任何分组中,则将上述被移除的组合元素插入到该上层元素上方
|
|
||||||
if (nextElement.groupId) {
|
|
||||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === nextElement.groupId)
|
|
||||||
copyOfElementList.splice(level + combineElementList.length, 0, movedElement)
|
|
||||||
}
|
|
||||||
else copyOfElementList.splice(level + 1, 0, movedElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyOfElementList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下移一层,操作方式同上移
|
|
||||||
* @param elementList 本页所有元素列表
|
|
||||||
* @param element 当前操作的元素
|
|
||||||
*/
|
|
||||||
const moveDownElement = (elementList: PPTElement[], element: PPTElement) => {
|
|
||||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
|
||||||
|
|
||||||
if (element.groupId) {
|
|
||||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
|
||||||
const { minLevel } = getCombineElementLevelRange(elementList, combineElementList)
|
|
||||||
if (minLevel === 0) return
|
|
||||||
|
|
||||||
const prevElement = copyOfElementList[minLevel - 1]
|
|
||||||
const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length)
|
|
||||||
|
|
||||||
if (prevElement.groupId) {
|
|
||||||
const prevCombineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
|
|
||||||
copyOfElementList.splice(minLevel - prevCombineElementList.length, 0, ...movedElementList)
|
|
||||||
}
|
|
||||||
else copyOfElementList.splice(minLevel - 1, 0, ...movedElementList)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const level = elementList.findIndex(item => item.id === element.id)
|
|
||||||
if (level === 0) return
|
|
||||||
|
|
||||||
const prevElement = copyOfElementList[level - 1]
|
|
||||||
const movedElement = copyOfElementList.splice(level, 1)[0]
|
|
||||||
|
|
||||||
if (prevElement.groupId) {
|
|
||||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === prevElement.groupId)
|
|
||||||
copyOfElementList.splice(level - combineElementList.length, 0, movedElement)
|
|
||||||
}
|
|
||||||
else copyOfElementList.splice(level - 1, 0, movedElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyOfElementList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 置顶层
|
|
||||||
* @param elementList 本页所有元素列表
|
|
||||||
* @param element 当前操作的元素
|
|
||||||
*/
|
|
||||||
const moveTopElement = (elementList: PPTElement[], element: PPTElement) => {
|
|
||||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
|
||||||
|
|
||||||
// 如果被操作的元素是组合元素成员,需要将该组合全部成员一起进行移动
|
|
||||||
if (element.groupId) {
|
|
||||||
|
|
||||||
// 获取到该组合全部成员,以及所有成员的层级范围
|
|
||||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
|
||||||
const { minLevel, maxLevel } = getCombineElementLevelRange(elementList, combineElementList)
|
|
||||||
|
|
||||||
// 已经处在顶层,无法继续移动
|
|
||||||
if (maxLevel === elementList.length - 1) return null
|
|
||||||
|
|
||||||
// 将该组合元素从元素列表中移除,然后将被移除的元素添加到元素列表顶部
|
|
||||||
const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length)
|
|
||||||
copyOfElementList.push(...movedElementList)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果被操作的元素不是组合元素成员
|
|
||||||
else {
|
|
||||||
|
|
||||||
// 获取该元素在列表中的层级
|
|
||||||
const level = elementList.findIndex(item => item.id === element.id)
|
|
||||||
|
|
||||||
// 已经处在顶层,无法继续移动
|
|
||||||
if (level === elementList.length - 1) return null
|
|
||||||
|
|
||||||
// 将该组合元素从元素列表中移除,然后将被移除的元素添加到元素列表底部
|
|
||||||
copyOfElementList.splice(level, 1)
|
|
||||||
copyOfElementList.push(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyOfElementList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 置底层,操作方式同置顶
|
|
||||||
* @param elementList 本页所有元素列表
|
|
||||||
* @param element 当前操作的元素
|
|
||||||
*/
|
|
||||||
const moveBottomElement = (elementList: PPTElement[], element: PPTElement) => {
|
|
||||||
const copyOfElementList: PPTElement[] = JSON.parse(JSON.stringify(elementList))
|
|
||||||
|
|
||||||
if (element.groupId) {
|
|
||||||
const combineElementList = copyOfElementList.filter(_element => _element.groupId === element.groupId)
|
|
||||||
const { minLevel } = getCombineElementLevelRange(elementList, combineElementList)
|
|
||||||
if (minLevel === 0) return
|
|
||||||
|
|
||||||
const movedElementList = copyOfElementList.splice(minLevel, combineElementList.length)
|
|
||||||
copyOfElementList.unshift(...movedElementList)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
const level = elementList.findIndex(item => item.id === element.id)
|
|
||||||
if (level === 0) return
|
|
||||||
|
|
||||||
copyOfElementList.splice(level, 1)
|
|
||||||
copyOfElementList.unshift(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyOfElementList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调整元素层级
|
|
||||||
* @param element 需要调整层级的元素
|
|
||||||
* @param command 调整命令:上移、下移、置顶、置底
|
|
||||||
*/
|
|
||||||
const orderElement = (element: PPTElement, command: ElementOrderCommands) => {
|
|
||||||
let newElementList
|
|
||||||
|
|
||||||
if (command === ElementOrderCommands.UP) newElementList = moveUpElement(currentSlide.value.elements, element)
|
|
||||||
else if (command === ElementOrderCommands.DOWN) newElementList = moveDownElement(currentSlide.value.elements, element)
|
|
||||||
else if (command === ElementOrderCommands.TOP) newElementList = moveTopElement(currentSlide.value.elements, element)
|
|
||||||
else if (command === ElementOrderCommands.BOTTOM) newElementList = moveBottomElement(currentSlide.value.elements, element)
|
|
||||||
|
|
||||||
if (!newElementList) return
|
|
||||||
|
|
||||||
slidesStore.updateSlide({ elements: newElementList })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
orderElement,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore } from '../store'
|
|
||||||
import { getImageDataURL } from '../utils/image'
|
|
||||||
import usePasteTextClipboardData from './usePasteTextClipboardData'
|
|
||||||
import useCreateElement from './useCreateElement'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { editorAreaFocus, thumbnailsFocus, disableHotkeys } = storeToRefs(useMainStore())
|
|
||||||
|
|
||||||
const { pasteTextClipboardData } = usePasteTextClipboardData()
|
|
||||||
const { createImageElement } = useCreateElement()
|
|
||||||
|
|
||||||
// 粘贴图片到幻灯片元素
|
|
||||||
const pasteImageFile = (imageFile: File) => {
|
|
||||||
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 粘贴事件监听
|
|
||||||
* @param e ClipboardEvent
|
|
||||||
*/
|
|
||||||
const pasteListener = (e: ClipboardEvent) => {
|
|
||||||
if (!editorAreaFocus.value && !thumbnailsFocus.value) return
|
|
||||||
if (disableHotkeys.value) return
|
|
||||||
|
|
||||||
if (!e.clipboardData) return
|
|
||||||
|
|
||||||
const clipboardDataItems = e.clipboardData.items
|
|
||||||
const clipboardDataFirstItem = clipboardDataItems[0]
|
|
||||||
|
|
||||||
if (!clipboardDataFirstItem) return
|
|
||||||
|
|
||||||
// 如果剪贴板内有图片,优先尝试读取图片
|
|
||||||
for (const item of clipboardDataItems) {
|
|
||||||
if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
|
|
||||||
const imageFile = item.getAsFile()
|
|
||||||
if (imageFile) pasteImageFile(imageFile)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果剪贴板内没有图片,但有文字内容,尝试解析文字内容
|
|
||||||
if (clipboardDataFirstItem.kind === 'string' && clipboardDataFirstItem.type === 'text/plain') {
|
|
||||||
clipboardDataFirstItem.getAsString(text => pasteTextClipboardData(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('paste', pasteListener)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('paste', pasteListener)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useKeyboardStore } from '../store'
|
|
||||||
import { pasteCustomClipboardString } from '../utils/clipboard'
|
|
||||||
import { parseText2Paragraphs } from '../utils/textParser'
|
|
||||||
import { getImageDataURL, isSVGString, svg2File } from '../utils/image'
|
|
||||||
import { isValidURL } from '../utils/common'
|
|
||||||
import useCreateElement from '../hooks/useCreateElement'
|
|
||||||
import useAddSlidesOrElements from '../hooks/useAddSlidesOrElements'
|
|
||||||
|
|
||||||
interface PasteTextClipboardDataOptions {
|
|
||||||
onlySlide?: boolean
|
|
||||||
onlyElements?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断图片URL字符串
|
|
||||||
*
|
|
||||||
* !!!注意,你需要判断允许哪些来源的图片地址被匹配,然后自行编写正则表达式
|
|
||||||
* !!!必须确保图片来源都是合法、可靠、可控、无访问限制的
|
|
||||||
*/
|
|
||||||
const isValidImgURL = (url: string) => {
|
|
||||||
return /^https:\/\/pptist.cn(\/[\w-./?%&=]*)?\.(jpg|jpeg|png|svg|webp)(\?.*)?$/i.test(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { shiftKeyState } = storeToRefs(useKeyboardStore())
|
|
||||||
|
|
||||||
const { createTextElement, createImageElement } = useCreateElement()
|
|
||||||
const { addElementsFromData, addSlidesFromData } = useAddSlidesOrElements()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 粘贴普通文本:创建为新的文本元素
|
|
||||||
* @param text 文本
|
|
||||||
*/
|
|
||||||
const createTextElementFromClipboard = (text: string) => {
|
|
||||||
createTextElement({
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
width: 600,
|
|
||||||
height: 50,
|
|
||||||
}, { content: text })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析剪贴板内容,根据解析结果选择合适的粘贴方式
|
|
||||||
* @param text 剪贴板内容
|
|
||||||
* @param options 配置项:onlySlide -- 仅处理页面粘贴;onlyElements -- 仅处理元素粘贴;
|
|
||||||
*/
|
|
||||||
const pasteTextClipboardData = (text: string, options?: PasteTextClipboardDataOptions) => {
|
|
||||||
const onlySlide = options?.onlySlide || false
|
|
||||||
const onlyElements = options?.onlyElements || false
|
|
||||||
|
|
||||||
const clipboardData = pasteCustomClipboardString(text)
|
|
||||||
|
|
||||||
// 元素或页面
|
|
||||||
if (typeof clipboardData === 'object') {
|
|
||||||
const { type, data } = clipboardData
|
|
||||||
|
|
||||||
if (type === 'elements' && !onlySlide) addElementsFromData(data)
|
|
||||||
else if (type === 'slides' && !onlyElements) addSlidesFromData(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通文本
|
|
||||||
else if (!onlyElements && !onlySlide) {
|
|
||||||
// 普通文字
|
|
||||||
if (shiftKeyState.value) {
|
|
||||||
const string = parseText2Paragraphs(clipboardData)
|
|
||||||
createTextElementFromClipboard(string)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 尝试检查是否为图片地址链接
|
|
||||||
if (isValidImgURL(clipboardData)) {
|
|
||||||
createImageElement(clipboardData)
|
|
||||||
}
|
|
||||||
// 尝试检查是否为超链接
|
|
||||||
else if (isValidURL(clipboardData)) {
|
|
||||||
createTextElementFromClipboard(`<a href="${clipboardData}" title="${clipboardData}" target="_blank">${clipboardData}</a>`)
|
|
||||||
}
|
|
||||||
// 尝试检查是否为SVG代码
|
|
||||||
else if (isSVGString(clipboardData)) {
|
|
||||||
const file = svg2File(clipboardData)
|
|
||||||
getImageDataURL(file).then(dataURL => createImageElement(dataURL))
|
|
||||||
}
|
|
||||||
// 普通文字
|
|
||||||
else {
|
|
||||||
const string = parseText2Paragraphs(clipboardData)
|
|
||||||
createTextElementFromClipboard(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
pasteTextClipboardData,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { computed } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useMainStore } from '../store'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const mainStore = useMainStore()
|
|
||||||
const { canvasPercentage, canvasScale, canvasDragged } = storeToRefs(mainStore)
|
|
||||||
|
|
||||||
const canvasScalePercentage = computed(() => Math.round(canvasScale.value * 100) + '%')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缩放画布百分比
|
|
||||||
* @param command 缩放命令:放大、缩小
|
|
||||||
*/
|
|
||||||
const scaleCanvas = (command: '+' | '-') => {
|
|
||||||
let percentage = canvasPercentage.value
|
|
||||||
const step = 5
|
|
||||||
const max = 200
|
|
||||||
const min = 30
|
|
||||||
if (command === '+' && percentage <= max) percentage += step
|
|
||||||
if (command === '-' && percentage >= min) percentage -= step
|
|
||||||
|
|
||||||
mainStore.setCanvasPercentage(percentage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置画布缩放比例
|
|
||||||
* 但不是直接设置该值,而是通过设置画布可视区域百分比来动态计算
|
|
||||||
* @param value 目标画布缩放比例
|
|
||||||
*/
|
|
||||||
const setCanvasScalePercentage = (value: number) => {
|
|
||||||
const percentage = Math.round(value / canvasScale.value * canvasPercentage.value) / 100
|
|
||||||
mainStore.setCanvasPercentage(percentage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置画布尺寸和位置
|
|
||||||
*/
|
|
||||||
const resetCanvas = () => {
|
|
||||||
mainStore.setCanvasPercentage(90)
|
|
||||||
if (canvasDragged) mainStore.setCanvasDragged(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
canvasScalePercentage,
|
|
||||||
setCanvasScalePercentage,
|
|
||||||
scaleCanvas,
|
|
||||||
resetCanvas,
|
|
||||||
}
|
|
||||||
}
|
|