|
|
<template>
|
|
|
<div class="training-task-page app-container">
|
|
|
<section class="task-filter">
|
|
|
<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="project-filter">
|
|
|
<span class="filter-label">所属项目:</span>
|
|
|
<button
|
|
|
v-for="project in projects"
|
|
|
:key="project"
|
|
|
type="button"
|
|
|
:class="['project-tab', { active: activeProject === project }]"
|
|
|
@click="toggleProject(project)"
|
|
|
>
|
|
|
{{ project }}
|
|
|
</button>
|
|
|
</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>
|
|
|
</div>
|
|
|
</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="editTask(task)">编辑任务</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">>>></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="primary" :loading="saving" @click="submitTask">保存配置</el-button>
|
|
|
</footer>
|
|
|
</div>
|
|
|
</el-drawer>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import {
|
|
|
listClassTrainingTasks,
|
|
|
saveClassTrainingTask,
|
|
|
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 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 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(() => {
|
|
|
loadTeachingClasses();
|
|
|
});
|
|
|
|
|
|
function toggleProject(project) {
|
|
|
activeProject.value = activeProject.value === project ? "" : project;
|
|
|
}
|
|
|
|
|
|
async function queryTasks() {
|
|
|
if (!selectedTeachingClassId.value) {
|
|
|
tasks.value = [];
|
|
|
activeProject.value = "";
|
|
|
return;
|
|
|
}
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
const res = await listClassTrainingTasks(selectedTeachingClassId.value, { enabledOnly: false });
|
|
|
tasks.value = (res.data || []).map(normalizeTask);
|
|
|
if (activeProject.value && !projects.value.includes(activeProject.value)) {
|
|
|
activeProject.value = "";
|
|
|
}
|
|
|
} catch (error) {
|
|
|
tasks.value = [];
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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 = "";
|
|
|
tasks.value = [];
|
|
|
} 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 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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 24px;
|
|
|
padding: 20px 22px;
|
|
|
margin: 0 0 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.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 {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
flex: 1;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 8px 12px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.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 {
|
|
|
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-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
gap: 22px;
|
|
|
padding-inline: 4px;
|
|
|
}
|
|
|
|
|
|
.search-panel {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
}
|
|
|
</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>
|