feat: harden ai agent admin workflow experience
This commit is contained in:
@@ -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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user