<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Word 文档排版工具</title>
<style>
:root {
--primary: #4f46e5;
--primary-hover: #4338ca;
--primary-light: #e0e7ff;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-500: #6b7280;
--gray-700: #374151;
--gray-900: #111827;
--green: #059669;
--green-light: #d1fae5;
--red: #dc2626;
--red-light: #fee2e2;
--blue-light: #dbeafe;
--radius: 12px;
--shadow: 0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px rgba(0,0,0,.05);
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: var(--gray-900);
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 40px 20px;
}
.header {
text-align: center;
margin-bottom: 32px;
color: #fff;
}
.header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.header p {
font-size: 15px;
opacity: 0.85;
}
.card {
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 32px;
margin-bottom: 20px;
}
.card h2 {
font-size: 16px;
font-weight: 600;
color: var(--gray-700);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.card h2 .icon {
width: 24px; height: 24px;
background: var(--primary-light);
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 14px;
}
/* 上传区 */
.upload-zone {
border: 2px dashed var(--gray-300);
border-radius: var(--radius);
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all .2s;
background: var(--gray-50);
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--primary);
background: var(--primary-light);
}
.upload-zone .icon-big {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.6;
}
.upload-zone p { color: var(--gray-500); font-size: 14px; }
.upload-zone .hint { font-size: 12px; color: var(--gray-500); margin-top: 6px; }
input[type="file"] { display: none; }
/* 文件列表 */
.file-list {
margin-top: 16px;
display: flex; flex-direction: column; gap: 8px;
}
.file-item {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px;
background: var(--gray-50);
border-radius: 8px;
font-size: 13px;
}
.file-item .name { flex:1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-item .size { color: var(--gray-500); font-size: 12px; margin-left: 12px; }
.file-item .remove {
margin-left: 12px; cursor: pointer; color: var(--red);
font-size: 18px; line-height: 1; border: none; background: none;
}
/* 预设选择 */
.preset-group {
display: flex; gap: 10px; flex-wrap: wrap;
}
.preset-btn {
flex: 1; min-width: 180px;
padding: 14px 16px;
border: 2px solid var(--gray-200);
border-radius: 10px;
background: #fff;
cursor: pointer;
transition: all .2s;
text-align: left;
}
.preset-btn:hover { border-color: var(--primary); }
.preset-btn.active {
border-color: var(--primary);
background: var(--primary-light);
}
.preset-btn .preset-title {
font-size: 14px; font-weight: 600; margin-bottom: 4px;
}
.preset-btn .preset-desc {
font-size: 11px; color: var(--gray-500); line-height: 1.4;
}
/* 自定义表单 */
.custom-form {
display: none;
margin-top: 16px;
gap: 12px;
}
.custom-form.show { display: grid; grid-template-columns: 1fr 1fr; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 12px; color: var(--gray-500); font-weight: 500; }
.form-group input, .form-group select {
padding: 8px 10px;
border: 1px solid var(--gray-200);
border-radius: 6px;
font-size: 13px;
outline: none;
transition: border .2s;
}
.form-group input:focus, .form-group select:focus { border-color: var(--primary); }
/* 按钮 */
.btn-primary {
display: block; width: 100%;
padding: 14px;
background: var(--primary);
color: #fff;
border: none; border-radius: 10px;
font-size: 16px; font-weight: 600;
cursor: pointer;
transition: background .2s;
margin-top: 24px;
}
.btn-primary:hover { background: var(--primary-hover); }
.btn-primary:disabled {
background: var(--gray-300); cursor: not-allowed;
}
/* 进度 */
.progress-area {
display: none;
margin-top: 20px;
}
.progress-area.show { display: block; }
.progress-bar-bg {
width: 100%; height: 8px;
background: var(--gray-200);
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%; width: 0%;
background: linear-gradient(90deg, var(--primary), #7c3aed);
border-radius: 4px;
transition: width .3s;
}
.progress-text {
text-align: center; font-size: 13px;
color: var(--gray-500); margin-top: 8px;
}
/* 结果 */
.result-area { display: none; }
.result-area.show { display: block; }
.result-success {
padding: 16px;
background: var(--green-light);
border-radius: 10px;
margin-bottom: 12px;
}
.result-success p { color: var(--green); font-weight: 600; font-size: 14px; }
.download-list { display: flex; flex-direction: column; gap: 8px; }
.download-item {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px;
background: var(--blue-light);
border-radius: 8px;
}
.download-item .fname {
font-size: 13px; font-weight: 500;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
flex: 1;
}
.download-item a {
display: inline-flex; align-items: center; gap: 4px;
padding: 6px 14px;
background: var(--primary);
color: #fff;
border-radius: 6px;
text-decoration: none;
font-size: 12px; font-weight: 500;
white-space: nowrap;
margin-left: 12px;
}
.download-item a:hover { background: var(--primary-hover); }
.result-errors {
margin-top: 12px; padding: 12px;
background: var(--red-light);
border-radius: 8px;
font-size: 13px; color: var(--red);
}
/* checkbox */
.checkbox-row {
display: flex; align-items: center; gap: 8px;
font-size: 13px; margin-top: 8px;
}
.checkbox-row input[type="checkbox"] {
width: 16px; height: 16px; accent-color: var(--primary);
}
@media (max-width: 600px) {
.container { padding: 20px 12px; }
.card { padding: 20px; }
.preset-group { flex-direction: column; }
.custom-form.show { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Word 文档排版工具</h1>
<p>上传 .docx 文件,一键排版,即时下载</p>
</div>
<!-- 上传区域 -->
<div class="card">
<h2><span class="icon">1</span> 上传文件</h2>
<div class="upload-zone" id="uploadZone">
<div class="icon-big">📄</div>
<p>点击选择文件,或将文件拖拽至此</p>
<p class="hint">支持 .docx 格式,可多选</p>
</div>
<input type="file" id="fileInput" accept=".docx" multiple>
<div class="file-list" id="fileList"></div>
</div>
<!-- 排版设置 -->
<div class="card">
<h2><span class="icon">2</span> 排版设置</h2>
<div class="preset-group" id="presetGroup">
<div class="preset-btn active" data-preset="exam">
<div class="preset-title">试卷排版</div>
<div class="preset-desc">A4 + 适中页边距 + 四号标题 + 小四正文 + 单倍行距</div>
</div>
<div class="preset-btn" data-preset="essay">
<div class="preset-title">论文排版</div>
<div class="preset-desc">A4 + 常规页边距 + 三号标题 + 小四正文 + 1.5倍行距</div>
</div>
<div class="preset-btn" data-preset="custom">
<div class="preset-title">自定义</div>
<div class="preset-desc">手动设置字体、字号、行距、页边距等参数</div>
</div>
</div>
<div class="custom-form" id="customForm">
<div class="form-group">
<label>中文字体</label>
<select id="fontCn">
<option value="宋体">宋体</option>
<option value="黑体">黑体</option>
<option value="楷体">楷体</option>
<option value="仿宋">仿宋</option>
<option value="微软雅黑">微软雅黑</option>
</select>
</div>
<div class="form-group">
<label>英文字体</label>
<select id="fontEn">
<option value="Times New Roman">Times New Roman</option>
<option value="Arial">Arial</option>
<option value="Calibri">Calibri</option>
</select>
</div>
<div class="form-group">
<label>标题字号 (pt)</label>
<input type="number" id="titleSize" value="14" min="8" max="48" step="0.5">
</div>
<div class="form-group">
<label>正文字号 (pt)</label>
<input type="number" id="bodySize" value="12" min="8" max="36" step="0.5">
</div>
<div class="form-group">
<label>行距 (倍)</label>
<input type="number" id="lineSpacing" value="1.0" min="0.5" max="3" step="0.1">
</div>
<div class="form-group">
<label>上边距 (cm)</label>
<input type="number" id="marginTop" value="2.54" min="0.5" max="5" step="0.1">
</div>
<div class="form-group">
<label>下边距 (cm)</label>
<input type="number" id="marginBottom" value="2.54" min="0.5" max="5" step="0.1">
</div>
<div class="form-group">
<label>左边距 (cm)</label>
<input type="number" id="marginLeft" value="1.91" min="0.5" max="5" step="0.1">
</div>
<div class="form-group">
<label>右边距 (cm)</label>
<input type="number" id="marginRight" value="1.91" min="0.5" max="5" step="0.1">
</div>
</div>
<div class="checkbox-row">
<input type="checkbox" id="wrapImages" checked>
<label for="wrapImages">图片转为嵌入模式并放入无边框表格</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="titleBold" checked>
<label for="titleBold">标题加粗</label>
</div>
</div>
<!-- 开始排版 -->
<button class="btn-primary" id="formatBtn" disabled>开始排版</button>
<!-- 进度 -->
<div class="progress-area" id="progressArea">
<div class="card" style="padding:20px">
<div class="progress-bar-bg"><div class="progress-bar" id="progressBar"></div></div>
<div class="progress-text" id="progressText">正在排版...</div>
</div>
</div>
<!-- 结果 -->
<div class="result-area" id="resultArea">
<div class="card">
<div class="result-success" id="resultSuccess">
<p id="resultMsg"></p>
</div>
<div class="download-list" id="downloadList"></div>
<div class="result-errors" id="resultErrors" style="display:none"></div>
<button class="btn-primary" onclick="resetAll()" style="background:#059669;margin-top:16px">继续排版其他文件</button>
</div>
</div>
</div>
<script>
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const formatBtn = document.getElementById('formatBtn');
const presetGroup = document.getElementById('presetGroup');
const customForm = document.getElementById('customForm');
const progressArea = document.getElementById('progressArea');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultArea = document.getElementById('resultArea');
let selectedFiles = [];
let currentPreset = 'exam';
// 上传区域
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
uploadZone.addEventListener('drop', e => {
e.preventDefault();
uploadZone.classList.remove('dragover');
addFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', e => addFiles(e.target.files));
function addFiles(files) {
for (const f of files) {
if (f.name.endsWith('.docx') && !selectedFiles.find(s => s.name === f.name)) {
selectedFiles.push(f);
}
}
renderFileList();
}
function renderFileList() {
fileList.innerHTML = '';
selectedFiles.forEach((f, i) => {
const div = document.createElement('div');
div.className = 'file-item';
const sizeKB = (f.size / 1024).toFixed(1);
div.innerHTML = `
<span class="name">${f.name}</span>
<span class="size">${sizeKB} KB</span>
<button class="remove" onclick="removeFile(${i})">×</button>
`;
fileList.appendChild(div);
});
formatBtn.disabled = selectedFiles.length === 0;
}
function removeFile(index) {
selectedFiles.splice(index, 1);
renderFileList();
}
// 预设
presetGroup.addEventListener('click', e => {
const btn = e.target.closest('.preset-btn');
if (!btn) return;
document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentPreset = btn.dataset.preset;
customForm.classList.toggle('show', currentPreset === 'custom');
});
// 排版
formatBtn.addEventListener('click', async () => {
if (selectedFiles.length === 0) return;
const fd = new FormData();
selectedFiles.forEach(f => fd.append('files', f));
fd.append('preset', currentPreset);
if (currentPreset === 'custom') {
fd.append('font_cn', document.getElementById('fontCn').value);
fd.append('font_en', document.getElementById('fontEn').value);
fd.append('title_size', document.getElementById('titleSize').value);
fd.append('body_size', document.getElementById('bodySize').value);
fd.append('line_spacing', document.getElementById('lineSpacing').value);
fd.append('margin_top', document.getElementById('marginTop').value);
fd.append('margin_bottom', document.getElementById('marginBottom').value);
fd.append('margin_left', document.getElementById('marginLeft').value);
fd.append('margin_right', document.getElementById('marginRight').value);
}
fd.append('wrap_images', document.getElementById('wrapImages').checked ? 'true' : 'false');
fd.append('title_bold', document.getElementById('titleBold').checked ? 'true' : 'false');
// 显示进度
formatBtn.disabled = true;
progressArea.classList.add('show');
resultArea.classList.remove('show');
let progress = 0;
const timer = setInterval(() => {
progress = Math.min(progress + Math.random() * 15, 90);
progressBar.style.width = progress + '%';
}, 300);
try {
const resp = await fetch('/api/format', { method: 'POST', body: fd });
clearInterval(timer);
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || '排版失败');
}
progressBar.style.width = '100%';
progressText.textContent = '排版完成!';
const data = await resp.json();
setTimeout(() => {
progressArea.classList.remove('show');
showResults(data);
}, 500);
} catch (e) {
clearInterval(timer);
progressArea.classList.remove('show');
alert('排版出错: ' + e.message);
formatBtn.disabled = false;
}
});
function showResults(data) {
resultArea.classList.add('show');
const msg = document.getElementById('resultMsg');
msg.textContent = `排版完成!共处理 ${data.results.length} 个文件`;
const list = document.getElementById('downloadList');
list.innerHTML = '';
data.results.forEach(r => {
const div = document.createElement('div');
div.className = 'download-item';
div.innerHTML = `
<span class="fname">${r.formatted}</span>
<a href="/api/download/${data.task_id}/${encodeURIComponent(r.formatted)}" download>下载</a>
`;
list.appendChild(div);
});
const errDiv = document.getElementById('resultErrors');
if (data.errors && data.errors.length > 0) {
errDiv.style.display = 'block';
errDiv.textContent = '部分文件出错: ' + data.errors.join('; ');
} else {
errDiv.style.display = 'none';
}
}
function resetAll() {
selectedFiles = [];
renderFileList();
resultArea.classList.remove('show');
progressArea.classList.remove('show');
progressBar.style.width = '0%';
progressText.textContent = '正在排版...';
formatBtn.disabled = true;
fileInput.value = '';
}
</script>
</body>
</html>
index.html
style.css
index.js
md
README.md
index.html