mcpField

Form Builder

github.com/eddiebe147文档处理46 次安装
代码仓库

关于 Form Builder

Form Builder 是用于制作可填写表单、调查问卷和交互式文档的技能,支持 PDF、HTML 等格式,并涵盖验证、条件逻辑及提交数据处理。

主要功能

  1. 01创建可填写的 PDF 表单
  2. 02生成 HTML 表单
  3. 03制作带条件逻辑的问卷
  4. 04添加字段验证规则
  5. 05处理提交并导出数据
  6. 06制作带进度的多页表单

适用场景

  1. 01制作注册或申请表
  2. 02创建反馈调查和问卷
  3. 03收集并处理表单提交数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/eddiebe147/claude-settings --skill form-builder

安装后在 Claude Code 中输入/form-builder确认已加载。

详细介绍

表单构建器

表单构建器技能可自动创建可填写表单、调查问卷、问卷和交互式文档。它支持多种表单类型,包括注册表单、反馈调查、申请表、登记表和评估工具。该技能支持多种输出格式(带可填写字段的 PDF、HTML 表单、Google Forms 集成),并提供验证规则、条件逻辑和数据收集功能。

无论有何种数据收集需求,都可以生成包含恰当字段类型、验证规则、样式和提交处理功能的专业表单。

核心工作流

工作流 1:创建可填写的 PDF 表单

用途: 生成带有交互式可填写字段的 PDF

步骤:

  1. 定义表单结构和字段
  2. 设置字段类型(文本、复选框、单选框、下拉菜单、日期)
  3. 为每个字段添加验证规则
  4. 配置字段属性(必填、maxLength 等)
  5. 应用样式和布局
  6. 添加说明和帮助文本
  7. 生成带有可填写字段的 PDF
  8. 测试表单功能

实现:

const { PDFDocument, PDFTextField, PDFCheckBox, PDFDropdown, rgb } = require('pdf-lib');
const fs = require('fs');

async function createFillablePDFForm(formData, outputPath) {
  const pdfDoc = await PDFDocument.create();
  const page = pdfDoc.addPage([612, 792]); // Letter size

  const { width, height } = page.getSize();
  const fontSize = 11;
  const labelFontSize = 10;

  // Add title
  page.drawText(formData.title, {
    x: 50,
    y: height - 50,
    size: 18,
    color: rgb(0.2, 0.2, 0.2)
  });

  // Add instructions
  if (formData.instructions) {
    page.drawText(formData.instructions, {
      x: 50,
      y: height - 80,
      size: 10,
      color: rgb(0.4, 0.4, 0.4),
      maxWidth: width - 100
    });
  }

  let currentY = height - 120;

  // Add form fields
  for (const field of formData.fields) {
    // Draw field label
    page.drawText(field.label + (field.required ? ' *' : ''), {
      x: 50,
      y: currentY,
      size: labelFontSize,
      color: rgb(0, 0, 0)
    });

    currentY -= 25;

    // Create field based on type
    switch (field.type) {
      case 'text':
      case 'email':
      case 'number':
        const textField = pdfDoc.getForm().createTextField(field.name);
        textField.addToPage(page, {
          x: 50,
          y: currentY,
          width: 300,
          height: 20,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        if (field.placeholder) {
          textField.setText(field.placeholder);
        }
        if (field.maxLength) {
          textField.setMaxLength(field.maxLength);
        }
        break;

      case 'textarea':
        const textArea = pdfDoc.getForm().createTextField(field.name);
        textArea.addToPage(page, {
          x: 50,
          y: currentY - 40,
          width: 500,
          height: 60,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        textArea.enableMultiline();
        currentY -= 40;
        break;

      case 'checkbox':
        field.options.forEach((option, index) => {
          const checkbox = pdfDoc.getForm().createCheckBox(`${field.name}_${index}`);
          checkbox.addToPage(page, {
            x: 50,
            y: currentY - (index * 20),
            width: 12,
            height: 12,
            borderColor: rgb(0.5, 0.5, 0.5),
            borderWidth: 1
          });

          page.drawText(option, {
            x: 70,
            y: currentY - (index * 20) + 2,
            size: 10,
            color: rgb(0, 0, 0)
          });
        });
        currentY -= field.options.length * 20;
        break;

      case 'radio':
        const radioGroup = pdfDoc.getForm().createRadioGroup(field.name);
        field.options.forEach((option, index) => {
          radioGroup.addOptionToPage(option, page, {
            x: 50,
            y: currentY - (index * 20),
            width: 12,
            height: 12,
            borderColor: rgb(0.5, 0.5, 0.5),
            borderWidth: 1
          });

          page.drawText(option, {
            x: 70,
            y: currentY - (index * 20) + 2,
            size: 10,
            color: rgb(0, 0, 0)
          });
        });
        currentY -= field.options.length * 20;
        break;

      case 'dropdown':
        const dropdown = pdfDoc.getForm().createDropdown(field.name);
        dropdown.addOptions(field.options);
        dropdown.addToPage(page, {
          x: 50,
          y: currentY,
          width: 200,
          height: 20,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        break;

      case 'date':
        const dateField = pdfDoc.getForm().createTextField(field.name);
        dateField.addToPage(page, {
          x: 50,
          y: currentY,
          width: 150,
          height: 20,
          borderColor: rgb(0.5, 0.5, 0.5),
          borderWidth: 1
        });
        dateField.setText('MM/DD/YYYY');
        break;
    }

    // Add help text if provided
    if (field.helpText) {
      page.drawText(field.helpText, {
        x: 370,
        y: currentY + 5,
        size: 8,
        color: rgb(0.6, 0.6, 0.6),
        maxWidth: 200
      });
    }

    currentY -= 40;

    // Add new page if running out of space
    if (currentY < 100) {
      const newPage = pdfDoc.addPage([612, 792]);
      currentY = height - 50;
    }
  }

  // Add submit button placeholder (text)
  page.drawText('Please save and email this completed form to: ' + formData.submitEmail, {
    x: 50,
    y: 50,
    size: 9,
    color: rgb(0.3, 0.3, 0.3)
  });

  const pdfBytes = await pdfDoc.save();
  fs.writeFileSync(outputPath, pdfBytes);

  return outputPath;
}
工作流 2:生成 HTML 表单

用途: 创建带有客户端验证的交互式 HTML 表单

步骤:

  1. 定义表单结构和字段
  2. 生成 HTML 标记
  3. 添加 CSS 样式,打造专业外观
  4. 实现 JavaScript 验证
  5. 处理表单提交
  6. 添加无障碍功能(ARIA 标签、键盘导航)
  7. 适配移动设备
  8. 在不同浏览器中测试

实现:

function generateHTMLForm(formData, outputPath) {
  const html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${formData.title}</title>
  <style>
    * { box-sizing: border-box; }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      max-width: 700px;
      margin: 40px auto;
      padding: 20px;
      background: #f5f5f5;
    }
    .form-container {
      background: white;
      padding: 40px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    h1 {
      color: #333;
      margin-bottom: 10px;
    }
    .instructions {
      color: #666;
      margin-bottom: 30px;
      font-size: 14px;
    }
    .form-group {
      margin-bottom: 25px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
      color: #333;
    }
    .required { color: #e74c3c; }
    input[type="text"],
    input[type="email"],
    input[type="number"],
    input[type="tel"],
    input[type="date"],
    textarea,
    select {
      width: 100%;
      padding: 10px 12px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 14px;
      transition: border-color 0.3s;
    }
    input:focus,
    textarea:focus,
    select:focus {
      outline: none;
      border-color: #3498db;
    }
    textarea {
      resize: vertical;
      min-height: 100px;
    }
    .help-text {
      font-size: 12px;
      color: #888;
      margin-top: 5px;
    }
    .checkbox-group,
    .radio-group {
      margin-top: 8px;
    }
    .checkbox-item,
    .radio-item {
      margin-bottom: 8px;
    }
    .checkbox-item input,
    .radio-item input {
      width: auto;
      margin-right: 8px;
    }
    .error {
      color: #e74c3c;
      font-size: 12px;
      margin-top: 5px;
      display: none;
    }
    .error.visible {
      display: block;
    }
    input.invalid,
    textarea.invalid,
    select.invalid {
      border-color: #e74c3c;
    }
    button[type="submit"] {
      background: #3498db;
      color: white;
      padding: 12px 30px;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
      transition: background 0.3s;
    }
    button[type="submit"]:hover {
      background: #2980b9;
    }
    button[type="submit"]:disabled {
      background: #bdc3c7;
      cursor: not-allowed;
    }
    .success-message {
      display: none;
      background: #27ae60;
      color: white;
      padding: 15px;
      border-radius: 4px;
      margin-bottom: 20px;
    }
    @media (max-width: 600px) {
      body { padding: 10px; }
      .form-container { padding: 20px; }
    }
  </style>
</head>
<body>
  <div class="form-container">
    <div class="success-message" id="successMessage">
      Form submitted successfully! Thank you.
    </div>

    <h1>${formData.title}</h1>
    ${formData.instructions ? `<p class="instructions">${formData.instructions}</p>` : ''}

    <form id="mainForm" action="${formData.submitUrl || '#'}" method="${formData.method || 'POST'}">
      ${formData.fields.map(field => generateFieldHTML(field)).join('\n')}

      <div class="form-group">
        <button type="submit" id="submitBtn">Submit</button>
      </div>
    </form>
  </div>

  <script>
    ${generateValidationScript(formData)}
  </script>
</body>
</html>
  `;

  fs.writeFileSync(outputPath, html, 'utf8');
  return outputPath;
}

function generateFieldHTML(field) {
  const requiredAttr = field.required ? 'required' : '';
  const requiredLabel = field.required ? '<span class="required">*</span>' : '';

  switch (field.type) {
    case 'text':
    case 'email':
    case 'number':
    case 'tel':
    case 'date':
      return `
        <div class="form-group">
          <label for="${field.name}">${field.label} ${requiredLabel}</label>
          <input
            type="${field.type}"
            id="${field.name}"
            name="${field.name}"
            placeholder="${field.placeholder || ''}"
            ${requiredAttr}
            ${field.maxLength ? `maxlength="${field.maxLength}"` : ''}
            ${field.pattern ? `pattern="${field.pattern}"` : ''}
          >
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">${field.errorMessage || 'This field is required'}</div>
        </div>
      `;

    case 'textarea':
      return `
        <div class="form-group">
          <label for="${field.name}">${field.label} ${requiredLabel}</label>
          <textarea
            id="${field.name}"
            name="${field.name}"
            placeholder="${field.placeholder || ''}"
            ${requiredAttr}
            ${field.maxLength ? `maxlength="${field.maxLength}"` : ''}
          ></textarea>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">This field is required</div>
        </div>
      `;

    case 'select':
      return `
        <div class="form-group">
          <label for="${field.name}">${field.label} ${requiredLabel}</label>
          <select id="${field.name}" name="${field.name}" ${requiredAttr}>
            <option value="">-- Select --</option>
            ${field.options.map(opt => `<option value="${opt}">${opt}</option>`).join('\n')}
          </select>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">Please select an option</div>
        </div>
      `;

    case 'checkbox':
      return `
        <div class="form-group">
          <label>${field.label} ${requiredLabel}</label>
          <div class="checkbox-group">
            ${field.options.map((opt, idx) => `
              <div class="checkbox-item">
                <input type="checkbox" id="${field.name}_${idx}" name="${field.name}" value="${opt}">
                <label for="${field.name}_${idx}">${opt}</label>
              </div>
            `).join('\n')}
          </div>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
        </div>
      `;

    case 'radio':
      return `
        <div class="form-group">
          <label>${field.label} ${requiredLabel}</label>
          <div class="radio-group">
            ${field.options.map((opt, idx) => `
              <div class="radio-item">
                <input type="radio" id="${field.name}_${idx}" name="${field.name}" value="${opt}" ${requiredAttr}>
                <label for="${field.name}_${idx}">${opt}</label>
              </div>
            `).join('\n')}
          </div>
          ${field.helpText ? `<div class="help-text">${field.helpText}</div>` : ''}
          <div class="error" id="${field.name}-error">Please select an option</div>
        </div>
      `;

    default:
      return '';
  }
}

function generateValidationScript(formData) {
  return `
    const form = document.getElementById('mainForm');
    const submitBtn = document.getElementById('submitBtn');
    const successMessage = document.getElementById('successMessage');

    // Validation rules
    const validationRules = {
      email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,
      phone: /^[\\d\\s\\-\\(\\)]+$/
    };

    // Real-time validation
    form.addEventListener('input', (e) => {
      validateField(e.target);
    });

    // Form submission
    form.addEventListener('submit', async (e) => {
      e.preventDefault();

      // Validate all fields
      let isValid = true;
      const fields = form.querySelectorAll('input, textarea, select');

      fields.forEach(field => {
        if (!validateField(field)) {
          isValid = false;
        }
      });

      if (!isValid) {
        return;
      }

      // Disable submit button
      submitBtn.disabled = true;
      submitBtn.textContent = 'Submitting...';

      try {
        // Collect form data
        const formData = new FormData(form);
        const data = Object.fromEntries(formData);

        // Submit to server
        ${formData.submitUrl ? `
        const response = await fetch('${formData.submitUrl}', {
          method: '${formData.method || 'POST'}',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });

        if (response.ok) {
          successMessage.style.display = 'block';
          form.reset();
        } else {
          alert('Submission failed. Please try again.');
        }
        ` : `
        // No submit URL - just show success
        console.log('Form data:', data);
        successMessage.style.display = 'block';
        form.reset();
        `}

      } catch (error) {
        alert('An error occurred. Please try again.');
        console.error(error);
      } finally {
        submitBtn.disabled = false;
        submitBtn.textContent = 'Submit';
      }
    });

    function validateField(field) {
      const errorEl = document.getElementById(field.name + '-error');
      if (!errorEl) return true;

      let isValid = true;
      let errorMessage = '';

      // Required check
      if (field.hasAttribute('required') && !field.value.trim()) {
        isValid = false;
        errorMessage = 'This field is required';
      }

      // Email validation
      if (field.type === 'email' && field.value && !validationRules.email.test(field.value)) {
        isValid = false;
        errorMessage = 'Please enter a valid email address';
      }

      // Pattern validation
      if (field.hasAttribute('pattern') && field.value) {
        const pattern = new RegExp(field.getAttribute('pattern'));
        if (!pattern.test(field.value)) {
          isValid = false;
          errorMessage = 'Please enter a valid value';
        }
      }

      // Update UI
      if (isValid) {
        field.classList.remove('invalid');
        errorEl.classList.remove('visible');
      } else {
        field.classList.add('invalid');
        errorEl.textContent = errorMessage;
        errorEl.classList.add('visible');
      }

      return isValid;
    }
  `;
}
工作流 3:调查问卷生成器

用途: 创建包含多种问题类型和逻辑的综合调查问卷

步骤:

  1. 定义调查问卷结构和分区
  2. 添加各种问题类型(单选、多选、评分、文本等)
  3. 实现条件逻辑(跳题模式)
  4. 添加验证规则
  5. 配置答案选项和量表
  6. 设计结果收集和分析
  7. 以首选格式生成调查问卷
  8. 设置回答跟踪

实现:

async function generateSurvey(surveyData, outputPath) {
  const survey = {
    title: surveyData.title,
    description: surveyData.description,
    sections: surveyData.sections.map(section => ({
      title: section.title,
      questions: section.questions.map((q, idx) => ({
        id: `q${section.id}_${idx + 1}`,
        type: q.type,
        text: q.text,
        required: q.required !== false,
        options: q.options,
        validation: q.validation,
        conditionalLogic: q.conditionalLogic
      }))
    })),
    settings: {
      allowAnonymous: surveyData.allowAnonymous !== false,
      showProgress: surveyData.showProgress !== false,
      randomizeQuestions: surveyData.randomizeQuestions || false,
      allowMultipleSubmissions: surveyData.allowMultipleSubmissions || false
    }
  };

  // Generate HTML survey
  const html = generateSurveyHTML(survey);
  fs.writeFileSync(outputPath, html, 'utf8');

  return {
    surveyPath: outputPath,
    questionCount: survey.sections.reduce((sum, s) => sum + s.questions.length, 0),
    settings: survey.settings
  };
}

function generateSurveyHTML(survey) {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${survey.title}</title>
  <style>
    /* Survey-specific styles */
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
      background: #f9f9f9;
    }
    .survey-container {
      background: white;
      padding: 40px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    .progress-bar {
      height: 8px;
      background: #e0e0e0;
      border-radius: 4px;
      margin-bottom: 30px;
    }
    .progress-fill {
      height: 100%;
      background: #4CAF50;
      border-radius: 4px;
      transition: width 0.3s;
    }
    .section {
      margin-bottom: 40px;
    }
    .section-title {
      font-size: 20px;
      font-weight: bold;
      margin-bottom: 20px;
      color: #333;
    }
    .question {
      margin-bottom: 30px;
      padding: 20px;
      background: #f5f5f5;
      border-radius: 6px;
    }
    .question-text {
      font-size: 16px;
      margin-bottom: 15px;
      color: #333;
    }
    .required-indicator {
      color: #e74c3c;
    }
    .rating-scale {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin: 20px 0;
    }
    .rating-option {
      width: 50px;
      height: 50px;
      border: 2px solid #ddd;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      transition: all 0.3s;
    }
    .rating-option:hover {
      border-color: #4CAF50;
      background: #f0f0f0;
    }
    .rating-option.selected {
      background: #4CAF50;
      color: white;
      border-color: #4CAF50;
    }
    button.submit-survey {
      background: #4CAF50;
      color: white;
      padding: 15px 40px;
      border: none;
      border-radius: 6px;
      font-size: 16px;
      cursor: pointer;
      width: 100%;
      margin-top: 20px;
    }
    button.submit-survey:hover {
      background: #45a049;
    }
  </style>
</head>
<body>
  <div class="survey-container">
    <h1>${survey.title}</h1>
    <p>${survey.description}</p>

    ${survey.settings.showProgress ? `
    <div class="progress-bar">
      <div class="progress-fill" id="progressFill" style="width: 0%"></div>
    </div>
    ` : ''}

    <form id="surveyForm">
      ${survey.sections.map(section => `
        <div class="section">
          <h2 class="section-title">${section.title}</h2>
          ${section.questions.map(q => generateQuestionHTML(q)).join('\n')}
        </div>
      `).join('\n')}

      <button type="submit" class="submit-survey">Submit Survey</button>
    </form>
  </div>

  <script>
    // Survey logic and submission
    ${generateSurveyScript(survey)}
  </script>
</body>
</html>
  `;
}

function generateQuestionHTML(question) {
  const requiredMark = question.required ? '<span class="required-indicator">*</span>' : '';

  switch (question.type) {
    case 'multipleChoice':
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          ${question.options.map((opt, idx) => `
            <div>
              <input type="radio" id="${question.id}_${idx}" name="${question.id}" value="${opt}" ${question.required ? 'required' : ''}>
              <label for="${question.id}_${idx}">${opt}</label>
            </div>
          `).join('\n')}
        </div>
      `;

    case 'rating':
      const scale = question.scale || 5;
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          <div class="rating-scale">
            ${Array.from({ length: scale }, (_, i) => i + 1).map(num => `
              <div class="rating-option" data-value="${num}" onclick="selectRating('${question.id}', ${num})">
                ${num}
              </div>
            `).join('\n')}
          </div>
          <input type="hidden" name="${question.id}" id="${question.id}" ${question.required ? 'required' : ''}>
        </div>
      `;

    case 'text':
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          <textarea name="${question.id}" rows="4" style="width: 100%; padding: 10px;" ${question.required ? 'required' : ''}></textarea>
        </div>
      `;

    case 'checkbox':
      return `
        <div class="question" data-question-id="${question.id}">
          <div class="question-text">${question.text} ${requiredMark}</div>
          ${question.options.map((opt, idx) => `
            <div>
              <input type="checkbox" id="${question.id}_${idx}" name="${question.id}" value="${opt}">
              <label for="${question.id}_${idx}">${opt}</label>
            </div>
          `).join('\n')}
        </div>
      `;

    default:
      return '';
  }
}

function generateSurveyScript(survey) {
  return `
    function selectRating(questionId, value) {
      // Remove previous selection
      const container = document.querySelector(\`[data-question-id="\${questionId}"]\`);
      container.querySelectorAll('.rating-option').forEach(opt => {
        opt.classList.remove('selected');
      });

      // Add new selection
      event.target.classList.add('selected');
      document.getElementById(questionId).value = value;
    }

    // Form submission
    document.getElementById('surveyForm').addEventListener('submit', async (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const responses = Object.fromEntries(formData);

      console.log('Survey responses:', responses);

      // Submit to server or save locally
      alert('Thank you for completing the survey!');
      e.target.reset();
    });

    // Progress tracking
    ${survey.settings.showProgress ? `
    const questions = document.querySelectorAll('.question');
    const progressFill = document.getElementById('progressFill');

    document.addEventListener('input', updateProgress);

    function updateProgress() {
      let answered = 0;
      questions.forEach(q => {
        const inputs = q.querySelectorAll('input, textarea');
        const hasAnswer = Array.from(inputs).some(input => {
          if (input.type === 'radio' || input.type === 'checkbox') {
            return input.checked;
          }
          return input.value.trim() !== '';
        });
        if (hasAnswer) answered++;
      });

      const progress = (answered / questions.length) * 100;
      progressFill.style.width = progress + '%';
    }
    ` : ''}
  `;
}
工作流 4:表单数据处理

用途: 处理表单提交并处理收集到的数据

步骤:

  1. 设置提交端点
  2. 验证提交的数据
  3. 处理并清理输入
  4. 将数据存储在数据库或电子表格中
  5. 发送确认邮件
  6. 生成汇总报告
  7. 导出数据以供分析
工作流 5:带进度跟踪的多页表单

用途: 将长表单拆分为多个页面,并跟踪进度

步骤:

  1. 将表单划分为逻辑分区
  2. 创建页面导航
  3. 实现进度条
  4. 保存各页面之间的进度
  5. 在每一步添加验证
  6. 允许返回修改
  7. 最后提交所有数据

快速参考

操作命令/触发语
可填写的 PDF 表单"create fillable PDF form"
HTML 表单"generate HTML form with [fields]"
调查问卷"build survey about [topic]"
联系表单"create contact form"
注册表单"make registration form"
反馈表单"generate feedback survey"
多步骤表单"create multi-page form"

最佳实践

  • 清晰的标签: 为所有字段使用描述清晰、简洁的标签
  • 合理分组: 将相关字段放在一起
  • 必填字段: 用星号清楚标记必填字段
  • 帮助文本: 为复杂字段提供说明
  • 验证: 同时实现客户端和服务端验证
  • 错误消息: 显示清晰、有帮助的错误消息
  • 无障碍性: 包含 ARIA 标签和键盘导航
  • 适配移动设备: 确保表单适用于所有设备
  • 进度指示器: 为长表单显示进度
  • 保存进度: 允许用户保存进度并稍后继续
  • 确认: 提交后显示成功消息
  • 安全性: 使用 CSRF 保护并清理输入

常见模式

联系表单:

{
  fields: [
    { name: 'name', type: 'text', label: 'Full Name', required: true },
    { name: 'email', type: 'email', label: 'Email', required: true },
    { name: 'phone', type: 'tel', label: 'Phone Number' },
    { name: 'subject', type: 'text', label: 'Subject', required: true },
    { name: 'message', type: 'textarea', label: 'Message', required: true }
  ]
}

活动注册:

{
  fields: [
    { name: 'attendeeName', type: 'text', required: true },
    { name: 'email', type: 'email', required: true },
    { name: 'ticketType', type: 'select', options: ['General', 'VIP'], required: true },
    { name: 'dietaryRestrictions', type: 'checkbox', options: ['Vegetarian', 'Vegan', 'Gluten-Free'] },
    { name: 'specialRequests', type: 'textarea' }
  ]
}

依赖项

安装所需的软件包:

npm install pdf-lib        # For PDF forms
npm install express        # For form submission handling
npm install validator      # For input validation
npm install nodemailer     # For email confirmations

错误处理

  • 验证错误: 显示具体的字段级错误
  • 网络错误: 妥善处理提交失败
  • 数据丢失: 自动保存进度以防止数据丢失
  • 浏览器兼容性: 在不同浏览器中进行测试

高级功能

条件逻辑:

conditionalLogic: {
  showIf: { field: 'employment', value: 'employed' },
  hideIf: { field: 'student', value: 'yes' }
}

文件上传:

{
  name: 'resume',
  type: 'file',
  accept: '.pdf,.doc,.docx',
  maxSize: 5242880 // 5MB
}

自动保存:

setInterval(() => {
  const formData = new FormData(form);
  localStorage.setItem('formDraft', JSON.stringify(Object.fromEntries(formData)));
}, 30000);

评论

支持 Markdown