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