mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:25:00 +00:00
feat: add localized high-risk status remap guard with optimized modal UX
This commit is contained in:
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
@@ -32,7 +32,30 @@ import MarkdownRenderer from '../markdown/MarkdownRenderer';
|
|||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const RiskAcknowledgementModal = ({
|
const RiskMarkdownBlock = React.memo(function RiskMarkdownBlock({
|
||||||
|
markdownContent,
|
||||||
|
}) {
|
||||||
|
if (!markdownContent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='rounded-lg'
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--semi-color-warning-light-hover)',
|
||||||
|
background:
|
||||||
|
'linear-gradient(180deg, var(--semi-color-warning-light-default) 0%, var(--semi-color-fill-0) 100%)',
|
||||||
|
padding: '12px',
|
||||||
|
contentVisibility: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MarkdownRenderer content={markdownContent} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
|
||||||
visible,
|
visible,
|
||||||
title,
|
title,
|
||||||
markdownContent = '',
|
markdownContent = '',
|
||||||
@@ -47,7 +70,7 @@ const RiskAcknowledgementModal = ({
|
|||||||
confirmText = '',
|
confirmText = '',
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => {
|
}) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [checkedItems, setCheckedItems] = useState([]);
|
const [checkedItems, setCheckedItems] = useState([]);
|
||||||
const [typedText, setTypedText] = useState('');
|
const [typedText, setTypedText] = useState('');
|
||||||
@@ -68,6 +91,17 @@ const RiskAcknowledgementModal = ({
|
|||||||
return typedText.trim() === requiredText.trim();
|
return typedText.trim() === requiredText.trim();
|
||||||
}, [typedText, requiredText]);
|
}, [typedText, requiredText]);
|
||||||
|
|
||||||
|
const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
|
||||||
|
const canConfirm = allChecked && typedMatched;
|
||||||
|
|
||||||
|
const handleChecklistChange = useCallback((index, checked) => {
|
||||||
|
setCheckedItems((previous) => {
|
||||||
|
const next = [...previous];
|
||||||
|
next[index] = checked;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@@ -85,7 +119,7 @@ const RiskAcknowledgementModal = ({
|
|||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
maxHeight: isMobile ? '70vh' : '72vh',
|
maxHeight: isMobile ? '70vh' : '72vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
padding: isMobile ? '12px 16px' : '16px 20px',
|
padding: isMobile ? '12px 16px' : '18px 22px',
|
||||||
}}
|
}}
|
||||||
footer={
|
footer={
|
||||||
<Space>
|
<Space>
|
||||||
@@ -93,7 +127,7 @@ const RiskAcknowledgementModal = ({
|
|||||||
<Button
|
<Button
|
||||||
theme='solid'
|
theme='solid'
|
||||||
type='danger'
|
type='danger'
|
||||||
disabled={!allChecked || !typedMatched}
|
disabled={!canConfirm}
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
>
|
>
|
||||||
{confirmText}
|
{confirmText}
|
||||||
@@ -102,31 +136,49 @@ const RiskAcknowledgementModal = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
{markdownContent ? (
|
<div
|
||||||
<div className='border border-warning-200 bg-warning-50 rounded-md px-3 py-2'>
|
className='rounded-lg'
|
||||||
<MarkdownRenderer content={markdownContent} />
|
style={{
|
||||||
</div>
|
border: '1px solid var(--semi-color-warning-light-hover)',
|
||||||
) : null}
|
background: 'var(--semi-color-warning-light-default)',
|
||||||
|
padding: isMobile ? '10px 12px' : '12px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RiskMarkdownBlock markdownContent={markdownContent} />
|
||||||
|
|
||||||
{detailItems.length > 0 ? (
|
{detailItems.length > 0 ? (
|
||||||
<div className='flex flex-col gap-2'>
|
<div
|
||||||
|
className='flex flex-col gap-2 rounded-lg'
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--semi-color-warning-light-hover)',
|
||||||
|
background: 'var(--semi-color-fill-0)',
|
||||||
|
padding: isMobile ? '10px 12px' : '12px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{detailTitle ? <Text strong>{detailTitle}</Text> : null}
|
{detailTitle ? <Text strong>{detailTitle}</Text> : null}
|
||||||
<div className='font-mono text-xs break-all bg-orange-50 border border-orange-200 rounded-md p-2'>
|
<div className='font-mono text-xs break-all bg-orange-50 border border-orange-200 rounded-md p-2'>
|
||||||
{detailItems.join(', ')}
|
{detailText}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{checklist.length > 0 ? (
|
{checklist.length > 0 ? (
|
||||||
<div className='flex flex-col gap-2'>
|
<div
|
||||||
|
className='flex flex-col gap-2 rounded-lg'
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--semi-color-border)',
|
||||||
|
background: 'var(--semi-color-fill-0)',
|
||||||
|
padding: isMobile ? '10px 12px' : '12px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{checklist.map((item, index) => (
|
{checklist.map((item, index) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={`risk-check-${index}`}
|
key={`risk-check-${index}`}
|
||||||
checked={!!checkedItems[index]}
|
checked={!!checkedItems[index]}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const next = [...checkedItems];
|
handleChecklistChange(index, event.target.checked);
|
||||||
next[index] = event.target.checked;
|
|
||||||
setCheckedItems(next);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
@@ -136,15 +188,23 @@ const RiskAcknowledgementModal = ({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{requiredText ? (
|
{requiredText ? (
|
||||||
<div className='flex flex-col gap-2'>
|
<div
|
||||||
|
className='flex flex-col gap-2 rounded-lg'
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--semi-color-danger-light-hover)',
|
||||||
|
background: 'var(--semi-color-danger-light-default)',
|
||||||
|
padding: isMobile ? '10px 12px' : '12px 14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
|
{inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
|
||||||
<div className='font-mono text-xs break-all bg-gray-50 border border-gray-200 rounded-md p-2'>
|
<div className='font-mono text-xs break-all rounded-md p-2 bg-gray-50 border border-gray-200'>
|
||||||
{requiredText}
|
{requiredText}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
value={typedText}
|
value={typedText}
|
||||||
onChange={setTypedText}
|
onChange={setTypedText}
|
||||||
placeholder={inputPlaceholder}
|
placeholder={inputPlaceholder}
|
||||||
|
autoFocus={visible}
|
||||||
onCopy={(event) => event.preventDefault()}
|
onCopy={(event) => event.preventDefault()}
|
||||||
onCut={(event) => event.preventDefault()}
|
onCut={(event) => event.preventDefault()}
|
||||||
onPaste={(event) => event.preventDefault()}
|
onPaste={(event) => event.preventDefault()}
|
||||||
@@ -160,6 +220,6 @@ const RiskAcknowledgementModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default RiskAcknowledgementModal;
|
export default RiskAcknowledgementModal;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import RiskAcknowledgementModal from '../../../common/modals/RiskAcknowledgementModal';
|
import RiskAcknowledgementModal from '../../../common/modals/RiskAcknowledgementModal';
|
||||||
import {
|
import {
|
||||||
@@ -6,13 +6,17 @@ import {
|
|||||||
STATUS_CODE_RISK_CHECKLIST_KEYS,
|
STATUS_CODE_RISK_CHECKLIST_KEYS,
|
||||||
} from './statusCodeRiskGuard';
|
} from './statusCodeRiskGuard';
|
||||||
|
|
||||||
const StatusCodeRiskGuardModal = ({
|
const StatusCodeRiskGuardModal = React.memo(function StatusCodeRiskGuardModal({
|
||||||
visible,
|
visible,
|
||||||
detailItems,
|
detailItems,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const checklist = useMemo(
|
||||||
|
() => STATUS_CODE_RISK_CHECKLIST_KEYS.map((item) => t(item)),
|
||||||
|
[t, i18n.language],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RiskAcknowledgementModal
|
<RiskAcknowledgementModal
|
||||||
@@ -21,7 +25,7 @@ const StatusCodeRiskGuardModal = ({
|
|||||||
markdownContent={t(STATUS_CODE_RISK_I18N_KEYS.markdown)}
|
markdownContent={t(STATUS_CODE_RISK_I18N_KEYS.markdown)}
|
||||||
detailTitle={t(STATUS_CODE_RISK_I18N_KEYS.detailTitle)}
|
detailTitle={t(STATUS_CODE_RISK_I18N_KEYS.detailTitle)}
|
||||||
detailItems={detailItems}
|
detailItems={detailItems}
|
||||||
checklist={STATUS_CODE_RISK_CHECKLIST_KEYS.map((item) => t(item))}
|
checklist={checklist}
|
||||||
inputPrompt={t(STATUS_CODE_RISK_I18N_KEYS.inputPrompt)}
|
inputPrompt={t(STATUS_CODE_RISK_I18N_KEYS.inputPrompt)}
|
||||||
requiredText={t(STATUS_CODE_RISK_I18N_KEYS.confirmText)}
|
requiredText={t(STATUS_CODE_RISK_I18N_KEYS.confirmText)}
|
||||||
inputPlaceholder={t(STATUS_CODE_RISK_I18N_KEYS.inputPlaceholder)}
|
inputPlaceholder={t(STATUS_CODE_RISK_I18N_KEYS.inputPlaceholder)}
|
||||||
@@ -32,6 +36,6 @@ const StatusCodeRiskGuardModal = ({
|
|||||||
onConfirm={onConfirm}
|
onConfirm={onConfirm}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default StatusCodeRiskGuardModal;
|
export default StatusCodeRiskGuardModal;
|
||||||
|
|||||||
Reference in New Issue
Block a user