mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加LaTeX(公式)元素
This commit is contained in:
parent
8c49e29fbc
commit
6c4dd90a25
5
package-lock.json
generated
5
package-lock.json
generated
@ -9236,6 +9236,11 @@
|
||||
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4=",
|
||||
"dev": true
|
||||
},
|
||||
"hfmath": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hfmath/-/hfmath-0.0.2.tgz",
|
||||
"integrity": "sha512-cKUi0yiQLGfLgs8+3Iw5nAiqSH13Knp7vCf0G1vlF5nfiKKO1XmxNagMvyp0F4ZvUNaHpRGTkmc7nowCy1S58g=="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "10.7.1",
|
||||
"resolved": "https://registry.npm.taobao.org/highlight.js/download/highlight.js-10.7.1.tgz",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"crypto-js": "^4.0.0",
|
||||
"dexie": "^3.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"hfmath": "0.0.2",
|
||||
"lodash": "^4.17.20",
|
||||
"mitt": "^3.0.0",
|
||||
"pptxgenjs": "^3.7.0",
|
||||
|
70
src/components/LaTeXEditor/FormulaContent.vue
Normal file
70
src/components/LaTeXEditor/FormulaContent.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<SvgWrapper
|
||||
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>
|
||||
</SvgWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, watch } from 'vue'
|
||||
import { hfmath } from './hfmath'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'formula-content',
|
||||
props: {
|
||||
latex: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
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
|
||||
})
|
||||
|
||||
return {
|
||||
box,
|
||||
pathd,
|
||||
scale,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
31
src/components/LaTeXEditor/SymbolContent.vue
Normal file
31
src/components/LaTeXEditor/SymbolContent.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="symbol-content" v-html="svg"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { hfmath } from './hfmath'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'symbol-content',
|
||||
props: {
|
||||
latex: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const svg = computed(() => {
|
||||
const eq = new hfmath(props.latex)
|
||||
return eq.svg({
|
||||
SCALE_X: 10,
|
||||
SCALE_Y: 10,
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
svg,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
5
src/components/LaTeXEditor/hfmath.ts
Normal file
5
src/components/LaTeXEditor/hfmath.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { hfmath, CONFIG as hfmathConfig } from 'hfmath'
|
||||
|
||||
hfmathConfig.SUB_SUP_SCALE = 0.5
|
||||
|
||||
export { hfmath }
|
310
src/components/LaTeXEditor/index.vue
Normal file
310
src/components/LaTeXEditor/index.vue
Normal file
@ -0,0 +1,310 @@
|
||||
<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">
|
||||
<div class="tabs">
|
||||
<div
|
||||
class="tab"
|
||||
:class="{ 'active': tab.value === toolbarState }"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
@click="toolbarState = tab.value"
|
||||
>{{tab.label}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="symbol" v-if="toolbarState === 'symbol'">
|
||||
<div class="symbol-tabs">
|
||||
<div
|
||||
class="symbol-tab"
|
||||
:class="{ 'active': selectedSymbolKey === group.type }"
|
||||
v-for="group in symbolList"
|
||||
:key="group.type"
|
||||
@click="selectedSymbolKey = group.type"
|
||||
>{{group.label}}</div>
|
||||
</div>
|
||||
<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="close()">取消</Button>
|
||||
<Button class="btn" type="primary" @click="update()">确定</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue'
|
||||
import { hfmath } from './hfmath'
|
||||
import { FORMULA_LIST, SYMBOL_LIST } from '@/configs/latex'
|
||||
|
||||
import FormulaContent from './FormulaContent.vue'
|
||||
import SymbolContent from './SymbolContent.vue'
|
||||
|
||||
const tabs = [
|
||||
{ label: '常用符号', value: 'symbol' },
|
||||
{ label: '预置公式', value: 'formula' },
|
||||
]
|
||||
|
||||
export default defineComponent({
|
||||
name: 'latex-editor',
|
||||
emits: ['update', 'close'],
|
||||
components: {
|
||||
FormulaContent,
|
||||
SymbolContent,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const latex = ref('')
|
||||
const toolbarState = ref('symbol')
|
||||
const textAreaRef = ref<HTMLTextAreaElement>()
|
||||
|
||||
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
|
||||
|
||||
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 close = () => emit('close')
|
||||
|
||||
const insertSymbol = (latex: string) => {
|
||||
if (!textAreaRef.value) return
|
||||
textAreaRef.value.focus()
|
||||
document.execCommand('insertText', false, latex)
|
||||
}
|
||||
|
||||
return {
|
||||
tabs,
|
||||
latex,
|
||||
toolbarState,
|
||||
selectedSymbolKey,
|
||||
formulaList: FORMULA_LIST,
|
||||
symbolList: SYMBOL_LIST,
|
||||
symbolPool,
|
||||
textAreaRef,
|
||||
update,
|
||||
close,
|
||||
insertSymbol,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.latex-editor {
|
||||
height: 560px;
|
||||
}
|
||||
.container {
|
||||
height: calc(100% - 50px);
|
||||
display: flex;
|
||||
padding-top: 25px;
|
||||
}
|
||||
.left {
|
||||
width: 540px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.input-area {
|
||||
flex: 1;
|
||||
|
||||
textarea {
|
||||
height: 100% !important;
|
||||
border-color: $borderColor !important;
|
||||
padding: 10px !important;
|
||||
|
||||
&: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;
|
||||
}
|
||||
.preview-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.right {
|
||||
height: 100%;
|
||||
margin-left: 20px;
|
||||
flex: 1;
|
||||
border: solid 1px $borderColor;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
.tabs {
|
||||
height: 40px;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $lightGray;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
& + .tab {
|
||||
border-left: 1px solid $borderColor;
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
width: 246px;
|
||||
height: 60px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $lightGray;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.symbol-tabs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
margin: 12px 12px 5px;
|
||||
}
|
||||
.symbol-tab {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-bottom: 2px solid $themeColor;
|
||||
}
|
||||
}
|
||||
.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>
|
274
src/configs/latex.ts
Normal file
274
src/configs/latex.ts
Normal file
@ -0,0 +1,274 @@
|
||||
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' },
|
||||
],
|
||||
},
|
||||
]
|
@ -223,6 +223,27 @@ export default () => {
|
||||
createElement(newElement)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建LaTeX元素
|
||||
* @param svg SVG代码
|
||||
*/
|
||||
const createLatexElement = (data: { path: string; latex: string; w: number; h: number; }) => {
|
||||
createElement({
|
||||
type: 'latex',
|
||||
id: createRandomCode(),
|
||||
width: data.w,
|
||||
height: data.h,
|
||||
left: (VIEWPORT_SIZE - data.w) / 2,
|
||||
top: (VIEWPORT_SIZE * viewportRatio.value - data.h) / 2,
|
||||
path: data.path,
|
||||
latex: data.latex,
|
||||
color: fontColor.value,
|
||||
strokeWidth: 2,
|
||||
viewBox: [data.w, data.h],
|
||||
fixedRatio: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建视频元素
|
||||
* @param src 视频地址
|
||||
@ -246,6 +267,7 @@ export default () => {
|
||||
createTextElement,
|
||||
createShapeElement,
|
||||
createLineElement,
|
||||
createLatexElement,
|
||||
createVideoElement,
|
||||
}
|
||||
}
|
@ -522,6 +522,22 @@ export default () => {
|
||||
|
||||
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 / 100,
|
||||
y: el.top / 100,
|
||||
w: el.width / 100,
|
||||
h: el.height / 100,
|
||||
}
|
||||
if (el.link) options.hyperlink = { url: el.link }
|
||||
|
||||
pptxSlide.addImage(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
pptx.writeFile({ fileName: `pptist.pptx` }).then(() => exporting.value = false).catch(() => {
|
||||
|
@ -52,6 +52,7 @@ app.component('RadioGroup', Radio.Group)
|
||||
app.component('RadioButton', Radio.Button)
|
||||
app.component('Input', Input)
|
||||
app.component('InputGroup', Input.Group)
|
||||
app.component('TextArea', Input.TextArea)
|
||||
app.component('Modal', Modal)
|
||||
app.component('Dropdown', Dropdown)
|
||||
app.component('Menu', Menu)
|
||||
|
@ -90,6 +90,7 @@ import {
|
||||
VolumeSmall,
|
||||
CycleOne,
|
||||
VideoTwo,
|
||||
Formula,
|
||||
} from '@icon-park/vue-next'
|
||||
|
||||
export default {
|
||||
@ -102,6 +103,7 @@ export default {
|
||||
app.component('IconChartProportion', ChartProportion)
|
||||
app.component('IconInsertTable', InsertTable)
|
||||
app.component('IconVideoTwo', VideoTwo)
|
||||
app.component('IconFormula', Formula)
|
||||
|
||||
// 锁定与解锁
|
||||
app.component('IconLock', Lock)
|
||||
|
@ -7,6 +7,7 @@ export const enum ElementTypes {
|
||||
LINE = 'line',
|
||||
CHART = 'chart',
|
||||
TABLE = 'table',
|
||||
LATEX = 'latex',
|
||||
VIDEO = 'video',
|
||||
}
|
||||
|
||||
@ -462,6 +463,33 @@ export interface PPTTableElement extends PPTBaseElement {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* LaTeX元素(公式)
|
||||
*
|
||||
* type: 元素类型(latex)
|
||||
*
|
||||
* latex: latex代码
|
||||
*
|
||||
* path: svg path
|
||||
*
|
||||
* color: 颜色
|
||||
*
|
||||
* strokeWidth: 路径宽度
|
||||
*
|
||||
* viewBox: SVG的viewBox属性
|
||||
*
|
||||
* fixedRatio: 固定形状宽高比例
|
||||
*/
|
||||
export interface PPTLatexElement extends PPTBaseElement {
|
||||
type: 'latex';
|
||||
latex: string;
|
||||
path: string;
|
||||
color: string;
|
||||
strokeWidth: number;
|
||||
viewBox: [number, number];
|
||||
fixedRatio: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频元素
|
||||
*
|
||||
@ -478,7 +506,7 @@ export interface PPTVideoElement extends PPTBaseElement {
|
||||
}
|
||||
|
||||
|
||||
export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTVideoElement
|
||||
export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement
|
||||
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@ import mitt, { Emitter } from 'mitt'
|
||||
export const enum EmitterEvents {
|
||||
RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND',
|
||||
OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR',
|
||||
OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR',
|
||||
}
|
||||
|
||||
export interface RichTextCommand {
|
||||
@ -13,6 +14,7 @@ export interface RichTextCommand {
|
||||
type Events = {
|
||||
[EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand | RichTextCommand[];
|
||||
[EmitterEvents.OPEN_CHART_DATA_EDITOR]: void;
|
||||
[EmitterEvents.OPEN_LATEX_EDITOR]: void;
|
||||
}
|
||||
|
||||
const emitter: Emitter<Events> = mitt<Events>()
|
||||
|
@ -37,6 +37,7 @@ import ShapeElement from '@/views/components/element/ShapeElement/index.vue'
|
||||
import LineElement from '@/views/components/element/LineElement/index.vue'
|
||||
import ChartElement from '@/views/components/element/ChartElement/index.vue'
|
||||
import TableElement from '@/views/components/element/TableElement/index.vue'
|
||||
import LatexElement from '@/views/components/element/LatexElement/index.vue'
|
||||
import VideoElement from '@/views/components/element/VideoElement/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
@ -72,6 +73,7 @@ export default defineComponent({
|
||||
[ElementTypes.LINE]: LineElement,
|
||||
[ElementTypes.CHART]: ChartElement,
|
||||
[ElementTypes.TABLE]: TableElement,
|
||||
[ElementTypes.LATEX]: LatexElement,
|
||||
[ElementTypes.VIDEO]: VideoElement,
|
||||
}
|
||||
return elementTypeMap[props.elementInfo.type] || null
|
||||
|
74
src/views/Editor/Canvas/Operate/LatexElementOperate.vue
Normal file
74
src/views/Editor/Canvas/Operate/LatexElementOperate.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="latex-element-operate">
|
||||
<BorderLine
|
||||
class="operate-border-line"
|
||||
v-for="line in borderLines"
|
||||
:key="line.type"
|
||||
:type="line.type"
|
||||
:style="line.style"
|
||||
/>
|
||||
<template v-if="!elementInfo.lock && (isActiveGroupElement || !isMultiSelect)">
|
||||
<ResizeHandler
|
||||
class="operate-resize-handler"
|
||||
v-for="point in resizeHandlers"
|
||||
:key="point.direction"
|
||||
:type="point.direction"
|
||||
:style="point.style"
|
||||
@mousedown.stop="$event => scaleElement($event, elementInfo, point.direction)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
import { PPTShapeElement } from '@/types/slides'
|
||||
import { OperateResizeHandler } from '@/types/edit'
|
||||
import useCommonOperate from '../hooks/useCommonOperate'
|
||||
|
||||
import ResizeHandler from './ResizeHandler.vue'
|
||||
import BorderLine from './BorderLine.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'latex-element-operate',
|
||||
inheritAttrs: false,
|
||||
components: {
|
||||
ResizeHandler,
|
||||
BorderLine,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTShapeElement>,
|
||||
required: true,
|
||||
},
|
||||
isActiveGroupElement: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isMultiSelect: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
scaleElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTShapeElement, command: OperateResizeHandler) => void>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
|
||||
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
|
||||
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
||||
const { resizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
||||
|
||||
return {
|
||||
scaleWidth,
|
||||
resizeHandlers,
|
||||
borderLines,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
@ -48,6 +48,7 @@ import ShapeElementOperate from './ShapeElementOperate.vue'
|
||||
import LineElementOperate from './LineElementOperate.vue'
|
||||
import ChartElementOperate from './ChartElementOperate.vue'
|
||||
import TableElementOperate from './TableElementOperate.vue'
|
||||
import LatexElementOperate from './LatexElementOperate.vue'
|
||||
import VideoElementOperate from './VideoElementOperate.vue'
|
||||
import LinkHandler from './LinkHandler.vue'
|
||||
|
||||
@ -108,6 +109,7 @@ export default defineComponent({
|
||||
[ElementTypes.LINE]: LineElementOperate,
|
||||
[ElementTypes.CHART]: ChartElementOperate,
|
||||
[ElementTypes.TABLE]: TableElementOperate,
|
||||
[ElementTypes.LATEX]: LatexElementOperate,
|
||||
[ElementTypes.VIDEO]: VideoElementOperate,
|
||||
}
|
||||
return elementTypeMap[props.elementInfo.type] || null
|
||||
|
@ -53,6 +53,9 @@
|
||||
<IconInsertTable class="handler-item" />
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入公式">
|
||||
<IconFormula class="handler-item" @click="latexEditorVisible = true" />
|
||||
</Tooltip>
|
||||
<Popover trigger="click" v-model:visible="videoInputVisible">
|
||||
<template #content>
|
||||
<VideoInput
|
||||
@ -74,6 +77,19 @@
|
||||
<IconFullScreen class="handler-item viewport-size-adaptation" @click="setCanvasPercentage(90)" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:visible="latexEditorVisible"
|
||||
:footer="null"
|
||||
centered
|
||||
:width="880"
|
||||
destroyOnClose
|
||||
>
|
||||
<LaTeXEditor
|
||||
@close="latexEditorVisible = false"
|
||||
@update="data => { createLatexElement(data); latexEditorVisible = false }"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -92,6 +108,7 @@ import LinePool from './LinePool.vue'
|
||||
import ChartPool from './ChartPool.vue'
|
||||
import TableGenerator from './TableGenerator.vue'
|
||||
import VideoInput from './VideoInput.vue'
|
||||
import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'canvas-tool',
|
||||
@ -101,6 +118,7 @@ export default defineComponent({
|
||||
ChartPool,
|
||||
TableGenerator,
|
||||
VideoInput,
|
||||
LaTeXEditor,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore()
|
||||
@ -113,7 +131,7 @@ export default defineComponent({
|
||||
const { scaleCanvas, setCanvasPercentage } = useScaleCanvas()
|
||||
const { redo, undo } = useHistorySnapshot()
|
||||
|
||||
const { createImageElement, createChartElement, createTableElement, createVideoElement } = useCreateElement()
|
||||
const { createImageElement, createChartElement, createTableElement, createLatexElement, createVideoElement } = useCreateElement()
|
||||
|
||||
const insertImageElement = (files: File[]) => {
|
||||
const imageFile = files[0]
|
||||
@ -126,6 +144,7 @@ export default defineComponent({
|
||||
const chartPoolVisible = ref(false)
|
||||
const tableGeneratorVisible = ref(false)
|
||||
const videoInputVisible = ref(false)
|
||||
const latexEditorVisible = ref(false)
|
||||
|
||||
// 绘制文字范围
|
||||
const drawText = () => {
|
||||
@ -167,11 +186,13 @@ export default defineComponent({
|
||||
chartPoolVisible,
|
||||
tableGeneratorVisible,
|
||||
videoInputVisible,
|
||||
latexEditorVisible,
|
||||
drawText,
|
||||
drawShape,
|
||||
drawLine,
|
||||
createChartElement,
|
||||
createTableElement,
|
||||
createLatexElement,
|
||||
createVideoElement,
|
||||
}
|
||||
},
|
||||
|
109
src/views/Editor/Toolbar/ElementStylePanel/LatexStylePanel.vue
Normal file
109
src/views/Editor/Toolbar/ElementStylePanel/LatexStylePanel.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="latex-style-panel">
|
||||
<div class="row"><Button style="flex: 1;" @click="latexEditorVisible = true">编辑 LaTeX</Button></div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="row">
|
||||
<div style="flex: 2;">颜色:</div>
|
||||
<Popover trigger="click">
|
||||
<template #content>
|
||||
<ColorPicker
|
||||
:modelValue="handleElement.color"
|
||||
@update:modelValue="value => updateLatex({ color: value })"
|
||||
/>
|
||||
</template>
|
||||
<ColorButton :color="handleElement.color" style="flex: 3;" />
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div style="flex: 2;">粗细:</div>
|
||||
<InputNumber
|
||||
:min="1"
|
||||
:max="3"
|
||||
:value="handleElement.strokeWidth"
|
||||
@change="value => updateLatex({ strokeWidth: value })"
|
||||
style="flex: 3;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:visible="latexEditorVisible"
|
||||
:footer="null"
|
||||
centered
|
||||
:width="880"
|
||||
destroyOnClose
|
||||
>
|
||||
<LaTeXEditor
|
||||
:value="handleElement.latex"
|
||||
@close="latexEditorVisible = false"
|
||||
@update="data => { updateLatexData(data); latexEditorVisible = false }"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onUnmounted, ref } from 'vue'
|
||||
import { MutationTypes, useStore } from '@/store'
|
||||
import { PPTLatexElement } from '@/types/slides'
|
||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import ColorButton from '../common/ColorButton.vue'
|
||||
import LaTeXEditor from '@/components/LaTeXEditor/index.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'latex-style-panel',
|
||||
components: {
|
||||
ColorButton,
|
||||
LaTeXEditor,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore()
|
||||
const handleElement = computed<PPTLatexElement>(() => store.getters.handleElement)
|
||||
|
||||
const latexEditorVisible = ref(false)
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const updateLatex = (props: Partial<PPTLatexElement>) => {
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
const updateLatexData = (data: { path: string; latex: string; w: number; h: number; }) => {
|
||||
updateLatex({
|
||||
path: data.path,
|
||||
latex: data.latex,
|
||||
width: data.w,
|
||||
height: data.h,
|
||||
viewBox: [data.w, data.h],
|
||||
})
|
||||
}
|
||||
|
||||
const openLatexEditor = () => latexEditorVisible.value = true
|
||||
|
||||
emitter.on(EmitterEvents.OPEN_LATEX_EDITOR, openLatexEditor)
|
||||
onUnmounted(() => {
|
||||
emitter.off(EmitterEvents.OPEN_LATEX_EDITOR, openLatexEditor)
|
||||
})
|
||||
|
||||
return {
|
||||
handleElement,
|
||||
latexEditorVisible,
|
||||
updateLatex,
|
||||
updateLatexData,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
@ -18,6 +18,7 @@ import ShapeStylePanel from './ShapeStylePanel.vue'
|
||||
import LineStylePanel from './LineStylePanel.vue'
|
||||
import ChartStylePanel from './ChartStylePanel/index.vue'
|
||||
import TableStylePanel from './TableStylePanel.vue'
|
||||
import LatexStylePanel from './LatexStylePanel.vue'
|
||||
import VideoStylePanel from './VideoStylePanel.vue'
|
||||
|
||||
export default defineComponent({
|
||||
@ -36,6 +37,7 @@ export default defineComponent({
|
||||
[ElementTypes.LINE]: LineStylePanel,
|
||||
[ElementTypes.CHART]: ChartStylePanel,
|
||||
[ElementTypes.TABLE]: TableStylePanel,
|
||||
[ElementTypes.LATEX]: LatexStylePanel,
|
||||
[ElementTypes.VIDEO]: VideoStylePanel,
|
||||
}
|
||||
return panelMap[handleElement.value.type] || null
|
||||
|
@ -30,6 +30,7 @@ import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeE
|
||||
import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
|
||||
import ScreenChartElement from '@/views/components/element/ChartElement/ScreenChartElement.vue'
|
||||
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
||||
import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexElement.vue'
|
||||
import ScreenVideoElement from '@/views/components/element/VideoElement/ScreenVideoElement.vue'
|
||||
|
||||
export default defineComponent({
|
||||
@ -57,6 +58,7 @@ export default defineComponent({
|
||||
[ElementTypes.LINE]: BaseLineElement,
|
||||
[ElementTypes.CHART]: ScreenChartElement,
|
||||
[ElementTypes.TABLE]: BaseTableElement,
|
||||
[ElementTypes.LATEX]: BaseLatexElement,
|
||||
[ElementTypes.VIDEO]: ScreenVideoElement,
|
||||
}
|
||||
return elementTypeMap[props.elementInfo.type] || null
|
||||
|
@ -24,6 +24,7 @@ import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeE
|
||||
import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
|
||||
import BaseChartElement from '@/views/components/element/ChartElement/BaseChartElement.vue'
|
||||
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
||||
import BaseLatexElement from '@/views/components/element/LatexElement/BaseLatexElement.vue'
|
||||
import BaseVideoElement from '@/views/components/element/VideoElement/BaseVideoElement.vue'
|
||||
|
||||
export default defineComponent({
|
||||
@ -47,6 +48,7 @@ export default defineComponent({
|
||||
[ElementTypes.LINE]: BaseLineElement,
|
||||
[ElementTypes.CHART]: BaseChartElement,
|
||||
[ElementTypes.TABLE]: BaseTableElement,
|
||||
[ElementTypes.LATEX]: BaseLatexElement,
|
||||
[ElementTypes.VIDEO]: BaseVideoElement,
|
||||
}
|
||||
return elementTypeMap[props.elementInfo.type] || null
|
||||
|
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-element-latex"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
width: elementInfo.width + 'px',
|
||||
height: elementInfo.height + 'px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rotate-wrapper"
|
||||
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
|
||||
>
|
||||
<div class="element-content">
|
||||
<SvgWrapper
|
||||
overflow="visible"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:stroke="elementInfo.color"
|
||||
:stroke-width="elementInfo.strokeWidth"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<g
|
||||
:transform="`scale(${elementInfo.width / elementInfo.viewBox[0]}, ${elementInfo.height / elementInfo.viewBox[1]}) translate(0,0) matrix(1,0,0,1,0,0)`"
|
||||
>
|
||||
<path :d="elementInfo.path"></path>
|
||||
</g>
|
||||
</SvgWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { PPTLatexElement } from '@/types/slides'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'base-element-latex',
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTLatexElement>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.base-element-latex {
|
||||
position: absolute;
|
||||
}
|
||||
.rotate-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.element-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
transform-origin: 0 0;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
107
src/views/components/element/LatexElement/index.vue
Normal file
107
src/views/components/element/LatexElement/index.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
class="editable-element-latex"
|
||||
:class="{ 'lock': elementInfo.lock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
width: elementInfo.width + 'px',
|
||||
height: elementInfo.height + 'px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rotate-wrapper"
|
||||
:style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
|
||||
>
|
||||
<div
|
||||
class="element-content"
|
||||
v-contextmenu="contextmenus"
|
||||
@mousedown="$event => handleSelectElement($event)"
|
||||
@dblclick="openLatexEditor()"
|
||||
>
|
||||
<SvgWrapper
|
||||
overflow="visible"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
:stroke="elementInfo.color"
|
||||
:stroke-width="elementInfo.strokeWidth"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<g
|
||||
:transform="`scale(${elementInfo.width / elementInfo.viewBox[0]}, ${elementInfo.height / elementInfo.viewBox[1]}) translate(0,0) matrix(1,0,0,1,0,0)`"
|
||||
>
|
||||
<path :d="elementInfo.path"></path>
|
||||
</g>
|
||||
</SvgWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { PPTLatexElement } from '@/types/slides'
|
||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-element-latex',
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTLatexElement>,
|
||||
required: true,
|
||||
},
|
||||
selectElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTLatexElement, canMove?: boolean) => void>,
|
||||
required: true,
|
||||
},
|
||||
contextmenus: {
|
||||
type: Function as PropType<() => ContextmenuItem[]>,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const handleSelectElement = (e: MouseEvent) => {
|
||||
if (props.elementInfo.lock) return
|
||||
e.stopPropagation()
|
||||
|
||||
props.selectElement(e, props.elementInfo)
|
||||
}
|
||||
|
||||
const openLatexEditor = () => {
|
||||
emitter.emit(EmitterEvents.OPEN_LATEX_EDITOR)
|
||||
}
|
||||
|
||||
return {
|
||||
handleSelectElement,
|
||||
openLatexEditor,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editable-element-latex {
|
||||
position: absolute;
|
||||
|
||||
&.lock .element-content {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.rotate-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.element-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
cursor: move;
|
||||
|
||||
svg {
|
||||
transform-origin: 0 0;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user