mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 03:20:12 +08:00
feat: new map by svg
This commit is contained in:
1 parent
517ab43e8c
commit
8f6914d46c
2 files changed
+86
-134
No files matched your search
+84
-132
@@ -1,163 +1,69 @@
|
|||||||
<!-- eslint-disable no-console -->
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { use, init } from 'echarts/core'
|
|
||||||
import { GraphChart } from 'echarts/charts'
|
|
||||||
import { SVGRenderer } from 'echarts/renderers'
|
|
||||||
import {
|
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
|
||||||
DataZoomComponent,
|
|
||||||
} from 'echarts/components'
|
|
||||||
import { DAG } from '@linux-odyssey/utils'
|
import { DAG } from '@linux-odyssey/utils'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { NodeImage } from '../img/svg.js'
|
|
||||||
import useUserProfile from '../store/userProfile'
|
import useUserProfile from '../store/userProfile'
|
||||||
|
|
||||||
const store = useUserProfile()
|
const store = useUserProfile()
|
||||||
const marginX = 500
|
const router = useRouter()
|
||||||
const marginY = 300
|
const toast = useToast()
|
||||||
const chartContainer = ref(null)
|
|
||||||
let chartInstance = null
|
|
||||||
const fullwidth = window.screen.width
|
|
||||||
|
|
||||||
use([
|
const quests = ref([])
|
||||||
GraphChart,
|
const svgWidth = ref(window.innerWidth)
|
||||||
SVGRenderer,
|
const svgHeight = ref(window.innerHeight)
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
const marginX = 60
|
||||||
DataZoomComponent,
|
const marginY = 50
|
||||||
])
|
|
||||||
|
|
||||||
async function getQuests() {
|
async function getQuests() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/quests')
|
const res = await api.get('/quests')
|
||||||
return res.data
|
quests.value = res.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const genOption = (nodes: any, edges: any) => ({
|
const graphData = computed(() => {
|
||||||
renderer: 'svg',
|
const dag = new DAG(quests.value)
|
||||||
tooltip: {},
|
|
||||||
animationDurationUpdate: 1500,
|
|
||||||
animationEasingUpdate: 'quinticInOut',
|
|
||||||
// dataZoom: [
|
|
||||||
// {
|
|
||||||
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
|
|
||||||
// orient: 'vertical', // This makes the data zoom component vertical
|
|
||||||
// start: 0, // The starting percentage of the window out of the data extent
|
|
||||||
// end: 100, // The ending percentage of the window out of the data extent
|
|
||||||
// show: true,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'graph',
|
|
||||||
layout: 'none',
|
|
||||||
symbol: () => {
|
|
||||||
return NodeImage
|
|
||||||
},
|
|
||||||
symbolSize: [fullwidth / 13, fullwidth / 30],
|
|
||||||
roam: 'move',
|
|
||||||
zoom: 1.5,
|
|
||||||
center: ['120%', '30%'],
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: fullwidth / 90,
|
|
||||||
},
|
|
||||||
edgeSymbol: ['pin', 'arrow'],
|
|
||||||
edgeSymbolSize: [2, 15],
|
|
||||||
edgeLabel: {
|
|
||||||
fontSize: 25,
|
|
||||||
},
|
|
||||||
data: nodes,
|
|
||||||
// links: [],
|
|
||||||
links: edges,
|
|
||||||
lineStyle: {
|
|
||||||
width: 3,
|
|
||||||
color: 'gray',
|
|
||||||
opacity: 4,
|
|
||||||
curveness: 0.08,
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
color: (params: any) => {
|
|
||||||
const {
|
|
||||||
data: { completed, unlocked },
|
|
||||||
} = params
|
|
||||||
if (completed) {
|
|
||||||
return '#00ff00'
|
|
||||||
}
|
|
||||||
if (unlocked) {
|
|
||||||
return '#ADADB5'
|
|
||||||
}
|
|
||||||
return '#505050'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
function getOption(quests: any, progress: any) {
|
|
||||||
const dag = new DAG(quests)
|
|
||||||
|
|
||||||
|
console.log(store.progress)
|
||||||
const nodes = dag.getNodes().map((node) => ({
|
const nodes = dag.getNodes().map((node) => ({
|
||||||
id: node._id,
|
id: node._id,
|
||||||
name: node.title,
|
title: node.title,
|
||||||
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
x: marginX * node.layer * 2,
|
||||||
x: marginY * node.layer * 2,
|
// y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
||||||
completed: progress[node._id]?.completed || false,
|
y: marginY * node.index + svgHeight.value / 2,
|
||||||
unlocked: node.requirements.every((req: any) => progress[req]?.completed),
|
completed: store.progress[node._id]?.completed || false,
|
||||||
|
unlocked: node.requirements.every(
|
||||||
|
(req: string) => store.progress[req]?.completed
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
const lineAppearence = (node: any) => {
|
|
||||||
try {
|
console.log(nodes)
|
||||||
const { unlocked } = node
|
|
||||||
return {
|
|
||||||
type: unlocked ? 'line' : 'dashed',
|
|
||||||
color: unlocked ? '#ADADB5' : '#454552',
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const edges = dag.getEdgesArray().map((edge: any) => ({
|
const edges = dag.getEdgesArray().map((edge: any) => ({
|
||||||
source: edge[0],
|
source: nodes.find((n) => n.id === edge[0]),
|
||||||
target: edge[1],
|
target: nodes.find((n) => n.id === edge[1]),
|
||||||
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
|
|
||||||
}))
|
}))
|
||||||
return genOption(nodes, edges)
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
return { nodes, edges }
|
||||||
const toast = useToast()
|
})
|
||||||
|
|
||||||
function initChart(option: any) {
|
function handleNodeClick(node: any) {
|
||||||
chartInstance = init(chartContainer.value)
|
if (node.unlocked) {
|
||||||
chartInstance.setOption(option)
|
router.push({ name: 'game', params: { questId: node.id } })
|
||||||
chartInstance.on('click', (params: any) => {
|
} else {
|
||||||
const {
|
toast.warning('你還沒完成前一個關卡!')
|
||||||
data: { id, unlocked },
|
}
|
||||||
} = params
|
|
||||||
|
|
||||||
if (unlocked) {
|
|
||||||
router.push({ name: 'game', params: { questId: id } })
|
|
||||||
} else {
|
|
||||||
toast.warning('你還沒完成前一個關卡!')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.loadUserProfile()
|
await store.loadUserProfile()
|
||||||
const quests = await getQuests()
|
await getQuests()
|
||||||
const option = getOption(quests, store.progress)
|
|
||||||
initChart(option)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -174,8 +80,54 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
踏上你的Linux冒險之旅吧!
|
踏上你的Linux冒險之旅吧!
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-wrap absolute w-full h-full z-1">
|
<svg :width="svgWidth" :height="svgHeight" class="absolute">
|
||||||
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
|
<!-- <g
|
||||||
</div>
|
v-for="edge in graphData.edges"
|
||||||
|
:key="`${edge.source.id}-${edge.target.id}`"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
:x1="edge.source.x"
|
||||||
|
:y1="edge.source.y"
|
||||||
|
:x2="edge.target.x"
|
||||||
|
:y2="edge.target.y"
|
||||||
|
:stroke="edge.target.unlocked ? '#ADADB5' : '#454552'"
|
||||||
|
:stroke-dasharray="edge.target.unlocked ? 'none' : '5,5'"
|
||||||
|
stroke-width="3"
|
||||||
|
/>
|
||||||
|
</g> -->
|
||||||
|
<g
|
||||||
|
v-for="node in graphData.nodes"
|
||||||
|
:key="node.id"
|
||||||
|
@click="handleNodeClick(node)"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
:x="node.x - 50"
|
||||||
|
:y="node.y - 25"
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
:fill="
|
||||||
|
node.completed ? '#00ff00' : node.unlocked ? '#ADADB5' : '#505050'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
:x="node.x"
|
||||||
|
:y="node.y"
|
||||||
|
text-anchor="middle"
|
||||||
|
alignment-baseline="middle"
|
||||||
|
fill="white"
|
||||||
|
font-size="12"
|
||||||
|
>
|
||||||
|
{{ node.title }}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,14 +12,14 @@ interface IProgress {
|
|||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
progress: Map<string, IProgress>
|
progress: Record<string, IProgress>
|
||||||
}
|
}
|
||||||
|
|
||||||
const useUserProfile = defineStore('userProfile', {
|
const useUserProfile = defineStore('userProfile', {
|
||||||
state: (): UserProfile => ({
|
state: (): UserProfile => ({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
progress: new Map<string, IProgress>(),
|
progress: {},
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async loadUserProfile() {
|
async loadUserProfile() {
|
||||||
|
|||||||
Reference in new issue
Block a user