优化构建

This commit is contained in:
liquidliang committed 2025-11-26 21:16:04 +08:00
1 parent f81ee54a3d
commit e783e1fb0c
12 files changed
+2172 -247

No files matched your search

+2094
View File
File diff suppressed because it is too large. Load diff
+6 -3
View File
@@ -6,10 +6,10 @@
"private": false,
"dependencies": {
"@floating-ui/react": "^0.26.24",
"mobile-detect": "^1.4.5",
"open-color": "^1.9.1",
"@plait-board/markdown-to-drawnix": "^0.0.6",
"@plait-board/mermaid-to-drawnix": "^0.0.7",
"@plait-board/markdown-to-drawnix": "^0.0.6"
"mobile-detect": "^1.4.5",
"open-color": "^1.9.1"
},
"exports": {
".": {
@@ -17,5 +17,8 @@
"require": "./index.js",
"types": "./index.d.ts"
}
},
"devDependencies": {
"@types/lodash": "^4.17.21"
}
}
@@ -106,7 +106,7 @@ export const DialogTrigger = React.forwardRef<
React.HTMLProps<HTMLElement> & DialogTriggerProps
>(function DialogTrigger({ children, asChild = false, ...props }, propRef) {
const context = useDialogContext();
const childrenRef = (children as any).ref;
const childrenRef = (children as React.ReactElement & { ref?: React.Ref<HTMLElement> }).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
// `asChild` allows the user to pass any element as the anchor
@@ -86,7 +86,7 @@ export const GenerationHistory: React.FC<GenerationHistoryProps> = ({
) : (
<div className="history-item-image history-video-placeholder">
<div className="placeholder-icon">🎬</div>
<span role="img" aria-label="视频" className="placeholder-icon">🎬</span>
</div>
)
)}
@@ -131,7 +131,7 @@ export const PopoverTrigger = React.forwardRef<
React.HTMLProps<HTMLElement> & PopoverTriggerProps
>(function PopoverTrigger({ children, asChild = false, ...props }, propRef) {
const context = usePopoverContext();
const childrenRef = (children as any).ref;
const childrenRef = (children as React.ReactElement & { ref?: React.Ref<HTMLElement> }).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
// `asChild` allows the user to pass any element as the anchor
@@ -61,29 +61,33 @@ export const LinkPopup = () => {
},
});
}
}, [board.viewport, target]);
}, [board.viewport, target, refs]);
const handleClickOutsideRef = useRef<(event: MouseEvent) => void>();
handleClickOutsideRef.current = (event: MouseEvent) => {
if (
refs.floating.current &&
!refs.floating.current.contains(event.target as Node)
) {
if (linkStateRef.current) {
const linkElement = LinkEditor.getLinkElement(
linkStateRef.current.editor
);
if (linkElement && !(linkElement[0] as LinkElement).url.trim()) {
LinkEditor.unwrapLink(linkStateRef.current.editor);
}
}
setAppState({
...appState,
linkState: null,
});
}
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
refs.floating.current &&
!refs.floating.current.contains(event.target as Node)
) {
if (linkStateRef.current) {
const linkElement = LinkEditor.getLinkElement(
linkStateRef.current.editor
);
if (linkElement && !(linkElement[0] as LinkElement).url.trim()) {
LinkEditor.unwrapLink(linkStateRef.current.editor);
}
}
setAppState({
...appState,
linkState: null,
});
}
handleClickOutsideRef.current?.(event);
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
@@ -91,21 +95,23 @@ export const LinkPopup = () => {
}, []);
const saveUrlAndExitEditing = () => {
if (url !== linkState!.targetElement.url) {
const editor = linkState!.editor;
const node = linkState!.targetElement;
if (!linkState) return;
if (url !== linkState.targetElement.url) {
const editor = linkState.editor;
const node = linkState.targetElement;
const path = ReactEditor.findPath(editor, node);
Transforms.setNodes(editor, { url: url }, { at: path });
}
const linkElement = LinkEditor.getLinkElement(linkState!.editor);
const linkElement = LinkEditor.getLinkElement(linkState.editor);
if (!linkElement) return;
setAppState({
...appState,
linkState: {
...appState.linkState!,
...appState.linkState,
targetElement: linkElement[0] as LinkElement,
isEditing: false,
isHoveringOrigin: true,
},
} as typeof appState.linkState,
});
};
@@ -117,22 +123,22 @@ export const LinkPopup = () => {
padding={1}
className={classNames('link-popup')}
onPointerEnter={() => {
if (!isHovering) {
if (!isHovering && appState.linkState) {
setAppState({
...appState,
linkState: {
...appState.linkState!,
...appState.linkState,
isHovering: true,
},
});
}
}}
onPointerLeave={() => {
if (!isEditing) {
if (!isEditing && appState.linkState) {
setAppState({
...appState,
linkState: {
...appState.linkState!,
...appState.linkState,
isHovering: false,
},
});
@@ -163,8 +169,9 @@ export const LinkPopup = () => {
title={`Delete link`}
aria-label={`Delete link`}
onPointerDown={() => {
const editor = linkState!.editor;
const targetElement = linkState!.targetElement;
if (!linkState) return;
const editor = linkState.editor;
const targetElement = linkState.targetElement;
const path = ReactEditor.findPath(editor, targetElement);
Transforms.unwrapNodes(editor, {
at: path,
@@ -195,10 +202,11 @@ export const LinkPopup = () => {
aria-label={`Edit link`}
onPointerDown={({ event }) => {
event.preventDefault();
if (!appState.linkState) return;
setAppState({
...appState,
linkState: {
...appState.linkState!,
...appState.linkState,
isEditing: true,
},
});
@@ -211,8 +219,9 @@ export const LinkPopup = () => {
title={`Delete link`}
aria-label={`Delete link`}
onPointerDown={() => {
const editor = linkState!.editor;
const targetElement = linkState!.targetElement;
if (!linkState) return;
const editor = linkState.editor;
const targetElement = linkState.targetElement;
const path = ReactEditor.findPath(editor, targetElement);
Transforms.unwrapNodes(editor, {
at: path,
+24 -23
View File
@@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { toFixed } from '@plait/core';
import './size-slider.scss';
import classNames from 'classnames';
@@ -46,33 +46,34 @@ export const SizeSlider: React.FC<SliderProps> = ({
}, [defaultValue]);
const handleSliderChange = useCallback(
throttle(
(event: React.MouseEvent<HTMLDivElement> | MouseEvent) => {
if (sliderRef.current && thumbRef.current) {
const sliderRect = sliderRef.current.getBoundingClientRect();
const x = event.clientX - sliderRect.left;
let percentage = Math.min(Math.max(x / sliderRect.width, 0), 1);
if (percentage >= (100 - thumbPercentageRef.current) / 100) {
percentage = 1;
} else if (percentage <= thumbPercentageRef.current / 100) {
percentage = 0;
}
const newValue =
Math.round((percentage * (max - min)) / step) * step + min;
setValue(newValue);
onChange && onChange(newValue);
(event: React.MouseEvent<HTMLDivElement> | MouseEvent) => {
if (sliderRef.current && thumbRef.current) {
const sliderRect = sliderRef.current.getBoundingClientRect();
const x = event.clientX - sliderRect.left;
let percentage = Math.min(Math.max(x / sliderRect.width, 0), 1);
if (percentage >= (100 - thumbPercentageRef.current) / 100) {
percentage = 1;
} else if (percentage <= thumbPercentageRef.current / 100) {
percentage = 0;
}
},
50,
{ leading: true, trailing: true }
),
const newValue =
Math.round((percentage * (max - min)) / step) * step + min;
setValue(newValue);
onChange && onChange(newValue);
}
},
[min, max, step, onChange]
);
const throttledSliderChange = useMemo(
() => throttle(handleSliderChange, 50, { leading: true, trailing: true }),
[handleSliderChange]
);
const handlePointerDown = useCallback(() => {
const handleMouseMove = (e: MouseEvent) => {
setIsDragging(true);
handleSliderChange(e);
throttledSliderChange(e);
};
const handleMouseUp = () => {
document.removeEventListener('pointermove', handleMouseMove);
@@ -85,7 +86,7 @@ export const SizeSlider: React.FC<SliderProps> = ({
document.addEventListener('pointermove', handleMouseMove);
document.addEventListener('pointerup', handleMouseUp);
}, [handleSliderChange]);
}, [throttledSliderChange, afterEnd]);
let percentage = ((value - min) / (max - min)) * 100;
if (percentage >= 100 - thumbPercentageRef.current) {
@@ -104,7 +105,7 @@ export const SizeSlider: React.FC<SliderProps> = ({
if (disabled || isDragging) {
return;
}
handleSliderChange(event);
throttledSliderChange(event);
}}
onPointerDown={(event) => {
event.preventDefault();
@@ -8,7 +8,7 @@
import React, { useMemo } from 'react';
import { TaskItem } from './TaskItem';
import { useTaskQueue } from '../../hooks/useTaskQueue';
import { Task, TaskType } from '../../types/task.types';
import { TaskType } from '../../types/task.types';
import { useDrawnix, DialogType } from '../../hooks/use-drawnix';
import { insertImageFromUrl } from '../../data/image';
import { insertVideoFromUrl } from '../../data/video';
@@ -6,7 +6,7 @@
*/
import React, { useState, useEffect } from 'react';
import { Button, Tag, Tooltip, Progress } from 'tdesign-react';
import { Button, Tag, Tooltip } from 'tdesign-react';
import { ImageIcon, VideoIcon, DeleteIcon, RefreshIcon, DownloadIcon, EditIcon, SaveIcon, CheckCircleFilledIcon } from 'tdesign-icons-react';
import { Task, TaskStatus, TaskType } from '../../types/task.types';
import { getRelativeTime, formatTaskDuration } from '../../utils/task-utils';
@@ -1 +0,0 @@
export { VersionUpdate, default } from './version-update';
@@ -1,179 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Button } from 'tdesign-react';
import { RefreshIcon } from 'tdesign-icons-react';
interface VersionInfo {
version: string;
buildTime: string;
gitCommit: string;
}
export const VersionUpdate: React.FC = () => {
const [hasUpdate, setHasUpdate] = useState(false);
const [currentVersion, setCurrentVersion] = useState('');
const [newVersion, setNewVersion] = useState('');
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
checkForUpdates();
// 每5分钟检查一次更新
const interval = setInterval(checkForUpdates, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
const checkForUpdates = async () => {
try {
// 获取当前版本(从HTML meta标签)
const currentVersionMeta = document.querySelector('meta[name="app-version"]');
const current = currentVersionMeta?.getAttribute('content') || '0.0.0';
setCurrentVersion(current);
// 获取服务器最新版本(添加时间戳防缓存)
const timestamp = Date.now();
const response = await fetch(`/version.json?t=${timestamp}`);
if (!response.ok) {
console.log('Version check failed:', response.status);
return;
}
const versionInfo: VersionInfo = await response.json();
// 比较版本号
if (versionInfo.version !== current && !isVersionNewer(current, versionInfo.version)) {
setNewVersion(versionInfo.version);
setHasUpdate(true);
console.log(`New version available: ${versionInfo.version} (current: ${current})`);
}
} catch (error) {
console.log('Failed to check for updates:', error);
}
};
// 简单的版本号比较
const isVersionNewer = (current: string, latest: string): boolean => {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const currentPart = currentParts[i] || 0;
const latestPart = latestParts[i] || 0;
if (latestPart > currentPart) return false;
if (latestPart < currentPart) return true;
}
return true; // 版本相同
};
const handleUpdate = async () => {
setIsUpdating(true);
try {
// 清除所有缓存
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
console.log('All caches cleared');
}
// 强制 Service Worker 更新
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.update();
if (registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
}
}
// 延迟后刷新页面,确保新版本加载
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
console.error('Update failed:', error);
setIsUpdating(false);
// 如果清缓存失败,直接刷新页面
window.location.reload();
}
};
const handleDismiss = () => {
setHasUpdate(false);
// 30分钟后再次检查
setTimeout(checkForUpdates, 30 * 60 * 1000);
};
// 监听 Service Worker 更新事件
useEffect(() => {
const handleSWUpdate = (event: MessageEvent) => {
if (event.data && event.data.type === 'SW_UPDATED') {
checkForUpdates();
}
};
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', handleSWUpdate);
return () => {
navigator.serviceWorker.removeEventListener('message', handleSWUpdate);
};
}
}, []);
if (!hasUpdate) {
return null;
}
return (
<div className="version-update-banner" style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
backgroundColor: '#e7f3ff',
padding: '12px 16px',
borderBottom: '1px solid #b3d8ff',
zIndex: 1001,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
animation: 'slideDown 0.3s ease-out'
}}>
<div className="version-update-content" style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div>
<strong>发现新版本 v{newVersion}</strong>
<span style={{ marginLeft: '8px', color: '#666' }}>
当前版本: v{currentVersion}
</span>
</div>
</div>
<div className="version-update-actions" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<Button
theme="primary"
size="small"
icon={<RefreshIcon />}
loading={isUpdating}
onClick={handleUpdate}
>
{isUpdating ? '更新中...' : '立即更新'}
</Button>
<Button
theme="default"
size="small"
variant="text"
onClick={handleDismiss}
disabled={isUpdating}
>
稍后
</Button>
</div>
</div>
);
};
export default VersionUpdate;
-2
View File
@@ -40,7 +40,6 @@ import { SettingsDialog } from './components/settings-dialog/settings-dialog';
import { buildTextLinkPlugin } from './plugins/with-text-link';
import { LinkPopup } from './components/popup/link-popup/link-popup';
import { useI18n, I18nProvider } from './i18n';
import { VersionUpdate } from './components/version-update';
import { withVideo } from './plugins/with-video';
import { TaskToolbar } from './components/task-queue/TaskToolbar';
import { useTaskStorage } from './hooks/useTaskStorage';
@@ -204,7 +203,6 @@ export const Drawnix: React.FC<DrawnixProps> = ({
<SettingsDialog container={containerRef.current}></SettingsDialog>
</Wrapper>
<TaskToolbar />
<VersionUpdate />
</div>
</DrawnixContext.Provider>
</I18nProvider>