From 79820b33eb2807730e966223a2a779dcf6daa60f Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 17 Jul 2026 17:55:12 +0800 Subject: [PATCH] feat(pages): import deployment packages from URL Add upload-from-url so admins can paste an HTTP(S) link and let the control plane download the archive with browser-like headers. Private/LAN hosts and insecure TLS certificates are allowed for internal artifact stores; package size and format checks reuse the existing local-upload pipeline. --- docs/changelog/index.md | 1 + docs/design/pages-design.md | 2 +- docs/docs.go | 216 +++++++++++++- docs/swagger.json | 216 +++++++++++++- docs/swagger.yaml | 135 ++++++++- .../settings/components/openflare-ops.tsx | 3 +- .../components/deployment-upload-dialog.tsx | 233 ++++++++++----- .../lib/services/openflare/pages.service.ts | 11 + frontend/lib/services/openflare/types.ts | 4 + internal/apps/openflare/pages/download_url.go | 274 ++++++++++++++++++ .../apps/openflare/pages/download_url_test.go | 110 +++++++ internal/apps/openflare/pages/errs.go | 4 + internal/apps/openflare/pages/logics.go | 52 +++- internal/apps/openflare/pages/routers.go | 37 ++- .../router/v1/openflare/register_pages.go | 1 + 15 files changed, 1196 insertions(+), 103 deletions(-) create mode 100644 internal/apps/openflare/pages/download_url.go create mode 100644 internal/apps/openflare/pages/download_url_test.go diff --git a/docs/changelog/index.md b/docs/changelog/index.md index b9ac2460..0064f25d 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -34,6 +34,7 @@ sidebar: false - 明确 Pages 历史部署保留语义为每个项目最多 N 条(激活必留、其余按从新到旧填充),裁剪失败会记录日志且不回滚已成功上传。 - 主配置版本与 Pages 部署双轨管理:Agent 以 Pages 项目 ID 为锚点请求最新激活部署包,项目内切换激活版本无需重新发布主配置即可在边缘自动更新。 - Agent 边缘仅保留每个 Pages 项目的最新部署包,新包就绪并切换成功后清理旧 release;拉取 latest 时增加 hash 二次校验与重试以降低激活竞态失败;单个项目同步失败不再阻塞同批其它项目。 +- Pages 支持从 URL 导入部署包:填写下载链接后由控制面代为拉取并创建部署。 ## [v3.3.0] - 2026-07-14 diff --git a/docs/design/pages-design.md b/docs/design/pages-design.md index 799ba6ef..be0f4fc8 100644 --- a/docs/design/pages-design.md +++ b/docs/design/pages-design.md @@ -19,7 +19,7 @@ ## 核心功能 Pages 静态托管子系统包含以下核心能力: -* **Direct Upload 部署模式**:支持直接上传预构建的静态资源压缩包(`zip`、`tar.gz`、`tar.xz`、`tar.bz2`、`tar`、`7z`),省去复杂的 Git 集成和构建环境依赖。 +* **Direct Upload 部署模式**:支持直接上传预构建的静态资源压缩包(`zip`、`tar.gz`、`tar.xz`、`tar.bz2`、`tar`、`7z`),或填写 URL 由控制面代为下载导入(允许内网地址与自签证书),省去复杂的 Git 集成和构建环境依赖。 * **不可变部署快照**:每次上传产生一个带唯一 ID 和 SHA-256 Checksum 的不可变部署记录。支持按系统配置保留最近 N 个历史部署,并可随时激活和回滚。 * **SPA Fallback 支持**:支持对单页应用(SPA)进行 Fallback 路由配置,请求找不到静态文件时自动重定向到入口文件。 * **内置 API 反代服务**:支持在 Pages 规则内一键启用 API 代理,消除跨域问题,将请求转发给指定的后端服务。 diff --git a/docs/docs.go b/docs/docs.go index 1a25dfa5..76715016 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4475,7 +4475,7 @@ const docTemplate = `{ "AgentTokenAuth": [] } ], - "description": "返回 upload 框架记录的 SHA-256 哈希,供 Agent 对比本地缓存并按需拉取部署包", + "description": "返回 upload 框架记录的 SHA-256 哈希,供 Agent 对比本地缓存并按需拉取部署包(兼容旧路径)", "produces": [ "application/json" ], @@ -4533,7 +4533,7 @@ const docTemplate = `{ "AgentTokenAuth": [] } ], - "description": "流式下载指定部署的静态资源压缩包,供 Agent 边缘分发", + "description": "流式下载指定部署的静态资源压缩包,供 Agent 边缘分发(兼容旧路径)", "produces": [ "application/octet-stream" ], @@ -4572,6 +4572,110 @@ const docTemplate = `{ } } }, + "/api/v1/agent/pages/projects/{project_id}/latest/hash": { + "get": { + "security": [ + { + "AgentTokenAuth": [] + } + ], + "description": "按项目 ID 返回当前激活部署的包哈希(类似 latest 指针),Agent 无需关心具体部署 ID", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-agent" + ], + "summary": "查询 Pages 项目最新激活部署哈希", + "parameters": [ + { + "type": "integer", + "description": "Pages 项目 ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_Rain-kl_Wavelet_pkg_protocol.PagesProjectLatestHashResponse" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "Token 无效", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/agent/pages/projects/{project_id}/latest/package": { + "get": { + "security": [ + { + "AgentTokenAuth": [] + } + ], + "description": "按项目 ID 下载当前激活部署的压缩包,供 Agent 边缘分发", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "openflare-agent" + ], + "summary": "下载 Pages 项目最新激活部署包", + "parameters": [ + { + "type": "integer", + "description": "Pages 项目 ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "部署包文件", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "Token 无效", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/agent/waf/ip-groups/sync": { "post": { "security": [ @@ -8198,7 +8302,7 @@ const docTemplate = `{ "SessionCookie": [] } ], - "description": "为指定项目上传 ZIP 部署包,需要管理员权限", + "description": "为指定项目上传静态资源压缩包(zip/tar.gz/tar.xz/tar.bz2/tar/7z),需要管理员权限", "consumes": [ "multipart/form-data" ], @@ -8219,7 +8323,7 @@ const docTemplate = `{ }, { "type": "file", - "description": "部署包 ZIP 文件", + "description": "部署包文件", "name": "package", "in": "formData", "required": true @@ -8271,6 +8375,88 @@ const docTemplate = `{ } } }, + "/api/v1/d/pages/{id}/deployments/upload-from-url": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "从用户提供的 HTTP(S) 链接下载部署包并创建部署记录;服务端使用浏览器伪装请求头拉取,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "从 URL 导入 Pages 部署包", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "下载链接", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pages.UploadFromURLInput" + } + } + ], + "responses": { + "200": { + "description": "部署记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.DeploymentView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/d/pages/{id}/deployments/{deployment_id}/activate": { "post": { "security": [ @@ -14220,6 +14406,20 @@ const docTemplate = `{ } } }, + "github_com_Rain-kl_Wavelet_pkg_protocol.PagesProjectLatestHashResponse": { + "type": "object", + "properties": { + "deployment_id": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "project_id": { + "type": "integer" + } + } + }, "github_com_Rain-kl_Wavelet_pkg_protocol.WAFIPGroup": { "type": "object", "properties": { @@ -16821,6 +17021,14 @@ const docTemplate = `{ } } }, + "pages.UploadFromURLInput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + }, "pages.View": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index e35868c6..3b9a4146 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4468,7 +4468,7 @@ "AgentTokenAuth": [] } ], - "description": "返回 upload 框架记录的 SHA-256 哈希,供 Agent 对比本地缓存并按需拉取部署包", + "description": "返回 upload 框架记录的 SHA-256 哈希,供 Agent 对比本地缓存并按需拉取部署包(兼容旧路径)", "produces": [ "application/json" ], @@ -4526,7 +4526,7 @@ "AgentTokenAuth": [] } ], - "description": "流式下载指定部署的静态资源压缩包,供 Agent 边缘分发", + "description": "流式下载指定部署的静态资源压缩包,供 Agent 边缘分发(兼容旧路径)", "produces": [ "application/octet-stream" ], @@ -4565,6 +4565,110 @@ } } }, + "/api/v1/agent/pages/projects/{project_id}/latest/hash": { + "get": { + "security": [ + { + "AgentTokenAuth": [] + } + ], + "description": "按项目 ID 返回当前激活部署的包哈希(类似 latest 指针),Agent 无需关心具体部署 ID", + "produces": [ + "application/json" + ], + "tags": [ + "openflare-agent" + ], + "summary": "查询 Pages 项目最新激活部署哈希", + "parameters": [ + { + "type": "integer", + "description": "Pages 项目 ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_Rain-kl_Wavelet_pkg_protocol.PagesProjectLatestHashResponse" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "Token 无效", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/agent/pages/projects/{project_id}/latest/package": { + "get": { + "security": [ + { + "AgentTokenAuth": [] + } + ], + "description": "按项目 ID 下载当前激活部署的压缩包,供 Agent 边缘分发", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "openflare-agent" + ], + "summary": "下载 Pages 项目最新激活部署包", + "parameters": [ + { + "type": "integer", + "description": "Pages 项目 ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "部署包文件", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "Token 无效", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/agent/waf/ip-groups/sync": { "post": { "security": [ @@ -8191,7 +8295,7 @@ "SessionCookie": [] } ], - "description": "为指定项目上传 ZIP 部署包,需要管理员权限", + "description": "为指定项目上传静态资源压缩包(zip/tar.gz/tar.xz/tar.bz2/tar/7z),需要管理员权限", "consumes": [ "multipart/form-data" ], @@ -8212,7 +8316,7 @@ }, { "type": "file", - "description": "部署包 ZIP 文件", + "description": "部署包文件", "name": "package", "in": "formData", "required": true @@ -8264,6 +8368,88 @@ } } }, + "/api/v1/d/pages/{id}/deployments/upload-from-url": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "从用户提供的 HTTP(S) 链接下载部署包并创建部署记录;服务端使用浏览器伪装请求头拉取,需要管理员权限", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "openflare-pages" + ], + "summary": "从 URL 导入 Pages 部署包", + "parameters": [ + { + "type": "integer", + "description": "项目 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "下载链接", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pages.UploadFromURLInput" + } + } + ], + "responses": { + "200": { + "description": "部署记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/pages.DeploymentView" + } + } + } + ] + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "项目不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/d/pages/{id}/deployments/{deployment_id}/activate": { "post": { "security": [ @@ -14213,6 +14399,20 @@ } } }, + "github_com_Rain-kl_Wavelet_pkg_protocol.PagesProjectLatestHashResponse": { + "type": "object", + "properties": { + "deployment_id": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "project_id": { + "type": "integer" + } + } + }, "github_com_Rain-kl_Wavelet_pkg_protocol.WAFIPGroup": { "type": "object", "properties": { @@ -16814,6 +17014,14 @@ } } }, + "pages.UploadFromURLInput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + }, "pages.View": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 97f3f440..25ea1cc8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -778,6 +778,15 @@ definitions: hash: type: string type: object + github_com_Rain-kl_Wavelet_pkg_protocol.PagesProjectLatestHashResponse: + properties: + deployment_id: + type: integer + hash: + type: string + project_id: + type: integer + type: object github_com_Rain-kl_Wavelet_pkg_protocol.WAFIPGroup: properties: checksum: @@ -2499,6 +2508,11 @@ definitions: spa_fallback_path: type: string type: object + pages.UploadFromURLInput: + properties: + url: + type: string + type: object pages.View: properties: active_deployment: @@ -6641,7 +6655,7 @@ paths: - openflare-agent /api/v1/agent/pages/deployments/{deployment_id}/hash: get: - description: 返回 upload 框架记录的 SHA-256 哈希,供 Agent 对比本地缓存并按需拉取部署包 + description: 返回 upload 框架记录的 SHA-256 哈希,供 Agent 对比本地缓存并按需拉取部署包(兼容旧路径) parameters: - description: 部署 ID in: path @@ -6675,7 +6689,7 @@ paths: - openflare-agent /api/v1/agent/pages/deployments/{deployment_id}/package: get: - description: 流式下载指定部署的静态资源压缩包,供 Agent 边缘分发 + description: 流式下载指定部署的静态资源压缩包,供 Agent 边缘分发(兼容旧路径) parameters: - description: 部署 ID in: path @@ -6702,6 +6716,69 @@ paths: summary: 下载 Pages 部署包 tags: - openflare-agent + /api/v1/agent/pages/projects/{project_id}/latest/hash: + get: + description: 按项目 ID 返回当前激活部署的包哈希(类似 latest 指针),Agent 无需关心具体部署 ID + parameters: + - description: Pages 项目 ID + in: path + name: project_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/github_com_Rain-kl_Wavelet_pkg_protocol.PagesProjectLatestHashResponse' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: Token 无效 + schema: + $ref: '#/definitions/response.Any' + security: + - AgentTokenAuth: [] + summary: 查询 Pages 项目最新激活部署哈希 + tags: + - openflare-agent + /api/v1/agent/pages/projects/{project_id}/latest/package: + get: + description: 按项目 ID 下载当前激活部署的压缩包,供 Agent 边缘分发 + parameters: + - description: Pages 项目 ID + in: path + name: project_id + required: true + type: integer + produces: + - application/octet-stream + responses: + "200": + description: 部署包文件 + schema: + type: file + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: Token 无效 + schema: + $ref: '#/definitions/response.Any' + security: + - AgentTokenAuth: [] + summary: 下载 Pages 项目最新激活部署包 + tags: + - openflare-agent /api/v1/agent/waf/ip-groups/sync: post: consumes: @@ -8930,14 +9007,14 @@ paths: post: consumes: - multipart/form-data - description: 为指定项目上传 ZIP 部署包,需要管理员权限 + description: 为指定项目上传静态资源压缩包(zip/tar.gz/tar.xz/tar.bz2/tar/7z),需要管理员权限 parameters: - description: 项目 ID in: path name: id required: true type: integer - - description: 部署包 ZIP 文件 + - description: 部署包文件 in: formData name: package required: true @@ -8975,6 +9052,56 @@ paths: summary: 上传 Pages 部署包 tags: - openflare-pages + /api/v1/d/pages/{id}/deployments/upload-from-url: + post: + consumes: + - application/json + description: 从用户提供的 HTTP(S) 链接下载部署包并创建部署记录;服务端使用浏览器伪装请求头拉取,需要管理员权限 + parameters: + - description: 项目 ID + in: path + name: id + required: true + type: integer + - description: 下载链接 + in: body + name: request + required: true + schema: + $ref: '#/definitions/pages.UploadFromURLInput' + produces: + - application/json + responses: + "200": + description: 部署记录 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/pages.DeploymentView' + type: object + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 项目不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 从 URL 导入 Pages 部署包 + tags: + - openflare-pages /api/v1/d/pages/{id}/update: post: consumes: diff --git a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx index 3794fa9c..7c378b4e 100644 --- a/frontend/app/(main)/admin/settings/components/openflare-ops.tsx +++ b/frontend/app/(main)/admin/settings/components/openflare-ops.tsx @@ -468,7 +468,8 @@ export function OpenFlareOpsSettings() { />

- 每个项目最多保留 N 条部署:激活部署始终保留,其余按从新到旧填充;超出的非激活部署会在上传成功后自动清理。支持 + 每个项目最多保留 N + 条部署:激活部署始终保留,其余按从新到旧填充;超出的非激活部署会在上传成功后自动清理。支持 zip、tar.gz、tar.xz、tar.bz2、tar、7z 格式。

diff --git a/frontend/app/(main)/pages/components/deployment-upload-dialog.tsx b/frontend/app/(main)/pages/components/deployment-upload-dialog.tsx index a4573a4b..d47b0e83 100644 --- a/frontend/app/(main)/pages/components/deployment-upload-dialog.tsx +++ b/frontend/app/(main)/pages/components/deployment-upload-dialog.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { Loader2, UploadCloud } from 'lucide-react'; +import { Link2, Loader2, UploadCloud } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -17,6 +17,7 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { PagesService } from '@/lib/services/openflare'; import { cn } from '@/lib/utils'; @@ -61,12 +62,15 @@ export function DeploymentUploadDialog({ }: DeploymentUploadDialogProps) { const queryClient = useQueryClient(); const fileInputRef = useRef(null); + const [mode, setMode] = useState<'file' | 'url'>('file'); const [file, setFile] = useState(null); + const [packageURL, setPackageURL] = useState(''); const [isDragActive, setIsDragActive] = useState(false); const [uploadProgress, setUploadProgress] = useState(null); const resetForm = () => { setFile(null); + setPackageURL(''); setIsDragActive(false); setUploadProgress(null); if (fileInputRef.current) fileInputRef.current.value = ''; @@ -77,7 +81,17 @@ export function DeploymentUploadDialog({ onOpenChange(nextOpen); }; - const uploadMutation = useMutation({ + const invalidate = async () => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: deploymentsQueryKey(projectId), + }), + queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), + queryClient.invalidateQueries({ queryKey: projectsQueryKey }), + ]); + }; + + const uploadFileMutation = useMutation({ mutationFn: () => { if (!file) throw new Error('请选择部署包'); return PagesService.uploadDeployment(projectId, { @@ -87,13 +101,7 @@ export function DeploymentUploadDialog({ }, onSuccess: async () => { toast.success('部署包上传成功'); - await Promise.all([ - queryClient.invalidateQueries({ - queryKey: deploymentsQueryKey(projectId), - }), - queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), - queryClient.invalidateQueries({ queryKey: projectsQueryKey }), - ]); + await invalidate(); handleClose(false); }, onError: (error) => { @@ -102,6 +110,27 @@ export function DeploymentUploadDialog({ }, }); + const uploadURLMutation = useMutation({ + mutationFn: () => { + const url = packageURL.trim(); + if (!url) throw new Error('请填写部署包下载链接'); + if (!/^https?:\/\//i.test(url)) { + throw new Error('链接必须以 http:// 或 https:// 开头'); + } + return PagesService.uploadDeploymentFromURL(projectId, { url }); + }, + onSuccess: async () => { + toast.success('已从链接下载并创建部署'); + await invalidate(); + handleClose(false); + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : '从链接导入失败'); + }, + }); + + const isPending = uploadFileMutation.isPending || uploadURLMutation.isPending; + const handleFileSelect = (selected: File | null) => { if (!selected) return; if (!isSupportedPagesPackage(selected.name)) { @@ -111,80 +140,129 @@ export function DeploymentUploadDialog({ setFile(selected); }; + const handleSubmit = () => { + if (mode === 'file') { + uploadFileMutation.mutate(); + return; + } + uploadURLMutation.mutate(); + }; + + const canSubmit = + mode === 'file' ? Boolean(file) : packageURL.trim().length > 0; + return ( - + 上传部署包 - 上传已构建的静态资源压缩包(zip / tar.gz / tar.xz / tar.bz2 / tar / - 7z),部署后可在列表中激活。 + 支持本地上传或从 URL 下载静态资源压缩包(zip / tar.gz / tar.xz / + tar.bz2 / tar / 7z),创建部署后可在列表中激活。 -
{ - e.preventDefault(); - setIsDragActive(true); - }} - onDragOver={(e) => e.preventDefault()} - onDragLeave={(e) => { - e.preventDefault(); - setIsDragActive(false); - }} - onDrop={(e) => { - e.preventDefault(); - setIsDragActive(false); - handleFileSelect(e.dataTransfer.files[0] ?? null); - }} + setMode(value as 'file' | 'url')} + className='w-full' > - -

拖拽部署包到此处,或点击选择文件

-

- 支持 zip、tar.gz、tar.xz、tar.bz2、tar、7z -

- - handleFileSelect(e.target.files?.[0] ?? null)} - /> -
+ + 本地上传 + 从 URL 下载 + - {file ? ( -
-

{file.name}

-

- {formatBytes(file.size)} -

-
- ) : null} - - {uploadProgress !== null ? ( -
-
- {uploadProgress >= 100 ? '服务端处理中' : '上传进度'} - - {uploadProgress >= 100 ? '请稍候' : `${uploadProgress}%`} - + +
{ + e.preventDefault(); + setIsDragActive(true); + }} + onDragOver={(e) => e.preventDefault()} + onDragLeave={(e) => { + e.preventDefault(); + setIsDragActive(false); + }} + onDrop={(e) => { + e.preventDefault(); + setIsDragActive(false); + handleFileSelect(e.dataTransfer.files[0] ?? null); + }} + > + +

拖拽部署包到此处,或点击选择文件

+

+ 支持 zip、tar.gz、tar.xz、tar.bz2、tar、7z +

+ + handleFileSelect(e.target.files?.[0] ?? null)} + />
- = 100 ? 100 : uploadProgress} /> -
- ) : null} + + {file ? ( +
+

{file.name}

+

+ {formatBytes(file.size)} +

+
+ ) : null} + + {uploadProgress !== null ? ( +
+
+ + {uploadProgress >= 100 ? '服务端处理中' : '上传进度'} + + + {uploadProgress >= 100 ? '请稍候' : `${uploadProgress}%`} + +
+ = 100 ? 100 : uploadProgress} + /> +
+ ) : null} + + + +
+ +
+ + setPackageURL(e.target.value)} + disabled={isPending} + /> +
+

+ 服务端将使用浏览器环境请求头从该链接下载压缩包,支持内网地址与自签证书 + HTTPS。 +

+
+
+
@@ -195,15 +273,14 @@ export function DeploymentUploadDialog({ -