1
0
This commit is contained in:
Snowz 2025-04-14 16:35:05 +08:00
commit b76fb31890
8 changed files with 477 additions and 0 deletions

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# Modern QRCode Generator
这是一个基于开源项目 [PHP QR Code](http://phpqrcode.sourceforge.net/) 重构的现代化二维码生成工具。我们对原项目进行了全面的改进和优化,同时保留了其核心功能和开源精神。
## 主要改进
- 使用 Laravel 框架重构后端,提供更好的代码组织和安全性
- 采用 Vue.js 构建响应式前端界面,提供更好的用户体验
- 增加二维码样式自定义功能
- 支持二维码 Logo 添加
- 实现二维码生成缓存机制
- 完善的API文档和单元测试
## 技术栈
- 后端Laravel 10.x
- 前端Vue 3 + Vite
- UI框架Element Plus
- 二维码生成PHP QR Code Library
- 缓存Redis
## 快速开始
### 环境要求
- PHP >= 8.1
- Composer
- Node.js >= 16
- Redis
### 安装步骤
1. 克隆项目
```bash
git clone [项目地址]
cd modern-qrcode
```
2. 安装后端依赖
```bash
composer install
cp .env.example .env
php artisan key:generate
```
3. 安装前端依赖
```bash
cd frontend
npm install
```
4. 启动开发服务器
```bash
# 后端
php artisan serve
# 前端
cd frontend
npm run dev
```
## API文档
访问 `/api/documentation` 查看完整的API文档。
## 致谢
感谢原项目 [PHP QR Code](http://phpqrcode.sourceforge.net/) 提供的优秀代码基础本项目在其基础上进行了现代化改造。原项目采用LGPL 3协议开源。
## 许可证
本项目采用 MIT 许可证。

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Services\QRCodeService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class QRCodeController extends Controller
{
protected $qrCodeService;
public function __construct(QRCodeService $qrCodeService)
{
$this->qrCodeService = $qrCodeService;
}
public function generate(Request $request)
{
$validator = Validator::make($request->all(), [
'text' => 'required|string|max:2048',
'size' => 'required|integer|min:100|max:800',
'errorCorrection' => 'required|in:L,M,Q,H',
'logo' => 'nullable|string'
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
$cacheKey = md5($request->text . $request->size . $request->errorCorrection . $request->logo);
try {
$qrCode = Cache::remember($cacheKey, 3600, function () use ($request) {
return $this->qrCodeService->generate(
$request->text,
$request->size,
$request->errorCorrection,
$request->logo
);
});
return response($qrCode)
->header('Content-Type', 'image/png')
->header('Cache-Control', 'public, max-age=3600');
} catch (\Exception $e) {
return response()->json(['error' => '二维码生成失败'], 500);
}
}
public function uploadLogo(Request $request)
{
$validator = Validator::make($request->all(), [
'file' => 'required|image|mimes:jpeg,png,jpg|max:2048'
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
try {
$path = $request->file('file')->store('logos', 'public');
return response()->json([
'url' => asset('storage/' . $path)
]);
} catch (\Exception $e) {
return response()->json(['error' => 'Logo上传失败'], 500);
}
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace App\Services;
use Intervention\Image\Facades\Image;
use Exception;
class QRCodeService
{
protected $phpqrcode;
protected $tempDir;
public function __construct()
{
$this->tempDir = storage_path('app/temp');
if (!file_exists($this->tempDir)) {
mkdir($this->tempDir, 0777, true);
}
// 引入PHP QR Code库
require_once base_path('vendor/phpqrcode/qrlib.php');
$this->phpqrcode = new \QRcode();
}
public function generate(string $text, int $size, string $errorCorrection = 'M', ?string $logoPath = null)
{
$tempFile = $this->tempDir . '/' . md5($text . time()) . '.png';
// 设置错误纠正级别
$errorCorrectionLevel = match($errorCorrection) {
'L' => QR_ECLEVEL_L,
'M' => QR_ECLEVEL_M,
'Q' => QR_ECLEVEL_Q,
'H' => QR_ECLEVEL_H,
default => QR_ECLEVEL_M
};
try {
// 生成二维码
$this->phpqrcode->png($text, $tempFile, $errorCorrectionLevel, $size);
// 如果提供了Logo则合并Logo
if ($logoPath) {
$this->mergeLogo($tempFile, $logoPath);
}
// 读取生成的图片
$qrCode = file_get_contents($tempFile);
// 删除临时文件
unlink($tempFile);
return $qrCode;
} catch (Exception $e) {
if (file_exists($tempFile)) {
unlink($tempFile);
}
throw $e;
}
}
protected function mergeLogo(string $qrCodePath, string $logoPath)
{
$qrCode = Image::make($qrCodePath);
$logo = Image::make($logoPath);
// 调整Logo大小不超过二维码的1/4
$logoSize = min($qrCode->width(), $qrCode->height()) / 4;
$logo->resize($logoSize, $logoSize, function ($constraint) {
$constraint->aspectRatio();
});
// 在二维码中心添加Logo
$qrCode->insert($logo, 'center');
$qrCode->save($qrCodePath);
}
}

1
composer.json Normal file
View File

@ -0,0 +1 @@
{"name":"modern-qrcode/generator","type":"project","description":"A modern QR code generator built with Laravel","keywords":["qrcode","laravel","generator"],"license":"MIT","require":{"php":"^8.1","laravel/framework":"^10.0","guzzlehttp/guzzle":"^7.2","intervention/image":"^2.7","predis/predis":"^2.0"},"require-dev":{"fakerphp/faker":"^1.9.1","laravel/pint":"^1.0","laravel/sail":"^1.18","mockery/mockery":"^1.4.4","nunomaduro/collision":"^7.0","phpunit/phpunit":"^10.1","spatie/laravel-ignition":"^2.0"},"autoload":{"psr-4":{"App\\":"app/","Database\\Factories\\":"database/factories/","Database\\Seeders\\":"database/seeders/"}},"autoload-dev":{"psr-4":{"Tests\\":"tests/"}},"scripts":{"post-autoload-dump":["Illuminate\\Foundation\\ComposerScripts::postAutoloadDump","@php artisan package:discover --ansi"],"post-update-cmd":["@php artisan vendor:publish --tag=laravel-assets --ansi --force"],"post-root-package-install":["@php -r \"file_exists('.env') || copy('.env.example', '.env');\""],"post-create-project-cmd":["@php artisan key:generate --ansi"]},"extra":{"laravel":{"dont-discover":[]}},"config":{"optimize-autoloader":true,"preferred-install":"dist","sort-packages":true,"allow-plugins":{"pestphp/pest-plugin":true,"php-http/discovery":true}}}

1
frontend/package.json Normal file
View File

@ -0,0 +1 @@
{"name":"modern-qrcode-frontend","private":true,"version":"0.0.0","type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"dependencies":{"@element-plus/icons-vue":"^2.1.0","axios":"^1.6.0","element-plus":"^2.4.0","pinia":"^2.1.0","vue":"^3.3.0","vue-router":"^4.2.0"},"devDependencies":{"@vitejs/plugin-vue":"^4.5.0","sass":"^1.69.0","unplugin-auto-import":"^0.16.0","unplugin-vue-components":"^0.25.0","vite":"^5.0.0"}}

212
frontend/src/App.vue Normal file
View File

@ -0,0 +1,212 @@
<template>
<el-container class="app-container">
<el-header class="app-header">
<h1>现代化二维码生成器</h1>
</el-header>
<el-main>
<el-row :gutter="20" justify="center">
<el-col :span="12">
<el-card class="qr-form">
<el-form :model="form" label-position="top">
<el-form-item label="内容">
<el-input
v-model="form.text"
type="textarea"
:rows="3"
placeholder="请输入需要生成二维码的内容"
/>
</el-form-item>
<el-form-item label="尺寸">
<el-slider
v-model="form.size"
:min="100"
:max="800"
:step="50"
show-input
/>
</el-form-item>
<el-form-item label="错误纠正级别">
<el-select v-model="form.errorCorrection" class="w-full">
<el-option label="低 (L)" value="L" />
<el-option label="中 (M)" value="M" />
<el-option label="高 (Q)" value="Q" />
<el-option label="最高 (H)" value="H" />
</el-select>
</el-form-item>
<el-form-item label="Logo">
<el-upload
class="logo-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleLogoSuccess"
>
<img v-if="form.logo" :src="form.logo" class="logo" />
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="generateQRCode" :loading="loading">
生成二维码
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="12" v-if="qrCodeUrl">
<el-card class="qr-preview">
<div class="qr-image-container">
<img :src="qrCodeUrl" alt="生成的二维码" />
<el-button type="success" @click="downloadQRCode">
下载二维码
</el-button>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
<el-footer class="app-footer">
<p>基于开源项目 PHP QR Code 重构的现代化二维码生成工具</p>
</el-footer>
</el-container>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const form = reactive({
text: '',
size: 300,
errorCorrection: 'M',
logo: ''
})
const loading = ref(false)
const qrCodeUrl = ref('')
const generateQRCode = async () => {
if (!form.text) {
ElMessage.warning('请输入需要生成二维码的内容')
return
}
loading.value = true
try {
const response = await fetch('/api/qrcode/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(form)
})
if (response.ok) {
const blob = await response.blob()
qrCodeUrl.value = URL.createObjectURL(blob)
ElMessage.success('二维码生成成功')
} else {
throw new Error('生成失败')
}
} catch (error) {
ElMessage.error('二维码生成失败,请重试')
} finally {
loading.value = false
}
}
const handleLogoSuccess = (response) => {
form.logo = response.url
ElMessage.success('Logo上传成功')
}
const downloadQRCode = () => {
const link = document.createElement('a')
link.href = qrCodeUrl.value
link.download = 'qrcode.png'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>
<style scoped>
.app-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.app-header {
background-color: #409eff;
color: white;
text-align: center;
line-height: 60px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-footer {
text-align: center;
color: #909399;
padding: 20px 0;
}
.qr-form {
margin-top: 20px;
}
.qr-preview {
margin-top: 20px;
}
.qr-image-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.qr-image-container img {
max-width: 100%;
height: auto;
}
.logo-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
}
.logo-uploader:hover {
border-color: #409eff;
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
}
.logo {
width: 100px;
height: 100px;
display: block;
object-fit: contain;
}
.w-full {
width: 100%;
}
</style>

15
frontend/src/main.js Normal file
View File

@ -0,0 +1,15 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

25
frontend/vite.config.js Normal file
View File

@ -0,0 +1,25 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
}
})