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",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"element-plus": "^2.9.1",
|
"element-plus": "^2.9.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
"vue": "^3.3.10",
|
"vue": "^3.3.10",
|
||||||
"vue-i18n": "^11.1.1",
|
"vue-i18n": "^11.1.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="toolbar" :class="{ 'toolbar--embed': props.isEmbed }">
|
<div class="toolbar" :class="{ 'toolbar--embed': props.isEmbed }">
|
||||||
<div class="toolbar-actions">
|
<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="Download" type="primary" @click="handleExport">{{ t('export') }}</el-button>
|
||||||
<el-button icon="View" type="success" @click="handlePreviewData">数据预览</el-button>
|
<el-button icon="View" type="success" @click="handlePreviewData">数据预览</el-button>
|
||||||
<el-button icon="Share" type="primary" @click="prepareCapture">{{ t('prepareCapture') }}</el-button>
|
<el-button icon="Share" type="primary" @click="prepareCapture">{{ t('prepareCapture') }}</el-button>
|
||||||
@@ -113,6 +113,72 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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%">
|
<el-dialog v-model="state.showAssetManagerDialog" title="素材管理" width="70%">
|
||||||
<div class="asset-manager-actions">
|
<div class="asset-manager-actions">
|
||||||
@@ -348,6 +414,7 @@ import {
|
|||||||
type ExpressionRuleDefinition,
|
type ExpressionRuleDefinition,
|
||||||
type RuleVariableDefinition
|
type RuleVariableDefinition
|
||||||
} from '@/configs/groupRules';
|
} from '@/configs/groupRules';
|
||||||
|
import { convertTeamCodeToRootDocument, decodeTeamCodeFromQrImage } from '@/utils/teamCodeService';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
isEmbed?: boolean;
|
isEmbed?: boolean;
|
||||||
@@ -379,10 +446,16 @@ const state = reactive({
|
|||||||
showUpdateLogDialog: false, // 控制更新日志对话框的显示状态
|
showUpdateLogDialog: false, // 控制更新日志对话框的显示状态
|
||||||
showFeedbackFormDialog: false, // 控制反馈表单对话框的显示状态
|
showFeedbackFormDialog: false, // 控制反馈表单对话框的显示状态
|
||||||
showDataPreviewDialog: false, // 控制数据预览对话框的显示状态
|
showDataPreviewDialog: false, // 控制数据预览对话框的显示状态
|
||||||
|
showImportDialog: false, // 控制导入来源对话框
|
||||||
|
importingTeamCode: false, // 阵容码导入中
|
||||||
|
decodingTeamCodeQr: false, // 阵容码二维码识别中
|
||||||
showAssetManagerDialog: false, // 控制素材管理对话框的显示状态
|
showAssetManagerDialog: false, // 控制素材管理对话框的显示状态
|
||||||
showRuleManagerDialog: false, // 控制规则管理对话框的显示状态
|
showRuleManagerDialog: false, // 控制规则管理对话框的显示状态
|
||||||
previewDataContent: '', // 存储预览的数据内容
|
previewDataContent: '', // 存储预览的数据内容
|
||||||
});
|
});
|
||||||
|
const importSource = ref<'json' | 'teamCode'>('json');
|
||||||
|
const teamCodeInput = ref('');
|
||||||
|
const teamCodeQrInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const assetLibraries = ASSET_LIBRARIES.map((item) => ({
|
const assetLibraries = ASSET_LIBRARIES.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
label: `${item.label}素材`
|
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');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = '.json';
|
input.accept = '.json';
|
||||||
@@ -936,10 +1024,56 @@ const handleImport = () => {
|
|||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
|
target.value = '';
|
||||||
};
|
};
|
||||||
input.click();
|
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 = () => {
|
const handleResetWorkspace = () => {
|
||||||
ElMessageBox.confirm('确定重置当前工作区?该操作不可撤销', '提示', {
|
ElMessageBox.confirm('确定重置当前工作区?该操作不可撤销', '提示', {
|
||||||
confirmButtonText: '重置',
|
confirmButtonText: '重置',
|
||||||
@@ -1221,6 +1355,22 @@ const handleClose = (done) => {
|
|||||||
margin-bottom: 12px;
|
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 {
|
.asset-upload-input {
|
||||||
display: none;
|
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