<template>
  <Water></Water>
  <view class="detail-page" :style="{ minHeight: pageHeight }" v-if="!!data && !loading">
    <view id="detailPageTopAnchor"></view>
    <view class="detail-card detail-bg" :style="{ backgroundImage: 'url(' + bg + ')' }">
      <view class="detail-drug-title">{{ data.physicName }}</view>
      <view class="detail-row detail-traceability">
        <view class="detail-label">追溯码：</view>
        <view class="detail-value detail-wrap">{{ formatTraceCode(traceCode) }}</view>
        <view class="detail-copy-btn" @click="copy(traceCode)">
          <image src="../../../static/images/copy.png" mode="scaleToFill" :style="{ width: '30rpx', height: '30rpx' }" />
        </view>
      </view>

      <view class="detail-row">
        <view class="detail-label">生产企业：</view>
        <view
          class="detail-value detail-wrap detail-company-value"
          :class="{ 'detail-clamp': !companyExpanded }"
          :style="{ marginBottom: needCompanyToggle ? '40rpx' : undefined }"
          id="companyValue">
          <text>{{ data.produceEntName }}</text>
        </view>
        <text v-if="needCompanyToggle" class="detail-expand-tag" @click="toggleCompanyExpand">{{ companyExpanded ? "收起" : "展开" }}</text>
      </view>
      <view class="detail-row">
        <view class="detail-label">批准文号：</view>
        <view class="detail-value">{{ data.approveNo }}</view>
      </view>
      <view class="detail-row">
        <view class="detail-label">生产日期：</view>
        <view class="detail-value">{{ data.produceDate }}</view>
      </view>
      <view class="detail-row" v-show="cardExpanded">
        <view class="detail-label">产品批号：</view>
        <view class="detail-value">{{ data.produceBatchNo }}</view>
      </view>
      <view class="detail-row" v-show="cardExpanded">
        <view class="detail-label"> 剂<text :style="{ opacity: 0 }">剂型</text>型：</view>
        <view class="detail-value">{{ data.prepnDesc }}</view>
      </view>
      <view class="detail-row" v-show="cardExpanded">
        <view class="detail-label">包装规格：</view>
        <view class="detail-value">{{ data.prepnSpec }}</view>
      </view>
      <view class="detail-row" v-show="cardExpanded">
        <view class="detail-label">有效期至：</view>
        <view class="detail-value">{{ data.validEndDate }}</view>
      </view>
      <view class="detail-card-toggle" @click="toggleCardExpand">
        <uni-icons :type="cardExpanded ? 'up' : 'down'" size="18" color="#9aa4b2"></uni-icons>
      </view>
    </view>

    <view class="detail-timeline detail-bg" :style="{ backgroundImage: 'url(' + bg + ')' }">
      <view class="detail-section-header">
        <view class="detail-section-title">扫码跟踪</view>
        <view class="detail-section-actions">
          <view class="detail-section-action" @tap="toggleTimelineView">
            <uni-icons :type="timelineView === 'text' ? 'image' : 'list'" size="20" color="#59a8f2"></uni-icons>
            <text>{{ timelineView === "text" ? "查看图形" : "查看文字" }}</text>
          </view>
          <view class="detail-section-action" @tap="onSetDefaultView">
            <uni-icons type="gear" size="20" color="#59a8f2"></uni-icons>
            <text>设置默认</text>
          </view>
        </view>
      </view>
      <view class="detail-graph-panel" v-if="timelineView === 'graph'">
        <view class="graph-mode-row">
          <view class="graph-mode-left">
            <view class="graph-mode-title">货权流向模式</view>
            <view class="graph-mode-desc">开启后则会按照码的实际实物货权关系，将委托企业(若有)还原到收发货企业上并绘制流向图。</view>
          </view>
          <switch :checked="ownershipFlowMode" color="#2c69ff" @change="onOwnershipModeChange" />
        </view>
        <view
          :style="{
            width: graphViewWidth + 'px',
            height: graphViewHeight + 'px',
          }"
          v-if="isShowGraph">
          <l-echart ref="chartRef" :beforeDelay="180" :lStyle="`width:${graphViewWidth}px;height:${graphViewHeight}px;`" @finished="initChart"></l-echart>
        </view>
        <view
          :style="{
            width: graphViewWidth + 'px',
            height: graphViewHeight + 'px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }"
          v-else>
          链路过长无法展示
        </view>
      </view>
      <view class="detail-timeline-toolbar" v-if="timelineView === 'text'">
        <view class="detail-toolbar-btn" @tap.stop="setAllTimelineDetail(!allTimelineDetail)">{{ !allTimelineDetail ? "全部展示详情" : "全部展示概要" }}</view>
        <!-- <view class="detail-toolbar-btn" @tap.stop="setAllTimelineDetail(false)">全部展示概要</view> -->
      </view>
      <view
        v-if="timelineView === 'text'"
        class="detail-timeline-item"
        :class="{ 'detail-timeline-item-first': i === 0 }"
        v-for="(value, i) in visibleBillDetailList"
        :key="i">
        <view class="detail-left">
          <view class="detail-dot" :class="{ 'detail-fill': false }"></view>
          <view class="detail-line" v-if="i < visibleBillDetailList.length - 1 || hasMoreTimelineItems"></view>
        </view>
        <view class="detail-right">
          <view class="detail-title-row">
            <view class="detail-title" :class="{ 'detail-bold': true }"> {{ value.billTypeName || "--" }} {{ value.billCode || "" }} </view>
            <view class="detail-item-toggle" @tap.stop="toggleTimelineItem(i)">
              {{ value._showDetail ? "概要" : "详情" }}
              <uni-icons :type="value._showDetail ? 'up' : 'down'" size="14" color="#888"></uni-icons>
            </view>
          </view>
          <view class="detail-info-row">
            <text class="detail-info-label">发货企业：</text>
            <text class="detail-info-value">{{ value.fromEntName || "--" }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.fromEntType !== '1'">
            <text class="detail-tag" v-if="value.fromCustomerNature">{{ value.fromCustomerNature }}</text>
            <text class="detail-tag" v-if="value.fromCustomerLevel">{{ value.fromCustomerLevel }}</text>
            <text class="detail-tag" v-if="value.fromResponsibleManager">{{ value.fromResponsibleManager }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail">
            <text class="detail-tag" :style="getTagColorStyle(value.fromEntTypeName)" v-if="value.fromEntTypeName">{{ value.fromEntTypeName }}</text>
            <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'from')">{{ getRegionByPrefix(value, "from") }}</text>
          </view>
          <view class="detail-info-row">
            <text class="detail-info-label">收货企业：</text>
            <text class="detail-info-value">{{ value.toEntName || "--" }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.toEntType !== '1'">
            <text class="detail-tag" v-if="value.toCustomerNature">{{ value.toCustomerNature }}</text>
            <text class="detail-tag" v-if="value.toCustomerLevel">{{ value.toCustomerLevel }}</text>
            <text class="detail-tag" v-if="value.toResponsibleManager">{{ value.toResponsibleManager }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail">
            <text class="detail-tag" :style="getTagColorStyle(value.toEntTypeName)" v-if="value.toEntTypeName">{{ value.toEntTypeName }}</text>
            <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'to')">{{ getRegionByPrefix(value, "to") }}</text>
          </view>
          <view class="detail-info-row">
            <text class="detail-info-label">入出库时间：</text>
            <text class="detail-info-value">{{ value.billTime || "--" }}</text>
          </view>
          <view class="detail-info-row" v-if="value._showDetail && value.uploadEntName">
            <text class="detail-info-label">上传企业：</text>
            <text class="detail-info-value">{{ value.uploadEntName }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.uploadEntName && value.uploadEntType !== '1'">
            <text class="detail-tag" v-if="value.uploadCustomerNature">{{ value.uploadCustomerNature }}</text>
            <text class="detail-tag" v-if="value.uploadCustomerLevel">{{ value.uploadCustomerLevel }}</text>
            <text class="detail-tag" v-if="value.uploadResponsibleManager">{{ value.uploadResponsibleManager }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.uploadEntName">
            <text class="detail-tag" :style="getTagColorStyle(value.uploadEntTypeName)" v-if="value.uploadEntTypeName">{{ value.uploadEntTypeName }}</text>
            <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'upload')">{{ getRegionByPrefix(value, "upload") }}</text>
          </view>
          <view class="detail-info-row" v-if="value._showDetail && value.uploadEntName">
            <text class="detail-info-label">上传时间：</text>
            <text class="detail-info-value">{{ value.uploadTime || "--" }}</text>
          </view>
          <view class="detail-info-row" v-if="value._showDetail && value.disEntName">
            <text class="detail-info-label">配送企业：</text>
            <text class="detail-info-value">{{ value.disEntName }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.disEntName && !value.disEntName.includes(',') && value.disEntType !== '1'">
            <text class="detail-tag" v-if="value.disCustomerNature">{{ value.disCustomerNature }}</text>
            <text class="detail-tag" v-if="value.disCustomerLevel">{{ value.disCustomerLevel }}</text>
            <text class="detail-tag" v-if="value.disResponsibleManager">{{ value.disResponsibleManager }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.disEntName && !value.disEntName.includes(',')">
            <text class="detail-tag" :style="getTagColorStyle(value.disEntTypeName)" v-if="value.disEntTypeName">{{ value.disEntTypeName }}</text>
            <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'dis')">{{ getRegionByPrefix(value, "dis") }}</text>
          </view>
          <view class="detail-info-row" v-if="value._showDetail && value.assEntName">
            <text class="detail-info-label">委托企业：</text>
            <text class="detail-info-value">{{ value.assEntName }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.assEntName && value.assEntType !== '1'">
            <text class="detail-tag" v-if="value.assCustomerNature">{{ value.assCustomerNature }}</text>
            <text class="detail-tag" v-if="value.assCustomerLevel">{{ value.assCustomerLevel }}</text>
            <text class="detail-tag" v-if="value.assResponsibleManager">{{ value.assResponsibleManager }}</text>
          </view>
          <view class="detail-tag-row" v-if="value._showDetail && value.assEntName">
            <text class="detail-tag" :style="getTagColorStyle(value.assEntTypeName)" v-if="value.assEntTypeName">{{ value.assEntTypeName }}</text>
            <text class="detail-tag detail-tag-region" v-if="getRegionByPrefix(value, 'ass')">{{ getRegionByPrefix(value, "ass") }}</text>
          </view>
        </view>
      </view>
      <view v-if="timelineView === 'text' && hasMoreTimelineItems" class="detail-timeline-loadmore">
        上拉加载更多（{{ visibleBillDetailList.length }}/{{ timelineTotalCount }}）
      </view>
    </view>
  </view>
  <transition name="back-top-fade">
    <view v-if="!!data && !loading && showBackTopButton" class="detail-back-top-btn" @tap="backToTop">
      <view class="detail-back-top-arrow"></view>
    </view>
  </transition>
  <view class="detail-page-empty" :style="{ height: pageHeight }" v-if="!data && !loading">
    <image src="../../../static/images/empty.png" mode="scaleToFill" :style="{ width: '264rpx', height: '200rpx' }" />
    <text
      :style="{
        fontSize: '24rpx',
        color: '#99a1ad',
        marginTop: '60rpx',
        textAlign: 'center',
      }"
      >所查询药品所在生产企业尚未纳入扫码范围，后续将进一步扩大，敬请期待</text
    >
  </view>
  <view class="detail-page-empty" :style="{ height: pageHeight }" v-if="loading">
    <image src="../../../static/images/loading.png" mode="scaleToFill" :style="{ width: '50rpx', height: '50rpx' }" class="detail-loading" />
  </view>
</template>

<script>
import bg from "../../../static/images/bg.png";
import { formatDate } from "../../../utils/utils.js";
import request from "../../../request/index.js";
import Water from "@/components/water/water.vue";
import lEchart from "../../../uni_modules/lime-echart/components/l-echart/l-echart.vue";
import { markRaw } from "vue";
const echarts = require("../../../static/js/echarts.min.js");
export default {
  components: {
    Water,
    lEchart,
  },
  data() {
    return {
      data: null,
      loading: true,
      statusBarHeight: 20,
      traceCode: "",
      pageHeight: 0,
      bg,
      companyExpanded: true,
      needCompanyToggle: false,
      cardExpanded: false,
      allTimelineDetail: false,
      timelineView: "text",
      defaultTimelineViewKey: "traceabilityCodeQuery.detail.timelineDefaultView",
      imageData: [],
      graphViewWidth: 300,
      graphViewHeight: 480,
      chartInstance: null,
      chartOption: {},
      ownershipFlowMode: false,
      timelinePageSize: 20,
      timelineVisibleCount: 20,
      scrollTopValue: 0,
      scrollIntoViewId: "",
      backTopThreshold: 0,
      showBackTopButton: false,
      backTopHideTimer: null,
    };
  },
  onLoad(options) {
    const info = uni.getSystemInfoSync();
    this.statusBarHeight = info.statusBarHeight || 20;
    this.pageHeight = (info.windowHeight && info.windowHeight + "px") || "100%";
    this.backTopThreshold = info.windowHeight || 667;
    this.graphViewWidth = Math.max((info.windowWidth || 375) - 36, 280);
    this.graphViewHeight = Math.max((info.windowHeight || 667) - 200, 420);
    if (options.traceCode) {
      this.traceCode = options.traceCode;
    }
    const defaultView = uni.getStorageSync(this.defaultTimelineViewKey);
    if (defaultView === "graph" || defaultView === "text") {
      this.timelineView = defaultView;
    }
    // this.getData(options.traceCode);
    this.getData2(options.traceCode);
  },
  onReady() {
    this.$nextTick(() => {
      const info = uni.getSystemInfoSync();
      const lineHeightPx = (info.windowWidth / 750) * 40;
      this.companyExpanded = true; // 展开测量高度
      uni
        .createSelectorQuery()
        .in(this)
        .select("#companyValue")
        .boundingClientRect((rect) => {
          if (!rect || !lineHeightPx) return;
          const lines = Math.round(rect.height / lineHeightPx);
          this.needCompanyToggle = lines > 2;
          this.companyExpanded = !this.needCompanyToggle;
        })
        .exec();
    });
  },
  onReachBottom() {
    if (this.timelineView !== "text") return;
    this.loadMoreTimelineItems();
  },
  onPageScroll(e) {
    this.handlePageScroll(e);
  },
  // 页面卸载时销毁图表实例
  beforeUnmount() {
    if (this.backTopHideTimer) {
      clearTimeout(this.backTopHideTimer);
      this.backTopHideTimer = null;
    }
    if (this.$refs.chartRef) {
      this.$refs.chartRef.dispose();
    }
  },
  computed: {
    isShowGraph() {
      return this.data.billDetailList.length < 30;
    },
    timelineTotalCount() {
      if (!this.data || !Array.isArray(this.data.billDetailList)) return 0;
      return this.data.billDetailList.length;
    },
    visibleBillDetailList() {
      if (!this.data || !Array.isArray(this.data.billDetailList)) return [];
      const maxCount = Math.min(this.timelineVisibleCount, this.timelineTotalCount);
      return this.data.billDetailList.slice(0, maxCount);
    },
    hasMoreTimelineItems() {
      return this.visibleBillDetailList.length < this.timelineTotalCount;
    },
  },
  methods: {
    getTagColorStyle(tag) {
      let color = "#57bfe1";
      if (tag.includes("生产")) color = "#f5c269";
      if (tag.includes("零售")) color = "#91c76d";
      return {
        color,
        borderColor: color + "80",
      };
    },
    // 判断 imageData 中是否存在一条 source -> target 的有向连接线
    hasDirectedEdge(sourceName, targetName, list = this.imageData) {
      const sourceNode = (list || []).find((n) => n && n.name === sourceName);
      if (!sourceNode || !Array.isArray(sourceNode.to)) return false;
      return sourceNode.to.some((t) => t && t.name === targetName);
    },
    // 1) 当前节点与相邻节点是否被同一节点指向，返回数量与当前位置（1-based）
    getInboundAdjacentGroupInfo(currentIndex, list = this.imageData) {
      const nodes = Array.isArray(list) ? list : [];
      if (currentIndex < 0 || currentIndex >= nodes.length) return null;
      const currentName = nodes[currentIndex]?.name;
      if (!currentName) return null;
      const sourceNames = nodes
        .filter((node) => Array.isArray(node?.to) && node.to.some((toItem) => toItem && toItem.name === currentName))
        .map((node) => node.name);
      let best = null;
      sourceNames.forEach((sourceName) => {
        let start = currentIndex;
        let end = currentIndex;
        while (start - 1 >= 0 && this.hasDirectedEdge(sourceName, nodes[start - 1]?.name, nodes)) {
          start--;
        }
        while (end + 1 < nodes.length && this.hasDirectedEdge(sourceName, nodes[end + 1]?.name, nodes)) {
          end++;
        }
        const count = end - start + 1;
        const position = currentIndex - start + 1;
        if (!best || count > best.count) {
          best = { sourceName, count, position, start, end };
        }
      });
      return (
        best || {
          sourceName: null,
          count: 1,
          position: 1,
          start: currentIndex,
          end: currentIndex,
        }
      );
    },
    // 2) 当前节点与相邻节点是否共同指向同一节点，返回数量与当前位置（1-based）
    getOutboundAdjacentGroupInfo(currentIndex, list = this.imageData) {
      const nodes = Array.isArray(list) ? list : [];
      if (currentIndex < 0 || currentIndex >= nodes.length) return null;
      const currentNode = nodes[currentIndex] || {};
      const targetNames = Array.isArray(currentNode.to) ? currentNode.to.map((t) => t && t.name).filter(Boolean) : [];
      let best = null;
      targetNames.forEach((targetName) => {
        let start = currentIndex;
        let end = currentIndex;
        while (start - 1 >= 0 && this.hasDirectedEdge(nodes[start - 1]?.name, targetName, nodes)) {
          start--;
        }
        while (end + 1 < nodes.length && this.hasDirectedEdge(nodes[end + 1]?.name, targetName, nodes)) {
          end++;
        }
        const count = end - start + 1;
        const position = currentIndex - start + 1;
        if (!best || count > best.count) {
          best = { targetName, count, position, start, end };
        }
      });
      return (
        best || {
          targetName: null,
          count: 1,
          position: 1,
          start: currentIndex,
          end: currentIndex,
        }
      );
    },
    // 3) 当前节点与目标节点之间是否只有一条连接线（双向算多条）
    // 3) 判断当前节点与目标节点之间是否只有一条连接线
    // 说明：当前节点可能有多条连线指向目标节点（to 中出现多次），目标节点也可能反向指向当前节点，均计入总数
    getConnectionLineInfo(currentName, targetName, list = this.imageData) {
      const nodes = Array.isArray(list) ? list : [];
      const countEdges = (fromName, toName) => {
        const fromNode = nodes.find((n) => n && n.name === fromName);
        if (!fromNode || !Array.isArray(fromNode.to)) return 0;
        return fromNode.to.filter((t) => t && t.name === toName).length;
      };
      const forward = countEdges(currentName, targetName);
      const backward = countEdges(targetName, currentName);
      const edgeCount = forward + backward;
      return {
        edgeCount,
        isSingleLine: edgeCount === 1,
        forwardCount: forward,
        backwardCount: backward,
        hasForward: forward > 0,
        hasBackward: backward > 0,
      };
    },
    // 4) 当前节点与上一个/下一个节点之间是否存在连接线（任一方向都算）
    getAdjacentConnectionInfo(currentIndex, list = this.imageData) {
      const nodes = Array.isArray(list) ? list : [];
      if (currentIndex < 0 || currentIndex >= nodes.length) return null;
      const currentName = nodes[currentIndex]?.name;
      const prevName = nodes[currentIndex - 1]?.name || null;
      const nextName = nodes[currentIndex + 1]?.name || null;
      const hasPrevConnection = prevName ? this.getConnectionLineInfo(currentName, prevName, nodes).edgeCount > 0 : false;
      const hasNextConnection = nextName ? this.getConnectionLineInfo(currentName, nextName, nodes).edgeCount > 0 : false;
      return { hasPrevConnection, hasNextConnection, prevName, nextName };
    },
    buildGraphLayout(nodes = []) {
      const chartWidth = Number(this.graphViewWidth) || 320;
      const nodeSize = 86;
      const sidePadding = nodeSize / 2 + 10;
      const centerX = chartWidth / 2;
      const hGap = Math.max(140, Math.min(210, chartWidth - sidePadding * 2));
      const vGap = 150;
      const componentGap = 15;
      const edgeRecords = [];
      const nameIndexMap = {};
      nodes.forEach((node, index) => {
        if (node && node.name) nameIndexMap[node.name] = index;
      });

      nodes.forEach((node, sourceIndex) => {
        const tos = Array.isArray(node && node.to) ? node.to : [];
        tos.forEach((to, targetIndex) => {
          const targetNodeIndex = nameIndexMap[to && to.name];
          if (!to || !to.name || to.name === node.name || typeof targetNodeIndex !== "number") return;
          edgeRecords.push({
            sourceIndex,
            targetIndex,
            targetNodeIndex,
            sourceName: node.name,
            targetName: to.name,
            uniqId: `${sourceIndex}_${targetIndex}`,
          });
        });
      });

      const parent = nodes.map((_, index) => index);
      const find = (index) => {
        if (parent[index] !== index) parent[index] = find(parent[index]);
        return parent[index];
      };
      const union = (a, b) => {
        const rootA = find(a);
        const rootB = find(b);
        if (rootA !== rootB) parent[rootB] = rootA;
      };
      edgeRecords.forEach((edge) => union(edge.sourceIndex, edge.targetNodeIndex));

      const componentMap = {};
      nodes.forEach((_, index) => {
        const root = find(index);
        if (!componentMap[root]) componentMap[root] = [];
        componentMap[root].push(index);
      });
      const components = Object.values(componentMap).sort((a, b) => Math.min(...a) - Math.min(...b));

      const positions = new Array(nodes.length);
      let baseY = 100;
      components.forEach((component) => {
        const componentSet = new Set(component);
        const layers = {};
        component.forEach((index) => {
          layers[index] = 0;
        });

        for (let i = 0; i < component.length; i++) {
          let changed = false;
          edgeRecords.forEach((edge) => {
            if (!componentSet.has(edge.sourceIndex) || !componentSet.has(edge.targetNodeIndex)) return;
            const nextLayer = layers[edge.sourceIndex] + 1;
            if (nextLayer > layers[edge.targetNodeIndex]) {
              layers[edge.targetNodeIndex] = nextLayer;
              changed = true;
            }
          });
          if (!changed) break;
        }

        const grouped = {};
        component.forEach((index) => {
          const layer = layers[index] || 0;
          if (!grouped[layer]) grouped[layer] = [];
          grouped[layer].push(index);
        });
        const layerKeys = Object.keys(grouped)
          .map(Number)
          .sort((a, b) => a - b);
        layerKeys.forEach((layer) => {
          const group = grouped[layer].sort((a, b) => a - b);
          group.forEach((nodeIndex, positionIndex) => {
            positions[nodeIndex] = {
              x: centerX + (positionIndex - (group.length - 1) / 2) * hGap,
              y: baseY + layer * vGap,
            };
          });
        });
        const maxLayer = layerKeys.length ? Math.max(...layerKeys) : 0;
        baseY += (maxLayer + 1) * vGap + componentGap;
      });

      const pairMembers = {};
      edgeRecords.forEach((edge) => {
        const pairKey = [edge.sourceName, edge.targetName].sort().join("__");
        if (!pairMembers[pairKey]) pairMembers[pairKey] = [];
        pairMembers[pairKey].push(edge);
      });
      const curvenessMap = {};
      const edgeMetaMap = {};
      Object.keys(pairMembers).forEach((pairKey) => {
        const list = pairMembers[pairKey];
        const step = this.ownershipFlowMode ? (list.length === 2 ? 0.86 : 0.68) : list.length === 2 ? 0.72 : 0.56;
        list.forEach((edge, index) => {
          curvenessMap[edge.uniqId] = list.length > 1 ? (index - (list.length - 1) / 2) * step : 0;
          edgeMetaMap[edge.uniqId] = {
            count: list.length,
            index,
            offset: index - (list.length - 1) / 2,
          };
        });
      });

      return { positions, curvenessMap, edgeMetaMap, height: Math.max(520, baseY - componentGap + 70) };
    },
    // 初始化图表
    async initChart() {
      if (!this.$refs.chartRef || !this.isShowGraph || this.timelineView !== "graph") return;
      if (this._initChartLock) return;
      this._initChartLock = true;
      try {
        await this._doInitChart();
      } finally {
        this._initChartLock = false;
      }
    },
    async _doInitChart() {
      let option = {};
      const data = [];
      const links = [];
      const lineWidth = 2;
      const _nodes = this.imageData || [];
      const { positions: _positions, curvenessMap: _curvenessMap, edgeMetaMap: _edgeMetaMap } = this.buildGraphLayout(_nodes);
      const nameIndexMap = {};
      _nodes.forEach((node, nodeIndex) => {
        if (node && node.name) nameIndexMap[node.name] = nodeIndex;
      });

      this.imageData.forEach((item, index) => {
        const { name, to } = item;
        const toLen = to.length;
        const { x, y } = _positions[index] || { x: 350, y: index * 250 + 100 };
        const color = name.includes("生产") ? "#fde58f" : name.includes("零售") ? "#b5ea8e" : name.includes("消费者") ? "#e0d3fe" : "#a3e6ff";
        data.push({
          name,
          x,
          y,
          itemStyle: {
            color,
          },
        });
        to.forEach((toItem, toIndex) => {
          const { name: toName, text: toText } = toItem;
          if (name === toName) {
            const loopLabelName = `${name}__self_loop_${toIndex}`;
            data.push({
              name: loopLabelName,
              x: x + 88,
              y,
              itemStyle: {
                color: "rgba(0,0,0,0)",
              },
              label: {
                show: true,
                color: "#c6c6c6",
                formatter: toText,
              },
              tooltip: {
                show: false,
              },
            });
            links.push(
              {
                source: name,
                target: loopLabelName,
                label: {
                  show: false,
                },
                lineStyle: {
                  color: "#c6c6c6",
                  width: lineWidth,
                  curveness: 0.34,
                },
              },
              {
                source: loopLabelName,
                target: name,
                label: {
                  show: false,
                },
                lineStyle: {
                  color: "#c6c6c6",
                  width: lineWidth,
                  curveness: 0.34,
                },
              },
            );
          } else {
            const isOut = toText.includes("出库");
            const isReturn = toText.includes("退货");
            // 文案包含"退货"时整条线统一橙色；其余按出/入库区分蓝/灰
            const linkColor = isReturn ? "#ff9500" : isOut ? "#80a6fd" : "#aaaaaa";
            // 从预计算的曲率表中取值：同一对节点间多条线已均匀分配，单条线为 0（直线）
            const assignedCurveness = _curvenessMap[`${index}_${toIndex}`] || 0;
            const edgeMeta = _edgeMetaMap[`${index}_${toIndex}`] || { count: 1, offset: 0 };
            const targetPosition = _positions[nameIndexMap[toName]] || { x, y: y + 120 };
            const midX = (x + targetPosition.x) / 2;
            const midY = (y + targetPosition.y) / 2;
            const dx = targetPosition.x - x;
            const dy = targetPosition.y - y;
            const length = Math.sqrt(dx * dx + dy * dy) || 1;
            const perpX = -dy / length;
            const perpY = dx / length;
            const labelName = `${name}__edge_label_${toIndex}`;
            const labelText = toText.split(/\s+/).filter(Boolean).join("\n");
            const stagger = edgeMeta.count > 1 ? edgeMeta.offset : 0;
            const labelGap = this.ownershipFlowMode ? 68 : 54;
            const labelPerpGap = this.ownershipFlowMode ? 32 : 24;
            data.push({
              name: labelName,
              x: midX + perpX * stagger * labelGap,
              y: midY + stagger * labelGap + perpY * stagger * labelPerpGap,
              symbolSize: 1,
              itemStyle: {
                color: "rgba(0,0,0,0)",
              },
              label: {
                show: true,
                formatter: () => labelText,
                color: linkColor,
                fontSize: 12,
                lineHeight: 14,
                width: 120,
                overflow: "breakAll",
              },
              tooltip: {
                show: false,
              },
            });
            links.push({
              source: name,
              target: toName,
              label: {
                show: false,
              },
              lineStyle: {
                color: linkColor,
                width: lineWidth,
                curveness: assignedCurveness,
              },
            });
          }
        });
      });
      // console.log("this.imageData", this.imageData);
      // console.log("data", data);
      // console.log("links", links);
      option = {
        tooltip: {},
        animationDurationUpdate: 1500,
        animationEasingUpdate: "quinticInOut",
        series: [
          {
            type: "graph",
            layout: "none",
            symbolSize: 86,
            roam: true,
            label: {
              show: true,
              fontSize: 8,
              lineHeight: 10,
              formatter: (params) => {
                const text = String((params && params.name) || "");
                if (!text) return "";
                return text.split(/\s+/).filter(Boolean).join("\n");
              },
            },
            edgeSymbol: ["circle", "arrow"],
            edgeSymbolSize: [4, 10],
            data,
            links,
            lineStyle: {
              opacity: 0.9,
              width: 2,
              curveness: 0,
            },
          },
        ],
      };

      try {
        await this.$nextTick();
        // if (this.chartInstance) {
        //   this.chartInstance.setOption(option, true);
        //   return;
        // }
        this.chartInstance = markRaw(await this.$refs.chartRef.init(echarts));
        this.chartInstance.setOption(option, true);
      } catch (error) {
        console.error("图表初始化失败:", error);
      }
    },
    // 更新图表数据
    updateChart(newOption) {
      if (this.chartInstance) {
        this.chartInstance.setOption(newOption);
      } else if (this.$refs.chartRef) {
        this.$refs.chartRef.setOption(newOption);
      }
    },
    // 调整图表大小
    resizeChart() {
      if (this.$refs.chartRef) {
        this.$refs.chartRef.resize();
      }
    },
    formatDate(date) {
      return formatDate({ date }, "YYYY-MM-DD");
    },
    getRegion(value = {}) {
      const { provinceDesc, cityDesc, areaDesc } = value;
      return [provinceDesc, cityDesc, areaDesc].filter(Boolean).join("，") || "";
    },
    getRegionByPrefix(value = {}, prefix = "from") {
      const province = value[`${prefix}ProvinceDesc`];
      const city = value[`${prefix}CityDesc`];
      const area = value[`${prefix}AreaDesc`];
      return [province, city, area].filter(Boolean).join("-") || "";
    },
    initTimelineVisibleCount() {
      this.timelineVisibleCount = this.timelinePageSize;
    },
    handlePageScroll(e) {
      const top = Number((e && (e.scrollTop || (e.detail && e.detail.scrollTop))) || 0);
      if (top < this.backTopThreshold) {
        this.showBackTopButton = false;
        if (this.backTopHideTimer) {
          clearTimeout(this.backTopHideTimer);
          this.backTopHideTimer = null;
        }
        return;
      }
      this.showBackTopButton = true;
      if (this.backTopHideTimer) {
        clearTimeout(this.backTopHideTimer);
      }
      this.backTopHideTimer = setTimeout(() => {
        this.showBackTopButton = false;
        this.backTopHideTimer = null;
      }, 3000);
    },
    backToTop() {
      this.scrollTopValue = 0;
      this.scrollIntoViewId = "detailPageTopAnchor";
      this.showBackTopButton = false;
      if (this.backTopHideTimer) {
        clearTimeout(this.backTopHideTimer);
        this.backTopHideTimer = null;
      }
      this.$nextTick(() => {
        this.scrollIntoViewId = "";
      });
      uni.pageScrollTo({
        scrollTop: 0,
        duration: 300,
      });
    },
    onTimelineScrollToLower() {
      if (this.timelineView !== "text") return;
      this.loadMoreTimelineItems();
    },
    loadMoreTimelineItems() {
      if (!this.hasMoreTimelineItems) return;
      this.timelineVisibleCount = Math.min(this.timelineVisibleCount + this.timelinePageSize, this.timelineTotalCount);
    },
    toggleTimelineItem(index) {
      if (!this.data || !Array.isArray(this.data.billDetailList)) return;
      const item = this.data.billDetailList[index];
      if (!item) return;
      this.$set(item, "_showDetail", !item._showDetail);
    },
    setAllTimelineDetail(showDetail) {
      this.allTimelineDetail = !!showDetail;
      if (!this.data || !Array.isArray(this.data.billDetailList)) return;
      this.data.billDetailList.forEach((item) => {
        if (item) this.$set(item, "_showDetail", !!showDetail);
      });
    },
    toggleTimelineView() {
      this.timelineView = this.timelineView === "text" ? "graph" : "text";
      if (this.timelineView === "text") {
        this.initTimelineVisibleCount();
      }
      if (this.timelineView === "graph" && this.data && Array.isArray(this.data.billDetailList)) {
        this.imageData = this.ownershipFlowMode ? this.buildImageData2(this.data.billDetailList) : this.buildImageData(this.data.billDetailList);
        this.updateGraphCanvasSize();
        this.$nextTick(() => this.initChart());
      }
    },
    onOwnershipModeChange(e) {
      this.ownershipFlowMode = !!(e && e.detail && e.detail.value);
      if (!this.data || !Array.isArray(this.data.billDetailList)) return;
      this.imageData = this.ownershipFlowMode ? this.buildImageData2(this.data.billDetailList) : this.buildImageData(this.data.billDetailList);
      this.updateGraphCanvasSize();
      if (this.timelineView === "graph") {
        this.$nextTick(() => this.initChart());
      }
    },
    onSetDefaultView() {
      uni.setStorageSync(this.defaultTimelineViewKey, this.timelineView);
      uni.showToast({
        title: "已设为默认",
        icon: "none",
      });
    },
    buildImageData2(data) {
      if (!data || !Array.isArray(data)) return [];
      let list = [];
      const buildTime = (time) => {
        return time.split(" ")[0];
      };
      const buildTo = (to, item) => {
        const hasAss = !!item.assEntName;
        const isWarehousing = item.billTypeName.includes("入库");
        const hasType = item.billTypeName && item.billTypeName !== "****";
        if (item.billTypeName === "使用出库") {
          to.push({
            name: "消费者",
            text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
          });
        } else if (item.toEntName) {
          const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join("，");
          const assAddress = [item.assProvinceDesc, item.assCityDesc, item.assAreaDesc].filter(Boolean).join("，");
          const name = item.toEntTypeName + " " + item.toEntName + " " + address;
          const assName = item.assEntTypeName + " " + item.assEntName + " " + assAddress;
          to.push({
            name: hasAss && isWarehousing ? assName : name,
            text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
          });
          if (hasAss && hasType) {
            to[to.length - 1].text += " 被委托:" + (isWarehousing ? item.toEntName : item.fromEntName);
          }
        }
        return to;
      };
      data.forEach((item, i) => {
        const hasAss = !!item.assEntName;
        const isWarehousing = item.billTypeName.includes("入库");
        const index = list.findIndex((i) => i.name.includes(hasAss && !isWarehousing ? item.assEntName : item.fromEntName));
        let to = [];
        if (index !== -1) {
          to = list[index].to;
        }
        to = buildTo(to, item);
        const address = [item.fromProvinceDesc, item.fromCityDesc, item.fromAreaDesc].filter(Boolean).join("，");
        const assAddress = [item.assProvinceDesc, item.assCityDesc, item.assAreaDesc].filter(Boolean).join("，");
        const name = item.fromEntTypeName + " " + item.fromEntName + " " + address;
        const assName = item.assEntTypeName + " " + item.assEntName + " " + assAddress;
        if (index === -1) {
          list.push({
            num: i,
            name: hasAss && !isWarehousing ? assName : name,
            to,
          });
        } else {
          list[index].to = to;
        }

        if (item.toEntName && list.findIndex((i) => i.name.includes(item.toEntName)) === -1) {
          const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join("，");
          list.push({
            num: i + 9999,
            name: item.toEntTypeName + " " + item.toEntName + " " + address,
            to: [],
          });
        }
      });
      list = list.filter((i) => Array.isArray(i.to) && i.to.length !== 0);
      const lastItem = list[list.length - 1];
      if (lastItem?.to?.[0]?.name === "消费者") {
        list.push({
          num: 99999,
          name: "消费者",
          to: [],
        });
      }
      list.sort((a, b) => a.num - b.num);
      return list;
    },
    buildImageData(data) {
      if (!data || !Array.isArray(data)) return [];
      const list = [];
      const buildTime = (time) => {
        return time.split(" ")[0];
      };
      const buildTo = (to, item) => {
        const hasType = item.billTypeName && item.billTypeName !== "****";
        if (item.billTypeName === "使用出库") {
          to.push({
            name: "消费者",
            text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
          });
        } else if (item.toEntName) {
          const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join("，");
          to.push({
            name: item.toEntTypeName + " " + item.toEntName + " " + address,
            text: !hasType ? "****" : buildTime(item.billTime) + " " + item.billTypeName,
          });
          if (item.assEntName && hasType) {
            to[to.length - 1].text += " 委托:" + item.assEntName;
          }
        }
        return to;
      };
      data.forEach((item, i) => {
        const index = list.findIndex((i) => i.name.includes(item.fromEntName));
        let to = [];
        if (index !== -1) {
          to = list[index].to;
        }
        to = buildTo(to, item);
        if (index === -1) {
          const address = [item.fromProvinceDesc, item.fromCityDesc, item.fromAreaDesc].filter(Boolean).join("，");
          list.push({
            num: i,
            name: item.fromEntTypeName + " " + item.fromEntName + " " + address,
            to,
          });
        } else {
          list[index].to = to;
        }

        if (item.toEntName && list.findIndex((i) => i.name.includes(item.toEntName)) === -1) {
          const address = [item.toProvinceDesc, item.toCityDesc, item.toAreaDesc].filter(Boolean).join("，");
          list.push({
            num: i + 9999,
            name: item.toEntTypeName + " " + item.toEntName + " " + address,
            to: [],
          });
        }
      });
      const lastItem = list[list.length - 1];
      if (lastItem?.to?.[0]?.name === "消费者") {
        list.push({
          num: 99999,
          name: "消费者",
          to: [],
        });
      }
      list.sort((a, b) => a.num - b.num);
      return list;
    },
    updateGraphCanvasSize() {
      if (!this.isShowGraph) {
        this.graphViewHeight = 50;
        return;
      }
      const list = this.imageData || [];
      this.graphViewHeight = this.buildGraphLayout(list).height;
    },
    getData2(traceCode) {
      request("/bills/scanCode2", {
        tracCode: traceCode,
        path: "/traceabilityCodeQuery/pages/detail/index.vue",
      }).then((res) => {
        if (res.code == 200) {
          const _data = res.data;
          this.data = _data || null;
          if (this.data && Array.isArray(this.data.billDetailList)) {
            this.data.billDetailList = this.data.billDetailList.map((item) => ({
              ...item,
              _showDetail: !!this.allTimelineDetail,
            }));
            this.initTimelineVisibleCount();
            if (this.isShowGraph) {
              this.imageData = this.ownershipFlowMode ? this.buildImageData2(this.data.billDetailList) : this.buildImageData(this.data.billDetailList);
            }
            this.updateGraphCanvasSize();
            this.initChart();
          }
          if (typeof _data === "object" && Object.values(_data).filter((i) => !!i).length == 0) {
            this.data = null;
          }
          this.loading = false;
        } else {
          uni.showToast({ title: "查询失败", icon: "none" });
        }
      });
    },
    getData(traceCode) {
      request("/bills/scanCode", {
        tracCode: traceCode,
        path: "/traceabilityCodeQuery/pages/detail/index.vue",
      }).then((res) => {
        if (res.code == 200) {
          // const _data = res.data;
          // this.data = _data || null;
          // const defaultData = [
          //   {
          //     billTypeName: "生产入库",
          //     billCode: "SYS_IN_2025041410194699357",
          //     fromEntId: "00000000000000959808",
          //     fromEntName: "惠州市九惠制药股份有限公司",
          //     fromEntType: "1",
          //     fromEntTypeName: "生产企业",
          //     fromProvinceDesc: "广东省",
          //     fromCityDesc: "惠州市",
          //     fromAreaDesc: "惠城区",
          //     toEntId: "00000000000000959808",
          //     toEntName: "惠州市九惠制药股份有限公司",
          //     toEntType: "1",
          //     toEntTypeName: "生产企业",
          //     toProvinceDesc: "广东省",
          //     toCityDesc: "惠州市",
          //     toAreaDesc: "惠城区",
          //     billTime: "2025-04-12 00:00:00",
          //     uploadEntId: "00000000000000959808",
          //     uploadEntName: "惠州市九惠制药股份有限公司",
          //     uploadEntType: "1",
          //     uploadEntTypeName: "生产企业",
          //     uploadProvinceDesc: "广东省",
          //     uploadCityDesc: "惠州市",
          //     uploadAreaDesc: "惠城区",
          //     uploadTime: "2025-04-14 10:19:47",
          //   },
          //   {
          //     billTypeName: "销售出库",
          //     billCode: "20250414093811017995",
          //     fromEntId: "00000000000000959808",
          //     fromEntName: "惠州市九惠制药股份有限公司",
          //     fromEntType: "1",
          //     fromEntTypeName: "生产企业",
          //     fromProvinceDesc: "广东省",
          //     fromCityDesc: "惠州市",
          //     fromAreaDesc: "惠城区",
          //     toEntId: "00000000000012618117",
          //     toEntName: "深圳华润三九医药贸易有限公司",
          //     toEntType: "2",
          //     toEntTypeName: "批发企业",
          //     toProvinceDesc: "广东省",
          //     toCityDesc: "深圳市",
          //     toAreaDesc: "龙华区",
          //     billTime: "2025-04-14 09:38:11",
          //     uploadEntId: "00000000000000592642",
          //     uploadEntName: "华润三九医药股份有限公司",
          //     uploadEntType: "1",
          //     uploadEntTypeName: "生产企业",
          //     uploadProvinceDesc: "广东省",
          //     uploadCityDesc: "深圳市",
          //     uploadAreaDesc: "龙华区",
          //     uploadTime: "2025-10-31 17:29:36",
          //   },
          //   {
          //     billTypeName: "销售出库",
          //     billCode: "2504032G-2",
          //     fromEntId: "00000000000012618117",
          //     fromEntName: "深圳华润三九医药贸易有限公司",
          //     fromEntType: "2",
          //     fromEntTypeName: "批发企业",
          //     fromProvinceDesc: "广东省",
          //     fromCityDesc: "深圳市",
          //     fromAreaDesc: "龙华区",
          //     toEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
          //     toEntName: "国药控股福建有限公司",
          //     toEntType: "5",
          //     toEntTypeName: "物流企业",
          //     toProvinceDesc: "福建省",
          //     toCityDesc: "厦门市",
          //     toAreaDesc: "思明区",
          //     billTime: "2025-09-27 00:00:00",
          //     uploadEntId: "00000000000012618117",
          //     uploadEntName: "深圳华润三九医药贸易有限公司",
          //     uploadEntType: "2",
          //     uploadEntTypeName: "批发企业",
          //     uploadProvinceDesc: "广东省",
          //     uploadCityDesc: "深圳市",
          //     uploadAreaDesc: "龙华区",
          //     uploadTime: "2025-09-28 10:04:12",
          //   },
          //   {
          //     billTypeName: "采购入库",
          //     billCode: "2504032G-1",
          //     fromEntId: "00000000000000592642",
          //     fromEntName: "华润三九医药股份有限公司",
          //     fromEntType: "1",
          //     fromEntTypeName: "生产企业",
          //     fromProvinceDesc: "广东省",
          //     fromCityDesc: "深圳市",
          //     fromAreaDesc: "龙华区",
          //     toEntId: "00000000000012618117",
          //     toEntName: "深圳华润三九医药贸易有限公司",
          //     toEntType: "2",
          //     toEntTypeName: "批发企业",
          //     toProvinceDesc: "广东省",
          //     toCityDesc: "深圳市",
          //     toAreaDesc: "龙华区",
          //     billTime: "2025-09-27 00:00:00",
          //     uploadEntId: "00000000000000592642",
          //     uploadEntName: "华润三九医药股份有限公司",
          //     uploadEntType: "1",
          //     uploadEntTypeName: "生产企业",
          //     uploadProvinceDesc: "广东省",
          //     uploadCityDesc: "深圳市",
          //     uploadAreaDesc: "龙华区",
          //     uploadTime: "2025-09-28 10:04:06",
          //   },
          //   {
          //     billTypeName: "采购入库",
          //     billCode: "FJASNP2509280580393",
          //     fromEntId: "00000000000012618117",
          //     fromEntName: "深圳华润三九医药贸易有限公司",
          //     fromEntType: "2",
          //     fromEntTypeName: "批发企业",
          //     fromProvinceDesc: "广东省",
          //     fromCityDesc: "深圳市",
          //     fromAreaDesc: "龙华区",
          //     toEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
          //     toEntName: "国药控股福建有限公司",
          //     toEntType: "5",
          //     toEntTypeName: "物流企业",
          //     toProvinceDesc: "福建省",
          //     toCityDesc: "厦门市",
          //     toAreaDesc: "思明区",
          //     billTime: "2025-10-30 00:00:00",
          //     uploadEntId: "00000000000012618117",
          //     uploadEntName: "深圳华润三九医药贸易有限公司",
          //     uploadEntType: "2",
          //     uploadEntTypeName: "批发企业",
          //     uploadProvinceDesc: "广东省",
          //     uploadCityDesc: "深圳市",
          //     uploadAreaDesc: "龙华区",
          //     uploadTime: "2025-10-30 17:05:35",
          //   },
          //   {
          //     billTypeName: "销售出库",
          //     billCode: "FJS0CFFPHL",
          //     fromEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
          //     fromEntName: "国药控股福建有限公司",
          //     fromEntType: "5",
          //     fromEntTypeName: "物流企业",
          //     fromProvinceDesc: "福建省",
          //     fromCityDesc: "厦门市",
          //     fromAreaDesc: "思明区",
          //     toEntId: "00000000000001312166",
          //     toEntName: "国药控股厦门有限公司",
          //     toEntType: "2",
          //     toEntTypeName: "批发企业",
          //     toProvinceDesc: "福建省",
          //     toCityDesc: "厦门市",
          //     toAreaDesc: "同安区",
          //     billTime: "2025-10-31 00:00:00",
          //     uploadEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
          //     uploadEntName: "国药控股福建有限公司",
          //     uploadEntType: "5",
          //     uploadEntTypeName: "物流企业",
          //     uploadProvinceDesc: "福建省",
          //     uploadCityDesc: "厦门市",
          //     uploadAreaDesc: "思明区",
          //     uploadTime: "2025-11-01 00:03:29",
          //   },
          //   {
          //     billTypeName: "采购入库",
          //     billCode: "XMASNP2510310469890",
          //     fromEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
          //     fromEntName: "国药控股福建有限公司",
          //     fromEntType: "5",
          //     fromEntTypeName: "物流企业",
          //     fromProvinceDesc: "福建省",
          //     fromCityDesc: "厦门市",
          //     fromAreaDesc: "思明区",
          //     toEntId: "00000000000001312166",
          //     toEntName: "国药控股厦门有限公司",
          //     toEntType: "2",
          //     toEntTypeName: "批发企业",
          //     toProvinceDesc: "福建省",
          //     toCityDesc: "厦门市",
          //     toAreaDesc: "同安区",
          //     billTime: "2025-11-03 00:00:00",
          //     uploadEntId: "8b63aa19a9a94cb6aa472ee69d309bb5",
          //     uploadEntName: "国药控股福建有限公司",
          //     uploadEntType: "5",
          //     uploadEntTypeName: "物流企业",
          //     uploadProvinceDesc: "福建省",
          //     uploadCityDesc: "厦门市",
          //     uploadAreaDesc: "思明区",
          //     uploadTime: "2025-11-03 17:14:48",
          //   },
          // ];
          // this.imageData = this.buildImageData(defaultData);
          // this.updateGraphCanvasSize();
          // this.initChart();
          //   if (
          //     typeof _data === "object" &&
          //     Object.values(_data).filter((i) => !!i).length == 0
          //   ) {
          //     this.data = null;
          //   }
          //   this.loading = false;
          // } else {
          //   uni.showToast({ title: "查询失败", icon: "none" });
          // }
        }
      });
    },
    copy(text) {
      const data = String(text || this.traceCode);
      uni.setClipboardData({
        data,
        success: () => {
          console.log("packages/traceabilityCodeQuery/pages/detail/index.vue" + "storage" + "clipboardData");
          uni.showToast({
            title: "已复制追溯码",
            icon: "none",
            duration: 2000,
          });
        },
      });
    },
    toggleCompanyExpand() {
      if (!this.needCompanyToggle) return;
      this.companyExpanded = !this.companyExpanded;
    },
    toggleCardExpand() {
      this.cardExpanded = !this.cardExpanded;
    },
    formatTraceCode(traceCode) {
      return String(traceCode || "")
        .replace(/(.{5})/g, "$1 ")
        .trim();
    },
  },
};
</script>

<style>
.detail-bg {
  background-position: center top;
  background-size: 100% auto;
}

.detail-page {
  box-sizing: border-box;
  background: rgb(226 226 226);
  padding-bottom: 50rpx;
}

.detail-page-empty {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 136rpx 280rpx;
}

.detail-back-top-btn {
  position: fixed;
  right: 28rpx;
  bottom: 110rpx;
  width: 84rpx;
  height: 84rpx;
  border-radius: 50%;
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.18);
  z-index: 999;
}

.back-top-fade-enter-active,
.back-top-fade-leave-active {
  transition:
    opacity 0.28s ease,
    transform 0.28s ease;
}

.back-top-fade-enter-from,
.back-top-fade-leave-to {
  opacity: 0;
  transform: translateY(16rpx) scale(0.92);
}

.detail-back-top-arrow {
  width: 20rpx;
  height: 20rpx;
  border-top: 4rpx solid #5f6773;
  border-left: 4rpx solid #5f6773;
  transform: rotate(45deg);
  margin-top: 8rpx;
}

.detail-card {
  padding: 40rpx;
}

.detail-drug-title {
  font-size: 45rpx;
  font-weight: 800;
  color: #000;
  margin-bottom: 16rpx;
}

.detail-row {
  position: relative;
  display: flex;
  margin-top: 12rpx;
  align-items: baseline;
}

.detail-label {
  flex-shrink: 0;
  display: inline-block;
  vertical-align: baseline;
  width: 150rpx;
  font-size: 28rpx;
}

.detail-value {
  display: inline-block;
  vertical-align: baseline;
  font-size: 28rpx;
}

.detail-company-value {
  line-height: 40rpx;
}

.detail-company-value.detail-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.detail-expand-tag {
  position: absolute;
  left: 150rpx;
  bottom: 0;
  color: #2c69ff;
  font-size: 28rpx;
}

.detail-value.detail-wrap {
  word-break: break-all;
  white-space: normal;
}

.detail-copy-btn {
  margin-left: 16rpx;
  display: flex;
  align-items: center;
  color: #2c69ff;
}

.detail-copy-text {
  margin-left: 6rpx;
  font-size: 26rpx;
}

.detail-traceability {
  font-size: 24rpx;
  margin: -10rpx 0 45rpx;
}

.detail-card-toggle {
  display: flex;
  justify-content: center;
  align-items: center;
  padding-top: 18rpx;
}

.detail-traceability .detail-label {
  width: auto;
}

.detail-section-header {
  padding: 30rpx 24rpx 20rpx 46rpx;
  border-bottom: 1rpx solid #55555526;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.detail-section-title {
  font-size: 35rpx;
  font-weight: 900;
}

.detail-section-actions {
  display: flex;
  align-items: center;
  gap: 12rpx;
}

.detail-section-action {
  font-size: 26rpx;
  color: #59a8f2;
  padding: 4rpx 6rpx;
  display: flex;
  align-items: center;
  gap: 6rpx;
}

.detail-section-action.active {
  color: #2c69ff;
  font-weight: 600;
}

.detail-timeline {
  margin-top: 20rpx;
  padding-bottom: 50rpx;
}

.detail-graph-placeholder {
  margin: 16rpx 20rpx 0;
  background: #fff;
  border-radius: 14rpx;
  font-size: 24rpx;
  text-align: center;
  color: #97a0ae;
  padding: 120rpx 20rpx;
}

.detail-graph-panel {
  margin: 16rpx 20rpx 0;
  background: #fff;
  border-radius: 14rpx;
  padding: 16rpx;
}

.graph-mode-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18rpx;
  margin-bottom: 18rpx;
  padding: 4rpx 6rpx 0;
}

.graph-mode-left {
  flex: 1;
}

.graph-mode-title {
  font-size: 30rpx;
  color: #333;
  font-weight: 600;
}

.graph-mode-desc {
  margin-top: 8rpx;
  font-size: 24rpx;
  color: #8d96a0;
  line-height: 1.45;
}

.detail-graph-canvas {
  width: 100%;
  height: 920rpx;
  background: #f8fafc;
  border-radius: 10rpx;
}

.detail-timeline-toolbar {
  display: flex;
  gap: 16rpx;
  padding: 20rpx 32rpx 10rpx;
}

.detail-toolbar-btn {
  padding: 8rpx 18rpx;
  border: 1rpx solid #d8dbe0;
  border-radius: 999rpx;
  color: #666;
  font-size: 24rpx;
  background: #fff;
}

.detail-timeline-item {
  position: relative;
  display: flex;
  margin-left: 36rpx;
}

.detail-timeline-item-first {
  margin-top: 20rpx;
}

.detail-timeline-loadmore {
  text-align: center;
  color: #8f98a3;
  font-size: 24rpx;
  padding: 18rpx 0 8rpx;
}

.detail-left {
  position: absolute;
  top: 34rpx;
  left: 0;
  height: 100%;
  display: flex;
  align-items: center;
  flex-direction: column;
}

.detail-dot {
  width: 16rpx;
  height: 16rpx;
  border-radius: 50%;
  border: 2rpx solid #2c69ff;
}

.detail-dot.detail-fill {
  background: #2c69ff;
  border-color: #2c69ff;
}

.detail-line {
  width: 2rpx;
  flex: 1;
  background: #8face68f;
  margin-right: 2rpx;
}

.detail-right {
  flex: 1;
  padding: 20rpx 0;
  margin-left: 45rpx;
  border-bottom: 1rpx solid #55555526;
}

.detail-title-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16rpx;
}

.detail-title {
  font-size: 32rpx;
  color: #333;
}

.detail-title.detail-bold {
  font-weight: 800;
}

.detail-item-toggle {
  flex-shrink: 0;
  padding: 6rpx 12rpx;
  border: 1rpx solid #d8dbe0;
  border-radius: 999rpx;
  font-size: 22rpx;
  color: #666;
  display: flex;
  align-items: center;
  gap: 6rpx;
  margin-right: 16rpx;
}

.detail-info-row {
  display: flex;
  margin-top: 10rpx;
  font-size: 28rpx;
  color: #444;
}

.detail-info-label {
  color: #666;
  flex-shrink: 0;
}

.detail-info-value {
  color: #333;
  word-break: break-all;
}

.detail-tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10rpx;
  margin-top: 8rpx;
  padding-left: 130rpx;
}

.detail-tag {
  color: #57bfe1;
  border-radius: 8rpx;
  padding: 5rpx 14rpx;
  font-size: 22rpx;
  border: 1rpx solid #57bfe180;
}

.detail-tag-region {
  color: #999;
  border: 1rpx solid #d8dbe0;
}

.detail-text-red {
  color: red;
}

.detail-loading {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}
</style>
