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