2025-03-21 13:09:08 +08:00

576 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {useCallback, useEffect, useMemo, useState} from "react";
import Icon from "@ant-design/icons";
import {useDebounceFn, useSetState} from "ahooks";
import {Flex, Space} from "antd";
import clsx from "clsx"
import {ProofreadStateEnum,} from "@/service/types/document.ts";
import {showToast} from "@/components/messages/Modal.tsx";
import './editor.less'
import {IconCheckOutline, IconMinus, IconRollback, IconRollbackCircle, IconRollbackOld} from "./icons.tsx";
import {ActionKey, ProofreadItem} from "./proofread-item.tsx";
import {IconLogo} from "./images/logo.tsx";
import {parseV3ProofreadResult} from "@/core/proofread-util.ts";
import {V3Enums} from "@/types/v3/enums.ts";
import {stringIsEmpty} from "@/core/string-utils.ts";
import {useBridge} from "@/core/bridge.ts";
import {CACHE_DATA} from "@/core/cache.ts";
import {process} from "@/service/api/v3/document.ts";
import {VirtualListItem} from "@/pages/proofread/components/virtual-list-item.tsx";
type ProofreadPanelProps = {
children?: React.ReactNode;
sentences: V3.ProofreadSentence[];
proofreadLoading?: boolean
onUpdateProofreadItem: (item: V3.ProofreadCorrect, action?: 'process' | 'redo') => void;
onSelectProofreadItem: (item: V3.ProofreadCorrect | null) => void;
activeProofreadItemId?: number;
disabledProofreadItemList: number[];
onStartProofread: () => Promise<void> | void;
selectedProofreadItemId?: number;
selectedAcceptStatus?: 1 | 2 | 3;
userinfo?: Account;
histories: V3.ProcessAction[];
setHistories: (values: V3.ProcessAction[]) => void;
onStateChange?: (state: { activeItemId: number; acceptState: AcceptStatus; }) => void;
}
enum AcceptStatus {
NotAdopted = 1,
Adopted = 2,
Default = 3
}
const GovCheckKeys = [
V3Enums.ProofreadType.leader.key,
V3Enums.ProofreadType.position.key,
V3Enums.ProofreadType.quotation.key,
V3Enums.ProofreadType.fallen_officers.key,
]
/**
* 是否需要复核
* @param item
*/
function itemNeedReview(item:V3.ProofreadCorrect){
if(
item.tag.trim().length == 0 // not delete insert replace
&& (
item.type == V3Enums.ProofreadType.sensitive.key
|| item.type == V3Enums.ProofreadType.blacklist.key
|| GovCheckKeys.includes(item.type)
)
) {
return true
}
return false
}
const ProofreadPanel: React.FC<ProofreadPanelProps> = (props) => {
const {userinfo, histories, setHistories} = props
const [state, setState] = useSetState<{
// 当前选中分类
currentType: string | null;
// 默认是否展开panel
open: boolean;
adoptAll?: boolean;
ignoreAll?: boolean;
listMaxCount: number;
activeIndex: number;
activeItemId: number;
acceptState: AcceptStatus;
}>({
currentType: null,
open: true,
adoptAll: false,
ignoreAll: false,
activeIndex: -1,
activeItemId: -1,
listMaxCount: -1,
acceptState: AcceptStatus.NotAdopted
})
const [proofreadResult, setProofreadResult] = useState<V3.ParsedProofreadResult>()
useEffect(() => {
handleTypeChange(null, false)
const result = parseV3ProofreadResult(props.sentences, props.disabledProofreadItemList)
setProofreadResult(result)
}, [props.sentences, props.proofreadLoading, props.disabledProofreadItemList])
const getCurrentProofreadTypeListByAcceptStatus = (_state?: AcceptStatus, disableType = false) => {
_state = _state || state.acceptState
let list = proofreadResult?.list || [];
if (_state == AcceptStatus.NotAdopted) {
// 获取未复核的校对项
list = list.filter(s => s.isAccept == ProofreadStateEnum.Default);
} else if (_state == AcceptStatus.Adopted) {
// 获取已复核的校对项
list = list.filter(s => s.isAccept != ProofreadStateEnum.Default)
}
if (state.currentType && !disableType) {
list = list.filter(s => {
if(state.currentType == 'gov-check'){
return GovCheckKeys.includes(s.type)
}
if(state.currentType == 'error'){
return s.type != 'blacklist' && s.type != 'sensitive' && !GovCheckKeys.includes(s.type)
}
return s.type == state.currentType
})
}
return list;
}
// 当前校对数据
const currentProofreadCount = useMemo(() => {
const all = ( proofreadResult?.list || []).length;
// 全局获取待复核的校对项 避免过滤后的数据为空
const not_adopted = ( proofreadResult?.list || []).filter(s => s.isAccept == ProofreadStateEnum.Default).length;
const list = getCurrentProofreadTypeListByAcceptStatus(undefined, true) || [];
const count: V3.ProofreadResultCount = {
all,
adopted: 0,
not_adopted,
error:0,
total: not_adopted,
sensitive: 0,
blacklist: 0,
govCheck: 0,
};
list.forEach(it => {
//count.total ++;
if (it.type === 'sensitive'){
count.sensitive ++;
}else if(it.type === 'blacklist'){
count.blacklist++;
}else if(GovCheckKeys.includes(it.type)){
count.govCheck ++;
}else{
count.error ++;
}
})
count.adopted = all - count.not_adopted;
// if(state.acceptState == AcceptStatus.Adopted) {
// }else{
// count.not_adopted = count.total;
// count.adopted = all - count.not_adopted;
// }
return count;
}, [proofreadResult, state.acceptState, state.activeItemId, props.histories])
// 当前状态校对列表
const currentProofreadList = useMemo(() => {
const list = proofreadResult?.list || [];
if (!state.currentType) return list;
// Array.isArray(state.currentType?.id) ? state.currentType?.id.includes(s.type) :
return list.filter(s => {
if(state.currentType == 'error'){
return s.type != 'blacklist' && s.type != 'sensitive'
}
return s.type == state.currentType
})
}, [proofreadResult, state.currentType])
// 撤销操作
const redoAction = async () => {
if (props.sentences.length == 0 || histories.length == 0) return;
const last = histories[histories.length - 1];
// 获取最后一个历史的同一批次记录
const currentList = histories.filter(s => s.id == last.id);
if (!currentList || currentList.length == 0) return;
currentList.forEach(it => {
// 状态调整为未处理
const item = proofreadResult?.list?.find(s => s.id == it.correctId)
if (item) {
item.isAccept = ProofreadStateEnum.Default
props.onUpdateProofreadItem(item, 'redo')
}
})
setHistories(histories.filter(s => s.id != last.id));
setProofreadResult({
count: proofreadResult!.count,
list: proofreadResult!.list
})
// 将状态调整为未复核
if (state.acceptState == AcceptStatus.Adopted) {
setState({acceptState: AcceptStatus.NotAdopted})
}
// 选中第一个
const actId = currentList[0];
// 找到要选中的item的index
const item = currentProofreadList.find(s => s.id == actId.correctId)
if (item) {
centerItem(-1, item)
// setState({activeIndex: index})
}
}
/**
* 获取全部/类似校对项
* @param isAccept
* @param p 如果不传入则获取该状态对应类型所有校对项
*/
const getSameProofreadItems = (isAccept?: boolean, p?: V3.ProofreadCorrect) => {
const list = currentProofreadList.filter(s => {
// if (isAccept && s.type == V3Enums.ProofreadType.sensitive.key) return false;
return s.isAccept == ProofreadStateEnum.Default;
})
// 获取当前类型下所有
return p ? list.filter(s => {
return s.origin == p.origin && s.text == p.text;
}) : (
[...list]
)
}
const addToHistory = (items: V3.ProofreadCorrect[]) => {
// 根据最后一个记录生成新的批次id
const last = histories.length == 0 ? null : histories[histories.length - 1]
const id = last ? last.id + 1 : 1;
setHistories([
...histories,
...items.map(s => ({id, correctId: s.id}))
])
setProofreadResult(proofreadResult)
}
// 批量处理
const batchProcess = (status: ProofreadStateEnum, proofread?: V3.ProofreadCorrect) => {
const isAccept = status == ProofreadStateEnum.Accept;
const list = getSameProofreadItems(isAccept, proofread);
if (list.length > 0) {
const ids: number[] = [];
// 设置 状态
list.map(s => {
ids.push(s.id)
// 处理以下复核情况
s.isAccept = isAccept && itemNeedReview(s) ? ProofreadStateEnum.Review :status;
props.onUpdateProofreadItem(s, 'process');
return s;
})
addToHistory(list)
process(CACHE_DATA.DocumentID, {
ids,
action: status
})
// 采用随机数 避免默认值均为-1 导致选中项不更新
const activeItemId = 0 - Math.ceil(Math.random() * 10)
setTimeout(()=>{
setState({activeItemId})
},20)
}
}
// 处理全部
const handleProcessAll = (action: ActionKey) => {
return () => {
if (props.sentences.length == 0) return;
batchProcess(action == 'acceptAll' ? ProofreadStateEnum.Accept : ProofreadStateEnum.Ignore)
}
}
// 让当前选中的 item 居中
const centerItem = (activeIndex: number, item: V3.ProofreadCorrect, showInCenter = true) => {
if (item.id == state.activeItemId) return;
setState((origin) => ({
...origin,
activeIndex,
activeItemId: item.id
}))
if (showInCenter) {
// 将提示显示在视图中间
const div = document.querySelector(`div.proofread-item-${item.id}`)
if (div) {
div.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest' // 水平对齐方式
})
}
}
//
props.onSelectProofreadItem(item);
}
useEffect(() => {
if (props.selectedProofreadItemId) {
const index = currentProofreadList.filter(s => (state.acceptState == AcceptStatus.Default
|| (state.acceptState == AcceptStatus.Adopted && s.isAccept != ProofreadStateEnum.Default)
|| (state.acceptState == AcceptStatus.NotAdopted && s.isAccept == ProofreadStateEnum.Default)
)).findIndex(s => s.id == props.selectedProofreadItemId)
if (index >= 0) {
centerItem(index, currentProofreadList[index])
}
setState({activeItemId: props.selectedProofreadItemId})
}
}, [props.selectedProofreadItemId])
useEffect(() => {
if (props.selectedAcceptStatus) {
setState({
acceptState: props.selectedAcceptStatus
})
}
}, [props.selectedAcceptStatus])
// 处理校对项
const handleItemAction = async (key: ActionKey, item: V3.ProofreadCorrect, index: number) => {
try {
if (key == 'click') {
if (item) {
setState({activeIndex: index, activeItemId: item.id})
}
centerItem(index, item, false)
return;
} else if (key == 'addToLexicon') {
// 添加到词库
showToast('添加词库成功', "success")
} else {
if (key == 'ignoreAll' || key == 'acceptAll') { // 同类型处理
batchProcess(key == 'acceptAll' ? ProofreadStateEnum.Accept : ProofreadStateEnum.Ignore, item)
} else {
if (key == 'ignore') { // 忽略
item.isAccept = ProofreadStateEnum.Ignore;
} else if (key == 'accept') { // 采纳
item.isAccept = ProofreadStateEnum.Accept;
} else if (key == 'review') { // 复核
item.isAccept = ProofreadStateEnum.Review;
}
props.onUpdateProofreadItem(item, 'process')
addToHistory([item])
process(CACHE_DATA.DocumentID, {
ids: [item.id],
action: item.isAccept
}).catch(console.log)
// 自动定位下一个
const currentList = state.acceptState == AcceptStatus.Default ? currentProofreadList : currentProofreadList.filter(s => (state.acceptState == AcceptStatus.Default
|| (state.acceptState == AcceptStatus.Adopted && s.isAccept != ProofreadStateEnum.Default)
|| (state.acceptState == AcceptStatus.NotAdopted && s.isAccept == ProofreadStateEnum.Default)
));
if (currentList.length == 0) {
setTimeout(() => {
setState((prevState) => ( {
...prevState,
activeIndex: -1,
activeItemId: 0 - Math.ceil(Math.random() * 100),
}))
}, 20)
return;
}
const _idx = index >= currentList.length ? index - 1 : (state.acceptState == AcceptStatus.Default ? index + 1 : index)
const _it = currentList[_idx];
setTimeout(() => {
centerItem(_idx, _it,false)
}, 20)
}
}
} catch (e) { // 处理错误了
console.log('process item action error:', (e as Error).message)
}
}
// 切换类型
const handleTypeChange = (currentType: string | null, resetIndex = true) => {
setState(_prev => ({
currentType: _prev.currentType == currentType ? null : currentType,
activeIndex: resetIndex ? -1 : _prev.activeIndex
}))
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
const {bridge} = useBridge();
const focusPane = useDebounceFn(() => bridge?.Focus(), {wait: 200,})
// Focus
const movePrevOrNextProofreadItem = (action: 'prev' | 'next') => {
setState(state => {
// 方向键移动
let list = getCurrentProofreadTypeListByAcceptStatus() || [];
if (list.length == 0) return state;
if (!stringIsEmpty(state.currentType)) {
list = list.filter(s => s.type == state.currentType)
}
// 当前选中项的索引
const index = list.findIndex(s => s.id == state.activeItemId)
const maxCount = list.length;
if (maxCount == 0
|| (action == 'next' && index >= maxCount - 1)
|| (action == 'prev' && index <= 0)
) {
return state;
}
const activeIndex = index + (action == 'next' ? 1 : -1)
const item = list[activeIndex]
const div = document.querySelector(`div.proofread-item-${item.id}`)
if (div) {
div.scrollIntoView({behavior: 'smooth', block: 'center',inline: 'nearest'})
}
props.onSelectProofreadItem(item);
setTimeout(focusPane.run, 50);
return {
...state,
activeIndex,
activeItemId: item.id
}
})
}
const handleUpAndDownEvent = useCallback((e: KeyboardEvent) => {
const key = e.key.toLowerCase();
if (key == 'arrowup' || key == 'arrowdown') {
console.log('handleUpAndDownEvent=>', proofreadResult)
if (proofreadResult) {
movePrevOrNextProofreadItem(key == 'arrowup' ? 'prev' : 'next')
}
}
}, [proofreadResult, state])
useEffect(() => {
// 绑定键盘事件
document.documentElement.addEventListener('keyup', handleUpAndDownEvent, false)
return () => {
document.documentElement.removeEventListener('keyup', handleUpAndDownEvent, false)
}
}, [handleUpAndDownEvent]);
const currentProofreadTotal = useMemo(() => {
//const list = getCurrentProofreadTypeListByAcceptStatus(undefined,true) || [];
return currentProofreadCount.total;
// return proofreadResult?.count.total && proofreadResult?.count.total > 0 ? proofreadResult?.count.total : 0
}, [currentProofreadCount])
const _currentList = useMemo(() => {
return getCurrentProofreadTypeListByAcceptStatus() || []
}, [currentProofreadList, state.acceptState, state.activeItemId])
const handleTabChange = (key: AcceptStatus) => {
let activeItemId = -1;
if (key == AcceptStatus.Adopted) {
// 如果切换到已复核 则自动定位到最后一个
if (histories.length > 0) {
// const list = getCurrentProofreadTypeListByAcceptStatus(AcceptStatus.Adopted)
// if(list){
// const last = list.length > 0 ? list[list.length - 1] : null
// if(last) {
// activeItemId = last.id
// }
// }
const last = histories[histories.length - 1];
console.log('---last', last)
activeItemId = last.correctId;
bridge?.SelectMarkById(activeItemId, CACHE_DATA.DocumentID)
}
}
setState({acceptState: key, activeItemId})
handleTypeChange(null)
props.onStateChange?.({acceptState: key, activeItemId})
CACHE_DATA.selectStatus = key
// 如果是
}
return (<div className={'proofread-panel-container v3'}>
<div className="panel-header system-font">
<Flex align="center">
<div style={{fontSize: 50, marginRight: 30}}>
<IconLogo style={{display: 'block'}}/>
</div>
<Flex justify={'center'} className={'operation-buttons'}>
<Space style={{margin: 10}}>
<button
disabled={props.proofreadLoading || state.acceptState == AcceptStatus.Adopted || props.sentences.length == 0 || currentProofreadTotal == 0}
className={'btn-process'}
onClick={handleProcessAll('acceptAll')}>
<div className="btn-icon"><Icon component={IconCheckOutline}/></div>
<div></div>
</button>
<button
disabled={props.proofreadLoading || state.acceptState == AcceptStatus.Adopted || props.sentences.length == 0 || currentProofreadTotal == 0}
className={'btn-process'}
onClick={handleProcessAll('ignore')}>
<div className="btn-icon"><Icon component={IconMinus}/></div>
<div></div>
</button>
<button
className="btn-process"
disabled={histories.length == 0 || props.sentences.length == 0 || (currentProofreadTotal == 0 && histories.length == 0)}
onClick={redoAction}>
<div className="btn-icon"><Icon component={IconRollbackCircle}/></div>
<div></div>
</button>
</Space>
</Flex>
{props.children}
</Flex>
<div className="state-filter-tab">
<div className="tab-wrapper">
{[
{title: '待复核', key: AcceptStatus.NotAdopted, count: currentProofreadCount.not_adopted},
{title: '已复核', key: AcceptStatus.Adopted,count: currentProofreadCount.adopted},
{title: '全部', key: AcceptStatus.Default,count: currentProofreadCount.all}
].map((it, idx) => (
<div
key={idx}
onClick={() => {handleTabChange(it.key)}}
className={clsx('tab-item d-flex justify-center item-center', {active: state.acceptState == it.key})}
>
<span>{it.title}</span>
<span className="tab-item-count" style={{marginLeft:5}}>{props.proofreadLoading?'...':it.count}</span>
</div>
))}
</div>
</div>
</div>
<div className={`proofread-list-wrapper ${proofreadResult ? 'has-result' : ''}`}>
<div className="proofread-result">
<div className={'proofread-list'}>
<div style={{paddingTop: 2}}>
{_currentList.map((it, index) => (
<VirtualListItem key={it.id} id={it.id} index={index}>
<ProofreadItem
it={it}
className={`proofread-index-${index}`}
index={index}
last={false}
proofreading={props.proofreadLoading}
selected={state.activeItemId == it.id}
onAction={key => handleItemAction(key, it, index)}
previewMode={false}
/>
</VirtualListItem>
))}
</div>
</div>
</div>
<div className="types system-font">
<div className="container">
{
[
{title: '错误', key: 'error', count: currentProofreadCount.error},
{title: '敏感词', key: 'sensitive', count: currentProofreadCount.sensitive},
{title: '黑名单', key: 'blacklist', count: currentProofreadCount.blacklist},
{title: '政务', key: 'gov-check', count: currentProofreadCount.govCheck,color:'hsla(179, 100%, 70%, 1)'}
].map((it, idx) => (
it.count <= 0 ? null : <div key={idx} onClick={() => handleTypeChange(it.key)}
className={`item ${state.currentType == it.key ? 'active' : ''}`}>
<div className="count" style={it.color?{color:it.color}:{}}>{props.proofreadLoading ? '...' : it.count}</div>
<div className="text">{it.title}</div>
</div>
))
}
</div>
</div>
</div>
</div>)
}
export default ProofreadPanel;