This commit is contained in:
2025-10-21 18:50:12 +08:00
parent 2ab8b06c6a
commit 476944d22e
3 changed files with 73 additions and 30 deletions

View File

@@ -5,15 +5,13 @@ import { useProjects } from "./composables/useProjects"
const { projects, fetchProjects } = useProjects() const { projects, fetchProjects } = useProjects()
const getApiBase = () => { function getApiBase() {
if (window.location.hostname !== "localhost") { if (window.location.hostname !== "localhost") {
return "/api" return `${window.location.protocol}//${window.location.host}/api`
} }
return "http://localhost:3000/api" return "http://localhost:3000/api"
} }
const API_BASE = getApiBase()
onMounted(() => { onMounted(() => {
fetchProjects() fetchProjects()
}) })
@@ -23,7 +21,7 @@ onMounted(() => {
<div class="app"> <div class="app">
<ProjectManager <ProjectManager
:projects="projects" :projects="projects"
:api-base="API_BASE" :api-base="getApiBase()"
@project-uploaded="fetchProjects" @project-uploaded="fetchProjects"
/> />
</div> </div>

View File

@@ -24,11 +24,27 @@ const fileInput = ref<HTMLInputElement>()
const handleFileSelect = () => { const handleFileSelect = () => {
const file = fileInput.value?.files?.[0] const file = fileInput.value?.files?.[0]
if (file && file.name.endsWith(".html")) { if (!file) return
uploadProject(file)
} else { // 文件类型验证
if (!file.name.endsWith(".html")) {
showMessage("请选择HTML文件", "error") showMessage("请选择HTML文件", "error")
return
} }
// 文件大小验证 (5MB)
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
showMessage("文件大小不能超过5MB", "error")
return
}
if (file.size === 0) {
showMessage("文件不能为空", "error")
return
}
uploadProject(file)
} }
const uploadProject = async (file: File) => { const uploadProject = async (file: File) => {
@@ -37,6 +53,11 @@ const uploadProject = async (file: File) => {
return return
} }
if (projectName.value.length > 50) {
showMessage("项目名称不能超过50个字符", "error")
return
}
isUploading.value = true isUploading.value = true
try { try {
@@ -129,7 +150,10 @@ const deleteProject = async (projectSlug: string, projectName: string) => {
id="projectName" id="projectName"
placeholder="请输入项目名称" placeholder="请输入项目名称"
class="form-input" class="form-input"
maxlength="50"
:disabled="isUploading"
/> />
<small class="input-hint">最多50个字符</small>
</div> </div>
<div class="file-upload"> <div class="file-upload">
@@ -139,6 +163,7 @@ const deleteProject = async (projectSlug: string, projectName: string) => {
accept=".html" accept=".html"
@change="handleFileSelect" @change="handleFileSelect"
style="display: none" style="display: none"
:disabled="isUploading"
/> />
<button <button
@click="fileInput?.click()" @click="fileInput?.click()"
@@ -294,13 +319,25 @@ const deleteProject = async (projectSlug: string, projectName: string) => {
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
} }
.form-input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.input-hint {
color: #666;
font-size: 12px;
margin-top: 4px;
display: block;
}
.upload-button { .upload-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border: none; border: none;
padding: 15px 30px; padding: 10px 20px;
font-size: 16px; font-size: 14px;
border-radius: 25px; border-radius: 20px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
font-weight: 500; font-weight: 500;
@@ -468,11 +505,12 @@ const deleteProject = async (projectSlug: string, projectName: string) => {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
text-decoration: none; text-decoration: none;
padding: 10px 15px; padding: 8px 12px;
border-radius: 6px; border-radius: 6px;
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
transition: all 0.3s ease; transition: all 0.3s ease;
font-size: 13px;
} }
.visit-button:hover { .visit-button:hover {
@@ -484,11 +522,12 @@ const deleteProject = async (projectSlug: string, projectName: string) => {
.delete-button { .delete-button {
color: white; color: white;
border: none; border: none;
padding: 10px 15px; padding: 8px 12px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: all 0.3s ease; transition: all 0.3s ease;
font-size: 13px;
} }
.copy-button { .copy-button {
@@ -514,11 +553,12 @@ const deleteProject = async (projectSlug: string, projectName: string) => {
.deactivate-button { .deactivate-button {
color: white; color: white;
border: none; border: none;
padding: 10px 15px; padding: 8px 12px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: all 0.3s ease; transition: all 0.3s ease;
font-size: 13px;
} }
.activate-button { .activate-button {

View File

@@ -1,10 +1,8 @@
import type { Project, UploadResponse, ToggleResponse } from '../types' import type { Project, UploadResponse, ToggleResponse } from "../types"
// 动态获取API基础URL
const getApiBase = () => { const getApiBase = () => {
// 在Docker环境中前端通过Caddy代理访问后端
if (window.location.hostname !== 'localhost') { if (window.location.hostname !== 'localhost') {
return '/api' return `${window.location.protocol}//${window.location.host}/api`
} }
// 开发环境直接访问后端 // 开发环境直接访问后端
return 'http://localhost:3000/api' return 'http://localhost:3000/api'
@@ -15,7 +13,7 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
const API_BASE = getApiBase() const API_BASE = getApiBase()
const response = await fetch(`${API_BASE}${url}`, { const response = await fetch(`${API_BASE}${url}`, {
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
...options.headers, ...options.headers,
}, },
...options, ...options,
@@ -24,7 +22,7 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
const data = await response.json() const data = await response.json()
if (!response.ok) { if (!response.ok) {
throw new Error(data.error || '请求失败') throw new Error(data.error || "请求失败")
} }
return data return data
@@ -33,25 +31,32 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
// API方法 // API方法
export const api = { export const api = {
// 获取项目列表 // 获取项目列表
getProjects: (): Promise<Project[]> => request<Project[]>('/projects'), getProjects: (): Promise<Project[]> => request<Project[]>("/projects"),
// 上传项目 // 上传项目
uploadProject: (formData: FormData): Promise<UploadResponse> => uploadProject: (formData: FormData): Promise<UploadResponse> => {
request<UploadResponse>('/upload', { const API_BASE = getApiBase()
method: 'POST', return fetch(`${API_BASE}/upload`, {
method: "POST",
body: formData, body: formData,
headers: {}, // 让浏览器自动设置Content-Type }).then(async (response) => {
}), const data = await response.json()
if (!response.ok) {
throw new Error(data.error || "请求失败")
}
return data
})
},
// 切换项目状态 // 切换项目状态
toggleProject: (slug: string): Promise<ToggleResponse> => toggleProject: (slug: string): Promise<ToggleResponse> =>
request<ToggleResponse>(`/projects/${slug}/toggle`, { request<ToggleResponse>(`/projects/${slug}/toggle`, {
method: 'PATCH', method: "PATCH",
}), }),
// 删除项目 // 删除项目
deleteProject: (slug: string): Promise<{ message: string }> => deleteProject: (slug: string): Promise<{ message: string }> =>
request<{ message: string }>(`/projects/${slug}`, { request<{ message: string }>(`/projects/${slug}`, {
method: 'DELETE', method: "DELETE",
}), }),
} }