From 4fbb4d5cdef8d23684601508756578623b937819 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Fri, 12 Jun 2026 19:46:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E9=AA=8C=E8=AF=81=E7=A0=81=E5=8F=98?= =?UTF-8?q?=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- file-upload-site/app.py | 10 +- file-upload-site/docker-compose.yml | 3 + file-upload-site/style.css | 742 ++++++++++++++++++++++++++++ 3 files changed, 750 insertions(+), 5 deletions(-) create mode 100644 file-upload-site/style.css diff --git a/file-upload-site/app.py b/file-upload-site/app.py index 0e6b913..53db67c 100644 --- a/file-upload-site/app.py +++ b/file-upload-site/app.py @@ -90,7 +90,7 @@ def generate_captcha_text(length: int = 4) -> str: def generate_captcha_image(text: str) -> Image.Image: """生成验证码图片(含干扰线和噪点)""" - width, height = 140, 48 + width, height = 220, 70 # 浅色随机背景 bg_color = (random.randint(235, 255), random.randint(235, 255), random.randint(235, 255)) image = Image.new('RGB', (width, height), bg_color) @@ -104,20 +104,20 @@ def generate_captcha_image(text: str) -> Image.Image: draw.line([(x1, y1), (x2, y2)], fill=line_color, width=1) # 噪点 - for _ in range(80): + for _ in range(120): x, y = random.randint(0, width - 1), random.randint(0, height - 1) dot_color = (random.randint(140, 200), random.randint(140, 200), random.randint(140, 200)) draw.point((x, y), fill=dot_color) # 绘制文字 try: - font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 30) + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 50) except OSError: font = ImageFont.load_default() for i, char in enumerate(text): - x = 12 + i * 30 - y = random.randint(4, 10) + x = 16 + i * 50 + y = random.randint(4, 12) color = (random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)) draw.text((x, y), char, font=font, fill=color) diff --git a/file-upload-site/docker-compose.yml b/file-upload-site/docker-compose.yml index 581ac7c..da0dbe1 100644 --- a/file-upload-site/docker-compose.yml +++ b/file-upload-site/docker-compose.yml @@ -7,6 +7,9 @@ services: - "7001:5000" env_file: - .env + environment: + # 强制覆盖,确保容器内写入路径与 volume 挂载点一致 + - UPLOAD_ROOT=/app/uploads volumes: # 映射 NAS 目录到容器内(按实际 NAS 挂载路径修改冒号左侧) - /volume1/文字稿:/app/uploads diff --git a/file-upload-site/style.css b/file-upload-site/style.css new file mode 100644 index 0000000..bf93bff --- /dev/null +++ b/file-upload-site/style.css @@ -0,0 +1,742 @@ +/* ── 基础变量 ───────────────────────────────────────────── */ +:root { + --bg: #faf9f7; + --bg-card: #ffffff; + --bg-hover: #f5f4f2; + --ink: #1a1816; + --muted: #7a756e; + --border: #e8e5e0; + --border-focus: #c4a882; + --accent: #b45309; + --accent-hover: #92400e; + --accent-light: #fef3c7; + --success: #0f766e; + --success-bg: #f0fdfa; + --error: #dc2626; + --error-bg: #fef2f2; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); + --shadow-lg: 0 10px 40px rgba(0,0,0,0.06), 0 2px 10px rgba(0,0,0,0.04); + --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + --font-mono: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace; + --transition: 0.2s ease; +} + +/* ── 重置 ───────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font); + color: var(--ink); + background: var(--bg); + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 1rem; + line-height: 1.6; +} + +/* ── 容器 ───────────────────────────────────────────────── */ +.container { + width: 100%; + max-width: 520px; +} + +/* ── 头部 ───────────────────────────────────────────────── */ +.header { + text-align: center; + margin-bottom: 2rem; +} + +.header-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border-radius: 14px; + background: linear-gradient(135deg, var(--accent), #d97706); + color: white; + margin-bottom: 1rem; + box-shadow: 0 4px 14px rgba(180, 83, 9, 0.25); +} + +.header h1 { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--ink); + margin-bottom: 0.25rem; +} + +.subtitle { + font-size: 0.9rem; + color: var(--muted); +} + +/* ── 表单卡片 ───────────────────────────────────────────── */ +.form { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.75rem; + box-shadow: var(--shadow); +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-group:last-of-type { + margin-bottom: 1rem; +} + +.form-group label { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.85rem; + font-weight: 600; + color: var(--ink); + margin-bottom: 0.5rem; +} + +.form-group label svg { + color: var(--muted); +} + +.form-hint { + display: block; + font-size: 0.75rem; + color: var(--muted); + margin-top: 0.35rem; +} + +/* ── 输入框 ─────────────────────────────────────────────── */ +input[type="text"] { + width: 100%; + padding: 0.65rem 0.9rem; + font-size: 0.9rem; + font-family: var(--font); + color: var(--ink); + background: var(--bg); + border: 1.5px solid var(--border); + border-radius: var(--radius-sm); + outline: none; + transition: border-color var(--transition), box-shadow var(--transition); +} + +input[type="text"]::placeholder { + color: #b5b0a8; +} + +input[type="text"]:focus { + border-color: var(--border-focus); + box-shadow: 0 0 0 3px rgba(196, 168, 130, 0.15); +} + +input[type="text"].input-error { + border-color: var(--error); + box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1); +} + +/* ── 验证码行 ───────────────────────────────────────────── */ +.captcha-row { + display: flex; + gap: 10px; + align-items: stretch; +} + +.captcha-row input { + flex: 1; + min-width: 0; + letter-spacing: 0.15em; + text-transform: uppercase; +} + +.captcha-img { + height: 42px; + width: 140px; + border-radius: var(--radius-sm); + border: 1.5px solid var(--border); + cursor: pointer; + object-fit: cover; + transition: opacity var(--transition); + flex-shrink: 0; +} + +.captcha-img:hover { + opacity: 0.85; +} + +.btn-icon { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border: 1.5px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); + color: var(--muted); + cursor: pointer; + transition: all var(--transition); + flex-shrink: 0; +} + +.btn-icon:hover { + border-color: var(--border-focus); + color: var(--accent); + background: var(--accent-light); +} + +/* ── 拖拽区 ─────────────────────────────────────────────── */ +.dropzone { + border: 2px dashed var(--border); + border-radius: var(--radius); + padding: 2rem 1.5rem; + text-align: center; + cursor: pointer; + transition: all var(--transition); + background: var(--bg); +} + +.dropzone:hover, +.dropzone.dragover { + border-color: var(--accent); + background: var(--accent-light); +} + +.dropzone.dragover { + transform: scale(1.01); +} + +.dropzone-content { + pointer-events: none; +} + +.dropzone-icon { + color: var(--muted); + margin-bottom: 0.5rem; + transition: color var(--transition); +} + +.dropzone:hover .dropzone-icon, +.dropzone.dragover .dropzone-icon { + color: var(--accent); +} + +.dropzone-text { + font-size: 0.9rem; + color: var(--muted); + margin-bottom: 0.35rem; +} + +.dropzone-link { + color: var(--accent); + font-weight: 600; + text-decoration: underline; + text-decoration-thickness: 1.5px; + text-underline-offset: 2px; +} + +.dropzone-hint { + font-size: 0.75rem; + color: #b5b0a8; + line-height: 1.5; +} + +/* ── 文件列表 ───────────────────────────────────────────── */ +.file-list { + margin-bottom: 1rem; +} + +.file-item { + display: flex; + align-items: center; + gap: 10px; + padding: 0.6rem 0.85rem; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + margin-bottom: 6px; + animation: slideIn 0.25s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.file-item-icon { + flex-shrink: 0; + color: var(--muted); +} + +.file-item-info { + flex: 1; + min-width: 0; +} + +.file-item-name { + font-size: 0.85rem; + font-weight: 500; + color: var(--ink); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.file-item-meta { + font-size: 0.72rem; + color: var(--muted); + margin-top: 1px; +} + +.file-item-status { + flex-shrink: 0; + font-size: 0.78rem; + font-weight: 600; +} + +.file-item-status.status-waiting { + color: var(--muted); +} + +.file-item-status.status-uploading { + color: var(--accent); +} + +.file-item-status.status-done { + color: var(--success); +} + +.file-item-status.status-error { + color: var(--error); +} + +.file-item-remove { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--muted); + cursor: pointer; + transition: all var(--transition); +} + +.file-item-remove:hover { + background: var(--error-bg); + color: var(--error); +} + +/* 进度条 */ +.progress-bar { + width: 100%; + height: 3px; + background: var(--border); + border-radius: 2px; + margin-top: 6px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), #d97706); + border-radius: 2px; + transition: width 0.3s ease; + width: 0%; +} + +/* ── 按钮 ───────────────────────────────────────────────── */ +.form-actions { + display: flex; + gap: 10px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 0.7rem 1.5rem; + font-size: 0.9rem; + font-weight: 600; + font-family: var(--font); + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + flex: 1; + background: linear-gradient(135deg, var(--accent), #d97706); + color: white; + box-shadow: 0 2px 8px rgba(180, 83, 9, 0.2); +} + +.btn-primary:hover:not(:disabled) { + box-shadow: 0 4px 14px rgba(180, 83, 9, 0.35); + transform: translateY(-1px); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0); +} + +.btn-secondary { + background: var(--bg); + color: var(--muted); + border: 1.5px solid var(--border); +} + +.btn-secondary:hover { + border-color: var(--border-focus); + color: var(--ink); + background: var(--bg-hover); +} + +/* ── 结果面板 ───────────────────────────────────────────── */ +.result-panel { + margin-top: 1.25rem; + animation: slideIn 0.3s ease; +} + +.result-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + box-shadow: var(--shadow); +} + +.result-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 0.75rem; +} + +.result-icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + flex-shrink: 0; +} + +.result-icon.icon-success { + background: var(--success-bg); + color: var(--success); +} + +.result-icon.icon-error { + background: var(--error-bg); + color: var(--error); +} + +.result-title { + font-size: 1rem; + font-weight: 700; +} + +.result-title.text-success { + color: var(--success); +} + +.result-title.text-error { + color: var(--error); +} + +.result-detail { + font-size: 0.85rem; + color: var(--muted); + line-height: 1.7; +} + +.result-detail strong { + color: var(--ink); + font-weight: 600; +} + +.result-files { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); +} + +.result-file-item { + display: flex; + align-items: center; + gap: 8px; + padding: 0.35rem 0; + font-size: 0.82rem; +} + +.result-file-item svg { + flex-shrink: 0; +} + +.result-file-item.file-success { + color: var(--success); +} + +.result-file-item.file-error { + color: var(--error); +} + +/* ── 弹窗 ───────────────────────────────────────────────── */ +.modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + z-index: 1000; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.modal-overlay.active { + display: flex; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal { + background: var(--bg-card); + border-radius: 14px; + width: 100%; + max-width: 380px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 4px 20px rgba(0, 0, 0, 0.08); + animation: modalSlideUp 0.25s ease; +} + +@keyframes modalSlideUp { + from { + opacity: 0; + transform: translateY(20px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.1rem 1.25rem 0; +} + +.modal-header h3 { + font-size: 1.05rem; + font-weight: 700; + color: var(--ink); +} + +.modal-close { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 8px; + background: transparent; + color: var(--muted); + cursor: pointer; + transition: all var(--transition); +} + +.modal-close:hover { + background: var(--bg-hover); + color: var(--ink); +} + +.modal-body { + padding: 1rem 1.25rem; +} + +.modal-desc { + font-size: 0.85rem; + color: var(--muted); + margin-bottom: 1rem; +} + +.modal-captcha-row { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 0.75rem; +} + +.modal-captcha-img { + height: 70px; + flex: 1; + border-radius: var(--radius-sm); + border: 1.5px solid var(--border); + cursor: pointer; + object-fit: contain; + background: var(--bg); + transition: opacity var(--transition); +} + +.modal-captcha-img:hover { + opacity: 0.85; +} + +.modal-captcha-input { + width: 100%; + padding: 0.7rem 0.9rem; + font-size: 1.1rem; + font-family: var(--font); + color: var(--ink); + background: var(--bg); + border: 1.5px solid var(--border); + border-radius: var(--radius-sm); + outline: none; + letter-spacing: 0.2em; + text-transform: uppercase; + text-align: center; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.modal-captcha-input::placeholder { + letter-spacing: normal; + text-transform: none; + font-size: 0.85rem; + color: #b5b0a8; +} + +.modal-captcha-input:focus { + border-color: var(--border-focus); + box-shadow: 0 0 0 3px rgba(196, 168, 130, 0.15); +} + +.modal-error { + font-size: 0.8rem; + color: var(--error); + min-height: 1.2rem; + margin-top: 0.4rem; +} + +.modal-footer { + display: flex; + gap: 10px; + padding: 0 1.25rem 1.25rem; +} + +.modal-footer .btn { + flex: 1; + padding: 0.65rem 1rem; +} + +/* ── 页脚 ───────────────────────────────────────────────── */ +.footer { + margin-top: 2rem; + text-align: center; + font-size: 0.78rem; + color: #b5b0a8; +} + +/* ── 响应式 ─────────────────────────────────────────────── */ +@media (max-width: 560px) { + body { + padding: 1rem 0.75rem; + } + + .header { + margin-bottom: 1rem; + } + + .header-icon { + width: 40px; + height: 40px; + border-radius: 10px; + margin-bottom: 0.5rem; + } + + .header-icon svg { + width: 22px; + height: 22px; + } + + .header h1 { + font-size: 1.2rem; + } + + .subtitle { + font-size: 0.8rem; + } + + .form { + padding: 1.25rem; + } + + .dropzone { + padding: 1.5rem 1rem; + } + + .btn-icon { + width: 38px; + height: 38px; + } + + .form-actions { + flex-direction: column; + } +} + +/* ── 工具类 ─────────────────────────────────────────────── */ +.hidden { + display: none !important; +} + +.shake { + animation: shake 0.4s ease; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 20%, 60% { transform: translateX(-6px); } + 40%, 80% { transform: translateX(6px); } +}