优化构建
This commit is contained in:
1 parent
f81ee54a3d
commit
e783e1fb0c
12 files changed
+2172
-247
No files matched your search
Generated
+2094
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user