提交 5543664d authored 作者: 王鹏飞's avatar 王鹏飞

Initial commit

上级
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
VITE_API_URL_WORD = http://8.141.148.247:7419
VITE_API_BASE_API_PREFFIX = /api
VITE_API_WEBSOCKET_URL = ws://8.141.148.247:7420
VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn
VITE_API_URL_WORD = http://192.168.11.88:7419
VITE_API_BASE_API_PREFFIX = /api
VITE_API_WEBSOCKET_URL = ws://192.168.11.88:7419
VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn
VITE_API_URL_WORD = https://zijingebook.ezijing.com/file/
VITE_API_BASE_API_PREFFIX = /api
VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com
VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn
\ No newline at end of file
# .eslintignore
/config
public
dist
mock
node_modules
.eslintrc.cjs
.prettierrc.cjs
.eslintrc.cjs
\ No newline at end of file
module.exports = {
extends: [require.resolve('@umijs/lint/dist/config/eslint')],
globals: {
page: true,
REACT_APP_ENV: true,
},
};
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
package-lock.json*
package-lock.json
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json
{
"extends": [
"development"
],
"hints": {
"meta-viewport": "off"
}
}
\ No newline at end of file
{
"*.{js,jsx,ts,tsx,json}": ["prettier --write", "eslint --fix"],
"*.css": ["stylelint --fix", "prettier --write"]
}
# .prettierignore
**/*.svg
package.json
/dist
.dockerignore
.DS_Store
.eslintignore
*.png
*.toml
docker
.editorconfig
Dockerfile*
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log
.history
CNAME
/build
/public
const fabric = require('@umijs/fabric');
module.exports = {
// 一行最多 120 字符
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: "as-needed",
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: true,
// 末尾需要有逗号
trailingComma: "all",
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: "always",
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: "preserve",
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: "css",
// vue 文件中的 script 和 style 内不用缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf
endOfLine: "lf",
// 格式化嵌入的内容
embeddedLanguageFormatting: "auto",
// 组件或者标签的属性是否控制一行只显示一个属性
singleAttributePerLine: false,
...fabric.prettier,
};
const fabric = require('@umijs/fabric');
module.exports = {
"extends": "stylelint-config-standard",
"rules": {
"string-quotes": "single",
"value-keyword-case": null,
"declaration-block-trailing-semicolon": "always",
"no-invalid-position-at-import-rule": null,
"selector-class-pattern": "^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:\\[.+\\])?$",
"property-no-unknown": [
true,
{
"ignoreProperties": ["composes"]
}
],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global"]
}
]
},
...fabric.stylelint,
};
BZIJING-HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./public/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<title>清控紫荆数智学堂</title>
</head>
<body>
<div id="root"></div>
<script src="/formula-editor/dist/formula-editor.min.js"></script>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
{
"compilerOptions": {
"target": "ES6",
"jsx": "react",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist", "build"]
}
{
"name": "zijing-html",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"start:test": "cross-env NODE_ENV=test vite",
"start:dev": "cross-env NODE_ENV=development vite",
"build": "vite build",
"build:dev": "cross-env NODE_ENV=development vite build",
"build:ali": "cross-env NODE_ENV=ali vite build",
"build:online": "cross-env NODE_ENV=online vite build",
"preview": "vite preview",
"prepare": "husky install",
"fix-memory-limit": "cross-env LIMIT=40240 increase-memory-limit",
"lint": "npm run lint:js && npm run lint:prettier",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
"prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\""
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@reduxjs/toolkit": "^1.9.7",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-react": "^1.0.6",
"@wangeditor/plugin-link-card": "^1.0.0",
"ali-oss": "^6.20.0",
"antd": "^5.11.1",
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"highlight.js": "^11.9.0",
"i": "^0.3.7",
"jquery": "^3.7.1",
"js-md5": "^0.8.3",
"katex": "^0.15.2",
"loadsh": "^0.0.4",
"lodash": "^4.17.21",
"qs": "^6.11.2",
"rc-slider-captcha": "^1.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.3",
"react-router-dom": "^6.18.0",
"redux-persist": "^6.0.0",
"snabbdom": "^3.6.2",
"xml-formatter": "^3.6.2"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@umijs/fabric": "^4.0.1",
"@umijs/lint": "^4.0.88",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-react": "^4.0.3",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.53.0",
"husky": "^8.0.3",
"less": "^4.2.0",
"lint-staged": "^15.1.0",
"picocolors": "^1.0.0",
"prettier": "^3.1.0",
"stylelint-config-standard": "^34.0.0",
"terser": "^5.26.0",
"vite": "^4.4.5",
"vite-plugin-progress": "^0.0.7"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
]
}
}
:root {
--me-theme: #bcd4e7;
--me-theme-light: #d0e3f2;
--me-hover: #f1f2f4;
--me-border: #ddd;
--me-gray: ##666;
}
.math-editor {
color: #000;
font-size: 18px;
text-align: left;
border: 1px solid #ccc;
overflow: hidden;
* {
margin: 0;
padding: 0;
}
div {
box-sizing: border-box;
}
.me-menu {
width: 100%;
padding: 0 10px;
background: var(--me-theme);
border-bottom: 1px solid var(--me-border);
&-item {
float: left;
position: relative;
width: 50px;
padding: 5px;
text-align: center;
cursor: pointer;
.icon {
display: block;
height: 22px;
margin-bottom: 5px;
line-height: 28px;
}
&.active {
background: var(--me-theme-light);
}
}
}
.me-latex,
.me-svg {
float: left;
width: 50%;
height: calc(100% - 1px);
padding: 10px 12px;
}
.me-latex {
font-size: 14px;
letter-spacing: 1px;
overflow-y: auto;
}
.me-svg {
display: flex;
align-items: center;
background: #f6f7f8;
overflow-x: auto;
}
.me-droplist {
position: absolute;
left: 0;
text-align: left;
background: #fff;
border: 1px solid var(--me-border);
border-radius: 3px;
.me-dp-title {
height: 25px;
padding-left: 12px;
font-size: 12px;
line-height: 29px;
background: var(--me-theme-light);
border-bottom: 1px solid var(--me-border);
}
.me-item:hover {
background: var(--me-hover);
}
.me-list {
padding: 5px 0;
list-style: none;
line-height: 1.36;
.me-item {
font-size: 16px;
padding: 5px 12px;
.title {
margin-bottom: 8px;
font-size: 12px;
color: var(--me-gray);
}
}
}
.me-block {
list-style: none;
padding: 5px;
.me-item {
box-sizing: border-box;
display: inline-block;
min-width: 32px;
min-height: 32px;
margin: 5px;
padding: 2px;
line-height: 30px;
text-align: center;
border: 1px solid var(--me-border);
border-radius: 3px;
}
}
}
.clearfix:after {
content: '';
display: block;
height: 0;
visibility: hidden;
clear: both;
}
}
export type ConfigType = {
width: number
height: number
menuHeight: number
/** MathJax的加载链接 */
mathJaxUrl: string
/** 挂载节点的z-index值,不传可能回导致下拉菜单遮挡 */
zIndex?: number
}
/** 编辑器默认配置 */
export const defaultConfig: ConfigType = {
width: 620,
height: 272,
menuHeight: 38,
mathJaxUrl:
'https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/mathjax/3.2.0/es5/tex-svg.min.js',
}
export type Presets = {
label: string
value: string
}[]
export type MenuType = {
menus: string[]
presets: FnType<Presets, FnType<string, string>>
operators: string[]
greekLetters: string[]
inequation: string[]
AdvancedMath: string[]
LinearAlgebra: string[]
arrows: string[]
trigonometric: string[]
physical: string[]
chemistry: string[]
}
/** 编辑器按钮配置 */
export const menusConfig: MenuType = {
menus: [
'presets',
'operators',
'greekLetters',
'inequation',
'AdvancedMath',
'LinearAlgebra',
'arrows',
'Trigonometric',
'physical',
'chemistry'
],
presets(t: FnType<string, string>): Presets {
return [
// 勾股定理
{
label: t('fe.pt'),
value: 'a^2+b^2=c^2',
},
// 双曲线
{
label: t('fe.h'),
value: '\\frac{x^2}{a^2}-\\frac{y^2}{b^2}=1',
},
// 三角函数关系
{
label: t('fe.tfr'),
value: '\\sin^2\\theta +\\cos^2\\theta = 1',
},
// 导数
{
label: t('fe.d'),
value: "\\left( e^x \\right) ' = e^x",
},
// 牛莱公式
{
label: t('fe.nl'),
value: '\\int_a^b f(x) dx = F(b) - F(a)',
},
]
},
operators: [
'+',
'-',
'\\pm',
'\\mp',
'\\times',
'\\div',
'\\ast',
'\\cdot',
'\\cap',
'\\cup',
'\\aleph',
'\\Re',
'\\top',
'\\bot',
'\\infty',
'\\partial',
'\\forall',
'\\exists',
'\\neg',
'\\because',
'\\therefore',
'\\varnothing',
'\\frac{b}{a}',
'\\circ',
'\\bullet',
'\\prime',
'\\triangle',
'\\angle',
'\\surd',
'\\barwedge',
'\\veebar',
'\\odot',
'\\oplus',
'\\otimes',
'\\oslash',
'\\circledcirc',
'\\boxdot',
'\\bigtriangledown',
'\\dagger',
'\\diamond',
'\\star',
'\\triangleleft',
'\\triangleright',
],
greekLetters: [
'\\alpha',
'\\beta',
'\\gamma',
'\\delta',
'\\epsilon',
'\\zeta',
'\\eta',
'\\theta',
'\\iota',
'\\kappa',
'\\lambda',
'\\mu',
'\\nu',
'\\xi',
'\\omicron',
'\\pi',
'\\rho',
'\\sigma',
'\\tau',
'\\upsilon',
'\\phi',
'\\chi',
'\\psi',
'\\omega',
'\\varepsilon',
'\\vartheta',
'\\varpi',
'\\varrho',
'\\varsigma',
'\\varphi',
'\\Gamma',
'\\Delta',
'\\Theta',
'\\Lambda',
'\\Xi',
'\\Pi',
'\\Sigma',
'\\Upsilon',
'\\Phi',
'\\Psi',
'\\Omega',
],
inequation: [
'=',
'\\leq',
'\\geq',
'\\prec',
'\\succ',
'\\preceq',
'\\succeq',
'\\ll',
'\\gg',
'\\equiv',
'\\sim',
'\\simeq',
'\\asymp',
'\\approx',
'\\ne',
'\\subset',
'\\supset',
'\\subseteq',
'\\supseteq',
'\\nsubseteq',
'\\nsupseteq',
'\\in',
'\\ni',
'\\notin',
],
AdvancedMath: [
'x_{a}',
'x^{b}',
'x_{a}^{b}',
'\\sqrt{x}',
'\\sqrt[n]{x}',
'\\bigcap_{a}^{b}',
'\\bigcup_{a}^{b}',
'\\prod_{a}^{b}',
'\\coprod_{a}^{b}',
'\\int_{a}^{b}',
'\\oint_{a}^{b}',
'\\sum_{a}^{b}{x}',
'\\lim_{a \\rightarrow b}{x}',
'\\frac{dy}{dx}|_{t=0}',
'\\vec{a}',
'\\bar{a}',
'\\tilde{a}',
'\\dot{a}',
'\\ddot{a}',
'\\hat{a}',
'\\overleftarrow{ab}',
'\\overline{ab}',
'\\overrightarrow{ab}',
'\\underline{ab}',
'\\overbrace{ab}',
'\\underbrace{ab}',
],
LinearAlgebra: [
'A^{*}',
'A^{T}',
'A^{-1}',
'\\left( x \\right)',
'\\left[ x \\right]',
'\\left \\{ x \\right \\}',
'\\left| x \\right|',
'\\begin{pmatrix}a&b\\\\c&d\\\\ \\end{pmatrix}',
'\\begin{bmatrix}a&b\\\\c&d\\\\ \\end{bmatrix}',
'\\begin{Bmatrix}a&b\\\\c&d\\\\ \\end{Bmatrix}',
'\\begin{vmatrix}a&b\\\\c&d\\\\ \\end{vmatrix}',
],
arrows: [
'\\leftarrow',
'\\rightarrow',
'\\leftrightarrow',
'\\Leftarrow',
'\\Rightarrow',
'\\Leftrightarrow',
'\\uparrow',
'\\downarrow',
'\\updownarrow',
'\\Uparrow',
'\\Downarrow',
'\\Updownarrow',
],
trigonometric: [
'\\sin \\theta',
'\\cos \\theta',
'\\tan \\theta',
'\\csc \\theta',
'\\sec \\theta',
'\\cot \\theta',
'\\arcsin \\theta',
'\\arccos \\theta',
],
// 物理
physical: [
'E = n{{ \\Delta \\Phi } \\over {\\Delta {t} }}',
'\\sum {{{ \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over F} }_i}} = \\frac{{d \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over v} }}{{dt}} = 0',
'\\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over F} = m \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over a} = m \\frac{{{d^2} \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over r} }}{{d{t^2}}}',
'{{ \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over F} }_{12}} = - {{ \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over F} }_{21}}',
'{E_p} = -\\frac{{GMm}}{r}',
'\\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over F} = k \\frac{{Qq}}{{{r^2}}} \\hat{r}',
'\\oint_L { \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over E} } \\cdot { \\rm{d}} \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over l} = 0',
'\\mathop \\Phi \\nolimits_e = \\oint { \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over E} \\cdot {d \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over S}} = {1 \\over {{\\varepsilon _0}}}\\sum {q} }',
'\\mathop \\Phi \\nolimits_e = \\oint { \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over E} \\cdot {d \\mathord{ \\buildrel{ \\lower3pt \\hbox{$ \\scriptscriptstyle \\rightharpoonup$}} \\over S}} = {1 \\over {{\\varepsilon _0}}}\\sum {q} }'
],
// 化学
chemistry : [
'\\ce{SO4^2- + Ba^2+ -> BaSO4 v}',
'\\ce{A v B (v) -> B ^ B (^)}',
'\\ce{Hg^2+ ->[I-] $\\underset{\\mathrm{red}}{\\ce{HgI2}}$ ->[I-] $\\underset{\\mathrm{red}}{\\ce{[Hg^{II}I4]^2-}}$}',
'\\ce{Zn^2+ <=>[+ 2OH-][+ 2H+] $\\underset{\\text{amphoteres Hydroxid}}{\\ce{Zn(OH)2 v}}$ <=>[+ 2OH-][+ 2H+] $\\underset{\\text{Hydroxozikat}}{\\ce{[Zn(OH)4]^2-}}$}'
]
}
import $, { DomElement } from '../utils/dom-core'
import Editor from './index'
import { createEmptyElem, isNullText } from '../utils/util'
class Command {
public editor: Editor
constructor(editor: Editor) {
this.editor = editor
}
/**
* 执行富文本操作的命令
* @param value value
*/
public do(value?: string | DomElement): void {
const selection = this.editor.selection
// 如果无选区,忽略
if (!selection.getRange()) return
// 恢复选取
selection.restoreSelection()
// 执行
this.insertHtml(value as string)
// 插入渲染数学公式
this.renderFormula()
// 最后,恢复选取保证光标在原来的位置闪烁
selection.saveRange()
selection.restoreSelection()
}
/**
* 公式编辑输入处理
* @param text 插入的字符串
* @param isSeparator 是否是定义的分隔符
*/
insert(text: string, isSeparator?: boolean): void {
if (isSeparator) {
const editor = this.editor
const range = editor.selection.getRange()
if (range) {
// 删除原来的输入
const textNode = range.startContainer
range.setStart(textNode, range.endOffset)
range.setEnd(textNode, range.endOffset)
range.deleteContents()
}
this.insertHtml(text === ' ' ? '&nbsp;' : text)
return
}
const htmlStr = createEmptyElem(text)
this.do(htmlStr)
}
/**
* 渲染数学公式
*/
public renderFormula(): void {
const editor = this.editor
// 获取输入框文本
let text = editor.latex.text()
const $textSvgElem = editor.$textSvgElem
// 没有文本
if (isNullText(text)) {
$textSvgElem.html('<p></p>')
return
}
// 空格处理
text = text.replace(/&nbsp;/g, ' ').trim()
// 文本没有变化
if ($textSvgElem.attr('data-latex') === text) return
const htmlText = `$ ${text} $`
$textSvgElem.attr('data-latex', text)
// 渲染
$textSvgElem.html(htmlText)
$textSvgElem.renderFormula()
}
/**
* 插入 html 片段
* @param html html字符串
*/
private insertHtml(html: string): void {
const editor = this.editor
const range = editor.selection.getRange()
if (!range) return
if (this.queryCommandSupported('insertHTML')) {
// W3C
this.execCommand('insertHTML', html)
} else if (range.insertNode) {
// IE
range.deleteContents()
if ($(html).elems.length > 0) {
range.insertNode($(html).elems[0])
} else {
const newNode = document.createElement('p')
newNode.appendChild(document.createTextNode(html))
range.insertNode(newNode)
}
editor.selection.collapseRange()
}
}
/**
* 执行 document.execCommand
* @param name name
* @param value value
*/
private execCommand(name: string, value: string): void {
document.execCommand(name, false, value)
}
/**
* 执行 document.queryCommandSupported
* @param name name
*/
public queryCommandSupported(name: string): boolean {
return document.queryCommandSupported(name)
}
}
export default Command
import $, { DomElement, DomElementSelector } from '../utils/dom-core'
import { defaultConfig, ConfigType } from '../config'
import { menusConfig, MenuType } from '../config/menus'
import { injectMathJax, initMathJax } from '../mathjax'
import initDom from './init-dom'
import CommandAPI from './command'
import SelectionAndRangeAPI from './selection'
import initSelection from './init-selection'
import Menus from '../menus/index'
import Text from '../text/index'
import { hightlightHtml } from '../utils/util'
import { t } from '../utils/i18n'
class Editor {
public config: ConfigType
public menusConfig: MenuType
public $editorRootElem: DomElement
public $toolbarElem: DomElement
public $textLatexElem: DomElement
public $textSvgElem: DomElement
public cmd: CommandAPI
public selection: SelectionAndRangeAPI
public menus: Menus
public latex: Text
public t: FnType<string, string>
constructor() {
this.config = defaultConfig
this.menusConfig = menusConfig
this.$editorRootElem = $('<div class="math-editor"></div>')
this.$toolbarElem = $('<div class="me-menu"></div>')
this.$textLatexElem = $('<div class="me-latex"></div>')
this.$textSvgElem = $('<div class="me-svg"></div>')
this.cmd = new CommandAPI(this)
this.selection = new SelectionAndRangeAPI(this)
this.menus = new Menus(this)
this.latex = new Text(this)
this.t = t
}
// 暴露 $
static $ = $
/**
* 创建编辑器 DOM
* @param rootSelector 公式弹窗附属DOM selector
* @param callback mathJax加载完成时执行
*/
public create(rootSelector: DomElementSelector, callback?: FnType): void {
// 加载 mathJax
injectMathJax(this.config.mathJaxUrl)
// 初始化颜色、字体、字号
// 初始化菜单
this.menus.init()
// 初始化编辑区
this.latex.init()
// 生成DOM元素
initDom(this)
const $rootElem = $(rootSelector)
$rootElem.text(this.t('fe.l'))
// mathJax加载完成
const onReady = () => {
$rootElem.text('').append(this.$editorRootElem)
// 初始化选区,将光标定位到内容尾部
this.initSelection()
// 渲染菜单
this.$toolbarElem.renderFormula()
callback && callback()
}
// 配置MathJax
initMathJax(onReady)
}
/**
* 二次编辑
*/
public append(value: string): void {
const html = hightlightHtml(value)
this.cmd.do(html)
}
/**
* 初始化选区
*/
public initSelection(): void {
initSelection(this)
}
/**
* 销毁编辑器 DOM
*/
public destoryDom(): void {
this.$editorRootElem.remove()
}
}
export default Editor
import Editor from './index'
import $, { DomElement } from '../utils/dom-core'
/**
* 初始化并添加编辑器Dom结构
* @param editor 编辑器实例
*/
function initDom(editor: Editor) {
const {
$editorRootElem,
$toolbarElem,
$textLatexElem,
$textSvgElem,
config,
} = editor
// 添加菜单
$editorRootElem.append($toolbarElem)
const $textElem: DomElement = $('<div></div>')
$textElem
.addClass('clearfix')
.css('width', '100%')
.css('height', `${config.height - config.menuHeight}px`)
.css('overflow', 'hidden')
// 设置latex编辑区域
$textLatexElem
.attr('contenteditable', 'true')
.css('outline', 'none')
.css('border-right', '1px solid #ccc')
// 添加latex编辑和数学公式渲染区域
$textElem.append($textLatexElem).append($textSvgElem)
// 初始化根节点
$editorRootElem
.css('width', config.width + 'px')
.css('height', config.height + 'px')
$editorRootElem.append($textElem)
}
export default initDom
import Editor from './index'
import $ from '../utils/dom-core'
import { EMPTY_P } from '../utils/constants'
/**
* 初始化编辑器选区,将光标定位到文档末尾
* @param editor 编辑器实例
* @param newLine 是否新增一行
*/
function initSelection(editor: Editor, newLine?: boolean) {
const $textLatexElem = editor.$textLatexElem
const $children = $textLatexElem.children()
if (!$children || !$children.length) {
// 如果编辑器区域无内容,添加一个空行,重新设置选区
$textLatexElem.append($(EMPTY_P))
initSelection(editor)
return
}
const $last = $children.last()
if (newLine) {
// 新增一个空行
const html = $last.html().toLowerCase()
const nodeName = $last.getNodeName()
if ((html !== '<br>' && html !== '<br/>') || nodeName !== 'P') {
// 最后一个元素不是 空标签,添加一个空行,重新设置选区
$textLatexElem.append($(EMPTY_P))
initSelection(editor)
return
}
}
editor.selection.createRangeByElem($last, false, true)
editor.selection.restoreSelection()
}
export default initSelection
import $, { DomElement } from '../utils/dom-core'
import Editor from './index'
import { UA } from '../utils/util'
import { EMPTY_P } from '../utils/constants'
type CurrentRangeType = Range | null | undefined
class SelectionAndRange {
public editor: Editor
private _currentRange: CurrentRangeType = null
constructor(editor: Editor) {
this.editor = editor
}
/**
* 获取当前 range
*/
public getRange(): CurrentRangeType {
return this._currentRange
}
/**
* 保存选区范围
* @param _range 选区范围
*/
public saveRange(_range?: Range): void {
if (_range) {
// 保存已有选区
this._currentRange = _range
return
}
// 获取当前的选区
const selection = window.getSelection() as Selection
if (selection.rangeCount === 0) return
const range = selection.getRangeAt(0)
// 获取选区范围的 DOM 元素
const $containerElem = this.getSelectionContainerElem(range)
// 当 选区范围内没有 DOM元素 则抛出
if (!$containerElem?.length) return
const editor = this.editor
const $textLatexElem = editor.$textLatexElem
if ($textLatexElem.isContain($containerElem)) {
if ($textLatexElem.elems[0] === $containerElem.elems[0]) {
if ($textLatexElem.html().trim() === EMPTY_P) {
const $children = $textLatexElem.children()
const $last = $children?.last()
editor.selection.createRangeByElem($last as DomElement, true, true)
editor.selection.restoreSelection()
}
}
// 是编辑内容之内的
this._currentRange = range
}
}
/**
* 折叠选区范围
* @param toStart true 开始位置,false 结束位置
*/
public collapseRange(toStart: boolean = false): void {
const range = this._currentRange
range && range.collapse(toStart)
}
/**
* 获取选区范围内的文字
*/
public getSelectionText(): string {
const range = this._currentRange
return range ? range.toString() : ''
}
/**
* 获取选区范围的 DOM 元素
* @param range 选区范围
*/
public getSelectionContainerElem(range?: Range): DomElement | undefined {
const r: CurrentRangeType = range || this._currentRange
if (r) {
const elem: Node = r.commonAncestorContainer
return $(elem.nodeType === 1 ? elem : elem.parentNode)
}
}
/**
* 选区范围开始的 DOM 元素
* @param range 选区范围
*/
public getSelectionStartElem(range?: Range): DomElement | undefined {
const r: CurrentRangeType = range || this._currentRange
if (r) {
const elem: Node = r.startContainer
return $(elem.nodeType === 1 ? elem : elem.parentNode)
}
}
/**
* 选区范围结束的 DOM 元素
* @param range 选区范围
*/
public getSelectionEndElem(range?: Range): DomElement | undefined {
const r: CurrentRangeType = range || this._currentRange
if (r) {
const elem: Node = r.endContainer
return $(elem.nodeType === 1 ? elem : elem.parentNode)
}
}
/**
* 选区是否为空(没有选择文字)
*/
public isSelectionEmpty(): boolean {
const range = this._currentRange
if (range && range.startContainer) {
if (range.startContainer === range.endContainer) {
if (range.startOffset === range.endOffset) {
return true
}
}
}
return false
}
/**
* 恢复选区范围
*/
public restoreSelection(): void {
const selection = window.getSelection()
const r = this._currentRange
if (selection && r) {
selection.removeAllRanges()
selection.addRange(r)
}
}
/**
* 重新设置选区
* @param startDom 选区开始的元素
* @param endDom 选区结束的元素
*/
public createRangeByElems(startDom: Node, endDom: Node): void {
const selection = window.getSelection
? window.getSelection()
: document.getSelection()
//清除所有的选区
selection?.removeAllRanges()
const range = document.createRange()
range.setStart(startDom, 0)
// 设置多行标签之后,第二个参数会被h标签内的b、font标签等影响range范围的选取
range.setEnd(endDom, endDom.childNodes.length || 1)
// 保存设置好的选区
this.saveRange(range)
//恢复选区
this.restoreSelection()
}
/**
* 根据 DOM 元素设置选区
* @param $elem DOM 元素
* @param toStart true 开始位置,false 结束位置
* @param isContent 是否选中 $elem 的内容
*/
public createRangeByElem(
$elem: DomElement,
toStart?: boolean,
isContent?: boolean,
): void {
if (!$elem.length) return
const elem = $elem.elems[0]
const range = document.createRange()
if (isContent) {
range.selectNodeContents(elem)
} else {
// 如果用户没有传入 isContent 参数,那就默认为 false
range.selectNode(elem)
}
if (toStart != null) {
// 传入了 toStart 参数,折叠选区。如果没传入 toStart 参数,则忽略这一步
range.collapse(toStart)
if (!toStart) {
this.saveRange(range)
this.editor.selection.moveCursor(elem)
}
}
// 存储 range
this.saveRange(range)
}
/**
* 获取 当前 选取范围的 顶级(段落) 元素
* @param $editor
*/
public getSelectionRangeTopNodes(): DomElement[] {
const $startElem = this.getSelectionStartElem()?.getNodeTop(this.editor)
const $endElem = this.getSelectionEndElem()?.getNodeTop(this.editor)
return this.recordSelectionNodes($($startElem), $($endElem))
}
/**
* 移动光标位置,默认情况下在尾部
* 有一个特殊情况是firefox下的文本节点会自动补充一个br元素,会导致自动换行
* 所以默认情况下在firefox下的文本节点会自动移动到br前面
* @param {Node} node 元素节点
* @param {number} position 光标的位置
*/
public moveCursor(node: Node, position?: number): void {
const range = this.getRange()
//对文本节点特殊处理
let len: number =
node.nodeType === 3
? (node.nodeValue?.length as number)
: node.childNodes.length
if ((UA.isFirefox || UA.isIE()) && len !== 0) {
// firefox下在节点为文本节点和节点最后一个元素为文本节点的情况下
if (node.nodeType === 3 || node.childNodes[len - 1].nodeName === 'BR') {
len = len - 1
}
}
const pos: number = position ?? len
if (!range) return
if (node) {
range.setStart(node, pos)
range.setEnd(node, pos)
this.restoreSelection()
}
}
/**
* 获取光标在当前选区的位置
*/
public getCursorPos(): number | undefined {
const selection = window.getSelection()
return selection?.anchorOffset
}
/**
* 清除当前选区的Range,notice:不影响已保存的Range
*/
public clearWindowSelectionRange(): void {
const selection = window.getSelection()
if (selection) {
selection.removeAllRanges()
}
}
/**
* 记录节点 - 从选区开始节点开始 一直到匹配到选区结束节点为止
* @param $node 节点
*/
public recordSelectionNodes(
$node: DomElement,
$endElem: DomElement,
): DomElement[] {
const $list: DomElement[] = []
let isEnd = true
// 解决ctrl+a全选报错的bug $elem.getNodeName()可能会触发$elem[0]未定义
try {
let $NODE: DomElement = $node
const $textLatexElem = this.editor.$textLatexElem
// $NODE元素为空时不需要进行循环
while (isEnd) {
const $elem = $NODE?.getNodeTop(this.editor)
if ($elem.getNodeName() === 'BODY') isEnd = false // 兜底
if ($elem.length > 0) {
$list.push($($NODE))
// 两个边界情况:
// 1. 当前元素就是我们要找的末尾元素
// 2. 当前元素已经是编辑区顶级元素(否则会找到编辑区的兄弟节点,比如placeholder元素)
if ($endElem?.equal($elem) || $textLatexElem.equal($elem)) {
isEnd = false
} else {
$NODE = $elem.getNextSibling()
}
}
}
} catch (e) {
isEnd = false
}
return $list
}
/**
* 将当前 range 设置到 node 元素并初始化位置
* 解决编辑器内容为空时,菜单不生效的问题
* @param node 元素节点
*/
public setRangeToElem(node: Node): void {
const range = this.getRange()
range?.setStart(node, 0)
range?.setEnd(node, 0)
}
}
export default SelectionAndRange
declare type FnType<T = void, V = unknown> = (...arg: V[]) => T
declare interface Window {
MathJax: any
}
declare type TimeoutId = ReturnType<typeof global.setTimeout>
// 记录代理事件绑定
declare type listener = (e: Event) => void
\ No newline at end of file
// 样式植入
import './assets/style/common.less'
export * from './mathjax'
import formulaEditor from './editor'
// 检验是否浏览器环境
try {
document
} catch (ex) {
throw new Error('Please run in the browser environment!')
}
export default formulaEditor
/**
* 引入MathJax插件
* @param {string} url
*/
export function injectMathJax(url: string) {
if (window.MathJax) return
const script = document.createElement('script')
script.src = url
script.async = true
document.head.appendChild(script)
}
/**
* 配置全局 MathJax
* @param {FnType} callback Mathjax 加载完成的回调
*/
export function initMathJax(callback: FnType<void>) {
if (window.MathJax) {
callback && callback()
return
}
window.MathJax = {
tex: {
inlineMath: [['$', '$']],
processEnvironments: true,
processRefs: true,
},
options: {
skipHtmlTags: ['noscript', 'style', 'textarea', 'pre', 'code'],
ignoreHtmlClass: 'tex2jax_ignore',
},
startup: {
pageReady: () => callback && callback(),
},
svg: {
fontCache: 'global',
},
}
}
/**
* 手动渲染公式
* @param {HTMLElement} el 需要触发渲染的节点
* @returns Promise
*/
export function renderFormula(el?: HTMLElement | HTMLElement[]): Promise<any> {
if (!window.MathJax || !window.MathJax.typesetPromise) return Promise.reject()
if (el && !Array.isArray(el)) {
el = [el]
}
return window.MathJax.typesetPromise(el)
}
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import AdvancedMathList from '../common/InlineBlockList'
class AdvancedMath extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('\\Sigma'))
const presetList = new AdvancedMathList(editor.menusConfig.AdvancedMath)
const presetConf = {
width: 244,
title: editor.t('fe.am'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default AdvancedMath
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import ArrowList from '../common/InlineBlockList'
class Arrows extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('\\rightarrow'))
const presetList = new ArrowList(editor.menusConfig.arrows)
const presetConf = {
title: editor.t('fe.a'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Arrows
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import OperatorList from '../common/InlineBlockList'
class Chemistry extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('H_{2}O'))
const presetList = new OperatorList(editor.menusConfig.chemistry)
const presetConf = {
title: editor.t('fe.c'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Chemistry
import $ from '../../utils/dom-core'
import { DropListItem } from '../menu-constructors/DropList'
/**
* BlockList 行内块状配置列表
*/
class InlineBlockList {
private itemList: DropListItem[]
constructor(list: string[]) {
this.itemList = list.map(item => ({
$elem: $(`<span>$${item}$</span>`),
value: item,
}))
}
public getItemList(): DropListItem[] {
return this.itemList
}
}
export default InlineBlockList
import $ from '../../utils/dom-core'
import { DropListItem } from '../menu-constructors/DropList'
import { Presets } from '../../config/menus'
/**
* PresetList 预设公式列表
*/
class PresetList {
private itemList: DropListItem[]
constructor(list: Presets) {
this.itemList = list.map(({label, value}) => {
return {
$elem: $(`<p class="title">${label}:</p><p>$ ${value} $</p>`),
value,
}
})
}
public getItemList(): DropListItem[] {
return this.itemList
}
}
export default PresetList
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import GreekLetterList from '../common/InlineBlockList'
class GreekLetters extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('\\alpha'))
const presetList = new GreekLetterList(editor.menusConfig.greekLetters)
const presetConf = {
title: editor.t('fe.gl'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default GreekLetters
import Editor from '../editor/index'
import Menu from './menu-constructors/Menu'
import MenuConstructorList from './menu-list'
class Menus {
public editor: Editor
public menuList: Menu[]
public constructorList: Record<string, any>
constructor(editor: Editor) {
this.editor = editor
this.menuList = []
this.constructorList = MenuConstructorList // 所有菜单构造函数的列表
}
/**
* 自定义添加菜单
* @param key 菜单 key ,和 editor.menusConfig.menus 对应
* @param Menu 菜单构造函数
*/
public extend(key: string, Menu: any) {
if (!Menu || typeof Menu !== 'function') return
this.constructorList[key] = Menu
}
// 初始化菜单
public init(): void {
// 从用户配置的 menus 入手,看需要初始化哪些菜单
const config = this.editor.menusConfig
config.menus.forEach((menuKey) => {
const MenuConstructor = this.constructorList[menuKey]
this._initMenuList(menuKey, MenuConstructor)
})
// 渲染 DOM
this._addToToolbar()
}
/**
* 创建 menu 实例,并放到 menuList 中
* @param menuKey 菜单 key ,和 editor.menusConfig.menus 对应
* @param MenuConstructor 菜单构造函数
*/
private _initMenuList(menuKey: String, MenuConstructor: any): void {
// 必须是 class
if (typeof MenuConstructor !== 'function') return
if (this.menuList.some((menu) => menu.key === menuKey)) {
console.warn('Duplicate menu name:' + menuKey)
} else {
const m = new MenuConstructor(this.editor)
m.key = menuKey
this.menuList.push(m)
}
}
// 添加到菜单栏
private _addToToolbar(): void {
const editor = this.editor
const $toolbarElem = editor.$toolbarElem
$toolbarElem.addClass('clearfix')
// 遍历添加到 DOM
this.menuList.forEach((menu) => {
const $elem = menu.$elem
if ($elem) {
$toolbarElem.append($elem)
}
})
}
/**
* 获取菜单对象
* @param 菜单名称 小写
* @return Menus 菜单对象
*/
public menuFind(key: string): Menu {
const menuList = this.menuList
for (let i = 0, l = menuList.length; i < l; i++) {
if (menuList[i].key === key) return menuList[i]
}
return menuList[0]
}
}
export default Menus
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import InequationList from '../common/InlineBlockList'
class Inequation extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('\\approx'))
const presetList = new InequationList(editor.menusConfig.inequation)
const presetConf = {
title: editor.t('fe.i'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Inequation
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import LinearAlgebraList from '../common/InlineBlockList'
class LinearAlgebra extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('\\left|x\\right|'))
const presetList = new LinearAlgebraList(editor.menusConfig.LinearAlgebra)
const presetConf = {
width: 244,
title: editor.t('fe.lg'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default LinearAlgebra
import $, { DomElement } from '../../utils/dom-core'
import DropListMenu from './DropListMenu'
import { EMPTY_FN } from '../../utils/constants'
const RANDOM_NUM = 76
export type DropListItem = {
$elem: DomElement
value: string
}
export type DropListConf = {
title: string
list: DropListItem[]
/** 下拉选项样式 list 列表形式(如“预设”菜单) block 块状形式(如“操作符”菜单) 默认: block */
type?: string
clickHandler?: (value: DropListItem['value']) => void
/** 下拉框宽度,默认232 */
width?: number
}
class DropList {
private menu: DropListMenu
private conf: DropListConf
private $container: DomElement
private rendered: boolean
private _show: boolean
public hideTimeoutId: number
constructor(menu: DropListMenu, conf: DropListConf) {
this.hideTimeoutId = 0
this.menu = menu
this.conf = conf
const { height: editorHeight, menuHeight, zIndex = 0 } = this.menu.editor.config
// 标题
const $title = $(`<p>${conf.title}</p>`)
.addClass('me-dp-title')
// 容器
const $container = $('<div class="me-droplist"></div>')
.append($title)
.css('z-index', zIndex + 1)
// 列表和类型
const list = conf.list || []
const type = conf.type || 'block'
// item 的点击事件
const clickHandler = conf.clickHandler || EMPTY_FN
// 加入 DOM 并绑定事件
const $list = $(`<ul class="${type === 'list' ? 'me-list' : 'me-block'}"></ul>`)
.css('max-height', editorHeight - menuHeight - RANDOM_NUM + 'px')
.css('overflow-y', 'auto')
list.forEach((item) => {
const $elem = item.$elem
const value = item.value
const $li = $('<li class="me-item"></li>')
if ($elem) {
$li.append($elem)
$list.append($li)
$li.on('click', (e: Event) => {
clickHandler(value)
// 阻止冒泡
e.stopPropagation()
// item 点击之后,隐藏 list
this.hideTimeoutId = window.setTimeout(() => {
this.hide()
})
})
}
})
$container.append($list)
// 绑定隐藏事件
$container.on('mouseleave', () => {
this.hideTimeoutId = window.setTimeout(() => {
this.hide()
})
})
// 记录属性
this.$container = $container
this.rendered = false
this._show = false
}
/**
* 显示 DropList
*/
public show() {
if (this.hideTimeoutId) {
// 清除之前的定时隐藏
clearTimeout(this.hideTimeoutId)
}
const menu = this.menu
const $menuELem = menu.$elem
const $container = this.$container
if (this._show) {
return
}
if (this.rendered) {
// 显示
$container.show()
} else {
// 加入 DOM 之前先定位位置
const { menuHeight } = this.menu.editor.config
const width = this.conf.width || 232
$container.css('top', menuHeight - 1 + 'px').css('width', width + 'px')
// 加入到 DOM
$menuELem.append($container)
this.rendered = true
// 渲染公式
$menuELem.renderFormula()
}
// 修改属性
this._show = true
}
/**
* 隐藏 DropList
*/
public hide() {
const $container = this.$container
if (!this._show) {
return
}
// 隐藏并需改属性
$container.hide()
this._show = false
}
public get isShow() {
return this._show
}
}
export default DropList
import { DomElement } from '../../utils/dom-core'
import { hightlightHtml } from '../../utils/util'
import Editor from '../../editor/index'
import Menu from './Menu'
import DropList, { DropListConf } from './DropList'
class DropListMenu extends Menu {
public dropList: DropList
constructor($elem: DomElement, editor: Editor, conf: DropListConf) {
super($elem, editor)
// 初始化 dropList
const dropList = new DropList(this, conf)
this.dropList = dropList
// 绑定事件
$elem
.on('mouseover', () => {
if (!editor.selection.getRange()) return
$elem.addClass('active')
// 显示
dropList.show()
})
.on('mouseleave', () => {
$elem.removeClass('active')
// 隐藏
dropList.hideTimeoutId = window.setTimeout(() => {
dropList.hide()
})
})
}
/**
* 执行命令
* @param value value
*/
public command(value: string): void {
const editor = this.editor
const isEmptySelection = editor.selection.isSelectionEmpty()
const selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
if (!selectionElem) return
const html = hightlightHtml(value)
editor.cmd.do(html)
if (isEmptySelection) {
// 需要将选区范围折叠起来
editor.selection.collapseRange()
editor.selection.restoreSelection()
}
}
}
export default DropListMenu
import { DomElement } from '../../utils/dom-core'
import Editor from '../../editor/index'
class Menu {
public key: string | undefined
public $elem: DomElement
public editor: Editor
/** 菜单是否处于激活状态,如选中一段加粗文字时,bold 菜单要被激活(即高亮显示)*/
private _active: boolean
constructor($elem: DomElement, editor: Editor) {
this.$elem = $elem
this.editor = editor
this._active = false
// 绑定菜单点击事件
$elem.on('click', (e: Event) => {
e.stopPropagation()
if (!editor.selection.getRange()) return
$elem.addClass('active')
this.clickHandler()
})
}
/**
* 菜单点击事件
*/
protected clickHandler(): void {}
/**
* 激活菜单,高亮显示
*/
protected active(): void {
this._active = true
this.$elem.addClass('me-active')
}
/**
* 取消激活,不再高亮显示
*/
protected unActive(): void {
this._active = false
this.$elem.removeClass('me-active')
}
/**
* 是否处于激活状态
*/
public get isActive() {
return this._active
}
}
export default Menu
export { DropListConf, DropListItem } from './DropList'
import Presets from './presets'
import Operators from './operators'
import GreekLetters from './greek-letters'
import Inequation from './inequation'
import AdvancedMath from './advanced-math'
import LinearAlgebra from './linear-algebra'
import Arrows from './arrows'
import Trigonometric from './trigonometric'
import Physical from './physical'
import Chemistry from './chemistry'
export default {
presets: Presets,
operators: Operators,
greekLetters: GreekLetters,
inequation: Inequation,
AdvancedMath: AdvancedMath,
LinearAlgebra: LinearAlgebra,
arrows: Arrows,
Trigonometric: Trigonometric,
physical: Physical,
chemistry: Chemistry,
}
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import OperatorList from '../common/InlineBlockList'
class Operators extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('+'))
const presetList = new OperatorList(editor.menusConfig.operators)
const presetConf = {
title: editor.t('fe.o'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Operators
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import OperatorList from '../common/InlineBlockList'
class Physical extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('E'))
const presetList = new OperatorList(editor.menusConfig.physical)
const presetConf = {
title: editor.t('fe.e'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Physical
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import PresetList from '../common/PresetList'
class Presets extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('f_{(x)}'))
const presetList = new PresetList(editor.menusConfig.presets(editor.t))
const presetConf = {
width: 252,
title: editor.t('fe.p'),
type: 'list',
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Presets
import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import { createMemuElem } from '../../utils/util'
import Editor from '../../editor/index'
import TrigonometricList from '../common/InlineBlockList'
class Trigonometric extends DropListMenu {
constructor(editor: Editor) {
const $elem = $(createMemuElem('\\theta'))
const presetList = new TrigonometricList(editor.menusConfig.trigonometric)
const presetConf = {
title: editor.t('fe.t'),
list: presetList.getItemList(),
clickHandler: (value: string) => {
this.command(value)
},
}
super($elem, editor, presetConf)
}
}
export default Trigonometric
import $ from '../utils/dom-core'
import Editor from '../editor/index'
import { debounce } from '../utils/util'
import {
EMPTY_P,
NON_CHARACTER_KEYS,
IUTERCEPTED_KEYS,
} from '../utils/constants'
class Text {
public editor: Editor
constructor(editor: Editor) {
this.editor = editor
}
/**
* 初始化
*/
public init(): void {
// 实时保存选取范围
this._saveRange()
}
/**
* 清空内容
*/
public clear(): void {
this.editor.$textLatexElem.replaceChildAll($(EMPTY_P))
}
/**
* 设置/获取 text
*/
public text(): string {
const $textLatexElem = this.editor.$textLatexElem
return $textLatexElem.text().replace(/&nbsp;/g, ' ')
}
/**
* 每一步操作,都实时保存选区范围
*/
private _saveRange(): void {
const editor = this.editor
const $textLatexElem = editor.$textLatexElem
const $document = $(document)
// 保存当前的选区
function saveRange() {
// 随时保存选区
editor.selection.saveRange()
}
// 按键后保存
function handleKeyup() {
saveRange()
editor.cmd.renderFormula()
}
$textLatexElem.on('keyup', debounce(handleKeyup))
// ctrl + 字符按键新建高亮
function handleKeydown(e: KeyboardEvent) {
const key = e.key
const isCtrl = e.ctrlKey && !NON_CHARACTER_KEYS.includes(key)
const isIntercepted = IUTERCEPTED_KEYS.includes(key)
if (isCtrl || isIntercepted) {
e.preventDefault()
saveRange()
editor.cmd.insert(key, isIntercepted)
}
}
$textLatexElem.on('keydown', handleKeydown)
// 点击后保存
function onceClickSaveRange() {
saveRange()
$textLatexElem.off('click', onceClickSaveRange)
}
$textLatexElem.on('click', onceClickSaveRange)
function handleMouseUp() {
// 在编辑器区域之外完成抬起,保存此时编辑区内的新选区,取消此时鼠标抬起事件
saveRange()
$document.off('mouseup', handleMouseUp)
}
function listenMouseLeave() {
// 当鼠标移动到外面,要监听鼠标抬起操作
$document.on('mouseup', handleMouseUp)
// 首次移出时即接触leave监听,防止用户不断移入移出多次注册handleMouseUp
$textLatexElem.off('mouseleave', listenMouseLeave)
}
$textLatexElem.on('mousedown', () => {
// mousedown 状态下,要坚听鼠标滑动到编辑区域外面
$textLatexElem.on('mouseleave', listenMouseLeave)
})
$textLatexElem.on('mouseup', () => {
// 记得移除$textLatexElem的mouseleave事件, 避免内存泄露
$textLatexElem.off('mouseleave', listenMouseLeave)
// fix:避免当选中一段文字之后,再次点击文字中间位置无法更新selection问题。issue#3096
setTimeout(() => {
const selection = editor.selection
const range = selection.getRange()
if (!range) return
saveRange()
}, 0)
})
}
}
export default Text
/** 空函数 */
export function EMPTY_FN() {}
/** 编辑器为了方便继续输入/换行等原因 主动生成的空标签 */
export const EMPTY_P = '<p data-we-empty-p=""><br></p>'
/** 公式高亮颜色 */
export const HIGHLIGHT_COLOR = '#ee7959'
/** 非字符按键 */
export const NON_CHARACTER_KEYS = [
'Control',
'Escape',
'Tab',
'CapsLock',
'Shift',
'Meta',
'Alt',
'ArrowLeft',
'ArrowUp',
'ArrowDown',
'ArrowRight',
'Enter',
'Backspace',
'Delete',
'Insert',
]
/** 需拦截操作的按键 */
export const IUTERCEPTED_KEYS = [' ', '{', '}']
差异被折叠。
/**
* 多语言中文配置
*/
const CHINESE: Record<string, string> = {
'fe.a': '箭头符号',
'fe.am': '高数',
'fe.lg': '线代',
'fe.gl': '希腊字母',
'fe.i': '不等式',
'fe.o': '运算符',
'fe.p': '预设公式',
'fe.t': '三角函数',
'fe.l': '资源加载中...',
'fe.pt': '勾股定理',
'fe.h': '双曲线',
'fe.tfr': '三角函数关系',
'fe.d': '导数',
'fe.nl': '牛莱公式',
'fe.e': '物理公式',
'fe.c': '化学公式',
}
/**
* 多语言转换函数(对应 i18n 的 t )
* @param key 对应的多语言配置key
*/
export function t(key: string): string {
return CHINESE[key] || key
}
import { HIGHLIGHT_COLOR } from './constants'
/**
* 获取随机字符
* @param prefix 前缀
*/
export function getRandom(prefix: string = ''): string {
return prefix + Math.random().toString().slice(2)
}
/**
* 替换 html 特殊字符
* @param html html 字符串
*/
export function replaceHtmlSymbol(html: string) {
return html
.replace(/</gm, '&lt;')
.replace(/>/gm, '&gt;')
.replace(/"/gm, '&quot;')
.replace(/(\r\n|\r|\n)/g, '<br/>')
}
export function replaceSpecialSymbol(value: string) {
return value
.replace(/&lt;/gm, '<')
.replace(/&gt;/gm, '>')
.replace(/&quot;/gm, '"')
}
/**
* 遍历类数组
* @param fakeArr 类数组
* @param fn 回调函数
*/
export function arrForEach<
T extends { length: number; [key: number]: unknown },
>(
fakeArr: T,
fn: (this: T, item: T[number], index: number) => boolean | unknown,
): void {
let i, item, result
const length = fakeArr.length || 0
for (i = 0; i < length; i++) {
item = fakeArr[i]
result = fn.call(fakeArr, item, i)
if (result === false) {
break
}
}
}
/**
* 节流
* @param fn 函数
* @param interval 间隔时间,毫秒
*/
export function throttle<C, T extends unknown[]>(
fn: (this: C, ...args: T) => unknown,
interval: number = 200,
) {
let flag = false
return function (this: C, ...args: T): void {
if (!flag) {
flag = true
setTimeout(() => {
flag = false
fn.call(this, ...args) // this 报语法错误,先用 null
}, interval)
}
}
}
/**
* 防抖
* @param fn 函数
* @param delay 间隔时间,毫秒
*/
export function debounce<C, T extends unknown[]>(
fn: (this: C, ...args: T) => void,
delay: number = 200,
): (this: C, ...args: T) => void {
let lastFn = 0
return function (...args: T) {
if (lastFn) {
window.clearTimeout(lastFn)
}
lastFn = window.setTimeout(() => {
lastFn = 0
fn.call(this, ...args) // this 报语法错误,先用 null
}, delay)
}
}
/**
* isFunction 是否是函数
* @param fn 函数
*/
export function isFunction(fn: any): fn is Function {
return typeof fn === 'function'
}
/**
* 引用与非引用值 深拷贝方法
* @param data
*/
export function deepClone<T>(data: T): T {
if (typeof data !== 'object' || typeof data === 'function' || data === null) {
return data
}
const item: any = Array.isArray(data) ? [] : {}
for (const i in data) {
if (Object.prototype.hasOwnProperty.call(data, i)) {
item[i] = deepClone(data[i])
}
}
return item
}
/**
* 将可遍历的对象转换为数组
* @param data 可遍历的对象
*/
export function toArray<T>(data: T) {
return Array.prototype.slice.call(data)
}
/**
* 唯一id生成
* @param length 随机数长度
*/
export function getRandomCode() {
return Math.random().toString(36).slice(-5)
}
/**
* 输入非数字或字母自动创建空元素
* @param str 字符串
*/
export function createEmptyElem(str: string): string {
return `<span style="color: ${HIGHLIGHT_COLOR}">${
str !== ' ' ? str : '&nbsp;'
}</span>`
}
/**
* 字符串转高亮HTML字符串
* @param str 字符串
*/
export function hightlightHtml(str: string): string {
const reg = /[A-Za-z0-9]+/g
str = str.replace(
reg,
(target: string) =>
`<span style="color: ${HIGHLIGHT_COLOR}">${target}</span>`,
)
return `<span>${str}</span>&nbsp;`
}
/**
* 创建菜单按钮
* @param icon 字符串
*/
export function createMemuElem(icon: string) {
return `<div class="me-menu-item"><span class="icon">$${icon}$</span></div>`
}
/**
* 是否空文本
* @param str 字符串
*/
export function isNullText(str: string): boolean {
str = str.replace(/(&nbsp;)|( )/g, '')
return !str
}
/**
* @description 工具函数集合
*/
class NavUA {
public _ua: string
// 是否为旧版 Edge
public isOldEdge: boolean
// 是否为 Firefox
public isFirefox: boolean
constructor() {
this._ua = navigator.userAgent
const math = this._ua.match(/(Edge?)\/(\d+)/)
this.isOldEdge =
math && math[1] === 'Edge' && parseInt(math[2]) < 19 ? true : false
this.isFirefox =
/Firefox\/\d+/.test(this._ua) && !/Seamonkey\/\d+/.test(this._ua)
? true
: false
}
/** 是否为 IE */
public isIE() {
return 'ActiveXObject' in window
}
/** 是否为 webkit */
public isWebkit() {
return /webkit/i.test(this._ua)
}
}
/* 和 UA 相关的属性 */
export const UA = new NavUA()
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
PC:
1. 预览时气泡的投影样式加深;
2. 修改默认字号为四号,18px;
3. 去掉选择图片时,查看链接的按钮;
4. 添加题目时,选择题选项正确答案的判断,单选和判断必须选一个,多选需要2个以上;
5. 修改画廊删除第一张图片无法正常删除,图片的标题和描述无法更新的问题,同时字段设置为不必须;
6. 为章头、节头、画廊(不包含行内)、扩展(不包含行内)、交互练习右上角添加关闭按钮,可以直接删除添加的内容。
7. 添加题目时,题干、答案、选项会过滤复制粘贴过来的样式,图片也会去除。
8. 添加交互练习时,可以进行分组,分组签名加上编号;同时题干和编号第一行对齐。
APP:
1. 划线和高亮问题修复;
2. 画廊的图片可以点击打开放大,添加图片左右切换;
3. app端答题页也添加分组,同时题干和编号第一行对齐。
4. loading关闭的时机调整;
5. 阅读页样式修改
\ No newline at end of file
import { useEffect, useState } from 'react';
import './App.less';
import { GetroutesDyamic } from '@/routes/index';
import { ErrorBoundary } from '@/common/errorBoundary';
import { ErrorFallback } from '@/common/errorBoundary/ErrorFallback';
import MakeError from '@/common/errorBoundary/error/MakeError';
function App() {
const [token, setToken] = useState('');
useEffect(() => {
const userToken = localStorage.getItem('kiwi.token');
if (userToken) {
setToken(userToken);
} else {
setToken('');
}
}, []);
const [hasError, setHasError] = useState(false);
const onError = (error) => {
// 日志上報
console.log(error);
setHasError(true);
};
const onReset = () => {
console.log('尝试恢复错误');
setHasError(false);
};
return (
<ErrorBoundary
fallbackRender={(fallbackProps) => <ErrorFallback {...fallbackProps} />}
onError={onError}
onReset={onReset}
>
<GetroutesDyamic />
</ErrorBoundary>
);
}
export default App;
html,
body,
a,
h1,
h2,
h3,
h4,
h5,
h6,
p,
strong,
em,
b,
i,
span,
sub,
sup,
dt,
dd,
dl,
div,
table,
tr,
td,
tbody,
thead,
tfoot,
th {
margin: 0;
padding: 0;
}
dl,
dt,
dd {
list-style: none;
}
.ant-table-wrapper .ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-wrapper .ant-table-thead > tr > th {
background-color: #f5f5f5 !important;
}
.ant-table-wrapper .ant-table-thead > tr > th::before {
content: '';
display: none;
}
.ant-table-wrapper .ant-table-tbody tr:nth-child(2n) {
background-color: #fbfbfb;
}
.ant-drawer-right > .ant-drawer-content-wrapper {
width: 25% !important;
min-width: 475px;
}
.ant-modal .ant-modal-close {
inset-inline-end: initial;
}
.submit {
background: #aa1941;
border-radius: 4px;
color: #ffffff;
font-size: 14px;
&:hover {
color: #ffffff !important;
background: #aa194283 !important;
}
}
// .cancel {
// background: #ffffff;
// border-radius: 4px;
// border: 1px solid #aa1941;
// font-size: 14px;
// color: #aa1941;
// &:hover {
// background: #ffffff7c !important;
// border-color: #aa194283 !important;
// color: #aa194283 !important;
// }
// }
.ant-input-number .ant-input-number-input {
text-align: center;
}
.ant-table-wrapper {
border: 1px solid #e5e5e5;
}
.ant-form-inline .ant-form-item {
margin-inline-end: 20px;
}
// .inline,.ant-space-item{
// display: inline-block !important;
// }
.inline {
display: inline-block !important;
}
.ant-space-item {
.ant-btn.css-dev-only-do-not-override-1scgnrh {
display: flex;
align-items: center;
justify-content: center;
}
}
.form-devices-inline {
.ant-form-inline {
.ant-form-item {
margin-bottom: 15px;
}
}
.ant-space {
align-items: flex-start;
}
}
// @font-face {
// font-family: "思源黑体";
// src: url("https://zxts-common-file.zijingebook.com/2024/03/19/video-1710838306427-vhs7ha8w5jj.ttf") format('truetype');
// }
// @font-face {
// font-family: "思源宋体";
// src: url("https://zxts-common-file.zijingebook.com/2024/03/19/video-1710848430762-z2hb59oxz4h.ttf") format('truetype');
// }
// @font-face {
// font-family: "楷体";
// src: url("https://zxts-common-file.zijingebook.com/2024/03/19/video-1710838338781-dxbppd4g6va.ttf") format('truetype');
// }
// @font-face {
// font-family: "仿宋";
// src: url("https://zxts-common-file.zijingebook.com/2024/03/19/video-1710838378832-blsljc5hycd.ttf") format('truetype');
// }
// @font-face {
// font-family: "宋体";
// src: url("https://zxts-common-file.zijingebook.com/2024/03/19/video-1710838324940-8vqbbw0lotx.ttf") format('truetype');
// }
// @font-face {
// font-family: "黑体";
// src: url("https://zxts-common-file.zijingebook.com/2024/03/19/video-1710838356409-40podr4vede.ttf") format('truetype');
// }
@font-face {
font-family: "思源黑体";
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131326219-q886x58lgm.ttf") format('truetype');
}
@font-face {
font-family: "思源宋体";
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131348680-t3miidf3sxs.ttf") format('truetype');
}
@font-face {
font-family: "楷体";
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131272079-k30tgmq5m7.ttf") format('truetype');
}
@font-face {
font-family: "仿宋";
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131169707-0x5pgjxiburq.ttf") format('truetype');
}
@font-face {
font-family: "宋体";
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131298815-mogsisbs1vl.ttf") format('truetype');
}
@font-face {
font-family: "黑体";
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131247994-gp497hgawyb.ttf") format('truetype');
}
// 仿宋 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131169707-0x5pgjxiburq.ttf
// 黑体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131247994-gp497hgawyb.ttf
// 楷体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131272079-k30tgmq5m7.ttf
// 宋体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131298815-mogsisbs1vl.ttf
// 思源黑体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131326219-q886x58lgm.ttf
// 思源宋体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131348680-t3miidf3sxs.ttf
.ant-table-wrapper .ant-table-container table>thead>tr:first-child >*:first-child {
border-start-start-radius: 0;
}
.ant-table-wrapper table, .ant-table-wrapper .ant-table .ant-table-header {
border-radius: 0;
}
.ant-table-wrapper .ant-table-container table>thead>tr:first-child >*:last-child {
border-start-end-radius: 0;
&.ant-table-cell-scrollbar {
background-color: #f5f5f5;
box-shadow: none;
}
}
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1690355161812" class="icon" viewBox="0 0 1466 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2253" width="91.625" height="64" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M535.770344 1017.621782c-51.025747 0-102.051494-19.134655-140.320804-63.782184L19.134655 539.255403C-6.378218 513.742529 0 475.473219 25.512874 449.960345s63.782184-19.134655 89.295057 6.378219l376.314885 414.584195c25.512874 25.512874 63.782184 25.512874 89.295057 6.378218l6.378218-6.378218L1326.669424 22.619714c25.512874-25.512874 63.782184-31.891092 89.295057-6.378219 25.512874 25.512874 31.891092 63.782184 6.378219 89.295058L676.091149 953.839598l-12.756437 12.756437c-38.26931 31.891092-82.916839 51.025747-127.564368 51.025747z" fill="#1677ff" p-id="2254"></path></svg>
\ No newline at end of file
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论