feat(流程图): 手机上也能画流程图了
原来手机上不给画(节点库是 HTML5 拖放,手机上拖不动),只能在结果页签里多一段只读的 「流程图点评」。换成插入式编辑器之后只用点,手机上照样能画: - 「代码」页签的工具栏里有「写代码 / 画流程图」、撤销、重做 - 屏幕底部那条在画流程图时换成「交给 AI 点评」 - 结果页签就是电脑上那一套(逐组结果、亮路径、载回画布),只读的那一段删了 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b7d87d677
commit
04c90e3f89
8 files changed
+36
-65
No files matched your search
@@ -206,13 +206,13 @@ const changeLanguage = (v: LANGUAGE) => {
|
|||||||
运行例子
|
运行例子
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart'" />
|
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart' && isDesktop" />
|
||||||
|
|
||||||
<SubmitCode v-else-if="isDesktop" />
|
<SubmitCode v-else-if="isDesktop" />
|
||||||
|
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
|
|
||||||
<FlowchartControls v-if="drawing && isDesktop" />
|
<FlowchartControls v-if="drawing" />
|
||||||
|
|
||||||
<n-button
|
<n-button
|
||||||
v-if="showHelpButton"
|
v-if="showHelpButton"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useFlowchartStore } from "oj/store/flowchart"
|
import { useFlowchartStore } from "oj/store/flowchart"
|
||||||
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,6 +12,8 @@ import { parseTime } from "utils/functions"
|
|||||||
*/
|
*/
|
||||||
const { editor, unchanged } = storeToRefs(useFlowchartStore())
|
const { editor, unchanged } = storeToRefs(useFlowchartStore())
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
|
// 手机上工具栏挤,「已保存」那几个字不放
|
||||||
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
const saved = computed(() => {
|
const saved = computed(() => {
|
||||||
// 「交给 AI 点评」灰着的时候说一句为什么(设计稿 FlowResPass)
|
// 「交给 AI 点评」灰着的时候说一句为什么(设计稿 FlowResPass)
|
||||||
@@ -22,7 +25,7 @@ const saved = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="editor" class="flowchart-controls">
|
<div v-if="editor" class="flowchart-controls">
|
||||||
<span v-if="saved" class="saved">{{ saved }}</span>
|
<span v-if="saved && isDesktop" class="saved">{{ saved }}</span>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="!editor.canUndo"
|
:disabled="!editor.canUndo"
|
||||||
aria-label="撤销"
|
aria-label="撤销"
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import { caseHeadline, caseReport } from "utils/flowchart"
|
|||||||
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
|
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
|
||||||
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
|
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
|
||||||
*
|
*
|
||||||
* viewOnly:手机上只能看、不能画(设计文档 5.6),不给「照着它写代码」「载回画布」。
|
|
||||||
*/
|
*/
|
||||||
const { viewOnly = false } = defineProps<{ viewOnly?: boolean }>()
|
|
||||||
|
|
||||||
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
||||||
|
|
||||||
@@ -154,7 +152,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => [selectedCase.value, report.value, canvasSame.value, editor.value] as const,
|
() => [selectedCase.value, report.value, canvasSame.value, editor.value] as const,
|
||||||
([index, next, same, instance]) => {
|
([index, next, same, instance]) => {
|
||||||
if (!instance || viewOnly) return
|
if (!instance) return
|
||||||
const row = index === null ? undefined : next?.cases[index]
|
const row = index === null ? undefined : next?.cases[index]
|
||||||
if (!row || !same || !versionDoc.value) {
|
if (!row || !same || !versionDoc.value) {
|
||||||
instance.setHighlight(null)
|
instance.setHighlight(null)
|
||||||
@@ -189,11 +187,7 @@ function goCode() {
|
|||||||
<n-empty
|
<n-empty
|
||||||
v-else-if="!scores.length && phase !== 'evaluating'"
|
v-else-if="!scores.length && phase !== 'evaluating'"
|
||||||
class="empty"
|
class="empty"
|
||||||
:description="
|
description="还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里"
|
||||||
viewOnly
|
|
||||||
? '还没交过流程图。流程图要在电脑上画(手机上拖不动),交给 AI 点评之后,评分和建议在这里也看得到'
|
|
||||||
: '还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里'
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
||||||
@@ -273,7 +267,7 @@ function goCode() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="detail">
|
<template v-if="detail">
|
||||||
<div v-if="detail.mermaidCode && (!report || canvasDiffers || viewOnly)" class="version">
|
<div v-if="detail.mermaidCode && (!report || canvasDiffers)" class="version">
|
||||||
<div class="version-head">
|
<div class="version-head">
|
||||||
<span class="version-title">{{ versionTitle }}</span>
|
<span class="version-title">{{ versionTitle }}</span>
|
||||||
<n-button text type="primary" size="small" @click="versionZoom = true">
|
<n-button text type="primary" size="small" @click="versionZoom = true">
|
||||||
@@ -327,7 +321,7 @@ function goCode() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!viewOnly" class="actions">
|
<div class="actions">
|
||||||
<n-button
|
<n-button
|
||||||
v-if="isFlowchartPass(current.grade) && canCode"
|
v-if="isFlowchartPass(current.grade) && canCode"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useProblemStore } from "oj/store/problem"
|
|||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import SubmitCode from "./SubmitCode.vue"
|
import SubmitCode from "./SubmitCode.vue"
|
||||||
|
|
||||||
|
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手机上屏幕底部固定的「运行例子 / 提交」(设计文档 5.6)。原来这两个按钮在「代码」页签
|
* 手机上屏幕底部固定的「运行例子 / 提交」(设计文档 5.6)。原来这两个按钮在「代码」页签
|
||||||
* 顶上,和语言、更多挤成两行;看着题面想交一次,还得先切回代码页签。
|
* 顶上,和语言、更多挤成两行;看着题面想交一次,还得先切回代码页签。
|
||||||
@@ -41,7 +43,8 @@ const canRunSamples = computed(
|
|||||||
运行例子
|
运行例子
|
||||||
</n-button>
|
</n-button>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<SubmitCode size="large" block />
|
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart'" size="large" block />
|
||||||
|
<SubmitCode v-else size="large" block />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -191,13 +191,12 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 流程图编辑器交给 store:结果页签(在左栏,不在编辑器底下)要拿当前画布比一比、
|
// 流程图编辑器交给 store:结果页签(在左栏,不在编辑器底下)要拿当前画布比一比、
|
||||||
// 把旧版本载回去。子组件 SubmitFlowchart 还是从 inject 拿
|
// 把旧版本载回去;「交给 AI 点评」在手机上挂在屏幕底部那条,也从 store 拿
|
||||||
const flowchartStore = useFlowchartStore()
|
const flowchartStore = useFlowchartStore()
|
||||||
watch(flowchartEditorRef, (editor) => flowchartStore.attachEditor(editor ?? null), {
|
watch(flowchartEditorRef, (editor) => flowchartStore.attachEditor(editor ?? null), {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => flowchartStore.attachEditor(null))
|
onBeforeUnmount(() => flowchartStore.attachEditor(null))
|
||||||
provide("flowchartEditorRef", flowchartEditorRef)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { useProblemStore } from "oj/store/problem"
|
|||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import { useCodeStore } from "oj/store/code"
|
import { useCodeStore } from "oj/store/code"
|
||||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import ResultHeader from "./ResultHeader.vue"
|
import ResultHeader from "./ResultHeader.vue"
|
||||||
@@ -33,8 +31,6 @@ const { submission, syntaxErrorInfo, resultSegment, formattedBeforeSubmit } =
|
|||||||
storeToRefs(submissionStore)
|
storeToRefs(submissionStore)
|
||||||
const { problem } = storeToRefs(useProblemStore())
|
const { problem } = storeToRefs(useProblemStore())
|
||||||
const codeStore = useCodeStore()
|
const codeStore = useCodeStore()
|
||||||
const ctx = useProblemPageContext()
|
|
||||||
const { isDesktop } = useBreakpoints()
|
|
||||||
|
|
||||||
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
||||||
const teacherCollab = useTeacherCollab()
|
const teacherCollab = useTeacherCollab()
|
||||||
@@ -42,24 +38,7 @@ const teacherCollab = useTeacherCollab()
|
|||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const { samplesRunAt } = submissionStore.trial
|
const { samplesRunAt } = submissionStore.trial
|
||||||
|
|
||||||
/**
|
type Segment = "submit" | "samples" | "custom"
|
||||||
* 手机上流程图题只能看点评、不能画(设计文档 5.6)。画不了,语言也就落不到 Flowchart 上,
|
|
||||||
* 下面的 drawing 永远是假 —— 原来学生在电脑上画过、AI 评过的,手机上一条都看不到。
|
|
||||||
* 这时多给一段「流程图点评」,只读。
|
|
||||||
*/
|
|
||||||
const flowchartReadable = computed(
|
|
||||||
() => !isDesktop.value && !!problem.value?.allowFlowchart && ctx.value.entry !== "contest",
|
|
||||||
)
|
|
||||||
/** 在看「流程图点评」那一段。只在这一页记着:点运行例子、提交时照常切回对应的分段 */
|
|
||||||
const viewingFlowchart = ref(false)
|
|
||||||
watch(
|
|
||||||
() => [submissionStore.resultSeq, problem.value?.id, flowchartReadable.value],
|
|
||||||
() => {
|
|
||||||
viewingFlowchart.value = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
type Segment = "submit" | "samples" | "custom" | "flowchart"
|
|
||||||
const codeSegments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
const codeSegments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
||||||
{
|
{
|
||||||
value: "submit",
|
value: "submit",
|
||||||
@@ -84,25 +63,16 @@ const drawing = computed(() => codeStore.code.language === "Flowchart")
|
|||||||
// 试跑走判题机,SQL 题不走那里;流程图也没得跑。这两种只留「提交结果」
|
// 试跑走判题机,SQL 题不走那里;流程图也没得跑。这两种只留「提交结果」
|
||||||
const canTrial = computed(() => !problem.value || !isSqlProblem(problem.value))
|
const canTrial = computed(() => !problem.value || !isSqlProblem(problem.value))
|
||||||
|
|
||||||
const segments = computed(() => {
|
const segments = computed(() =>
|
||||||
const list = canTrial.value ? codeSegments.value : codeSegments.value.slice(0, 1)
|
canTrial.value ? codeSegments.value : codeSegments.value.slice(0, 1),
|
||||||
return flowchartReadable.value
|
)
|
||||||
? [...list, { value: "flowchart" as const, label: "流程图点评", time: "" }]
|
/** 只有一段(SQL 题)就不摆分段了 */
|
||||||
: list
|
|
||||||
})
|
|
||||||
/** 只有一段(SQL 题、又不是手机上的流程图题)就不摆分段了 */
|
|
||||||
const showSegments = computed(() => segments.value.length > 1)
|
const showSegments = computed(() => segments.value.length > 1)
|
||||||
|
|
||||||
function isActive(segment: Segment) {
|
function isActive(segment: Segment) {
|
||||||
if (viewingFlowchart.value) return segment === "flowchart"
|
|
||||||
return resultSegment.value === segment
|
return resultSegment.value === segment
|
||||||
}
|
}
|
||||||
function pick(segment: Segment) {
|
function pick(segment: Segment) {
|
||||||
if (segment === "flowchart") {
|
|
||||||
viewingFlowchart.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewingFlowchart.value = false
|
|
||||||
resultSegment.value = segment
|
resultSegment.value = segment
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -124,14 +94,13 @@ function pick(segment: Segment) {
|
|||||||
>
|
>
|
||||||
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
||||||
</button>
|
</button>
|
||||||
<span v-if="!viewingFlowchart && resultSegment === 'custom'" class="segment-note"
|
<span v-if="resultSegment === 'custom'" class="segment-note"
|
||||||
>自己编数据试试,这里不判对错</span
|
>自己编数据试试,这里不判对错</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FlowchartResult v-if="viewingFlowchart && flowchartReadable" view-only />
|
|
||||||
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
||||||
<div v-show="!viewingFlowchart && (resultSegment === 'submit' || !canTrial)">
|
<div v-show="resultSegment === 'submit' || !canTrial">
|
||||||
<template v-if="syntaxErrorInfo">
|
<template v-if="syntaxErrorInfo">
|
||||||
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
|
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
|
||||||
<CompileErrorExplain :err-info="syntaxErrorInfo" />
|
<CompileErrorExplain :err-info="syntaxErrorInfo" />
|
||||||
@@ -149,7 +118,7 @@ function pick(segment: Segment) {
|
|||||||
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
|
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="canTrial && !viewingFlowchart">
|
<template v-if="canTrial">
|
||||||
<SampleRunResult v-if="resultSegment === 'samples'" />
|
<SampleRunResult v-if="resultSegment === 'samples'" />
|
||||||
<CustomRun v-else-if="resultSegment === 'custom'" />
|
<CustomRun v-else-if="resultSegment === 'custom'" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { storeToRefs } from "pinia"
|
|||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { countBlank } from "shared/components/FlowchartEditor/model"
|
import { countBlank } from "shared/components/FlowchartEditor/model"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { useFlowchartStore, type FlowchartEditorInstance } from "oj/store/flowchart"
|
import { useFlowchartStore } from "oj/store/flowchart"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「交给 AI 点评」按钮。
|
* 「交给 AI 点评」按钮。
|
||||||
@@ -13,16 +13,20 @@ import { useFlowchartStore, type FlowchartEditorInstance } from "oj/store/flowch
|
|||||||
* 弹框盖住画布,学生没法一边看改进建议一边改图。
|
* 弹框盖住画布,学生没法一边看改进建议一边改图。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const flowchartEditorRef = inject<Ref<FlowchartEditorInstance | null>>("flowchartEditorRef")
|
/** 手机上它在屏幕底部那条(MobileActionBar),不在编辑器里面,所以画布从 store 拿 */
|
||||||
|
const { size, block = false } = defineProps<{
|
||||||
|
size?: "small" | "medium" | "large"
|
||||||
|
block?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { problem } = storeToRefs(useProblemStore())
|
const { problem } = storeToRefs(useProblemStore())
|
||||||
const flowchartStore = useFlowchartStore()
|
const flowchartStore = useFlowchartStore()
|
||||||
const { loading, unchanged } = storeToRefs(flowchartStore)
|
const { loading, unchanged, editor: canvas } = storeToRefs(flowchartStore)
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const editor = flowchartEditorRef?.value
|
const editor = canvas.value
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const doc = editor.getDoc()
|
const doc = editor.getDoc()
|
||||||
if (!doc.steps.length) {
|
if (!doc.steps.length) {
|
||||||
@@ -44,7 +48,8 @@ watch(() => problem.value?.id, flowchartStore.ensureLoaded, { immediate: true })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-button
|
<n-button
|
||||||
:size="isDesktop ? 'medium' : 'small'"
|
:size="size ?? (isDesktop ? 'medium' : 'small')"
|
||||||
|
:block="block"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="loading || unchanged"
|
:disabled="loading || unchanged"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
|
||||||
import type { LANGUAGE, ProblemDetail } from "utils/types"
|
import type { LANGUAGE, ProblemDetail } from "utils/types"
|
||||||
import { problemEntryOf } from "oj/problem/composables/problemPageContext"
|
import { problemEntryOf } from "oj/problem/composables/problemPageContext"
|
||||||
import { useCodeStore } from "oj/store/code"
|
import { useCodeStore } from "oj/store/code"
|
||||||
@@ -28,14 +27,13 @@ export const useProblemStore = defineStore("problem", () => {
|
|||||||
*/
|
*/
|
||||||
const failCount = computed(() => (problem.value?.myFailedCount ?? 0) + sessionFailCount.value)
|
const failCount = computed(() => (problem.value?.myFailedCount ?? 0) + sessionFailCount.value)
|
||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 这道题能不能画流程图。比赛里不给(入口差异表);题单里原来也不给,现在给了,画到 A/S
|
* 这道题能不能画流程图。比赛里不给(入口差异表);题单里原来也不给,现在给了,画到 A/S
|
||||||
* 算完成(设计文档第 3 节决定 4)。手机上不给:节点库是 HTML5 拖放,手机上拖不动。
|
* 算完成(设计文档第 3 节决定 4)。手机上原来不给(节点库是 HTML5 拖放,拖不动),
|
||||||
|
* 2026-10 换成插入式编辑器之后只用点,手机上也能画
|
||||||
*/
|
*/
|
||||||
const canDraw = computed(
|
const canDraw = computed(
|
||||||
() => !!problem.value?.allowFlowchart && problemEntryOf(route) !== "contest" && isDesktop.value,
|
() => !!problem.value?.allowFlowchart && problemEntryOf(route) !== "contest",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user