init
This commit is contained in:
commit
4203a8ca37
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.env.*
|
||||||
|
test
|
||||||
|
*test*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
stats.html
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# React + TypeScript + 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
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||||
|
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "quill-test",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.2.6",
|
||||||
|
"@react-three/drei": "^9.92.5",
|
||||||
|
"@readyplayerme/visage": "^4.12.4",
|
||||||
|
"@tiptap/pm": "^2.1.13",
|
||||||
|
"@tiptap/react": "^2.1.13",
|
||||||
|
"@tiptap/starter-kit": "^2.1.13",
|
||||||
|
"antd": "^5.12.4",
|
||||||
|
"re-resizable": "^6.9.11",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-quill": "^2.0.0",
|
||||||
|
"three": "^0.159.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@types/three": "^0.159.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/baifenhao_no_anim.glb
Normal file
BIN
public/baifenhao_no_anim.glb
Normal file
Binary file not shown.
BIN
public/cloud.glb
Normal file
BIN
public/cloud.glb
Normal file
Binary file not shown.
BIN
public/gift.glb
Normal file
BIN
public/gift.glb
Normal file
Binary file not shown.
BIN
public/jinzita.glb
Normal file
BIN
public/jinzita.glb
Normal file
Binary file not shown.
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
137
src/App.css
Normal file
137
src/App.css
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
#root {
|
||||||
|
/* width: 1440px;
|
||||||
|
max-width: 90%; */
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-editor {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-toolbar.ql-snow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
position: relative;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
.ql-container.ql-snow{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mouse-pos {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: #feb03b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container .hilight {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container .hilight-item {
|
||||||
|
position: absolute;
|
||||||
|
--selected-background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container .hilight-item-selected {
|
||||||
|
--selected-background: rgba(254, 176, 59, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container .hilight-item:before {
|
||||||
|
content: "" !important;
|
||||||
|
position: absolute !important;
|
||||||
|
width: 100% !important;
|
||||||
|
left: 0 !important;
|
||||||
|
height: 100% !important;
|
||||||
|
top: 0 !important;
|
||||||
|
background: var(--selected-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container .hilight-item:after {
|
||||||
|
visibility: visible !important;
|
||||||
|
bottom: -2px !important;
|
||||||
|
height: 2px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
content: "" !important;
|
||||||
|
position: absolute !important;
|
||||||
|
width: 100%;
|
||||||
|
left: 0 !important;
|
||||||
|
background: #feb03b;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-wrapper {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar{
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: inherit;
|
||||||
|
word-wrap: normal;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
*{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.editor-resize-demo{
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.container{
|
||||||
|
|
||||||
|
width: 1000px;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 0px auto;
|
||||||
|
}
|
||||||
|
.editor-container{
|
||||||
|
flex: 1;
|
||||||
|
/* padding: 20px 0;
|
||||||
|
background-color: #6ff; */
|
||||||
|
}
|
||||||
|
.editor-container .inner {
|
||||||
|
width: 1000px;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 0px auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.editor-resize-demo .panel {
|
||||||
|
background: #f00;
|
||||||
|
height: 100%;
|
||||||
|
}
|
13
src/App.tsx
Normal file
13
src/App.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Editor from "./views/editor";
|
||||||
|
import './App.css'
|
||||||
|
import './quill.less'
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<Editor />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
30
src/component/editor/delete.ts
Normal file
30
src/component/editor/delete.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import ReactQuill from "react-quill";
|
||||||
|
import { TProofreadItem } from "../../views/types";
|
||||||
|
const Inline = ReactQuill.Quill.import('blots/inline');
|
||||||
|
|
||||||
|
class DeleteParserBlot extends Inline {
|
||||||
|
static tagName = "del";
|
||||||
|
static className = "proodfread-delete-item";
|
||||||
|
static blotName = "delete";
|
||||||
|
|
||||||
|
static create(data: TProofreadItem) {
|
||||||
|
// if(!data.correctedContent){
|
||||||
|
// return super.create(data)
|
||||||
|
// }
|
||||||
|
const node = super.create() as HTMLElement;
|
||||||
|
|
||||||
|
node.setAttribute('data-proofread-id', String(data.id))
|
||||||
|
node.classList.add(`proofread-item-${data.id}`)
|
||||||
|
node.classList.add(`proofread-item`)
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
static formats(node: HTMLElement) {
|
||||||
|
console.log(node.innerText,node.getAttribute('data-proofread-id'),'<--',node.className)
|
||||||
|
return {
|
||||||
|
id: node.getAttribute("data-proofread-id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactQuill.Quill.register(DeleteParserBlot, true);
|
23
src/component/editor/insert.ts
Normal file
23
src/component/editor/insert.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import ReactQuill from "react-quill";
|
||||||
|
const Inline = ReactQuill.Quill.import('blots/inline');
|
||||||
|
|
||||||
|
class InsertParserBlot extends Inline {
|
||||||
|
static tagName = "span";
|
||||||
|
static className = "proodfread-insert-item";
|
||||||
|
static blotName = "insert";
|
||||||
|
static create(data: {origin:string;text:string;}) {
|
||||||
|
const node = super.create() as HTMLElement;
|
||||||
|
node.setAttribute('data-origin',data.origin)
|
||||||
|
node.setAttribute('data-text',data.text)
|
||||||
|
node.innerHTML = ''
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
static formats(node: HTMLElement){
|
||||||
|
return {
|
||||||
|
origin: node.getAttribute("data-origin"),
|
||||||
|
text: node.getAttribute("data-text"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactQuill.Quill.register(InsertParserBlot, true);
|
40
src/component/editor/replace.ts
Normal file
40
src/component/editor/replace.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import ReactQuill from "react-quill";
|
||||||
|
import { TProofreadItem } from "../../views/types";
|
||||||
|
const Inline = ReactQuill.Quill.import('blots/inline');
|
||||||
|
|
||||||
|
class ReplaceParserBlot extends Inline {
|
||||||
|
static tagName = "span";
|
||||||
|
static className = "proofread-item";
|
||||||
|
static blotName = "replace";
|
||||||
|
// static create(data: {origin:string;text:string;}) {
|
||||||
|
// const node = super.create() as HTMLElement;
|
||||||
|
// node.setAttribute('data-origin',data.origin)
|
||||||
|
// node.setAttribute('data-text',data.text)
|
||||||
|
// return node;
|
||||||
|
// }
|
||||||
|
// static formats(node: HTMLElement){
|
||||||
|
// return {
|
||||||
|
// origin: node.getAttribute("data-origin"),
|
||||||
|
// text: node.getAttribute("data-text"),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
static create(data: TProofreadItem) {
|
||||||
|
// if(!data.correctedContent){
|
||||||
|
// return super.create(data)
|
||||||
|
// }
|
||||||
|
const node = super.create() as HTMLElement;
|
||||||
|
|
||||||
|
node.setAttribute('data-proofread-id', String(data.id))
|
||||||
|
node.classList.add(`proofread-item-${data.id}`)
|
||||||
|
node.classList.add(`proofread-${data.action || ''}-item`)
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
static formats(node: HTMLElement) {
|
||||||
|
// console.log(node.innerText, node.getAttribute('data-proofread-id'), '<--', node.className)
|
||||||
|
return {
|
||||||
|
id: Number(node.getAttribute("data-proofread-id")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactQuill.Quill.register(ReplaceParserBlot, true);
|
43
src/index.css
Normal file
43
src/index.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button {
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.4em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #747bffe4;
|
||||||
|
color:#fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button + button{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
background-color: #646cff;
|
||||||
|
} */
|
14
src/main.tsx
Normal file
14
src/main.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import './component/editor/replace.ts'
|
||||||
|
import './component/editor/insert.ts'
|
||||||
|
import './component/editor/delete.ts'
|
||||||
|
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
183
src/mock.ts
Normal file
183
src/mock.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
const resultList = {
|
||||||
|
"code": 0,
|
||||||
|
"msg": "",
|
||||||
|
"data": {
|
||||||
|
"countResp": {
|
||||||
|
"blackWord": 0,
|
||||||
|
"characterError": 7,
|
||||||
|
"wordError": 0,
|
||||||
|
"numberError": 0,
|
||||||
|
"unitsError": 0,
|
||||||
|
"punctuationError": 0,
|
||||||
|
"grammarError": 0,
|
||||||
|
"knowledgeableError": 0,
|
||||||
|
"tendentiousError": 0,
|
||||||
|
"inconsistentError": 0,
|
||||||
|
"formatError": 0,
|
||||||
|
"total": 7,
|
||||||
|
"untreated": 7
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 3333,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "0632d957fbe9c61a1fcfe564b7063930",
|
||||||
|
"text": "威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。",
|
||||||
|
"newText": "威望的增加、权力的扩张丝毫没有改变他原有的样子,他迈步走进岳楼,回忆起在湖南省张家界市的一段往事。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "r",
|
||||||
|
"origin": "北",
|
||||||
|
"text": "南",
|
||||||
|
"start": 38,
|
||||||
|
"end": 39
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 89,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3332,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "0632d957fbe9c61a1fcfe564b7063930",
|
||||||
|
"text": "威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。",
|
||||||
|
"newText": "威望的增加、权力的扩张丝毫没有改变他原有的样子,他迈步走进岳楼,回忆起在湖南省张家界市的一段往事。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "d",
|
||||||
|
"origin": "样",
|
||||||
|
"text": "",
|
||||||
|
"start": 21,
|
||||||
|
"end": 22
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 89,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3331,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "0632d957fbe9c61a1fcfe564b7063930",
|
||||||
|
"text": "威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。",
|
||||||
|
"newText": "威望的增加、权力的扩张丝毫没有改变他原有的样子,他迈步走进岳楼,回忆起在湖南省张家界市的一段往事。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "r",
|
||||||
|
"origin": "利",
|
||||||
|
"text": "力",
|
||||||
|
"start": 7,
|
||||||
|
"end": 8
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 89,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3330,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "1fd42303b7851f8649c7d22285d733db",
|
||||||
|
"text": "原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由\n然而\n生。",
|
||||||
|
"newText": "原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而是生的。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "i",
|
||||||
|
"origin": "",
|
||||||
|
"text": "的",
|
||||||
|
"start": 38,
|
||||||
|
"end": 39
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 49,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3329,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "1fd42303b7851f8649c7d22285d733db",
|
||||||
|
"text": "原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由\n然而\n生。",
|
||||||
|
"newText": "原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而是生的。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "r",
|
||||||
|
"origin": "\n",
|
||||||
|
"text": "是",
|
||||||
|
"start": 37,
|
||||||
|
"end": 38
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 49,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3328,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "1fd42303b7851f8649c7d22285d733db",
|
||||||
|
"text": "原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由\n然而\n生。",
|
||||||
|
"newText": "原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而是生的。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "d",
|
||||||
|
"origin": "\n",
|
||||||
|
"text": "",
|
||||||
|
"start": 34,
|
||||||
|
"end": 35
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 49,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3327,
|
||||||
|
"uid": 8,
|
||||||
|
"fid": 224,
|
||||||
|
"key": "251feba0841e26f461afe044093cb199",
|
||||||
|
"text": "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。",
|
||||||
|
"newText": "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上继往开来,成就了一番伟业。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "r",
|
||||||
|
"origin": "既",
|
||||||
|
"text": "继",
|
||||||
|
"start": 36,
|
||||||
|
"end": 37
|
||||||
|
},
|
||||||
|
"checkTime": 1696667454,
|
||||||
|
"offset": 0,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1696667454,
|
||||||
|
"utime": 1696667454,
|
||||||
|
"dtime": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"current": 1,
|
||||||
|
"pageSize": 1000,
|
||||||
|
"total": 7,
|
||||||
|
"sort": "id desc"
|
||||||
|
}
|
||||||
|
}
|
31
src/quill.less
Normal file
31
src/quill.less
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.editor-container .ql-editor {
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p+p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofread-item {
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
border-bottom: solid 2px #f00;
|
||||||
|
&.selected{
|
||||||
|
background-color: rgba(254, 209, 198, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofread-delete-item {
|
||||||
|
border-bottom: none;
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: #f00;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proofread-insert-item {
|
||||||
|
border-bottom: double 6px #00f;
|
||||||
|
}
|
245
src/views/EditorBasic.tsx
Normal file
245
src/views/EditorBasic.tsx
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import ReactQuill, { Quill } from 'react-quill'
|
||||||
|
import 'react-quill/dist/quill.snow.css'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { TOPS, TProofreadItem } from './types'
|
||||||
|
import { test_content_1 } from './convert'
|
||||||
|
|
||||||
|
const ProofreadList: TProofreadItem[] = [
|
||||||
|
{
|
||||||
|
id: 17009,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
text: "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
newText:
|
||||||
|
"虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "d",
|
||||||
|
origin: "打发",
|
||||||
|
text: "",
|
||||||
|
start: 21,
|
||||||
|
end: 23,
|
||||||
|
},
|
||||||
|
action: "delete",
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 35,
|
||||||
|
isAccept: 2,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17010,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
text: "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
newText:
|
||||||
|
"虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "i",
|
||||||
|
origin: "",
|
||||||
|
text: "赚",
|
||||||
|
start: 29,
|
||||||
|
end: 29,
|
||||||
|
},
|
||||||
|
action: "insert",
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 35,
|
||||||
|
isAccept: 0,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17011,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
text: "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
newText:
|
||||||
|
"虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "i",
|
||||||
|
origin: "",
|
||||||
|
text: "的工作",
|
||||||
|
start: 30,
|
||||||
|
end: 30,
|
||||||
|
},
|
||||||
|
action: "insert",
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 35,
|
||||||
|
isAccept: 2,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17012,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "dbe3ddd3dae993521b00f39c01744695",
|
||||||
|
text: "\n然而,有一天晚上,我在酒吧喝了太多酒,失去失去了意识。",
|
||||||
|
newText: "然而,有一天晚上,我在酒吧喝了太多酒,失去了意识。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "d",
|
||||||
|
origin: "失去",
|
||||||
|
text: "",
|
||||||
|
start: 22,
|
||||||
|
end: 24,
|
||||||
|
},
|
||||||
|
action: "delete",
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 69,
|
||||||
|
isAccept: 0,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17013,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "895f18874c0b473558d49409872c78ea",
|
||||||
|
text: "在酒经的作用下,我变得迷迷糊糊,不知道自己在做什么。",
|
||||||
|
newText: "在酒精的作用下,我变得迷迷糊糊,不知道自己在做什么。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "r",
|
||||||
|
origin: "经",
|
||||||
|
text: "精",
|
||||||
|
start: 2,
|
||||||
|
end: 3,
|
||||||
|
},
|
||||||
|
action: "replace",
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 97,
|
||||||
|
isAccept: 0,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
// 将数组转换成对象
|
||||||
|
const proofreadData: Map<number, TProofreadItem> = new Map();
|
||||||
|
for (let i = 0; i < ProofreadList.length; i++) {
|
||||||
|
proofreadData.set(ProofreadList[i].id, ProofreadList[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: {
|
||||||
|
ops: Array<TOPS>
|
||||||
|
} = {
|
||||||
|
ops: test_content_1
|
||||||
|
}
|
||||||
|
|
||||||
|
const BasicEditor = () => {
|
||||||
|
const quill = useRef<ReactQuill>(null)
|
||||||
|
|
||||||
|
const [data, setData] = useState<{
|
||||||
|
content: string;
|
||||||
|
text?: string;
|
||||||
|
ops: any
|
||||||
|
}>({
|
||||||
|
content: '',
|
||||||
|
text: '',
|
||||||
|
ops: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const setContent = () => {
|
||||||
|
const editor = quill.current?.editor;
|
||||||
|
if (!editor) return;
|
||||||
|
editor.setContents(content as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContent = () => {
|
||||||
|
const editor = quill.current?.editor;
|
||||||
|
if (!editor) return;
|
||||||
|
const content = editor.getContents()
|
||||||
|
content.ops?.forEach((it) => {
|
||||||
|
// const proofread = ;
|
||||||
|
if (it.attributes?.delete && it.attributes.delete.id) {
|
||||||
|
const proofread = proofreadData.get(Number(it.attributes.delete.id));
|
||||||
|
if (proofread) {
|
||||||
|
console.log('delete proofread data', proofread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(
|
||||||
|
content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let prevSelectNode: HTMLSpanElement | null = null;
|
||||||
|
const handleEditorClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const node = e.target as HTMLSpanElement;
|
||||||
|
if (!node || !node.classList.contains('proofread-item') || !node.getAttribute('data-proofread-id') || prevSelectNode == node) return;
|
||||||
|
prevSelectNode?.classList.remove('selected')
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
node.classList.add('selected')
|
||||||
|
prevSelectNode = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCursor = () => {
|
||||||
|
if(!quill.current) return;
|
||||||
|
const node = document.querySelector('.proofread-item-170')
|
||||||
|
if(!node) return;
|
||||||
|
|
||||||
|
node.setAttribute('data-result', '~~~');
|
||||||
|
node.innerHTML = '<del></del>';
|
||||||
|
// node.appendChild(document.createElement('del'));
|
||||||
|
const editor = quill.current.editor!;
|
||||||
|
const b = Quill.find(node);
|
||||||
|
var i = editor.getIndex(b);
|
||||||
|
// console.log(i)
|
||||||
|
editor?.setSelection(i, 0, 'silent')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container'>
|
||||||
|
<div className='editor-container' style={{ width: '100%', marginTop: 10 }} onClick={handleEditorClick}>
|
||||||
|
<ReactQuill
|
||||||
|
ref={quill}
|
||||||
|
theme='snow'
|
||||||
|
modules={{ toolbar: false }}
|
||||||
|
onChangeSelection={(selection) => {
|
||||||
|
if (!selection) return;
|
||||||
|
// handleEditorSelectionChange(selection.index)
|
||||||
|
// console.log(
|
||||||
|
// editor.getBounds(selection!.index)
|
||||||
|
// )
|
||||||
|
}}
|
||||||
|
onChange={(content, _delta, source) => {
|
||||||
|
// console.log('source', source, _delta)
|
||||||
|
const ops = quill.current!.editor!.getContents().ops
|
||||||
|
let originText = data.text || ''
|
||||||
|
let text = quill.current?.editor?.getText() || '';
|
||||||
|
setData({
|
||||||
|
content,
|
||||||
|
text,
|
||||||
|
ops
|
||||||
|
})
|
||||||
|
// if (source === 'user') {
|
||||||
|
// handleEditorTextChange(text, originText)
|
||||||
|
// }
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
{/* <button onClick={origin}>origin</button>
|
||||||
|
<button onClick={handleProcess}>process</button>
|
||||||
|
<button onClick={testContentEdit}>testContentEdit</button> */}
|
||||||
|
<div style={{ margin: '10px 0' }}>
|
||||||
|
<button onClick={getContent}>test</button>
|
||||||
|
<button onClick={setContent}>setContent</button>
|
||||||
|
<button onClick={setCursor}>setCursor</button>
|
||||||
|
</div>
|
||||||
|
<div className="code-wrapper">
|
||||||
|
<div>{JSON.stringify(data, null, '\t')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BasicEditor
|
30
src/views/ResizeDemo.tsx
Normal file
30
src/views/ResizeDemo.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Resizable } from 're-resizable';
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
const ResizeDemo: React.FC = () => {
|
||||||
|
const [width, setWidth] = React.useState(320);
|
||||||
|
return <div className="editor-resize-demo">
|
||||||
|
<div className="editor-container">
|
||||||
|
<div className="inner"></div>
|
||||||
|
</div>
|
||||||
|
<Resizable
|
||||||
|
defaultSize={{
|
||||||
|
width: width,
|
||||||
|
height: '100%'
|
||||||
|
|
||||||
|
}}
|
||||||
|
minWidth={320}
|
||||||
|
onResizeStop={(e, direction, ref, d) => {
|
||||||
|
console.log(
|
||||||
|
e, direction, ref, d
|
||||||
|
)
|
||||||
|
setWidth(width + d.width);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="panel"></div>
|
||||||
|
</Resizable>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResizeDemo;
|
178
src/views/compare-left.ts
Normal file
178
src/views/compare-left.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
export const compare_left = [
|
||||||
|
{
|
||||||
|
"id": 18088,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
"text": "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
"newText": "虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "d",
|
||||||
|
"origin": "不错",
|
||||||
|
"text": "",
|
||||||
|
"start": 21,
|
||||||
|
"end": 23
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 35,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18089,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
"text": "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
"newText": "虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "i",
|
||||||
|
"origin": "",
|
||||||
|
"text": "赚",
|
||||||
|
"start": 31,
|
||||||
|
"end": 31
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 35,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18090,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
"text": "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
"newText": "虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "i",
|
||||||
|
"origin": "",
|
||||||
|
"text": "的工作",
|
||||||
|
"start": 32,
|
||||||
|
"end": 32
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 35,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18091,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "dbe3ddd3dae993521b00f39c01744695",
|
||||||
|
"text": "\n然而,有一天晚上,我在酒吧喝了太多酒,失去失去了意识。",
|
||||||
|
"newText": "然而,有一天晚上,我在酒吧喝了太多酒,失去了意识。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "d",
|
||||||
|
"origin": "失去",
|
||||||
|
"text": "",
|
||||||
|
"start": 22,
|
||||||
|
"end": 24
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 68,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18092,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "895f18874c0b473558d49409872c78ea",
|
||||||
|
"text": "在酒经的作用下,我变得迷迷糊糊,不知道自己在做什么。",
|
||||||
|
"newText": "在酒精的作用下,我变得迷迷糊糊,不知道自己在做什么。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "r",
|
||||||
|
"origin": "经",
|
||||||
|
"text": "精",
|
||||||
|
"start": 2,
|
||||||
|
"end": 3
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 96,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18093,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "187d12e64879f6c2d42cba2982d50e30",
|
||||||
|
"text": "我努力回地忆着昨晚的事情,但是脑海中一片空白。",
|
||||||
|
"newText": "我努力地回忆着昨晚的事情,但是脑海中一片空白。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "i",
|
||||||
|
"origin": "",
|
||||||
|
"text": "地",
|
||||||
|
"start": 3,
|
||||||
|
"end": 3
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 175,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18094,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "187d12e64879f6c2d42cba2982d50e30",
|
||||||
|
"text": "我努力回地忆着昨晚的事情,但是脑海中一片空白。",
|
||||||
|
"newText": "我努力地回忆着昨晚的事情,但是脑海中一片空白。",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "d",
|
||||||
|
"origin": "地",
|
||||||
|
"text": "",
|
||||||
|
"start": 4,
|
||||||
|
"end": 5
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 175,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18095,
|
||||||
|
"uid": 3,
|
||||||
|
"fid": 13,
|
||||||
|
"key": "84849fdc3e4e5b81afa9254526551679",
|
||||||
|
"text": "我意识可能做了一些蠢事,但又不敢去想象",
|
||||||
|
"newText": "我意识到可能做了一些蠢事,但又不敢去想象",
|
||||||
|
"correctedContent": {
|
||||||
|
"tag": "i",
|
||||||
|
"origin": "",
|
||||||
|
"text": "到",
|
||||||
|
"start": 3,
|
||||||
|
"end": 3
|
||||||
|
},
|
||||||
|
"checkTime": 1697361613,
|
||||||
|
"offset": 198,
|
||||||
|
"isAccept": 0,
|
||||||
|
"type": 2,
|
||||||
|
"ctime": 1697361613,
|
||||||
|
"utime": 1697361613,
|
||||||
|
"dtime": 0
|
||||||
|
}
|
||||||
|
]
|
169
src/views/convert.ts
Normal file
169
src/views/convert.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { TOPS } from "./types";
|
||||||
|
|
||||||
|
export const test_content_1: TOPS[] = [
|
||||||
|
{
|
||||||
|
attributes: {},
|
||||||
|
insert:
|
||||||
|
"失业在家的日子过得很无聊,为了打发时间,我决定给我妈朋友的儿子当家教。虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
replace: {
|
||||||
|
id: 17010,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
text: "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
newText:
|
||||||
|
"虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "i",
|
||||||
|
origin: "赚",
|
||||||
|
text: "赚",
|
||||||
|
start: 29,
|
||||||
|
end: 29,
|
||||||
|
},
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 35,
|
||||||
|
isAccept: 0,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
action: "insert",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
insert: "赚",
|
||||||
|
},
|
||||||
|
{ attributes: {}, insert: "钱" },
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
replace: {
|
||||||
|
id: 17011,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
text: "虽然我没有相关的教学经验,但我觉得这是一个不错锻炼自己,并且能钱。",
|
||||||
|
newText:
|
||||||
|
"虽然我没有相关的教学经验,但我觉得这是一个锻炼自己,并且能赚钱的工作。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "i",
|
||||||
|
origin: "的工作",
|
||||||
|
text: "的工作",
|
||||||
|
start: 30,
|
||||||
|
end: 30,
|
||||||
|
},
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 35,
|
||||||
|
isAccept: 2,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
action: "insert",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
insert: "的工作",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
insert:'~~~',
|
||||||
|
attributes:{
|
||||||
|
delete:{
|
||||||
|
id: 170,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "5aa5de13391f8038c55ab2174f13bb84",
|
||||||
|
text: "",
|
||||||
|
newText:"",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "d",
|
||||||
|
origin: "~~~",
|
||||||
|
text: "",
|
||||||
|
start: 30,
|
||||||
|
end: 30,
|
||||||
|
},
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 35,
|
||||||
|
isAccept: 2,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
action: "delete",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {},
|
||||||
|
insert: "。\n然而,有一天晚上,我在酒吧喝了太多酒,失去失",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
replace: {
|
||||||
|
id: 17012,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "dbe3ddd3dae993521b00f39c01744695",
|
||||||
|
text: "\\n然而,有一天晚上,我在酒吧喝了太多酒,失去失去了意识。",
|
||||||
|
newText: "然而,有一天晚上,我在酒吧喝了太多酒,失去了意识。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "d",
|
||||||
|
origin: "失去",
|
||||||
|
text: "",
|
||||||
|
start: 22,
|
||||||
|
end: 24,
|
||||||
|
},
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 69,
|
||||||
|
isAccept: 0,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
action: "delete",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
insert: "失去",
|
||||||
|
},
|
||||||
|
{ attributes: {}, insert: "意识。在酒经" },
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
replace: {
|
||||||
|
id: 17013,
|
||||||
|
uid: 3,
|
||||||
|
fid: 290,
|
||||||
|
key: "895f18874c0b473558d49409872c78ea",
|
||||||
|
text: "在酒经的作用下,我变得迷迷糊糊,不知道自己在做什么。",
|
||||||
|
newText: "在酒精的作用下,我变得迷迷糊糊,不知道自己在做什么。",
|
||||||
|
correctedContent: {
|
||||||
|
tag: "r",
|
||||||
|
origin: "经",
|
||||||
|
text: "精",
|
||||||
|
start: 2,
|
||||||
|
end: 3,
|
||||||
|
},
|
||||||
|
checkTime: 1697186287,
|
||||||
|
offset: 97,
|
||||||
|
isAccept: 0,
|
||||||
|
type: 2,
|
||||||
|
ctime: 1697186287,
|
||||||
|
utime: 1697199789,
|
||||||
|
dtime: 0,
|
||||||
|
action: "replace",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
insert: "经",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {},
|
||||||
|
insert:"作用下,我变得迷迷糊糊,不知道自己在做什么。当我醒来时,我发现自己竟然睡在了他家的客房里。\n",
|
||||||
|
},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
{insert:"当我醒来时,我发现自己竟然睡在了他家的客房里。\n",},
|
||||||
|
];
|
494
src/views/editor/index.tsx
Normal file
494
src/views/editor/index.tsx
Normal file
@ -0,0 +1,494 @@
|
|||||||
|
import ReactQuill from 'react-quill'
|
||||||
|
import 'react-quill/dist/quill.snow.css'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { TOPS } from '../types'
|
||||||
|
import { Button, Dropdown, MenuProps, Space } from 'antd'
|
||||||
|
|
||||||
|
type TError = {
|
||||||
|
id: number;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
type ErrorPosition = {
|
||||||
|
index: number;
|
||||||
|
error?: TError;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
const content: {
|
||||||
|
ops: Array<TOPS>
|
||||||
|
} = {
|
||||||
|
ops: [
|
||||||
|
{
|
||||||
|
insert: "hello "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
bold: true
|
||||||
|
},
|
||||||
|
insert: "world"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
content.ops = [
|
||||||
|
{
|
||||||
|
insert: "该摄"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
insert: {
|
||||||
|
origin: '',
|
||||||
|
text: '像'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insert: '像'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
bold: true,
|
||||||
|
italic: true
|
||||||
|
},
|
||||||
|
insert: "头头"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
insert: "可以在单"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
bold: true,
|
||||||
|
replace: {
|
||||||
|
origin: '次爆',
|
||||||
|
text: '次爆爆'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insert: "次爆"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
italic: true
|
||||||
|
},
|
||||||
|
insert: "光中以每秒"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": " 480 万帧的速度采集动态的事件\n的图像。\n\n采集快速运动过程(如落下的水滴或分子相互作用)的 青晰 图像,我们需要使用昂贵的超快摄像头(每秒可采集数百万张图像)。据国外媒报道,加拿大国立科学研究院(INRS)宣布开发出新摄像头,能够以极低的成本实现超快速成像,并"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
bold: true
|
||||||
|
},
|
||||||
|
insert: "适合"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributes: {
|
||||||
|
italic: true,
|
||||||
|
bold: true
|
||||||
|
},
|
||||||
|
insert: "多种(many)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
insert: "应用,例如实时监测药物输送或用于自动驾驶的高速激光雷达系统。\n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误列表
|
||||||
|
*/
|
||||||
|
const ResultErrorList = [
|
||||||
|
{ offset: 4, length: 1, id: 1, },
|
||||||
|
{ offset: 10, length: 2, id: 2 },
|
||||||
|
{ offset: 28, length: 1, id: 3 },
|
||||||
|
{ offset: 30, length: 6, id: 4 },
|
||||||
|
{ offset: 124, length: 7, id: 5 },
|
||||||
|
]
|
||||||
|
function Editor() {
|
||||||
|
const quill = useRef<ReactQuill>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
const editor = quill.current?.editor;
|
||||||
|
if (!editor) return;
|
||||||
|
editor.setContents(content as any)
|
||||||
|
}, [quill])
|
||||||
|
const [data, setData] = useState<{
|
||||||
|
content: string;
|
||||||
|
text?: string;
|
||||||
|
ops: any
|
||||||
|
}>({
|
||||||
|
content: '',
|
||||||
|
text: '',
|
||||||
|
ops: []
|
||||||
|
})
|
||||||
|
// const [state, setState] = useState<{
|
||||||
|
// bottom?: number;
|
||||||
|
// height?: number;
|
||||||
|
// left?: number;
|
||||||
|
// right?: number;
|
||||||
|
// top?: number;
|
||||||
|
// width?: number;
|
||||||
|
// }>({})
|
||||||
|
const offset = 10, length = 2
|
||||||
|
const [editor, setEditor] = useState<{
|
||||||
|
div?: HTMLDivElement,
|
||||||
|
info: {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
top: number;
|
||||||
|
}
|
||||||
|
}>({
|
||||||
|
info: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const getAllChildNodes = (node: ChildNode) => {
|
||||||
|
const childNodes = node.childNodes;
|
||||||
|
if (childNodes.length > 0) {
|
||||||
|
const _: any = [];
|
||||||
|
for (let i = 0; i < childNodes.length; i++) {
|
||||||
|
_.push(getAllChildNodes(childNodes[i]))
|
||||||
|
}
|
||||||
|
return _
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
// 根据位置获得节点索引
|
||||||
|
const getNodeIndexByOffsetAndLength = (lengthList: number[], offset: number, length: number) => {
|
||||||
|
const posList: number[][] = []
|
||||||
|
// 开始范围位置
|
||||||
|
let startPos = 0
|
||||||
|
// 结束范围位置
|
||||||
|
let endPos = 0;
|
||||||
|
for (let i = 0; i < lengthList.length; i++) {
|
||||||
|
if (lengthList[i] === 0) continue; // 跳过空
|
||||||
|
endPos = startPos + lengthList[i];
|
||||||
|
//1.完全在该长度范围内容
|
||||||
|
if (offset >= startPos && (offset + length) < endPos) {
|
||||||
|
posList.push([i, offset - startPos, length])
|
||||||
|
return posList
|
||||||
|
}
|
||||||
|
//2. 开始在范围内,结束没有在范围内
|
||||||
|
if (offset >= startPos && offset < endPos && (offset + length) >= endPos) {
|
||||||
|
const curLen = endPos - offset
|
||||||
|
posList.push([i, offset - startPos, curLen])
|
||||||
|
// 剩余长度减去已经处理的值
|
||||||
|
length = length - curLen;
|
||||||
|
if (length === 0) return posList;
|
||||||
|
// 开始位置为当前位置结束 + 1
|
||||||
|
offset = endPos;
|
||||||
|
}
|
||||||
|
startPos = endPos;
|
||||||
|
}
|
||||||
|
return posList
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcRectInfo = (rect: DOMRect) => {
|
||||||
|
return {
|
||||||
|
left: rect.left - editor.info.left,
|
||||||
|
top: rect.top - editor.info.top,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getBounds = (textNodes: Node[], pos: number[][], error: TError) => {
|
||||||
|
const bounds: ErrorPosition[] = []
|
||||||
|
for (let i = 0; i < pos.length; i++) {
|
||||||
|
const [index, offset, length] = pos[i]
|
||||||
|
const node = textNodes[index]
|
||||||
|
// start range
|
||||||
|
let rangeStart: Range | null = document.createRange();
|
||||||
|
rangeStart.setStart(node, 0);
|
||||||
|
rangeStart.setEnd(node, 0);
|
||||||
|
// start height
|
||||||
|
const startHeight = rangeStart.getBoundingClientRect().height;
|
||||||
|
rangeStart.detach()
|
||||||
|
rangeStart = null;
|
||||||
|
// content range
|
||||||
|
let range: Range | null = document.createRange()
|
||||||
|
range.setStart(node, offset)
|
||||||
|
range.setEnd(node, offset + length)
|
||||||
|
const rect = calcRectInfo(range.getBoundingClientRect())
|
||||||
|
|
||||||
|
range.detach();
|
||||||
|
range = null;
|
||||||
|
if (Math.floor(rect.height) == Math.floor(startHeight)) { // start equal end so 在一行了啦
|
||||||
|
bounds.push({
|
||||||
|
index,
|
||||||
|
error,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
...rect
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// not in same line, calculate each char in string
|
||||||
|
const highlights: any[] = [];
|
||||||
|
let start = 0, count = 1;
|
||||||
|
while (count <= length) { //
|
||||||
|
let strRange: Range | null = document.createRange()
|
||||||
|
strRange.setStart(node, offset + start)
|
||||||
|
strRange.setEnd(node, offset + count)
|
||||||
|
const rect = calcRectInfo(strRange.getBoundingClientRect());
|
||||||
|
strRange.detach();
|
||||||
|
strRange = null;
|
||||||
|
if (Math.floor(rect.height) == Math.floor(startHeight)) { // in first line
|
||||||
|
if (count !== 1) highlights.pop();
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
if (start == count - 1) {
|
||||||
|
highlights.pop();
|
||||||
|
start = count;
|
||||||
|
count -= 1;
|
||||||
|
} else {
|
||||||
|
start = count - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
highlights.push({
|
||||||
|
index: start,
|
||||||
|
error,
|
||||||
|
offset: offset + start,
|
||||||
|
length: count,
|
||||||
|
...rect
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bounds.push(...highlights)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
const [errPos, setErrPos] = useState<ErrorPosition[]>([])
|
||||||
|
const getAllTextNodes = () => {
|
||||||
|
const qcs = editor.div!.childNodes;
|
||||||
|
const qcList = [];
|
||||||
|
for (let i = 0; i < qcs.length; i++) {
|
||||||
|
qcList.push(getAllChildNodes(qcs[i]))
|
||||||
|
}
|
||||||
|
return qcList.flat(10) as Node[];
|
||||||
|
}
|
||||||
|
const origin = () => {
|
||||||
|
const nodeList = getAllTextNodes();
|
||||||
|
const lenList: number[] = []
|
||||||
|
nodeList.forEach(node => {
|
||||||
|
if (node.nodeName === '#text') {
|
||||||
|
lenList.push((node as Text).length)
|
||||||
|
} else {
|
||||||
|
lenList.push(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const bounds: ErrorPosition[][] = [];
|
||||||
|
ResultErrorList.forEach(err => {
|
||||||
|
if (ErrorData.find(s => s.id == err.id)?.processed) return;
|
||||||
|
const posList = getNodeIndexByOffsetAndLength(
|
||||||
|
lenList,
|
||||||
|
err.offset,
|
||||||
|
err.length
|
||||||
|
)
|
||||||
|
// posList.forEach(p => {
|
||||||
|
// const [index, offset, length] = p;
|
||||||
|
// console.log(nodeList[index], p, nodeList[index].textContent?.slice(offset, offset + length))
|
||||||
|
// })
|
||||||
|
bounds.push(getBounds(nodeList, posList, err))
|
||||||
|
})
|
||||||
|
// const bounds = getBounds(nodeList, lenList)
|
||||||
|
setErrPos(bounds.flat(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const div = document.querySelector<HTMLDivElement>('.ql-editor')!
|
||||||
|
setEditor({
|
||||||
|
div,
|
||||||
|
info: div.getBoundingClientRect(),
|
||||||
|
})
|
||||||
|
}, [quill])
|
||||||
|
const [selectId, setSelectId] = useState<number>(-1)
|
||||||
|
const handleEditorSelectionChange = (index: number) => {
|
||||||
|
const editor = quill.current?.editor!;
|
||||||
|
const info = editor.getBounds(index)
|
||||||
|
console.log('editor change info', info)
|
||||||
|
let id = -1;
|
||||||
|
errPos.forEach(err => {
|
||||||
|
// 判断是否在范围内
|
||||||
|
if (
|
||||||
|
info.left >= err.left && info.left <= (err.left + err.width)
|
||||||
|
&& info.top >= err.top && info.top <= (err.top + err.height)
|
||||||
|
) {
|
||||||
|
id = err.error?.id || -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setSelectId(id)
|
||||||
|
}
|
||||||
|
const handleEditorTextChange = (newText: string, originText: string) => {
|
||||||
|
const range = window.getSelection()?.getRangeAt(0)
|
||||||
|
if (!range) return;
|
||||||
|
originText = originText.replace(/ /gi, " ");
|
||||||
|
newText = newText.replace(/ /gi, " ");
|
||||||
|
const count = newText.length - originText.length;
|
||||||
|
const nodeList = getAllTextNodes();
|
||||||
|
let startOffset = 0;
|
||||||
|
// 计算偏移量
|
||||||
|
for (let node of nodeList) {
|
||||||
|
if (range.startContainer == node) {
|
||||||
|
// 找到该节点就不计算后续的了
|
||||||
|
startOffset += range.startOffset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (node.nodeName === '#text') startOffset += (node as Text).length
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(ResultErrorList))
|
||||||
|
// 更新位置
|
||||||
|
for (let i = 0; i < ResultErrorList.length; i++) {
|
||||||
|
// 判断要更新的位置是否在修改位置后面
|
||||||
|
if (ResultErrorList[i].offset >= startOffset - count) {
|
||||||
|
ResultErrorList[i].offset += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify(ResultErrorList))
|
||||||
|
origin()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorData = [
|
||||||
|
{ id: 1, tag: 'd', origin: '头', text: '', processed: false },
|
||||||
|
{ id: 2, tag: 'd', origin: '爆光', text: '曝光', processed: false },
|
||||||
|
{ id: 3, tag: 'r', origin: '动', text: '洞', processed: false },
|
||||||
|
{ id: 4, tag: 'd', origin: '动', text: '洞', processed: false },
|
||||||
|
]
|
||||||
|
const handleProcess = () => {
|
||||||
|
console.log('selectId', selectId)
|
||||||
|
const err = ErrorData.find(item => item.id === selectId)
|
||||||
|
if (!err) return;
|
||||||
|
const nodeList = getAllTextNodes();
|
||||||
|
const processList = errPos.filter(s => s.error?.id == selectId)
|
||||||
|
err.processed = true;
|
||||||
|
processList.forEach(it => {
|
||||||
|
const node = nodeList[it.index] as Text;
|
||||||
|
const content = node.textContent!
|
||||||
|
//
|
||||||
|
// console.log(it, nodeList[it.index])
|
||||||
|
if (err.tag == 'd') {
|
||||||
|
// 删除
|
||||||
|
node.textContent = content.slice(0, it.offset) + content.slice(it.offset + it.length)
|
||||||
|
} else if (err.tag == 'r') { // 替换
|
||||||
|
console.log(node.textContent!.slice(0, it.offset), node.textContent!.slice(it.offset + it.length))
|
||||||
|
node.textContent = node.textContent!.slice(0, it.offset) + err.text + node.textContent!.slice(it.offset + it.length)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// origin();
|
||||||
|
}
|
||||||
|
const test = () => {
|
||||||
|
const editor = quill.current?.editor!;
|
||||||
|
// editor.insertText(pos[0], 'hello world')
|
||||||
|
// const bounds = editor.getBounds(pos[0], pos[1])
|
||||||
|
// console.log(bounds)
|
||||||
|
// setState(bounds)
|
||||||
|
// editor.setSelection(pos[0], pos[1])
|
||||||
|
editor.deleteText(offset, length)
|
||||||
|
editor.insertText(offset, '爆光', {
|
||||||
|
replace: {
|
||||||
|
origin: '爆光',
|
||||||
|
text: '曝光'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const testContentEdit = () => {
|
||||||
|
const spans = document.querySelectorAll('.proofread-replace-item')
|
||||||
|
Array.from(spans).forEach((node) => {
|
||||||
|
console.log(node.textContent, node)
|
||||||
|
const replacedText = node.getAttribute('data-text')
|
||||||
|
if (replacedText) {
|
||||||
|
//(node as HTMLElement).innerText = replacedText
|
||||||
|
node.textContent = replacedText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(quill.current!.editor!.getText())
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(quill.current!.editor!.getText())
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
const items: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
label: <a href="https://www.antgroup.com">1st menu item</a>,
|
||||||
|
key: '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <a href="https://www.aliyun.com">2nd menu item</a>,
|
||||||
|
key: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '3rd menu item',
|
||||||
|
key: '3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space>
|
||||||
|
<Button type='primary' onClick={origin}>origin</Button>
|
||||||
|
<Button onClick={handleProcess}>process</Button>
|
||||||
|
<Button onClick={test}>test</Button>
|
||||||
|
<Button onClick={testContentEdit}>testContentEdit</Button>
|
||||||
|
</Space>
|
||||||
|
{/* <p>该摄像<strong><em>头头</em></strong>可以在单<i>次爆</i></p> */}
|
||||||
|
<div className='editor-container' style={{ width: '100%', marginTop: 10 }}>
|
||||||
|
<ReactQuill
|
||||||
|
ref={quill}
|
||||||
|
theme='snow'
|
||||||
|
modules={{ toolbar: false }}
|
||||||
|
onChangeSelection={(selection) => {
|
||||||
|
if (!selection) return;
|
||||||
|
handleEditorSelectionChange(selection.index)
|
||||||
|
// console.log(
|
||||||
|
// editor.getBounds(selection!.index)
|
||||||
|
// )
|
||||||
|
}}
|
||||||
|
onChange={(content, _delta, source) => {
|
||||||
|
// console.log('source', source, _delta)
|
||||||
|
const ops = quill.current!.editor!.getContents().ops
|
||||||
|
let originText = data.text || ''
|
||||||
|
let text = quill.current?.editor?.getText() || '';
|
||||||
|
setData({
|
||||||
|
content,
|
||||||
|
text,
|
||||||
|
ops
|
||||||
|
})
|
||||||
|
if (source === 'user') {
|
||||||
|
handleEditorTextChange(text, originText)
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<div className="hilight">
|
||||||
|
{errPos.map((err, errIndex) => <Dropdown open={err.error && err.error?.id == selectId ? true : false} key={errIndex} menu={{ items }}>
|
||||||
|
<div data-error-id={err.error?.id} className={`hilight-item ${err.error?.id == selectId ? 'hilight-item-selected' : ''}`} style={{
|
||||||
|
left: err.left || 0,
|
||||||
|
top: err.top || 0,
|
||||||
|
width: err.width || 0,
|
||||||
|
height: err.height || 0,
|
||||||
|
}}></div>
|
||||||
|
</Dropdown>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="code-wrapper">
|
||||||
|
<pre>{JSON.stringify(data, null, ' ')}</pre>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Editor
|
13
src/views/glb/index.tsx
Normal file
13
src/views/glb/index.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import GLTFViewer from "./viewer";
|
||||||
|
|
||||||
|
|
||||||
|
const ViewerDemo: React.FC = () => {
|
||||||
|
return (<div>
|
||||||
|
<div><GLTFViewer gltfUrl="/cloud.glb" /></div>
|
||||||
|
{/* <div><GLTFViewer gltfUrl="/gift.glb" /></div>
|
||||||
|
<div><GLTFViewer gltfUrl="/jinzita.glb" /></div> */}
|
||||||
|
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
}
|
||||||
|
export default ViewerDemo;
|
72
src/views/glb/viewer/index.tsx
Normal file
72
src/views/glb/viewer/index.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||||
|
|
||||||
|
export type GLTFViewerProp = {
|
||||||
|
gltfUrl: string;
|
||||||
|
scale?: number;
|
||||||
|
position?: [number, number, number];
|
||||||
|
rotation?: [number, number, number];
|
||||||
|
onLoad?: () => void;
|
||||||
|
onError?: () => void;
|
||||||
|
autoPlay?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
const GLTFViewer: React.FC<GLTFViewerProp> = (props) => {
|
||||||
|
const { gltfUrl, scale = 1, position = [0, 0, 0], rotation = [0, 0, 0], width = 500, height = 400, autoPlay = true } = props;
|
||||||
|
const viewer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scence = new THREE.Scene();
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
|
||||||
|
//光源
|
||||||
|
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
|
||||||
|
scence.add(ambient);
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);//光源,color:灯光颜色,intensity:光照强度
|
||||||
|
directionalLight.position.set(400, 200, 300);
|
||||||
|
scence.add(directionalLight);
|
||||||
|
//设置相机位置
|
||||||
|
camera.position.set(-10, 100, 10);
|
||||||
|
//设置相机方向
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
//辅助坐标轴
|
||||||
|
// const axesHelper = new THREE.AxesHelper(200);//参数200标示坐标系大小,可以根据场景大小去设置
|
||||||
|
// scence.add(axesHelper);
|
||||||
|
scence.background = new THREE.Color(0xeaeaea);
|
||||||
|
//
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
|
||||||
|
const render = ()=>{
|
||||||
|
renderer.render(scence, camera);
|
||||||
|
controls.update();
|
||||||
|
requestAnimationFrame(render);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewer.current) {
|
||||||
|
viewer.current.appendChild(renderer.domElement);
|
||||||
|
// renderer.render(scence, camera);
|
||||||
|
loader.load(gltfUrl, (gltf) => {
|
||||||
|
// console.log(gltf)
|
||||||
|
gltf.scene.scale.set(5, 5, 5);//设置模型大小
|
||||||
|
scence.add(gltf.scene);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [viewer])
|
||||||
|
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<h1>{gltfUrl}</h1>
|
||||||
|
<div ref={viewer} style={{ width, height }}></div>
|
||||||
|
</>)
|
||||||
|
|
||||||
|
}
|
||||||
|
export default GLTFViewer;
|
||||||
|
|
||||||
|
// import { useGLTF } from '@react-three/drei'
|
50
src/views/tiptap/editor.css
Normal file
50
src/views/tiptap/editor.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.tiptap-container {
|
||||||
|
border: solid 1px #eee;
|
||||||
|
}
|
||||||
|
.tiptap-container .toolbars {
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: solid 1px #eee;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.tiptap-container .toolbars button {
|
||||||
|
border: solid 1px transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: none;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.tiptap-container .toolbars button:hover,
|
||||||
|
.tiptap-container .toolbars button.is-active {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
.tiptap-editor .tiptap {
|
||||||
|
outline: none;
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.tiptap-editor p {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.tiptap-editor ul,
|
||||||
|
.tiptap-editor ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.tiptap-editor blockquote {
|
||||||
|
border-left: solid 4px #999;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.tiptap-editor code {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #999;
|
||||||
|
padding: 15px;
|
||||||
|
display: block;
|
||||||
|
color: #000;
|
||||||
|
}
|
52
src/views/tiptap/editor.less
Normal file
52
src/views/tiptap/editor.less
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
.tiptap-container{
|
||||||
|
border: solid 1px #eee;
|
||||||
|
.toolbars{
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: solid 1px #eee;
|
||||||
|
padding: 10px;
|
||||||
|
button{
|
||||||
|
border:solid 1px transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
background:none;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
&:hover,&.is-active{
|
||||||
|
background-color:#efefef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tiptap-editor{
|
||||||
|
|
||||||
|
.tiptap{
|
||||||
|
outline: none;
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
ul,ol{
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
blockquote{
|
||||||
|
border-left:solid 4px #999;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code{
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #999;
|
||||||
|
padding: 15px;
|
||||||
|
display: block;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
88
src/views/tiptap/exts/indents.ts
Normal file
88
src/views/tiptap/exts/indents.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Command, Extension } from '@tiptap/core';
|
||||||
|
import { Transaction } from '@tiptap/pm/state';
|
||||||
|
|
||||||
|
export interface IndentOptions {
|
||||||
|
types: string[];
|
||||||
|
minLevel: number;
|
||||||
|
maxLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands {
|
||||||
|
indent: {
|
||||||
|
indent: () => Command;
|
||||||
|
outdent: () => Command;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置节点缩进
|
||||||
|
function setNodeIndent(tr: Transaction, delta: number) {
|
||||||
|
if (!tr.doc || !tr.selection) return tr;
|
||||||
|
|
||||||
|
// if(node.type.name === 'paragraph'){
|
||||||
|
// node.attrs.indent = node.attrs.indent + delta;
|
||||||
|
// }
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Indent = Extension.create<IndentOptions>({
|
||||||
|
name: 'indent',
|
||||||
|
|
||||||
|
defaultOptions: {
|
||||||
|
types: ['paragraph'],
|
||||||
|
minLevel: 1,
|
||||||
|
maxLevel: 8,
|
||||||
|
},
|
||||||
|
addGlobalAttributes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
types: this.options.types,
|
||||||
|
attributes: {
|
||||||
|
indent: {
|
||||||
|
default: 0,
|
||||||
|
parseHTML: element => {
|
||||||
|
const { level } = element.dataset;
|
||||||
|
return level ? Number(level) : 0;
|
||||||
|
},
|
||||||
|
renderHTML: attributes => {
|
||||||
|
return {
|
||||||
|
'data-indent': attributes.indent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
const applyIndent = (delta: number) => ({ tr, state, dispatch }) => {
|
||||||
|
const { selection } = state;
|
||||||
|
tr = tr.setSelection(selection);
|
||||||
|
tr = setNodeIndent(tr, delta)
|
||||||
|
if (tr.docChanged) {
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
dispatch && dispatch(tr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
indent: () => applyIndent(1),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outdent a paragraph.
|
||||||
|
*/
|
||||||
|
outdent: () => applyIndent(-1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Tab: () => {
|
||||||
|
this.editor.commands.indent();
|
||||||
|
},
|
||||||
|
'Shift-Tab': () => {
|
||||||
|
this.editor.commands.outdent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
47
src/views/tiptap/index.tsx
Normal file
47
src/views/tiptap/index.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useEditor, EditorContent, FloatingMenu, BubbleMenu } from '@tiptap/react'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { TiptapMenuBar } from './toolbar'
|
||||||
|
import './editor.less'
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
StarterKit,
|
||||||
|
]
|
||||||
|
const content = `
|
||||||
|
<p>该摄<span class="proodfread-insert-item" data-origin="" data-text="像">像</span><strong><em>头头</em></strong>可以在单<strong><span class="proofread-item proofread-item-undefined proofread--item" data-proofread-id="undefined">次爆</span></strong><em>光中以每秒</em> 480 万帧的速度采集动态的事件</p><p>的图像。</p><p><br></p><p>采集快速运动过程(如落下的水滴或分子相互作用)的 青晰 图像,我们需要使用昂贵的超快摄像头(每秒可采集数百万张图像)。据国外媒报道,加拿大国立科学研究院(INRS)宣布开发出新摄像头,能够以极低的成本实现超快速成像,并<strong>适合<em>多种(many)</em></strong>应用,例如实时监测药物输送或用于自动驾驶的高速激光雷达系统。</p>
|
||||||
|
`
|
||||||
|
function Editor() {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
return (<div className='tiptap-container'>
|
||||||
|
<div className="toolbars">
|
||||||
|
<TiptapMenuBar editor={editor} />
|
||||||
|
</div>
|
||||||
|
<EditorContent className='tiptap-editor' editor={editor} />
|
||||||
|
<FloatingMenu>This is the floating menu</FloatingMenu>
|
||||||
|
<BubbleMenu>
|
||||||
|
<button
|
||||||
|
onClick={() => editor?.chain().focus().toggleBold().run()}
|
||||||
|
className={editor?.isActive('bold') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
Bold
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor?.chain().focus().toggleItalic().run()}
|
||||||
|
className={editor?.isActive('italic') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
Italic
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor?.chain().focus().toggleStrike().run()}
|
||||||
|
className={editor?.isActive('strike') ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
Strike
|
||||||
|
</button>
|
||||||
|
</BubbleMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Editor
|
82
src/views/tiptap/toolbar.tsx
Normal file
82
src/views/tiptap/toolbar.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { createFromIconfontCN } from "@ant-design/icons";
|
||||||
|
import { Editor } from "@tiptap/react"
|
||||||
|
import { Popover } from "antd";
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
type TiptapMenuBarProps = {
|
||||||
|
editor: Editor | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconFont = createFromIconfontCN({
|
||||||
|
scriptUrl: 'https://at.alicdn.com/t/c/font_4382753_t9zvof0xm5.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TiptapMenuBar: React.FC<TiptapMenuBarProps> = (props) => {
|
||||||
|
const { editor } = props
|
||||||
|
const buttons = [
|
||||||
|
{ click: () => editor?.chain().focus().toggleBold().run(), disabled: false, icon: 'bold', key: 'bold' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleItalic().run(), disabled: false, icon: 'italic', key: 'italic' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleStrike().run(), disabled: false, icon: 'strikethrough', key: 'strike', tips: '删除' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleCode().run(), disabled: false, icon: 'code', key: 'code' },
|
||||||
|
{ click: () => editor?.chain().focus().setParagraph().run(), disabled: false, icon: 'paragraph', key: 'paragraph' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleBulletList().run(), disabled: false, icon: 'unorderedlist', key: 'bulletList' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleOrderedList().run(), disabled: false, icon: 'orderedlist', key: 'orderedList' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleBlockquote().run(), disabled: false, icon: 'quote', key: 'blockquote',tips:'引用' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleBlockquote().run(), disabled: false, icon: 'quote', key: 'blockquote',tips:'引用' },
|
||||||
|
{ click: () => editor?.chain().focus().toggleBlockquote().run(), disabled: false, icon: 'quote', key: 'blockquote',tips:'引用' },
|
||||||
|
]
|
||||||
|
return !editor ? null : (
|
||||||
|
<div className="tiptap-menus">
|
||||||
|
{buttons.map(btn => <button key={btn.key}
|
||||||
|
onClick={btn.click}
|
||||||
|
disabled={btn.disabled}
|
||||||
|
className={editor.isActive(btn.key) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
<Popover content={btn.tips || btn.key}><IconFont type={`icon-${btn.icon}`} /></Popover>
|
||||||
|
</button>)}
|
||||||
|
|
||||||
|
{[1, 2, 3, 4].map((level: any) => <button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level }).run()}
|
||||||
|
className={editor.isActive('heading', { level }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
<Popover content={`标题${level}`}><IconFont type={`icon-h${level}`} /></Popover>
|
||||||
|
</button>)}
|
||||||
|
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
|
||||||
|
horizontal rule
|
||||||
|
</button>
|
||||||
|
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
|
||||||
|
hard break
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.undo()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
undo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.redo()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
redo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editor.chain().focus().setColor('#958DF1').run()}
|
||||||
|
className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
|
||||||
|
>
|
||||||
|
purple
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
135
src/views/types.ts
Normal file
135
src/views/types.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
export type int = number;
|
||||||
|
export type bool = boolean;
|
||||||
|
|
||||||
|
// 0默认 1忽略 2采纳 3未采纳
|
||||||
|
export enum ProofreadStateEnum {
|
||||||
|
Default,
|
||||||
|
Ignore,
|
||||||
|
Accept,
|
||||||
|
NotAdopted,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//校验内容
|
||||||
|
export type TCorrectedContent = {
|
||||||
|
/**
|
||||||
|
* 'd' | 'i' | 'r'
|
||||||
|
*/
|
||||||
|
tag: 'd' | 'i' | 'r' | string;
|
||||||
|
//原内容
|
||||||
|
origin: string;
|
||||||
|
//替换内容
|
||||||
|
text: string;
|
||||||
|
//开始位置
|
||||||
|
start: int;
|
||||||
|
//结束位置
|
||||||
|
end: int;
|
||||||
|
}
|
||||||
|
export enum ProofreadTypeEnum {
|
||||||
|
unknown,
|
||||||
|
blackWord,
|
||||||
|
/**
|
||||||
|
* 字错
|
||||||
|
*/
|
||||||
|
characterError,
|
||||||
|
/**
|
||||||
|
* 词错
|
||||||
|
*/
|
||||||
|
wordError,
|
||||||
|
/**
|
||||||
|
* 数字错
|
||||||
|
*/
|
||||||
|
numberError,
|
||||||
|
/**
|
||||||
|
* 计量单位错
|
||||||
|
*/
|
||||||
|
unitsError,
|
||||||
|
/**
|
||||||
|
* 标点错
|
||||||
|
*/
|
||||||
|
punctuationError,
|
||||||
|
/**
|
||||||
|
* 语法错
|
||||||
|
*/
|
||||||
|
grammarError,
|
||||||
|
/**
|
||||||
|
* 知识性差错
|
||||||
|
*/
|
||||||
|
knowledgeableError,
|
||||||
|
/**
|
||||||
|
* 倾向性差错
|
||||||
|
*/
|
||||||
|
tendentiousError,
|
||||||
|
/**
|
||||||
|
* 不⼀致
|
||||||
|
*/
|
||||||
|
inconsistentError,
|
||||||
|
/**
|
||||||
|
* 格式错
|
||||||
|
*/
|
||||||
|
formatError,
|
||||||
|
}
|
||||||
|
export type TProofreadItem = {
|
||||||
|
/**
|
||||||
|
* id
|
||||||
|
*/
|
||||||
|
id: int;
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
uid: string | number;
|
||||||
|
/**
|
||||||
|
* 文稿ID
|
||||||
|
*/
|
||||||
|
fid: int;
|
||||||
|
//md5加密字段(原始内容加密字段)
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* 原始内容
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
/**
|
||||||
|
* 校验后的文本
|
||||||
|
*/
|
||||||
|
newText: string;
|
||||||
|
correctedContent: TCorrectedContent;
|
||||||
|
/**
|
||||||
|
* 检验时间
|
||||||
|
*/
|
||||||
|
checkTime: int;
|
||||||
|
offset: int;
|
||||||
|
/**
|
||||||
|
* 是否采纳 0默认 1忽略 2采纳 3未采纳
|
||||||
|
*/
|
||||||
|
isAccept: ProofreadStateEnum;
|
||||||
|
/**
|
||||||
|
* 异常类型 0未知 1个人黑名单 2字错 3词错 4数字错 5标点错 6计量单位错 7语法错误 8知识性差错 9倾向性差错 10不一致 11格式错
|
||||||
|
*/
|
||||||
|
type: ProofreadTypeEnum;
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
ctime: int;
|
||||||
|
utime: int;
|
||||||
|
dtime: int;
|
||||||
|
action?: 'replace' | 'delete' | 'insert'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TNormalProofreadItem = {
|
||||||
|
origin:string;
|
||||||
|
text:string;
|
||||||
|
}
|
||||||
|
export type TOPS = {
|
||||||
|
attributes?: {
|
||||||
|
bold?: boolean;
|
||||||
|
italic?: boolean;
|
||||||
|
proofread?: {
|
||||||
|
action: 'insert' | 'delete' | 'replace';
|
||||||
|
id: number;
|
||||||
|
},
|
||||||
|
replace?: TProofreadItem;
|
||||||
|
insert?: TNormalProofreadItem;
|
||||||
|
delete?: TProofreadItem;
|
||||||
|
},
|
||||||
|
insert?: string;
|
||||||
|
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 10086
|
||||||
|
}
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user