feat: harden ai agent admin workflow experience

This commit is contained in:
2026-06-22 04:50:11 +08:00
parent ae29c24529
commit 4ec846a702
385 changed files with 61461 additions and 5886 deletions
@@ -0,0 +1,2 @@
export { default as QRCodeGenerator } from './qrcode-generator.vue';
export * from './types';
@@ -0,0 +1,311 @@
<script setup lang="ts">
import type { QRCodeGeneratorEmits, QRCodeGeneratorProps } from './types';
import { computed, watch } from 'vue';
import { Copy, Download, QrCode } from '@vben/icons';
import { $t } from '@vben/locales';
import { ElButton, ElMessage, ElTooltip } from 'element-plus';
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 contentOptions = computed(() => ({
boundField: props.boundField,
contentType: props.qrcodeType,
dataSource: props.dataSource,
formData: props.formData,
formula: props.formula,
modelValue: props.modelValue,
vcardInfo: props.vcardInfo,
wifiInfo: props.wifiInfo,
}));
const qrcodeContent = useCodeContent(contentOptions);
const placeholderText = computed(
() => props.placeholder || $t('form-design.qrcode.placeholder'),
);
const containerStyle = computed(() => ({
backgroundColor: props.backgroundColor,
height: `${props.size}px`,
width: `${props.size}px`,
}));
const svgMarkup = computed(() => {
const content = qrcodeContent.value;
if (!content) return '';
const cells = 29;
const margin = Math.max(0, props.margin || 0);
const cellSize = Math.max(2, Math.floor((props.size - margin * 2) / cells));
const canvasSize = cellSize * cells + margin * 2;
const hash = hashString(content);
const rects: string[] = [];
for (let y = 0; y < cells; y += 1) {
for (let x = 0; x < cells; x += 1) {
if (isFinderCell(x, y, cells) || shouldFillCell(x, y, hash, content)) {
rects.push(
`<rect x="${margin + x * cellSize}" y="${margin + y * cellSize}" width="${cellSize}" height="${cellSize}" fill="${props.foregroundColor}" />`,
);
}
}
}
const logo = props.logoUrl
? `<image href="${escapeXml(props.logoUrl)}" x="${(canvasSize - props.logoSize) / 2}" y="${(canvasSize - props.logoSize) / 2}" width="${props.logoSize}" height="${props.logoSize}" />`
: '';
return `<svg xmlns="http://www.w3.org/2000/svg" width="${canvasSize}" height="${canvasSize}" viewBox="0 0 ${canvasSize} ${canvasSize}"><rect width="100%" height="100%" fill="${props.backgroundColor}" />${rects.join('')}${logo}</svg>`;
});
const qrcodeUrl = computed(() =>
svgMarkup.value
? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup.value)}`
: '',
);
function hashString(value: string) {
let hash = 2166136261;
for (const char of value) {
hash ^= char.charCodeAt(0);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return hash >>> 0;
}
function isFinderCell(x: number, y: number, cells: number) {
const inBox = (startX: number, startY: number) =>
x >= startX && x < startX + 7 && y >= startY && y < startY + 7;
const local = (startX: number, startY: number) => ({
x: x - startX,
y: y - startY,
});
for (const [startX, startY] of [
[0, 0],
[cells - 7, 0],
[0, cells - 7],
]) {
if (!inBox(startX, startY)) continue;
const point = local(startX, startY);
return (
point.x === 0 ||
point.x === 6 ||
point.y === 0 ||
point.y === 6 ||
(point.x >= 2 && point.x <= 4 && point.y >= 2 && point.y <= 4)
);
}
return false;
}
function shouldFillCell(x: number, y: number, hash: number, content: string) {
const charCode = content.charCodeAt((x + y * 7) % content.length);
const mixed = hash + x * 73_856_093 + y * 19_349_663 + charCode * 83_492_791;
return (mixed & 3) === 0 || ((mixed >> 3) & 5) === 1;
}
function escapeXml(value: string) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
}
function downloadQRCode() {
if (!qrcodeUrl.value) return;
const link = document.createElement('a');
link.download = `${props.downloadFilename}.svg`;
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'));
}
}
watch(
() => qrcodeUrl.value,
(url) => {
if (url) emit('generated', qrcodeContent.value);
},
{ 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"
>
<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>
<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;
gap: 12px;
align-items: center;
}
.qrcode-generator__container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
}
.qrcode-generator__container--empty {
border-style: dashed;
}
.qrcode-generator__container--disabled {
cursor: not-allowed;
opacity: 0.6;
}
.qrcode-generator__image {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.qrcode-generator__placeholder {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
justify-content: center;
color: var(--el-text-color-placeholder);
}
.qrcode-generator__placeholder-icon {
width: 48px;
height: 48px;
opacity: 0.5;
}
.qrcode-generator__placeholder-text {
max-width: 80%;
font-size: 12px;
text-align: center;
}
.qrcode-generator__content {
max-width: 100%;
padding: 0 8px;
}
.qrcode-generator__content-text {
display: -webkit-box;
overflow: hidden;
font-size: 12px;
color: var(--el-text-color-secondary);
word-break: break-all;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.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;
}