This commit is contained in:
feie9454 2025-07-13 14:50:33 +08:00
commit 6ae842176f
111 changed files with 6347 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>5大潜能测试</title>
</head>
<body>
<div id="app"></div>
<!-- 公众号 JSSDK -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 云开发 Web SDK -->
<script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1797
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "loreal-survey",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build --emptyOutDir",
"preview": "vite preview"
},
"dependencies": {
"@types/node": "^22.10.1",
"dedent": "^1.5.3",
"vue": "^3.5.13",
"vue-i18n": "^10.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"sass-embedded": "^1.82.0",
"typescript": "~5.6.2",
"vite": "^6.0.1",
"vue-tsc": "^2.1.10"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

164
src/App.vue Normal file
View File

@ -0,0 +1,164 @@
<script setup lang="ts">
import Home from './views/Home.vue';
import imgs from './assets/imgs'
import { DimensionColors, DimensionName, findAllTraitByDimension, getRandomDimensionRating, getRandomTraitRating, TraitName } from './config';
import Game from './views/Game.vue';
import { computed, Ref, ref } from 'vue';
import Result from './views/Result.vue';
const pIndex = ref(0)
const result: Ref<Record<DimensionName, number>> = ref(getRandomDimensionRating())
const trait: Ref<Record<TraitName, number>> = ref(getRandomTraitRating())
function resetGame() {
result.value = getRandomDimensionRating()
trait.value = getRandomTraitRating()
pIndex.value = 1
rarity.value = Math.floor(Math.random() * 11) + 80
}
function skipGame() {
pIndex.value = 2
}
function finishGame(r: Record<DimensionName, number>, t: Record<TraitName, number>) {
pIndex.value = 2
result.value = r
trait.value = t
}
import lang from './locates'
import Report from './views/Report.vue';
import PersonalInfo from './views/PersonalInfo.vue';
import ReportMask from './views/ReportMask.vue';
const GraphDimensions = computed(() => {
const average = Object.values(result.value).reduce((acc, cur) => acc + cur, 0) / 5;
const p = 1.1
const sign = (num: number) => num > 0 ? 1 : -1
const newScores = Object.entries(result.value).map(([dimName, score]) => {
const mean = average
const newScore = Math.max(8, score + sign(score - mean) * Math.abs(score - mean) ** p)
return {
name_zh: lang.zh_CN.questionData.DimensionName[dimName as DimensionName],
name_en: lang.en.questionData.DimensionName[dimName as DimensionName],
value: newScore,
color: DimensionColors[dimName as DimensionName],
iconUrl: imgs.icons[dimName as DimensionName],
}
})
return newScores
})
//Random from 80 to 99
const rarity: Ref<number> = ref(Math.floor(Math.random() * 19) + 80)
const topFiveTraits = computed(() => {
const traitsSet = new Set<TraitName>();
const dimensions = Object.entries(result.value) as [DimensionName, number][];
const sortedDimensions = dimensions.sort((a, b) => b[1] - a[1]);
let targetScore = 5;
while (targetScore > 0 && traitsSet.size < 5) {
for (const [dimName] of sortedDimensions) {
const dimTraits = findAllTraitByDimension(dimName);
dimTraits.forEach(traitName => {
if (trait.value[traitName] === targetScore) {
traitsSet.add(traitName);
}
});
if (traitsSet.size >= 5) break;
}
targetScore--;
}
return Array.from(traitsSet).slice(0, 5);
});
const gender = ref('_gender_')
const username = ref('_username_')
const age = ref(0)
function submitUserInfo(data: { gender: string, username: string, age: number }) {
gender.value = data.gender
username.value = data.username
age.value = data.age
pIndex.value = 3
}
const showDebug = ref(1)
</script>
<template>
<div class="main">
<div class="debug" v-if="showDebug > 0 ">
<button v-for="i in [0, 1, 2, 3, 4,5]" @click="pIndex = i">qIndex = {{ i
}}</button>
<button
@click="result = getRandomDimensionRating(); trait = getRandomTraitRating()">RandomResult</button>
<button @click="$i18n.locale = $i18n.locale === 'en' ? 'zh' : 'en'">Lang:
{{ $i18n.locale }}</button>
</div>
<TransitionGroup name="fade">
<Home key="home" v-if="pIndex == 0" @next="pIndex = 1" />
<Game key="game" v-if="pIndex == 1" @skip="skipGame"
@finish="finishGame" />
<PersonalInfo key="personalInfo" v-if="pIndex == 2"
@submit="submitUserInfo" />
<Result key="result" v-if="pIndex == 3" :result="result" :trait="trait"
:top-five-traits="topFiveTraits" @next="pIndex++"
:graph="GraphDimensions" @retry="resetGame" :rarity="rarity" />
<Report key="report" v-if="pIndex == 4" :result="result" :trait="trait"
:top-five-traits="topFiveTraits" :username="username"
:graph="GraphDimensions" @retry="resetGame" :rarity="rarity" @next="pIndex++" />
<ReportMask key="report-mask" v-if="pIndex == 5" :result="result" :trait="trait"
:top-five-traits="topFiveTraits" :username="username"
:graph="GraphDimensions" @retry="resetGame" :rarity="rarity"@back="pIndex--" />
</TransitionGroup>
<img :src="imgs.logo" alt="" class="logo" @click="showDebug++"
v-if="pIndex == 0 || pIndex == 1 || pIndex == 2"
:style="{ left: pIndex == 0 ? '50%' : '10%', transform: pIndex == 0 ? 'translate3d(-50%, -50%, 0)' : 'translate3d(0, -50%, 0)', transition: '0.3s ease-in-out' }">
</div>
</template>
<style scoped lang="scss">
@use './mixin.scss' as *;
.main {
position: absolute;
inset: 0;
@include full-size;
}
.debug {
position: absolute;
left: .5rem;
bottom: .5rem;
z-index: 99;
width: max-content;
display: flex;
gap: .125rem;
flex-direction: column;
button {
font-size: .6rem;
opacity: .6;
}
}
.logo {
position: absolute;
top: 4%;
z-index: 99;
width: 20%;
}
</style>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,40 @@
@font-face {
font-family: 'LOREAL Essentielle';
src: url('LOREALEssentielle-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'LOREAL Essentielle';
src: url('LOREALEssentielle-BoldItalic.woff2') format('woff2');
font-weight: bold;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'M XiangHe Hei SC Std Book';
src: url('MXiangHeHeiSCStd-Book.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'AR NewcuheiGB';
src: url('ARNewcuheiGB-Bold.woff2') format('woff2');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('subset-HarmonyOS_Sans_SC_Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}

Binary file not shown.

BIN
src/assets/imgs/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src/assets/imgs/bg/A1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
src/assets/imgs/bg/A2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
src/assets/imgs/bg/A3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
src/assets/imgs/bg/A4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
src/assets/imgs/bg/A5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
src/assets/imgs/bg/B1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
src/assets/imgs/bg/B2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
src/assets/imgs/bg/B3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
src/assets/imgs/bg/B4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
src/assets/imgs/bg/B5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
src/assets/imgs/bg/C1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
src/assets/imgs/bg/C2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

BIN
src/assets/imgs/bg/C3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
src/assets/imgs/bg/C4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
src/assets/imgs/bg/C5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
src/assets/imgs/bg/D1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
src/assets/imgs/bg/D2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
src/assets/imgs/bg/D3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
src/assets/imgs/bg/D4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
src/assets/imgs/bg/D5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
src/assets/imgs/bg/E1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
src/assets/imgs/bg/E2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
src/assets/imgs/bg/E3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
src/assets/imgs/bg/E4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
src/assets/imgs/bg/E5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

65
src/assets/imgs/index.ts Normal file
View File

@ -0,0 +1,65 @@
export default {
logo: new URL("@/assets/imgs/logo.png", import.meta.url).href,
slideDown: new URL("@/assets/imgs/slide-down.png", import.meta.url).href,
back: new URL("@/assets/imgs/back.png", import.meta.url).href,
game: {
决断力: new URL("@/assets/imgs/game/决断力.png", import.meta.url).href,
感知力: new URL("@/assets/imgs/game/感知力.png", import.meta.url).href,
渴望力: new URL("@/assets/imgs/game/渴望力.png", import.meta.url).href,
学习敏捷力: new URL("@/assets/imgs/game/学习敏捷力.png", import.meta.url).href,
复原力: new URL("@/assets/imgs/game/复原力.png", import.meta.url).href,
},
icons: {
Ambition: new URL("@/assets/imgs/icons/ambition.png", import.meta.url).href,
Judgement: new URL("@/assets/imgs/icons/judgement.png", import.meta.url).href,
Learning_Agility: new URL("@/assets/imgs/icons/learningAgility.png", import.meta.url).href,
Empathy: new URL("@/assets/imgs/icons/empathy.png", import.meta.url).href,
Resilience: new URL("@/assets/imgs/icons/resilience.png", import.meta.url).href,
},
report: {
Ambition: new URL("@/assets/imgs/report/ambition.webp", import.meta.url).href,
Judgement: new URL("@/assets/imgs/report/judgement.webp", import.meta.url).href,
Learning_Agility: new URL("@/assets/imgs/report/learningAgility.webp", import.meta.url).href,
Empathy: new URL("@/assets/imgs/report/empathy.webp", import.meta.url).href,
Resilience: new URL("@/assets/imgs/report/resilience.webp", import.meta.url).href,
bottomRightShutter: new URL("@/assets/imgs/report/bottom-right-shutter.png", import.meta.url).href,
},
bg: {
Ambition: new URL("@/assets/imgs/bg/ambition.webp", import.meta.url).href,
Judgement: new URL("@/assets/imgs/bg/judgement.webp", import.meta.url).href,
Learning_Agility: new URL("@/assets/imgs/bg/learningAgility.webp", import.meta.url).href,
Empathy: new URL("@/assets/imgs/bg/empathy.webp", import.meta.url).href,
Resilience: new URL("@/assets/imgs/bg/resilience.webp", import.meta.url).href,
A1: new URL("@/assets/imgs/bg/A1.webp", import.meta.url).href,
A2: new URL("@/assets/imgs/bg/A2.webp", import.meta.url).href,
A3: new URL("@/assets/imgs/bg/A3.webp", import.meta.url).href,
A4: new URL("@/assets/imgs/bg/A4.webp", import.meta.url).href,
A5: new URL("@/assets/imgs/bg/A5.webp", import.meta.url).href,
B1: new URL("@/assets/imgs/bg/B1.webp", import.meta.url).href,
B2: new URL("@/assets/imgs/bg/B2.webp", import.meta.url).href,
B3: new URL("@/assets/imgs/bg/B3.webp", import.meta.url).href,
B4: new URL("@/assets/imgs/bg/B4.webp", import.meta.url).href,
B5: new URL("@/assets/imgs/bg/B5.webp", import.meta.url).href,
C1: new URL("@/assets/imgs/bg/C1.webp", import.meta.url).href,
C2: new URL("@/assets/imgs/bg/C2.webp", import.meta.url).href,
C3: new URL("@/assets/imgs/bg/C3.webp", import.meta.url).href,
C4: new URL("@/assets/imgs/bg/C4.webp", import.meta.url).href,
C5: new URL("@/assets/imgs/bg/C5.webp", import.meta.url).href,
D1: new URL("@/assets/imgs/bg/D1.webp", import.meta.url).href,
D2: new URL("@/assets/imgs/bg/D2.webp", import.meta.url).href,
D3: new URL("@/assets/imgs/bg/D3.webp", import.meta.url).href,
D4: new URL("@/assets/imgs/bg/D4.webp", import.meta.url).href,
D5: new URL("@/assets/imgs/bg/D5.webp", import.meta.url).href,
E1: new URL("@/assets/imgs/bg/E1.webp", import.meta.url).href,
E2: new URL("@/assets/imgs/bg/E2.webp", import.meta.url).href,
E3: new URL("@/assets/imgs/bg/E3.webp", import.meta.url).href,
E4: new URL("@/assets/imgs/bg/E4.webp", import.meta.url).href,
E5: new URL("@/assets/imgs/bg/E5.webp", import.meta.url).href,
},
personalInfo: {
male: new URL("@/assets/imgs/personalInfo/male.png", import.meta.url).href,
female: new URL("@/assets/imgs/personalInfo/female.png", import.meta.url).href,
}
};

BIN
src/assets/imgs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,117 @@
// FloatingIcons.vue
<script setup lang="ts">
import { computed } from 'vue';
interface Position {
x: number; // 0-100
y: number; // 0-100
}
interface IconConfig {
src: string; //
size: number; // (px)
blur?: number; //
position?: Position; //
floatRange?: number; // (px)
duration?: number; // ()
}
interface Props {
icons: IconConfig[];
globalBlur?: number; //
floatAmplitude?: number; //
breatheScale?: number; //
}
const props = withDefaults(defineProps<Props>(), {
globalBlur: 4,
floatAmplitude: 10,
breatheScale: 1.05,
});
//
const getRandomDelay = (): number => Math.random() * 5;
const getRandomPosition = (): Position => ({
x: Math.random() * 80 + 10, // 10%-90%
y: Math.random() * 80 + 10
});
//
const processedIcons = computed(() => props.icons.map(icon => ({
...icon,
blur: icon.blur ?? props.globalBlur,
position: icon.position ?? getRandomPosition(),
floatRange: icon.floatRange ?? props.floatAmplitude,
duration: icon.duration ?? 3 + Math.random() * 2, // 3-5
})));
</script>
<template>
<div class="floating-icons-container">
<div v-for="(icon, index) in processedIcons" :key="index"
class="floating-icon" :style="{
width: `${icon.size}px`,
height: `${icon.size}px`,
top: `calc(${icon.position.y}% - ${icon.size / 2}px)`,
left: `calc(${icon.position.x}% - ${icon.size / 2}px)`,
filter: `blur(${icon.blur}px)`,
animationDuration: `${icon.duration}s`,
'--float-range': `${icon.floatRange}px`,
'--breathe-scale': breatheScale,
animationDelay: `-${getRandomDelay()}s`
}">
<img :src="icon.src" :alt="`floating icon ${index}`"
class="icon-image" draggable="false" />
</div>
</div>
</template>
<style scoped>
.floating-icons-container {
width: 100%;
height: 100%;
overflow: hidden;
background: transparent;
}
.floating-icon {
position: absolute;
transform-origin: center;
animation: float var(--animation-duration, 4s) infinite ease-in-out,
breathe var(--animation-duration, 4s) infinite ease-in-out;
}
.icon-image {
width: 100%;
height: 100%;
object-fit: contain;
}
@keyframes float {
0%,
100% {
transform: translateY(0) scale(1);
}
25% {
transform: translateY(calc(var(--float-range) * -1)) scale(var(--breathe-scale));
}
75% {
transform: translateY(var(--float-range)) scale(var(--breathe-scale));
}
}
@keyframes breathe {
0%,
100% {
opacity: 0.7;
}
50% {
opacity: 1;
}
}
</style>

284
src/components/Graph.vue Normal file
View File

@ -0,0 +1,284 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { createLinear2CubicBezier, forEachFrameInMilliseconds } from '../utils';
interface Dimension {
name_en: string;
name_zh: string;
value: number;
color: string;
iconUrl: string;
}
// props
const props = withDefaults(defineProps<{
dimensions: Dimension[];
title?: string;
subtitle?: string;
innerRadius?: number;
labelRadius?: number;
outerRadius?: number;
gap?: number;
iconSize?: number;
scoreRadio?: number;
usePercentages?: boolean;
iconsFilter?: string;
enableAnimation?: boolean;
}>(), {
innerRadius: 80,
outerRadius: 100,
labelRadius: 101,
gap: 6,
iconSize: 40,
scoreRadio: 1,
usePercentages: true,
enableAnimation: true,
});
const partAngle = computed(() => {
const total = props.dimensions.reduce((acc, cur) => acc + cur.value, 0);
return props.dimensions.map(dim => dim.value / total * 360)
})
const finalPartRange = computed(() => {
let startAngle = 0;
return partAngle.value.map(angle => {
const range = [startAngle, startAngle + angle];
startAngle += angle;
return range;
});
})
const animationCurrentDuration = ref(0)
const opacityDuration = ref(0)
const timingFunction = createLinear2CubicBezier()
onMounted(() => {
if (!props.enableAnimation) {
animationCurrentDuration.value = 1
opacityDuration.value = 1
return
}
forEachFrameInMilliseconds((elapsedTime: number, totalTime: number) => {
animationCurrentDuration.value = timingFunction(elapsedTime / totalTime);
}, 1000)
setTimeout(() => {
forEachFrameInMilliseconds((elapsedTime: number, totalTime: number) => {
opacityDuration.value = timingFunction(elapsedTime / totalTime);
}, 500)
}, 780);
})
const partRange = computed(() => finalPartRange.value.map(range => range.map(angle => angle * animationCurrentDuration.value)))
const total = computed(() => props.dimensions.reduce((acc, cur) => acc + cur.value, 0))
const labelPos = computed(() => {
return finalPartRange.value.map((range, index) => {
return ({
dim: props.dimensions[index],
top: getLabelPosition((range[0] + range[1]) / 2 * animationCurrentDuration.value, props.outerRadius * 1.0 * props.labelRadius / 100).y / 4,
left: getLabelPosition((range[0] + range[1]) / 2 * animationCurrentDuration.value, props.outerRadius * 1.1 * props.labelRadius / 100).x / 4,
finalTop: getLabelPosition((range[0] + range[1]) / 2, props.outerRadius * 1.0 * props.labelRadius / 100).y / 4,
finalLeft: getLabelPosition((range[0] + range[1]) / 2, props.outerRadius * 1.1 * props.labelRadius / 100).x / 4
})
})
})
//
const polarToCartesian = (centerX: number, centerY: number, radius: number, angleInDegrees: number) => {
const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
};
//
const calculateSegmentPath = (startAngle: number, endAngle: number, innerRadius: number, outerRadius: number): string => {
const innerStart = polarToCartesian(200, 200, innerRadius, startAngle);
const innerEnd = polarToCartesian(200, 200, innerRadius, endAngle);
const outerStart = polarToCartesian(200, 200, outerRadius, startAngle);
const outerEnd = polarToCartesian(200, 200, outerRadius, endAngle);
const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
return [
'M', outerStart.x, outerStart.y, //
'A', outerRadius, outerRadius, 0, largeArcFlag, 1, outerEnd.x, outerEnd.y, //
'L', innerEnd.x, innerEnd.y, // 线
'A', innerRadius, innerRadius, 0, largeArcFlag, 0, innerStart.x, innerStart.y, //
'L', outerStart.x, outerStart.y, //
'Z' //
].join(' ');
};
//
const getLabelPosition = (angle: number, radius: number) => {
return polarToCartesian(200, 200, radius + 40, angle);
};
</script>
<template>
<div class="container">
<svg viewBox="0 0 400 400" class="">
<!-- 中心标题 -->
<!-- 绘制扇形和标签 -->
<g v-for="(dim, index) in dimensions" :key="dim.name_zh">
<!-- 扇形区域 -->
<path :d="calculateSegmentPath(
partRange[index][0] + gap * animationCurrentDuration / 2,
partRange[index][1] - gap * animationCurrentDuration / 2,
innerRadius,
outerRadius
)" :fill="dim.color" />
</g>
</svg>
<div class="labels">
<div class="label" v-for="pos in labelPos" :key="pos.dim.name_en"
:style="{
top: `${pos.top}%`,
left: `calc(${pos.left}% - .25em)`,
opacity: opacityDuration,
}" :class="{
right: pos.finalLeft > 50,
top: pos.finalTop < 50,
}">
<div class="label-value">
<img :src="pos.dim.iconUrl" :alt="pos.dim.name_zh"
:style="{ filter: iconsFilter }" />
<span class="value">
{{ usePercentages
? Math.round(pos.dim.value / total * 100)
: pos.dim.value * scoreRadio }}
</span>
<span class="percent" v-if="usePercentages">%</span>
</div>
<div class="label-name" :lang="$i18n.locale">
<span class="zh">{{ pos.dim.name_zh }}</span>
<span class="en">{{ pos.dim.name_en }}</span>
</div>
</div>
<div class="center-title label">
<div>{{ title }}</div>
<div>{{ subtitle }}</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.container {
svg {
position: relative;
display: block;
}
.labels {
position: absolute;
inset: 0;
height: 100%;
width: 100%;
.center-title {
top: 50%;
left: 50%;
text-align: center;
font-size: .68em;
text-transform: uppercase;
}
.label {
position: absolute;
transform: translate3d(-50%, -50%, 0);
/* width: 7em; */
width: max-content;
display: flex;
align-items: flex-end;
flex-direction: column;
&.center-title {
align-items: center;
}
&.top {
flex-direction: column-reverse;
.label-name {
flex-direction: column-reverse;
}
}
.label-value {
line-height: 1.0;
img {
display: inline-block;
width: 2.6em;
height: 2.25em;
object-fit: cover;
vertical-align: top;
}
span.value {
font-size: 2.1em;
vertical-align: bottom;
margin-inline-end: .05em;
vertical-align: top;
}
span.percent {
font-size: 1em;
position: relative;
top: .15em;
}
}
.label-name {
/* padding: 0 8px; */
align-self: flex-end;
position: relative;
right: .125em;
&:lang(en) {
.zh {
display: none;
}
.en {
font-size: .9em;
}
}
.en {
font-size: .7em;
}
.zh {
font-size: .8em;
}
display: flex;
flex-direction: column;
align-items: flex-end;
}
}
}
}
.text-sm {
font-size: .5em;
}
.font-bold {
font-weight: bold;
}
</style>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
interface Position {
x: number;
y: number;
}
interface Shape {
color: string;
size: number;
aspectRatio?: number;
position: Position;
delay: number;
blurAmount?: number;
}
interface Props {
blurAmount: number;
animationDuration: number;
shapes: Shape[];
background: string;
screenShadow?: {
top: number;
left: number;
bottom: number;
right: number;
color: string;
blur: number;
};
filter?: string;
}
defineProps<Props>()
</script>
<template>
<div class="blur-background" :style="{ background, filter }">
<div class="screen-shadow" v-if="screenShadow" :style="{
position: 'absolute',
inset: '0',
zIndex: 9,
height: '100%',
width: '100%',
borderTop: `${screenShadow.top}px solid ${screenShadow.color}`,
borderLeft: `${screenShadow.left}px solid ${screenShadow.color}`,
borderBottom: `${screenShadow.bottom}px solid ${screenShadow.color}`,
borderRight: `${screenShadow.right}px solid ${screenShadow.color}`,
filter: `blur(${screenShadow.blur}px)`,
}">
</div>
<div v-for="(shape, index) in shapes" :key="index" class="blur-shape"
:style="{
width: `${shape.size}px`,
height: `${shape.size / (shape.aspectRatio ? shape.aspectRatio : 1)}px`,
background: shape.color,
top: `calc(${shape.position.y}% - ${shape.size / 2}px)`,
left: `calc(${shape.position.x}% - ${shape.size / 2}px)`,
filter: `blur(${shape.blurAmount ? shape.blurAmount : blurAmount}px)`,
animationDuration: `${animationDuration}s`,
animationDelay: `${shape.delay}s`,
}" />
</div>
</template>
<style scoped>
.blur-background {
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
.blur-shape {
position: absolute;
border-radius: 50%;
transition: .8s ease-in-out;
animation: breathe var(--animation-duration, 4s) infinite ease-in-out;
}
@keyframes breathe {
0%,
100% {
transform: scale(1);
opacity: .7;
}
50% {
transform: scale(1.2);
opacity: .9;
}
}
</style>

161
src/components/Tags.vue Normal file
View File

@ -0,0 +1,161 @@
<script setup lang="ts">
import { DimensionColors, findTraitDimension, TraitName, } from '../config';
import { useI18n } from 'vue-i18n'
import type { MessageSchema } from '../locates'
const { t } = useI18n<{ message: MessageSchema }>({ useScope: 'global' })
defineProps<{
topFiveTraits: [TraitName, number][]
rarity: number,
borderColor: string,
username: string,
}>()
</script>
<template>
<div class="tags" :style="{ '--border-color': borderColor }">
<span class="label" :lang="$i18n.locale">
<span class="pre"></span>
<span class="content" :style="{ color: borderColor }">
{{ username }}{{ t('result.tagLabel') }}</span>
<span class="after"></span>
</span>
<!-- <span class="only-for-you">{{ t('result.onlyForYou') }}</span> -->
<div class="traits">
<div class="trait" :lang="$i18n.locale" v-for="trt in topFiveTraits"
:style="{
backgroundColor: DimensionColors[findTraitDimension(trt[0])!]
}">{{
t(`questionData.trait.${trt[0]}`) }}</div>
</div>
<span class="footnote" :lang="$i18n.locale">
<span class="text">{{ t('result.footnote') }}</span>
<span class="rarity" :style="{ color: borderColor }">
{{ rarity }}%
</span>
</span>
<span class="footnote2" :lang="$i18n.locale">
{{ t('result.footnote2') }} {{ 100 - rarity }}%
</span>
</div>
</template>
<style lang="scss" scoped>
@use '../mixin.scss' as *;
.footnote {
position: absolute;
right: 0;
width: fit-content;
display: flex;
align-items: flex-start;
gap: .35rem;
&:lang(en) {}
.text {
position: relative;
top: .15rem;
font-size: .8rem;
white-space: pre;
text-transform: uppercase;
text-align: end;
line-height: 1;
}
.rarity {
font-size: 2.1rem;
line-height: 1;
position: relative;
top: -.2rem;
}
}
.footnote2 {
position: absolute;
top: calc(100% + .25rem);
right: 0;
font-size: .6rem;
&:lang(en) {
font-size: .55rem;
}
}
.tags {
position: relative;
border-left: .8px solid var(--border-color);
border-right: .8px solid var(--border-color);
border-bottom: .8px solid var(--border-color);
padding-bottom: 2rem;
.label {
position: absolute;
top: 0%;
left: 50%;
width: max-content;
transform: translate(-50%, -50%);
@include font(.9rem, 1, normal, center);
width: 100%;
display: flex;
gap: .5rem;
&:lang(en) {
.content {
font-size: 0.9rem;
}
}
span {
height: 0;
display: block;
}
.content {
line-height: 1rem;
font-size: 1.05rem;
transform: translateY(-.5lh);
}
.pre,
.after {
flex: 1;
border-top: .8px solid var(--border-color);
border-left: .8px solid var(--border-color);
border-right: .8px solid var(--border-color);
}
}
.only-for-you {
position: absolute;
top: .125rem;
right: .125rem;
color: #ffffffEE;
@include font(.6rem, 1.2, );
}
}
.traits {
padding: 1rem .75rem;
line-height: 1.5;
min-height: 4rem;
}
.trait {
height: 1.05rem;
padding: 0 .5rem;
margin-right: .5rem;
border-radius: .7rem;
vertical-align: middle;
@include font(.75rem, 1, normal, center);
&:lang(en) {
@include font(.72rem, 1, normal, center);
}
display: inline-flex;
align-items: center;
justify-content: center;
}
</style>

298
src/config.ts Normal file
View File

@ -0,0 +1,298 @@
const ThemeColor = {
'yellow': '#F6C532',
'pink': '#FF796F',
'purple': '#B285B7',
'blue': '#066CF7',
'green': '#00D78D'
}
interface AssessmentItem {
id: string;
dimension: DimensionName;
trait: TraitName;
scores: number[];
}
const DimensionNames = ["Learning_Agility", "Ambition", "Empathy", "Judgement", "Resilience"] as const;
type DimensionName = (typeof DimensionNames)[number];
const DimensionColors: Record<DimensionName, string> = {
"Learning_Agility": ThemeColor.yellow,
"Ambition": ThemeColor.pink,
"Empathy": ThemeColor.purple,
"Judgement": ThemeColor.blue,
"Resilience": ThemeColor.green,
}
const GamePageDimensionColors: Record<DimensionName, string> = {
"Learning_Agility": '#F7C632',
"Resilience": '#00D88E',
"Empathy": '#B386B8',
"Ambition": '#FF796F',
"Judgement": '#066CF8'
}
const Traits = [
// Learning_Agility
"Pioneer",
"Knowledge_Seeker",
"Change_Pioneer",
"Curious_Mind",
"Trial_Warrior",
// Resilience
"Giants_Shoulders",
"Stress_Master",
"Roly-Poly",
"Steady_Anchor",
"Planner",
// Empathy
"Communication_Expert",
"Sincere_Partner",
"Mind_Hunter",
"Active_Listener",
"Emotion_Management_Master",
// Ambition
"Future_Prophet",
"Helmsman",
"Owner",
"Strategic_Leader",
"Perfectionist",
// Judgement
"Simplifier",
"Problem_Sniper",
"Eagle_Eye",
"Thought_Challenger",
"Rational_and_Intuitive"
] as const;
type TraitName = (typeof Traits)[number];
interface AssessmentDimension {
name: DimensionName;
items: AssessmentItem[];
}
const PersonalityAssessment: AssessmentDimension[] = [
{
name: "Learning_Agility",
items: [
{
id: "A1",
dimension: "Learning_Agility",
trait: "Pioneer",
scores: [1, 2, 3, 4, 5]
},
{
id: "A2",
dimension: "Learning_Agility",
trait: "Knowledge_Seeker",
scores: [1, 2, 3, 4, 5]
},
{
id: "A3",
dimension: "Learning_Agility",
trait: "Change_Pioneer",
scores: [1, 2, 3, 4, 5]
},
{
id: "A4",
dimension: "Learning_Agility",
trait: "Curious_Mind",
scores: [1, 2, 3, 4, 5]
},
{
id: "A5",
dimension: "Learning_Agility",
trait: "Trial_Warrior",
scores: [1, 2, 3, 4, 5]
}
]
},
{
name: "Resilience",
items: [
{
id: "B1",
dimension: "Resilience",
trait: "Giants_Shoulders",
scores: [1, 2, 3, 4, 5]
},
{
id: "B2",
dimension: "Resilience",
trait: "Stress_Master",
scores: [1, 2, 3, 4, 5]
},
{
id: "B3",
dimension: "Resilience",
trait: "Roly-Poly",
scores: [1, 2, 3, 4, 5]
},
{
id: "B4",
dimension: "Resilience",
trait: "Steady_Anchor",
scores: [5, 4, 3, 2, 1]
},
{
id: "B5",
dimension: "Resilience",
trait: "Planner",
scores: [1, 2, 3, 4, 5]
}
]
},
{
name: "Empathy",
items: [
{
id: "C1",
dimension: "Empathy",
trait: "Communication_Expert",
scores: [1, 2, 3, 4, 5]
},
{
id: "C2",
dimension: "Empathy",
trait: "Sincere_Partner",
scores: [1, 2, 3, 4, 5]
},
{
id: "C3",
dimension: "Empathy",
trait: "Mind_Hunter",
scores: [1, 2, 3, 4, 5]
},
{
id: "C4",
dimension: "Empathy",
trait: "Active_Listener",
scores: [5, 4, 3, 2, 1]
},
{
id: "C5",
dimension: "Empathy",
trait: "Emotion_Management_Master",
scores: [1, 2, 3, 4, 5]
}
]
},
{
name: "Ambition",
items: [
{
id: "D1",
dimension: "Ambition",
trait: "Future_Prophet",
scores: [1, 2, 3, 4, 5]
},
{
id: "D2",
dimension: "Ambition",
trait: "Helmsman",
scores: [1, 2, 3, 4, 5]
},
{
id: "D3",
dimension: "Ambition",
trait: "Owner",
scores: [1, 2, 3, 4, 5]
},
{
id: "D4",
dimension: "Ambition",
trait: "Strategic_Leader",
scores: [1, 2, 3, 4, 5]
},
{
id: "D5",
dimension: "Ambition",
trait: "Perfectionist",
scores: [5, 4, 3, 2, 1]
}
]
},
{
name: "Judgement",
items: [
{
id: "E1",
dimension: "Judgement",
trait: "Simplifier",
scores: [1, 2, 3, 4, 5]
},
{
id: "E2",
dimension: "Judgement",
trait: "Problem_Sniper",
scores: [5, 4, 3, 2, 1]
},
{
id: "E3",
dimension: "Judgement",
trait: "Eagle_Eye",
scores: [1, 2, 3, 4, 5]
},
{
id: "E4",
dimension: "Judgement",
trait: "Thought_Challenger",
scores: [5, 4, 3, 2, 1]
},
{
id: "E5",
dimension: "Judgement",
trait: "Rational_and_Intuitive",
scores: [1, 2, 3, 4, 5]
}
]
}
];
function shuffle(array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getPersonalityAssessment(): AssessmentItem[] {
return shuffle(Object.values(PersonalityAssessment).map(dimension => dimension.items).flat());
}
function getDimensionQuestionLength(dimension: DimensionName): number {
return Object.values(PersonalityAssessment).find(d => d.name === dimension)?.items.length || 0;
}
function getRandomDimensionRating(): Record<DimensionName, number> {
const random = (start: number, end: number) => Math.floor(Math.random() * (end - start + 1) + start);
return DimensionNames.reduce((acc, q) => ({
...acc,
[q]: random(getDimensionQuestionLength(q) * 1, getDimensionQuestionLength(q) * 5)
}), {} as Record<DimensionName, number>)
}
function getRandomTraitRating(): Record<TraitName, number> {
const random = (start: number, end: number) => Math.floor(Math.random() * (end - start + 1) + start);
return Traits.reduce((acc, q) => ({
...acc,
[q]: random(1, 5)
}), {} as Record<TraitName, number>)
}
function findTraitDimension(trait: TraitName): DimensionName | undefined {
return Object.values(PersonalityAssessment).find(d => d.items.some(i => i.trait === trait))?.name;
}
function findAllTraitByDimension(dimension: DimensionName): TraitName[] {
return Object.values(PersonalityAssessment).find(d => d.name === dimension)?.items.map(i => i.trait) || [];
}
export {
ThemeColor, PersonalityAssessment, GamePageDimensionColors,
getPersonalityAssessment, findTraitDimension, findAllTraitByDimension, getRandomTraitRating,
DimensionNames, DimensionColors, getDimensionQuestionLength, getRandomDimensionRating
}
export type { DimensionName, TraitName }

861
src/locates/index.ts Normal file
View File

@ -0,0 +1,861 @@
/**
* Date: 2024/12/19
*
* Note: 多行文本可以使用 `` 使 dedent
* Note: 为了此文件的可读性 \
* Note: 较短的多行文本可以直接使用引号 \n
*
* Note: 必须保证中英文键名对应 Typescript
*
* Note: 关于上传和编译
* Note: 需要先使用上传将编辑好的文件上传至服务器
* Note: 本编辑器没有自动保存功能
*/
// @ts-ignore
import dedent from "dedent";
interface Dedent {
(literals: string): string;
(strings: TemplateStringsArray, ...values: unknown[]): string;
}
declare const dedent: Dedent;
const zh_CN = {
switchLang: new URL("@/assets/imgs/switch-lang_zh.png", import.meta.url).href,
you: "你",
home: {
title: new URL("@/assets/imgs/home/title_zh.png", import.meta.url).href,
button: new URL("@/assets/imgs/home/button_zh.png", import.meta.url).href,
intro: new URL("@/assets/imgs/home/intro_zh.png", import.meta.url).href,
},
game: {
agree: "非常准确",
disagree: "非常不准确",
},
questionData: {
DimensionName: {
"Learning_Agility": "学习敏捷力",
"Resilience": "复原力",
"Empathy": "感知力",
"Ambition": "渴望力",
"Judgement": "决断力"
},
description: {
"A1": "在工作中,我总会主动寻找新的项目和挑战,哪怕意味着要学习新的技能。",
"A2": "工作完成后,我总会主动寻求同事和上级的反馈,并认真思考如何改进。",
"A3": "面对失败的项目或任务,我倾向于将其视为学习和成长的机会",
"A4": "我总是充满好奇心,会主动寻找和利用各种学习资源。",
"A5": "在实践中,我倾向于使用熟悉的技能和稳妥的方法来解决问题。",
"B1": "我总是勇于承担挫折和失败的责任。",
"B2": "遇到困难时,我很容易感到气馁,并且需要很长时间才能恢复过来。",
"B3": "哪怕经历项目失败或重大挫折,我仍会充满信心,并坚信自己能够克服困难。",
"B4": "当遇到挑战或挫折时,我很容易感到焦虑、沮丧,并且难以恢复。",
"B5": "我懂得接受无法避免的失败,并俯瞰全局,尝试走好下一步棋。",
"C1": "我擅长与他人建立良好关系,并积极促进团队合作,如组织团队活动、解决冲突等。",
"C2": "当同事完成一项出色工作时,我总会及时给予肯定和赞扬,并表达我的欣赏。",
"C3": "我善于分析市场数据、用户反馈等,并能准确洞察消费者需求。",
"C4": "我倾向于将注意力集中在自己,而非他人的情绪和需求上,这能帮助我保持客观与专注。",
"C5": "我会克制并有效引导自己的负面情绪,避免将其传递给他人。",
"D1": "我关注行业发展趋势,并能将其转化为创新的想法和项目。",
"D2": "我有明确的职业目标,并喜欢设定有挑战性的目标来推动自己不断进步。",
"D3": "我具备极强的主观能动性,以主人翁精神引领合作,推进想法与项目。",
"D4": "我喜欢成为引领变革的人,设定突破性目标,并坚持不懈地努力实现。",
"D5": "相比于不断改进,我更容易安于现状,我认为每时每刻的精益求精会导致效率低下。",
"E1": "在处理复杂任务时,我能够快速识别关键信息,并简化流程以提高效率。",
"E2": "面对问题时,我更关注如何解决问题,而不是寻找问题出现的根本原因。",
"E3": "我能够有效地安排工作优先级,并优先处理重要且紧急的任务。",
"E4": "在团队讨论中,我很少反驳他人的观点,即使我有不一样的想法或顾虑。",
"E5": "我能平衡直觉和理性,兼顾长短期利益及重要细节,并高效做出关键决策。"
},
trait: {
Pioneer: "开拓者",
"Knowledge_Seeker": "求知者",
"Change_Pioneer": "变革先锋",
"Curious_Mind": "好奇宝宝",
"Trial_Warrior": "试错勇士",
"Giants_Shoulders": "巨人的肩膀",
"Stress_Master": "抗压达人",
"Roly-Poly": "不倒翁",
"Steady_Anchor": "定海神针",
"Planner": "规划者",
"Communication_Expert": "沟通高手",
"Sincere_Partner": "诚以待人",
"Mind_Hunter": "心灵捕手",
"Active_Listener": "倾听者",
"Emotion_Management_Master": "情绪管理大师",
"Future_Prophet": "未来预言家",
"Helmsman": "掌舵人",
"Owner": "主人翁",
"Strategic_Leader": "战略领袖",
"Perfectionist": "完美主义者",
"Simplifier": "化繁为简",
"Problem_Sniper": "问题狙击手",
"Eagle_Eye": "鹰眼",
"Thought_Challenger": "思想碰撞者",
"Rational_and_Intuitive": "理性与感性兼备"
}
},
personalInfo: {
title: '感谢你的耐心作答\n接下来最后一步',
placeholder: {
username: "请输入你的昵称",
age: "请输入你的年龄",
gender: "请选择你的性别",
},
gender: {
male: "男性",
female: "女性",
},
error: {
username: "请输入你的昵称",
age: "请输入你的年龄",
gender: "请选择你的性别",
},
submit: new URL("@/assets/imgs/personalInfo/submit_zh.png", import.meta.url).href,
},
result: {
title: "你的 5 大潜能评分",
subtitle: "此报告旨在表现你各维度潜能的分布比例\n助你了解核心优势与短板",
tagLabel: "已获得如下潜能标签:",
footnote: "标签组合稀有度*",
footnote2: "*潜能标签组合出现的几率不超过",
rare: {
"SSR": "SSR",
"UR": "UR",
},
onlyForYou: "*仅供参考",
slideDown: "下滑查看欧莱雅 5 大潜能定义\n并获取你的最终报告",
slideUp: new URL("@/assets/imgs/result/slide-up-notice_zh.png", import.meta.url).href,
ratingExplanation: new URL("@/assets/imgs/result/rating-explanation_zh.png", import.meta.url).href,
actions: {
tryAgain: "再测一次",
generateReport: "生成个人报告",
}
},
report: {
title: "的核心潜能是",
dimDescription: {
Learning_Agility: dedent`
\
\
\
\
\
`,
Resilience: dedent`
\
使
\
\
`,
Empathy: dedent`
\
使\
\
`,
Ambition: dedent`
怀\
\
\
`,
Judgement: dedent`
\
\
`
},
top: new URL("@/assets/imgs/report/top.png", import.meta.url).href,
bottom: new URL("@/assets/imgs/report/bottom_zh.png", import.meta.url).href,
bottomLeft: new URL("@/assets/imgs/report/bottom-left_zh.png", import.meta.url).href,
bottomRight: new URL("@/assets/imgs/report/bottom-right_zh.png", import.meta.url).href,
shareMask: new URL("@/assets/imgs/report/share-mask_zh.png", import.meta.url).href,
}
}
const en: typeof zh_CN = {
switchLang: new URL("@/assets/imgs/switch-lang_en.png", import.meta.url).href,
you: "You",
home: {
title: new URL("@/assets/imgs/home/title_en.png", import.meta.url).href,
button: new URL("@/assets/imgs/home/button_en.png", import.meta.url).href,
intro: new URL("@/assets/imgs/home/intro_en.png", import.meta.url).href,
},
game: {
agree: "Agree",
disagree: "Disagree",
},
questionData: {
DimensionName: {
"Learning_Agility": "Learning Agility",
"Resilience": "Resilience",
"Empathy": "Empathy",
"Ambition": "Ambition",
"Judgement": "Judgement"
},
description: {
"A1": "In my work, I always take the initiative to seek out new projects and challenges, even if it requires learning new skills.",
"A2": "After completing a task, I always take the initiative to seek feedback from colleagues and leaders, and I reflect on areas for improvement.",
"A3": "When faced with a failed project or task, I tend to see it as an opportunity for learning and growth.",
"A4": "I am always full of curiosity and actively seek out and utilize various learning resources.",
"A5": "In practice, I tend to use familiar skills and proven methods to solve problems.",
"B1": "I am always willing to take responsibility for setbacks and failures.",
"B2": "When faced with difficulties, I easily feel discouraged and take a long time to recover.",
"B3": "Even after experiencing project failures or major setbacks, I remain confident in my ability to overcome.",
"B4": "When faced with challenges or setbacks, I easily feel anxious and frustrated, and I find it difficult to recover.",
"B5": "I understand how to accept unavoidable failures, keep a broader perspective, and try to make the next move wisely.",
"C1": "I am good at building positive relationships with others and actively promoting teamwork, such as organizing team activities and resolving conflicts.",
"C2": "When a colleague does excellent work, I always provide timely recognition and praise, expressing my appreciation.",
"C3": "I am skilled at analyzing market data and user feedback, and I can accurately identify consumer needs.",
"C4": "I tend to focus my attention on myself rather than on the emotions and needs of others, which helps me stay objective and focused.",
"C5": "I can control and effectively manage my negative emotions, avoiding passing them on to others.",
"D1": "I pay attention to industry development trends and can transform them into innovative ideas and projects.",
"D2": "I have clear career goals and enjoy setting challenging objectives to drive my continuous improvement.",
"D3": "I possess strong initiative and lead collaboration with a sense of ownership, advancing ideas and projects.",
"D4": "I enjoy being a change leader, setting groundbreaking goals, and persistently working to achieve them.",
"D5": "Compared to continuous improvement, I find it easier to settle for the status quo, as I believe striving for perfection at every moment can lead to inefficiency.",
"E1": "When handling complex tasks, I can quickly identify key information and streamline processes to improve efficiency.",
"E2": "When faced with problems, I focus more on how to solve the issue rather than seeking the root cause of its occurrence.",
"E3": "I can effectively prioritize work and focus on important and urgent tasks first.",
"E4": "In team discussions, I rarely contradict others' viewpoints, even if I have different ideas or concerns.",
"E5": "I can balance intuition and rationality, consider both short-term and long-term interests along with important details, and make key decisions efficiently."
},
trait: {
Pioneer: "Explorer",
"Knowledge_Seeker": "Learner",
"Change_Pioneer": "Champion of Change",
"Curious_Mind": "Curious Soul",
"Trial_Warrior": "Trial-and-Error Warrior",
"Giants_Shoulders": "Shoulders of Giants",
"Stress_Master": "Resilience Rockstar",
"Roly-Poly": "Bounder-Backer",
"Steady_Anchor": "Steady Hand",
"Planner": "Strategist",
"Communication_Expert": "Communication Expert",
"Sincere_Partner": "Sincere Individual",
"Mind_Hunter": "Astute Observer",
"Active_Listener": "Listener",
"Emotion_Management_Master": "Emotion Management Master",
"Future_Prophet": "Visionary",
"Helmsman": "Helmsman",
"Owner": "Go-Getter",
"Strategic_Leader": "Strategic Leader",
"Perfectionist": "Perfectionist",
"Simplifier": "Simplifier",
"Problem_Sniper": "Troubleshooter",
"Eagle_Eye": "Prioritizer",
"Thought_Challenger": "Brainstormer",
"Rational_and_Intuitive": "Integrated Thinker"
}
},
personalInfo: {
title: 'Thank you for your patience in answering.\nThis is the final step.',
placeholder: {
username: "Your nickname is",
age: "Your age is",
gender: "Select your gender",
},
gender: {
male: "Male",
female: "Female",
},
error: {
username: "Please enter your nickname",
age: "Please enter your age",
gender: "Please select your gender",
},
submit: new URL("@/assets/imgs/personalInfo/submit_en.png", import.meta.url).href,
},
result: {
title: "Your 5 DIMENSIONS\nof Potential<sup>©</sup> Rating",
subtitle: "This report aims to illustrate the distribution of your potential across various dimensions, helping you understand your core strengths and weaknesses.",
tagLabel: " have obtained the following pillar tags:",
footnote: "Tag combination\nrarity*",
footnote2: "*The probability of the combination of potential labels appearing is less than",
rare: {
"SSR": "SSR",
"UR": "UR",
},
onlyForYou: "*For reference only",
slideDown: dedent`
<span style='font-size: 1.3rem'>Scroll down</span>
<div style='font-size: .65rem;line-height: 1.2;'>to view official description about
The 5 L'ORÉAL DIMENSIONS of Potential and obtain your final report</div>
`,
slideUp: new URL("@/assets/imgs/result/slide-up-notice_en.png", import.meta.url).href,
ratingExplanation: new URL("@/assets/imgs/result/rating-explanation_en.png", import.meta.url).href,
actions: {
tryAgain: "Test again",
generateReport: "Generate report",
}
},
report: {
title: "'s core pillar is",
top: new URL("@/assets/imgs/report/top.png", import.meta.url).href,
bottom: new URL("@/assets/imgs/report/bottom_en.png", import.meta.url).href,
bottomLeft: new URL("@/assets/imgs/report/bottom-left_en.png", import.meta.url).href,
bottomRight: new URL("@/assets/imgs/report/bottom-right_en.png", import.meta.url).href,
dimDescription: {
Learning_Agility: dedent`
You are a natural explorer, \
and you are curious about the world and eager to explore the unknown, \
constantly trying new approaches.
You are a learner and change agent, adapting quickly and learning from experience. \
You share your knowledge and foster a positive learning environment.
`,
Resilience: dedent`
You are resilient and strong, and you maintain optimism even in setbacks.
You recover quickly from challenges, learn from experience, \
and find the best solutions. Your positive attitude, \
responsibility, and ability to motivate make you a trusted partner.
`,
Empathy: dedent`
You are a gifted communicator with strong empathy and insight, \
and you understand others deeply.
Your sincerity and listening skills build strong relationships, \
enabling you to identify consumer needs and create win-win solutions. \
You believe success lies in understanding and respecting everyone.
`,
Ambition: dedent`
You are a visionary strategic leader, embodying the spirit of "freedom to go beyond."
You have grand ambitions and pursue them relentlessly. \
Never complacent, you challenge yourself, push boundaries, \
and explore new possibilities, leading your team like a skilled captain. \
Your strong ownership creates infinite potential.
`,
Judgement: dedent`
You are a decisive leader with both intuition and analytical skills, \
and you excel at navigating complex situations and making informed decisions.
Besides, you are a skilled problem-solver, \
simplifying complexity, thinking strategically, \
and guiding your team to success.
`
},
shareMask: new URL("@/assets/imgs/report/share-mask_en.png", import.meta.url).href,
}
}
const zh_TW : typeof zh_CN = {
// 參照 zh 物件補全,並將圖片路徑改為 zhtw
switchLang: new URL("@/assets/imgs/switch-lang_zhtw.png", import.meta.url).href,
you: "你",
home: {
title: new URL("@/assets/imgs/home/title_zhtw.png", import.meta.url).href,
button: new URL("@/assets/imgs/home/button_zhtw.png", import.meta.url).href,
intro: new URL("@/assets/imgs/home/intro_zhtw.png", import.meta.url).href,
},
// 以下為您提供的譯文,已修正格式
game: {
agree: "非常準確",
disagree: "非常不準確",
},
questionData: {
DimensionName: {
"Learning_Agility": "學習敏捷度",
"Resilience": "韌性",
"Empathy": "同理心",
"Ambition": "企圖心",
"Judgement": "判斷力"
},
description: {
"A1": "我會努力探索自己的極限,勇於跨出舒適圈嘗試新事物和體驗。",
"A2": "我主動尋求並傾聽回饋,不斷提升自我。",
"A3": "我視危機為轉機,並視失敗為學習的良機。",
"A4": "我充滿好奇心,會主動尋找和利用各種學習資源。",
"A5": "我勇於在實踐中學習,即使不保證成功也會盡力嘗試新方法。",
"B1": "我勇於承擔挫折和失敗的責任。",
"B2": "我善於管理壓力,能迅速走出挫折,保持積極樂觀。",
"B3": "我遇到困難不輕易放棄,追尋並享受挑戰帶來的刺激與鼓勵。",
"B4": "在情勢不明朗時,我能保持冷靜與沉著,帶領成功到來。",
"B5": "我懂得接受無法避免的失敗,俯瞰全局,並嘗試規畫下一步。",
"C1": "我善於與他人建立關係,支持並推動團隊之間的合作。",
"C2": "我總以真誠待人,直接表達尊重和欣賞。",
"C3": "我能夠理解消費者行為背後的動機和心理,並將其應用到市場行銷策略中。",
"C4": "我待人友善,總能保持體貼,並懂得積極傾聽以瞭解他人的想法和感受。",
"C5": "我會克制並有效引導自己的負面情緒,避免將其傳遞給他人。",
"D1": "我目光長遠,善於發現新趨勢並分享具有創意的洞見與願景。",
"D2": "我具有企圖心且有抱負,會給自己設定極具挑戰的目標。",
"D3": "我具備強烈的主動性,以主人翁精神引領合作,推進想法與專案。",
"D4": "以卓越的企圖心引領變革,設定與利害相關者目標相符的突破性目標,並始終堅持誠信正直。",
"D5": "我追求卓越,不滿足於現狀,總是尋求改進的機會與突破的可能。",
"E1": "我時刻關注效率,善於簡化過程,突出重點。",
"E2": "我善於發現問題的根源,不斷從複雜的困境抽絲剝繭,尋求解決方案。",
"E3": "我總能區分輕重緩急,集中精力以解決關鍵挑戰。",
"E4": "勇於以建設性的方式挑戰並改善他人的想法。",
"E5": "全面思考,兼顧長短期利益及重要細節,並有效結合分析和直覺,高效做出關鍵決策。"
},
trait: {
Pioneer: "開拓者",
"Knowledge_Seeker": "求知者",
"Change_Pioneer": "變革先鋒",
"Curious_Mind": "好奇寶寶",
"Trial_Warrior": "試錯勇士",
"Giants_Shoulders": "巨人的肩膀",
"Stress_Master": "抗壓達人",
"Roly-Poly": "不倒翁",
"Steady_Anchor": "定海神針",
"Planner": "規劃者",
"Communication_Expert": "溝通高手",
"Sincere_Partner": "誠以待人",
"Mind_Hunter": "心靈捕手",
"Active_Listener": "傾聽者",
"Emotion_Management_Master": "情緒管理大師",
"Future_Prophet": "未來預言家",
"Helmsman": "掌舵人",
"Owner": "主人翁",
"Strategic_Leader": "戰略領袖",
"Perfectionist": "完美主義者",
"Simplifier": "化繁為簡",
"Problem_Sniper": "問題狙擊手",
"Eagle_Eye": "鷹眼",
"Thought_Challenger": "思想碰撞者",
"Rational_and_Intuitive": "理性與感性兼備"
}
},
personalInfo: {
title: '感謝你的耐心作答\n接下來最後一步',
placeholder: {
username: "請輸入你的暱稱",
age: "請輸入你的年齡",
gender: "請選擇你的性別",
},
gender: {
male: "男性",
female: "女性",
},
error: {
username: "請輸入你的暱稱",
age: "請輸入你的年齡",
gender: "請選擇你的性別",
},
// 參照 zh 物件補全
submit: new URL("@/assets/imgs/personalInfo/submit_zhtw.png", import.meta.url).href,
},
result: {
title: "你的 5 大潛能評分",
// 參照 zh 物件補全並翻譯
subtitle: "此報告旨在呈現你各維度潛能的分佈比例\n助你了解核心優勢與短板",
tagLabel: "你已獲得如下潛能標籤:",
footnote: "標籤組合稀有度*",
footnote2: "*潛能標籤組合出現的機率不超過",
rare: {
"SSR": "SSR",
"UR": "UR",
},
// 參照 zh 物件補全並翻譯
onlyForYou: "*僅供參考",
slideDown: "下滑查看萊雅 5 大潛能定義\n並獲取你的最終報告",
slideUp: new URL("@/assets/imgs/result/slide-up-notice_zhtw.png", import.meta.url).href,
ratingExplanation: new URL("@/assets/imgs/result/rating-explanation_zhtw.png", import.meta.url).href,
actions: {
tryAgain: "再測一次",
generateReport: "生成個人報告",
}
},
report: {
// 參照 zh 物件補全
title: "的核心潛能是",
// 將您提供的譯文移至此處,並使用 dedent 整理
dimDescription: {
Learning_Agility: dedent`
滿\
滿\
\
\
\
`,
Resilience: dedent`
\
使
調\
\
`,
Empathy: dedent`
\
使\
\
`,
Ambition: dedent`
Freedom to go beyond \
滿\
\
`,
Judgement: dedent`
\
\
`
},
// 參照 zh 物件補全,並將圖片路徑改為 zhtw
top: new URL("@/assets/imgs/report/top.png", import.meta.url).href,
bottom: new URL("@/assets/imgs/report/bottom_zhtw.png", import.meta.url).href,
bottomLeft: new URL("@/assets/imgs/report/bottom-left_zhtw.png", import.meta.url).href,
bottomRight: new URL("@/assets/imgs/report/bottom-right_zhtw.png", import.meta.url).href,
shareMask: new URL("@/assets/imgs/report/share-mask_zhtw.png", import.meta.url).href,
}
}
const ko: typeof zh_CN = {
// 參照 zh 物件補全,並將圖片路徑改為 ko
switchLang: new URL("@/assets/imgs/switch-lang_ko.png", import.meta.url).href,
you: "당신",
home: {
title: new URL("@/assets/imgs/home/title_ko.png", import.meta.url).href,
button: new URL("@/assets/imgs/home/button_ko.png", import.meta.url).href,
intro: new URL("@/assets/imgs/home/intro_ko.png", import.meta.url).href,
},
// 以下為您提供的譯文,已修正格式
game: {
agree: "매우 그렇다",
disagree: "매우 그렇지 않다",
},
questionData: {
DimensionName: {
"Learning_Agility": "학습민첩성",
"Resilience": "회복탄력성",
"Empathy": "공감",
"Ambition": "야심",
"Judgement": "판단력"
},
description: {
"A1": "나는 끊임없이 내 한계를 탐색하며, 컴포트 존을 벗어나 새로운 시도와 경험에 과감하게 도전한다.",
"A2": "나는 적극적으로 피드백을 구하고 경청하며, 끊임없이 나 자신을 성장시킨다.",
"A3": "나는 위기를 전환점으로 여기고, 실패를 학습의 좋은 기회로 삼는다.",
"A4": "나는 호기심이 넘치며, 스스로 다양한 학습 자원을 찾아 적극적으로 활용한다.",
"A5": "나는 경험을 통해 배우는 것을 두려워하지 않으며, 성공이 보장되지 않더라도 새로운 방법을 시도하는 데 최선을 다한다.",
"B1": "나는 좌절하거나 실패의 책임을 지는 것에 주저하지 않는다.",
"B2": "나는 스트레스를 잘 관리하고, 좌절감에서 빠르게 벗어나 긍정적이고 낙관적인 태도를 유지한다.",
"B3": "나는 어려움이 닥쳐도 쉽게 포기하지 않으며, 도전을 통해 얻는 동기부여와 영감을 적극적으로 찾고 즐긴다.",
"B4": "나는 상황이 불투명할 때도 냉정한 태도를 유지하며, 꾸준히 노력해서 성공을 향해 나아간다.",
"B5": "나는 피할 수 없는 실패를 겸허히받아들이고, 상황을 한 발 물러서서 바라보며 다음 기회를 위해 최선을 다해 준비한다.",
"C1": "나는 다른 사람들과의 관계를 잘 형성하며, 팀 간의 콜라보레이션을 북돋는다.",
"C2": "나는 언제나 진정성 있게 사람들을 대하고, 존중과 감사를 직접 표현한다.",
"C3": "나는 소비자 행동 뒤에 있는 동기와 심리를 이해하고, 이를 마케팅 전략에 적용할 수 있다.",
"C4": "나는 사람들에게 친절하게 대하며 언제나 배려심을 갖고, 타인의 생각과 감정을 이해하기 위해 적극적으로 경청하려고 한다.",
"C5": "나는 부정적인 감정을 잘 억제하고 효과적으로 관리하며, 다른 사람들에게 부정적인 영향이 가지 않도록 노력한다.",
"D1": "나는 장기적으로 상황을 바라볼 줄 아는 안목이 있고 새로운 트렌드를 잘 포착하며, 창의적인 통찰과 비전을 갖고 있다.",
"D2": "나는 야심이 넘치고 높은 포부를 지니며, 스스로에게 매우 도전적인 목표를 설정한다.",
"D3": "나는 강한 주도성을 갖추고 있으며, 오너십을 가지고 협업을 이끌어 아이디어와 프로젝트를 적극적으로 추진한다.",
"D4": "탁월한 야심으로 변화를 이끌며, 이해관계자들의 목표에 부합하는 혁신적인 목표를 설정하고, 언제나 정직한 태도를 유지한다.",
"D5": "나는 탁월함을 추구하며 현상유지를 하거나 현재에 안주하지 않고, 언제나 개선의 기회와 돌파구를 모색한다.",
"E1": "나는 항상 효율성에 신경 쓰며, 과정을 간소화하고 핵심에 집중하는 데 뛰어나다.",
"E2": "나는 문제의 근원을 잘 파악하고, 복잡한 난관을 차근차근 풀어내며 해결책을 모색한다.",
"E3": "나는 언제나 우선순위를 명확히 정하고, 핵심 과제에 전력을 집중하여 해결책을 마련한다.",
"E4": "나는 건설적인 방식으로 타인의 아이디어에 이의를 제기하고, 더욱 발전할 수 있도록 개선책을 제시한다.",
"E5": "나는 장·단기 이익 및 중요한 세부 사항을 균형 있게 고려하여 종합적으로 사고하고, 분석과 직관을 효과적으로 결합해 핵심적인 의사결정을 신속하게 내린다."
},
trait: {
Pioneer: "개척자",
"Knowledge_Seeker": "지식 줍줍러",
"Change_Pioneer": "변화의 주인공",
"Curious_Mind": "궁금한 거 못참음",
"Trial_Warrior": "겁없는 도전러",
"Giants_Shoulders": "거인의 어깨 위",
"Stress_Master": "멘탈 갑",
"Roly-Poly": "오뚝이",
"Steady_Anchor": "인생 3회차",
"Planner": "파워 J",
"Communication_Expert": "입으로 먹고 삼",
"Sincere_Partner": "믿고 맡기는 찐친 바이브",
"Mind_Hunter": "마인드 헌터",
"Active_Listener": "리액션 장인 경청러",
"Emotion_Management_Master": "감정기복 ZERO",
"Future_Prophet": "미래 각도기",
"Helmsman": "8톤트럭 운전수",
"Owner": "책임감 MAX",
"Strategic_Leader": "전략가 타입",
"Perfectionist": "완벽주의 ON",
"Simplifier": "복잡한 거 시러시러",
"Problem_Sniper": "문제풀이 덕후",
"Eagle_Eye": "디테일 덕후",
"Thought_Challenger": "물음표 킬러",
"Rational_and_Intuitive": "좌뇌 우뇌 밸런스 굿"
}
},
personalInfo: {
title: '성실히 답변해 주셔서 감사합니다\n이제 마지막 단계입니다',
placeholder: {
username: "닉네임을 입력하세요",
age: "나이를 입력하세요",
gender: "성별을 선택하세요",
},
gender: {
male: "남성",
female: "여성",
},
error: { // 參照 placeholder 補全
username: "닉네임을 입력하세요",
age: "나이를 입력하세요",
gender: "성별을 선택하세요",
},
submit: new URL("@/assets/imgs/personalInfo/submit_ko.png", import.meta.url).href,
},
result: {
title: "당신의 다섯가지 잠재력 (5 Potentials) 점수",
subtitle: "이 리포트는 당신의 잠재력 분포 비율을 보여주며\n핵심 강점과 보완점을 파악하는 데 도움을 줍니다",
tagLabel: "다음과 같은 잠재력 태그를 획득했습니다:",
footnote: "태그 조합 희소도*",
footnote2: "*잠재력 태그 조합이 나타날 확률은 다음을 초과하지 않습니다",
rare: {
"SSR": "SSR",
"UR": "UR",
},
onlyForYou: "*참고용",
slideDown: "아래로 스크롤하여 로레알 5대 잠재력에 대한 정의를 확인하고\n최종 리포트를 받아보세요",
slideUp: new URL("@/assets/imgs/result/slide-up-notice_ko.png", import.meta.url).href,
ratingExplanation: new URL("@/assets/imgs/result/rating-explanation_ko.png", import.meta.url).href,
actions: {
tryAgain: "다시 테스트하기",
generateReport: "개인 리포트 생성",
}
},
report: {
title: "님의 핵심 잠재력은",
dimDescription: {
Learning_Agility: dedent`
, . , .
. , . , .
`,
Resilience: dedent`
, . .
, . , 믿 .
`,
Empathy: dedent`
, . '마인드 헌터', .
, . , . 믿.
`,
Ambition: dedent`
, '감히 도전하고 한계를 뛰어넘는' .
, .
, . , . , 믿.
`,
Judgement: dedent`
, . .
, . , .
`
},
top: new URL("@/assets/imgs/report/top.png", import.meta.url).href,
bottom: new URL("@/assets/imgs/report/bottom_ko.png", import.meta.url).href,
bottomLeft: new URL("@/assets/imgs/report/bottom-left_ko.png", import.meta.url).href,
bottomRight: new URL("@/assets/imgs/report/bottom-right_ko.png", import.meta.url).href,
shareMask: new URL("@/assets/imgs/report/share-mask_ko.png", import.meta.url).href,
}
}
const ja: typeof zh_CN = {
// 參照 zh 物件補全,並將圖片路徑改為 ja
switchLang: new URL("@/assets/imgs/switch-lang_ja.png", import.meta.url).href,
you: "あなた",
home: {
title: new URL("@/assets/imgs/home/title_ja.png", import.meta.url).href,
button: new URL("@/assets/imgs/home/button_ja.png", import.meta.url).href,
intro: new URL("@/assets/imgs/home/intro_ja.png", import.meta.url).href,
},
// 以下為您提供的譯文,已修正格式
game: {
agree: "同意する",
disagree: "同意しない",
},
questionData: {
DimensionName: {
"Learning_Agility": "学ぶ意欲",
"Resilience": "忍耐強さ",
"Empathy": "共感力",
"Ambition": "向上心",
"Judgement": "判断力"
},
description: {
"A1": "自分の限界に挑み、心地よい環境から踏み出して新しいことや経験に挑戦するよう努めています。",
"A2": "積極的にフィードバックを求め、継続的成長を目指しています。",
"A3": "危機をチャンスと捉え、失敗を学習の機会と考えています。",
"A4": "好奇心に満ちあふれ、学ぶ機会を積極的に探しています。",
"A5": "成功の保証がなくても実践を通じて学び、新しい方法を試す勇気を持っています。",
"B1": "失敗や挫折に対して向き合い、責任を負います。",
"B2": "ストレス耐性が強く、挫折から素早く立ち直り、前向きな姿勢を保ちます。",
"B3": "困難に直面しても簡単にはあきらめず、挑戦の中にモチベーションとインスピレーションを見出します。",
"B4": "不確かな状況でも冷静さを失わず、成功へと導きます。",
"B5": "避けられない失敗を受け入れ、広い視野を保ち、次のアクションを計画する術を知っています。",
"C1": "他者とのつながりを築くのが得意で、チーム間のコラボレーションを醸成します。",
"C2": "常に誠実に相手と向き合い、尊敬と感謝の気持ちを率直に伝えることができます。",
"C3": "消費者行動の背景にある動機や心理を理解し、それをマーケティング戦略等に活かすことができます。",
"C4": "友好的で思いやりをもち、相手の考えや感情を理解するために積極的に傾聴することができます。",
"C5": "自分のネガティブな感情をコントロールし、周りに悪影響を及ぼさないよう適切に感情コントロールできます。",
"D1": "長期的なビジョンを持ち、新たなトレンドを見極め、創造的な洞察やビジョンを共有することに長けています。",
"D2": "大きな向上心を持ち、自分自身に簡単には達成できないような目標を設定します。",
"D3": "強い主体性を発揮し、当事者意識をもって協働をリードし、アイデアやプロジェクトを前進させます。",
"D4": "大志をもって変革をリードし、ステークホルダーの視座に合わせた目標を設定し、誠実さを保って遂行します。",
"D5": "卓越性を追求し、現状に甘んじることなく、常に改善と飛躍の機会を模索します。",
"E1": "常に効率性に着目したプロセスの簡素化や、重要なポイントを明確化することに長けています。",
"E2": "複雑な問題であっても、根本原因を突き止め、解決策を追求するのが得意です。",
"E3": "優先順位を見極め、重要な課題に集中することができます。",
"E4": "建設的な方法で他者のアイデアに異論を申し立て、改善を促す勇気を持っています。",
"E5": "包括的に物事を捉え、長期的・短期的な利益と細部の状況の見通しを両立させ、論理と直感を効果的に組み合わせて迅速かつ的確な意思決定を行います。"
},
trait: {
"Pioneer": "先駆者",
"Knowledge_Seeker": "知識探求者",
"Change_Pioneer": "変革の先駆者",
"Curious_Mind": "好奇心旺盛",
"Trial_Warrior": "挑戦の戦士",
"Giants_Shoulders": "巨人の肩",
"Stress_Master": "ストレスマスター",
"Roly-Poly": "起き上がりこぼし",
"Steady_Anchor": "頼れる錨",
"Planner": "プランナー",
"Communication_Expert": "コミュニケーションの達人",
"Sincere_Partner": "誠実なパートナー",
"Mind_Hunter": "マインドハンター",
"Active_Listener": "傾聴者",
"Emotion_Management_Master": "感情マネジメントの達人",
"Future_Prophet": "未来の預言者",
"Helmsman": "舵取り役",
"Owner": "オーナー",
"Strategic_Leader": "戦略的リーダー",
"Perfectionist": "完璧主義者",
"Simplifier": "簡素化の達人",
"Problem_Sniper": "問題解決者",
"Eagle_Eye": "イーグルアイ",
"Thought_Challenger": "思考の挑戦者",
"Rational_and_Intuitive": "論理的かつ直感的"
}
},
personalInfo: {
title: "回答にご協力いただきありがとうございます。\nこれが最後のステップです。",
placeholder: {
username: "あなたのニックネームは",
age: "あなたの年齢は",
gender: "性別を選択してください"
},
gender: {
male: "男性",
female: "女性"
},
error: {
username: "ニックネームを入力してください",
age: "年齢を入力してください",
gender: "性別を選択してください"
},
submit: new URL("@/assets/imgs/personalInfo/submit_ja.png", import.meta.url).href,
},
result: {
title: "あなたの5つのポテンシャルスコア",
subtitle: "このレポートは、あなたの各ポテンシャルの分布比率を示し、\nあなたの強みと改善点を理解するのに役立ちます",
tagLabel: "次のポテンシャルタグを取得しました:",
footnote: "タグ組み合わせのレア度*",
footnote2: "*タグの組み合わせの出現率は以下です",
rare: {
"SSR": "SSR",
"UR": "UR",
},
onlyForYou: "※参考用",
slideDown: "下にスクロールしてロレアルの5つのポテンシャルの定義を確認し、\n最終レポートを入手してください",
slideUp: new URL("@/assets/imgs/result/slide-up-notice_ja.png", import.meta.url).href,
ratingExplanation: new URL("@/assets/imgs/result/rating-explanation_ja.png", import.meta.url).href,
actions: {
tryAgain: "もう一度試す",
generateReport: "個人レポートを作成",
}
},
report: {
title: "のコアポテンシャルは",
dimDescription: {
Learning_Agility: dedent`
`,
Resilience: dedent`
姿
姿
`,
Empathy: dedent`
`,
Ambition: dedent`
`,
Judgement: dedent`
`
},
top: new URL("@/assets/imgs/report/top.png", import.meta.url).href,
bottom: new URL("@/assets/imgs/report/bottom_ja.png", import.meta.url).href,
bottomLeft: new URL("@/assets/imgs/report/bottom-left_ja.png", import.meta.url).href,
bottomRight: new URL("@/assets/imgs/report/bottom-right_ja.png", import.meta.url).href,
shareMask: new URL("@/assets/imgs/report/share-mask_ja.png", import.meta.url).href,
}
}
export type MessageSchema = typeof zh_CN
export default {
zh_CN,
zh_TW,
en,
ja,
ko
}

60
src/main.ts Normal file
View File

@ -0,0 +1,60 @@
import { createApp } from 'vue'
import './assets/fonts/stylesheet.css'
import './style.scss'
import App from './App.vue'
import { createI18n } from 'vue-i18n';
import lang from './locates'
import type { MessageSchema } from './locates'
import imgs from './assets/imgs';
// @ts-ignore
const userLang = navigator.language || navigator.userLanguage;
console.log("Detected language:", userLang);
// 语言检测逻辑
let language = 'en'; // 默认语言
if (userLang) {
const langCode = userLang.toLowerCase();
if (langCode.startsWith('zh-cn') || langCode === 'zh') {
language = 'zh_CN';
} else if (langCode.startsWith('zh-tw') || langCode.startsWith('zh-hk')) {
language = 'zh_TW';
} else if (langCode.startsWith('ja')) {
language = 'ja';
} else if (langCode.startsWith('ko')) {
language = 'ko';
} else if (langCode.startsWith('en')) {
language = 'en';
}
}
console.log("Selected language:", language);
const i18n = createI18n<[MessageSchema], 'en' | 'zh_CN' | 'zh_TW' | 'ja' | 'ko'>({
locale: language,
fallbackLocale: 'en',
// @ts-ignore
messages: lang
})
const flattenNestedObject = (obj: any) => {
const flatten = (item: any): string[] | string => {
if (typeof item == 'string')
return item;
else
return Object.values(item).map(flatten).flat();
};
return Object.values(obj).map(flatten).flat();
};
flattenNestedObject(imgs).forEach(img => {
let link = document.createElement('link');
link.rel = 'preload';
link.href = img;
link.as = 'image';
document.head.appendChild(link);
});
createApp(App).use(i18n).mount('#app')

35
src/mixin.scss Normal file
View File

@ -0,0 +1,35 @@
@mixin flex-center($gap) {
display: flex;
align-items: center;
gap: $gap;
}
@mixin abs-pos($x, $y, $scale: 1) {
position: absolute;
transform-origin: center;
@if $y < 0 {
bottom: -$y;
transform: translateX(-50%) scale($scale);
}
@else {
top: $y;
transform: translate(-50%, -50%) scale($scale);
}
left: $x;
}
@mixin font($font-size, $line-height, $white-space: normal, $text-align: start) {
font-size: $font-size;
line-height: $line-height;
white-space: $white-space;
text-align: $text-align;
}
@mixin full-size {
height: 100%;
width: 100%;
}

150
src/style.scss Normal file
View File

@ -0,0 +1,150 @@
@use './mixin.scss' as *;
body {
margin: 0;
color: white;
background-color: #ffffff;
}
body,
input,
textarea,
button {
font-family: 'LOREAL Essentielle', 'AR NewcuheiGB';
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
appearance: none;
}
* {
box-sizing: border-box;
}
img {
display: block;
}
#app {
height: 100vh;
height: 100dvh;
width: 100%;
max-width: 60vh;
position: relative;
margin: 0 auto;
box-shadow: 0 0 16px rgba(0, 0, 0, 0.5);
}
.page {
position: absolute;
inset: 0;
@include full-size;
}
.bg {
position: absolute;
inset: 0;
@include full-size;
/* img {
@include full-size;
object-fit: cover;
} */
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.7s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@keyframes slide-down {
0% {
transform: translateY(-30%);
opacity: .5;
}
50% {
transform: translateY(10%);
opacity: 1;
}
100% {
transform: translateY(-30%);
opacity: .5;
}
}
@keyframes slide-up {
0% {
transform: translateY(-30%) rotate(-180deg);
opacity: .5;
}
50% {
transform: translateY(10%) rotate(-180deg);
opacity: 1;
}
100% {
transform: translateY(-30%) rotate(-180deg);
opacity: .5;
}
}
:root {
font-size: 15px;
}
@media screen and ((max-width: 412px) or (max-height: 732px)) {
:root {
font-size: 13.4px;
}
}
@keyframes growing-bg-animation {
0% {
filter: brightness(1) contrast(1) saturate(1) blur(2px);
transform: scale(1) translate3d(0, 0, 0);
}
25% {
filter: brightness(1.05) contrast(1.05) saturate(1.05) blur(6px);
transform: scale(1.1) translate3d(3px, 3px, 0);
}
50% {
filter: brightness(1) contrast(1) saturate(1) blur(6px);
transform: scale(1) translate3d(0, 0, 0);
}
68% {
filter: brightness(.95) contrast(.95) saturate(.95) blur(4px);
transform: scale(1.05) translate3d(-5px, -5px, 0);
}
79% {
filter: brightness(.95) contrast(.95) saturate(.95) blur(4px);
transform: scale(1.02) translate3d(2px, 2px, 0);
}
100% {
filter: brightness(1) contrast(1) saturate(1) blur(2px);
transform: scale(1) translate3d(0, 0, 0);
}
}
.growing-bg {
position: absolute;
left: -4px;
top: -4px;
width: calc(100% + 8px);
height: calc(100% + 8px);
object-fit: cover;
animation: growing-bg-animation 8s ease infinite;
}

560
src/utils.ts Normal file
View File

@ -0,0 +1,560 @@
/**
* @file utils.ts
* @description Canvas EasyCanvas
* @version 1.0.0
* @date 2024-08-31
* @author feie9454
*/
/**
* Darkens a hex color by a given percentage
* @param hex - Hex color code (e.g. '#00D78D' or '00D78D')
* @param percentage - Amount to darken by (0-100)
* @returns Darkened hex color
*/
export function darken(hex: string, percentage: number = 20): string {
// Remove # if present
hex = hex.replace('#', '');
if (hex.length !== 6) {
throw new Error('Invalid hex color format');
}
// Ensure percentage is between 0 and 100
percentage = Math.min(100, Math.max(0, percentage));
// Convert hex to RGB
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
// Darken each component
const darkFactor = (100 - percentage) / 100;
const newR = Math.floor(r * darkFactor);
const newG = Math.floor(g * darkFactor);
const newB = Math.floor(b * darkFactor);
// Convert back to hex
const toHex = (n: number): string => {
const hex = Math.max(0, Math.min(255, n)).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
}
/**
*
* @param {(index: number, total: number) => void} callback -
* @param {number} frameCount -
*/
export function forEachFrame(callback: (index: number, total: number) => void, frameCount: number) {
if (frameCount <= 0) {
return;
}
let index = 0;
const total = frameCount;
const f = () => {
callback(index, total);
index++;
if (index < total) {
requestAnimationFrame(f);
}
};
requestAnimationFrame(f);
}
/**
*
* @param {(elapsedTime: number, totalTime: number) => void} callback -
* @param {number} milliseconds -
*/
export function forEachFrameInMilliseconds(callback: (elapsedTime: number, totalTime: number) => void, milliseconds: number) {
let startTime = Date.now();
let totalTime = milliseconds
const f = () => {
let elapsedTime = Date.now() - startTime;
if (elapsedTime < totalTime) {
callback(elapsedTime, totalTime);
requestAnimationFrame(f);
} else {
callback(totalTime, totalTime);
}
};
requestAnimationFrame(f);
}
/**
* 线线
* @param {[number, number, number, number]} [f=[0.42, 0, 0.58, 1]] - 线
* @returns {(t: number) => number}
*/
export function createLinear2CubicBezier(f: [number, number, number, number] = [0.42, 0, 0.58, 1]): (t: number) => number {
return (t: number) => {
return linear2CubicBezier(t, f)
}
}
/**
* 线线
* @param {number} source - 0 1
* @param {[number, number, number, number]} [f=[0.42, 0, 0.58, 1]] - 线
* @returns {number}
* @throws {Error} 0 1
*/
export function linear2CubicBezier(source: number, f: [number, number, number, number] = [0.42, 0, 0.58, 1]): number {
if (source < 0 || source > 1) {
throw new Error("Source must be between 0 and 1");
}
const [x1, y1, x2, y2] = f;
const cubicBezier = (t: number, p0: number, p1: number, p2: number, p3: number) => {
const u = 1 - t;
return (u * u * u * p0) + (3 * u * u * t * p1) + (3 * u * t * t * p2) + (t * t * t * p3);
}
const solveCubicBezierX = (xTarget: number, epsilon = 0.00001) => {
let t = xTarget;
let x = cubicBezier(t, 0, x1, x2, 1);
let iteration = 0;
while (Math.abs(x - xTarget) > epsilon && iteration < 100) {
const d = 3 * (1 - t) * (1 - t) * (x1 - 0) + 6 * (1 - t) * t * (x2 - x1) + 3 * t * t * (1 - x2);
if (d === 0) break;
t -= (x - xTarget) / d;
x = cubicBezier(t, 0, x1, x2, 1);
iteration++;
}
return t;
}
const t = solveCubicBezierX(source);
const y = cubicBezier(t, 0, y1, y2, 1);
return y;
}
/**
*
* @param {number} length -
* @returns {string}
*/
export function generateRandomHexString(length: number): string {
const characters = '0123456789abcdef';
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters[randomIndex];
}
return result;
}
/**
* EasyCanvas Canvas
*/
export class EasyCanvas {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
statusMap = new Map<string, ImageData>();
/**
*
* @param {HTMLCanvasElement} canvas - Canvas
*/
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d")!;
}
static checkSaveSupportMIME(mimetype: string,) {
return new Promise<boolean>(resolve => {
const canvas = document.createElement('canvas');
canvas.toBlob(blob => {
// 检查生成的 Blob 类型是否与指定的 mimetype 匹配
resolve(!!(blob && blob.type === mimetype));
}, mimetype);
})
}
static async getAvailableSaveTypes() {
const types = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp'];
const results = await Promise.all(types.map(this.checkSaveSupportMIME));
return types.filter((_, index) => results[index]);
}
/**
*
* @param {CanvasRenderingContext2D} ctx - Canvas
* @param {string} text -
* @param {number} maxWidth -
* @returns {string[]}
*/
static getWrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, considerMarkInLineEnd = true): string[] {
const txtList = [];
const markList = [
",", ".", "!", "?", ";", ":", "", "。", "", "", "", "",
"-", "", "—", "(", ")", "[", "]", "{", "}", "<", ">", "", "",
"“", "”", "\"", "'", "`", "~", "@", "#", "$", "%", "^", "&", "*",
"_", "+", "=", "|", "\\", "/", "、", "·", "…", "《", "》", "〈",
"〉", "「", "」", "『", "』", "【", "】", "〖", "〗", "", "",
"", "", "", "", "", "﹋", "﹌", "", "", "", "﹐",
"﹑", "﹒", "﹔", "﹖", "﹗", "﹟", "﹠", "﹡", "﹢", "﹣", "﹤",
"﹥", "﹦", "", "﹩", "﹪", "﹫"
];
let str = "";
for (let i = 0, len = text.length; i < len; i++) {
str += text.charAt(i);
if (ctx.measureText(str).width > maxWidth) {
if (considerMarkInLineEnd && markList.includes(text.charAt(i))) {
continue
}
txtList.push(str.substring(0, str.length - 1))
str = ""
i--
}
}
txtList.push(str)
return txtList;
}
/**
* Canvas
* @param {string} font -
* @param {string} fillStyle -
* @param {string} text -
* @param {number} x - x
* @param {number} y - y
* @param {Object} [options] -
* @returns {this} EasyCanvas
*/
fillText(font: string, fillStyle: string, text: string, x: number, y: number,
options?:
| { align?: "left" | "right" }
| { align?: "left" | "right", maxWidth: number, lineHeight: number }
| { align: "centerW", width: number }
| { align: "centerW", width: number, maxWidth: number, lineHeight: number }
| { align: "centerP" }
| { align: "centerP", maxWidth: number, lineHeight: number }
): this {
this.ctx.font = font;
this.ctx.fillStyle = fillStyle;
if (options) {
if ("align" in options && options.align == "right") {
if ("maxWidth" in options && "lineHeight" in options) {
EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => {
this.ctx.fillText(str, x - this.ctx.measureText(str).width, y + options.lineHeight * index)
})
} else {
this.ctx.fillText(text, x - this.ctx.measureText(text).width, y)
}
} else if ("align" in options && options.align == "centerW") {
if ("maxWidth" in options && "lineHeight" in options) {
EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => {
let textWidth = this.ctx.measureText(str).width;
this.ctx.fillText(str, x + (options.width - textWidth) / 2, y + options.lineHeight * index)
})
} else {
let textWidth = this.ctx.measureText(text).width;
this.ctx.fillText(text, x + (options.width - textWidth) / 2, y)
}
} else if ("align" in options && options.align == "centerP") {
if ("maxWidth" in options && "lineHeight" in options) {
EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => {
let textWidth = this.ctx.measureText(str).width;
this.ctx.fillText(str, x - textWidth / 2, y + options.lineHeight * index)
})
} else {
let textWidth = this.ctx.measureText(text).width;
this.ctx.fillText(text, x - textWidth / 2, y)
}
} else if ("align" in options && options.align == "left" || !("align" in options)) {
if ("maxWidth" in options && "lineHeight" in options) {
EasyCanvas.getWrapText(this.ctx, text, options.maxWidth).forEach((str, index) => {
this.ctx.fillText(str, x, y + options.lineHeight * index)
})
} else {
this.ctx.fillText(text, x, y)
}
}
} else {
this.ctx.fillText(text, x, y);
}
return this
}
/**
* Canvas
* @param {CanvasImageSource} image -
* @param {number} dx - canvas x
* @param {number} dy - canvas y
* @param {number} dWidth - canvas
* @param {number} dHeight - canvas
* @returns {this} EasyCanvas
*/
drawImage(image: CanvasImageSource, dx: number, dy: number, dWidth: number, dHeight: number): this {
this.ctx.drawImage(image, dx, dy, dWidth, dHeight);
return this
}
/**
* Canvas
* @param {string} statusName -
* @returns {this} EasyCanvas
*/
saveStatus(statusName: string): this {
let canvasData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
this.statusMap.set(statusName, canvasData);
return this
}
/**
* Canvas
* @param {string} statusName -
* @returns {this} EasyCanvas
*/
restoreStatus(statusName: string): this {
if (this.statusMap.has(statusName)) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.putImageData(this.statusMap.get(statusName)!, 0, 0);
}
return this
}
/**
* Canvas
* @param {string} statusName -
* @returns {this} EasyCanvas
*/
deleteStatus(statusName: string): this {
this.statusMap.delete(statusName);
return this
}
/**
* Canvas 线
* @param {...[number, number][]} points - 线
* @returns {this} EasyCanvas
*/
lineTo(...points: [number, number][]): this {
points.forEach(([x, y]) => this.ctx.lineTo(x, y));
return this
}
/**
*
* @param {...[number, number][]} points -
* @returns {this} EasyCanvas
*/
createPath(...points: [number, number][]): this {
this.ctx.beginPath();
points.forEach(([x, y], index) => {
if (index === 0) this.ctx.moveTo(x, y);
this.ctx.lineTo(x, y)
});
this.ctx.closePath();
return this
}
/**
*
* @param {string} strokeStyle -
* @returns {this} EasyCanvas
*/
stroke(strokeStyle: string): this {
this.ctx.strokeStyle = strokeStyle;
this.ctx.stroke();
return this
}
/**
*
* @param {string} fillStyle -
* @returns {this} EasyCanvas
*/
fill(fillStyle: string): this {
this.ctx.fillStyle = fillStyle;
this.ctx.fill();
return this
}
/**
*
* @param {(progress: number, p: this) => void} onFrame -
* @param {Object} options -
* @param {number} options.duration -
* @param {(t: number) => number} options.easing -
* @param {boolean} [options.autoRecover] -
* @param {number} [options.delay] -
* @returns {this} EasyCanvas
*/
createAnimation(
onFrame: (progress: number, p: this) => void,
options: { duration: number, easing: (t: number) => number, autoRecover?: boolean, delay?: number }
): this {
const { duration, easing } = options;
let animationId = generateRandomHexString(16);
if (options.autoRecover) this.saveStatus(animationId);
setTimeout(() => {
forEachFrameInMilliseconds((index, total) => {
const progress = index / total;
if (options.autoRecover) this.restoreStatus(animationId);
onFrame(easing(progress), this);
}, duration);
}, options.delay);
return this
}
/**
*
* @param {Object} options -
* @param {[number, number]} options.centerPoint -
* @param {number} options.r -
* @param {string} options.fillStyle -
* @param {string} options.strokeStyle -
* @param {number} [options.startAngle] -
* @param {(index: number, domainCount: number) => number} [options.angleCal] -
* @param {...number[]} value -
* @returns {this} EasyCanvas
*/
createGraph(options: { centerPoint: [number, number], r: number, fillStyle: string, strokeStyle: string, startAngle?: number, angleCal?: (index: number, domainCount: number) => number }, ...value: number[]): this {
const { centerPoint, r, strokeStyle, fillStyle } = options;
let points: [number, number][] = [];
const domainCount = value.length;
for (let i = 0; i < domainCount; i++) {
if (!options.angleCal) {
options.angleCal = (index: number, domainCount: number) => index / domainCount * Math.PI * 2;
}
if (!options.startAngle) {
options.startAngle = 0;
}
const angle = options.angleCal(i, domainCount) + options.startAngle;
const x = centerPoint[0] + r * Math.cos(angle) * value[i];
const y = centerPoint[1] + r * Math.sin(angle) * value[i];
points.push([x, y]);
}
return this.createPath(...points)
.fill(fillStyle)
.stroke(strokeStyle)
}
}
export class EasyAudio {
private static audioCtx: AudioContext | null = null;
private buffers: Map<string, AudioBuffer> = new Map();
private sources: Map<string, AudioBufferSourceNode> = new Map();
private audioOptions: Map<
string,
{ audioUrl: string; loop?: boolean; volume?: number }
> = new Map();
private get audioCtx(): AudioContext {
if (!EasyAudio.audioCtx) {
EasyAudio.audioCtx = new AudioContext();
}
return EasyAudio.audioCtx;
}
constructor(
audios?:
| { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }
| { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }[]
) {
if (audios) {
this.add(audios);
}
}
private async createAudioBuffer(audioUrl: string): Promise<AudioBuffer> {
const response = await fetch(audioUrl);
const arrayBuffer = await response.arrayBuffer();
return await this.audioCtx.decodeAudioData(arrayBuffer);
}
private async loadAudio(name: string) {
const options = this.audioOptions.get(name);
if (!options) {
throw new Error(`音频 ${name} 未找到`);
}
const buffer = await this.createAudioBuffer(options.audioUrl);
this.buffers.set(name, buffer);
}
async load(): Promise<void> {
const promises = Array.from(this.audioOptions.keys()).map(name =>
this.loadAudio(name)
);
await Promise.all(promises);
}
add(
audios:
| { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }
| { name: string; audioUrl: string | URL; loop?: boolean; volume?: number }[]
): void {
if (Array.isArray(audios)) {
audios.forEach(audio => {
this.audioOptions.set(audio.name, {
audioUrl: typeof (audio.audioUrl) === "string" ? audio.audioUrl : audio.audioUrl.href,
loop: audio.loop,
volume: audio.volume,
});
});
} else {
this.audioOptions.set(audios.name, {
audioUrl: typeof (audios.audioUrl) === "string" ? audios.audioUrl : audios.audioUrl.href,
loop: audios.loop,
volume: audios.volume,
});
}
}
async play(name: string) {
if (!this.buffers.has(name)) {
await this.loadAudio(name);
}
const buffer = this.buffers.get(name);
const options = this.audioOptions.get(name);
if (!buffer || !options) {
throw new Error(`音频 ${name} 未找到`);
}
const source = this.audioCtx.createBufferSource();
source.buffer = buffer;
source.loop = options.loop || false;
let gainNode: GainNode | null = null;
if (options.volume !== undefined) {
gainNode = this.audioCtx.createGain();
gainNode.gain.value = options.volume;
source.connect(gainNode);
gainNode.connect(this.audioCtx.destination);
} else {
source.connect(this.audioCtx.destination);
}
source.start();
this.sources.set(name, source);
console.log(`音频 ${name} 播放`);
}
stop(name: string) {
const source = this.sources.get(name);
if (source) {
source.stop();
this.sources.delete(name);
}
}
stopAll() {
this.sources.forEach(source => source.stop());
this.sources.clear();
}
}

Some files were not shown because too many files have changed in this diff Show More