mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
[优化] 操作优化
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
presslygoose "github.com/pressly/goose/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const versionPagesProjectRootDir int64 = 202606040002
|
||||
|
||||
// migration202606040002 adds RootDir and EntryFile fields to Pages projects.
|
||||
func migration202606040002(backend string, ctx Context) *presslygoose.Migration {
|
||||
return newGORMMigration(
|
||||
versionPagesProjectRootDir,
|
||||
"202606040002_add_pages_project_root_dir.go",
|
||||
backend,
|
||||
ctx,
|
||||
migratePagesProjectRootDir,
|
||||
)
|
||||
}
|
||||
|
||||
func migratePagesProjectRootDir(ctx Context, db *gorm.DB, backend string) error {
|
||||
if err := ctx.ApplyCurrentSchema(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
// Verify that the columns exist
|
||||
if !db.Migrator().HasColumn("pages_projects", "root_dir") {
|
||||
return fmt.Errorf("column pages_projects.root_dir is missing")
|
||||
}
|
||||
if !db.Migrator().HasColumn("pages_projects", "entry_file") {
|
||||
return fmt.Errorf("column pages_projects.entry_file is missing")
|
||||
}
|
||||
// Backfill existing rows: if entry_file is empty, set to 'index.html'
|
||||
type PagesProject struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
EntryFile string `gorm:"size:512;not null;default:'index.html'"`
|
||||
}
|
||||
if err := db.Model(&PagesProject{}).Where("entry_file = '' OR entry_file IS NULL").Update("entry_file", "index.html").Error; err != nil {
|
||||
return fmt.Errorf("failed to backfill pages_projects.entry_file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -46,6 +46,7 @@ func registeredMigrations(backend string, ctx Context) []*presslygoose.Migration
|
||||
migration202606030003(backend, ctx),
|
||||
migration202606030004(backend, ctx),
|
||||
migration202606040001(backend, ctx),
|
||||
migration202606040002(backend, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ type PagesProject struct {
|
||||
APIProxyPass string `json:"api_proxy_pass" gorm:"size:2048;not null;default:''"`
|
||||
APIProxyRewrite string `json:"api_proxy_rewrite" gorm:"size:255;not null;default:''"`
|
||||
ActiveDeploymentID *uint `json:"active_deployment_id" gorm:"index"`
|
||||
RootDir string `json:"root_dir" gorm:"size:512;not null;default:''"`
|
||||
EntryFile string `json:"entry_file" gorm:"size:512;not null;default:'index.html'"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ type PagesProjectInput struct {
|
||||
APIProxyPath string `json:"api_proxy_path"`
|
||||
APIProxyPass string `json:"api_proxy_pass"`
|
||||
APIProxyRewrite string `json:"api_proxy_rewrite"`
|
||||
RootDir string `json:"root_dir"`
|
||||
EntryFile string `json:"entry_file"`
|
||||
}
|
||||
|
||||
type PagesProjectView struct {
|
||||
@@ -55,6 +57,8 @@ type PagesProjectView struct {
|
||||
APIProxyPath string `json:"api_proxy_path"`
|
||||
APIProxyPass string `json:"api_proxy_pass"`
|
||||
APIProxyRewrite string `json:"api_proxy_rewrite"`
|
||||
RootDir string `json:"root_dir"`
|
||||
EntryFile string `json:"entry_file"`
|
||||
ActiveDeploymentID *uint `json:"active_deployment_id"`
|
||||
ActiveDeployment *PagesDeploymentView `json:"active_deployment,omitempty"`
|
||||
DeploymentCount int64 `json:"deployment_count"`
|
||||
@@ -151,6 +155,8 @@ func UpdatePagesProject(id uint, input PagesProjectInput) (*PagesProjectView, er
|
||||
"api_proxy_path": project.APIProxyPath,
|
||||
"api_proxy_pass": project.APIProxyPass,
|
||||
"api_proxy_rewrite": project.APIProxyRewrite,
|
||||
"root_dir": project.RootDir,
|
||||
"entry_file": project.EntryFile,
|
||||
}).Error; err != nil {
|
||||
if model.IsUniqueConstraintError(err) {
|
||||
return nil, errors.New("Pages 项目标识已存在")
|
||||
@@ -241,11 +247,11 @@ func UploadPagesDeployment(projectID uint, fileHeader *multipart.FileHeader, roo
|
||||
if !strings.EqualFold(filepath.Ext(fileHeader.Filename), ".zip") {
|
||||
return nil, errors.New("Pages 部署包必须是 .zip 文件")
|
||||
}
|
||||
rootDir, err = validateAndNormalizePagesRootDir(rootDir)
|
||||
rootDir, err = validateAndNormalizePagesRootDir(project.RootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entryFile = normalizePagesEntryFile(entryFile)
|
||||
entryFile = normalizePagesEntryFile(project.EntryFile)
|
||||
tempPath, checksum, err := persistPagesUploadTemp(fileHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -485,6 +491,13 @@ func buildPagesProject(project *model.PagesProject, input PagesProjectInput) (*m
|
||||
project.APIProxyPass = apiProxyPass
|
||||
project.APIProxyRewrite = apiProxyRewrite
|
||||
|
||||
rootDir, err := validateAndNormalizePagesRootDir(input.RootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
project.RootDir = rootDir
|
||||
project.EntryFile = normalizePagesEntryFile(input.EntryFile)
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
@@ -504,6 +517,8 @@ func buildPagesProjectView(project *model.PagesProject) (*PagesProjectView, erro
|
||||
APIProxyPath: project.APIProxyPath,
|
||||
APIProxyPass: project.APIProxyPass,
|
||||
APIProxyRewrite: project.APIProxyRewrite,
|
||||
RootDir: project.RootDir,
|
||||
EntryFile: project.EntryFile,
|
||||
ActiveDeploymentID: project.ActiveDeploymentID,
|
||||
CreatedAt: project.CreatedAt,
|
||||
UpdatedAt: project.UpdatedAt,
|
||||
|
||||
@@ -331,15 +331,17 @@ func TestUploadPagesDeploymentWithRootDir(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
RootDir: "build",
|
||||
EntryFile: "index.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
|
||||
// 1. Upload a zip with files inside a subfolder, specifying "build" as rootDir.
|
||||
// 1. Upload a zip with files inside a subfolder.
|
||||
uploadHeader := multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{
|
||||
"build/index.html": "<h1>App Root</h1>",
|
||||
"build/static/bundle.js": "console.log('app')",
|
||||
@@ -359,12 +361,34 @@ func TestUploadPagesDeploymentWithRootDir(t *testing.T) {
|
||||
t.Fatalf("expected EntryFile to be 'index.html', got %q", deployment.EntryFile)
|
||||
}
|
||||
|
||||
// 2. Try uploading with wrong entry file relative to root directory, should fail
|
||||
// 2. Update project configuration to a wrong entry file relative to root directory, upload should fail
|
||||
project, err = UpdatePagesProject(project.ID, PagesProjectInput{
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
RootDir: "build",
|
||||
EntryFile: "missing.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePagesProject failed: %v", err)
|
||||
}
|
||||
_, err = UploadPagesDeployment(project.ID, uploadHeader, "build", "missing.html", "root")
|
||||
if err == nil || !strings.Contains(err.Error(), "缺少入口文件") {
|
||||
t.Fatalf("expected failure for missing entry file, got %v", err)
|
||||
}
|
||||
|
||||
// Revert to correct config for snapshot check
|
||||
project, err = UpdatePagesProject(project.ID, PagesProjectInput{
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
RootDir: "build",
|
||||
EntryFile: "index.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePagesProject failed: %v", err)
|
||||
}
|
||||
|
||||
// 3. Test config snapshot LocalRoot path rendering
|
||||
project, err = ActivatePagesDeployment(project.ID, deployment.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { UploadCloud, File as FileIcon, X } from 'lucide-react';
|
||||
|
||||
import { AppModal } from '@/components/ui/app-modal';
|
||||
import {
|
||||
@@ -10,10 +11,9 @@ import {
|
||||
} from '@/features/pages/api/pages';
|
||||
import {
|
||||
PrimaryButton,
|
||||
ResourceField,
|
||||
ResourceInput,
|
||||
SecondaryButton,
|
||||
} from '@/features/shared/components/resource-primitives';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import {
|
||||
deploymentsQueryKey,
|
||||
projectQueryKey,
|
||||
@@ -26,6 +26,15 @@ interface PagesDeploymentUploadModalProps {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function PagesDeploymentUploadModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -33,17 +42,19 @@ export function PagesDeploymentUploadModal({
|
||||
}: PagesDeploymentUploadModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [rootDir, setRootDir] = useState('');
|
||||
const [entryFile, setEntryFile] = useState('index.html');
|
||||
const [isDragActive, setIsDragActive] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const resetForm = () => {
|
||||
setFile(null);
|
||||
setRootDir('');
|
||||
setEntryFile('index.html');
|
||||
setIsDragActive(false);
|
||||
setUploadProgress(null);
|
||||
setErrorMessage(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -51,6 +62,31 @@ export function PagesDeploymentUploadModal({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setIsDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
setIsDragActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragActive(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
const droppedFile = e.dataTransfer.files[0];
|
||||
if (droppedFile.name.toLowerCase().endsWith('.zip')) {
|
||||
setFile(droppedFile);
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
setErrorMessage('仅支持 zip 格式的文件');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: async ({ shouldActivate }: { shouldActivate: boolean }) => {
|
||||
if (!file) {
|
||||
@@ -62,8 +98,8 @@ export function PagesDeploymentUploadModal({
|
||||
const deployment = await uploadPagesDeployment(
|
||||
projectId,
|
||||
file,
|
||||
rootDir,
|
||||
entryFile,
|
||||
'',
|
||||
'index.html',
|
||||
(percent) => {
|
||||
setUploadProgress(percent);
|
||||
},
|
||||
@@ -106,7 +142,7 @@ export function PagesDeploymentUploadModal({
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title="上传部署包"
|
||||
description="上传已构建的 zip 静态资源包,默认入口为 index.html。"
|
||||
description="上传已构建的 zip 静态资源包。项目将使用配置的根目录与入口文件路径。"
|
||||
footer={
|
||||
<div className="flex flex-wrap justify-end gap-3">
|
||||
<SecondaryButton
|
||||
@@ -140,36 +176,98 @@ export function PagesDeploymentUploadModal({
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<ResourceField
|
||||
label="部署包"
|
||||
hint="仅支持 zip,Server 会校验文件数量、体积、路径逃逸和入口文件。"
|
||||
error={errorMessage ?? undefined}
|
||||
>
|
||||
<ResourceInput
|
||||
type="file"
|
||||
accept=".zip,application/zip"
|
||||
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label="根目录 (可选)"
|
||||
hint="静态资源的根文件夹路径 (例如 build 或 dist),若为空则默认为 zip 包根目录。"
|
||||
>
|
||||
<ResourceInput
|
||||
value={rootDir}
|
||||
onChange={(event) => setRootDir(event.target.value)}
|
||||
placeholder="例如: build"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label="入口文件"
|
||||
hint="基于根目录下的入口文件路径。"
|
||||
>
|
||||
<ResourceInput
|
||||
value={entryFile}
|
||||
onChange={(event) => setEntryFile(event.target.value)}
|
||||
/>
|
||||
</ResourceField>
|
||||
{/* Hidden File Input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".zip,application/zip"
|
||||
onChange={(event) => {
|
||||
const selectedFile = event.target.files?.[0] ?? null;
|
||||
if (selectedFile) {
|
||||
if (selectedFile.name.toLowerCase().endsWith('.zip')) {
|
||||
setFile(selectedFile);
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
setErrorMessage('仅支持 zip 格式的文件');
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="block space-y-2">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
<span>部署包</span>
|
||||
</span>
|
||||
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={cn(
|
||||
'group relative flex min-h-48 cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-[var(--border-default)] bg-[var(--surface-elevated)] p-6 text-center transition-all duration-200 hover:border-[var(--brand-primary)] hover:bg-[var(--surface-hover)]',
|
||||
isDragActive &&
|
||||
'scale-[0.99] border-[var(--brand-primary)] bg-[var(--accent-soft)]/20 shadow-inner',
|
||||
file && 'border-solid border-[var(--brand-primary)]',
|
||||
)}
|
||||
>
|
||||
{file ? (
|
||||
<div
|
||||
className="flex flex-col items-center space-y-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--accent-soft)] text-[var(--brand-primary)] transition duration-200 group-hover:scale-110">
|
||||
<FileIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="max-w-[300px] truncate text-sm font-medium text-[var(--foreground-primary)]">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--foreground-secondary)]">
|
||||
大小: {formatBytes(file.size)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}}
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--border-default)] bg-[var(--control-background)] px-3 text-xs font-medium text-[var(--foreground-primary)] transition hover:border-[var(--status-danger-border)] hover:bg-[var(--status-danger-soft)] hover:text-[var(--status-danger-foreground)]"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
清除文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pointer-events-none flex flex-col items-center space-y-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--surface-muted)] text-[var(--foreground-muted)] transition duration-200 group-hover:scale-110 group-hover:bg-[var(--accent-soft)] group-hover:text-[var(--brand-primary)]">
|
||||
<UploadCloud className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-[var(--foreground-primary)]">
|
||||
点击或拖拽 zip 部署包到此处
|
||||
</p>
|
||||
<p className="text-xs text-[var(--foreground-secondary)]">
|
||||
仅支持 zip,Server 会校验文件数量、体积、路径逃逸和入口文件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<span className="mt-1 block text-xs text-[var(--status-danger-foreground)]">
|
||||
{errorMessage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{uploadProgress !== null && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs font-medium text-[var(--foreground-secondary)]">
|
||||
|
||||
@@ -40,6 +40,8 @@ function toFormValues(project?: PagesProject | null): PagesProjectFormValues {
|
||||
api_proxy_path: '',
|
||||
api_proxy_pass: '',
|
||||
api_proxy_rewrite: '',
|
||||
root_dir: '',
|
||||
entry_file: 'index.html',
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -52,6 +54,8 @@ function toFormValues(project?: PagesProject | null): PagesProjectFormValues {
|
||||
api_proxy_path: project.api_proxy_path || '',
|
||||
api_proxy_pass: project.api_proxy_pass || '',
|
||||
api_proxy_rewrite: project.api_proxy_rewrite || '',
|
||||
root_dir: project.root_dir || '',
|
||||
entry_file: project.entry_file || 'index.html',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +101,8 @@ export function PagesProjectEditorModal({
|
||||
api_proxy_rewrite: values.api_proxy_enabled
|
||||
? values.api_proxy_rewrite.trim()
|
||||
: '',
|
||||
root_dir: values.root_dir?.trim() || '',
|
||||
entry_file: values.entry_file?.trim() || 'index.html',
|
||||
};
|
||||
return project
|
||||
? updatePagesProject(project.id, payload)
|
||||
@@ -185,6 +191,27 @@ export function PagesProjectEditorModal({
|
||||
{...form.register('description')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label="根目录 (可选)"
|
||||
hint="静态资源的根文件夹路径 (例如 build 或 dist),若为空则默认为 zip 包根目录。"
|
||||
error={form.formState.errors.root_dir?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
placeholder="例如: build"
|
||||
{...form.register('root_dir')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField
|
||||
label="入口文件"
|
||||
hint="基于根目录下的入口文件路径。"
|
||||
error={form.formState.errors.entry_file?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
placeholder="index.html"
|
||||
{...form.register('entry_file')}
|
||||
required
|
||||
/>
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label="启用 SPA fallback"
|
||||
description="开启后未命中的路径会回退到指定文件,适合 React/Vue history 路由。"
|
||||
|
||||
@@ -42,13 +42,7 @@ export function PagesProjectListItem({ project }: { project: PagesProject }) {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid shrink-0 grid-cols-2 gap-3 text-sm md:min-w-80">
|
||||
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-muted)] px-4 py-3">
|
||||
<p className="text-xs text-[var(--foreground-secondary)]">部署数</p>
|
||||
<p className="mt-1 font-semibold text-[var(--foreground-primary)]">
|
||||
{project.deployment_count}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid shrink-0 grid-cols-1 gap-3 text-sm md:min-w-40">
|
||||
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-muted)] px-4 py-3">
|
||||
<p className="text-xs text-[var(--foreground-secondary)]">
|
||||
当前激活
|
||||
|
||||
@@ -25,6 +25,17 @@ export const pagesProjectSchema = z
|
||||
api_proxy_path: z.string().trim(),
|
||||
api_proxy_pass: z.string().trim(),
|
||||
api_proxy_rewrite: z.string().trim(),
|
||||
root_dir: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(512, '根目录不能超过 512 个字符')
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
entry_file: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, '请输入入口文件')
|
||||
.max(512, '入口文件不能超过 512 个字符'),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.spa_fallback_enabled) {
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface PagesProject {
|
||||
api_proxy_path: string;
|
||||
api_proxy_pass: string;
|
||||
api_proxy_rewrite: string;
|
||||
root_dir?: string;
|
||||
entry_file: string;
|
||||
active_deployment_id?: number | null;
|
||||
active_deployment?: PagesDeployment | null;
|
||||
deployment_count: number;
|
||||
@@ -43,4 +45,6 @@ export interface PagesProjectPayload {
|
||||
api_proxy_path: string;
|
||||
api_proxy_pass: string;
|
||||
api_proxy_rewrite: string;
|
||||
root_dir?: string;
|
||||
entry_file: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user