From 9e325bcab0ba83457c399921a3ac005713f5bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E7=93=9C=C2=B0?= Date: Sat, 3 Oct 2026 01:24:44 +0800 Subject: [PATCH] =?UTF-8?q?new:=E6=96=B0=E5=A2=9E=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 8 ++ frontend/src/pages/Character/Index.vue | 105 ++++++++++++++++++ .../Skill/components/SkillTree/index.tsx | 2 +- 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 643863f0..d20791d6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@vueuse/core": "^12.8.2", "axios": "^1.8.2", "hoci": "^0.6.0", + "html-to-image": "^1.11.13", "pako": "^2.1.0", "pinia": "^3.0.1", "query-string": "^9.1.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ab3513a5..bea804b8 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: hoci: specifier: ^0.6.0 version: 0.6.0(@babel/parser@7.26.9)(@vueuse/core@12.8.2(typescript@5.8.2))(rollup@4.35.0)(vue@3.5.13(typescript@5.8.2)) + html-to-image: + specifier: ^1.11.13 + version: 1.11.13 pako: specifier: ^2.1.0 version: 2.1.0 @@ -1547,6 +1550,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-to-image@1.11.13: + resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} + human-signals@8.0.0: resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} engines: {node: '>=18.18.0'} @@ -3877,6 +3883,8 @@ snapshots: hookable@5.5.3: {} + html-to-image@1.11.13: {} + human-signals@8.0.0: {} ignore@5.3.2: {} diff --git a/frontend/src/pages/Character/Index.vue b/frontend/src/pages/Character/Index.vue index eef55aba..af91ab65 100644 --- a/frontend/src/pages/Character/Index.vue +++ b/frontend/src/pages/Character/Index.vue @@ -28,6 +28,13 @@ class="ml-1" icon="exchange" > + @@ -51,8 +58,16 @@ import ImportVue from './Config/import.vue' import ImportSkillVue from './Config/importSkill.vue' import ImportDnfhelper from './Config/importDnfhelper.vue' import Result from './Result/Index.vue' +import SkillTree from './Skill/components/SkillTree' import { useDnfHelperStore } from '@/stores/dnfhelper.ts' import { type IHelperRoleInfo } from '@/api/info/type' +import { useDialog } from '@/components/hooks/dialog' +import { toPng } from 'html-to-image' +import { h, nextTick, render } from 'vue' + +const skillTreeExportWidth = 550 +const skillTreeExportPixelRatio = 2 + const props = defineProps<{ alter: string }>() @@ -63,6 +78,8 @@ const importVisible = ref(false) const importSkillVisible = ref(false) const helperStore = useDnfHelperStore() const toImport = ref() +const exportingSkillTree = ref(false) +const { alert } = useDialog() // const shouldWatch = ref(true) @@ -92,6 +109,79 @@ const showImportSkill = () => { importSkillVisible.value = true } +const waitForSkillImages = async (root: HTMLElement) => { + const pendingImages = Array.from(root.querySelectorAll('img')).filter((image) => !image.complete) + if (pendingImages.length === 0) return + + await Promise.race([ + Promise.all( + pendingImages.map( + (image) => + new Promise((resolve) => { + image.addEventListener('load', () => resolve(), { once: true }) + image.addEventListener('error', () => resolve(), { once: true }) + }), + ), + ), + new Promise((resolve) => window.setTimeout(resolve, 3000)), + ]) +} + +const exportSkillTree = async () => { + if (exportingSkillTree.value || !infoStore.infos) return + + exportingSkillTree.value = true + const exportHost = document.createElement('div') + exportHost.className = 'skill-tree-export-host' + const exportRoot = document.createElement('div') + exportRoot.className = 'skill-tree-export-root' + exportHost.appendChild(exportRoot) + document.body.appendChild(exportHost) + + try { + render( + h(SkillTree, { + skills: infoStore.skills, + lvInfo: configStore.config.skills, + bindAwake: configStore.config.bindAwake, + alter: infoStore.infos.alter, + }), + exportRoot, + ) + + await nextTick() + await document.fonts.ready + await waitForSkillImages(exportRoot) + + const exportHeight = exportRoot.scrollHeight + const dataUrl = await toPng(exportRoot, { + width: skillTreeExportWidth, + height: exportHeight, + canvasWidth: skillTreeExportWidth, + canvasHeight: exportHeight, + pixelRatio: skillTreeExportPixelRatio, + backgroundColor: '#101012', + cacheBust: true, + }) + + const fileName = `${infoStore.infos.name || props.alter}-技能加点.png`.replace( + /[\\/:*?"<>|]/g, + '-', + ) + const downloadLink = document.createElement('a') + downloadLink.download = fileName + downloadLink.href = dataUrl + downloadLink.click() + } catch (error) { + console.error('技能加点导出失败', error) + await alert({ content: '技能加点导出失败,请稍后重试' }) + } finally { + render(null, exportRoot) + exportHost.remove() + exportingSkillTree.value = false + } +} + const route = useRoute() onMounted(async () => { @@ -163,4 +253,19 @@ onUnmounted(() => { left: 0; background-color: rgba(50, 50, 50, 0.75); } + +.skill-tree-export-host { + position: fixed; + top: 0; + left: -10000px; + pointer-events: none; +} + +.skill-tree-export-root { + width: 550px; + min-width: 550px; + overflow: visible; + color: #e9c558; + background: #101012; +} diff --git a/frontend/src/pages/Character/Skill/components/SkillTree/index.tsx b/frontend/src/pages/Character/Skill/components/SkillTree/index.tsx index 65acf22a..2df161ab 100644 --- a/frontend/src/pages/Character/Skill/components/SkillTree/index.tsx +++ b/frontend/src/pages/Character/Skill/components/SkillTree/index.tsx @@ -4,7 +4,7 @@ import { useVModel } from '@vueuse/core' import CalcButton from '@/components/calc/button/index.vue' import './style.scss' -const rowPrefixes = ['通用', 1, '', 10, '', 20, '', 30, '', 40, 50, 60, 70, 80, 90, 100] +const rowPrefixes = ['通用', 1, '', 10, '', 20, '', 30, '', 40, '', 50, 60, 70, 80, 90, 100] const defaultColumnCount = 11 const altersWithoutRowPrefix = new Set(['creator', 'dark_knight'])