redesign
This commit is contained in:
99
src/App.vue
99
src/App.vue
@@ -23,8 +23,22 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app">
|
||||
<div class="project-manager">
|
||||
<div class="page">
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
<div class="brand">
|
||||
<svg class="brand-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
||||
<path d="M2 17l10 5 10-5"/>
|
||||
<path d="M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
<span class="brand-name">港湾</span>
|
||||
</div>
|
||||
<p class="brand-tagline">自托管 HTML 项目发布平台</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<ProjectUpload @project-uploaded="handleProjectUpdated" />
|
||||
|
||||
<ProjectList
|
||||
@@ -35,20 +49,83 @@ onMounted(() => {
|
||||
@clear-search="clearSearch"
|
||||
@project-updated="handleProjectUpdated"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.project-manager {
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
border-left: 1px solid var(--color-border);
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 32px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.header-inner {
|
||||
padding: 12px 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,184 +42,296 @@ const handleProjectUpdated = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="projects-section">
|
||||
<div class="projects-header">
|
||||
<h2>📋 我的托管项目</h2>
|
||||
<div class="search-container">
|
||||
<div class="search-box">
|
||||
<div class="list-card">
|
||||
<div class="list-header">
|
||||
<div class="list-title-group">
|
||||
<div class="list-icon-wrap">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="list-title">我的项目</h2>
|
||||
<p class="list-subtitle">{{ projects.length }} 个项目</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<div class="search-input-wrap">
|
||||
<svg class="search-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchInput"
|
||||
type="text"
|
||||
placeholder="搜索项目名称..."
|
||||
type="search"
|
||||
placeholder="搜索项目..."
|
||||
class="search-input"
|
||||
@input="handleSearchInput"
|
||||
@keyup.enter="handleSearch"
|
||||
aria-label="搜索项目"
|
||||
/>
|
||||
<button
|
||||
@click="handleSearch"
|
||||
class="search-button"
|
||||
:disabled="!searchInput.trim()"
|
||||
>
|
||||
🔍 搜索
|
||||
</button>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="handleClearSearch"
|
||||
class="clear-search-button"
|
||||
>
|
||||
✕ 清除
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="handleSearch"
|
||||
class="search-btn"
|
||||
:disabled="!searchInput.trim()"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="handleClearSearch"
|
||||
class="clear-btn"
|
||||
aria-label="清除搜索"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="searchQuery" class="search-results-info">
|
||||
<p>搜索 "{{ searchQuery }}" 的结果 ({{ projects.length }} 个项目)</p>
|
||||
<div v-if="searchQuery" class="search-results-banner">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
搜索 "<strong>{{ searchQuery }}</strong>" — 找到 {{ projects.length }} 个结果
|
||||
</div>
|
||||
|
||||
<div v-if="projects.length === 0" class="empty-state">
|
||||
<p v-if="searchQuery">没有找到匹配的项目,请尝试其他搜索词。</p>
|
||||
<p v-else>还没有托管任何项目,上传一个HTML文件开始吧!</p>
|
||||
</div>
|
||||
<div class="list-body">
|
||||
<div v-if="projects.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="empty-title">
|
||||
{{ searchQuery ? "没有找到匹配的项目" : "还没有项目" }}
|
||||
</p>
|
||||
<p class="empty-hint">
|
||||
{{ searchQuery ? "试试其他关键词" : "上传一个HTML文件开始使用吧" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="projects-grid">
|
||||
<ProjectCard
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
:api-base="apiBase"
|
||||
@project-updated="handleProjectUpdated"
|
||||
/>
|
||||
<div v-else class="projects-grid">
|
||||
<ProjectCard
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
:api-base="apiBase"
|
||||
@project-updated="handleProjectUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.projects-section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
.list-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.projects-header h2 {
|
||||
.list-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-600);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.list-subtitle {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-gray-400);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
padding: 8px 12px 8px 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
outline: none;
|
||||
width: 220px;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
border-color: var(--color-border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.search-button,
|
||||
.clear-search-button {
|
||||
padding: 8px 12px;
|
||||
.search-input::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 8px 14px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
.search-btn:hover:not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.search-button:hover:not(:disabled) {
|
||||
background: #5a6fd8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.search-button:disabled {
|
||||
background: #ccc;
|
||||
.search-btn:disabled {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-gray-400);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.clear-search-button {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-600);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clear-search-button:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
.clear-btn:hover {
|
||||
background: var(--color-gray-200);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.search-results-info {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #667eea;
|
||||
.search-results-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 24px;
|
||||
font-size: 13px;
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
border-bottom: 1px solid var(--color-primary-border);
|
||||
}
|
||||
|
||||
.search-results-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
.list-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-400);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 响应式设计:小屏幕时切换为单列 */
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 900px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
@media (max-width: 640px) {
|
||||
.list-header {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
@@ -227,7 +339,11 @@ const handleProjectUpdated = () => {
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,41 +12,64 @@ const emit = defineEmits<Emits>()
|
||||
const { message: uploadMessage, messageType, showMessage } = useMessage()
|
||||
const projectName = ref("")
|
||||
const isUploading = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
const validateFile = (file: File): boolean => {
|
||||
if (!file.name.endsWith(".html")) {
|
||||
showMessage("请选择HTML文件", "error")
|
||||
return false
|
||||
}
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
showMessage("文件大小不能超过5MB", "error")
|
||||
return false
|
||||
}
|
||||
if (file.size === 0) {
|
||||
showMessage("文件不能为空", "error")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleFileSelect = () => {
|
||||
const file = fileInput.value?.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// 文件类型验证
|
||||
if (!file.name.endsWith(".html")) {
|
||||
showMessage("请选择HTML文件", "error")
|
||||
return
|
||||
if (validateFile(file)) {
|
||||
uploadProject(file)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件大小验证 (5MB)
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
showMessage("文件大小不能超过5MB", "error")
|
||||
return
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
const file = event.dataTransfer?.files?.[0]
|
||||
if (!file) return
|
||||
if (validateFile(file)) {
|
||||
uploadProject(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (file.size === 0) {
|
||||
showMessage("文件不能为空", "error")
|
||||
return
|
||||
}
|
||||
|
||||
uploadProject(file)
|
||||
const triggerFileSelect = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const uploadProject = async (file: File) => {
|
||||
if (!projectName.value.trim()) {
|
||||
showMessage("请输入项目名称", "error")
|
||||
showMessage("请先输入项目名称", "error")
|
||||
return
|
||||
}
|
||||
|
||||
if (projectName.value.length > 50) {
|
||||
showMessage("项目名称不能超过50个字符", "error")
|
||||
if (projectName.value.length > 20) {
|
||||
showMessage("项目名称不能超过20个字符", "error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,10 +81,7 @@ const uploadProject = async (file: File) => {
|
||||
formData.append("projectName", projectName.value)
|
||||
|
||||
const result = await api.uploadProject(formData)
|
||||
showMessage(
|
||||
`项目 "${result.name}" 上传成功!访问地址: ${result.url}`,
|
||||
"success"
|
||||
)
|
||||
showMessage(`"${result.name}" 上传成功`, "success")
|
||||
emit("project-uploaded")
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
@@ -69,7 +89,6 @@ const uploadProject = async (file: File) => {
|
||||
error instanceof Error ? error.message : "上传失败,请检查网络连接",
|
||||
"error"
|
||||
)
|
||||
console.error("上传错误:", error)
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
}
|
||||
@@ -84,164 +103,269 @@ const resetForm = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-section">
|
||||
<h2>🚀 立即出发</h2>
|
||||
<p class="description">上传HTML文件,立即获得在线访问链接。</p>
|
||||
<div class="upload-card">
|
||||
<div class="upload-header">
|
||||
<div class="upload-icon-wrap">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="upload-title">上传项目</h2>
|
||||
<p class="upload-subtitle">上传HTML文件,立即获得在线访问链接</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-form">
|
||||
<div class="form-group">
|
||||
<label for="projectName">项目名称:</label>
|
||||
<div class="upload-body">
|
||||
<div class="form-field">
|
||||
<label class="field-label" for="projectName">项目名称</label>
|
||||
<input
|
||||
v-model="projectName"
|
||||
type="text"
|
||||
id="projectName"
|
||||
placeholder="请输入项目名称"
|
||||
class="form-input"
|
||||
placeholder="为你的项目起个名字"
|
||||
class="field-input"
|
||||
maxlength="20"
|
||||
:disabled="isUploading"
|
||||
/>
|
||||
<small class="input-hint">最多20个字符</small>
|
||||
<span class="field-hint">最多20个字符</span>
|
||||
</div>
|
||||
|
||||
<div class="file-upload">
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".html"
|
||||
@change="handleFileSelect"
|
||||
style="display: none"
|
||||
:disabled="isUploading"
|
||||
/>
|
||||
<button
|
||||
@click="fileInput?.click()"
|
||||
:disabled="isUploading"
|
||||
class="upload-button"
|
||||
>
|
||||
{{ isUploading ? "上传中..." : "选择HTML文件" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".html"
|
||||
@change="handleFileSelect"
|
||||
style="display: none"
|
||||
:disabled="isUploading"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="uploadMessage"
|
||||
class="upload-message"
|
||||
:class="{
|
||||
success: messageType === 'success',
|
||||
error: messageType === 'error',
|
||||
}"
|
||||
>
|
||||
{{ uploadMessage }}
|
||||
<div
|
||||
class="drop-zone"
|
||||
:class="{ 'drop-zone--dragging': isDragging, 'drop-zone--disabled': isUploading }"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@click="triggerFileSelect"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter="triggerFileSelect"
|
||||
@keydown.space.prevent="triggerFileSelect"
|
||||
:aria-label="isUploading ? '上传中' : '点击选择或拖拽HTML文件到此处'"
|
||||
>
|
||||
<div class="drop-zone-content">
|
||||
<div class="drop-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="12" y1="18" x2="12" y2="12"/>
|
||||
<line x1="9" y1="15" x2="15" y2="15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="drop-main-text">
|
||||
{{ isUploading ? "上传中..." : "点击选择或拖拽HTML文件到此处" }}
|
||||
</p>
|
||||
<p class="drop-hint">支持 .html 文件,最大 5MB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="uploadMessage"
|
||||
class="feedback-message"
|
||||
:class="messageType === 'success' ? 'feedback-message--success' : 'feedback-message--error'"
|
||||
role="alert"
|
||||
>
|
||||
<svg v-if="messageType === 'success'" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
{{ uploadMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
.upload-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.upload-icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.upload-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.upload-body {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
.field-input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
outline: none;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
border-color: var(--color-border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.field-input:disabled {
|
||||
background: var(--color-gray-50);
|
||||
color: var(--color-text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px 24px;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-base), background var(--transition-base);
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.upload-section h2 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
.drop-zone:focus-visible {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0 0 20px 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
.drop-zone:hover:not(.drop-zone--disabled) {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.project-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
.drop-zone--dragging {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.drop-zone--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.drop-zone-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
.drop-icon {
|
||||
color: var(--color-gray-400);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
.drop-zone:hover:not(.drop-zone--disabled) .drop-icon,
|
||||
.drop-zone--dragging .drop-icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.drop-main-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
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;
|
||||
.drop-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
.feedback-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.upload-button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
.feedback-message svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.upload-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
.feedback-message--success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
border: 1px solid var(--color-success-border);
|
||||
}
|
||||
|
||||
.upload-message {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.upload-message:not(.success) {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
.feedback-message--error {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
border: 1px solid var(--color-danger-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,69 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
/* Design Tokens */
|
||||
:root {
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-hover: #1d4ed8;
|
||||
--color-primary-light: #eff6ff;
|
||||
--color-primary-border: #bfdbfe;
|
||||
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-hover: #b91c1c;
|
||||
--color-danger-light: #fef2f2;
|
||||
--color-danger-border: #fecaca;
|
||||
|
||||
--color-success: #16a34a;
|
||||
--color-success-light: #f0fdf4;
|
||||
--color-success-border: #bbf7d0;
|
||||
|
||||
--color-warning: #d97706;
|
||||
--color-warning-light: #fffbeb;
|
||||
--color-warning-border: #fde68a;
|
||||
|
||||
--color-gray-50: #f8fafc;
|
||||
--color-gray-100: #f1f5f9;
|
||||
--color-gray-200: #e2e8f0;
|
||||
--color-gray-300: #cbd5e1;
|
||||
--color-gray-400: #94a3b8;
|
||||
--color-gray-500: #64748b;
|
||||
--color-gray-600: #475569;
|
||||
--color-gray-700: #334155;
|
||||
--color-gray-800: #1e293b;
|
||||
--color-gray-900: #0f172a;
|
||||
|
||||
--color-surface: #ffffff;
|
||||
--color-bg: #f1f5f9;
|
||||
--color-text: #1e293b;
|
||||
--color-text-muted: #64748b;
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-focus: #2563eb;
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.07), 0 4px 6px -4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
Reference in New Issue
Block a user