You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/training/CustomTrainingStepPage.vue

185 lines
6.9 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="custom-training-step-page">
<main v-if="step" class="step-shell">
<header class="step-header">
<p class="eyebrow">自定义实训步骤</p>
<h1>{{ task?.taskName }} · {{ step.name }}</h1>
<p>请完成本步骤的说明、分析结论或成果附件;可先保存,确认后单独提交本步骤。</p>
</header>
<TrainingTaskBrief
:background="task?.background"
:goals="task?.objectives"
:requirements="task?.requirements"
:material-name="task?.materialName"
:material-url="task?.materialUrl"
/>
<section class="answer-card">
<h2>{{ step.name }}</h2>
<el-input
v-model="form.content"
type="textarea"
:autosize="{ minRows: 10, maxRows: 20 }"
placeholder="填写本步骤的完成过程、分析结果和结论"
/>
<div class="attachment-area">
<input ref="fileInput" class="hidden-input" type="file" @change="uploadFile" />
<el-button :loading="uploading" @click="fileInput?.click()">上传成果附件</el-button>
<a v-for="file in form.attachments" :key="file.url" :href="file.url" target="_blank" rel="noopener">
{{ file.name }}
</a>
</div>
</section>
<footer class="actions step-navigation">
<el-button v-if="stepIndex > 0" :disabled="saving" @click="goPrevious">上一步</el-button>
<el-button type="primary" :loading="saving" @click="save(false)">保存当前进度</el-button>
<el-button v-if="stepIndex < taskSteps.length - 1" type="primary" :disabled="saving" @click="goNext">下一步</el-button>
</footer>
</main>
<el-empty v-else description="未找到这个自定义实训步骤" />
</div>
</template>
<script setup>
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const task = ref(null);
const step = ref(null);
const form = reactive({ content: "", attachments: [] });
const dynamicAnswers = ref({});
const fileInput = ref(null);
const saving = ref(false);
const uploading = ref(false);
const taskSteps = computed(() => parseSteps(task.value?.steps));
const stepIndex = computed(() => taskSteps.value.findIndex((item) => item.id === step.value?.id));
watch(
() => [route.params.taskKey, route.params.stepId],
() => load(),
{ immediate: true }
);
async function load() {
task.value = null;
step.value = null;
form.content = "";
form.attachments = [];
dynamicAnswers.value = {};
const taskKey = String(route.params.taskKey || "");
const stepId = String(route.params.stepId || "");
if (!taskKey || !stepId) return;
try {
const [taskRes, answerRes] = await Promise.all([getTrainingTaskByKey(taskKey), getStudentTrainingAnswer(taskKey)]);
task.value = taskRes?.data || null;
step.value = parseSteps(task.value?.steps).find((item) => item.id === stepId && item.kind === "custom") || null;
dynamicAnswers.value = parseAnswers(answerRes?.data?.dynamicStepAnswers);
const saved = dynamicAnswers.value[stepId] || {};
form.content = String(saved.content || "");
form.attachments = Array.isArray(saved.attachments) ? saved.attachments : [];
} catch (error) {
proxy?.$modal?.msgError("加载自定义步骤失败");
}
}
function parseSteps(value) {
let source = value;
if (typeof value === "string") {
try { source = JSON.parse(value); } catch (error) { source = value.split(/[,,\n]/); }
}
if (!Array.isArray(source)) return [];
return source.map((item, index) => {
const raw = item && typeof item === "object" ? item : {};
const name = typeof item === "object" ? String(raw.name || "").trim() : String(item || "").trim();
return {
id: raw.id || `${index < 4 ? "builtin" : "custom"}-${index + 1}`,
name,
kind: raw.kind === "custom" || (!raw.kind && index >= 4) ? "custom" : "builtin",
};
});
}
function parseAnswers(value) {
if (!value) return {};
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
} catch (error) {
return {};
}
}
async function uploadFile(event) {
const file = event.target.files?.[0];
if (!file) return;
const data = new FormData();
data.append("file", file);
uploading.value = true;
try {
const res = await uploadStudentTrainingFile(data);
const url = res?.url || res?.fileName || res?.data?.url || res?.data?.fileName || "";
if (!url) throw new Error("missing upload url");
form.attachments.push({ name: file.name, url });
proxy?.$modal?.msgSuccess("上传成功");
} finally {
uploading.value = false;
event.target.value = "";
}
}
async function save(submitStep) {
if (!step.value || saving.value) return;
saving.value = true;
try {
dynamicAnswers.value[step.value.id] = {
content: form.content,
attachments: form.attachments,
submitted: submitStep,
submittedAt: submitStep ? new Date().toISOString() : null,
};
const stepIndex = parseSteps(task.value?.steps).findIndex((item) => item.id === step.value.id);
await saveStudentTrainingAnswer(String(route.params.taskKey), {
saveAction: "SAVE",
currentStep: Math.max(1, stepIndex + 1),
dynamicStepAnswers: JSON.stringify(dynamicAnswers.value),
});
proxy?.$modal?.msgSuccess(submitStep ? "本步骤已提交" : "草稿已保存");
} finally {
saving.value = false;
}
}
function goPrevious() {
const previous = taskSteps.value[stepIndex.value - 1];
if (previous) router.push(`/training-step/${route.params.taskKey}/${previous.id}`);
}
async function goNext() {
await save(false);
const next = taskSteps.value[stepIndex.value + 1];
if (next) router.push(`/training-step/${route.params.taskKey}/${next.id}`);
}
</script>
<style lang="scss" scoped>
.custom-training-step-page { min-height: 100%; padding: 28px; color: #e9f7ff; }
.step-shell { max-width: 1080px; margin: 0 auto; }
.step-header, .answer-card { padding: 22px; border: 1px solid rgba(70, 181, 231, .36); border-radius: 16px; background: rgba(2, 22, 37, .82); }
.step-header { margin-bottom: 20px; }
.step-header h1 { margin: 5px 0 10px; font-size: 26px; }
.step-header p { margin: 0; color: #b9ddf1; line-height: 1.75; }
.eyebrow { color: #72dcff !important; font-weight: 800; }
.answer-card h2 { margin-top: 0; font-size: 20px; }
.attachment-area { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; }
.attachment-area a { color: #75dcff; }
.hidden-input { display: none; }
.actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; }
@media (max-width: 768px) { .custom-training-step-page { padding: 14px; } .actions { flex-wrap: wrap; } }
</style>