超级简历模板内容下载静态页

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>简历数据源精准解析工具 (完美适配一页)</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
    <style>
      * {
        box-sizing: border-box;
      }
      body {
        font-family: 'Microsoft YaHei', sans-serif;
        margin: 0;
        padding: 20px;
        background-color: #f0f2f5;
        display: flex;
        gap: 20px;
        height: 100vh;
        overflow: hidden;
      }
      .editor-panel {
        width: 420px;
        background: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        overflow-y: auto;
        flex-shrink: 0;
      }
      .editor-panel h2 {
        margin-top: 0;
        color: #333;
        font-size: 18px;
        border-bottom: 2px solid #1a5fb4;
        padding-bottom: 10px;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .parse-section {
        background: #f8f9fa;
        border: 1px dashed #adb5bd;
        border-radius: 6px;
        padding: 15px;
        margin-bottom: 20px;
      }
      .parse-section h3 {
        margin: 0 0 10px 0;
        font-size: 15px;
        color: #495057;
        display: flex;
        align-items: center;
        gap: 6px;
      }
      .parse-section textarea {
        width: 100%;
        height: 120px;
        padding: 8px;
        border: 1px solid #ced4da;
        border-radius: 4px;
        font-size: 12px;
        resize: vertical;
        margin-bottom: 10px;
        font-family: monospace;
      }
      .btn-parse {
        width: 100%;
        background-color: #28a745;
        color: white;
        border: none;
        padding: 8px;
        border-radius: 4px;
        cursor: pointer;
        font-size: 13px;
        transition: 0.2s;
      }
      .btn-parse:hover {
        background-color: #218838;
      }

      .form-group {
        margin-bottom: 15px;
      }
      .form-group label {
        display: block;
        font-size: 13px;
        color: #666;
        margin-bottom: 5px;
        font-weight: bold;
      }
      .form-group input,
      .form-group textarea {
        width: 100%;
        padding: 8px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 13px;
      }
      .form-group textarea {
        height: 80px;
        resize: vertical;
      }

      .btn-group {
        display: flex;
        flex-direction: column;
        gap: 10px;
        margin-top: 20px;
        padding-bottom: 20px;
      }
      button {
        padding: 10px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
        transition: 0.2s;
      }
      .btn-primary {
        background-color: #1a5fb4;
        color: white;
      }
      .btn-primary:hover {
        background-color: #134a8e;
      }
      .btn-success {
        background-color: #28a745;
        color: white;
      }
      .btn-success:hover {
        background-color: #218838;
      }
      .btn-warning {
        background-color: #ffc107;
        color: #333;
      }
      .btn-danger {
        background-color: #dc3545;
        color: white;
        font-size: 12px;
        padding: 5px 10px;
        border-radius: 4px;
        cursor: pointer;
        border: none;
      }
      .btn-danger:hover {
        background-color: #c82333;
      }

      .preview-panel {
        flex: 1;
        display: flex;
        flex-direction: column;
        overflow-y: auto;
        align-items: center;
        padding-bottom: 40px;
      }
      .toolbar {
        position: sticky;
        top: 0;
        z-index: 10;
        margin-bottom: 20px;
        display: flex;
        gap: 10px;
        background: white;
        padding: 10px 20px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
      }

      /* 🔥 关键修改:移除 overflow:hidden,允许内容自然撑开以获取真实高度 */
      #resume-page {
        width: 210mm;
        min-height: 297mm;
        background: white;
        box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
        padding: 15mm 20mm;
        color: #333;
        transition: transform 0.3s ease;
        flex-shrink: 0;
        transform-origin: top center; /* 缩放锚点设为顶部居中 */
      }

      /* 模板样式 */
      .template-classic .header {
        border-bottom: 2px solid #333;
        padding-bottom: 15px;
        margin-bottom: 20px;
      }
      .template-classic .name {
        font-size: 26px;
        font-weight: bold;
        margin-bottom: 8px;
      }
      .template-classic .contact {
        font-size: 13px;
        color: #555;
        display: flex;
        flex-wrap: wrap;
        gap: 15px;
      }
      .template-classic .section {
        margin-bottom: 18px;
      }
      .template-classic .section-title {
        font-size: 17px;
        font-weight: bold;
        border-bottom: 1px solid #ddd;
        padding-bottom: 5px;
        margin-bottom: 12px;
      }
      .template-classic .item-desc {
        font-size: 12px;
        line-height: 1.6;
        text-align: justify;
        white-space: pre-wrap;
      }
    </style>
  </head>
  <body>
    <div class="editor-panel">
      <h2>
        📝 简历信息填写
        <button class="btn-danger" onclick="clearCache()" title="清空所有已保存的数据">🗑️ 清空缓存</button>
      </h2>

      <div class="parse-section">
        <h3>📥 精准解析 WonderCV HTML 源码</h3>
        <textarea id="raw-data-input" placeholder="在此粘贴完整的 WonderCV HTML 源码..."></textarea>
        <button class="btn-parse" onclick="parseWonderCV()">🔍 一键精准解析</button>
      </div>

      <div class="form-group"><label>姓名</label><input type="text" id="input-name" value="" /></div>
      <div class="form-group"><label>电话</label><input type="text" id="input-phone" value="" /></div>
      <div class="form-group"><label>邮箱</label><input type="text" id="input-email" value="" /></div>
      <div class="form-group"><label>求职意向</label><input type="text" id="input-target" value="" /></div>
      <div class="form-group"><label>城市 / 生日 / 状态</label><input type="text" id="input-other" value="" /></div>

      <div class="form-group"><label>专业技能 (换行分隔)</label><textarea id="input-skills"></textarea></div>
      <div class="form-group">
        <label>工作经历 (换行分隔每段经历)</label><textarea id="input-work" style="height: 150px"></textarea>
      </div>
      <div class="form-group"><label>教育经历</label><textarea id="input-edu"></textarea></div>

      <div class="btn-group">
        <button class="btn-warning" onclick="smartFillOnePage()">✨ 智能适配一页</button>
        <button class="btn-primary" onclick="resetScale()">↩️ 重置缩放</button>
        <button class="btn-success" onclick="exportPDF()">⬇️ 导出 PDF</button>
      </div>
    </div>

    <div class="preview-panel">
      <div class="toolbar">
        <span style="align-self: center; font-weight: bold">切换模板:</span>
        <button class="btn-primary" onclick="switchTemplate('classic')">经典简约</button>
      </div>
      <div id="resume-page" class="template-classic"></div>
    </div>

    <script>
      let currentTemplate = 'classic'
      const CACHE_KEY = 'wondercv_resume_data_cache'
      // A4 纸在 96dpi 下的像素高度约为 1123px,减去上下 padding (15mm*2 ≈ 113px)
      const A4_CONTENT_HEIGHT_PX = 1010

      // ====================== 缓存管理模块 ======================
      function saveToCache() {
        const data = getData()
        localStorage.setItem(CACHE_KEY, JSON.stringify(data))
      }

      function loadFromCache() {
        const cached = localStorage.getItem(CACHE_KEY)
        if (cached) {
          try {
            const data = JSON.parse(cached)
            document.getElementById('input-name').value = data.name || ''
            document.getElementById('input-phone').value = data.phone || ''
            document.getElementById('input-email').value = data.email || ''
            document.getElementById('input-target').value = data.target || ''
            document.getElementById('input-other').value = data.other || ''
            document.getElementById('input-skills').value = data.skills || ''
            document.getElementById('input-work').value = data.work || ''
            document.getElementById('input-edu').value = data.edu || ''
          } catch (e) {
            console.error('缓存数据损坏', e)
          }
        }
      }

      function clearCache() {
        if (confirm('确定要清空所有已缓存的简历数据吗?')) {
          localStorage.removeItem(CACHE_KEY)
          document.querySelectorAll('.editor-panel input, .editor-panel textarea').forEach((el) => (el.value = ''))
          resetScale()
          renderResume()
          alert('🗑️ 缓存已清空!')
        }
      }

      // ====================== 核心:预处理 + 深度适配 WonderCV DOM ======================
      function parseWonderCV() {
        let rawHtml = document.getElementById('raw-data-input').value.trim()
        if (!rawHtml) {
          alert('❌ 请先粘贴 HTML 源码!')
          return
        }

        try {
          rawHtml = rawHtml
            .replace(/\\n/g, '\n')
            .replace(/\\t/g, '\t')
            .replace(/\\x3C!--.*?-->/g, '')
            .replace(/&nbsp;/g, ' ')

          const parser = new DOMParser()
          const doc = parser.parseFromString(rawHtml, 'text/html')
          const cleanText = (str) => (str ? str.replace(/\s+/g, ' ').trim() : '')

          const name = cleanText(doc.querySelector('.user-name')?.textContent)
          let phone = '',
            email = '',
            target = ''
          const otherArr = []
          doc.querySelectorAll('.profile-item span').forEach((span) => {
            const label = span.querySelector('.attr-name')?.textContent || ''
            const text = cleanText(span.textContent.replace(label, ''))
            if (label.includes('电话')) phone = text
            else if (label.includes('邮箱')) email = text
            else if (label.includes('求职意向')) target = text
            else if (label.includes('现居城市') || label.includes('生日') || label.includes('当前状态')) {
              if (text) otherArr.push(text)
            }
          })
          const other = otherArr.join(' | ')

          const skillP = doc.querySelector('#skill_list p')
          const skills = skillP
            ? skillP.innerHTML
                .replace(/<br\s*\/?>/gi, '\n')
                .replace(/<[^>]+>/g, '')
                .trim()
            : ''

          const workLines = []
          doc.querySelectorAll('#works > .info-content > .menu').forEach((block) => {
            const title1 = block.querySelector('.menu-title:nth-child(1)')
            const title2 = block.querySelector('.menu-title:nth-child(2)')
            const company = cleanText(title1?.querySelector('.font_w_b')?.textContent)
            const time = cleanText(title1?.querySelector('.right')?.textContent)
            const position = cleanText(title2?.querySelector('.left')?.textContent)
            const contentP = block.querySelector('.menu-content p, .menu-content')
            const content = contentP
              ? contentP.innerHTML
                  .replace(/<br\s*\/?>/gi, '\n')
                  .replace(/<\/p><p>/gi, '\n')
                  .replace(/<[^>]+>/g, '')
                  .trim()
              : ''
            if (company) workLines.push(`${company} | ${position} | ${time}\n${content}`)
          })
          const work = workLines.join('\n\n')

          const eduLines = []
          doc.querySelectorAll('#edus > .info-content > .menu').forEach((block) => {
            const title1 = block.querySelector('.menu-title:nth-child(1)')
            const title2 = block.querySelector('.menu-title:nth-child(2)')
            const school = cleanText(title1?.querySelector('.font_w_b')?.textContent)
            const time = cleanText(title1?.querySelector('.right')?.textContent)
            const majorInfo = cleanText(title2?.querySelector('.left')?.textContent)
            if (school) eduLines.push(`${school} | ${majorInfo} | ${time}`)
          })
          const edu = eduLines.join('\n')

          document.getElementById('input-name').value = name
          document.getElementById('input-phone').value = phone
          document.getElementById('input-email').value = email
          document.getElementById('input-target').value = target
          document.getElementById('input-other').value = other
          document.getElementById('input-skills').value = skills
          document.getElementById('input-work').value = work
          document.getElementById('input-edu').value = edu

          saveToCache()
          renderResume()
          alert(`✅ 解析成功并已自动保存!\n已提取: ${name} | ${phone}`)
        } catch (e) {
          console.error(e)
          alert(`❌ 解析失败: ${e.message}`)
        }
      }

      // ====================== 🔥 核心修复:整体缩放适配一页 ======================
      function smartFillOnePage() {
        // 先重置缩放,获取简历内容的真实自然高度
        const page = document.getElementById('resume-page')
        page.style.transform = 'scale(1)'

        // 强制浏览器重排,确保获取到最新的 scrollHeight
        void page.offsetHeight

        const actualHeight = page.scrollHeight

        if (actualHeight <= 10) {
          alert('⚠️ 简历内容为空,请先填写或解析数据!')
          return
        }

        // 计算缩放比例:目标高度 / 实际高度
        // 0.98 是为了留出微小的安全边距,防止导出 PDF 时被裁切
        let scale = (A4_CONTENT_HEIGHT_PX / actualHeight) * 0.98

        // 限制最大缩放比例为 1.2,避免字数极少时字体过大导致排版失真
        if (scale > 1.2) scale = 1.2

        // 限制最小缩放比例为 0.5,避免字数极多时字体过小无法阅读
        if (scale < 0.5) {
          alert('⚠️ 内容严重超出一页,已按最小可读比例(0.5)缩放。\n建议精简工作经历或项目描述!')
          scale = 0.5
        }

        page.style.transform = `scale(${scale})`
        console.log(`📏 实际高度: ${actualHeight}px, 应用缩放: ${scale.toFixed(3)}`)
      }

      function resetScale() {
        document.getElementById('resume-page').style.transform = 'scale(1)'
      }

      // ====================== 渲染与导出逻辑 ======================
      function getData() {
        return {
          name: document.getElementById('input-name').value,
          phone: document.getElementById('input-phone').value,
          email: document.getElementById('input-email').value,
          target: document.getElementById('input-target').value,
          other: document.getElementById('input-other').value,
          skills: document.getElementById('input-skills').value,
          work: document.getElementById('input-work').value,
          edu: document.getElementById('input-edu').value,
        }
      }

      function renderResume() {
        const data = getData()
        const page = document.getElementById('resume-page')
        page.className = `template-${currentTemplate}`

        page.innerHTML = `
        <div class="header">
            <div class="name">${data.name}</div>
            <div class="contact">
                <span>📞 ${data.phone}</span>
                <span>📧 ${data.email}</span>
                <span>🎯 ${data.target}</span>
                <span>📍 ${data.other}</span>
            </div>
        </div>
        <div class="section"><div class="section-title">专业技能</div><div class="item-desc">${data.skills}</div></div>
        <div class="section"><div class="section-title">工作经历</div><div class="item-desc">${data.work}</div></div>
        <div class="section"><div class="section-title">教育经历</div><div class="item-desc">${data.edu}</div></div>
    `
      }

      function switchTemplate(template) {
        currentTemplate = template
        resetScale() // 切换模板时重置缩放,避免残留
        renderResume()
      }

      function exportPDF() {
        // 导出前暂时重置缩放,让 html2pdf 按照原始尺寸渲染,保证清晰度
        const page = document.getElementById('resume-page')
        const currentTransform = page.style.transform
        page.style.transform = 'scale(1)'

        const element = document.getElementById('resume-page')
        html2pdf()
          .set({
            margin: 0,
            filename: `${getData().name || '简历'}_简历.pdf`,
            image: { type: 'jpeg', quality: 0.98 },
            html2canvas: { scale: 2, useCORS: true },
            jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
          })
          .from(element)
          .save()
          .then(() => {
            // 导出完成后恢复预览区的缩放状态
            page.style.transform = currentTransform
          })
      }

      // ====================== 初始化与事件绑定 ======================
      document.querySelectorAll('.editor-panel input, .editor-panel textarea').forEach((el) => {
        el.addEventListener('input', () => {
          saveToCache()
          renderResume()
        })
      })

      window.addEventListener('DOMContentLoaded', () => {
        loadFromCache()
        renderResume()
      })
    </script>
  </body>
</html>


posted @ 2026-06-02 11:05  lambertlt  阅读(6)  评论(0)    收藏  举报