fix(plugins): 插件管理 - 合并 PR #544 上传弹窗与减弱动画修复

Merge pull request #544 from zwm417. Preserve the original author and current main behavior.
This commit is contained in:
daoge05178
2026-09-20 04:56:42 +08:00
committed by ddCat
3 changed files with 250 additions and 21 deletions

View File

@@ -974,7 +974,9 @@
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
[data-admin-root] *, [data-admin-root] *,
body.admin-overlays * { body.admin-overlays * {
transition: none !important; transition-duration: 0.01ms !important;
animation: none !important; animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
} }
} }

View File

@@ -12,11 +12,28 @@ import "./plugins.css";
type UploadPluginModalProps = { type UploadPluginModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onUpload: (file: File) => void; onUpload: (file: File) => void | Promise<void>;
}; };
const MINIMAL_MANIFEST_SNIPPET = `{
"apiVersion": "yingce.plugin/v1",
"id": "my-custom-plugin",
"name": "我的自定义插件",
"version": "1.0.0",
"author": "Developer",
"description": "通过统一清单扩展平台功能",
"permissions": ["generation.run", "media.read"],
"contributes": {
"providers": [],
"workflows": []
}
}`;
export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModalProps) { export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModalProps) {
const [activeTab, setActiveTab] = useState<"install" | "guide">("install");
const [isDraggingPlugin, setIsDraggingPlugin] = useState(false); const [isDraggingPlugin, setIsDraggingPlugin] = useState(false);
const [uploading, setUploading] = useState(false);
const [copied, setCopied] = useState(false);
const dragDepth = useRef(0); const dragDepth = useRef(0);
const handlePluginDragEnter = (event: React.DragEvent<HTMLDivElement>) => { const handlePluginDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
@@ -40,22 +57,74 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
setIsDraggingPlugin(false); setIsDraggingPlugin(false);
}; };
const handleExecuteUpload = async (file: File) => {
setUploading(true);
try {
await onUpload(file);
} finally {
setUploading(false);
}
};
const copySnippet = () => {
void navigator.clipboard?.writeText(MINIMAL_MANIFEST_SNIPPET).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
const isCompact = activeTab === "install";
return ( return (
<Modal <Modal
className="workspace-modal workspace-modal-wide plugin-upload-modal" className={`workspace-modal plugin-upload-modal ${isCompact ? "plugin-upload-modal-compact" : "workspace-modal-wide"}`}
title="上传插件" title="上传插件"
open={open} open={open}
centered centered
footer={null} footer={null}
destroyOnHidden destroyOnHidden
onCancel={onClose} mask={{ closable: !uploading }}
styles={{ body: { maxHeight: "min(82vh, 900px)", overflowY: "auto", overscrollBehavior: "contain" } }} onCancel={uploading ? undefined : onClose}
styles={{ body: { maxHeight: "min(84vh, 900px)", overflowY: "auto", overscrollBehavior: "contain", padding: "16px 20px 24px" } }}
> >
<div className="plugin-upload-layout"> <div className="plugin-upload-tabs-bar">
<section className="plugin-upload-guide"> <div className="plugin-upload-tabs-nav">
<PluginMarkdown source={pluginDevelopmentGuideMarkdown} /> <button
</section> type="button"
<aside className="plugin-upload-panel"> className={`plugin-upload-tab-btn ${activeTab === "install" ? "is-active" : ""}`}
onClick={() => setActiveTab("install")}
>
<CloudUpload className="size-4" />
<span>安装插件包</span>
</button>
<button
type="button"
className={`plugin-upload-tab-btn ${activeTab === "guide" ? "is-active" : ""}`}
onClick={() => setActiveTab("guide")}
>
<FileText className="size-4" />
<span>开发规范与示例</span>
</button>
</div>
{activeTab === "guide" ? (
<button
type="button"
className="plugin-upload-copy-btn"
onClick={copySnippet}
title="复制最小 manifest.json 配置模板"
>
<span>{copied ? "已复制清单" : "复制最小清单"}</span>
</button>
) : (
<div className="plugin-upload-badge">
<ShieldCheck className="size-3.5 text-status-success" />
<span>沙箱受控隔离</span>
</div>
)}
</div>
{activeTab === "install" ? (
<div className="plugin-upload-install-view">
<div className="plugin-upload-panel-heading"> <div className="plugin-upload-panel-heading">
<span className="plugin-upload-panel-icon"><CloudUpload className="size-5" /></span> <span className="plugin-upload-panel-icon"><CloudUpload className="size-5" /></span>
<div> <div>
@@ -63,8 +132,9 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
<p>选择统一站点插件包,安装后会立即进入插件中心。</p> <p>选择统一站点插件包,安装后会立即进入插件中心。</p>
</div> </div>
</div> </div>
<div <div
className={`plugin-upload-dropzone-shell${isDraggingPlugin ? " is-dragging" : ""}`} className={`plugin-upload-dropzone-shell${isDraggingPlugin ? " is-dragging" : ""}${uploading ? " is-uploading" : ""}`}
onDragEnter={handlePluginDragEnter} onDragEnter={handlePluginDragEnter}
onDragLeave={handlePluginDragLeave} onDragLeave={handlePluginDragLeave}
onDragOver={(event) => event.preventDefault()} onDragOver={(event) => event.preventDefault()}
@@ -72,25 +142,54 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
> >
<Upload.Dragger <Upload.Dragger
className="plugin-upload-dropzone" className="plugin-upload-dropzone"
accept=".yingce-plugin,application/zip" accept=".yingce-plugin,.zip"
maxCount={1} maxCount={1}
disabled={uploading}
showUploadList={false} showUploadList={false}
beforeUpload={(file) => { beforeUpload={(file) => {
onUpload(file); void handleExecuteUpload(file);
return false; return false;
}} }}
> >
<CloudUpload className="plugin-upload-dropzone-icon" /> {uploading ? (
<p className="ant-upload-text">{isDraggingPlugin ? "释放文件以上传插件" : "点击选择插件文件,也可拖拽到此处"}</p> <div className="py-6 flex flex-col items-center justify-center gap-3">
<p className="ant-upload-hint">支持 .yingce-plugin 包 · 大小不超过 48 MiB</p> <div className="size-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<p className="ant-upload-text">正在安装插件包,请稍候...</p>
<p className="ant-upload-hint">正在解压、校验清单合法性与版本声明</p>
</div>
) : (
<>
<CloudUpload className="plugin-upload-dropzone-icon" />
<p className="ant-upload-text">{isDraggingPlugin ? "释放文件以上传插件" : "点击选择插件文件,也可拖拽到此处"}</p>
<p className="ant-upload-hint">支持 .yingce-plugin 包 · 大小不超过 48 MiB</p>
</>
)}
</Upload.Dragger> </Upload.Dragger>
</div> </div>
<div className="plugin-upload-notice"> <div className="plugin-upload-notice">
<ShieldCheck className="size-4" /> <ShieldCheck className="size-4 shrink-0 text-status-success" />
<span>上传前请确认插件来源可信。Web 入口只能进入声明的隔离运行时,不会获得主页面权限;密钥也不会从清单读取。</span> <span>上传前请确认插件来源可信。Web 入口只能进入声明的隔离运行时,不会获得主页面权限;密钥也不会从清单读取。</span>
</div> </div>
</aside>
</div> <div className="plugin-upload-switch-guide-hint">
<span>初次制作插件?</span>
<button type="button" onClick={() => setActiveTab("guide")} className="plugin-upload-link-btn">
查看《开发与打包规范说明》
</button>
</div>
</div>
) : (
<div className="plugin-upload-guide-view">
<div className="plugin-upload-guide-banner">
<ShieldCheck className="size-4 shrink-0 text-primary" />
<span>以下为开发者技术规范与清单编写参考(只读文档)。安装插件请切换至「安装插件包」。</span>
</div>
<section className="plugin-upload-guide">
<PluginMarkdown source={pluginDevelopmentGuideMarkdown} />
</section>
</div>
)}
</Modal> </Modal>
); );
} }

View File

@@ -485,7 +485,135 @@
} }
.workspace-modal.plugin-upload-modal { .workspace-modal.plugin-upload-modal {
width: min(1480px, calc(100vw - var(--space-4))) !important; width: min(1120px, calc(100vw - var(--space-4))) !important;
}
.workspace-modal.plugin-upload-modal.plugin-upload-modal-compact {
width: min(620px, calc(100vw - var(--space-4))) !important;
}
.plugin-upload-tabs-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--workspace-border);
}
.plugin-upload-tabs-nav {
display: inline-flex;
align-items: center;
padding: 3px;
border-radius: var(--r-md);
background: color-mix(in srgb, var(--foreground) 6%, transparent);
gap: 2px;
}
.plugin-upload-tab-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border: none;
border-radius: var(--r-sm);
background: transparent;
color: color-mix(in srgb, var(--foreground) 60%, transparent);
font-size: var(--fs-label);
font-weight: 550;
cursor: pointer;
transition: all 0.18s ease;
}
.plugin-upload-tab-btn:hover {
color: var(--foreground);
}
.plugin-upload-tab-btn.is-active {
background: var(--workspace-surface-strong, #ffffff);
color: var(--workspace-accent, #1677ff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.plugin-upload-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: var(--fs-caption);
color: color-mix(in srgb, var(--foreground) 50%, transparent);
}
.plugin-upload-copy-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border: 1px solid var(--workspace-border);
border-radius: var(--r-sm);
background: transparent;
color: color-mix(in srgb, var(--foreground) 75%, transparent);
font-size: var(--fs-caption);
cursor: pointer;
transition: all 0.15s ease;
}
.plugin-upload-copy-btn:hover {
border-color: var(--workspace-accent);
color: var(--workspace-accent);
}
.plugin-upload-install-view {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.plugin-upload-dropzone-shell.is-uploading {
pointer-events: none;
opacity: 0.85;
}
.plugin-upload-switch-guide-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: var(--fs-label);
color: color-mix(in srgb, var(--foreground) 45%, transparent);
margin-top: var(--space-1);
}
.plugin-upload-link-btn {
background: transparent;
border: none;
padding: 0;
color: var(--workspace-accent, #1677ff);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
font-size: inherit;
}
.plugin-upload-link-btn:hover {
opacity: 0.8;
}
.plugin-upload-guide-view {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.plugin-upload-guide-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: var(--r-md);
background: color-mix(in srgb, var(--workspace-accent) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--workspace-accent) 20%, transparent);
font-size: var(--fs-label);
color: color-mix(in srgb, var(--foreground) 75%, transparent);
} }
.plugin-upload-layout { .plugin-upload-layout {