mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 11:30:13 +08:00
categorise components
This commit is contained in:
1 parent
9a4f9b958a
commit
b5e49547ea
14 files changed
+12
-12
No files matched your search
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
||||
<div id="cheatsheets">
|
||||
<p class="text-text-secondary">--指令列表--</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ref } from 'vue'
|
||||
import { Vue3Lottie } from 'vue3-lottie'
|
||||
// import { surveyUrl } from '../config'
|
||||
import CompleteAnimation from '../img/complete-animation.json'
|
||||
|
||||
const showModal = ref(true) // Controls whether the modal is visible or not
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const closeModal = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
const defineComponent = () => ({
|
||||
components: {
|
||||
Vue3Lottie,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
CompleteAnimation,
|
||||
}
|
||||
},
|
||||
})
|
||||
const backtoMap = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
// useToast().error('Failed to load map')
|
||||
toast.error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate completing a quest to show the modal
|
||||
setTimeout(() => {
|
||||
showModal.value = true
|
||||
}, 5000) // Show modal after 5 seconds
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showModal" class="modal">
|
||||
<div class="modal-content bg-bg-secondary rounded-lg">
|
||||
<span class="close text-text" @click="closeModal">×</span>
|
||||
|
||||
<h2
|
||||
id="QuestCompleted"
|
||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||
>
|
||||
關卡完成!
|
||||
</h2>
|
||||
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
|
||||
<Vue3Lottie
|
||||
:animationData="CompleteAnimation"
|
||||
:height="200"
|
||||
:width="200"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-base text-text text-center">恭喜你完成這一關!</p>
|
||||
<p class="text-base text-text text-center">
|
||||
接下來回到地圖,迎接新的挑戰吧!
|
||||
</p>
|
||||
<!-- <p class="text-base text-text">
|
||||
您已成功完成挑戰,真的非常感謝您的參與!<br />
|
||||
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
|
||||
</p> -->
|
||||
<!-- <a
|
||||
id="SurveyButton"
|
||||
:href="surveyUrl"
|
||||
target="_blank"
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
|
||||
>填寫問卷</a
|
||||
> -->
|
||||
<a
|
||||
id="BacktoMap"
|
||||
target="_blank"
|
||||
@click="backtoMap"
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
|
||||
>回到地圖</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { createSession } from '../store/session'
|
||||
import { surveyUrl } from '../config'
|
||||
|
||||
const router = useRouter()
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await createSession()
|
||||
}
|
||||
const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
useToast().error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
|
||||
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'file-invoice']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">問卷</p>
|
||||
</a>
|
||||
</button>
|
||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-question']"
|
||||
class="text-text-disabled"
|
||||
/>
|
||||
<p class="text-text-disabled inline ml-1">解答</p>
|
||||
</button>
|
||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-rotate-left']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">重來</p>
|
||||
</button>
|
||||
<button
|
||||
id="continue"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="continuePlay"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-right']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">繼續</p>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import sessionStore from '../store/session'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
|
||||
const current = ref(-1)
|
||||
watch(
|
||||
() => sessionStore.session.hints,
|
||||
(hints) => {
|
||||
current.value = hints.length - 1
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const left = () => {
|
||||
if (current.value > 0) {
|
||||
current.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
const right = () => {
|
||||
if (current.value < sessionStore.session.hints.length - 1) {
|
||||
current.value += 1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Hint Header -->
|
||||
<div class="flex items-center p-2 h-10">
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'lightbulb']"
|
||||
class="text-yellow-200 p-2 content-center"
|
||||
/>
|
||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
|
||||
<!-- Hint -->
|
||||
提示
|
||||
</h1>
|
||||
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
||||
<button
|
||||
class="p-2 m-1 w-1/8"
|
||||
id="Lbutton"
|
||||
@click="left"
|
||||
:disabled="current <= 0"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-right']"
|
||||
class="text-text"
|
||||
rotation="180"
|
||||
/>
|
||||
</button>
|
||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
|
||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
|
||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">
|
||||
{{ sessionStore.session.hints.length }}
|
||||
</p>
|
||||
<button
|
||||
class="p-2 m-1 w-1/8"
|
||||
id="Rbutton"
|
||||
@click="right"
|
||||
:disabled="current === sessionStore.session.hints.length - 1"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hint" class="bg-bg p-8 overflow-y-auto">
|
||||
<ul>
|
||||
<li
|
||||
v-for="response in sessionStore.session.responses[current]"
|
||||
:key="response.type"
|
||||
class="text-text font-xl whitespace-pre-wrap"
|
||||
>
|
||||
<div v-for="content in response.content" :key="content">
|
||||
<MarkdownText :content="content" />
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-for="hint in sessionStore.session.hints[current]"
|
||||
:key="hint"
|
||||
class="text-text-primary font-xl whitespace-pre-wrap"
|
||||
>
|
||||
<MarkdownText :content="hint" />
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
import sessionStore from '../store/session'
|
||||
import { Task } from '../types'
|
||||
|
||||
const tasks = computed(() => {
|
||||
return sessionStore.session.tasks.map((task: Task) => {
|
||||
const prefix = task.completed ? '✓' : '➤'
|
||||
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||
return {
|
||||
...task,
|
||||
class: `${color} markdown-content`,
|
||||
content: `${prefix} ${task.name}`,
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full max-h-full">
|
||||
<p id="topic" class="text-text font-black" v-if="sessionStore.quest">
|
||||
{{ sessionStore.quest.title }}
|
||||
</p>
|
||||
<br />
|
||||
<div>
|
||||
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
||||
<MarkdownText
|
||||
class="text-text markdown-content"
|
||||
:content="sessionStore.quest.instruction"
|
||||
/>
|
||||
<br />
|
||||
<p id="tasks" class="text-text">任務:</p>
|
||||
<ul v-if="sessionStore.session">
|
||||
<li v-for="task in tasks" :key="task.id">
|
||||
<MarkdownText :class="task.class" :content="task.content" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="text-text" v-else>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useTerminal } from '../store/session'
|
||||
|
||||
const terminal = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const term = useTerminal()
|
||||
if (!terminal.value) return
|
||||
term.mount(terminal.value)
|
||||
window.addEventListener('resize', () => term.resizeScreen())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
|
||||
<button id="Terminal" class="text-text h-[5%]">終端機</button>
|
||||
<div
|
||||
id="terminal"
|
||||
ref="terminal"
|
||||
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
|
||||
></div>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import FileNode from './FileNode.vue'
|
||||
import sessionStore from '../store/session'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree w-max h-max">
|
||||
<ul>
|
||||
<li>
|
||||
<FileNode
|
||||
:node="sessionStore.session.graph"
|
||||
:pwd="sessionStore.session.pwd"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tree ul {
|
||||
padding-top: 20px;
|
||||
position: relative;
|
||||
|
||||
transition: all 0.5s;
|
||||
-webkit-transition: all 0.5s;
|
||||
-moz-transition: all 0.5s;
|
||||
}
|
||||
|
||||
.tree li {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
padding: 20px 5px 0 5px;
|
||||
vertical-align: top;
|
||||
|
||||
transition: all 0.5s;
|
||||
-webkit-transition: all 0.5s;
|
||||
-moz-transition: all 0.5s;
|
||||
}
|
||||
|
||||
.tree li::before,
|
||||
.tree li::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 50%;
|
||||
border-top: 1px solid #00ff00;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
}
|
||||
.tree li::after {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
border-left: 1px solid #00ff00;
|
||||
}
|
||||
|
||||
.tree li:only-child::after,
|
||||
.tree li:only-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree li:only-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.tree li:first-child::before,
|
||||
.tree li:last-child::after {
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.tree li:last-child::before {
|
||||
border-right: 1px solid #00ff00;
|
||||
border-radius: 0 5px 0 0;
|
||||
-webkit-border-radius: 0 5px 0 0;
|
||||
-moz-border-radius: 0 5px 0 0;
|
||||
}
|
||||
.tree li:first-child::after {
|
||||
border-radius: 5px 0 0 0;
|
||||
-webkit-border-radius: 5px 0 0 0;
|
||||
-moz-border-radius: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.tree ul ul::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border-left: 1px solid #00ff00;
|
||||
width: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.tree li a {
|
||||
border: 1px solid #00ff00;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
font-family: arial, verdana, tahoma;
|
||||
font-size: 11px;
|
||||
display: inline-block;
|
||||
|
||||
border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
|
||||
transition: all 0.5s;
|
||||
-webkit-transition: all 0.5s;
|
||||
-moz-transition: all 0.5s;
|
||||
}
|
||||
|
||||
a.discovered {
|
||||
background: #526b4e;
|
||||
color: #000;
|
||||
border: 1px solid #94a0b4;
|
||||
}
|
||||
|
||||
ul.discovered,
|
||||
li.discovered {
|
||||
border-color: #94a0b4;
|
||||
}
|
||||
|
||||
a.pwd {
|
||||
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import TreeChart from './TreeChart.vue'
|
||||
|
||||
const treeRef = ref<HTMLElement | null>(null)
|
||||
let pos = { top: 0, left: 0, x: 0, y: 0 }
|
||||
|
||||
function mouseMoveHandler(e: MouseEvent) {
|
||||
const dx = e.clientX - pos.x
|
||||
const dy = e.clientY - pos.y
|
||||
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.scrollTop = pos.top - dy
|
||||
treeRef.value.scrollLeft = pos.left - dx
|
||||
}
|
||||
|
||||
function mouseUpHandler() {
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.style.cursor = 'grab'
|
||||
treeRef.value.style.removeProperty('user-select')
|
||||
|
||||
document.removeEventListener('mousemove', mouseMoveHandler)
|
||||
document.removeEventListener('mouseup', mouseUpHandler)
|
||||
}
|
||||
|
||||
function mouseDownHandler(e: MouseEvent) {
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.style.cursor = 'grabbing'
|
||||
treeRef.value.style.userSelect = 'none'
|
||||
|
||||
pos = {
|
||||
left: treeRef.value.scrollLeft,
|
||||
top: treeRef.value.scrollTop,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', mouseMoveHandler)
|
||||
document.addEventListener('mouseup', mouseUpHandler)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.style.cursor = 'grab'
|
||||
treeRef.value.addEventListener('mousedown', mouseDownHandler)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
treeRef?.value?.removeEventListener('mousedown', mouseDownHandler)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="tree"
|
||||
ref="treeRef"
|
||||
class="h-full w-full overflow-auto cursor-grabbing"
|
||||
>
|
||||
<TreeChart />
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user