mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 06:55:26 +00:00
feat: add team-code UI import and API integration
This commit is contained in:
@@ -56,6 +56,7 @@
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"element-plus": "^2.9.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.3.10",
|
||||
"vue-i18n": "^11.1.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="toolbar" :class="{ 'toolbar--embed': props.isEmbed }">
|
||||
<div class="toolbar-actions">
|
||||
<el-button icon="Upload" type="primary" @click="handleImport">{{ t('import') }}</el-button>
|
||||
<el-button icon="Upload" type="primary" @click="openImportDialog">{{ t('import') }}</el-button>
|
||||
<el-button icon="Download" type="primary" @click="handleExport">{{ t('export') }}</el-button>
|
||||
<el-button icon="View" type="success" @click="handlePreviewData">数据预览</el-button>
|
||||
<el-button icon="Share" type="primary" @click="prepareCapture">{{ t('prepareCapture') }}</el-button>
|
||||
@@ -113,6 +113,72 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="state.showImportDialog" title="导入数据" width="560px">
|
||||
<el-form label-width="88px" class="import-form">
|
||||
<el-form-item label="导入来源">
|
||||
<el-radio-group v-model="importSource">
|
||||
<el-radio-button label="json">JSON 文件</el-radio-button>
|
||||
<el-radio-button label="teamCode">阵容码</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="importSource === 'teamCode'" label="阵容码">
|
||||
<el-input
|
||||
v-model="teamCodeInput"
|
||||
type="textarea"
|
||||
:rows="7"
|
||||
placeholder="请粘贴 #TA# 开头的阵容码"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="importSource === 'teamCode'" label="二维码">
|
||||
<div class="team-code-qr-actions">
|
||||
<input
|
||||
ref="teamCodeQrInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="asset-upload-input"
|
||||
@change="handleTeamCodeQrImport"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="state.decodingTeamCodeQr"
|
||||
@click="triggerTeamCodeQrImport"
|
||||
>
|
||||
选择二维码图片
|
||||
</el-button>
|
||||
<span class="team-code-qr-tip">支持从截图或相册图片识别官方阵容码二维码</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
v-if="importSource === 'teamCode'"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="支持粘贴阵容码字符串,或上传二维码图片自动识别。"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="state.showImportDialog = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="importSource === 'json'"
|
||||
type="primary"
|
||||
@click="triggerJsonFileImport"
|
||||
>
|
||||
选择 JSON 文件
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
:loading="state.importingTeamCode"
|
||||
@click="handleTeamCodeImport"
|
||||
>
|
||||
导入阵容码
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 素材管理对话框 -->
|
||||
<el-dialog v-model="state.showAssetManagerDialog" title="素材管理" width="70%">
|
||||
<div class="asset-manager-actions">
|
||||
@@ -348,6 +414,7 @@ import {
|
||||
type ExpressionRuleDefinition,
|
||||
type RuleVariableDefinition
|
||||
} from '@/configs/groupRules';
|
||||
import { convertTeamCodeToRootDocument, decodeTeamCodeFromQrImage } from '@/utils/teamCodeService';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
isEmbed?: boolean;
|
||||
@@ -379,10 +446,16 @@ const state = reactive({
|
||||
showUpdateLogDialog: false, // 控制更新日志对话框的显示状态
|
||||
showFeedbackFormDialog: false, // 控制反馈表单对话框的显示状态
|
||||
showDataPreviewDialog: false, // 控制数据预览对话框的显示状态
|
||||
showImportDialog: false, // 控制导入来源对话框
|
||||
importingTeamCode: false, // 阵容码导入中
|
||||
decodingTeamCodeQr: false, // 阵容码二维码识别中
|
||||
showAssetManagerDialog: false, // 控制素材管理对话框的显示状态
|
||||
showRuleManagerDialog: false, // 控制规则管理对话框的显示状态
|
||||
previewDataContent: '', // 存储预览的数据内容
|
||||
});
|
||||
const importSource = ref<'json' | 'teamCode'>('json');
|
||||
const teamCodeInput = ref('');
|
||||
const teamCodeQrInputRef = ref<HTMLInputElement | null>(null);
|
||||
const assetLibraries = ASSET_LIBRARIES.map((item) => ({
|
||||
id: item.id,
|
||||
label: `${item.label}素材`
|
||||
@@ -914,7 +987,22 @@ const copyDataToClipboard = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const openImportDialog = () => {
|
||||
importSource.value = 'json';
|
||||
teamCodeInput.value = '';
|
||||
state.showImportDialog = true;
|
||||
};
|
||||
|
||||
const triggerJsonFileImport = () => {
|
||||
state.showImportDialog = false;
|
||||
handleJsonImport();
|
||||
};
|
||||
|
||||
const triggerTeamCodeQrImport = () => {
|
||||
teamCodeQrInputRef.value?.click();
|
||||
};
|
||||
|
||||
const handleJsonImport = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
@@ -936,10 +1024,56 @@ const handleImport = () => {
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
target.value = '';
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleTeamCodeImport = async () => {
|
||||
const rawTeamCode = teamCodeInput.value.trim();
|
||||
if (!rawTeamCode) {
|
||||
showMessage('warning', '请先粘贴阵容码');
|
||||
return;
|
||||
}
|
||||
|
||||
state.importingTeamCode = true;
|
||||
try {
|
||||
const rootDocument = await convertTeamCodeToRootDocument(rawTeamCode);
|
||||
filesStore.importData(rootDocument);
|
||||
refreshLogicFlowCanvas('LogicFlow 画布已重新渲染(阵容码导入)');
|
||||
state.showImportDialog = false;
|
||||
teamCodeInput.value = '';
|
||||
showMessage('success', '阵容码导入成功');
|
||||
} catch (error: any) {
|
||||
console.error('阵容码导入失败:', error);
|
||||
showMessage('error', error?.message || '阵容码导入失败');
|
||||
} finally {
|
||||
state.importingTeamCode = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTeamCodeQrImport = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement | null;
|
||||
const file = target?.files?.[0];
|
||||
if (!file) {
|
||||
if (target) target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
state.decodingTeamCodeQr = true;
|
||||
try {
|
||||
const decodedTeamCode = await decodeTeamCodeFromQrImage(file);
|
||||
teamCodeInput.value = decodedTeamCode;
|
||||
showMessage('success', '二维码识别成功,已填入阵容码');
|
||||
} catch (error: any) {
|
||||
console.error('二维码识别失败:', error);
|
||||
showMessage('error', error?.message || '二维码识别失败');
|
||||
} finally {
|
||||
state.decodingTeamCodeQr = false;
|
||||
if (target) target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetWorkspace = () => {
|
||||
ElMessageBox.confirm('确定重置当前工作区?该操作不可撤销', '提示', {
|
||||
confirmButtonText: '重置',
|
||||
@@ -1221,6 +1355,22 @@ const handleClose = (done) => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.import-form {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.team-code-qr-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.team-code-qr-tip {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.asset-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
122
src/utils/teamCodeService.ts
Normal file
122
src/utils/teamCodeService.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import jsQR from 'jsqr'
|
||||
|
||||
const DEFAULT_TEAM_CODE_SERVICE_URL = 'http://127.0.0.1:8788/api/team-code/convert'
|
||||
|
||||
export const TEAM_CODE_SERVICE_URL = (
|
||||
import.meta.env.VITE_TEAM_CODE_SERVICE_URL as string | undefined
|
||||
)?.trim() || DEFAULT_TEAM_CODE_SERVICE_URL
|
||||
|
||||
type UnknownRecord = Record<string, unknown>
|
||||
|
||||
const normalizeText = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')
|
||||
|
||||
const readErrorMessageFromPayload = (payload: unknown): string => {
|
||||
if (!payload || typeof payload !== 'object') return ''
|
||||
const record = payload as UnknownRecord
|
||||
return normalizeText(record.error) || normalizeText(record.message)
|
||||
}
|
||||
|
||||
const pickRootDocument = (payload: unknown): UnknownRecord | null => {
|
||||
if (!payload || typeof payload !== 'object') return null
|
||||
const root = payload as UnknownRecord
|
||||
if (Array.isArray(root.fileList)) return root
|
||||
|
||||
const candidates = [root.data, root.root, root.payload]
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate || typeof candidate !== 'object') continue
|
||||
if (Array.isArray((candidate as UnknownRecord).fileList)) {
|
||||
return candidate as UnknownRecord
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const createImageElementFromFile = (file: File): Promise<HTMLImageElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
const image = new Image()
|
||||
image.onload = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
resolve(image)
|
||||
}
|
||||
image.onerror = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
reject(new Error('无法读取二维码图片,请确认图片格式正确'))
|
||||
}
|
||||
image.src = url
|
||||
})
|
||||
}
|
||||
|
||||
const extractImageData = (image: HTMLImageElement, scale = 1): ImageData => {
|
||||
const width = Math.max(1, Math.floor(image.naturalWidth * scale))
|
||||
const height = Math.max(1, Math.floor(image.naturalHeight * scale))
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) {
|
||||
throw new Error('浏览器无法创建 Canvas 2D 上下文')
|
||||
}
|
||||
context.drawImage(image, 0, 0, width, height)
|
||||
return context.getImageData(0, 0, width, height)
|
||||
}
|
||||
|
||||
// 前端仅负责二维码图片识别为字符串,不做阵容码协议解析
|
||||
export const decodeTeamCodeFromQrImage = async (file: File): Promise<string> => {
|
||||
if (!file) {
|
||||
throw new Error('请选择二维码图片')
|
||||
}
|
||||
|
||||
const image = await createImageElementFromFile(file)
|
||||
const scales = [1, 0.75, 0.5, 1.5, 2]
|
||||
|
||||
for (const scale of scales) {
|
||||
const imageData = extractImageData(image, scale)
|
||||
const result = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: 'attemptBoth',
|
||||
})
|
||||
const rawValue = normalizeText(result?.data)
|
||||
if (rawValue) return rawValue
|
||||
}
|
||||
|
||||
throw new Error('未识别到有效二维码,请确认图片清晰且仅包含一个阵容码二维码')
|
||||
}
|
||||
|
||||
// 阵容码字符串 -> RootDocument 由后端服务完成
|
||||
export const convertTeamCodeToRootDocument = async (
|
||||
teamCode: string,
|
||||
serviceUrl = TEAM_CODE_SERVICE_URL,
|
||||
): Promise<UnknownRecord> => {
|
||||
const normalizedTeamCode = normalizeText(teamCode)
|
||||
if (!normalizedTeamCode) {
|
||||
throw new Error('请输入阵容码')
|
||||
}
|
||||
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ teamCode: normalizedTeamCode }),
|
||||
})
|
||||
|
||||
let payload: unknown = null
|
||||
try {
|
||||
payload = await response.json()
|
||||
} catch (error) {
|
||||
console.error('解析后端响应失败:', error)
|
||||
throw new Error('阵容码服务返回了非 JSON 响应')
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = readErrorMessageFromPayload(payload)
|
||||
throw new Error(errorMessage || '阵容码解析失败')
|
||||
}
|
||||
|
||||
const root = pickRootDocument(payload)
|
||||
if (!root) {
|
||||
throw new Error('阵容码解析成功,但响应不包含可导入的 fileList')
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user