未命名 TPX2WEedit icon

创建者:
用户zdlDCoL7
Fork(复制)
下载
嵌入
BUG反馈
index.html
style.css
index.js
md
README.md
index.html
            
            <!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})">&times;</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>

        
编辑器加载中
预览
控制台