本文目录导读:

我来帮您梳理 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 和移动设备
- 配置灵活: 可自定义摄像頭、声音、震动等
- 服务端生成: 支持生成包含表单数据的二维码
这个方案提供了完整的二维码扫描集成,用户可以直接通过摄像头扫描二维码并自动填充表单数据。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播