feat: 添加LaTeX(公式)元素

This commit is contained in:
pipipi-pikachu 2021-09-15 22:10:24 +08:00
parent 8c49e29fbc
commit 6c4dd90a25
23 changed files with 1160 additions and 2 deletions

5
package-lock.json generated
View File

@ -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",

View File

@ -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",

View 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>

View 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>

View File

@ -0,0 +1,5 @@
import { hfmath, CONFIG as hfmathConfig } from 'hfmath'
hfmathConfig.SUB_SUP_SCALE = 0.5
export { hfmath }

View 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
View 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' },
],
},
]

View File

@ -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,
}
}

View File

@ -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(() => {

View File

@ -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)

View File

@ -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)

View File

@ -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
/**

View File

@ -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>()

View File

@ -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

View 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>

View File

@ -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

View File

@ -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,
}
},

View 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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View 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>