mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4f50ebbd | ||
|
|
5fd24190d2 | ||
|
|
d2bb079f50 | ||
|
|
d959ab22fe | ||
|
|
9c8260a411 | ||
|
|
3c92c9d143 | ||
|
|
5208d87bd6 | ||
|
|
634464065d | ||
|
|
4c90793fc6 | ||
|
|
169587c54d | ||
|
|
4dd5f94c60 | ||
|
|
14bcb3b0d5 | ||
|
|
8281a9d882 | ||
|
|
dcf72d046c | ||
|
|
b73607fb8d | ||
|
|
b64d707d16 | ||
|
|
7dcac306f9 | ||
|
|
eddbda23d7 | ||
|
|
124c5cc401 | ||
|
|
21d08b8156 | ||
|
|
3745011504 | ||
|
|
3f08452ea4 | ||
|
|
acfb09c6ca | ||
|
|
c2c100e6e6 | ||
|
|
4033eefa0d | ||
|
|
334b34ef65 | ||
|
|
ac89d521fc | ||
|
|
60641ba0ad | ||
|
|
dd67bcb1bd | ||
|
|
d002b21aa8 | ||
|
|
033c808c02 | ||
|
|
1be4f73e7e | ||
|
|
9ddfcd2a86 | ||
|
|
ca36f6391a | ||
|
|
1572b2ea06 | ||
|
|
8f6914d46c | ||
|
|
517ab43e8c | ||
|
|
74c49e1df7 | ||
|
|
cb0f3cc3b0 | ||
|
|
5605b27127 | ||
|
|
a7368110dd | ||
|
|
bdb7def73a | ||
|
|
b74866e50f | ||
|
|
d577656eb1 | ||
|
|
6400aa4a56 | ||
|
|
887d205ec1 | ||
|
|
8597c22b9f | ||
|
|
d8c0ee4b20 | ||
|
|
8180e9a0cf | ||
|
|
6daa11bff7 | ||
|
|
976dc6363c |
No files matched your search
+1
-1
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formbricks/js": "^2.1.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
@@ -22,7 +23,6 @@
|
|||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"dompurify": "^3.0.6",
|
"dompurify": "^3.0.6",
|
||||||
"echarts": "^5.4.3",
|
|
||||||
"marked": "^10.0.0",
|
"marked": "^10.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
|
|||||||
File renamed without changes.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -20,7 +20,21 @@ function nodeClass() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
|
<a href="#" :class="nodeClass()">
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="node.type === 'folder'"
|
||||||
|
:icon="['far', 'folder']"
|
||||||
|
class="text-text-primary size-8"
|
||||||
|
/>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-else
|
||||||
|
:icon="['far', 'file']"
|
||||||
|
class="text-text-primary size-8"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
{{ node.discovered ? node.name : '???' }}
|
||||||
|
{{ node.name === '' && node.discovered ? '/' : '' }}
|
||||||
|
</a>
|
||||||
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
|
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
|
||||||
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
|
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
|
||||||
<FileNode :node="child" :pwd="pwd" />
|
<FileNode :node="child" :pwd="pwd" />
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { ref, defineProps, onMounted } from 'vue'
|
||||||
|
import { getQuest } from '../utils/api'
|
||||||
|
import MarkdownText from './MarkdownText.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
questTitle: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
questId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
questUnlocked: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['closeIntro'])
|
||||||
|
const showIntro = ref(true) // Controls whether the modal is visible or not
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToast()
|
||||||
|
const { questTitle, questId, questUnlocked } = props
|
||||||
|
const questInstruction = ref('')
|
||||||
|
const questColor = ref('')
|
||||||
|
const questTextColor = ref('')
|
||||||
|
const closeModal = () => {
|
||||||
|
showIntro.value = false
|
||||||
|
emit('closeIntro', true)
|
||||||
|
router.push({ name: 'map' })
|
||||||
|
}
|
||||||
|
const handleQuests = (id: string) => {
|
||||||
|
if (questUnlocked) {
|
||||||
|
router.push({ name: 'game', params: { questId: id } })
|
||||||
|
} else {
|
||||||
|
toast.warning('你還沒完成前一個關卡!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleColour = () => {
|
||||||
|
if (questUnlocked) {
|
||||||
|
questColor.value = '#00ff00'
|
||||||
|
questTextColor.value = '#000000'
|
||||||
|
} else {
|
||||||
|
questColor.value = '#505050'
|
||||||
|
questTextColor.value = '#a0a0a0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(async () => {
|
||||||
|
const quest = await getQuest(questId)
|
||||||
|
questInstruction.value = quest.instruction
|
||||||
|
handleColour()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="showIntro" class="modal">
|
||||||
|
<div class="modal-content bg-bg-secondary rounded-lg">
|
||||||
|
<span class="close text-text content-between" @click="closeModal"
|
||||||
|
>×</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="QuestTitle"
|
||||||
|
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||||
|
>
|
||||||
|
{{ questTitle }}
|
||||||
|
</h2>
|
||||||
|
<div class="w-full h-3/5 p-8">
|
||||||
|
<div class="px-5 whitespace-pre-wrap">
|
||||||
|
<MarkdownText
|
||||||
|
class="text-text markdown-content"
|
||||||
|
:content="questInstruction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
id="StartAdventure"
|
||||||
|
target="_blank"
|
||||||
|
@click="handleQuests(questId)"
|
||||||
|
:style="{ backgroundColor: questColor, color: questTextColor }"
|
||||||
|
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full text-lg"
|
||||||
|
>開始冒險</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: 10% auto;
|
||||||
|
padding: 20px;
|
||||||
|
width: 60%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.markdown-content {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+232
-159
@@ -1,181 +1,254 @@
|
|||||||
<!-- eslint-disable no-console -->
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
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 { getQuests } from '../utils/api'
|
||||||
import { NodeImage } from '../img/svg.js'
|
|
||||||
import useUserProfile from '../store/userProfile'
|
import useUserProfile from '../store/userProfile'
|
||||||
|
import QuestIntro from './QuestIntro.vue'
|
||||||
|
|
||||||
const store = useUserProfile()
|
const store = useUserProfile()
|
||||||
const marginX = 500
|
|
||||||
const marginY = 300
|
|
||||||
const chartContainer = ref(null)
|
|
||||||
let chartInstance = null
|
|
||||||
const fullwidth = window.screen.width
|
|
||||||
|
|
||||||
use([
|
|
||||||
GraphChart,
|
|
||||||
SVGRenderer,
|
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
|
||||||
DataZoomComponent,
|
|
||||||
])
|
|
||||||
|
|
||||||
async function getQuests() {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/quests')
|
|
||||||
return res.data
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const genOption = (nodes: any, edges: any) => ({
|
|
||||||
renderer: 'svg',
|
|
||||||
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)
|
|
||||||
|
|
||||||
const nodes = dag.getNodes().map((node) => ({
|
|
||||||
id: node._id,
|
|
||||||
name: node.title,
|
|
||||||
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
|
||||||
x: marginY * node.layer * 2,
|
|
||||||
completed: progress[node._id]?.completed || false,
|
|
||||||
unlocked: node.requirements.every((req: any) => progress[req]?.completed),
|
|
||||||
}))
|
|
||||||
const lineAppearence = (node: any) => {
|
|
||||||
try {
|
|
||||||
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) => ({
|
|
||||||
source: edge[0],
|
|
||||||
target: edge[1],
|
|
||||||
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
|
|
||||||
}))
|
|
||||||
return genOption(nodes, edges)
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
function initChart(option: any) {
|
const svgWidth = ref(window.innerWidth)
|
||||||
chartInstance = init(chartContainer.value)
|
const svgHeight = ref(window.innerHeight)
|
||||||
chartInstance.setOption(option)
|
|
||||||
chartInstance.on('click', (params: any) => {
|
|
||||||
const {
|
|
||||||
data: { id, unlocked },
|
|
||||||
} = params
|
|
||||||
|
|
||||||
if (unlocked) {
|
const marginX = 120
|
||||||
router.push({ name: 'game', params: { questId: id } })
|
const marginY = 120
|
||||||
} else {
|
|
||||||
toast.warning('你還沒完成前一個關卡!')
|
const nodeWidth = 160
|
||||||
}
|
const nodeHeight = 50
|
||||||
})
|
const opened = ref<Node | null>(null)
|
||||||
|
type Node = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
index: number
|
||||||
|
completed: boolean
|
||||||
|
unlocked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Edge = {
|
||||||
|
source: Node
|
||||||
|
target: Node
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphData = ref<{ nodes: Node[]; edges: Edge[] }>({
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const dragStartX = ref(0)
|
||||||
|
const dragStartY = ref(0)
|
||||||
|
const offsetX = ref(0)
|
||||||
|
const offsetY = ref(0)
|
||||||
|
|
||||||
|
function startDrag(event: MouseEvent) {
|
||||||
|
isDragging.value = true
|
||||||
|
dragStartX.value = event.clientX - offsetX.value
|
||||||
|
dragStartY.value = event.clientY - offsetY.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(event: MouseEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
offsetX.value = event.clientX - dragStartX.value
|
||||||
|
offsetY.value = event.clientY - dragStartY.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDrag() {
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeGraphData() {
|
||||||
|
if (!store.progress) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const quests = await getQuests()
|
||||||
|
const dag = new DAG(quests)
|
||||||
|
const nodesValues = dag.getNodes()
|
||||||
|
|
||||||
|
const nodes = nodesValues.map((node) => ({
|
||||||
|
id: node._id,
|
||||||
|
title: node.title,
|
||||||
|
x: marginX * node.layer * 2,
|
||||||
|
y:
|
||||||
|
marginY * node.index -
|
||||||
|
(marginX * dag.getLayer(node._id)) / 2 +
|
||||||
|
svgHeight.value / 2,
|
||||||
|
completed: store.progress[node._id]?.completed || false,
|
||||||
|
unlocked: node.requirements.every(
|
||||||
|
(req: string) => store.progress[req]?.completed
|
||||||
|
),
|
||||||
|
index: node.index,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const edges = dag
|
||||||
|
.getEdgesArray()
|
||||||
|
.map((edge) => ({
|
||||||
|
source: nodes.find((n) => n.id === edge[0]),
|
||||||
|
target: nodes.find((n) => n.id === edge[1]),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
(edge): edge is Edge =>
|
||||||
|
edge.source !== undefined && edge.target !== undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
graphData.value = { nodes, edges }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error computing graph data:', error)
|
||||||
|
toast.error('Failed to load quest data')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleNodeClick(node: Node) {
|
||||||
|
opened.value = node
|
||||||
|
}
|
||||||
|
function closeIntro(close: boolean) {
|
||||||
|
if (close) {
|
||||||
|
opened.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.loadUserProfile()
|
await store.loadUserProfile()
|
||||||
const quests = await getQuests()
|
await computeGraphData()
|
||||||
const option = getOption(quests, store.progress)
|
})
|
||||||
initChart(option)
|
|
||||||
|
watch(() => store.progress, computeGraphData, { deep: true })
|
||||||
|
|
||||||
|
const curvedPath = computed(() => {
|
||||||
|
return (source: Node, target: Node) => {
|
||||||
|
// const dx = target.x - source.x
|
||||||
|
// const dy = target.y - source.y
|
||||||
|
// const dr = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
const dr = 0
|
||||||
|
const sweep = target.index > source.index ? 0 : 1 // Determines if the arc should curve up or down based on node index
|
||||||
|
return `M${source.x},${source.y}A${dr},${dr} 0 0,${sweep} ${target.x},${target.y}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeStyle = computed(() => {
|
||||||
|
return (node: Node) => {
|
||||||
|
if (node.completed) return 'completed'
|
||||||
|
if (node.unlocked) return 'unlocked'
|
||||||
|
return 'locked'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const edgeStyle = computed(() => {
|
||||||
|
return (edge: Edge) => {
|
||||||
|
return edge.target.unlocked ? 'unlocked' : 'locked'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative bg-black w-full h-full flex">
|
<div class="relative bg-black w-full h-full flex">
|
||||||
<img
|
<div
|
||||||
src="../img/catelogbg.svg"
|
class="flex place-content-center w-full bg-catelogbg bg-cover bg-scroll"
|
||||||
alt="bg"
|
|
||||||
class="p-2 stroke-2 scale-y-120 justify-center items-center"
|
|
||||||
/>
|
|
||||||
<h1
|
|
||||||
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
|
||||||
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
|
||||||
>
|
>
|
||||||
踏上你的Linux冒險之旅吧!
|
<h1
|
||||||
</h1>
|
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
||||||
<div class="flex flex-wrap absolute w-full h-full z-1">
|
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
||||||
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
|
>
|
||||||
|
踏上你的Linux冒險之旅吧!
|
||||||
|
</h1>
|
||||||
|
<svg
|
||||||
|
:width="svgWidth"
|
||||||
|
:height="svgHeight"
|
||||||
|
class="absolute"
|
||||||
|
@mousedown="startDrag"
|
||||||
|
@mousemove="drag"
|
||||||
|
@mouseup="endDrag"
|
||||||
|
@mouseleave="endDrag"
|
||||||
|
>
|
||||||
|
<g :transform="`translate(${offsetX}, ${offsetY})`">
|
||||||
|
<g
|
||||||
|
v-for="edge in graphData.edges"
|
||||||
|
:key="`${edge.source.id}-${edge.target.id}`"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:d="curvedPath(edge.source, edge.target)"
|
||||||
|
:class="['edge', edgeStyle(edge)]"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
v-for="node in graphData.nodes"
|
||||||
|
:key="node.id"
|
||||||
|
@click="handleNodeClick(node)"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
:x="node.x - nodeWidth / 2"
|
||||||
|
:y="node.y - nodeHeight / 2"
|
||||||
|
:width="nodeWidth"
|
||||||
|
:height="nodeHeight"
|
||||||
|
rx="10"
|
||||||
|
ry="10"
|
||||||
|
:class="['node', nodeStyle(node)]"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
:x="node.x"
|
||||||
|
:y="node.y"
|
||||||
|
text-anchor="middle"
|
||||||
|
alignment-baseline="middle"
|
||||||
|
:class="['node-text', nodeStyle(node)]"
|
||||||
|
font-size="18"
|
||||||
|
>
|
||||||
|
{{ node.title }}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<QuestIntro
|
||||||
|
:questTitle="opened.title"
|
||||||
|
:questId="opened.id"
|
||||||
|
:questUnlocked="opened.unlocked"
|
||||||
|
v-if="opened"
|
||||||
|
@close-intro="closeIntro"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
&.completed {
|
||||||
|
fill: #6cf76c;
|
||||||
|
}
|
||||||
|
&.unlocked {
|
||||||
|
fill: #8c8c92;
|
||||||
|
}
|
||||||
|
&.locked {
|
||||||
|
fill: #505050;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-text {
|
||||||
|
&.completed {
|
||||||
|
fill: #1d1d1d;
|
||||||
|
}
|
||||||
|
&.unlocked {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
&.locked {
|
||||||
|
fill: #a0a0a0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge {
|
||||||
|
stroke-width: 3;
|
||||||
|
fill: none;
|
||||||
|
&.unlocked {
|
||||||
|
stroke: #adadb5;
|
||||||
|
stroke-dasharray: none;
|
||||||
|
}
|
||||||
|
&.locked {
|
||||||
|
stroke: #454552;
|
||||||
|
stroke-dasharray: 5, 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,9 +9,11 @@ import CompleteAnimation from '../../img/complete-animation.json'
|
|||||||
const showModal = ref(true) // Controls whether the modal is visible or not
|
const showModal = ref(true) // Controls whether the modal is visible or not
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const defineComponent = () => ({
|
const defineComponent = () => ({
|
||||||
components: {
|
components: {
|
||||||
Vue3Lottie,
|
Vue3Lottie,
|
||||||
@@ -22,6 +24,7 @@ const defineComponent = () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const backtoMap = async () => {
|
const backtoMap = async () => {
|
||||||
try {
|
try {
|
||||||
router.push({ name: 'map' })
|
router.push({ name: 'map' })
|
||||||
@@ -67,6 +70,21 @@ setTimeout(() => {
|
|||||||
>回到地圖</a
|
>回到地圖</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="position: relative; height: 100vh; overflow: auto">
|
||||||
|
<iframe
|
||||||
|
src="https://app.formbricks.com/s/clyffdm0c0be2wh6335clle0l"
|
||||||
|
frameborder="0"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import useSession from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import { surveyUrl } from '../../config'
|
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const sessionStore = useSession()
|
const sessionStore = useSession()
|
||||||
@@ -21,31 +21,39 @@ const continuePlay = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section class="mt-2 flex flex-row gap-3 justify-center">
|
||||||
class="h-fit w-fit px-1 py-1 grid grid-cols-4 gap-2 place-content-between"
|
<button
|
||||||
>
|
id="survey"
|
||||||
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
class="bg-bg-secondary rounded-lg p-2"
|
||||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
@click="openQuestSurvey"
|
||||||
<font-awesome-icon
|
>
|
||||||
:icon="['fas', 'file-invoice']"
|
<font-awesome-icon
|
||||||
class="text-text-primary px-1"
|
:icon="['fas', 'file-invoice']"
|
||||||
/>
|
class="text-text-primary px-1"
|
||||||
<p class="text-text-primary inline ml-1 md:flex">問卷</p>
|
/>
|
||||||
</a>
|
<p class="text-text-primary inline md:flex">問卷</p>
|
||||||
</button>
|
</button>
|
||||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
<button
|
||||||
|
title="Bug Report"
|
||||||
|
@click="openBugReport"
|
||||||
|
class="bg-bg-secondary rounded-lg p-2"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||||
|
<p class="text-text-primary inline ml-1 md:flex">回報</p>
|
||||||
|
</button>
|
||||||
|
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['far', 'circle-question']"
|
:icon="['far', 'circle-question']"
|
||||||
class="text-text-disabled px-1"
|
class="text-text-disabled px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
||||||
</button>
|
</button> -->
|
||||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['fas', 'arrow-rotate-left']"
|
:icon="['fas', 'arrow-rotate-left']"
|
||||||
class="text-text-primary px-1"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1 hidden md:flex">重來</p>
|
<p class="text-text-primary inline ml-1 md:flex">重來</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="continue"
|
id="continue"
|
||||||
@@ -56,7 +64,7 @@ const continuePlay = async () => {
|
|||||||
:icon="['far', 'circle-right']"
|
:icon="['far', 'circle-right']"
|
||||||
class="text-text-primary px-1"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1 hidden md:flex">繼續</p>
|
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,14 +48,14 @@ const sessionStore = useSession()
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 50%;
|
right: 50%;
|
||||||
border-top: 1px solid #00ff00;
|
border-top: 2px solid #00ff00;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
.tree li::after {
|
.tree li::after {
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
border-left: 1px solid #00ff00;
|
border-left: 2px solid #00ff00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree li:only-child::after,
|
.tree li:only-child::after,
|
||||||
@@ -73,7 +73,7 @@ const sessionStore = useSession()
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree li:last-child::before {
|
.tree li:last-child::before {
|
||||||
border-right: 1px solid #00ff00;
|
border-right: 2px solid #00ff00;
|
||||||
border-radius: 0 5px 0 0;
|
border-radius: 0 5px 0 0;
|
||||||
-webkit-border-radius: 0 5px 0 0;
|
-webkit-border-radius: 0 5px 0 0;
|
||||||
-moz-border-radius: 0 5px 0 0;
|
-moz-border-radius: 0 5px 0 0;
|
||||||
@@ -89,7 +89,7 @@ const sessionStore = useSession()
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
border-left: 1px solid #00ff00;
|
border-left: 2px solid #00ff00;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ const sessionStore = useSession()
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-family: arial, verdana, tahoma;
|
font-family: arial, verdana, tahoma;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -112,18 +112,10 @@ const sessionStore = useSession()
|
|||||||
-moz-transition: all 0.5s;
|
-moz-transition: all 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.discovered {
|
.tree li a.pwd {
|
||||||
background: #526b4e;
|
border: 3px solid #00ff00;
|
||||||
color: #000;
|
background: #008080;
|
||||||
border: 1px solid #94a0b4;
|
font-weight: bold;
|
||||||
}
|
font-size: 13px;
|
||||||
|
|
||||||
ul.discovered,
|
|
||||||
li.discovered {
|
|
||||||
border-color: #94a0b4;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.pwd {
|
|
||||||
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,7 +3,8 @@ import { onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { logout } from '../../utils/auth'
|
import { logout } from '../../utils/auth'
|
||||||
import { bugReportUrl, surveyUrl } from '../../config'
|
import { surveyUrl } from '../../config'
|
||||||
|
import { openBugReport } from '../../utils/formbricks'
|
||||||
import useSession from '../../store/session'
|
import useSession from '../../store/session'
|
||||||
import useUserProfile from '../../store/userProfile'
|
import useUserProfile from '../../store/userProfile'
|
||||||
|
|
||||||
@@ -84,15 +85,14 @@ onMounted(async () => {
|
|||||||
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
|
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
|
||||||
<span class="text-text sm:hidden px-2">Map</span>
|
<span class="text-text sm:hidden px-2">Map</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<a
|
<button
|
||||||
title="Bug Report"
|
title="Bug Report"
|
||||||
:href="bugReportUrl"
|
@click="openBugReport"
|
||||||
target="_blank"
|
|
||||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||||
>
|
>
|
||||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||||
<span class="text-text sm:hidden px-2">Bug Report</span>
|
<span class="text-text sm:hidden px-2">Bug Report</span>
|
||||||
</a>
|
</button>
|
||||||
<button
|
<button
|
||||||
title="Sign Out"
|
title="Sign Out"
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
|
|||||||
+32
-28
@@ -3,28 +3,30 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||||||
|
|
||||||
/* import specific icons */
|
/* import specific icons */
|
||||||
import {
|
import {
|
||||||
faBars,
|
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faList,
|
|
||||||
faGear,
|
|
||||||
faTriangleExclamation,
|
|
||||||
faExpand,
|
|
||||||
faTerminal,
|
|
||||||
faArrowRotateLeft,
|
|
||||||
faArrowRightFromBracket,
|
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faArrowRightFromBracket,
|
||||||
|
faArrowRotateLeft,
|
||||||
|
faBars,
|
||||||
faBug,
|
faBug,
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
faMap,
|
|
||||||
faTrophy,
|
|
||||||
faFileInvoice,
|
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
|
faExpand,
|
||||||
|
faFileInvoice,
|
||||||
|
faGear,
|
||||||
|
faList,
|
||||||
|
faMap,
|
||||||
faPlay,
|
faPlay,
|
||||||
|
faTerminal,
|
||||||
|
faTriangleExclamation,
|
||||||
|
faTrophy,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import {
|
||||||
faLightbulb,
|
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faCircleRight,
|
faCircleRight,
|
||||||
|
faFile,
|
||||||
|
faFolder,
|
||||||
|
faLightbulb,
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
import {
|
import {
|
||||||
faFacebook,
|
faFacebook,
|
||||||
@@ -34,27 +36,29 @@ import {
|
|||||||
|
|
||||||
/* add icons to the library */
|
/* add icons to the library */
|
||||||
library.add(
|
library.add(
|
||||||
faBars,
|
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faList,
|
|
||||||
faGear,
|
|
||||||
faTriangleExclamation,
|
|
||||||
faExpand,
|
|
||||||
faTerminal,
|
|
||||||
faArrowRotateLeft,
|
|
||||||
faArrowRightFromBracket,
|
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faArrowRightFromBracket,
|
||||||
|
faArrowRotateLeft,
|
||||||
|
faBars,
|
||||||
faBug,
|
faBug,
|
||||||
faMap,
|
|
||||||
faTrophy,
|
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
faLightbulb,
|
faEnvelope,
|
||||||
|
faExpand,
|
||||||
|
faFileInvoice,
|
||||||
|
faGear,
|
||||||
|
faList,
|
||||||
|
faMap,
|
||||||
|
faPlay,
|
||||||
|
faTerminal,
|
||||||
|
faTriangleExclamation,
|
||||||
|
faTrophy,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faCircleRight,
|
faCircleRight,
|
||||||
faGithub,
|
faFile,
|
||||||
faGoogle,
|
faFolder,
|
||||||
faFileInvoice,
|
faLightbulb,
|
||||||
faFacebook,
|
faFacebook,
|
||||||
faEnvelope,
|
faGithub,
|
||||||
faPlay
|
faGoogle
|
||||||
)
|
)
|
||||||
+7
-1
@@ -10,9 +10,9 @@ import Toast, { POSITION } from 'vue-toastification'
|
|||||||
/* import Vue Lottie */
|
/* import Vue Lottie */
|
||||||
import Vue3Lottie from 'vue3-lottie'
|
import Vue3Lottie from 'vue3-lottie'
|
||||||
import 'vue-toastification/dist/index.css'
|
import 'vue-toastification/dist/index.css'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import formbricks from './utils/formbricks'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@@ -22,6 +22,12 @@ app.component('font-awesome-icon', FontAwesomeIcon)
|
|||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
if (typeof formbricks !== 'undefined') {
|
||||||
|
formbricks.registerRouteChange()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
position: POSITION.TOP_RIGHT,
|
position: POSITION.TOP_RIGHT,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,15 @@ const useSession = defineStore('session', {
|
|||||||
if (response.tasks) {
|
if (response.tasks) {
|
||||||
this.session.tasks = response.tasks
|
this.session.tasks = response.tasks
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
response.status === 'finished' &&
|
||||||
|
this.session.status !== 'finished'
|
||||||
|
) {
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
this.session.status = response.status
|
this.session.status = response.status
|
||||||
},
|
},
|
||||||
|
finish() {},
|
||||||
reset() {
|
reset() {
|
||||||
socket.reset()
|
socket.reset()
|
||||||
term.reset()
|
term.reset()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -43,3 +43,8 @@ export async function getActiveSession(
|
|||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getQuests(): Promise<IQuest[]> {
|
||||||
|
const res = await api.get<IQuest[]>('/quests')
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import formbricks from '@formbricks/js/website'
|
||||||
|
import useSession from '../store/session'
|
||||||
|
import useUserProfile from '../store/userProfile'
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
formbricks.init({
|
||||||
|
environmentId: 'clzcdqqn0000kadvtqykdo0pc',
|
||||||
|
apiHost: 'https://app.formbricks.com',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openQuestSurvey() {
|
||||||
|
const sessionStore = useSession()
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
|
||||||
|
formbricks.track('quest_completed', {
|
||||||
|
hiddenFields: {
|
||||||
|
quest: sessionStore.questId,
|
||||||
|
session: sessionStore.session?._id,
|
||||||
|
username: userStore.username,
|
||||||
|
email: userStore.email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openBugReport() {
|
||||||
|
const sessionStore = useSession()
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
|
||||||
|
formbricks.track('bug_report', {
|
||||||
|
hiddenFields: {
|
||||||
|
quest: sessionStore.questId,
|
||||||
|
session: sessionStore.session?._id,
|
||||||
|
username: userStore.username,
|
||||||
|
email: userStore.email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formbricks
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import useSession from '../store/session'
|
import useSession from '../store/session'
|
||||||
|
import useUserProfile from '../store/userProfile'
|
||||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||||
import DescriptionPart from '../components/game/DescriptionPart.vue'
|
import DescriptionPart from '../components/game/DescriptionPart.vue'
|
||||||
@@ -10,13 +11,22 @@ import VisualizationPart from '../components/game/VisualizationPart.vue'
|
|||||||
import ControlPalette from '../components/game/ControlPalette.vue'
|
import ControlPalette from '../components/game/ControlPalette.vue'
|
||||||
import CompleteModal from '../components/game/CompleteModal.vue'
|
import CompleteModal from '../components/game/CompleteModal.vue'
|
||||||
import StartButton from '../components/game/StartButton.vue'
|
import StartButton from '../components/game/StartButton.vue'
|
||||||
|
import { openQuestSurvey } from '../utils/formbricks'
|
||||||
|
|
||||||
const sessionStore = useSession()
|
const sessionStore = useSession()
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
|
||||||
const completed = computed(() => {
|
const completed = computed(() => {
|
||||||
return sessionStore.session?.status === 'finished'
|
return sessionStore.session?.status === 'finished'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(completed, (newValue, oldValue) => {
|
||||||
|
if (newValue && !oldValue) {
|
||||||
|
console.log('finish', userStore)
|
||||||
|
openQuestSurvey()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
questId: {
|
questId: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export default {
|
|||||||
DEFAULT: '#FF0000',
|
DEFAULT: '#FF0000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
catelogbg: "url('/catelogbg.svg')",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"latest": "^0.2.0",
|
||||||
"typescript-eslint": "^7.4.0"
|
"typescript-eslint": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,12 @@ interface Node {
|
|||||||
|
|
||||||
export default class DAG {
|
export default class DAG {
|
||||||
private nodes = new Map()
|
private nodes = new Map()
|
||||||
private edges = new Set()
|
private edges = new Set<[string, string]>()
|
||||||
private layers: number[] = []
|
private layers: number[] = []
|
||||||
|
|
||||||
constructor(nodes: Node[]) {
|
constructor(nodes: Node[]) {
|
||||||
this.nodes = new Map(nodes.map((node) => [node._id, node]))
|
this.nodes = new Map(nodes.map((node) => [node._id, node]))
|
||||||
this.edges = new Set()
|
this.edges = new Set<[string, string]>()
|
||||||
this.nodes.forEach((_, id) => {
|
this.nodes.forEach((_, id) => {
|
||||||
this.setLayer(id)
|
this.setLayer(id)
|
||||||
this.addEdges(id)
|
this.addEdges(id)
|
||||||
@@ -42,7 +42,7 @@ export default class DAG {
|
|||||||
if (!node) {
|
if (!node) {
|
||||||
throw new Error(`Node ${id} not found`)
|
throw new Error(`Node ${id} not found`)
|
||||||
}
|
}
|
||||||
return this.layers[node.layer - 1]
|
return this.layers[node.layer - 1] || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private setLayer(id: string) {
|
private setLayer(id: string) {
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
title: 輸出文字
|
title: 輸出文字
|
||||||
instruction: |
|
instruction: |
|
||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何使用 `echo` 指令
|
1. 如何使用 `echo` 在終端機上顯示文字。
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始
|
task: 按下 `開始` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM linuxodyssey/quest-base
|
||||||
|
|
||||||
|
COPY home ${HOME}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
echo start
|
||||||
|
echo next
|
||||||
|
ls
|
||||||
|
cd folder03
|
||||||
|
pwd
|
||||||
|
cd ../folder02
|
||||||
|
ls ../folder01
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
title: 測驗一
|
||||||
|
instruction: |
|
||||||
|
在這個關卡中,將會考驗你是否理解以下內容:
|
||||||
|
1. `echo`:用來輸出文字
|
||||||
|
2. `ls`:用來查看資料夾的內容
|
||||||
|
3. `cd`:用來在資料夾之間移動
|
||||||
|
4. `pwd`:用來顯示目前的路徑
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
- path
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- id: start
|
||||||
|
task: 按下 `開始` 按鈕開始
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^echo start$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
這個關卡將會測試你是否理解之前的教學內容。
|
||||||
|
因此不會告訴你要輸入什麼指令。
|
||||||
|
請思考並應用正確的指令來完成任務。
|
||||||
|
|
||||||
|
現在,請讓終端機輸出 `next` 來繼續。
|
||||||
|
|
||||||
|
- id: next
|
||||||
|
task: 讓終端機輸出 `next`
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
condition:
|
||||||
|
output:
|
||||||
|
- ^next$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
現在請你查看目前資料夾的內容。
|
||||||
|
|
||||||
|
- id: ls-~
|
||||||
|
task: 查看目前資料夾的內容
|
||||||
|
requirements:
|
||||||
|
- next
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
請進入 `folder03` 資料夾。
|
||||||
|
|
||||||
|
- id: cd-folder03
|
||||||
|
task: 進入 `folder03` 資料夾
|
||||||
|
requirements:
|
||||||
|
- ls-~
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander/folder03$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
請在終端機顯示目前的路徑。
|
||||||
|
|
||||||
|
- id: pwd-folder03
|
||||||
|
task: 顯示目前的路徑
|
||||||
|
requirements:
|
||||||
|
- cd-folder03
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^pwd$
|
||||||
|
output:
|
||||||
|
- ^/home/commander/folder03$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
現在請你進入 `folder02` 資料夾,
|
||||||
|
並查看 `folder02` 資料夾的內容。
|
||||||
|
|
||||||
|
- id: ls-folder02
|
||||||
|
task: 進入並查看 `folder02` 資料夾的內容
|
||||||
|
requirements:
|
||||||
|
- pwd-folder03
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander/folder02$
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
最後,請你留在 `folder02` 資料夾,
|
||||||
|
並查看 `folder01` 資料夾的內容。
|
||||||
|
|
||||||
|
- id: END
|
||||||
|
requirements:
|
||||||
|
- ls-folder02
|
||||||
|
task: 留在 `folder02` 資料夾,查看 `folder01` 資料夾的內容。
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander/folder02$
|
||||||
|
command:
|
||||||
|
- ^ls
|
||||||
|
output:
|
||||||
|
- file1.txt
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
恭喜你通過了這個測驗!
|
||||||
|
你可以繼續學習之後的教學內容了。
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
|||||||
|
FROM linuxodyssey/quest-base
|
||||||
|
|
||||||
|
COPY home ${HOME}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
echo start
|
||||||
|
echo next
|
||||||
|
ls
|
||||||
|
cat hello.txt
|
||||||
|
cat pneumonoultramicroscopicsilicovolcanoconiosis.txt
|
||||||
|
mkdir folder
|
||||||
|
touch folder/file.txt
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
title: 測驗二
|
||||||
|
instruction: |
|
||||||
|
在這個關卡中,將會考驗你是否理解以下內容:
|
||||||
|
1. `cat`:用來查看檔案的內容
|
||||||
|
2. `touch`:用來建立檔案
|
||||||
|
3. `mkdir`:用來建立資料夾
|
||||||
|
4. 用 `tab` 鍵來自動補全
|
||||||
|
5. 之前測驗包含的內容
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
- create
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- id: start
|
||||||
|
task: 按下 `開始` 按鈕開始
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^echo start$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
這個關卡將會測試你是否理解之前的教學內容。
|
||||||
|
因此不會告訴你要輸入什麼指令。
|
||||||
|
請思考並應用正確的指令來完成任務。
|
||||||
|
|
||||||
|
現在,請讓終端機輸出 `next` 來繼續。
|
||||||
|
|
||||||
|
- id: next
|
||||||
|
task: 讓終端機輸出 `next`
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
condition:
|
||||||
|
output:
|
||||||
|
- ^next$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
現在請你查看目前資料夾的內容。
|
||||||
|
|
||||||
|
- id: ls-~
|
||||||
|
task: 查看目前資料夾的內容
|
||||||
|
requirements:
|
||||||
|
- next
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
請查看 `hello.txt` 檔案的內容。
|
||||||
|
|
||||||
|
- id: cat-hello
|
||||||
|
task: 查看 `hello.txt` 檔案內容
|
||||||
|
requirements:
|
||||||
|
- ls-~
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^cat hello.txt$
|
||||||
|
output:
|
||||||
|
- ^This is `hello.txt`.$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
請查看另一個有著很長檔名的檔案內容。
|
||||||
|
|
||||||
|
- id: cat-long-filename
|
||||||
|
task: 查看另一個檔案的內容
|
||||||
|
requirements:
|
||||||
|
- cat-hello
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^cat pneumonoultramicroscopicsilicovolcanoconiosis.txt$
|
||||||
|
output:
|
||||||
|
- ^A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
現在請你建立一個名稱為 `folder` 的資料夾。
|
||||||
|
|
||||||
|
- id: mkdir-folder
|
||||||
|
task: 建立名稱為 `folder` 的資料夾
|
||||||
|
requirements:
|
||||||
|
- cat-long-filename
|
||||||
|
condition:
|
||||||
|
files:
|
||||||
|
- path: /home/commander/folder
|
||||||
|
type: folder
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
最後,請你在 `folder` 資料夾中,
|
||||||
|
建立檔名為 `file.txt` 的檔案。
|
||||||
|
|
||||||
|
- id: END
|
||||||
|
requirements:
|
||||||
|
- mkdir-folder
|
||||||
|
task: 在 `folder` 中建立檔名為 `file.txt` 的檔案
|
||||||
|
condition:
|
||||||
|
files:
|
||||||
|
- path: /home/commander/folder/file.txt
|
||||||
|
type: file
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
恭喜你通過了這個測驗!
|
||||||
|
你可以繼續學習之後的教學內容了。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
This is `hello.txt`.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM linuxodyssey/quest-base
|
||||||
|
|
||||||
|
USER root
|
||||||
|
COPY home .
|
||||||
|
RUN chown -R ${USERNAME}:${USERNAME} .
|
||||||
|
|
||||||
|
USER ${USERNAME}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
echo start
|
||||||
|
echo next
|
||||||
|
ls
|
||||||
|
cd folder
|
||||||
|
cp hello.txt world.txt
|
||||||
|
rm hello.txt
|
||||||
|
mv world.txt ..
|
||||||
|
cd ..
|
||||||
|
mv world.txt hello.txt
|
||||||
|
cat hello.txt
|
||||||
|
rmdir folder
|
||||||
|
rm -r trash_can
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
title: 測驗三
|
||||||
|
instruction: |
|
||||||
|
在這個關卡中,將會考驗你是否理解以下內容:
|
||||||
|
1. `cp`:用來複製檔案和資料夾
|
||||||
|
2. `mv`:用來移動檔案和資料夾或更改名稱
|
||||||
|
3. `rm`:用來刪除檔案和資料夾
|
||||||
|
4. `rmdir`:用來刪除空的資料夾
|
||||||
|
5. 透過選項讓指令達成不同的功能,例如 `rm -r`
|
||||||
|
6. 之前測驗包含的內容
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
- remove
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- id: start
|
||||||
|
task: 按下 `開始` 按鈕開始
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^echo start$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
這個關卡將會測試你是否理解之前的教學內容。
|
||||||
|
因此不會告訴你要輸入什麼指令。
|
||||||
|
請思考並應用正確的指令來完成任務。
|
||||||
|
|
||||||
|
現在,請讓終端機輸出 `next` 來繼續。
|
||||||
|
|
||||||
|
- id: next
|
||||||
|
task: 讓終端機輸出 `next`
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
condition:
|
||||||
|
output:
|
||||||
|
- ^next$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
現在請你查看目前資料夾的內容。
|
||||||
|
|
||||||
|
- id: ls-~
|
||||||
|
task: 查看目前資料夾的內容
|
||||||
|
requirements:
|
||||||
|
- next
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
請複製 `folder` 資料夾中的檔案到 `folder` 中,並命名為 `world.txt` 。
|
||||||
|
|
||||||
|
- id: cp-hello-world
|
||||||
|
task: 複製 `folder` 中的檔案並命名為 `world.txt`
|
||||||
|
requirements:
|
||||||
|
- ls-~
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^cp .*hello.txt .*world.txt$
|
||||||
|
files:
|
||||||
|
- path: /home/commander/folder/hello.txt
|
||||||
|
type: file
|
||||||
|
- path: /home/commander/folder/world.txt
|
||||||
|
type: file
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
請刪除 `hello.txt` 檔案。
|
||||||
|
|
||||||
|
- id: rm-hello
|
||||||
|
task: 刪除 `hello.txt`
|
||||||
|
requirements:
|
||||||
|
- cp-hello-world
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^rm .*hello.txt$
|
||||||
|
files:
|
||||||
|
- path: /home/commander/folder/hello.txt
|
||||||
|
type: file
|
||||||
|
exists: false
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
現在請將 `world.txt` 移出 `folder` 資料夾。
|
||||||
|
(移到 `commander` 資料夾中)
|
||||||
|
|
||||||
|
- id: mv-world-..
|
||||||
|
task: 將 `world.txt` 移出 `folder` 資料夾
|
||||||
|
requirements:
|
||||||
|
- rm-hello
|
||||||
|
condition:
|
||||||
|
files:
|
||||||
|
- path: /home/commander/folder/world.txt
|
||||||
|
type: file
|
||||||
|
exists: false
|
||||||
|
- path: /home/commander/world.txt
|
||||||
|
type: file
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
將 `world.txt` 的名稱改回 `hello.txt` ,
|
||||||
|
並查看檔案的內容。
|
||||||
|
|
||||||
|
- id: cat-hello
|
||||||
|
task: 將 `world.txt` 的名稱改回 `hello.txt` 後查看檔案內容
|
||||||
|
requirements:
|
||||||
|
- mv-world-..
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^cat hello.txt$
|
||||||
|
files:
|
||||||
|
- path: /home/commander/world.txt
|
||||||
|
type: file
|
||||||
|
exists: false
|
||||||
|
- path: /home/commander/hello.txt
|
||||||
|
type: file
|
||||||
|
output:
|
||||||
|
- ^This is `hello.txt`.$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
刪除 `folder` 資料夾。
|
||||||
|
|
||||||
|
- id: rmdir-folder
|
||||||
|
task: 刪除 `folder` 資料夾
|
||||||
|
requirements:
|
||||||
|
- cat-hello
|
||||||
|
condition:
|
||||||
|
files:
|
||||||
|
- path: /home/commander/folder
|
||||||
|
type: folder
|
||||||
|
exists: false
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
刪除 `trash_can` 資料夾。
|
||||||
|
|
||||||
|
- id: END
|
||||||
|
requirements:
|
||||||
|
- rmdir-folder
|
||||||
|
task: 刪除 `trash_can` 資料夾
|
||||||
|
condition:
|
||||||
|
files:
|
||||||
|
- path: /home/commander/trash_can
|
||||||
|
type: folder
|
||||||
|
exists: false
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
恭喜你通過了這個測驗!
|
||||||
|
你可以繼續學習之後的教學內容了。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
This is `hello.txt`.
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1 @@
|
|||||||
|
aHR0cHM6Ly9yZXVybC5jYy9YUjZZMDA=
|
||||||
Whitespace-only changes.
@@ -2,7 +2,7 @@ echo start
|
|||||||
ls
|
ls
|
||||||
cat hello.txt
|
cat hello.txt
|
||||||
cat world.txt
|
cat world.txt
|
||||||
echo continue
|
echo next
|
||||||
cp hello.txt hello2.txt
|
cp hello.txt hello2.txt
|
||||||
cat hello2.txt
|
cat hello2.txt
|
||||||
cp world.txt world2.txt
|
cp world.txt world2.txt
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
title: 複製檔案
|
title: 複製檔案
|
||||||
instruction: |
|
instruction: |
|
||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何用 `cp` 指令複製檔案
|
1. 如何使用 `cp` 複製檔案
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始
|
task: 按下 `開始` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
@@ -46,7 +44,8 @@ stages:
|
|||||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content: 可以看到現在資料夾裡有 `hello.txt` 跟 `world.txt`。
|
content: |
|
||||||
|
可以看到現在資料夾裡有 `hello.txt` 跟 `world.txt`。
|
||||||
請使用 `cat` 來查看它們的內容。
|
請使用 `cat` 來查看它們的內容。
|
||||||
看完之後,使用 `echo next` 來繼續吧!
|
看完之後,使用 `echo next` 來繼續吧!
|
||||||
hints:
|
hints:
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ instruction: |
|
|||||||
1. 如何使用 `touch` 建立檔案
|
1. 如何使用 `touch` 建立檔案
|
||||||
2. 如何使用 `mkdir` 建立資料夾
|
2. 如何使用 `mkdir` 建立資料夾
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- read
|
- tab
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始
|
task: 按下 `開始` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM linuxodyssey/quest-base
|
||||||
|
|
||||||
|
USER root
|
||||||
|
COPY home ${HOME}
|
||||||
|
|
||||||
|
USER commander
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 執行檔案
|
||||||
|
|
||||||
|
## Outline
|
||||||
|
|
||||||
|
1. 什麼是可執行檔
|
||||||
|
2. 使用 `which` 指令查看執行檔的位置
|
||||||
|
3. 如何執行可執行檔
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
在我們先前的課程裡,已經使用了許多不同的指令,像是 `ls`, `cat`, `cp`, `mv`, `rm` 等。
|
||||||
|
這些指令,其實都是一個**可執行檔**,在你打指令的時候被作業系統執行。
|
||||||
|
所以大部分的指令,我們其實都能找到它在檔案系統裡的原始位置。
|
||||||
|
要查看一個指令它的檔案位置,可以使用 `which` 指令。
|
||||||
|
輸入 `which [指令名稱]` 就能夠查看指令的檔案位置了。
|
||||||
|
|
||||||
|
> 使用 `which ls` 來查看 `ls` 的路徑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到終端機顯示出 `/bin/ls` 這個路徑,它其實就是 `ls` 指令它檔案存在的位置。
|
||||||
|
`/bin` 是一個系統資料夾,裡面會儲存許多系統內建的程式,像是 `cat`, `cp` 等指令也都被存在這裡。
|
||||||
|
|
||||||
|
> 使用 `which cat` 查看 `cat` 的路徑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
我們要執行一個可執行檔時,只要直接輸入它的路徑就可以執行它。
|
||||||
|
我們來試看看使用執行檔的路徑來執行指令。
|
||||||
|
|
||||||
|
> 輸入 `/bin/ls` 來查看當前的資料夾
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你會看到它列出了我們資料夾內的檔案,你可以自己比較看看,跟單純打 `ls` 的結果會一模一樣。
|
||||||
|
它一樣可以使用參數。我們來用 `cat` 試試看。
|
||||||
|
|
||||||
|
> 輸入 `/bin/cat hello.txt` 來顯示 `hello.txt` 的內容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你會看到了 `hello.txt` 的內容,效果跟單純用 `cat` 一模一樣。
|
||||||
|
事實上,這是必然的,因為 `cat` 的檔案位置就是 `/bin/cat`,我們執行了同一個程式,當然會有同樣的結果。
|
||||||
|
其實指令,就是由 **一個執行檔** + **參數** 所構成。
|
||||||
|
我們之所以不用打 `/bin/ls`,而是只要打 `ls` 就能執行,是因為有 `PATH` 這個環境變數,幫我們預設好了終端機要去哪些*資料夾*找檔案。
|
||||||
|
但因為這超出了本次課程的範圍,關於 `PATH` 以及環境變數的議題,我們會在未來的課程再討論。
|
||||||
|
|
||||||
|
我們也可以執行不是內建的指令,像是現在資料夾中的 `dog` 檔案。
|
||||||
|
|
||||||
|
> 使用 `cat dog` 來查看 `dog` 的內容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你會看到這是一個用 `echo` 和 `cat` 所組成的簡單程式。
|
||||||
|
詳細的內容不用了解沒關係,功能是和 `cat` 一樣,用來印出檔案內容的程式。
|
||||||
|
但是這支程式,在沒有給要印出的檔案時,不會停留在輸入狀態。
|
||||||
|
|
||||||
|
當我們要執行一個檔案時,就是用指定路徑的方式。
|
||||||
|
那因為 `dog` 位在家目錄底下,所以牠的絕對路徑就是 `/home/commander/dog`,我們也可以用簡寫 `~/dog`。
|
||||||
|
|
||||||
|
> 輸入 `~/dog` 來執行 `dog` 檔案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了終端機輸出這隻傲嬌的狗。
|
||||||
|
除了使用絕對路徑,很多時候,我們會處在跟程式相同的資料夾,這時使用相對路徑會更為便利。
|
||||||
|
但跟平常列出檔案時不一樣,我們為了區別它不是內建的指令,需要加上 `./` 在檔名前面。
|
||||||
|
`.` 在路徑中,代表的就是當前的資料夾。
|
||||||
|
那我們再試試看使用相對路徑,並讓牠接收一個檔案作為參數。
|
||||||
|
|
||||||
|
> 輸入 `./dog hello.txt` 來印出 `hello.txt` 的內容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到這隻狗在講了幾句話後也印出了 `hello.txt` 的內容了。
|
||||||
|
這個範例,其實就是一個簡單的自訂指令。
|
||||||
|
我們可以把一大串指令,寫在一個檔案裡讓它自動執行,幫我們處理一些複雜或是重複性的工作。
|
||||||
|
我們在使用別人寫的程式時,也常會用到這個技巧。
|
||||||
|
|
||||||
|
最後留一個小挑戰給你,請試著移動到 `commands` 資料夾後,再使用相對路徑執行裡面的 `lucky` 檔案。
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
title: 執行檔案
|
||||||
|
instruction: |
|
||||||
|
在這個關卡中,你將會學到:
|
||||||
|
1. 什麼是可執行檔
|
||||||
|
2. 使用 `which` 指令查看執行檔的位置
|
||||||
|
3. 如何執行可執行檔
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
- remove
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- id: start
|
||||||
|
task: 按下開始按鈕
|
||||||
|
type: start
|
||||||
|
condition:
|
||||||
|
command: ^echo start$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
在我們先前的課程裡,已經使用了許多不同的指令,像是 `ls`, `cat`, `cp`, `mv`, `rm` 等。
|
||||||
|
這些指令,其實都是一個**可執行檔**,在你打指令的時候被作業系統執行。
|
||||||
|
所以大部分的指令,我們其實都能找到它在檔案系統裡的原始位置。
|
||||||
|
要查看一個指令它的檔案位置,可以使用 `which` 指令。
|
||||||
|
輸入 `which [指令名稱]` 就能夠查看指令的檔案位置了。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `which ls` 來查看 `ls` 的路徑
|
||||||
|
|
||||||
|
- id: which-ls
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
task: 使用 `which` 指令查看 `ls` 的路徑
|
||||||
|
condition:
|
||||||
|
command: ^which ls$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到終端機顯示出 `/bin/ls` 這個路徑,它其實就是 `ls` 指令它檔案存在的位置。
|
||||||
|
`/bin` 是一個系統資料夾,裡面會儲存許多系統內建的程式,像是 `cat`, `cp` 等指令也都被存在這裡。
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `which cat` 查看 `cat` 的路徑
|
||||||
|
|
||||||
|
- id: which-cat
|
||||||
|
requirements:
|
||||||
|
- which-ls
|
||||||
|
task: 使用 `which` 指令查看 `cat` 的路徑
|
||||||
|
condition:
|
||||||
|
command: ^which cat$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了 `/bin/cat` 這個路徑,它就是 `cat` 指令它檔案存在的位置。
|
||||||
|
我們要執行一個可執行檔時,只要 **直接輸入它的路徑** 就可以執行它。
|
||||||
|
我們來試看看使用執行檔的路徑來執行指令。
|
||||||
|
|
||||||
|
在此之前,我們先試試看一般執行 `ls` 指令的結果。
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `ls` 來查看當前的資料夾
|
||||||
|
|
||||||
|
- id: ls
|
||||||
|
requirements:
|
||||||
|
- which-cat
|
||||||
|
task: 使用 `ls` 指令查看當前的資料夾
|
||||||
|
condition:
|
||||||
|
command: ^ls$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了當前的資料夾內有 `commands` 資料夾和 `dog`, `hello.txt` 這兩個檔案。
|
||||||
|
我們再來試看看用 `ls` 的路徑來執行它。
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `/bin/ls` 來查看當前的資料夾
|
||||||
|
|
||||||
|
- id: bin-ls
|
||||||
|
requirements:
|
||||||
|
- ls
|
||||||
|
task: 使用執行檔的絕對路徑來執行 `ls`
|
||||||
|
condition:
|
||||||
|
command: ^/bin/ls$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你會看到它列出了我們資料夾內的檔案,你會發現它的輸出跟單純打 `ls` 的結果一模一樣。
|
||||||
|
它一樣可以使用參數。我們來用 `cat` 試試看。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `/bin/cat hello.txt` 來顯示 `hello.txt` 的內容
|
||||||
|
|
||||||
|
- id: bin-cat
|
||||||
|
requirements:
|
||||||
|
- bin-ls
|
||||||
|
task: 使用執行檔的絕對路徑來執行 `cat`
|
||||||
|
condition:
|
||||||
|
command: ^/bin/cat hello.txt$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了 `hello.txt` 的內容,跟單純使用 `cat` 一模一樣。
|
||||||
|
事實上,這是必然的,因為 `cat` 的檔案位置就是 `/bin/cat`,我們執行了同一個程式,當然會有同樣的結果。
|
||||||
|
|
||||||
|
因為一個指令,就是由 `執行檔 參數1 參數2 ...` 所構成。
|
||||||
|
`執行檔` 就是我們要執行的程式,像是 `ls`, `cat`, `which` 等。
|
||||||
|
`參數` 則是我們要提供給程式的資訊,像是接在 `cat` 後面的 `hello.txt`。
|
||||||
|
|
||||||
|
我們之所以不用打 `/bin/ls`,而是只要打 `ls` 就能執行,是因為有 `PATH` 這個環境變數,幫我們預設好了終端機要去哪些*資料夾*找檔案。
|
||||||
|
但因為這超出了本次課程的範圍,關於 `PATH` 以及環境變數的議題,我們會在未來的課程再討論。
|
||||||
|
|
||||||
|
接著我們來試著執行不是內建的指令。
|
||||||
|
現在資料夾中的 `dog` 檔案,是一個在這門課程所提供的自訂指令。
|
||||||
|
我們先來看看它是怎麼寫的。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `cat dog` 來查看 `dog` 的內容
|
||||||
|
|
||||||
|
- id: cat-dog
|
||||||
|
requirements:
|
||||||
|
- bin-cat
|
||||||
|
task: 使用 `cat` 指令查看 `dog` 的內容
|
||||||
|
condition:
|
||||||
|
command: ^cat dog$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了這是一個用 `echo` 和 `sleep` 所組成的簡單程式。
|
||||||
|
詳細的內容不用了解沒關係,只是輸出一些文字而已。
|
||||||
|
|
||||||
|
當我們要執行一個檔案時,就是用指定路徑的方式。
|
||||||
|
也就是說 `執行檔` 就是 `dog` 的路徑,`參數` 在這個範例中不會使用到。
|
||||||
|
那因為 `dog` 位在家目錄底下,所以牠的絕對路徑就是 `/home/commander/dog`。
|
||||||
|
那在 `執行檔 參數` 這個格式下,我們要打的指令就會是
|
||||||
|
```
|
||||||
|
/home/commander/dog
|
||||||
|
```
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `/home/commander/dog` 來執行 `dog` 檔案
|
||||||
|
|
||||||
|
- id: dog-abs
|
||||||
|
requirements:
|
||||||
|
- cat-dog
|
||||||
|
task: 使用絕對路徑來執行 `dog`
|
||||||
|
condition:
|
||||||
|
or:
|
||||||
|
- command: ^/home/commander/dog$
|
||||||
|
- command: ^~/dog$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了終端機輸出這隻傲嬌的狗。
|
||||||
|
除了使用絕對路徑,很多時候,我們會處在跟程式相同的資料夾,這時使用相對路徑會更為便利。
|
||||||
|
但跟平常列出檔案時不一樣,我們為了區別它不是內建的指令,需要加上 `./` 在檔名前面。
|
||||||
|
`.` 在路徑中,代表的就是當前的資料夾。
|
||||||
|
那我們再試試看使用相對路徑來執行 `dog` 檔案。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `./dog` 來執行 `dog` 檔案
|
||||||
|
|
||||||
|
- id: dog-rel
|
||||||
|
requirements:
|
||||||
|
- dog-abs
|
||||||
|
task: 使用相對路徑來執行 `dog`,並加上參數
|
||||||
|
condition:
|
||||||
|
pwd: /home/commander
|
||||||
|
command: ^./dog$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到這隻狗執行了跟剛才一樣的結果。
|
||||||
|
這個範例,其實就是一個簡單的自訂指令。
|
||||||
|
我們可以把一大串指令,寫在一個檔案裡讓它自動執行,幫我們處理一些複雜或是重複性的工作。
|
||||||
|
我們在使用別人寫的程式時,也常會用到這個技巧。
|
||||||
|
|
||||||
|
最後留一個小挑戰給你,請試著先進入 `commands` 資料夾後,再使用相對路徑執行裡面的 `lucky` 檔案。
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `cd` 來進入資料夾
|
||||||
|
- ➤ 使用 `ls` 查看檔案
|
||||||
|
- ➤ 輸入 `./檔案名稱` 來執行檔案
|
||||||
|
|
||||||
|
- id: END
|
||||||
|
requirements:
|
||||||
|
- dog-rel
|
||||||
|
task: 使用相對路徑來執行 `lucky`
|
||||||
|
condition:
|
||||||
|
pwd: /home/commander/commands
|
||||||
|
command: ^./lucky$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了終端機輸出 `You are lucky!` 這個字串。
|
||||||
|
恭喜你,你已經完成了這個課程!
|
||||||
|
exceptions:
|
||||||
|
condition:
|
||||||
|
command: ^echo help$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你需要先移動到 `commands` 資料夾,再使用相對路徑執行 `lucky` 檔案。
|
||||||
|
請先輸入 `cd commands` 來移動到 `commands` 資料夾,再使用相對路徑 `./lucky` 來執行 `lucky` 檔案。
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "I am SO LUCKY!!!"
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "你執行了 dog 檔案"
|
||||||
|
echo "汪汪汪"
|
||||||
|
sleep 1
|
||||||
|
echo "你知道嗎?"
|
||||||
|
sleep 1
|
||||||
|
echo "我比貓還厲害"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Hello! This is a file hello.txt
|
||||||
@@ -4,14 +4,12 @@ instruction: |
|
|||||||
1. 如何使用 `ls` 查看資料夾的內容
|
1. 如何使用 `ls` 查看資料夾的內容
|
||||||
2. 如何使用 `cd` 在資料夾之間移動
|
2. 如何使用 `cd` 在資料夾之間移動
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始教程
|
task: 按下 `開始` 按鈕開始教程
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ instruction: |
|
|||||||
1. 如何使用這個網站
|
1. 如何使用這個網站
|
||||||
2. 如何使用終端機
|
2. 如何使用終端機
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始
|
task: 按下 `開始` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
echo start
|
echo start
|
||||||
echo continue
|
echo next
|
||||||
mv hello.txt notes
|
mv hello.txt notes
|
||||||
ls
|
ls
|
||||||
ls notes
|
ls notes
|
||||||
echo continue
|
echo next
|
||||||
mv world.txt wow.txt
|
mv world.txt wow.txt
|
||||||
ls
|
ls
|
||||||
mv notes docs
|
mv notes docs
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
title: 移動檔案
|
title: 移動檔案
|
||||||
instruction: |
|
instruction: |
|
||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何用 `mv` 移動檔案與資料夾
|
1. 如何使用 `mv` 移動檔案與資料夾
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始
|
task: 按下 `開始` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ instruction: |
|
|||||||
1. 絕對路徑和相對路徑
|
1. 絕對路徑和相對路徑
|
||||||
2. 如何使用 `pwd` 顯示目前的路徑
|
2. 如何使用 `pwd` 顯示目前的路徑
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- cast
|
- cast
|
||||||
- explore
|
- explore
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始教程
|
task: 按下 `開始` 按鈕開始教程
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ instruction: |
|
|||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何使用 `cat` 查看檔案的內容
|
1. 如何使用 `cat` 查看檔案的內容
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- path
|
- path
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始教程
|
task: 按下 `開始` 按鈕開始教程
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
title: 刪除檔案與資料夾
|
title: 刪除檔案與資料夾
|
||||||
instruction: |
|
instruction: |
|
||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 用 `rm` 指令刪除檔案
|
1. 如何使用 `rm` 指令刪除檔案
|
||||||
2. 用 `rmdir` 與資料夾
|
2. 如何使用 `rmdir` 刪除空資料夾
|
||||||
3. 用 `rm -r` 刪除資料夾與它的所有檔案
|
3. 如何使用 `rm -r` 刪除資料夾與其中的所有檔案
|
||||||
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
|
|
||||||
|
|
||||||
按下中間的 `start` 按鈕來開始這個關卡。
|
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- copy
|
- copy
|
||||||
@@ -14,7 +11,7 @@ requirements:
|
|||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 按下 `start` 按鈕開始
|
task: 按下 `開始` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM linuxodyssey/quest-base
|
||||||
|
|
||||||
|
COPY home ${HOME}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
title: Tab 自動補全
|
||||||
|
instruction: |
|
||||||
|
在這個關卡,你將會學到
|
||||||
|
|
||||||
|
1. 如何使用 Tab 自動補全路徑
|
||||||
|
2. 出現衝突時如何處理
|
||||||
|
|
||||||
|
按下中間的按鈕來開始這個關卡。
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
- read
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- id: start
|
||||||
|
task: 按下開始按鈕開始關卡
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^echo start$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
在終端機裡,所有的操作都是用文字進行。
|
||||||
|
然而,打字很花時間,而且只要一個字打錯,指令就會執行失敗。
|
||||||
|
因些終端機提供「自動補全」的功能,自動幫你完成檔案路徑、指令名稱、參數名稱,節省你的時間,也避免輸入錯誤。
|
||||||
|
|
||||||
|
在開始練習之前,我們先來看看家目錄下有哪些檔案。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `ls` 查看檔案列表
|
||||||
|
|
||||||
|
- id: ls
|
||||||
|
task: 使用 `ls` 查看檔案列表
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了這個資料夾中充滿了超級長和有錯字的檔案名稱,如果這些都要我們一個一個字輸入,肯定會很痛苦。
|
||||||
|
但不要緊,只要使用自動補全,檔名不論多長都能輕鬆輸入。
|
||||||
|
|
||||||
|
使用自動補全的方法是,**先輸入部分的檔名,再按下 Tab**
|
||||||
|
(Tab 是 Q 左邊的按鍵)
|
||||||
|
例如要補全 `SuperSecretPlansForWorldDomination.txt`,你可以先輸入 `Su` 再按下 Tab,終端機就會幫你打完剩下的檔名了。
|
||||||
|
|
||||||
|
我們試看看印出 `SuperSecretPlansForWorldDomination.txt` 這個檔案的內容。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `cat SuperSecretPlansForWorldDomination.txt` 來印出檔案內容
|
||||||
|
- ➤ 在輸入 `cat Su` 之後按下 Tab 來自動補全檔名,完成後按 Enter 就能執行指令
|
||||||
|
|
||||||
|
- id: cat_super_secret
|
||||||
|
task: 使用 `cat` 印出 SuperSecretPlansForWorldDomination.txt 的內容
|
||||||
|
requirements:
|
||||||
|
- ls
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^cat SuperSecretPlansForWorldDomination.txt$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了終端機自動幫你補全了 `SuperSecretPlansForWorldDomination.txt`,就算是很長的檔名,也不用怕打錯字。
|
||||||
|
開頭不論你打了幾個字都無所謂,你可以只打 `S`、`Su`、`Sup`、`Super` 或甚至完全不打字都沒關係。
|
||||||
|
但是你打的字越少,就越容易跟其他的檔案重複,這時候就要做衝突處理。
|
||||||
|
關於衝突處理,我們會在待會練習。
|
||||||
|
|
||||||
|
自動補全也可以用來對付拼字錯誤,像是 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 這個檔案充滿了錯字,任誰來打都難保不會失手。
|
||||||
|
這一次,請你一樣用 `cat` 來試著印出 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 的內容。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 來印出檔案內容
|
||||||
|
- ➤ 輸入 `cat Thsi` 之後按下 Tab 做自動補全
|
||||||
|
- ➤ 試試看輸入更少的字元就按 Tab 會有什麼樣的結果
|
||||||
|
|
||||||
|
- id: cat_typo
|
||||||
|
task: 使用 `cat` 印出 ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt 的內容
|
||||||
|
requirements:
|
||||||
|
- cat_super_secret
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了這個充滿錯字的檔案了。
|
||||||
|
只要使用自動補全,不管是錯字、大小寫變化、還是隨機亂碼都不再是困擾了。
|
||||||
|
讓我們再練習一個,這次請你查看 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾中的檔案。
|
||||||
|
一樣,輸入前幾個字,然後按 Tab 自動補全。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `ls ThingsIWillOrganizeLaterWhenIHaveTime` 查看資料夾中的檔案
|
||||||
|
- ➤ 輸入 `ls Thing` 之後按下 Tab 做自動補全
|
||||||
|
|
||||||
|
- id: ls_things
|
||||||
|
task: 使用 `ls` 查看 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾中的檔案
|
||||||
|
requirements:
|
||||||
|
- cat_typo
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^ls ThingsIWillOrganizeLaterWhenIHaveTime$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了這個資料夾裡有著三個檔案:一個如何做三明治的教學跟兩個在半夜隨機下載的東西。
|
||||||
|
假如我們要打開三明治教學 `HowToProperlyMakeASandwich.txt`,但是它位在資料夾中,我們沒辦法只按一次 Tab 就補全它的檔名。
|
||||||
|
這次我們就要來練習使用**連續的自動補全**。
|
||||||
|
|
||||||
|
像這樣檔案位在其他資料夾的情況,我們只要先輸入資料夾名稱的開頭,按一次 Tab,再輸入檔案的開頭,再按一次 Tab,就能夠補全 `資料夾/檔名` 了。
|
||||||
|
而且當你補全的對象是資料夾時,終端機還會自動幫你在結尾加上 `/`,省去你自己輸入的時間。
|
||||||
|
那麼我們就來打開 `HowToProperlyMakeASandwich.txt`。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt` 來印出檔案內容
|
||||||
|
- ➤ 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
|
||||||
|
- ➤ 再輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/How` 按 Tab 補全至完整路徑
|
||||||
|
|
||||||
|
- id: cat_sandwich
|
||||||
|
task: 使用 `cat` 印出 ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt 的內容
|
||||||
|
requirements:
|
||||||
|
- ls_things
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了這份嚴謹的三明治製作教學。
|
||||||
|
那我們剩下來要看的就是資料夾裡的兩個檔案:`RandomStuffThatIDownloadedAt3AM.txt` 和 `RandomStuffThatIDownloadedAt4AM.txt`
|
||||||
|
兩個檔名非常相似,只有最後一個是 3AM 另一個是 4AM。
|
||||||
|
這樣我們只打前面幾個字的時候,沒有辦法分辦到底選的是哪一個檔案。
|
||||||
|
這就是**發生衝突**。
|
||||||
|
|
||||||
|
遇到衝突的解決方法,其實跟使用資料夾是一樣的。
|
||||||
|
在輸入 `Ran` 按下 Tab 後,終端機會補全到衝突發生前的位置,也就是 `RandomStuffThatIDownloadedAt`。
|
||||||
|
我們只要根據我們想開啟的檔案,輸入 `3` 或是 `4`,再按一下 Tab,就能夠補全剩下的檔名了。
|
||||||
|
|
||||||
|
_如果我們把完整的路徑視作補全的對象,那資料夾其實就是一種衝突的情況。_
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 印出 `ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt` 的檔案內容
|
||||||
|
- ➤ 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
|
||||||
|
- ➤ 再接續輸入 `Ran` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt`
|
||||||
|
- ➤ 再接續輸入 `3` 後按 Tab 補全至完整的路徑。
|
||||||
|
|
||||||
|
- id: cat_3am
|
||||||
|
task: 使用 `cat` 印出 ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt 的內容
|
||||||
|
requirements:
|
||||||
|
- cat_sandwich
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
你看到了一份凌晨三點下載的檔案列表。嗯…我今天網路用的夠多了。
|
||||||
|
最後留下一個挑戰給你,請先進入到 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾後,再印出 `RandomStuffThatIDownloadedAt4AM.txt`。
|
||||||
|
練習使用今天教的自動補全來完成任務。
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 先使用 `cd` 進入 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾
|
||||||
|
- ➤ 再使用 `cat` 印出 RandomStuffThatIDownloadedAt4AM.txt 的內容
|
||||||
|
- ➤ 記得使用 Tab 自動補全來節省時間和避免錯誤
|
||||||
|
|
||||||
|
- id: cd_things
|
||||||
|
task: 使用 `cd` 進入 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾
|
||||||
|
requirements:
|
||||||
|
- cat_3am
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander/ThingsIWillOrganizeLaterWhenIHaveTime$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
很好,你已經成功進入 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾了。
|
||||||
|
現在請使用 `cat` 指令來印出 `RandomStuffThatIDownloadedAt4AM.txt` 的內容。
|
||||||
|
記得運用 Tab 自動補全功能!
|
||||||
|
|
||||||
|
- id: END
|
||||||
|
task: 使用 `cat` 印出 RandomStuffThatIDownloadedAt4AM.txt 的內容
|
||||||
|
requirements:
|
||||||
|
- cd_things
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander/ThingsIWillOrganizeLaterWhenIHaveTime$
|
||||||
|
command:
|
||||||
|
- ^cat RandomStuffThatIDownloadedAt4AM.txt$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
恭喜你!你已經成功完成了所有的任務,並且熟練地運用了 Tab 自動補全功能。
|
||||||
|
這個技能將會在你未來的終端機操作中帶來極大的便利。
|
||||||
|
記住,不論是長檔名、複雜路徑,還是難以拼寫的檔案名稱,Tab 自動補全都是你的好幫手!
|
||||||
|
|
||||||
|
hints:
|
||||||
|
- ➤ 你已經完成了所有任務!可以繼續探索或是結束這個關卡。
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
1. 收集全世界的襪子,讓所有人都找不到另一支襪子。
|
||||||
|
2. 訓練一支企鵝軍隊,用可愛征服世界。
|
||||||
|
3. 發明一種能讓所有人講話都變成雞叫聲的裝置。
|
||||||
|
4. 把所有的咖啡替換成無咖啡因的,看著世界陷入混亂。
|
||||||
|
5. 用巨大的橡皮擦擦掉所有國家的邊界,宣布地球是一個巨大的遊樂場。
|
||||||
|
6. 讓所有政治家在演講時必須穿著小丑服裝。
|
||||||
|
7. 把所有的數學課本改成烹飪食譜,看看會發生什麼。
|
||||||
|
8. 發明一種能讓人說出真心話的香水,然後在聯合國大會上噴灑。
|
||||||
|
9. 用巨大的磁鐵吸走地球上所有的金屬,建造一個太空迪士尼樂園。
|
||||||
|
10. 最後,如果以上都失敗了,就用可愛貓咪影片轟炸全世界的社交媒體,直到所有人都投降為止。
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
如何"正確"製作一個三明治:
|
||||||
|
|
||||||
|
1. 用力拍醒你的麵包。它們整天都在發懶!
|
||||||
|
2. 仔細選擇配料,確保每種都有自己的個性。例如,挑選一片特別愛說笑話的火腿。
|
||||||
|
3. 用尺精確測量每片配料的厚度,誤差不得超過1毫米。
|
||||||
|
4. 在組裝三明治時,必須戴上白手套和實驗室防護鏡,以確保最高級別的衛生。
|
||||||
|
5. 完成後,用放大鏡仔細檢查每一寸三明治,確保完美無缺。
|
||||||
|
|
||||||
|
記住:製作三明治是一門精確的科學,絕不容許有任何差錯或創意!
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
我在凌晨 3 點開始下載的東西,但我一直沒有時間去整理。
|
||||||
|
|
||||||
|
1. 一個可以把所有食物變成披薩的魔法棒設計圖
|
||||||
|
2. 如何在五分鐘內學會十種外星語言的秘密指南
|
||||||
|
3. 用舊襪子製作時尚帽子的教程視頻(連結已失效)
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
我在凌晨 4 點開始下載的東西,但我一直沒有時間去整理。
|
||||||
|
|
||||||
|
1. 一份詳細解釋為什麼貓咪才是地球真正統治者的論文
|
||||||
|
2. 如何用口香糖和迴紋針修理國際太空站的WikiHow頁面
|
||||||
|
3. 訓練水母成為水下偵探的完整課程大綱
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Congrats! You fond a flie namd by somone who cleerly had a strok. If you can raed this, you mght be a geenius or jsut realy gud at Wheel of Fortnue.
|
||||||
|
|
||||||
|
Remeber: freinds don't let freinds name files aftr drinkng too much. But hey, at lest you used tab compltion to get here!
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# 使用 Tab 自動補全
|
||||||
|
|
||||||
|
在這個關卡,你將會學到
|
||||||
|
|
||||||
|
1. 如何使用 Tab 自動補全路徑
|
||||||
|
2. 出現衝突時如何處理
|
||||||
|
|
||||||
|
按下中間的按鈕來開始這個關卡。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
在終端機裡,所有的操作都是用文字進行。
|
||||||
|
然而,打字很花時間,而且只要一個字打錯,指令就會執行失敗。
|
||||||
|
因此終端機提供「自動補全」的功能,自動幫你完成檔案路徑、指令名稱、參數名稱,節省你的時間,也避免輸入錯誤。
|
||||||
|
|
||||||
|
在開始練習之前,我們先來看看家目錄下有哪些檔案。
|
||||||
|
|
||||||
|
> 使用 `ls` 查看檔案列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了這個資料夾中充滿了超級長和有錯字的檔案名稱,如果這些都要我們一個一個字輸入,肯定會很痛苦。
|
||||||
|
但不要緊,只要使用自動補全,檔名不論多長都能輕鬆輸入。
|
||||||
|
|
||||||
|
使用自動補全的方法是,**先輸入部分的檔名,再按下 Tab**
|
||||||
|
(Tab 是 Q 左邊的按鍵)
|
||||||
|
例如要補全 `SuperSecretPlansForWorldDomination.txt`,你可以先輸入 `Su` 再按下 Tab,終端機就會幫你打完剩下的檔名了。
|
||||||
|
|
||||||
|
我們試看看印出 `SuperSecretPlansForWorldDomination.txt` 這個檔案的內容。
|
||||||
|
|
||||||
|
> 使用 `cat SuperSecretPlansForWorldDomination.txt` 來印出檔案內容
|
||||||
|
> 在輸入 `cat Su` 之後按下 Tab 來自動補全檔名,完成後按 Enter 就能執行指令
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了終端機自動幫你補全了 `SuperSecretPlansForWorldDomination.txt`,就算是很長的檔名,也不用怕打錯字。
|
||||||
|
開頭不論你打了幾個字都無所謂,你可以只打 `S`、`Su`、`Sup`、`Super` 或甚至完全不打字都沒關係。
|
||||||
|
但是你打的字越少,就越容易跟其他的檔案重複,這時候就要做衝突處理。
|
||||||
|
關於衝突處理,我們會在待會練習。
|
||||||
|
|
||||||
|
自動補全也可以用來對付拼字錯誤,像是 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 這個檔案充滿了錯字,任誰來打都難保不會失手。
|
||||||
|
這一次,請你一樣用 `cat` 來試著印出 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 的內容。
|
||||||
|
|
||||||
|
> 輸入 `cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 來印出檔案內容
|
||||||
|
> 輸入 `cat Thsi` 之後按下 Tab 做自動補全
|
||||||
|
> 試試看輸入更少的字元就按 Tab 會有什麼樣的結果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了這個充滿錯字的檔案了。
|
||||||
|
只要使用自動補全,不管是錯字、大小寫變化、還是隨機亂碼都不再是困擾了。
|
||||||
|
讓我們再練習一個,這次請你查看 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾中的檔案。
|
||||||
|
一樣,輸入前幾個字,然後按 Tab 自動補全。
|
||||||
|
|
||||||
|
> 輸入 `ls ThingsIWillOrganizeLaterWhenIHaveTime` 查看資料夾中的檔案
|
||||||
|
> 輸入 `ls Thing` 之後按下 Tab 做自動補全
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了這個資料夾裡有著三個檔案:一個如何做三明治的教學跟兩個在半夜隨機下載的東西。
|
||||||
|
假如我們要打開三明治教學 `HowToProperlyMakeASandwich.txt`,但是它位在資料夾中,我們沒辦法只按一次 Tab 就補全它的檔名。
|
||||||
|
這次我們就要來練習使用**連續的自動補全**。
|
||||||
|
|
||||||
|
像這樣檔案位在其他資料夾的情況,我們只要先輸入資料夾名稱的開頭,按一次 Tab,再輸入檔案的開頭,再按一次 Tab,就能夠補全 `資料夾/檔名` 了。
|
||||||
|
而且當你補全的對象是資料夾時,終端機還會自動幫你在結尾加上 `/`,省去你自己輸入的時間。
|
||||||
|
那麼我們就來打開 `HowToProperlyMakeASandwich.txt`。
|
||||||
|
|
||||||
|
> 輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt` 來印出檔案內容
|
||||||
|
> 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
|
||||||
|
> 再輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/How` 按 Tab 補全至完整路徑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了這份嚴謹的三明治製作教學。
|
||||||
|
那我們剩下來要看的就是資料夾裡的兩個檔案:`RandomStuffThatIDownloadedAt3AM.txt` 和 `RandomStuffThatIDownloadedAt4AM.txt`
|
||||||
|
兩個檔名非常相似,只有最後一個是 3AM 另一個是 4AM。
|
||||||
|
這樣我們只打前面幾個字的時候,沒有辦法分辦到底選的是哪一個檔案。
|
||||||
|
這就是**發生衝突**。
|
||||||
|
|
||||||
|
遇到衝突的解決方法,其實跟使用資料夾是一樣的。
|
||||||
|
在輸入 `Ran` 按下 Tab 後,終端機會補全到衝突發生前的位置,也就是 `RandomStuffThatIDownloadedAt`。
|
||||||
|
我們只要根據我們想開啟的檔案,輸入 `3` 或是 `4`,再按一下 Tab,就能夠補全剩下的檔名了。
|
||||||
|
|
||||||
|
_如果我們把完整的路徑視作補全的對象,那資料夾其實就是一種衝突的情況。_
|
||||||
|
|
||||||
|
> 輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt` 來印出檔案內容
|
||||||
|
> 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
|
||||||
|
> 再接續輸入 `Ran` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt`
|
||||||
|
> 再接續輸入 `3` 後按 Tab 補全至完整的路徑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
你看到了一份凌晨三點下載的檔案列表。嗯…我今天網路用的夠多了。
|
||||||
|
最後留下一個挑戰給你,請先進入到 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾後,再印出 `RandomStuffThatIDownloadedAt4AM.txt`。
|
||||||
|
練習使用今天教的自動補全來完成任務。
|
||||||
@@ -43,7 +43,10 @@ export const createSession = asyncHandler(
|
|||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { questId } = matchedData(req)
|
const { questId } = matchedData(req)
|
||||||
const user = await User.findById((req.user as Express.ExistingUser).id)
|
const user = await User.findById((req.user as Express.ExistingUser).id)
|
||||||
if (user && (await isQuestUnlocked(user, questId))) {
|
if (
|
||||||
|
user &&
|
||||||
|
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
||||||
|
) {
|
||||||
const session = await createNewSession(user, questId)
|
const session = await createNewSession(user, questId)
|
||||||
res.status(201).json(sessionDetail(session))
|
res.status(201).json(sessionDetail(session))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in new issue
Block a user