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/teacherEnd/trainingTask/index.vue

1315 lines
33 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="training-task-page app-container">
<section class="task-filter">
<div class="filter-primary-row">
<div class="class-filter">
<span class="filter-label">教学班</span>
<el-select
v-model="selectedTeachingClassId"
:loading="classesLoading"
placeholder="请选择教学班"
class="class-select"
@change="queryTasks"
>
<el-option
v-for="schoolClass in teachingClassList"
:key="schoolClass.schoolClassId"
:label="schoolClass.className"
:value="schoolClass.schoolClassId"
/>
</el-select>
</div>
<div class="search-panel">
<el-input v-model.trim="keyword" clearable placeholder="请输入实训任务名称" @keyup.enter="queryTasks" />
<el-button type="primary" @click="queryTasks">查询</el-button>
<div class="toolbar-actions">
<el-tag v-if="selectedTeachingClass" type="success">班级配置</el-tag>
<el-tag v-else>默认任务</el-tag>
<el-button
v-if="selectedTeachingClass"
type="danger"
plain
size="small"
:loading="restoring"
@click="restoreAllTasksDefaults"
>
恢复本班全部默认任务
</el-button>
</div>
</div>
</div>
<div class="project-filter">
<span class="filter-label">所属项目:</span>
<button
type="button"
:class="['project-tab', { active: !activeProject }]"
@click="activeProject = ''"
>
全部({{ tasks.length }})
</button>
<button
v-for="project in projectOptions"
:key="project.name"
type="button"
:class="['project-tab', { active: activeProject === project.name }]"
@click="toggleProject(project.name)"
>
{{ project.name }}({{ project.count }})
</button>
<span class="task-count-badge">当前显示 {{ filteredTasks.length }} / {{ tasks.length }}</span>
</div>
</section>
<section v-loading="loading" class="task-grid">
<article
v-for="(task, index) in filteredTasks"
:key="task.id || task.taskKey"
class="task-card"
role="button"
tabindex="0"
@click="viewTask(task)"
@keydown.enter.prevent="viewTask(task)"
>
<div class="task-card-index">{{ String(index + 1).padStart(2, "0") }}</div>
<div class="task-card-core">
<div class="task-project">{{ task.project }}</div>
<div class="task-name">{{ task.title }}</div>
<div class="task-card-hint">点击卡片查看详情</div>
</div>
<div class="task-actions">
<el-button type="primary" size="small" @click.stop="handleTaskAction(task)">
{{ selectedTeachingClass ? "编辑任务" : "查看任务" }}
</el-button>
</div>
<div class="task-card-scanline"></div>
</article>
<el-empty v-if="!filteredTasks.length && !loading" class="task-empty" description="暂无匹配的实训任务" />
</section>
<el-drawer
v-model="dialogVisible"
:size="860"
class="training-task-drawer"
append-to-body
destroy-on-close
:with-header="false"
>
<div class="drawer-shell">
<header class="drawer-header">
<div class="drawer-heading">
<span class="drawer-kicker">{{ dialogMode === "view" ? "Training Task Preview" : "Training Task Editor" }}</span>
<h3>{{ dialogMode === "view" ? "查看实训任务" : "编辑实训任务" }}</h3>
<p>任务数量与步骤数量由系统固定,老师可维护任务显示名称、实训说明、目标、要求和步骤名称。</p>
</div>
<div class="drawer-header-actions">
<button type="button" class="drawer-close-button" aria-label="关闭抽屉" @click="dialogVisible = false">x</button>
</div>
</header>
<div class="drawer-identity">
<div>
<span>所属项目</span>
<strong>{{ taskForm.project }}</strong>
</div>
<div>
<span>实训任务</span>
<strong>{{ taskForm.title }}</strong>
</div>
<div>
<span>任务状态</span>
<strong>{{ taskForm.enabled ? "启用" : "停用" }}</strong>
</div>
</div>
<el-form class="task-form" label-position="top" :disabled="isReadonly">
<section class="drawer-section">
<div class="section-title">
<span>01</span>
<h4>基础说明</h4>
</div>
<el-form-item label="实训任务名称:">
<el-input v-model="taskForm.title" placeholder="保存后会同步为学生端左侧导航名称" />
</el-form-item>
<el-form-item label="实训背景:">
<el-input v-model="taskForm.background" type="textarea" :rows="5" placeholder="请输入实训背景" />
</el-form-item>
</section>
<section class="drawer-section">
<div class="section-title">
<span>02</span>
<h4>目标与要求</h4>
</div>
<el-form-item label="实训目标:">
<div class="objective-list">
<div v-for="(objective, index) in taskForm.objectives" :key="index" class="objective-row">
<el-input v-model="taskForm.objectives[index]" placeholder="请输入实训目标" />
<button
v-if="!isReadonly"
type="button"
class="circle-action"
:disabled="taskForm.objectives.length <= 1"
@click="removeObjective(index)"
>
-
</button>
</div>
<button v-if="!isReadonly" type="button" class="add-objective-button" @click="addObjective">+ 新增目标</button>
</div>
</el-form-item>
<el-form-item label="实训要求:">
<el-input v-model="taskForm.requirements" type="textarea" :rows="5" placeholder="请输入实训要求" />
</el-form-item>
</section>
<section class="drawer-section">
<div class="section-title">
<span>03</span>
<h4>资料与步骤</h4>
</div>
<div class="material-row">
<a v-if="hasTaskMaterial" class="material-link" :href="taskForm.materialUrl || 'javascript:void(0)'" target="_blank">{{ taskForm.materialName }}</a>
<span v-else class="material-placeholder">暂未上传案例资料</span>
<el-upload
v-if="!isReadonly"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="handleMaterialChange"
>
<el-button type="primary" :loading="materialUploading">上传案例资料文档</el-button>
</el-upload>
</div>
<el-form-item label="步骤名称:">
<div class="step-row">
<template v-for="(step, index) in taskForm.steps" :key="index">
<el-input v-model="taskForm.steps[index]" placeholder="步骤名称" />
<span v-if="index < taskForm.steps.length - 1" class="step-arrow">&gt;&gt;&gt;</span>
</template>
</div>
</el-form-item>
</section>
</el-form>
<footer class="drawer-footer">
<el-button @click="dialogVisible = false">{{ isReadonly ? "关闭" : "取消" }}</el-button>
<el-button
v-if="!isReadonly"
type="danger"
plain
:loading="restoring"
@click="restoreTaskDefault"
>
恢复此任务默认内容
</el-button>
<el-button v-if="!isReadonly" type="primary" :loading="saving" @click="submitTask">保存配置</el-button>
</footer>
</div>
</el-drawer>
</div>
</template>
<script setup>
import {
listTrainingTasks,
listClassTrainingTasks,
saveClassTrainingTask,
restoreClassTrainingTaskDefault,
restoreClassTrainingTasksDefaults,
uploadTrainingTaskMaterial,
} from "@/api/trainingTask";
import * as indexApi from "@/api/teacher";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const activeProject = ref("");
const keyword = ref("");
const loading = ref(false);
const classesLoading = ref(false);
const saving = ref(false);
const restoring = ref(false);
const materialUploading = ref(false);
const tasks = ref([]);
const classList = ref([]);
const selectedTeachingClassId = ref("");
const dialogVisible = ref(false);
const dialogMode = ref("view");
const taskForm = reactive(createEmptyTask());
const currentUserId = computed(() => userStore.userInfo.userId);
const isReadonly = computed(() => dialogMode.value === "view");
const hasTaskMaterial = computed(() => isRealMaterial(taskForm.materialName, taskForm.materialUrl));
const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))]);
const projectOptions = computed(() => {
return projects.value.map((project) => ({
name: project,
count: tasks.value.filter((task) => task.project === project).length,
}));
});
const teachingClassList = computed(() => {
return classList.value.filter((schoolClass) => {
return normalizeClassType(schoolClass) === "TEACHING" && String(schoolClass.createdBy || "") === String(currentUserId.value || "");
});
});
const selectedTeachingClass = computed(() => {
return teachingClassList.value.find((schoolClass) => schoolClass.schoolClassId === selectedTeachingClassId.value);
});
const filteredTasks = computed(() => {
const text = keyword.value.trim();
return tasks.value.filter((task) => {
const matchProject = !activeProject.value || task.project === activeProject.value;
const matchKeyword = !text || task.title.includes(text);
return matchProject && matchKeyword;
});
});
onMounted(() => {
setTrainingTaskMainTheme(true);
loadTeachingClasses();
});
onActivated(() => {
setTrainingTaskMainTheme(true);
});
onDeactivated(() => {
setTrainingTaskMainTheme(false);
});
onBeforeUnmount(() => {
setTrainingTaskMainTheme(false);
});
function setTrainingTaskMainTheme(enabled) {
const main = document.querySelector(".teacher-main");
if (!main) return;
main.classList.toggle("training-task-main-theme", enabled);
}
function toggleProject(project) {
activeProject.value = activeProject.value === project ? "" : project;
}
async function queryTasks() {
if (!selectedTeachingClassId.value) {
await queryDefaultTasks();
return;
}
loading.value = true;
try {
const res = await listClassTrainingTasks(selectedTeachingClassId.value, { enabledOnly: false });
tasks.value = (res.data || []).map(normalizeTask);
resetInvalidProject();
} catch (error) {
await queryDefaultTasks();
proxy?.$modal?.msgWarning?.("班级任务加载失败,已显示系统默认任务");
} finally {
loading.value = false;
}
}
async function queryDefaultTasks() {
loading.value = true;
try {
const res = await listTrainingTasks({ enabledOnly: false });
tasks.value = (res.data || []).map(normalizeTask);
resetInvalidProject();
} catch (error) {
tasks.value = [];
} finally {
loading.value = false;
}
}
function resetInvalidProject() {
if (activeProject.value && !projects.value.includes(activeProject.value)) {
activeProject.value = "";
}
}
async function loadTeachingClasses() {
classesLoading.value = true;
try {
const res = await indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId });
classList.value = res.data || [];
selectedTeachingClassId.value = teachingClassList.value[0]?.schoolClassId || "";
await queryTasks();
} catch (error) {
classList.value = [];
selectedTeachingClassId.value = "";
await queryDefaultTasks();
} finally {
classesLoading.value = false;
}
}
function normalizeClassType(schoolClass) {
if (schoolClass?.classType) return schoolClass.classType;
return schoolClass?.createdBy ? "TEACHING" : "ADMIN";
}
function viewTask(task) {
openTaskDialog(task, "view");
}
function editTask(task) {
openTaskDialog(task, "edit");
}
function handleTaskAction(task) {
if (!selectedTeachingClass.value) {
viewTask(task);
return;
}
editTask(task);
}
function createEmptyTask() {
return {
id: "",
taskKey: "",
project: "",
title: "",
background: "",
objectives: [""],
requirements: "",
materialName: "",
materialUrl: "",
steps: ["", "", "", ""],
sort: 0,
enabled: true,
};
}
function normalizeTask(task) {
const material = normalizeMaterial(task.materialName, task.materialUrl);
return {
id: task.id,
taskKey: task.taskKey,
project: task.projectName,
title: task.taskName,
background: task.background || "",
objectives: parseArray(task.objectives, [""]),
requirements: task.requirements || "",
materialName: material.name,
materialUrl: material.url,
steps: normalizeSteps(parseArray(task.steps, ["", "", "", ""])),
sort: task.sort || 0,
enabled: task.enabled !== false,
};
}
function normalizeMaterial(materialName, materialUrl) {
const name = String(materialName || "").trim();
const url = String(materialUrl || "").trim();
return isRealMaterial(name, url) ? { name, url } : { name: "", url: "" };
}
function isRealMaterial(materialName, materialUrl) {
const name = String(materialName || "").trim();
const url = String(materialUrl || "").trim();
if (!name) {
return false;
}
return !(name === "案例资料.rar" && (!url || url === "/file/example.rar"));
}
function parseArray(value, fallback) {
if (Array.isArray(value)) {
return value.length ? value : fallback;
}
if (!value) {
return fallback;
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) && parsed.length ? parsed : fallback;
} catch (error) {
return String(value)
.split(/[;|]/)
.map((item) => item.trim())
.filter(Boolean);
}
}
function normalizeSteps(steps) {
const result = steps.filter((item) => String(item || "").trim()).slice(0, 4);
while (result.length < 4) {
result.push("");
}
return result;
}
function openTaskDialog(task, mode) {
dialogMode.value = mode;
Object.assign(taskForm, cloneTask(task));
dialogVisible.value = true;
}
function cloneTask(task) {
return {
...task,
objectives: [...(task.objectives?.length ? task.objectives : [""])],
steps: normalizeSteps(task.steps?.length ? task.steps : ["", "", "", ""]),
};
}
function addObjective() {
taskForm.objectives.push("");
}
function removeObjective(index) {
if (taskForm.objectives.length <= 1) return;
taskForm.objectives.splice(index, 1);
}
async function handleMaterialChange(file) {
const rawFile = file?.raw;
if (!rawFile) return;
const data = new FormData();
data.append("file", rawFile);
materialUploading.value = true;
try {
const res = await uploadTrainingTaskMaterial(data);
taskForm.materialName = res?.originalFilename || rawFile.name || file?.name || taskForm.materialName;
taskForm.materialUrl = res?.url || res?.fileName || "";
proxy?.$modal?.msgSuccess("案例资料上传成功");
} catch (error) {
proxy?.$modal?.msgError?.("案例资料上传失败");
} finally {
materialUploading.value = false;
}
}
async function submitTask() {
if (!selectedTeachingClass.value || !taskForm.taskKey) {
proxy?.$modal?.msgError?.("请选择教学班后再保存");
return;
}
saving.value = true;
try {
await saveClassTrainingTask(selectedTeachingClassId.value, taskForm.taskKey, buildPayload());
dialogVisible.value = false;
proxy?.$modal?.msgSuccess("保存成功");
await queryTasks();
} finally {
saving.value = false;
}
}
async function restoreTaskDefault() {
if (!selectedTeachingClass.value || !taskForm.taskKey) return;
const confirmed = await confirmRestoreDefault("恢复此任务默认内容");
if (!confirmed) return;
restoring.value = true;
try {
await restoreClassTrainingTaskDefault(selectedTeachingClassId.value, taskForm.taskKey);
dialogVisible.value = false;
proxy?.$modal?.msgSuccess("已恢复此任务的默认内容");
await queryTasks();
} finally {
restoring.value = false;
}
}
async function restoreAllTasksDefaults() {
if (!selectedTeachingClass.value) return;
const confirmed = await confirmRestoreDefault("恢复本班全部默认任务");
if (!confirmed) return;
restoring.value = true;
try {
await restoreClassTrainingTasksDefaults(selectedTeachingClassId.value);
proxy?.$modal?.msgSuccess("已恢复本班全部默认任务");
await queryTasks();
} finally {
restoring.value = false;
}
}
async function confirmRestoreDefault(action) {
try {
await proxy?.$modal?.confirm(
`确认${action}吗?仅恢复当前教学班的实训任务内容,不影响学生已提交数据、成绩、进度及其他班级。`,
"确认恢复默认内容"
);
return true;
} catch (error) {
return false;
}
}
function buildPayload() {
return {
projectName: taskForm.project,
taskKey: taskForm.taskKey,
taskName: taskForm.title,
background: taskForm.background,
objectives: JSON.stringify(taskForm.objectives.map((item) => item.trim()).filter(Boolean)),
requirements: taskForm.requirements,
steps: JSON.stringify(taskForm.steps.map((item) => item.trim()).filter(Boolean)),
materialName: taskForm.materialName,
materialUrl: taskForm.materialUrl,
sort: taskForm.sort,
enabled: taskForm.enabled,
};
}
</script>
<style lang="scss" scoped>
.training-task-page {
min-height: calc(100vh - 130px);
color: #eff8ff;
}
.task-filter {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
padding: 20px 22px;
margin: 0 54px 32px;
border: 1px solid rgba(42, 190, 255, 0.5);
border-radius: 8px;
background:
linear-gradient(90deg, rgba(23, 187, 255, 0.12), transparent 28%, rgba(18, 116, 178, 0.08)),
linear-gradient(135deg, rgba(3, 34, 53, 0.94), rgba(4, 22, 36, 0.9));
box-shadow:
inset 0 1px 0 rgba(174, 238, 255, 0.12),
inset 0 0 34px rgba(0, 174, 255, 0.08),
0 16px 40px rgba(0, 0, 0, 0.22);
&::before,
&::after {
position: absolute;
width: 92px;
height: 1px;
content: "";
background: linear-gradient(90deg, rgba(58, 219, 255, 0), rgba(58, 219, 255, 0.85));
}
&::before {
top: 0;
left: 24px;
}
&::after {
right: 24px;
bottom: 0;
transform: rotate(180deg);
}
}
.filter-primary-row {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
width: 100%;
}
.class-filter {
position: relative;
z-index: 1;
display: flex;
align-items: center;
flex: 0 0 300px;
gap: 10px;
min-width: 280px;
}
.class-select {
flex: 1;
min-width: 180px;
}
.project-filter {
position: relative;
z-index: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
width: 100%;
min-width: 0;
padding-top: 2px;
}
.filter-label {
flex: 0 0 auto;
color: #e8f7ff;
font-weight: 700;
}
.project-tab {
flex: 0 0 auto;
height: 34px;
padding: 0 14px;
color: #b9d5e5;
font-weight: 600;
line-height: 32px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px;
background: rgba(5, 32, 50, 0.48);
transition: all 0.18s ease;
&:hover,
&.active {
color: #ffffff;
border-color: rgba(48, 197, 255, 0.72);
background: linear-gradient(180deg, rgba(34, 169, 239, 0.72), rgba(7, 91, 140, 0.62));
box-shadow: 0 0 16px rgba(33, 178, 255, 0.26), inset 0 0 16px rgba(255, 255, 255, 0.08);
}
}
.task-count-badge {
flex: 0 0 auto;
min-height: 28px;
padding: 0 12px;
color: #8addff;
font-size: 12px;
font-weight: 800;
line-height: 28px;
border: 1px solid rgba(72, 199, 255, 0.26);
border-radius: 999px;
background: rgba(4, 30, 48, 0.56);
}
.search-panel {
display: flex;
align-items: center;
flex: 0 0 600px;
gap: 12px;
:deep(.el-input) {
flex: 1;
min-width: 220px;
}
:deep(.el-input__wrapper) {
border: 1px solid rgba(76, 190, 245, 0.48);
border-radius: 4px;
background: rgba(0, 20, 32, 0.92);
box-shadow: none;
}
:deep(.el-input__inner) {
color: #e9f8ff;
}
:deep(.el-button) {
min-width: 72px;
border: 0;
margin-left: 0;
background: linear-gradient(180deg, #22a9ef, #087bc2);
}
}
.toolbar-actions {
display: inline-flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
height: 32px;
padding-left: 10px;
border-left: 1px solid rgba(92, 190, 236, 0.2);
:deep(.el-upload) {
display: inline-flex;
}
}
.tool-link {
height: 28px;
padding: 0 8px;
color: #9bdcf6;
font-size: 13px;
font-weight: 700;
cursor: pointer;
border: 0;
border-radius: 4px;
background: transparent;
transition: color 0.18s ease, background 0.18s ease;
&:hover {
color: #ffffff;
background: rgba(56, 190, 255, 0.12);
}
}
.task-grid {
display: grid;
grid-template-columns: repeat(3, minmax(240px, 1fr));
gap: 42px 70px;
min-height: 280px;
padding: 4px 54px 42px;
}
.task-card {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 166px;
padding: 28px 28px 24px;
text-align: center;
isolation: isolate;
cursor: pointer;
border: 1px solid rgba(76, 199, 255, 0.58);
border-radius: 8px;
background:
radial-gradient(circle at 20% 0%, rgba(65, 221, 255, 0.18), transparent 34%),
radial-gradient(circle at 86% 112%, rgba(0, 128, 255, 0.22), transparent 34%),
linear-gradient(145deg, rgba(13, 58, 79, 0.9), rgba(3, 24, 39, 0.96) 58%, rgba(4, 38, 58, 0.92));
box-shadow:
inset 0 1px 0 rgba(195, 246, 255, 0.12),
inset 0 0 28px rgba(23, 164, 226, 0.08),
0 18px 42px rgba(0, 0, 0, 0.18);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
&::before,
&::after {
position: absolute;
width: 36px;
height: 36px;
content: "";
z-index: -1;
}
&::before {
top: 13px;
left: 13px;
border-top: 2px solid rgba(71, 225, 255, 0.78);
border-left: 2px solid rgba(71, 225, 255, 0.78);
}
&::after {
right: 13px;
bottom: 13px;
border-right: 2px solid rgba(71, 225, 255, 0.52);
border-bottom: 2px solid rgba(71, 225, 255, 0.52);
}
&:hover {
border-color: rgba(90, 233, 255, 0.98);
box-shadow:
0 0 0 1px rgba(62, 216, 255, 0.16),
0 18px 44px rgba(0, 0, 0, 0.28),
0 0 34px rgba(34, 179, 242, 0.18),
inset 0 0 32px rgba(34, 179, 242, 0.14);
transform: translateY(-3px);
.task-card-scanline {
opacity: 1;
transform: translateX(55%);
}
}
}
.task-card-index {
position: absolute;
top: 18px;
right: 20px;
color: rgba(87, 220, 255, 0.2);
font-size: 42px;
font-weight: 800;
line-height: 1;
pointer-events: none;
}
.task-card-core {
position: relative;
z-index: 1;
width: 100%;
}
.task-project {
display: inline-flex;
align-items: center;
max-width: 100%;
min-height: 26px;
padding: 0 12px;
margin-bottom: 16px;
color: #8addff;
font-size: 12px;
line-height: 24px;
border: 1px solid rgba(72, 199, 255, 0.28);
border-radius: 999px;
background: rgba(6, 31, 49, 0.72);
box-shadow: inset 0 0 12px rgba(32, 180, 245, 0.08);
}
.task-name {
color: #f4fbff;
font-size: 18px;
font-weight: 700;
text-shadow: 0 0 16px rgba(96, 219, 255, 0.2);
}
.task-card-hint {
margin-top: 10px;
color: rgba(176, 225, 243, 0.62);
font-size: 12px;
}
.task-actions {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
margin-top: 18px;
:deep(.el-button) {
min-width: 92px;
border: 0;
margin-left: 0;
border-radius: 999px;
background: linear-gradient(180deg, #2ec4ff, #0784ce);
box-shadow: 0 8px 18px rgba(0, 132, 204, 0.2);
&:hover {
background: linear-gradient(180deg, #5ad8ff, #0b93df);
}
}
}
.task-card-scanline {
position: absolute;
top: 0;
bottom: 0;
left: -60%;
width: 38%;
opacity: 0;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(136, 238, 255, 0.12), transparent);
transform: translateX(0);
transition: opacity 0.2s ease, transform 0.62s ease;
}
.task-empty {
grid-column: 1 / -1;
}
@media (max-width: 1280px) {
.task-filter {
margin-inline: 24px;
}
.filter-primary-row {
align-items: flex-start;
flex-direction: column;
}
.search-panel {
width: 100%;
flex-basis: auto;
}
.task-grid {
grid-template-columns: repeat(2, minmax(220px, 1fr));
padding-inline: 24px;
}
}
@media (max-width: 760px) {
.task-filter {
margin-inline: 4px;
}
.task-grid {
grid-template-columns: 1fr;
gap: 22px;
padding-inline: 4px;
}
.search-panel {
flex-direction: column;
align-items: stretch;
}
}
</style>
<style lang="scss">
.teacher-main.training-task-main-theme {
height: calc(100vh - 50px);
overflow-x: hidden;
overflow-y: auto !important;
}
</style>
<style lang="scss">
.training-task-drawer {
color: #e9f8ff;
border-left: 1px solid rgba(72, 201, 255, 0.58);
background:
radial-gradient(circle at 14% 0%, rgba(40, 187, 255, 0.2), transparent 34%),
linear-gradient(160deg, #081827 0%, #06111d 58%, #071b2a 100%);
box-shadow: -28px 0 80px rgba(0, 0, 0, 0.42), inset 1px 0 0 rgba(151, 226, 255, 0.12);
.el-drawer__body {
height: 100%;
padding: 0;
overflow: hidden;
}
.drawer-shell {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.drawer-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
padding: 28px 32px 22px;
border-bottom: 1px solid rgba(59, 186, 244, 0.32);
background:
linear-gradient(90deg, rgba(13, 65, 96, 0.96), rgba(8, 36, 58, 0.9)),
linear-gradient(135deg, rgba(48, 197, 255, 0.12), transparent);
}
.drawer-heading {
min-width: 0;
h3 {
margin: 6px 0 8px;
color: #f4fbff;
font-size: 24px;
font-weight: 900;
line-height: 1.25;
letter-spacing: 0;
}
p {
max-width: 560px;
margin: 0;
color: #b8d8ea;
font-size: 13px;
line-height: 1.7;
}
}
.drawer-kicker {
display: inline-flex;
padding: 4px 12px;
border: 1px solid rgba(94, 217, 255, 0.5);
border-radius: 999px;
color: #96e9ff;
background: rgba(4, 38, 58, 0.8);
font-size: 12px;
font-weight: 800;
}
.drawer-header-actions {
display: flex;
align-items: center;
flex: 0 0 auto;
gap: 12px;
}
.drawer-close-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
color: #d9f4ff;
font-size: 18px;
line-height: 1;
cursor: pointer;
border: 1px solid rgba(109, 211, 255, 0.48);
border-radius: 50%;
background: rgba(5, 31, 48, 0.72);
transition: all 0.18s ease;
&:hover {
color: #ffffff;
border-color: rgba(109, 211, 255, 0.88);
background: rgba(18, 91, 130, 0.86);
box-shadow: 0 0 14px rgba(33, 178, 255, 0.24);
}
}
.drawer-identity {
display: grid;
grid-template-columns: 1fr 1.35fr 120px;
gap: 12px;
padding: 18px 32px;
border-bottom: 1px solid rgba(59, 186, 244, 0.18);
background: rgba(2, 20, 32, 0.58);
div {
min-width: 0;
padding: 13px 14px;
border: 1px solid rgba(68, 183, 234, 0.28);
border-radius: 8px;
background: rgba(1, 23, 36, 0.74);
}
span {
display: block;
margin-bottom: 6px;
color: #7fc8e8;
font-size: 12px;
font-weight: 700;
}
strong {
display: block;
overflow: hidden;
color: #f4fbff;
font-size: 14px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.task-form {
flex: 1 1 auto;
min-height: 0;
padding: 22px 32px 118px;
overflow-y: auto;
.el-form-item {
margin-bottom: 18px;
}
.el-form-item__label {
color: #e9f8ff;
font-weight: 800;
line-height: 22px;
}
.el-input__wrapper,
.el-textarea__inner {
color: #effaff;
border: 1px solid rgba(80, 191, 241, 0.58);
border-radius: 6px;
background: rgba(1, 23, 36, 0.92);
box-shadow: inset 0 0 16px rgba(38, 176, 244, 0.08);
}
.el-input__inner,
.el-textarea__inner {
color: #effaff;
}
.el-input.is-disabled .el-input__wrapper,
.el-textarea.is-disabled .el-textarea__inner {
border-color: rgba(73, 147, 184, 0.34);
background: rgba(9, 31, 46, 0.72);
box-shadow: none;
}
.el-input.is-disabled .el-input__inner,
.el-textarea.is-disabled .el-textarea__inner {
color: #bed8e8;
-webkit-text-fill-color: #bed8e8;
}
}
.drawer-section {
padding: 20px 22px 22px;
border: 1px solid rgba(54, 177, 232, 0.34);
border-radius: 10px;
background:
linear-gradient(145deg, rgba(6, 36, 55, 0.88), rgba(3, 20, 32, 0.88)),
radial-gradient(circle at 100% 0%, rgba(48, 197, 255, 0.1), transparent 38%);
box-shadow: inset 0 0 22px rgba(42, 176, 244, 0.06);
& + .drawer-section {
margin-top: 18px;
}
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 18px;
span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 26px;
border: 1px solid rgba(96, 219, 255, 0.56);
border-radius: 999px;
color: #8eeaff;
background: rgba(10, 77, 110, 0.58);
font-size: 12px;
font-weight: 900;
}
h4 {
margin: 0;
color: #f4fbff;
font-size: 16px;
font-weight: 900;
}
}
.objective-list {
width: 100%;
}
.objective-row {
display: grid;
grid-template-columns: 1fr 34px;
gap: 12px;
align-items: center;
& + .objective-row {
margin-top: 10px;
}
}
.circle-action {
width: 32px;
height: 32px;
color: #ecfbff;
font-size: 18px;
font-weight: 800;
line-height: 28px;
cursor: pointer;
border: 1px solid rgba(120, 217, 255, 0.72);
border-radius: 50%;
background: rgba(8, 51, 75, 0.86);
&:disabled {
cursor: not-allowed;
opacity: 0.42;
}
}
.add-objective-button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 34px;
padding: 0 16px;
margin-top: 12px;
color: #9feaff;
font-weight: 800;
cursor: pointer;
border: 1px dashed rgba(103, 219, 255, 0.55);
border-radius: 999px;
background: rgba(6, 43, 65, 0.56);
transition: all 0.18s ease;
&:hover {
color: #ffffff;
border-style: solid;
border-color: rgba(130, 235, 255, 0.9);
background: rgba(9, 72, 104, 0.74);
box-shadow: 0 0 18px rgba(35, 185, 245, 0.12);
}
}
.material-row {
display: flex;
align-items: center;
gap: 20px;
min-height: 54px;
padding: 12px 0 18px;
margin-bottom: 14px;
border-bottom: 1px solid rgba(65, 171, 220, 0.22);
}
.material-link {
color: #6fd8ff;
text-decoration: none;
}
.material-placeholder {
color: rgba(184, 218, 235, 0.68);
}
.step-row {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr;
gap: 12px;
align-items: center;
width: 100%;
}
.step-arrow {
color: #7ddaff;
font-weight: 800;
}
.drawer-footer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: flex-end;
gap: 14px;
padding: 18px 32px;
border-top: 1px solid rgba(59, 186, 244, 0.26);
background: rgba(4, 18, 30, 0.94);
box-shadow: 0 -14px 30px rgba(0, 0, 0, 0.24);
.el-button {
min-width: 112px;
border-radius: 5px;
font-weight: 800;
}
.el-button--primary {
border: 0;
background: linear-gradient(180deg, #22a9ef, #087bc2);
}
}
}
@media (max-width: 960px) {
.training-task-drawer {
width: calc(100vw - 20px) !important;
.drawer-header,
.drawer-identity,
.task-form,
.drawer-footer {
padding-right: 20px;
padding-left: 20px;
}
.drawer-header {
flex-direction: column;
}
.drawer-identity {
grid-template-columns: 1fr;
}
.step-row {
grid-template-columns: 1fr;
}
.step-arrow {
display: none;
}
}
}
</style>