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

1224 lines
32 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="market-selection-page student-training-shell">
<main class="training-core">
<section class="task-header">
<div class="task-badge">产品规划与设计</div>
<h1 class="task-title">{{ taskTitle }}</h1>
</section>
<TrainingTaskBrief
:background="taskBackground"
:goals="taskGoals"
:requirements="taskRequirement"
:show-material="false"
/>
<section class="task-card">
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton
:material-name="taskConfig?.materialName"
:material-url="taskConfig?.materialUrl"
:training-data-name="taskConfig?.trainingDataName"
:training-data-url="taskConfig?.trainingDataUrl"
:guide-manual-name="taskConfig?.guideManualName"
:guide-manual-url="taskConfig?.guideManualUrl"
:ai-manual-name="taskConfig?.aiManualName"
:ai-manual-url="taskConfig?.aiManualUrl"
/>
<button type="button" class="outline-action" @click="exportOutcome">
<el-icon><Download /></el-icon>
导出实训成果
</button>
</div>
</div>
<nav v-if="steps.length" class="step-indicator" aria-label="市场机会识别与选品步骤">
<template v-for="step in steps" :key="step.no">
<button
type="button"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
:aria-label="`切换至第 ${step.no} 步:${step.name}`"
:aria-current="currentStep === step.no ? 'step' : undefined"
@click="changeStep(step.no)"
>
<span class="step-marker" aria-hidden="true">{{ String(step.no).padStart(2, '0') }}</span>
<span class="step-copy">
<strong class="step-name" :title="step.name">{{ step.name }}</strong>
<span class="step-action">{{ currentStep === step.no ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="step.no < steps.length" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section class="step-card market-selection-step-card">
<div v-if="activeStepName" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, '0') }}</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<section v-show="currentStep === 1" class="step-content">
<p class="task-desc">聚焦“智能空气监测仪/空气质量检测仪”品类,完成以下 PEST 分析表。</p>
<div class="pest-table-wrap">
<div class="table-title">PEST分析表</div>
<div class="pest-table">
<div class="pest-row pest-head">
<div>分析维度</div>
<div>具体影响因素与现象</div>
<div>初步识别的市场机会与风险</div>
</div>
<div v-for="row in pestRows" :key="row.key" class="pest-row">
<div class="dimension-cell">{{ row.label }}</div>
<textarea v-model="form.pest[row.key].factor" :placeholder="row.factorPlaceholder" />
<textarea v-model="form.pest[row.key].opportunity" :placeholder="row.opportunityPlaceholder" />
</div>
</div>
</div>
</section>
<section v-show="currentStep === 2" class="step-content">
<p class="task-desc">访问天猫或京东平台,选择“小米(米家)”“青萍”等作为主要竞品分析对象,完成下表。</p>
<div class="competitor-fields">
<label>
<span>竞争对手A:</span>
<input v-model="form.competitor.competitorA" />
</label>
<label>
<span>竞争对手B:</span>
<input v-model="form.competitor.competitorB" />
</label>
</div>
<div class="training-table-wrap">
<div class="table-title">竞争对手分析与需求挖掘表</div>
<table class="training-data-table">
<thead>
<tr>
<th>分析项目</th>
<th>竞争对手A:{{ form.competitor.competitorA }}</th>
<th>竞争对手B:{{ form.competitor.competitorB }}</th>
<th>发现的市场缺口与核心痛点</th>
</tr>
</thead>
<tbody>
<tr v-for="row in form.competitor.rows" :key="row.key">
<td class="dimension-cell">{{ row.name }}</td>
<td><textarea v-model="row.competitorA" class="cell-textarea" /></td>
<td><textarea v-model="row.competitorB" class="cell-textarea" /></td>
<td><textarea v-model="row.gap" class="cell-textarea" /></td>
</tr>
</tbody>
</table>
</div>
<label class="field-block">
<span>我们的初步对策:</span>
<textarea v-model="form.competitor.strategy" class="input-field" rows="5" placeholder="针对上述痛点,我们计划:" />
</label>
</section>
<section v-show="currentStep === 3" class="step-content">
<p class="task-desc">根据销量、市场增长率和相对市场份额,完成数据化选品判断。</p>
<div class="training-table-wrap">
<div class="table-title">汇总计算结果</div>
<table class="training-data-table compact-table">
<thead>
<tr>
<th>产品类别</th>
<th>市场增长率(%)</th>
<th>市场增长率行业均值</th>
<th>相对市场份额(%)</th>
<th>相对市场份额均值</th>
<th>产品分类</th>
</tr>
</thead>
<tbody>
<tr v-for="row in form.dataSelection.rows" :key="row.category">
<td>{{ row.category }}</td>
<td><input v-model="row.growthRate" class="cell-input small-input" /></td>
<td>10%</td>
<td><input v-model="row.relativeShare" class="cell-input small-input" /></td>
<td>100%</td>
<td>
<select v-model="row.classification" class="cell-select">
<option value="">请选择</option>
<option>明星产品</option>
<option>现金牛产品</option>
<option>问题产品</option>
<option>瘦狗产品</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<label class="field-block">
<span>请结合百度指数分析波士顿矩阵模型在数据化选品中的优缺点:</span>
<textarea v-model="form.dataSelection.reflection" class="input-field" rows="5" />
</label>
</section>
<section v-show="currentStep === 4" class="step-content">
<p class="task-desc">基于以上分析,运用 SWOT 思路进行综合判断,并得出初步决策。</p>
<label class="field-block">
<span>我们的优势:</span>
<textarea v-model="form.swot.strengths" class="input-field" rows="4" placeholder="团队在____方面有专长;可以采用____策略降低成本等。" />
</label>
<label class="field-block">
<span>我们的劣势:</span>
<textarea v-model="form.swot.weaknesses" class="input-field" rows="4" placeholder="已提取交互____;应避免与巨头在____方面直接竞争等。" />
</label>
<label class="field-block">
<span>我们的机会:</span>
<textarea v-model="form.swot.opportunities" class="input-field" rows="4" placeholder="宏观环境中的____趋势;竞争对手普遍存在的____短板。" />
</label>
<label class="field-block">
<span>我们的威胁:</span>
<textarea v-model="form.swot.threats" class="input-field" rows="4" placeholder="巨头的可能模仿;技术快速迭代等。" />
</label>
<label class="field-block">
<span>结论:基于以上分析,给出具体结论,是否继续开发相关产品,开发什么样的产品</span>
<textarea v-model="form.swot.conclusion" class="input-field" rows="6" />
</label>
</section>
<div v-if="steps.length" class="step-actions">
<el-button class="step-action-btn step-action-btn--previous" :disabled="currentStep === 1" @click="previousStep">
<el-icon><ArrowLeft /></el-icon>
上一步
</el-button>
<el-button class="step-action-btn step-action-btn--save" :loading="saving" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</el-button>
<el-button class="step-action-btn step-action-btn--next" :disabled="currentStep === steps.length" @click="nextStep">
下一步
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>
<TrainingAiSidebar
study-tip="当前步骤建议:先从政策、经济、社会、技术四类因素中提取关键现象,再判断对应的机会与风险。"
reference-title="选品机会判断参考"
reference-text="· 政治:政策补贴、标准认证、合规门槛<br />· 经济:价格带、采购成本、市场规模<br />· 社会:健康意识、家庭场景、消费偏好<br />· 技术:传感器、联网能力、数据分析能力"
assist-text="可以把每个维度先写成“现象 -> 机会/风险”的句式,最后再汇总判断是否适合作为选品方向。"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
const TASK_KEY = "market-opportunity-selection";
const { proxy } = getCurrentInstance();
const taskConfig = ref(null);
const saving = ref(false);
const currentStep = ref(1);
const defaultRequirement = "市场机会识别与选品需要把宏观环境、竞争态势、需求数据和品类适配度结合起来,判断产品是否具备进入价值。";
const defaultBackground =
"以智能空气监测仪/空气质量检测仪为例,学生需要从 PEST 四个维度识别外部环境中的机会与风险,并形成后续选品判断依据。";
const defaultGoals = [
"掌握从宏观环境中识别产品机会的方法。",
"能够使用 PEST 模型拆解品类机会与风险。",
"能够为后续数据化选品和综合判断提供结构化依据。",
];
const pestRows = [
{
key: "political",
label: "政治",
factorPlaceholder: "填写政策、标准、认证、监管等因素",
opportunityPlaceholder: "填写可能带来的机会或合规风险",
},
{
key: "economic",
label: "经济",
factorPlaceholder: "填写价格带、市场规模、采购成本、消费能力等因素",
opportunityPlaceholder: "填写市场容量、成本优势或价格风险",
},
{
key: "social",
label: "社会",
factorPlaceholder: "填写健康意识、家庭场景、用户偏好等因素",
opportunityPlaceholder: "填写用户需求、场景增长或接受度风险",
},
{
key: "technology",
label: "技术",
factorPlaceholder: "填写传感器、联网、数据分析、产品稳定性等因素",
opportunityPlaceholder: "填写技术差异化机会或落地风险",
},
];
function createForm() {
return {
pest: {
political: { factor: "", opportunity: "" },
economic: { factor: "", opportunity: "" },
social: { factor: "", opportunity: "" },
technology: { factor: "", opportunity: "" },
},
competitor: {
competitorA: "",
competitorB: "",
rows: [
{ key: "price", name: "核心产品与定价", competitorA: "", competitorB: "", gap: "" },
{ key: "selling", name: "核心卖点", competitorA: "", competitorB: "", gap: "" },
{ key: "review", name: "用户评价分析(聚焦中差评)", competitorA: "", competitorB: "", gap: "" },
],
strategy: "",
},
dataSelection: {
rows: [
{ category: "电饭煲", growthRate: "5.17%", relativeShare: "101.75%", classification: "现金牛产品" },
{ category: "扫地机器人", growthRate: "24.41%", relativeShare: "143.95%", classification: "明星产品" },
{ category: "咖啡机", growthRate: "18.67%", relativeShare: "37.64%", classification: "问题产品" },
{ category: "电动牙刷", growthRate: "2.36%", relativeShare: "41.74%", classification: "瘦狗产品" },
],
reflection: "",
},
swot: {
strengths: "",
weaknesses: "",
opportunities: "",
threats: "",
conclusion: "",
},
};
}
const form = ref(createForm());
const taskTitle = computed(() => taskConfig.value?.taskName || "市场机会识别与选品");
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
const flowSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, []));
const steps = computed(() =>
flowSteps.value.map((name, index) => ({
no: index + 1,
name,
}))
);
const activeStepName = computed(() => steps.value[currentStep.value - 1]?.name || "");
watch(steps, (items) => {
if (items.length && currentStep.value > items.length) {
currentStep.value = items.length;
}
});
const progressItems = computed(() => [
{ text: "进行中:PEST 四维度分析", status: "doing" },
{
text: isPestStarted.value ? "已填写:市场机会与风险" : "待完成:市场机会与风险",
status: isPestStarted.value ? "done" : "",
},
{ text: "待完成:导出实训结果", status: "" },
]);
const isPestStarted = computed(() =>
Object.values(form.value.pest).some((item) => item.factor?.trim() || item.opportunity?.trim())
);
onMounted(async () => {
await loadTaskConfig();
await loadSavedAnswer();
});
async function loadTaskConfig() {
try {
const res = await getTrainingTaskByKey(TASK_KEY);
taskConfig.value = res?.data || null;
} catch (error) {
taskConfig.value = null;
}
}
async function loadSavedAnswer() {
try {
const res = await getStudentTrainingAnswer(TASK_KEY);
const answer = res?.data;
if (answer?.step1Answer) {
applySavedAnswer(answer.step1Answer);
}
} catch (error) {
// 保持页面可用,后端异常时不阻断学生填写。
}
}
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) {
const list = String(value)
.split(/[;;|]/)
.map((item) => item.trim())
.filter(Boolean);
return list.length ? list : fallback;
}
}
function applySavedAnswer(value) {
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object") return;
const base = createForm();
form.value = {
...base,
...parsed,
pest: {
...base.pest,
...(parsed.pest || {}),
},
competitor: {
...base.competitor,
...(parsed.competitor || {}),
},
dataSelection: {
...base.dataSelection,
...(parsed.dataSelection || {}),
},
swot: {
...base.swot,
...(parsed.swot || {}),
},
};
currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), 4);
} catch (error) {
// 忽略历史异常数据。
}
}
function buildAnswerPayload(status) {
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
saveAction,
currentStep: currentStep.value,
step1Answer: JSON.stringify({
taskKey: TASK_KEY,
taskName: taskTitle.value,
taskId: taskConfig.value?.id || "",
currentStep: currentStep.value,
flowSteps: flowSteps.value,
...form.value,
}),
};
}
async function persist(status = "IN_PROGRESS", silent = false) {
saving.value = true;
try {
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
if (!silent) {
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
}
} catch (error) {
if (!silent) {
proxy?.$modal?.msgError?.(status === "SUBMITTED" ? "提交失败" : "保存失败");
}
} finally {
saving.value = false;
}
}
function saveCurrentProgress() {
return persist("IN_PROGRESS");
}
function changeStep(targetStep) {
const maxStep = steps.value.length;
if (!maxStep) return;
const next = Math.min(Math.max(Number(targetStep) || 1, 1), maxStep);
if (next === currentStep.value) return;
currentStep.value = next;
void persist("IN_PROGRESS", true);
}
function previousStep() {
changeStep(currentStep.value - 1);
}
function nextStep() {
changeStep(currentStep.value + 1);
}
function submitTask() {
return persist("SUBMITTED");
}
async function resetTraining() {
form.value = createForm();
currentStep.value = 1;
await persist("RESET");
}
function exportOutcome() {
const content = {
taskName: taskTitle.value,
flowSteps: flowSteps.value,
currentStep: currentStep.value,
...form.value,
};
const blob = new Blob([JSON.stringify(content, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${taskTitle.value || "市场机会识别与选品"}-实训结果.json`;
link.click();
URL.revokeObjectURL(url);
}
</script>
<style lang="scss" scoped>
.market-selection-page {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(720px, 1fr) 340px;
gap: 24px;
width: 100%;
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 24px 24px 32px;
color: #1e3a5f;
background: #ffffff;
}
.training-core {
padding: 28px;
border: 1px solid #cfe2f5;
border-radius: 36px;
background: #f0f6fd;
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(30, 136, 229, 0.05);
backdrop-filter: blur(8px);
}
.task-header {
margin-bottom: 24px;
}
.task-badge {
display: inline-flex;
margin-bottom: 12px;
padding: 4px 14px;
border: 1px solid #cfe2f5;
border-radius: 30px;
color: #1e3a5f;
background: #e3f2fd;
font-size: var(--student-fs-caption);
font-weight: 700;
}
.task-title {
margin: 0;
color: transparent;
background: linear-gradient(135deg, #1565c0, #1e88e5);
-webkit-background-clip: text;
background-clip: text;
font-size: var(--student-fs-display);
line-height: 1.3;
font-weight: 900;
}
.goal-bg,
.task-card,
.step-card {
margin-bottom: 28px;
padding: 18px 22px;
border: 1px solid #cfe2f5;
border-radius: 26px;
background: #f0f6fd;
}
.goal-bg {
p,
li {
color: #2c4a6a;
font-size: var(--student-fs-body);
line-height: 1.8;
}
p,
ol {
margin: 0;
}
ol {
padding-left: 18px;
}
}
.subtitle-icon {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: #5a7a9a;
font-weight: 800;
}
.subtitle-icon--spaced {
margin-top: 12px;
}
.workbench-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid #cfe2f5;
h3 {
margin: 4px 0 8px;
color: #1e3a5f;
font-size: var(--student-fs-h1);
font-weight: 800;
line-height: 1.3;
}
}
.workbench-actions {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
margin-left: auto;
justify-content: flex-end;
gap: 10px;
}
.section-eyebrow {
margin: 0;
color: #1e88e5;
font-size: var(--student-fs-caption);
font-weight: 800;
line-height: 1.4;
}
.workbench-description {
margin: 0;
color: #2c4a6a;
font-size: var(--student-fs-body);
font-weight: 400;
line-height: 1.8;
}
.outline-action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0 18px;
border: 1px solid #cfe2f5;
border-radius: 999px;
color: #1e3a5f;
background: #e3f2fd;
font-size: var(--student-fs-button);
font-weight: 800;
&:hover:not(.is-disabled) {
transform: translateY(-1px);
border-color: #1e88e5;
background: #bbdefb;
}
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
border: 1px solid #cfe2f5;
border-radius: 18px;
background:
linear-gradient(90deg, #f0f6fd, #f0f6fd),
#f0f6fd;
box-shadow: inset 0 1px 0 rgba(30, 136, 229, 0.05);
overflow-x: auto;
}
.step-tab {
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid #cfe2f5;
border-radius: 8px;
color: #5a7a9a;
background: #f0f6fd;
box-shadow: inset 0 1px 0 rgba(30, 136, 229, 0.03);
font-family: inherit;
font-size: var(--student-fs-body);
font-weight: 800;
cursor: pointer;
text-align: left;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
&:hover {
border-color: #1e88e5;
color: #1e3a5f;
background: #bbdefb;
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(30, 136, 229, 0.1);
transform: translateY(-2px);
.step-action {
color: #1e88e5;
}
}
&:focus-visible {
outline: 2px solid #1e88e5;
outline-offset: 4px;
}
&.active {
border-color: #1e88e5;
color: #ffffff;
background: linear-gradient(115deg, #1e88e5, #1565c0);
box-shadow: 0 0 18px rgba(30, 136, 229, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
.step-marker {
border-color: #1e88e5;
color: #ffffff;
background: linear-gradient(135deg, #1e88e5, #1565c0);
box-shadow: 0 0 0 5px rgba(30, 136, 229, 0.15), 0 0 18px rgba(30, 136, 229, 0.4);
}
}
&.complete {
color: #2c4a6a;
.step-marker {
border-color: #1e88e5;
color: #1e3a5f;
background: #bbdefb;
box-shadow: 0 0 12px rgba(30, 136, 229, 0.25);
}
}
}
.step-marker {
display: inline-flex;
flex: 0 0 38px;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid #cfe2f5;
border-radius: 50%;
color: #5a7a9a;
background: #f0f6fd;
box-shadow: inset 0 0 12px rgba(30, 136, 229, 0.06);
font-size: var(--student-fs-caption);
font-weight: 900;
letter-spacing: 0.04em;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-copy {
display: grid;
gap: 4px;
min-width: 0;
margin-left: 10px;
}
.step-name {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: var(--student-fs-body);
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.step-action {
color: #1e88e5;
font-size: var(--student-fs-caption);
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
}
.step-connector {
position: relative;
flex: 0 0 32px;
min-width: 18px;
height: 2px;
margin: 0 14px;
overflow: visible;
background: #cfe2f5;
}
.step-connector::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #1e88e5;
border-right: 1px solid #1e88e5;
content: "";
transform: translateY(-50%) rotate(45deg);
}
.step-tab.complete + .step-connector {
background: linear-gradient(90deg, #bbdefb, #1e88e5);
box-shadow: 0 0 9px rgba(30, 136, 229, 0.3);
&::after {
border-color: #1e88e5;
}
}
.step-card {
margin-bottom: 0;
}
.step-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #1e3a5f;
font-size: var(--student-fs-h2);
font-weight: 900;
line-height: 1.3;
}
}
.task-desc {
margin: 0 0 20px;
color: #2c4a6a;
font-size: var(--student-fs-body);
line-height: 1.8;
}
.pest-table-wrap {
margin-top: 12px;
}
.table-title {
margin-bottom: 12px;
color: #2c4a6a;
font-size: var(--student-fs-body);
font-weight: 800;
text-align: left;
}
.pest-table {
overflow: hidden;
border: 1px solid #cfe2f5;
border-radius: 22px;
background: #f0f6fd;
}
.pest-row {
display: grid;
grid-template-columns: 170px minmax(0, 1fr) minmax(0, 1fr);
border-top: 1px solid #cfe2f5;
> div,
> textarea {
min-height: 132px;
border: 0;
border-right: 1px solid #cfe2f5;
}
> :last-child {
border-right: 0;
}
}
.pest-head {
border-top: 0;
background: #bbdefb;
> div {
display: flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 10px 12px;
color: #1e3a5f;
font-weight: 900;
text-align: center;
}
}
.dimension-cell {
display: flex;
align-items: center;
justify-content: center;
padding: 14px;
color: #1e3a5f;
font-weight: 900;
}
.pest-row textarea {
width: 100%;
padding: 14px 16px;
outline: none;
resize: vertical;
color: #1e3a5f;
background: #ffffff;
font-size: var(--student-fs-body);
line-height: 1.65;
transition: background 0.16s ease, box-shadow 0.16s ease;
&::placeholder {
color: #5a7a9a;
opacity: 0.9;
}
&:focus {
background: #ffffff;
box-shadow: inset 0 0 0 2px #1e88e5;
}
}
.competitor-fields {
display: flex;
flex-wrap: wrap;
gap: 40px;
margin-bottom: 18px;
label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #2c4a6a;
font-weight: 800;
}
input {
width: 190px;
min-height: 42px;
padding: 0 14px;
border: 1px solid #cfe2f5;
border-radius: 14px;
color: #1e3a5f;
background: #ffffff;
outline: none;
}
}
.training-table-wrap {
overflow-x: auto;
border: 1px solid #cfe2f5;
border-radius: 22px;
background: #f0f6fd;
}
.training-data-table {
width: 100%;
min-width: 820px;
border-collapse: collapse;
th,
td {
border: 1px solid #cfe2f5;
padding: 12px;
vertical-align: top;
}
th {
color: #1e3a5f;
background: #bbdefb;
font-size: var(--student-fs-body);
font-weight: 900;
text-align: center;
}
td {
color: #2c4a6a;
background: #f0f6fd;
}
}
.compact-table {
min-width: 720px;
}
.field-block {
display: grid;
gap: 8px;
margin-top: 18px;
margin-bottom: 14px;
color: #2c4a6a;
font-size: var(--student-fs-body);
font-weight: 800;
> span {
line-height: 1.6;
}
}
.input-field,
.cell-input,
.cell-textarea,
.cell-select {
width: 100%;
border: 1px solid #cfe2f5;
border-radius: 14px;
color: #1e3a5f;
background: #ffffff;
font-family: inherit;
font-size: var(--student-fs-body-sm);
outline: none;
transition: 0.2s;
&:focus {
border-color: #1e88e5;
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.12);
}
&::placeholder {
color: #5a7a9a;
}
}
.input-field {
min-height: 110px;
padding: 12px 16px;
line-height: 1.8;
resize: vertical;
}
.cell-input,
.cell-select {
min-height: 36px;
padding: 8px 10px;
}
.small-input {
max-width: 110px;
text-align: center;
}
.cell-textarea {
min-height: 96px;
padding: 10px;
line-height: 1.7;
resize: vertical;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
}
.training-actions :deep(.training-action) {
min-width: 172px;
height: 64px;
padding: 0 28px;
border: 1px solid #1e88e5;
border-radius: 6px;
color: #2c4a6a;
background: #e3f2fd;
box-shadow: none;
font-size: var(--student-fs-h1);
font-weight: 900;
line-height: 1;
letter-spacing: 0;
text-shadow: none;
}
.training-actions :deep(.training-action span) {
display: inline-flex;
align-items: center;
color: inherit;
font-size: inherit;
line-height: 1;
}
.training-actions :deep(.training-action--submit),
.training-actions :deep(.training-action--reset),
.training-actions :deep(.training-action--export) {
border-color: #1e88e5;
color: #2c4a6a;
background: #e3f2fd;
}
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #1565c0;
color: #1e3a5f;
background: #bbdefb;
}
.step-actions {
display: flex;
justify-content: center;
gap: 14px;
margin-top: 22px;
}
.step-actions :deep(.step-action-btn) {
min-height: 40px;
padding: 0 18px;
border-radius: 999px;
font-size: var(--student-fs-button);
font-weight: 800;
}
.step-actions :deep(.step-action-btn--previous) {
border-color: #cfe2f5;
color: #1e3a5f;
background: #e3f2fd;
}
.step-actions :deep(.step-action-btn--save),
.step-actions :deep(.step-action-btn--next) {
border-color: #1e88e5;
color: #ffffff;
background: linear-gradient(95deg, #1e88e5, #1565c0);
}
.step-actions :deep(.step-action-btn:hover:not(.is-disabled)) {
transform: translateY(-1px);
}
.step-actions :deep(.step-action-btn.is-disabled) {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 1100px) {
.market-selection-page {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
.workbench-actions :deep(.material-download),
.workbench-actions .outline-action {
width: 100%;
}
.market-selection-page {
padding: 14px 14px 18px;
}
.step-indicator {
width: 100%;
margin: 0 0 32px;
padding: 16px;
overflow: visible;
flex-direction: column;
align-items: stretch;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
min-width: 0;
}
.step-connector {
flex: 0 0 20px;
align-self: flex-start;
width: 2px;
min-width: 0;
height: 20px;
margin: 4px 0 4px 18px;
}
.step-connector::after {
top: auto;
right: auto;
bottom: -1px;
left: 50%;
transform: translateX(-50%) rotate(135deg);
}
.step-actions,
.training-actions {
gap: 14px;
}
.step-actions :deep(.step-action-btn),
.training-actions :deep(.training-action) {
width: 100%;
}
}
@media (max-width: 760px) {
.pest-table {
overflow-x: auto;
}
.pest-row {
grid-template-columns: 110px repeat(2, minmax(220px, 1fr));
min-width: 650px;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>