This commit is contained in:
LittleBoy 2024-01-04 16:46:41 +08:00
commit 4203a8ca37
36 changed files with 5681 additions and 0 deletions

29
.gitignore vendored Normal file
View 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
View 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
View 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
View 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"
}
}

Binary file not shown.

BIN
public/cloud.glb Normal file

Binary file not shown.

BIN
public/gift.glb Normal file

Binary file not shown.

BIN
public/jinzita.glb Normal file

Binary file not shown.

1
public/vite.svg Normal file
View 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
View 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
View 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;

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

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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(/&nbsp;/gi, " ");
newText = newText.replace(/&nbsp;/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
View 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;

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

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

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

View 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();
}
}
},
});

View 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

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

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
tsconfig.json Normal file
View 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
View 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
View 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
}
})

3356
yarn.lock Normal file

File diff suppressed because it is too large Load Diff