<script setup>
import { computed, nextTick, ref } from "vue";
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import TrainingDifficultyBadge from "./components/TrainingDifficultyBadge.vue";
import TrainingDifficultyPreviewCard from "./components/TrainingDifficultyPreviewCard.vue";
import TrainingDifficultyStartButton from "./components/TrainingDifficultyStartButton.vue";
import audioManager from "@/audioManager";
import {
  createPractiseV2API,
  endPractiseAPI,
  getTrainingDifficultyListAPI,
} from "@/apis";

// 难度页接口数据源：
// 1. 接口：GET /training/difficulty/list?type=base/endurance/precision/rhythm
// 2. 当前进度：接口 user_levels / list.completed，路由参数可覆盖选中难度
const trainingDifficultyStorageKey = "training-selection";
const defaultTrainingType = "precision";
const defaultUnlockedDifficultyId = "lv1";
const trainingTypeMetaMap = {
  base: {
    key: "base",
    title: "基础训练",
  },
  endurance: {
    key: "endurance",
    title: "耐力训练",
  },
  precision: {
    key: "precision",
    title: "精准训练",
  },
  rhythm: {
    key: "rhythm",
    title: "节奏训练",
  },
};
const routeModeTypeMap = {
  basic: "base",
  base: "base",
  endurance: "endurance",
  precision: "precision",
  rhythm: "rhythm",
};
const defaultTargetType = 1;

const resolveTrainingType = (mode) => {
  const normalizedMode = String(mode || "").toLowerCase();
  return routeModeTypeMap[normalizedMode] || defaultTrainingType;
};

const createDifficultyId = (level) => `lv${level}`;

const toNumber = (value, fallback = 0) => {
  const numberValue = Number(value);
  return Number.isFinite(numberValue) ? numberValue : fallback;
};

const clampProgress = (value) => {
  return Math.min(Math.max(value, 0), 100);
};

const getDifficultyProgress = (item = {}) => {
  const completedCnt = toNumber(item.completed_cnt);
  const promoteCnt = toNumber(item.promote_cnt);

  if (item.completed) {
    return 100;
  }

  if (promoteCnt <= 0) {
    return 0;
  }

  return clampProgress(Math.round((completedCnt / promoteCnt) * 100));
};

const checkDifficultyCompleted = (item = {}) => {
  const completedCnt = toNumber(item.completed_cnt);
  const promoteCnt = toNumber(item.promote_cnt);

  return Boolean(item.completed) || (promoteCnt > 0 && completedCnt >= promoteCnt);
};

const getDifficultyModeText = (mode) => {
  return Number(mode) === 1 ? "随机区域+指定环数" : "随机区域命中";
};

const createEmptyModeConfig = (type = defaultTrainingType) => {
  const meta = trainingTypeMetaMap[type] || trainingTypeMetaMap[defaultTrainingType];

  return {
    key: meta.key,
    title: meta.title,
    nodes: [],
    details: {},
    activeDifficultyId: defaultUnlockedDifficultyId,
    progressMap: {},
  };
};

const createDifficultySummary = (item = {}) => {
  const desc = String(item.desc || "").trim();
  const type = item.type;
  const arrows = toNumber(item.arrows);
  const timeLimit = toNumber(item.time_limit);
  const hitReq = toNumber(item.hit_req);
  const totalReq = toNumber(item.total_req);
  const blocks = toNumber(item.blocks);
  const promoteCnt = toNumber(item.promote_cnt);
  const timeText = timeLimit > 0 ? `${timeLimit}秒内完成` : "不限时完成";
  const promoteText = promoteCnt > 0 ? `完成${promoteCnt}次晋级` : "";

  const summaryMap = {
    base: [
      desc || (hitReq > 0 ? `每箭命中${hitReq}环以上` : "上靶即可"),
      [`${arrows}箭`, promoteText].filter(Boolean).join(" · "),
    ],
    endurance: [
      desc || `${timeText}${arrows}箭`,
      [`累计${totalReq}环`, promoteText].filter(Boolean).join(" · "),
    ],
    precision: [
      desc || `命中${blocks}个指定区域`,
      [
        `${arrows}箭`,
        timeText,
        getDifficultyModeText(item.mode),
        promoteText,
      ]
        .filter(Boolean)
        .join(" · "),
    ],
    rhythm: [
      desc || `间隔${timeLimit}秒射击`,
      [
        `${arrows}箭`,
        hitReq > 0 ? `每箭${hitReq}环以上` : "上靶即可",
        getDifficultyModeText(item.mode),
        promoteText,
      ]
        .filter(Boolean)
        .join(" · "),
    ],
  };

  return (summaryMap[type] || [desc]).filter(Boolean);
};

const normalizeTrainingDifficultyConfig = (result, type) => {
  const meta = trainingTypeMetaMap[type] || trainingTypeMetaMap[defaultTrainingType];
  const list = Array.isArray(result?.list) ? result.list : [];
  const rawItems = list.filter((item) => !item?.type || item.type === meta.key);
  const difficultyItems = rawItems
    .map((item) => {
      const level = toNumber(item?.difficulty);

      if (level <= 0) {
        return null;
      }

      const id = createDifficultyId(level);
      const label = `Lv${level}`;

      return {
        ...item,
        recordId: item.id,
        completedCnt: toNumber(item.completed_cnt),
        promoteCnt: toNumber(item.promote_cnt),
        id,
        level,
        label,
        title: `${label}难度`,
        summary: createDifficultySummary(item),
        startText: "开始",
        targetPaperType: "20CM全环靶",
      };
    })
    .filter(Boolean)
    .sort((first, second) => first.level - second.level);
  const maxLevel = difficultyItems.reduce(
    (currentMax, item) => Math.max(currentMax, item.level),
    0
  );
  const completedLevelFromList = difficultyItems.reduce((currentMax, item) => {
    return checkDifficultyCompleted(item)
      ? Math.max(currentMax, item.level)
      : currentMax;
  }, 0);
  const userCompletedLevel = toNumber(result?.user_levels?.[meta.key]);
  const highestCompletedLevel = userCompletedLevel || completedLevelFromList;
  const unlockedLevel = maxLevel
    ? Math.min(Math.max(highestCompletedLevel + 1, 1), maxLevel)
    : 1;

  return {
    key: meta.key,
    title: meta.title,
    nodes: difficultyItems.map((item) => ({
      id: item.id,
      label: item.label,
    })),
    details: Object.fromEntries(
      difficultyItems.map((item) => [item.id, item])
    ),
    activeDifficultyId: createDifficultyId(unlockedLevel),
    progressMap: Object.fromEntries(
      difficultyItems.map((item) => [item.id, getDifficultyProgress(item)])
    ),
  };
};

// 难度轴布局参数，节点按“等级越低越靠下”的方式排列。
const nodesLayout = {
  viewportHeightRpx: 1020,
  topPaddingRpx: 136,
  bottomPaddingRpx: 144,
  verticalGapRpx: 188,
  anchorOffsetRpx: 796,
  horizontalPatternRpx: [388, 232, 516, 258, 458, 304],
  nearHorizontalDistanceRpx: 170,
  extraGapScale: 0.5,
};

const emptyDifficulty = {
  id: "",
  label: "",
  title: "",
  summary: [],
  startText: "开始",
  targetPaperType: "",
};

// 页面基础状态
const pageConfig = ref(createEmptyModeConfig(defaultTrainingType));
const unlockedDifficultyId = ref(defaultUnlockedDifficultyId);
const selectedDifficultyId = ref(defaultUnlockedDifficultyId);
const nodesScrollTop = ref(0);
const nodesScrollWithAnimation = ref(false);
const routeOptions = ref({});
const shouldRefreshOnShow = ref(false);
const creatingPractice = ref(false);
let pageStateRequestGeneration = 0;

const difficultyProgressMap = computed(() => {
  return pageConfig.value?.progressMap || {};
});

const clamp = (value, min, max) => {
  return Math.min(Math.max(value, min), max);
};

// 从 lv1 / lv20 这类 id 中提取等级数值，统一用于排序、解锁判断和进度比较。
const getDifficultyLevel = (difficultyId = "") => {
  const matched = String(difficultyId).match(/\d+/);
  return matched ? Number(matched[0]) : 0;
};

// 合并节点基础信息和难度详情，并统一按等级升序整理。
const createDifficultyNodes = (config) => {
  const details = config?.details || {};
  const rawNodes = Array.isArray(config?.nodes) ? config.nodes : [];
  const nodeMap = new Map(rawNodes.map((node) => [node.id, node]));
  const difficultyIds = new Set([
    ...rawNodes.map((node) => node.id),
    ...Object.keys(details),
  ]);

  return Array.from(difficultyIds)
    .map((difficultyId) => {
      const level = getDifficultyLevel(difficultyId);
      const node = nodeMap.get(difficultyId) || {};
      const detail = details[difficultyId] || {};
      const label = node.label || detail.label || `Lv${level || ""}`;

      return {
        ...node,
        ...detail,
        id: difficultyId,
        level,
        label,
        title: detail.title || `${label}难度`,
        summary: Array.isArray(detail.summary) ? detail.summary : [],
        startText: detail.startText || "开始",
        targetPaperType: detail.targetPaperType || "",
      };
    })
    .filter((node) => node.id && node.level > 0)
    .sort((first, second) => first.level - second.level);
};

const findValidDifficultyId = (difficultyId, nodes) => {
  return nodes.some((node) => node.id === difficultyId) ? difficultyId : "";
};

const getNextDifficultyId = (difficultyId, nodes) => {
  const currentLevel = getDifficultyLevel(difficultyId);
  return (
    nodes.find((node) => node.level === currentLevel + 1)?.id || difficultyId
  );
};

// 统一解析当前最新已解锁难度：
// completedDifficultyId 优先级最高，可在完成当前难度后自动推进到下一关。
const resolveUnlockedDifficultyId = (options, config, nodes) => {
  const completedDifficultyId = findValidDifficultyId(
    options.completedDifficultyId,
    nodes
  );

  if (completedDifficultyId) {
    return getNextDifficultyId(completedDifficultyId, nodes);
  }

  return (
    [
      options.currentDifficultyId,
      options.latestDifficultyId,
      options.activeDifficultyId,
      config.activeDifficultyId,
      defaultUnlockedDifficultyId,
    ].find((difficultyId) => findValidDifficultyId(difficultyId, nodes)) ||
    nodes[0]?.id ||
    defaultUnlockedDifficultyId
  );
};

// 如果传入的默认选中项尚未解锁，则自动回退到当前最新已解锁难度。
const resolveSelectedDifficultyId = (difficultyId, nodes, currentUnlockedId) => {
  const safeDifficultyId = findValidDifficultyId(difficultyId, nodes);

  if (!safeDifficultyId) {
    return currentUnlockedId;
  }

  return getDifficultyLevel(safeDifficultyId) <=
    getDifficultyLevel(currentUnlockedId)
    ? safeDifficultyId
    : currentUnlockedId;
};

// 页面渲染使用的难度节点列表，包含纵向轨道坐标。
const difficultyNodes = computed(() => {
  const nodes = createDifficultyNodes(pageConfig.value);
  const leftPositions = nodes.map((_, index) => {
    return nodesLayout.horizontalPatternRpx[
      index % nodesLayout.horizontalPatternRpx.length
    ];
  });
  const offsetsFromBottom = [];
  let accumulatedOffsetRpx = 0;

  nodes.forEach((node, index) => {
    if (index > 0) {
      const previousLeftRpx = leftPositions[index - 1];
      const currentLeftRpx = leftPositions[index];
      const horizontalDistanceRpx = Math.abs(currentLeftRpx - previousLeftRpx);
      const extraGapRpx =
        Math.max(
          0,
          nodesLayout.nearHorizontalDistanceRpx - horizontalDistanceRpx
        ) * nodesLayout.extraGapScale;

      accumulatedOffsetRpx +=
        nodesLayout.verticalGapRpx + Math.round(extraGapRpx);
    }

    offsetsFromBottom.push(accumulatedOffsetRpx);
  });

  const contentHeightRpx = Math.max(
    nodesLayout.viewportHeightRpx,
    nodesLayout.topPaddingRpx +
      nodesLayout.bottomPaddingRpx +
      (offsetsFromBottom[offsetsFromBottom.length - 1] || 0)
  );

  return nodes.map((node, index) => {
    const leftRpx = leftPositions[index];
    const topRpx =
      contentHeightRpx -
      nodesLayout.bottomPaddingRpx -
      offsetsFromBottom[index];

    return {
      ...node,
      leftRpx,
      topRpx,
      style: {
        left: `${leftRpx}rpx`,
        top: `${topRpx}rpx`,
      },
    };
  });
});

const difficultyConnectors = computed(() => {
  const nodes = difficultyNodes.value;

  return nodes.slice(1).map((currentNode, index) => {
    const previousNode = nodes[index];
    const startX = Number(previousNode?.leftRpx || 0);
    const startY = Number(previousNode?.topRpx || 0);
    const endX = Number(currentNode?.leftRpx || 0);
    const endY = Number(currentNode?.topRpx || 0);
    const midX = (startX + endX) / 2;
    const midY = (startY + endY) / 2;
    const angle =
      (Math.atan2(endY - startY, endX - startX) * 180) / Math.PI + 90;

    return {
      id: `${previousNode.id}-${currentNode.id}`,
      left: `${midX}rpx`,
      top: `${midY}rpx`,
      transform: `translate(-50%, -50%) rotate(${angle}deg)`,
    };
  });
});

const nodesTrackHeightRpx = computed(() => {
  const bottomNode = difficultyNodes.value[0];

  if (!bottomNode) {
    return nodesLayout.viewportHeightRpx;
  }

  return Math.max(
    nodesLayout.viewportHeightRpx,
    bottomNode.topRpx + nodesLayout.bottomPaddingRpx
  );
});

const nodesTrackStyle = computed(() => {
  return {
    height: `${nodesTrackHeightRpx.value}rpx`,
  };
});

const selectedDifficulty = computed(() => {
  return (
    difficultyNodes.value.find((node) => node.id === selectedDifficultyId.value) ||
    difficultyNodes.value[0] ||
    emptyDifficulty
  );
});

const targetTypeImageMap = {
  20: "https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/20cm.png",
  40: "https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/40cm.png",
};

// 根据接口返回的 target_type 展示当前难度使用的靶纸类型。
const selectedTargetTypeImage = computed(() => {
  return targetTypeImageMap[toNumber(selectedDifficulty.value?.target_type)] || "";
});

// 优先显示配置中的难度进度；没有配置时，再按已解锁等级推导完成态。
const getCompletedDifficultyProgress = (node) => {
  const configuredProgress = Number(difficultyProgressMap.value[node?.id]);

  if (Number.isFinite(configuredProgress) && configuredProgress > 0) {
    return configuredProgress;
  }

  return getDifficultyLevel(node?.id) < getDifficultyLevel(unlockedDifficultyId.value)
    ? 100
    : 0;
};

const checkDifficultyLocked = (node) => {
  return getDifficultyLevel(node?.id) > getDifficultyLevel(unlockedDifficultyId.value);
};

// 根据目标难度计算 scroll-view 应滚动到的位置，顶部/底部会自动吸附边界。
const scrollToDifficulty = (difficultyId, animated = false) => {
  const node = difficultyNodes.value.find((item) => item.id === difficultyId);

  if (!node) {
    return;
  }

  const maxScrollRpx = Math.max(
    nodesTrackHeightRpx.value - nodesLayout.viewportHeightRpx,
    0
  );
  const targetScrollRpx = clamp(
    node.topRpx - nodesLayout.anchorOffsetRpx,
    0,
    maxScrollRpx
  );

  nodesScrollWithAnimation.value = animated;
  nodesScrollTop.value = uni.upx2px(targetScrollRpx);
};

// 首次进入页面需要静默定位到默认难度；
// 定位完成后再开启滚动动画，避免第一次手动切换时丢失过渡效果。
const initScrollPosition = () => {
  scrollToDifficulty(selectedDifficultyId.value, false);

  nextTick(() => {
    nodesScrollWithAnimation.value = true;
  });
};

const normalizeRouteOptions = (options = {}) => {
  const difficultyLevel = toNumber(options.difficulty);
  const completedDifficultyLevel = toNumber(options.completedDifficulty);

  return {
    ...options,
    difficultyId:
      options.difficultyId ||
      (difficultyLevel > 0 ? createDifficultyId(difficultyLevel) : ""),
    completedDifficultyId:
      options.completedDifficultyId ||
      (completedDifficultyLevel > 0
        ? createDifficultyId(completedDifficultyLevel)
        : ""),
  };
};

const applyPageState = (options = {}, config) => {
  const safeOptions = normalizeRouteOptions(options);
  const nodes = createDifficultyNodes(config);
  const currentUnlockedId = resolveUnlockedDifficultyId(
    safeOptions,
    config,
    nodes
  );

  pageConfig.value = config;
  unlockedDifficultyId.value = currentUnlockedId;
  selectedDifficultyId.value = resolveSelectedDifficultyId(
    safeOptions.difficultyId,
    nodes,
    currentUnlockedId
  );

  nextTick(() => {
    initScrollPosition();
  });
};

const initPageState = async (options = {}, refreshOptions = {}) => {
  const requestGeneration = ++pageStateRequestGeneration;
  const { keepCurrent = false } = refreshOptions;
  const trainingType = resolveTrainingType(options.mode);
  const fallbackConfig = createEmptyModeConfig(trainingType);

  if (!keepCurrent) {
    pageConfig.value = fallbackConfig;
  }

  try {
    const result = await getTrainingDifficultyListAPI(trainingType);
    if (requestGeneration !== pageStateRequestGeneration) {
      return;
    }

    applyPageState(
      options,
      normalizeTrainingDifficultyConfig(result, trainingType)
    );
  } catch (error) {
    if (requestGeneration !== pageStateRequestGeneration) {
      return;
    }

    console.log("training difficulty load failed", error);
    if (!keepCurrent) {
      applyPageState(options, fallbackConfig);
    }
    uni.showToast({
      title: "训练难度加载失败",
      icon: "none",
    });
  }
};

const cleanQueryValue = (value) => {
  if (value === undefined || value === null || value === "") {
    return "";
  }

  if (typeof value === "number" && !Number.isFinite(value)) {
    return "";
  }

  return value;
};

const createPracticeQuery = (difficulty) => {
  const trainingType = pageConfig.value.key || defaultTrainingType;
  const commonQuery = {
    type: trainingType,
    difficultyId: difficulty.id,
    difficulty: difficulty.level,
    recordId: difficulty.recordId,
    arrows: toNumber(difficulty.arrows, 12),
    target: defaultTargetType,
  };
  const typedQueryMap = {
    base: {
      hitReq: toNumber(difficulty.hit_req),
    },
    endurance: {
      totalReq: toNumber(difficulty.total_req),
    },
    precision: {
      blocks: toNumber(difficulty.blocks),
      mode: toNumber(difficulty.mode),
    },
    rhythm: {
      hitReq: toNumber(difficulty.hit_req),
      mode: toNumber(difficulty.mode),
    },
  };

  return {
    ...commonQuery,
    ...(typedQueryMap[trainingType] || {}),
  };
};

const createPracticeUrl = (difficulty) => {
  const query = Object.entries(createPracticeQuery(difficulty))
    .map(([key, value]) => [key, cleanQueryValue(value)])
    .filter(([, value]) => value !== "")
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
    .join("&");

  return `/pages/training/practise-one${query ? `?${query}` : ""}`;
};

const saveTrainingContext = (practice = {}) => {
  const difficulty = selectedDifficulty.value;

  if (!difficulty.id) {
    return;
  }

  uni.setStorageSync(trainingDifficultyStorageKey, {
    trainingType: pageConfig.value.key,
    trainingTitle: pageConfig.value.title,
    difficultyId: difficulty.id,
    difficultyLabel: difficulty.label,
    targetType: defaultTargetType,
    targetPaperType: difficulty.targetPaperType,
    practiceId: practice.id || "",
    serverAddr: practice.serverAddr || "",
    createdAt: practice.id ? Date.now() : 0,
  });
};

const navigateToPractice = (url) => {
  return new Promise((resolve, reject) => {
    uni.navigateTo({
      url,
      success: resolve,
      fail: reject,
    });
  });
};

const stopCreatedPractice = async (id) => {
  if (!id) return;

  try {
    await endPractiseAPI(id);
  } catch (error) {
    console.error("training practice cleanup failed", error);
  }
};

const handleSelectDifficulty = (node) => {
  if (!node?.id) {
    return;
  }

  if (checkDifficultyLocked(node)) {
    uni.showToast({
      title: "难度尚未解锁",
      icon: "none",
    });
    return;
  }

  audioManager.play("点击按钮");

  if (node.id === selectedDifficultyId.value) {
    return;
  }

  selectedDifficultyId.value = node.id;
  nextTick(() => {
    scrollToDifficulty(node.id, true);
  });
};

const handleStart = async () => {
  if (!selectedDifficulty.value.id || creatingPractice.value) {
    return;
  }

  const trainingType = pageConfig.value.key || defaultTrainingType;
  const difficultyLevel = selectedDifficulty.value.level;
  let createdPracticeId = "";

  creatingPractice.value = true;

  try {
    // 先由业务接口创建训练和比赛服，再把连接上下文交给目标页。
    const result = await createPractiseV2API(trainingType, difficultyLevel);
    createdPracticeId = result?.id || "";

    if (!createdPracticeId) {
      saveTrainingContext();
      uni.showToast({
        title: "训练创建失败，请重试",
        icon: "none",
      });
      return;
    }

    if (!String(result?.serverAddr || "").trim()) {
      await stopCreatedPractice(createdPracticeId);
      saveTrainingContext();
      uni.showToast({
        title: "练习连接信息异常，请重试",
        icon: "none",
      });
      return;
    }

    saveTrainingContext(result);
    await navigateToPractice(createPracticeUrl(selectedDifficulty.value));
    createdPracticeId = "";
  } catch (error) {
    await stopCreatedPractice(createdPracticeId);
    saveTrainingContext();
    console.error("training practice create failed", error);

    if (String(error?.errMsg || "").includes("navigateTo")) {
      uni.showToast({
        title: "进入训练失败，请重试",
        icon: "none",
      });
    }
  } finally {
    creatingPractice.value = false;
  }
};

onLoad((options = {}) => {
  routeOptions.value = { ...options };
  void initPageState(options);
});

onHide(() => {
  shouldRefreshOnShow.value = true;
});

onShow(() => {
  if (!shouldRefreshOnShow.value) return;

  shouldRefreshOnShow.value = false;
  void initPageState(routeOptions.value, {
    keepCurrent: true,
  });
});
</script>

<template>
  <Container
    :title="pageConfig.title"
    :bgType="8"
    bgColor="#1c1c23"
    :scroll="false"
    :loading="creatingPractice"
    loadingText="训练创建中..."
  >
    <view class="difficulty-page">
      <view class="difficulty-page__nodes">
        <scroll-view
          class="difficulty-page__nodes-scroll"
          scroll-y
          enhanced
          :scroll-top="nodesScrollTop"
          :scroll-with-animation="nodesScrollWithAnimation"
          :show-scrollbar="false"
        >
          <view class="difficulty-page__nodes-track" :style="nodesTrackStyle">
            <image
              v-for="connector in difficultyConnectors"
              :key="connector.id"
              class="difficulty-page__connector"
              src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/jiantou.png"
              mode="aspectFit"
              :style="connector"
            />
            <TrainingDifficultyBadge
              v-for="node in difficultyNodes"
              :key="node.id"
              :node="node"
              :active="node.id === selectedDifficultyId"
              :locked="checkDifficultyLocked(node)"
              :completedProgress="getCompletedDifficultyProgress(node)"
              @click="handleSelectDifficulty"
            />
          </view>
        </scroll-view>
      </view>

      <view class="difficulty-page__preview">
        <TrainingDifficultyPreviewCard
          :title="selectedDifficulty.title"
          :lines="selectedDifficulty.summary"
        />
        <image
          v-if="selectedTargetTypeImage"
          class="difficulty-page__target-type"
          :src="selectedTargetTypeImage"
          mode="aspectFit"
        />
      </view>

      <view class="difficulty-page__start">
        <TrainingDifficultyStartButton
          :text="selectedDifficulty.startText"
          @click="$clickSound(handleStart)"
        />
      </view>
    </view>
  </Container>
</template>

<style scoped>
.difficulty-page {
  height: 100%;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 18rpx 0 40rpx;
  overflow: hidden;
}

.difficulty-page__nodes {
  position: relative;
  flex: 1;
  min-height: 0;
  z-index: 2;
  margin-bottom: 8rpx;
}

.difficulty-page__nodes-scroll {
  width: 100%;
  height: 100%;
}

.difficulty-page__nodes-track {
  position: relative;
  width: 100%;
  min-height: 100%;
}

.difficulty-page__connector {
  position: absolute;
  width: 18rpx;
  height: 28rpx;
  z-index: 1;
  pointer-events: none;
}

.difficulty-page__preview {
  position: relative;
  flex: none;
  z-index: 3;
  width: 540rpx;
  height: 172rpx;
  margin: 0 auto;
}

.difficulty-page__target-type {
  position: absolute;
  top: 10rpx;
  left: 2rpx;
  z-index: 2;
  width: 100rpx;
  height: 74rpx;
  pointer-events: none;
}

.difficulty-page__start {
  position: relative;
  flex: none;
  z-index: 4;
  width: 302rpx;
  height: 190rpx;
  margin: 0 auto;
  top: -16rpx;
}
</style>
