Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,2 @@
export { default as QRCodeGenerator } from './qrcode-generator.vue';
export * from './types';
@@ -0,0 +1,341 @@
<script setup lang="ts">
import type { QRCodeGeneratorEmits, QRCodeGeneratorProps } from './types';
import { computed, nextTick, ref, watch } from 'vue';
import { Copy, Download, QrCode } from '@vben/icons';
import { $t } from '@vben/locales';
import { ElButton, ElMessage, ElTooltip } from 'element-plus';
import QRCode from 'qrcode';
import { useCodeContent } from '../shared/useCodeContent';
defineOptions({
name: 'QRCodeGenerator',
inheritAttrs: false,
});
const props = withDefaults(defineProps<QRCodeGeneratorProps>(), {
dataSource: 'static',
qrcodeType: 'text',
size: 200,
errorCorrectionLevel: 'M',
foregroundColor: '#000000',
backgroundColor: '#FFFFFF',
logoSize: 40,
margin: 2,
showContent: false,
enableDownload: false,
enableCopy: false,
downloadFilename: 'qrcode',
disabled: false,
readonly: false,
placeholder: '',
});
const emit = defineEmits<QRCodeGeneratorEmits>();
const qrcodeUrl = ref<string>('');
const isGenerating = ref(false);
const canvasRef = ref<HTMLCanvasElement>();
const contentOptions = computed(() => ({
dataSource: props.dataSource,
modelValue: props.modelValue,
boundField: props.boundField,
formula: props.formula,
formData: props.formData,
contentType: props.qrcodeType,
vcardInfo: props.vcardInfo,
wifiInfo: props.wifiInfo,
}));
const qrcodeContent = useCodeContent(contentOptions);
async function generateQRCode() {
const content = qrcodeContent.value;
if (!content) {
qrcodeUrl.value = '';
return;
}
isGenerating.value = true;
try {
const canvas = canvasRef.value;
if (!canvas) return;
await QRCode.toCanvas(canvas, content, {
width: props.size,
margin: props.margin,
errorCorrectionLevel: props.errorCorrectionLevel,
color: {
dark: props.foregroundColor,
light: props.backgroundColor,
},
});
if (props.logoUrl) {
await drawLogo(canvas, props.logoUrl);
}
qrcodeUrl.value = canvas.toDataURL('image/png');
emit('generated', content);
} catch (error) {
console.error('Generate QRCode failed:', error);
ElMessage.error($t('form-design.qrcode.generateError'));
} finally {
isGenerating.value = false;
}
}
async function drawLogo(
canvas: HTMLCanvasElement,
logoUrl: string,
): Promise<void> {
return new Promise((resolve) => {
const ctx = canvas.getContext('2d');
if (!ctx) {
resolve();
return;
}
const img = new Image();
img.crossOrigin = 'anonymous';
img.addEventListener('load', () => {
const logoSize = props.logoSize;
const x = (canvas.width - logoSize) / 2;
const y = (canvas.height - logoSize) / 2;
ctx.fillStyle = props.backgroundColor;
ctx.fillRect(x - 2, y - 2, logoSize + 4, logoSize + 4);
ctx.drawImage(img, x, y, logoSize, logoSize);
resolve();
});
img.onerror = () => {
console.warn('Load logo failed');
resolve();
};
img.src = logoUrl;
});
}
function downloadQRCode() {
if (!qrcodeUrl.value) return;
const link = document.createElement('a');
link.download = `${props.downloadFilename}.png`;
link.href = qrcodeUrl.value;
link.click();
emit('downloaded');
ElMessage.success($t('form-design.qrcode.downloadSuccess'));
}
async function copyContent() {
const content = qrcodeContent.value;
if (!content) return;
try {
await navigator.clipboard.writeText(content);
emit('copied');
ElMessage.success($t('form-design.qrcode.copySuccess'));
} catch (error) {
console.error('Copy failed:', error);
ElMessage.error($t('form-design.qrcode.copyError'));
}
}
const placeholderText = computed(
() => props.placeholder || $t('form-design.qrcode.placeholder'),
);
const containerStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
backgroundColor: props.backgroundColor,
}));
watch(
() => [
qrcodeContent.value,
props.size,
props.foregroundColor,
props.backgroundColor,
props.errorCorrectionLevel,
props.margin,
props.logoUrl,
props.logoSize,
],
() => {
nextTick(() => {
generateQRCode();
});
},
{ immediate: true },
);
</script>
<template>
<div class="qrcode-generator">
<div
class="qrcode-generator__container"
:class="{
'qrcode-generator__container--empty': !qrcodeContent,
'qrcode-generator__container--disabled': disabled,
}"
:style="containerStyle"
>
<canvas ref="canvasRef" style="display: none"></canvas>
<img
v-if="qrcodeUrl"
:src="qrcodeUrl"
alt="QRCode"
class="qrcode-generator__image"
/>
<div v-else class="qrcode-generator__placeholder">
<QrCode class="qrcode-generator__placeholder-icon" />
<span class="qrcode-generator__placeholder-text">{{
placeholderText
}}</span>
</div>
<div v-if="isGenerating" class="qrcode-generator__loading">
<div class="qrcode-generator__spinner"></div>
</div>
</div>
<div v-if="showContent && qrcodeContent" class="qrcode-generator__content">
<ElTooltip :content="qrcodeContent" placement="top" :show-after="300">
<span class="qrcode-generator__content-text">{{ qrcodeContent }}</span>
</ElTooltip>
</div>
<div
v-if="(enableDownload || enableCopy) && qrcodeUrl && !disabled"
class="qrcode-generator__actions"
>
<ElButton
v-if="enableDownload"
type="primary"
size="small"
:icon="Download"
@click="downloadQRCode"
>
{{ $t('form-design.qrcode.download') }}
</ElButton>
<ElButton
v-if="enableCopy"
size="small"
:icon="Copy"
@click="copyContent"
>
{{ $t('form-design.qrcode.copy') }}
</ElButton>
</div>
</div>
</template>
<style scoped>
.qrcode-generator {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.qrcode-generator__container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
overflow: hidden;
}
.qrcode-generator__container--empty {
border-style: dashed;
}
.qrcode-generator__container--disabled {
opacity: 0.6;
cursor: not-allowed;
}
.qrcode-generator__image {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.qrcode-generator__placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--el-text-color-placeholder);
}
.qrcode-generator__placeholder-icon {
width: 48px;
height: 48px;
opacity: 0.5;
}
.qrcode-generator__placeholder-text {
font-size: 12px;
text-align: center;
max-width: 80%;
}
.qrcode-generator__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: color-mix(in srgb, var(--el-bg-color) 80%, transparent);
}
.qrcode-generator__spinner {
width: 24px;
height: 24px;
border: 2px solid var(--el-border-color);
border-top-color: var(--el-color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.qrcode-generator__content {
max-width: 100%;
padding: 0 8px;
}
.qrcode-generator__content-text {
font-size: 12px;
color: var(--el-text-color-secondary);
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.qrcode-generator__actions {
display: flex;
gap: 8px;
}
</style>
@@ -0,0 +1,33 @@
import type {
CodeDisplayBaseProps,
ErrorCorrectionLevel,
QRCodeType,
VCardInfo,
WiFiInfo,
} from '../shared/types';
export type { QRCodeType, ErrorCorrectionLevel, VCardInfo, WiFiInfo };
export type QRCodeDataSource = 'field' | 'formula' | 'static';
export interface QRCodeGeneratorProps extends CodeDisplayBaseProps {
modelValue?: null | string;
qrcodeType?: QRCodeType;
size?: number;
errorCorrectionLevel?: ErrorCorrectionLevel;
foregroundColor?: string;
backgroundColor?: string;
logoUrl?: string;
logoSize?: number;
margin?: number;
vcardInfo?: VCardInfo;
wifiInfo?: WiFiInfo;
}
export interface QRCodeGeneratorEmits {
(e: 'update:modelValue', value: null | string): void;
(e: 'change', value: null | string): void;
(e: 'generated', content: string): void;
(e: 'downloaded'): void;
(e: 'copied'): void;
}