Compare commits

...
2 Commits
5 changed files with 104 additions and 22 deletions

No files matched your search

@@ -101,7 +101,7 @@ function reset() {
<span style="color: #bbb; width: 22px; text-align: right; flex-shrink: 0">
{{ idx + 1 }}
</span>
<span v-html="lineHtml[idx]" style="white-space: pre" />
<span v-html="lineHtml[idx]" style="white-space: pre; min-width: 0; overflow-x: auto" />
</div>
</n-space>
@@ -96,6 +96,10 @@ function inputWidth(idx: number): string {
v-else
:value="userInputs[seg.index]"
:disabled="allCorrect"
autocapitalize="off"
autocorrect="off"
autocomplete="off"
spellcheck="false"
:style="{
width: inputWidth(seg.index),
fontFamily: 'var(--learn-code-font)',
@@ -2,6 +2,7 @@
import ExerciseResult from "./ExerciseResult.vue"
import type { Exercise, ExerciseGroupData } from "utils/types"
import { shuffle } from "../composables/useShuffle"
import { useBreakpoints } from "shared/composables/breakpoints"
const props = defineProps<{ exercise: Exercise; lang?: string }>()
const emit = defineEmits<{
@@ -9,16 +10,20 @@ const emit = defineEmits<{
}>()
const data = computed(() => props.exercise.data as ExerciseGroupData)
const { isDesktop } = useBreakpoints()
const order = ref<number[]>([]) // item 的稳定展示顺序(初始乱序)
const placement = ref<number[]>([]) // placement[itemIdx] = 桶下标,-1 表示在池中
const dragIdx = ref<number | null>(null)
// 手机上没有 HTML5 拖放(触屏不触发 dragstart),所以再给一条点选的路:先点项目、再点分组
const pickedIdx = ref<number | null>(null)
const submitted = ref(false)
function init() {
order.value = shuffle(data.value.items.map((_, i) => i))
placement.value = Array(data.value.items.length).fill(-1)
dragIdx.value = null
pickedIdx.value = null
submitted.value = false
}
@@ -32,6 +37,7 @@ const locked = computed(() => submitted.value && allCorrect.value)
function onDragStart(i: number) {
if (locked.value) return
dragIdx.value = i
pickedIdx.value = null
}
function dropTo(bucket: number) {
@@ -41,6 +47,25 @@ function dropTo(bucket: number) {
submitted.value = false
}
// 手机上分组框不大,点分组时多半点在框里已有的项目上 —— 这时算「放进它所在的分组」,
// 只有点同一区域里的另一项才是换选
function onItemClick(i: number) {
if (locked.value) return
const picked = pickedIdx.value
if (picked !== null && picked !== i && placement.value[picked] !== placement.value[i]) {
onAreaClick(placement.value[i])
return
}
pickedIdx.value = picked === i ? null : i
}
function onAreaClick(bucket: number) {
if (locked.value || pickedIdx.value === null) return
placement.value[pickedIdx.value] = bucket
pickedIdx.value = null
submitted.value = false
}
const poolItems = computed(() => order.value.filter((i) => placement.value[i] === -1))
function itemsIn(bucket: number): number[] {
return order.value.filter((i) => placement.value[i] === bucket)
@@ -56,6 +81,17 @@ function chipStyle(i: number): Record<string, string> {
const color =
status === "correct" ? "#18a058" : status === "wrong" ? "#d03050" : "var(--learn-control-line)"
const plain = color === "var(--learn-control-line)"
if (pickedIdx.value === i) {
return {
padding: "5.5px 11.5px",
borderRadius: "6px",
border: "2px solid #2080f0",
background: "rgba(32,128,240,0.08)",
cursor: "pointer",
userSelect: "none",
fontSize: "15px",
}
}
return {
padding: "6px 12px",
borderRadius: "6px",
@@ -86,7 +122,11 @@ function reset() {
{{ data.question }}
</p>
<p style="color: var(--learn-muted); font-size: 13px; margin: 0 0 12px">
把下面的项目拖到对应的分组里,可在分组间拖动调整。
{{
isDesktop
? "把下面的项目拖到对应的分组里,可在分组间拖动调整。"
: "先点一个项目,再点它该去的分组。"
}}
</p>
<div
@@ -102,6 +142,7 @@ function reset() {
}"
@dragover.prevent
@drop="dropTo(-1)"
@click="onAreaClick(-1)"
>
<span v-if="poolItems.length === 0" style="color: var(--learn-muted); font-size: 13px">
(已全部归类)
@@ -112,6 +153,7 @@ function reset() {
draggable="true"
:style="chipStyle(i)"
@dragstart="onDragStart(i)"
@click.stop="onItemClick(i)"
>
{{ data.items[i] }}
</div>
@@ -135,6 +177,7 @@ function reset() {
}"
@dragover.prevent
@drop="dropTo(b)"
@click="onAreaClick(b)"
>
<p style="font-weight: 600; margin: 0 0 8px; text-align: center; font-size: 14px">
{{ bucket }}
@@ -146,6 +189,7 @@ function reset() {
draggable="true"
:style="chipStyle(i)"
@dragstart="onDragStart(i)"
@click.stop="onItemClick(i)"
>
{{ data.items[i] }}
</div>
@@ -81,6 +81,7 @@ function reset() {
:rows="3"
:disabled="submitted && allCorrect"
placeholder="在这里输入程序会打印的内容"
:input-props="{ autocapitalize: 'off', autocorrect: 'off', spellcheck: false }"
style="font-family: var(--learn-code-font)"
/>
@@ -3,6 +3,7 @@ import ExerciseResult from "./ExerciseResult.vue"
import type { Exercise, ExerciseSortData } from "utils/types"
import { shuffle } from "../composables/useShuffle"
import { highlightLines } from "shared/highlight"
import { useBreakpoints } from "shared/composables/breakpoints"
const props = defineProps<{ exercise: Exercise; lang?: string }>()
const emit = defineEmits<{
@@ -10,6 +11,7 @@ const emit = defineEmits<{
}>()
const data = computed(() => props.exercise.data as ExerciseSortData)
const { isDesktop } = useBreakpoints()
type LineItem = { originalIdx: number; text: string }
@@ -24,6 +26,7 @@ function init() {
;[shuffled[0], shuffled[1]] = [shuffled[1], shuffled[0]]
}
lines.value = shuffled
pickedIdx.value = null
submitted.value = false
}
@@ -31,19 +34,37 @@ onMounted(init)
watch(() => props.exercise.id, init)
const dragIdx = ref<number | null>(null)
// 手机上没有 HTML5 拖放(触屏不触发 dragstart),所以再给一条点选的路:先点一行、再点它该去的位置
const pickedIdx = ref<number | null>(null)
function onDragStart(idx: number) {
dragIdx.value = idx
pickedIdx.value = null
}
function moveLine(from: number, to: number) {
if (from === to) return
const newLines = [...lines.value]
const [moved] = newLines.splice(from, 1)
newLines.splice(to, 0, moved)
lines.value = newLines
submitted.value = false
}
function onDrop(targetIdx: number) {
if (dragIdx.value === null || dragIdx.value === targetIdx) return
const newLines = [...lines.value]
const [moved] = newLines.splice(dragIdx.value, 1)
newLines.splice(targetIdx, 0, moved)
lines.value = newLines
if (dragIdx.value === null) return
moveLine(dragIdx.value, targetIdx)
dragIdx.value = null
submitted.value = false
}
function onLineClick(idx: number) {
if (submitted.value && allCorrect.value) return
if (pickedIdx.value === null) {
pickedIdx.value = idx
return
}
moveLine(pickedIdx.value, idx)
pickedIdx.value = null
}
function lineStatus(idx: number): "correct" | "wrong" | "default" {
@@ -71,9 +92,12 @@ const lineHtml = computed<string[]>(() => highlightLines(data.value.lines, props
<template>
<div>
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">
<p :style="{ fontWeight: 500, fontSize: '16px', marginBottom: isDesktop ? '12px' : '8px' }">
{{ data.question }}
</p>
<p v-if="!isDesktop" style="color: var(--learn-muted); font-size: 13px; margin: 0 0 12px">
先点一行,再点它该去的位置。
</p>
<n-space vertical :size="6">
<div
@@ -86,19 +110,24 @@ const lineHtml = computed<string[]>(() => highlightLines(data.value.lines, props
gap: '10px',
padding: '8px 12px',
borderRadius: '6px',
border: `1.5px ${submitted ? 'solid' : 'dashed'} ${
lineStatus(idx) === 'correct'
? '#18a058'
: lineStatus(idx) === 'wrong'
? '#d03050'
: 'var(--learn-control-line)'
}`,
border:
pickedIdx === idx
? '2px solid #2080f0'
: `1.5px ${submitted ? 'solid' : 'dashed'} ${
lineStatus(idx) === 'correct'
? '#18a058'
: lineStatus(idx) === 'wrong'
? '#d03050'
: 'var(--learn-control-line)'
}`,
background:
lineStatus(idx) === 'correct'
? 'rgba(24,160,88,0.08)'
: lineStatus(idx) === 'wrong'
? 'rgba(208,48,80,0.07)'
: 'transparent',
pickedIdx === idx
? 'rgba(32,128,240,0.08)'
: lineStatus(idx) === 'correct'
? 'rgba(24,160,88,0.08)'
: lineStatus(idx) === 'wrong'
? 'rgba(208,48,80,0.07)'
: 'transparent',
cursor: 'grab',
fontFamily: 'var(--learn-code-font)',
userSelect: 'none',
@@ -106,9 +135,13 @@ const lineHtml = computed<string[]>(() => highlightLines(data.value.lines, props
@dragstart="onDragStart(idx)"
@dragover.prevent
@drop="onDrop(idx)"
@click="onLineClick(idx)"
>
<span style="color: #bbb; cursor: grab">⠿</span>
<span v-html="lineHtml[line.originalIdx]" style="white-space: pre" />
<span
v-html="lineHtml[line.originalIdx]"
style="white-space: pre; min-width: 0; overflow-x: auto"
/>
</div>
</n-space>