PHP项目Symfony form与二维码扫描

wen PHP项目 3

本文目录导读:

PHP项目Symfony form与二维码扫描

  1. 核心架构
  2. 安装依赖
  3. 功能特点

我来帮您梳理 PHP Symfony 项目结合二维码扫描的方案。

核心架构

前端二维码扫描方案

<!-- views/scanner.html.twig -->
<div class="scanner-container">
    <video id="scanner" autoplay></video>
    <canvas id="canvas" hidden></canvas>
    <button id="startScan">开始扫描</button>
    <p id="result">扫描结果: <span id="scanResult"></span></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
<script>
const video = document.getElementById('scanner');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const resultSpan = document.getElementById('scanResult');
async function startScanner() {
    const stream = await navigator.mediaDevices.getUserMedia({ 
        video: { facingMode: 'environment' } 
    });
    video.srcObject = stream;
    video.play();
    requestAnimationFrame(scan);
}
function scan() {
    if (video.readyState === video.HAVE_ENOUGH_DATA) {
        canvas.height = video.videoHeight;
        canvas.width = video.videoWidth;
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height);
        if (code) {
            resultSpan.textContent = code.data;
            // 自动填充到表单
            document.querySelector('[name="form[qrCode]"]').value = code.data;
        }
    }
    requestAnimationFrame(scan);
}
document.getElementById('startScan').addEventListener('click', startScanner);
</script>

Symfony Form 扩展

// src/Form/Type/QrScannerType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class QrScannerType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        parent::buildForm($builder, $options);
    }
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['scanner_enabled'] = $options['scanner_enabled'];
        $view->vars['scanner_config'] = $options['scanner_config'] ?? [];
    }
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'scanner_enabled' => true,
            'scanner_config' => [
                'camera' => 'environment',
                'sound' => true,
                'vibrate' => true
            ]
        ]);
    }
    public function getParent()
    {
        return TextType::class;
    }
    public function getBlockPrefix()
    {
        return 'qr_scanner';
    }
}

Form 模板自定义

{# templates/form/qr_scanner.html.twig #}
{% block qr_scanner_widget %}
    <div class="qr-scanner-wrapper">
        <div class="input-group">
            {{ block('form_widget_simple') }}
            {% if scanner_enabled %}
                <div class="input-group-append">
                    <button type="button" 
                            class="btn btn-outline-secondary scan-btn"
                            data-target="{{ id }}"
                            data-config="{{ scanner_config|json_encode }}">
                        <i class="fas fa-qrcode"></i> 扫码
                    </button>
                </div>
            {% endif %}
        </div>
        <div id="scanner-modal-{{ id }}" class="modal fade" tabindex="-1">
            <div class="modal-dialog modal-lg">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">扫描二维码</h5>
                        <button type="button" class="close" data-dismiss="modal">
                            <span>&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <video id="video-{{ id }}" autoplay playsinline></video>
                        <canvas id="canvas-{{ id }}" hidden></canvas>
                        <div id="loading-{{ id }}" class="text-center">
                            正在启动相机...
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

Controller 处理

// src/Controller/FormController.php
namespace App\Controller;
use App\Form\Type\QrScannerType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class FormController extends AbstractController
{
    #[Route('/form-with-scanner', name: 'form_with_scanner')]
    public function formWithScanner(Request $request): Response
    {
        $form = $this->createFormBuilder()
            ->add('qrCode', QrScannerType::class, [
                'label' => '二维码内容',
                'scanner_enabled' => true,
                'scanner_config' => [
                    'camera' => 'environment', // 后置摄像头
                    'sound' => true,
                    'vibrate' => false
                ],
                'required' => false,
                'attr' => [
                    'placeholder' => '扫描或输入二维码内容'
                ]
            ])
            ->add('submit', SubmitType::class, [
                'label' => '提交'
            ])
            ->getForm();
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();
            // 处理二维码数据
            return $this->redirectToRoute('success_page');
        }
        return $this->render('form/scanner.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

二维码生成(服务端)

// src/Service/QrCodeService.php
namespace App\Service;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\Label\LabelAlignment;
use Endroid\QrCode\Label\Font\NotoSans;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;
class QrCodeService
{
    public function generateQrCode(string $data, array $options = []): string
    {
        $builder = new Builder(
            writer: new PngWriter(),
            writerOptions: [],
            validateResult: false,
            data: $data,
            encoding: new Encoding('UTF-8'),
            errorCorrectionLevel: new ErrorCorrectionLevelHigh(),
            size: $options['size'] ?? 300,
            margin: $options['margin'] ?? 10,
            roundBlockSizeMode: new RoundBlockSizeModeMargin(),
        );
        if (isset($options['label'])) {
            $builder->label(
                text: $options['label'],
                alignment: new LabelAlignment('center'),
                font: new NotoSans(20)
            );
        }
        $result = $builder->build();
        return $result->getDataUri();
    }
    public function generateQrCodeForForm(string $formData): string
    {
        // 生成包含表单数据的二维码
        $qrData = json_encode([
            'type' => 'form_submit',
            'data' => $formData,
            'timestamp' => time()
        ]);
        return $this->generateQrCode($qrData, [
            'size' => 200,
            'label' => '扫描提交'
        ]);
    }
}

JavaScript 增强功能

// public/js/qr-scanner.js
class QrScanner {
    constructor(elementId, config = {}) {
        this.elementId = elementId;
        this.config = {
            camera: 'environment',
            sound: true,
            vibrate: true,
            ...config
        };
        this.isScanning = false;
        this.init();
    }
    init() {
        const scanBtn = document.querySelector(`[data-target="${this.elementId}"]`);
        if (!scanBtn) return;
        scanBtn.addEventListener('click', () => this.openModal());
    }
    openModal() {
        const modal = new bootstrap.Modal(
            document.getElementById(`scanner-modal-${this.elementId}`)
        );
        modal.show();
        modal._element.addEventListener('shown.bs.modal', () => {
            this.startScan();
        });
        modal._element.addEventListener('hidden.bs.modal', () => {
            this.stopScan();
        });
    }
    async startScan() {
        this.isScanning = true;
        const video = document.getElementById(`video-${this.elementId}`);
        try {
            const stream = await navigator.mediaDevices.getUserMedia({
                video: { facingMode: this.config.camera }
            });
            video.srcObject = stream;
            video.play();
            this.scanLoop();
        } catch (error) {
            console.error('Camera access error:', error);
            this.showError('无法访问摄像头');
        }
    }
    scanLoop() {
        if (!this.isScanning) return;
        const video = document.getElementById(`video-${this.elementId}`);
        const canvas = document.getElementById(`canvas-${this.elementId}`);
        const ctx = canvas.getContext('2d');
        if (video.readyState === video.HAVE_ENOUGH_DATA) {
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const code = jsQR(imageData.data, imageData.width, imageData.height);
            if (code) {
                this.onSuccess(code.data);
                return;
            }
        }
        requestAnimationFrame(() => this.scanLoop());
    }
    onSuccess(data) {
        document.getElementById(this.elementId).value = data;
        if (this.config.sound) {
            this.playSuccessSound();
        }
        if (this.config.vibrate && navigator.vibrate) {
            navigator.vibrate(200);
        }
        this.stopScan();
        bootstrap.Modal.getInstance(
            document.getElementById(`scanner-modal-${this.elementId}`)
        ).hide();
    }
    stopScan() {
        this.isScanning = false;
        const video = document.getElementById(`video-${this.elementId}`);
        if (video && video.srcObject) {
            video.srcObject.getTracks().forEach(track => track.stop());
        }
    }
    playSuccessSound() {
        // 播放提示音
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const oscillator = audioContext.createOscillator();
        oscillator.frequency.value = 800;
        oscillator.connect(audioContext.destination);
        oscillator.start();
        setTimeout(() => oscillator.stop(), 200);
    }
    showError(message) {
        const loading = document.getElementById(`loading-${this.elementId}`);
        if (loading) {
            loading.textContent = message;
            loading.style.color = '#dc3545';
        }
    }
}
// 初始化扫码器
document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('.scan-btn').forEach(btn => {
        const target = btn.dataset.target;
        const config = JSON.parse(btn.dataset.config || '{}');
        new QrScanner(target, config);
    });
});

实体关联

// src/Entity/Product.php
namespace App\Entity;
use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;
    #[ORM\Column(length: 255)]
    private ?string $name = null;
    #[ORM\Column(length: 255, nullable: true)]
    private ?string $qrCode = null;
    // getters and setters...
}

安装依赖

composer require endroid/qr-code
npm install jsqr

功能特点

  1. 实时扫描: 使用摄像头实时识别二维码
  2. 自动填充: 扫描结果自动填入表单
  3. 多平台支持: 支持 PC 和移动设备
  4. 配置灵活: 可自定义摄像頭、声音、震动等
  5. 服务端生成: 支持生成包含表单数据的二维码

这个方案提供了完整的二维码扫描集成,用户可以直接通过摄像头扫描二维码并自动填充表单数据。

抱歉,评论功能暂时关闭!