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) {
[data-admin-root] *,
body.admin-overlays * {
transition: none !important;
animation: none !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}

View File

@@ -12,11 +12,28 @@ import "./plugins.css";
type UploadPluginModalProps = {
open: boolean;
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) {
const [activeTab, setActiveTab] = useState<"install" | "guide">("install");
const [isDraggingPlugin, setIsDraggingPlugin] = useState(false);
const [uploading, setUploading] = useState(false);
const [copied, setCopied] = useState(false);
const dragDepth = useRef(0);
const handlePluginDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
@@ -40,22 +57,74 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
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 (
<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="上传插件"
open={open}
centered
footer={null}
destroyOnHidden
onCancel={onClose}
styles={{ body: { maxHeight: "min(82vh, 900px)", overflowY: "auto", overscrollBehavior: "contain" } }}
mask={{ closable: !uploading }}
onCancel={uploading ? undefined : onClose}
styles={{ body: { maxHeight: "min(84vh, 900px)", overflowY: "auto", overscrollBehavior: "contain", padding: "16px 20px 24px" } }}
>
<div className="plugin-upload-layout">
<section className="plugin-upload-guide">
<PluginMarkdown source={pluginDevelopmentGuideMarkdown} />
</section>
<aside className="plugin-upload-panel">
<div className="plugin-upload-tabs-bar">
<div className="plugin-upload-tabs-nav">
<button
type="button"
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">
<span className="plugin-upload-panel-icon"><CloudUpload className="size-5" /></span>
<div>
@@ -63,8 +132,9 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
<p>选择统一站点插件包,安装后会立即进入插件中心。</p>
</div>
</div>
<div
className={`plugin-upload-dropzone-shell${isDraggingPlugin ? " is-dragging" : ""}`}
className={`plugin-upload-dropzone-shell${isDraggingPlugin ? " is-dragging" : ""}${uploading ? " is-uploading" : ""}`}
onDragEnter={handlePluginDragEnter}
onDragLeave={handlePluginDragLeave}
onDragOver={(event) => event.preventDefault()}
@@ -72,25 +142,54 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
>
<Upload.Dragger
className="plugin-upload-dropzone"
accept=".yingce-plugin,application/zip"
accept=".yingce-plugin,.zip"
maxCount={1}
disabled={uploading}
showUploadList={false}
beforeUpload={(file) => {
onUpload(file);
void handleExecuteUpload(file);
return false;
}}
>
<CloudUpload className="plugin-upload-dropzone-icon" />
<p className="ant-upload-text">{isDraggingPlugin ? "释放文件以上传插件" : "点击选择插件文件,也可拖拽到此处"}</p>
<p className="ant-upload-hint">支持 .yingce-plugin 包 · 大小不超过 48 MiB</p>
{uploading ? (
<div className="py-6 flex flex-col items-center justify-center gap-3">
<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>
</div>
<div className="plugin-upload-notice">
<ShieldCheck className="size-4" />
<ShieldCheck className="size-4 shrink-0 text-status-success" />
<span>上传前请确认插件来源可信。Web 入口只能进入声明的隔离运行时,不会获得主页面权限;密钥也不会从清单读取。</span>
</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>
);
}

View File

@@ -485,7 +485,135 @@
}
.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 {