使用React如何静默打印页面:完整的前端打印解决方案

关键词:React静默打印, web-print-pdf npm包, 前端打印, 无预览打印, 自动化打印, React组件, 企业级打印, 打印解决方案
摘要:本文深入介绍如何在React应用中实现静默打印功能,基于web-print-pdf npm包提供完整的打印解决方案。文章涵盖了React组件设计、打印API集成、错误处理、性能优化等关键技术点,为React开发者提供了实用的静默打印实现指南。

引言

在现代React应用开发中,静默打印 是一个重要的技术需求,特别是在企业级应用中需要自动化打印的场景。传统的Web打印方案存在用户交互、兼容性差等问题,而web-print-pdf npm包为React应用提供了完美的静默打印解决方案。

本文将详细介绍如何在React应用中集成web-print-pdf npm包,实现高效、稳定的静默打印功能,包括组件设计、API调用、错误处理等关键技术点。

React静默打印的核心需求

1. 核心功能特性

根据实际项目经验,React静默打印需要满足以下核心需求:

  • 🖨️ 打印预览与直接打印:支持打印预览功能,用户可以在打印前预览内容,也支持直接打印无需预览
  • 📄 强大的报表功能:支持各种复杂报表,包括交叉表、嵌套表、二维码、图片等所有内容类型
  • 🎯 精确打印控制:可设置页眉、页脚、页边距、打印份数、纸张大小等详细信息
  • 📊 多格式内容支持:HTML、PDF、图片、Base64等多种内容格式
  • ⚡ 高性能处理:支持批量打印、队列管理、并发控制
  • 🔄 完善错误处理:完善的错误提示和重试机制
  • 📱 响应式适配:适配不同设备和屏幕尺寸

2. 技术架构优势

web-print-pdf npm包在React应用中的技术优势:

  • 零依赖部署:基于npm包,无需安装额外插件
  • 跨浏览器兼容:通过本地服务规避浏览器差异
  • 企业级功能:支持打印队列、并发控制、错误重试
  • 开发友好:API简洁,集成方便

React静默打印实现方案

1. 安装和配置

安装web-print-pdf npm包
bash 复制代码
npm install web-print-pdf
基础配置
javascript 复制代码
// utils/printConfig.js
import { printHtml, printPdfByUrl, printHtmlByUrl } from 'web-print-pdf';

// 默认打印配置
export const defaultPrintOptions = {
  silent: true,           // 静默打印
  copies: 1,             // 打印份数
  paper: 'A4',           // 纸张大小
  orientation: 'portrait', // 打印方向
  margin: {
    top: 10,
    right: 10,
    bottom: 10,
    left: 10
  }
};

// 打印机配置
export const printerConfig = {
  defaultPrinter: true,   // 使用默认打印机
  // printer: 'HP-LaserJet-Pro', // 指定打印机
};

2. React打印Hook设计

自定义打印Hook
javascript 复制代码
// hooks/usePrint.js
import { useState, useCallback } from 'react';
import { printHtml, printPdfByUrl, printHtmlByUrl } from 'web-print-pdf';
import { defaultPrintOptions, printerConfig } from '../utils/printConfig';

export const usePrint = () => {
  const [isPrinting, setIsPrinting] = useState(false);
  const [printError, setPrintError] = useState(null);

  // 打印HTML内容
  const printHtmlContent = useCallback(async (htmlContent, options = {}) => {
    try {
      setIsPrinting(true);
      setPrintError(null);

      const printOptions = {
        ...defaultPrintOptions,
        ...printerConfig,
        ...options
      };

      await printHtml({
        content: htmlContent,
        ...printOptions
      });

      console.log('HTML内容打印成功');
    } catch (error) {
      console.error('HTML打印失败:', error);
      setPrintError(error.message);
      throw error;
    } finally {
      setIsPrinting(false);
    }
  }, []);

  // 打印PDF文件
  const printPdfFile = useCallback(async (pdfUrl, options = {}) => {
    try {
      setIsPrinting(true);
      setPrintError(null);

      const printOptions = {
        ...defaultPrintOptions,
        ...printerConfig,
        ...options
      };

      await printPdfByUrl({
        url: pdfUrl,
        ...printOptions
      });

      console.log('PDF文件打印成功');
    } catch (error) {
      console.error('PDF打印失败:', error);
      setPrintError(error.message);
      throw error;
    } finally {
      setIsPrinting(false);
    }
  }, []);

  // 打印网页URL
  const printWebPage = useCallback(async (pageUrl, options = {}) => {
    try {
      setIsPrinting(true);
      setPrintError(null);

      const printOptions = {
        ...defaultPrintOptions,
        ...printerConfig,
        ...options
      };

      await printHtmlByUrl({
        url: pageUrl,
        ...printOptions
      });

      console.log('网页打印成功');
    } catch (error) {
      console.error('网页打印失败:', error);
      setPrintError(error.message);
      throw error;
    } finally {
      setIsPrinting(false);
    }
  }, []);

  return {
    isPrinting,
    printError,
    printHtmlContent,
    printPdfFile,
    printWebPage
  };
};

3. React打印组件实现

基础打印组件
javascript 复制代码
// components/PrintButton.jsx
import React from 'react';
import { usePrint } from '../hooks/usePrint';

const PrintButton = ({ 
  content, 
  type = 'html', 
  options = {},
  children,
  className = '',
  disabled = false 
}) => {
  const { isPrinting, printError, printHtmlContent, printPdfFile, printWebPage } = usePrint();

  const handlePrint = async () => {
    try {
      switch (type) {
        case 'html':
          await printHtmlContent(content, options);
          break;
        case 'pdf':
          await printPdfFile(content, options);
          break;
        case 'url':
          await printWebPage(content, options);
          break;
        default:
          throw new Error('不支持的打印类型');
      }
    } catch (error) {
      console.error('打印失败:', error);
    }
  };

  return (
    <div className="print-button-container">
      <button
        onClick={handlePrint}
        disabled={disabled || isPrinting}
        className={`print-btn ${className} ${isPrinting ? 'printing' : ''}`}
      >
        {isPrinting ? '打印中...' : (children || '打印')}
      </button>
      
      {printError && (
        <div className="print-error">
          打印失败: {printError}
        </div>
      )}
    </div>
  );
};

export default PrintButton;
高级打印组件
javascript 复制代码
// components/AdvancedPrintPanel.jsx
import React, { useState } from 'react';
import { usePrint } from '../hooks/usePrint';

const AdvancedPrintPanel = ({ content, type = 'html' }) => {
  const { isPrinting, printError, printHtmlContent, printPdfFile, printWebPage } = usePrint();
  
  const [printOptions, setPrintOptions] = useState({
    copies: 1,
    paper: 'A4',
    orientation: 'portrait',
    margin: { top: 10, right: 10, bottom: 10, left: 10 },
    header: '', // 页眉设置
    footer: '', // 页脚设置
    printer: '', // 空字符串表示使用默认打印机
    silent: true,
    preview: false // 是否显示预览
  });

  const handlePrint = async () => {
    try {
      switch (type) {
        case 'html':
          await printHtmlContent(content, printOptions);
          break;
        case 'pdf':
          await printPdfFile(content, printOptions);
          break;
        case 'url':
          await printWebPage(content, printOptions);
          break;
      }
    } catch (error) {
      console.error('打印失败:', error);
    }
  };

  return (
    <div className="advanced-print-panel">
      <h3>打印设置</h3>
      
      <div className="print-options">
        <div className="option-group">
          <label>打印份数:</label>
          <input
            type="number"
            min="1"
            max="99"
            value={printOptions.copies}
            onChange={(e) => setPrintOptions({
              ...printOptions,
              copies: parseInt(e.target.value) || 1
            })}
          />
        </div>

        <div className="option-group">
          <label>纸张大小:</label>
          <select
            value={printOptions.paper}
            onChange={(e) => setPrintOptions({
              ...printOptions,
              paper: e.target.value
            })}
          >
            <option value="A4">A4</option>
            <option value="A3">A3</option>
            <option value="Letter">Letter</option>
            <option value="Legal">Legal</option>
          </select>
        </div>

        <div className="option-group">
          <label>打印方向:</label>
          <select
            value={printOptions.orientation}
            onChange={(e) => setPrintOptions({
              ...printOptions,
              orientation: e.target.value
            })}
          >
            <option value="portrait">纵向</option>
            <option value="landscape">横向</option>
          </select>
        </div>

        <div className="option-group">
          <label>页眉:</label>
          <input
            type="text"
            placeholder="设置页眉内容"
            value={printOptions.header}
            onChange={(e) => setPrintOptions({
              ...printOptions,
              header: e.target.value
            })}
          />
        </div>

        <div className="option-group">
          <label>页脚:</label>
          <input
            type="text"
            placeholder="设置页脚内容"
            value={printOptions.footer}
            onChange={(e) => setPrintOptions({
              ...printOptions,
              footer: e.target.value
            })}
          />
        </div>

        <div className="option-group">
          <label>页边距 (mm):</label>
          <div className="margin-controls">
            <input
              type="number"
              placeholder="上"
              value={printOptions.margin.top}
              onChange={(e) => setPrintOptions({
                ...printOptions,
                margin: { ...printOptions.margin, top: parseInt(e.target.value) || 0 }
              })}
            />
            <input
              type="number"
              placeholder="右"
              value={printOptions.margin.right}
              onChange={(e) => setPrintOptions({
                ...printOptions,
                margin: { ...printOptions.margin, right: parseInt(e.target.value) || 0 }
              })}
            />
            <input
              type="number"
              placeholder="下"
              value={printOptions.margin.bottom}
              onChange={(e) => setPrintOptions({
                ...printOptions,
                margin: { ...printOptions.margin, bottom: parseInt(e.target.value) || 0 }
              })}
            />
            <input
              type="number"
              placeholder="左"
              value={printOptions.margin.left}
              onChange={(e) => setPrintOptions({
                ...printOptions,
                margin: { ...printOptions.margin, left: parseInt(e.target.value) || 0 }
              })}
            />
          </div>
        </div>

        <div className="option-group">
          <label>打印机:</label>
          <input
            type="text"
            placeholder="留空使用默认打印机"
            value={printOptions.printer}
            onChange={(e) => setPrintOptions({
              ...printOptions,
              printer: e.target.value
            })}
          />
        </div>

        <div className="option-group">
          <label>
            <input
              type="checkbox"
              checked={printOptions.preview}
              onChange={(e) => setPrintOptions({
                ...printOptions,
                preview: e.target.checked
              })}
            />
            打印预览
          </label>
        </div>
      </div>

      <button
        onClick={handlePrint}
        disabled={isPrinting}
        className="print-btn"
      >
        {isPrinting ? '打印中...' : '开始打印'}
      </button>

      {printError && (
        <div className="error-message">
          错误: {printError}
        </div>
      )}
    </div>
  );
};

export default AdvancedPrintPanel;

4. 批量打印实现

批量打印Hook
javascript 复制代码
// hooks/useBatchPrint.js
import { useState, useCallback } from 'react';
import { usePrint } from './usePrint';

export const useBatchPrint = () => {
  const { printHtmlContent, printPdfFile, printWebPage } = usePrint();
  const [batchStatus, setBatchStatus] = useState({
    isProcessing: false,
    completed: 0,
    total: 0,
    errors: []
  });

  const processBatch = useCallback(async (printTasks, options = {}) => {
    setBatchStatus({
      isProcessing: true,
      completed: 0,
      total: printTasks.length,
      errors: []
    });

    const errors = [];

    for (let i = 0; i < printTasks.length; i++) {
      const task = printTasks[i];
      
      try {
        switch (task.type) {
          case 'html':
            await printHtmlContent(task.content, { ...options, ...task.options });
            break;
          case 'pdf':
            await printPdfFile(task.content, { ...options, ...task.options });
            break;
          case 'url':
            await printWebPage(task.content, { ...options, ...task.options });
            break;
        }
      } catch (error) {
        errors.push({
          index: i,
          task: task,
          error: error.message
        });
      }

      setBatchStatus(prev => ({
        ...prev,
        completed: i + 1
      }));
    }

    setBatchStatus(prev => ({
      ...prev,
      isProcessing: false,
      errors
    }));

    return {
      success: errors.length === 0,
      errors
    };
  }, [printHtmlContent, printPdfFile, printWebPage]);

  return {
    batchStatus,
    processBatch
  };
};
批量打印组件
javascript 复制代码
// components/BatchPrintPanel.jsx
import React, { useState } from 'react';
import { useBatchPrint } from '../hooks/useBatchPrint';

const BatchPrintPanel = () => {
  const { batchStatus, processBatch } = useBatchPrint();
  const [printTasks, setPrintTasks] = useState([]);

  const addTask = (task) => {
    setPrintTasks(prev => [...prev, task]);
  };

  const removeTask = (index) => {
    setPrintTasks(prev => prev.filter((_, i) => i !== index));
  };

  const handleBatchPrint = async () => {
    await processBatch(printTasks);
  };

  return (
    <div className="batch-print-panel">
      <h3>批量打印</h3>
      
      <div className="task-list">
        {printTasks.map((task, index) => (
          <div key={index} className="task-item">
            <span>{task.name || `任务 ${index + 1}`}</span>
            <button onClick={() => removeTask(index)}>删除</button>
          </div>
        ))}
      </div>

      <div className="batch-controls">
        <button onClick={handleBatchPrint} disabled={batchStatus.isProcessing}>
          {batchStatus.isProcessing ? '处理中...' : '开始批量打印'}
        </button>
      </div>

      {batchStatus.isProcessing && (
        <div className="progress-info">
          进度: {batchStatus.completed}/{batchStatus.total}
        </div>
      )}

      {batchStatus.errors.length > 0 && (
        <div className="error-list">
          <h4>打印错误:</h4>
          {batchStatus.errors.map((error, index) => (
            <div key={index} className="error-item">
              任务 {error.index + 1}: {error.error}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default BatchPrintPanel;

5. 实际应用示例

复杂报表打印组件
javascript 复制代码
// components/ComplexReportPrint.jsx
import React from 'react';
import PrintButton from './PrintButton';

const ComplexReportPrint = ({ reportData }) => {
  const generateComplexReportHtml = (data) => {
    return `
      <div style="font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto;">
        <!-- 页眉 -->
        <div style="text-align: center; margin-bottom: 20px; border-bottom: 2px solid #333;">
          <h1 style="color: #333; margin: 0;">${data.title}</h1>
          <p style="margin: 5px 0; color: #666;">${data.subtitle}</p>
        </div>
        
        <!-- 交叉报表 -->
        <div style="margin: 20px 0;">
          <h3>销售交叉报表</h3>
          <table style="width: 100%; border-collapse: collapse; border: 2px solid #333;">
            <thead>
              <tr style="background-color: #f0f0f0;">
                <th style="border: 1px solid #333; padding: 10px; text-align: center;">产品类别</th>
                <th style="border: 1px solid #333; padding: 10px; text-align: center;">Q1销售额</th>
                <th style="border: 1px solid #333; padding: 10px; text-align: center;">Q2销售额</th>
                <th style="border: 1px solid #333; padding: 10px; text-align: center;">Q3销售额</th>
                <th style="border: 1px solid #333; padding: 10px; text-align: center;">Q4销售额</th>
                <th style="border: 1px solid #333; padding: 10px; text-align: center;">总计</th>
              </tr>
            </thead>
            <tbody>
              ${data.crossTable.map(row => `
                <tr>
                  <td style="border: 1px solid #333; padding: 8px; font-weight: bold;">${row.category}</td>
                  <td style="border: 1px solid #333; padding: 8px; text-align: right;">¥${row.q1}</td>
                  <td style="border: 1px solid #333; padding: 8px; text-align: right;">¥${row.q2}</td>
                  <td style="border: 1px solid #333; padding: 8px; text-align: right;">¥${row.q3}</td>
                  <td style="border: 1px solid #333; padding: 8px; text-align: right;">¥${row.q4}</td>
                  <td style="border: 1px solid #333; padding: 8px; text-align: right; font-weight: bold;">¥${row.total}</td>
                </tr>
              `).join('')}
            </tbody>
          </table>
        </div>

        <!-- 嵌套表格 -->
        <div style="margin: 20px 0;">
          <h3>详细产品信息</h3>
          ${data.nestedTables.map(table => `
            <div style="margin: 15px 0; border: 1px solid #ddd; padding: 10px;">
              <h4 style="margin: 0 0 10px 0; color: #333;">${table.category}</h4>
              <table style="width: 100%; border-collapse: collapse;">
                <thead>
                  <tr style="background-color: #f8f8f8;">
                    <th style="border: 1px solid #ddd; padding: 8px;">产品名称</th>
                    <th style="border: 1px solid #ddd; padding: 8px;">规格</th>
                    <th style="border: 1px solid #ddd; padding: 8px;">库存</th>
                    <th style="border: 1px solid #ddd; padding: 8px;">价格</th>
                  </tr>
                </thead>
                <tbody>
                  ${table.products.map(product => ` <tr> <td style="border: 1px solid #ddd; padding: 8px;">${product.name}</td> <td style="border: 1px solid #ddd; padding: 8px;">${product.spec}</td> <td style="border: 1px solid #ddd; padding: 8px; text-align: center;">${product.stock}</td> <td style="border: 1px solid #ddd; padding: 8px; text-align: right;">¥${product.price}</td> </tr> `).join('')}
                </tbody>
              </table>
            </div>
          `).join('')}
        </div>

        <!-- 二维码和图片 -->
        <div style="margin: 20px 0; display: flex; justify-content: space-between; align-items: center;">
          <div style="text-align: center;">
            <h4>二维码</h4>
            <div style="width: 100px; height: 100px; border: 1px solid #ddd; display: flex; align-items: center; justify-content: center;">
              <span style="font-size: 12px; color: #666;">二维码图片</span>
            </div>
            <p style="margin: 5px 0; font-size: 12px;">${data.qrCode}</p>
          </div>
          
          <div style="text-align: center;">
            <h4>产品图片</h4>
            <div style="width: 100px; height: 100px; border: 1px solid #ddd; display: flex; align-items: center; justify-content: center;">
              <span style="font-size: 12px; color: #666;">产品图片</span>
            </div>
            <p style="margin: 5px 0; font-size: 12px;">产品展示图</p>
          </div>
        </div>

        <!-- 页脚 -->
        <div style="margin-top: 30px; text-align: center; border-top: 1px solid #ddd; padding-top: 10px;">
          <p style="margin: 5px 0; color: #666;">生成时间: ${new Date().toLocaleString()}</p>
          <p style="margin: 5px 0; color: #666;">报表编号: ${data.reportId}</p>
        </div>
      </div>
    `;
  };

  return (
    <div className="complex-report-print">
      <PrintButton
        content={generateComplexReportHtml(reportData)}
        type="html"
        options={{
          copies: 1,
          paper: 'A4',
          orientation: 'landscape',
          margin: { top: 15, right: 15, bottom: 15, left: 15 },
          header: '复杂报表 - 销售数据分析',
          footer: '第 {page} 页,共 {pages} 页',
          silent: true,
          preview: true
        }}
        className="report-print-btn"
      >
        打印复杂报表
      </PrintButton>
    </div>
  );
};

export default ComplexReportPrint;

性能优化和最佳实践

1. 打印性能优化

javascript 复制代码
// utils/printOptimizer.js
export class PrintOptimizer {
  constructor() {
    this.printQueue = [];
    this.isProcessing = false;
    this.maxConcurrent = 3; // 最大并发数
  }

  // 队列管理
  addToQueue(printTask) {
    this.printQueue.push(printTask);
    this.processQueue();
  }

  async processQueue() {
    if (this.isProcessing || this.printQueue.length === 0) {
      return;
    }

    this.isProcessing = true;
    const concurrentTasks = [];

    // 控制并发数量
    for (let i = 0; i < Math.min(this.maxConcurrent, this.printQueue.length); i++) {
      const task = this.printQueue.shift();
      concurrentTasks.push(this.executeTask(task));
    }

    await Promise.allSettled(concurrentTasks);
    this.isProcessing = false;

    // 继续处理剩余任务
    if (this.printQueue.length > 0) {
      setTimeout(() => this.processQueue(), 100);
    }
  }

  async executeTask(task) {
    try {
      await task.execute();
      console.log('打印任务完成:', task.id);
    } catch (error) {
      console.error('打印任务失败:', task.id, error);
    }
  }
}

2. 错误处理和重试机制

javascript 复制代码
// utils/printRetry.js
export const withRetry = async (printFunction, maxRetries = 3, delay = 1000) => {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await printFunction();
    } catch (error) {
      lastError = error;
      console.warn(`打印尝试 ${attempt} 失败:`, error.message);

      if (attempt < maxRetries) {
        console.log(`等待 ${delay}ms 后重试...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        delay *= 2; // 指数退避
      }
    }
  }

  throw new Error(`打印失败,已重试 ${maxRetries} 次: ${lastError.message}`);
};

// 使用示例
const printWithRetry = async (content, options) => {
  return withRetry(async () => {
    return await printHtml({ content, ...options });
  });
};

3. 打印状态监控

javascript 复制代码
// hooks/usePrintStatus.js
import { useState, useEffect } from 'react';

export const usePrintStatus = () => {
  const [status, setStatus] = useState({
    isConnected: false,
    printerCount: 0,
    lastPrintTime: null,
    errorCount: 0
  });

  useEffect(() => {
    // 检查打印服务连接状态
    const checkConnection = async () => {
      try {
        // 这里可以调用web-print-pdf的状态检查API
        setStatus(prev => ({ ...prev, isConnected: true }));
      } catch (error) {
        setStatus(prev => ({ ...prev, isConnected: false }));
      }
    };

    checkConnection();
    const interval = setInterval(checkConnection, 5000);

    return () => clearInterval(interval);
  }, []);

  return status;
};

总结

通过web-print-pdf npm包,React应用可以轻松实现强大的静默打印功能:

核心优势

  1. 开发简单:基于npm包,API简洁易用
  2. 功能强大:支持打印预览、直接打印、页眉页脚、页边距、打印份数、纸张大小等完整功能
  3. 报表能力:支持强大的报表功能,包括交叉表、嵌套表、二维码、图片等所有内容类型
  4. 性能优秀:支持批量打印、队列管理、并发控制
  5. 错误处理:完善的错误提示和重试机制
  6. 企业级:支持打印机管理、打印参数控制

技术要点

  1. Hook设计:使用自定义Hook封装打印逻辑
  2. 组件化:可复用的打印组件设计
  3. 性能优化:队列管理、并发控制、错误重试
  4. 状态管理:完善的打印状态监控

最佳实践

  1. 错误处理:完善的错误提示和重试机制
  2. 性能优化:合理的队列管理和并发控制
  3. 用户体验:清晰的打印状态反馈
  4. 代码复用:可复用的Hook和组件设计

web-print-pdf npm包为React应用提供了完整的静默打印解决方案,让开发者能够轻松实现企业级的打印功能,提升用户体验和开发效率。

常见问题解答 (FAQ)

Q1: web-print-pdf npm包在React中如何集成?

A: 通过npm安装后,可以使用自定义Hook封装打印逻辑,然后创建可复用的打印组件。

Q2: 如何实现批量打印功能?

A: 使用队列管理机制,控制并发数量,支持批量任务处理。

Q3: 如何处理打印错误和重试?

A: 实现指数退避的重试机制,提供完善的错误提示和状态反馈。

Q4: 如何优化打印性能?

A: 通过队列管理、并发控制、任务优先级等方式优化打印性能。

Q5: 如何监控打印状态?

A: 使用WebSocket连接监控打印服务状态,提供实时的状态反馈。

相关推荐
喜欢踢足球的老罗3 小时前
[特殊字符] PM2 入门实战:从 0 到线上托管 React SPA
前端·react.js·前端框架
叶凡要飞3 小时前
RTX5060Ti安装cuda加速的openCV
1024程序员节
MATLAB代码顾问3 小时前
MATLAB 实现图像边缘检测与轮廓提取(Canny、Sobel、Prewitt 算子对比)
1024程序员节
DeeplyMind3 小时前
AMD rocr-libhsakmt分析系列3-4:svm-reserve模式实现分析
linux·驱动开发·1024程序员节·amdgpu·kfd·rocr
神秘的土鸡3 小时前
从数据仓库到数据中台再到数据飞轮:我的数据技术成长之路
java·服务器·aigc·数据库架构·1024程序员节
烦恼归林3 小时前
学习经验分享篇(4)——硕士入门电机控制的经历经验分享
经验分享·电机·电力电子·1024程序员节·电机控制·永磁同步电机·simulink仿真
sponge'3 小时前
opencv学习笔记6:SVM分类器
人工智能·机器学习·支持向量机·1024程序员节
小光学长3 小时前
基于Vue的课程达成度分析系统t84pzgwk(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库·vue.js
allnlei3 小时前
使用CLion进行远程开发(Remote Development)
ide·1024程序员节