new
This commit is contained in:
commit
b76fb31890
72
README.md
Normal file
72
README.md
Normal 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 许可证。
|
73
app/Http/Controllers/QRCodeController.php
Normal file
73
app/Http/Controllers/QRCodeController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
78
app/Services/QRCodeService.php
Normal file
78
app/Services/QRCodeService.php
Normal 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
1
composer.json
Normal 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
1
frontend/package.json
Normal 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
212
frontend/src/App.vue
Normal 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
15
frontend/src/main.js
Normal 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
25
frontend/vite.config.js
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user