fix(admin): 优化插件拖拽上传与价格开关提示 (#386)

This commit is contained in:
Rou
2026-09-02 20:33:38 +08:00
committed by GitHub
parent 5dd515b8ab
commit 2ee97d45bc
5 changed files with 77 additions and 19 deletions

View File

@@ -746,7 +746,7 @@ function PriceTierFields({
<div className="admin-price-tier-toggle"> <div className="admin-price-tier-toggle">
<div> <div>
<strong>可供用户使用</strong> <strong>可供用户使用</strong>
<span>关闭后保留配置,但用户请求不再匹配这档价格</span> <span>开启后参与用户请求匹配;关闭后仅保留配置,不参与计费匹配</span>
</div> </div>
<Switch <Switch
aria-label="可供用户使用" aria-label="可供用户使用"

View File

@@ -1,5 +1,6 @@
import { Modal, Upload } from "antd"; import { Modal, Upload } from "antd";
import { CloudUpload, FileText, ShieldCheck } from "lucide-react"; import { CloudUpload, FileText, ShieldCheck } from "lucide-react";
import { useRef, useState } from "react";
import type { RegisteredPlugin } from "@/lib/plugins/plugin-types"; import type { RegisteredPlugin } from "@/lib/plugins/plugin-types";
@@ -15,6 +16,30 @@ type UploadPluginModalProps = {
}; };
export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModalProps) { export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModalProps) {
const [isDraggingPlugin, setIsDraggingPlugin] = useState(false);
const dragDepth = useRef(0);
const handlePluginDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
dragDepth.current += 1;
setIsDraggingPlugin(true);
};
const handlePluginDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
dragDepth.current -= 1;
if (dragDepth.current <= 0) {
dragDepth.current = 0;
setIsDraggingPlugin(false);
}
};
const handlePluginDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
dragDepth.current = 0;
setIsDraggingPlugin(false);
};
return ( return (
<Modal <Modal
className="workspace-modal workspace-modal-wide plugin-upload-modal" className="workspace-modal workspace-modal-wide plugin-upload-modal"
@@ -38,20 +63,28 @@ export function UploadPluginModal({ open, onClose, onUpload }: UploadPluginModal
<p>选择统一影策插件包,安装后会立即进入插件中心。</p> <p>选择统一影策插件包,安装后会立即进入插件中心。</p>
</div> </div>
</div> </div>
<Upload.Dragger <div
className="plugin-upload-dropzone" className={`plugin-upload-dropzone-shell${isDraggingPlugin ? " is-dragging" : ""}`}
accept=".yingce-plugin,application/zip" onDragEnter={handlePluginDragEnter}
maxCount={1} onDragLeave={handlePluginDragLeave}
showUploadList={false} onDragOver={(event) => event.preventDefault()}
beforeUpload={(file) => { onDrop={handlePluginDrop}
onUpload(file);
return false;
}}
> >
<CloudUpload className="plugin-upload-dropzone-icon" /> <Upload.Dragger
<p className="ant-upload-text">点击选择插件文件</p> className="plugin-upload-dropzone"
<p className="ant-upload-hint">仅支持 .yingce-plugin 包,大小不超过 16 MiB</p> accept=".yingce-plugin,application/zip"
</Upload.Dragger> maxCount={1}
showUploadList={false}
beforeUpload={(file) => {
onUpload(file);
return false;
}}
>
<CloudUpload className="plugin-upload-dropzone-icon" />
<p className="ant-upload-text">{isDraggingPlugin ? "释放文件以上传插件" : "点击选择插件文件,也可拖拽到此处"}</p>
<p className="ant-upload-hint">支持 .yingce-plugin 包 · 大小不超过 16 MiB</p>
</Upload.Dragger>
</div>
<div className="plugin-upload-notice"> <div className="plugin-upload-notice">
<ShieldCheck className="size-4" /> <ShieldCheck className="size-4" />
<span>上传前请确认插件来源可信。Web 入口只能进入声明的隔离运行时,不会获得主页面权限;密钥也不会从清单读取。</span> <span>上传前请确认插件来源可信。Web 入口只能进入声明的隔离运行时,不会获得主页面权限;密钥也不会从清单读取。</span>

View File

@@ -542,6 +542,12 @@
border-color: var(--workspace-accent); border-color: var(--workspace-accent);
} }
.plugin-upload-dropzone-shell.is-dragging .plugin-upload-dropzone.ant-upload-wrapper .ant-upload-drag {
border-color: var(--workspace-accent);
background: color-mix(in srgb, var(--workspace-accent) 11%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--workspace-accent) 24%, transparent);
}
.plugin-upload-dropzone-icon { .plugin-upload-dropzone-icon {
width: 30px; width: 30px;
height: 30px; height: 30px;

View File

@@ -8520,7 +8520,7 @@
.admin-price-tier-controls { .admin-price-tier-controls {
display: grid !important; display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: minmax(0, 1fr);
gap: 10px !important; gap: 10px !important;
} }
@@ -8528,7 +8528,7 @@
display: flex; display: flex;
min-width: 0; min-width: 0;
min-height: 58px; min-height: 58px;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
border: 1px solid var(--admin-card-border); border: 1px solid var(--admin-card-border);
@@ -8539,6 +8539,7 @@
.admin-price-tier-toggle > div { .admin-price-tier-toggle > div {
display: grid; display: grid;
flex: 1 1 auto;
min-width: 0; min-width: 0;
gap: 2px; gap: 2px;
} }
@@ -8551,12 +8552,11 @@
} }
.admin-price-tier-toggle span { .admin-price-tier-toggle span {
overflow: hidden; overflow-wrap: anywhere;
color: var(--admin-text-muted); color: var(--admin-text-muted);
font-size: 9px; font-size: 9px;
line-height: 1.4; line-height: 1.4;
text-overflow: ellipsis; white-space: normal;
white-space: nowrap;
} }
.admin-model-editor-add-tier.ant-btn { .admin-model-editor-add-tier.ant-btn {

View File

@@ -33,6 +33,25 @@ test("announcement editor preserves image and pinned fields through edit and sav
expect(safetySource).toContain("pinned?: boolean"); expect(safetySource).toContain("pinned?: boolean");
}); });
test("plugin upload owns native drops and price availability text remains readable", async () => {
const [pluginSource, adminCss] = await Promise.all([
Bun.file(new URL("../src/pages/plugins/plugin-documentation-modals.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(),
]);
const toggleCss = sourceSection(adminCss, ".admin-price-tier-toggle span {", ".admin-model-editor-add-tier.ant-btn {");
expect(pluginSource).toContain("event.preventDefault()");
expect(pluginSource).toContain("onDragOver={(event)");
expect(pluginSource).toContain("onDrop={handlePluginDrop}");
expect(pluginSource).toContain("点击选择插件文件,也可拖拽到此处");
expect(pluginSource).toContain("释放文件以上传插件");
expect(pluginSource).toContain("isDraggingPlugin");
expect(compactSource(adminCss)).toContain(".admin-price-tier-controls { display: grid !important; grid-template-columns: minmax(0, 1fr);");
expect(toggleCss).toContain("overflow-wrap: anywhere;");
expect(toggleCss).toContain("white-space: normal;");
expect(toggleCss).not.toContain("text-overflow: ellipsis;");
});
test("analytics keeps fixed range presets distinct and uses enabled channel models for pricing", async () => { test("analytics keeps fixed range presets distinct and uses enabled channel models for pricing", async () => {
const source = compactSource(await Bun.file(new URL("../src/pages/admin/components/analytics-panel.tsx", import.meta.url)).text()); const source = compactSource(await Bun.file(new URL("../src/pages/admin/components/analytics-panel.tsx", import.meta.url)).text());