本文目录导读:

我来帮您梳理 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>×</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
功能特点
- 实时扫描: 使用摄像头实时识别二维码
- 自动填充: 扫描结果自动填入表单
- 多平台支持: 支持 PC 和移动设备
- 配置灵活: 可自定义摄像頭、声音、震动等
- 服务端生成: 支持生成包含表单数据的二维码
这个方案提供了完整的二维码扫描集成,用户可以直接通过摄像头扫描二维码并自动填充表单数据。