InviteTreeChildRows.vue 11.1 KB
<!--
 * @Author: 赵丽婷
 * @Date: 2026-01-06
 * @LastEditors: 赵丽婷
 * @LastEditTime: 2026-01-13 20:29:25
 * @FilePath: \LinkMed\linkmed-vue3\src\components\common\InviteTreeChildRows.vue
 * @Description: 邀请树行渲染组件(支持父行和子行)
 * Copyright (c) 2025 by 北京连心医疗科技有限公司, All Rights Reserved.
-->
<template>
  <!-- 渲染单个节点(用于父行) -->
  <template v-if="renderSingle">
    <tr
      :class="{
        'child-row': level > 0,
        'target-node-row':
          item.startUserId !== undefined && item.startUserId === item.userId,
      }"
      :data-level="level"
    >
      <td>
        <button
          v-if="
            !targetNodeChildrenUserIds.has(item.userId) &&
            item.descendantCount !== undefined &&
            item.descendantCount !== null &&
            item.descendantCount > 0
          "
          @click="handleToggleExpand(item.userId)"
          class="expand-btn"
          :class="{ expanded: expandedRows.has(item.userId) }"
        >
          <i
            class="expand-icon fas"
            :class="
              expandedRows.has(item.userId)
                ? 'fa-chevron-down'
                : 'fa-chevron-right'
            "
          ></i>
        </button>
      </td>
      <td>
        {{ formatDateTime(item.registeredAt || "") }}
      </td>
      <td>
        <span>{{ formatSourceUserName(item.username) || "-" }}</span>
        <span
          v-if="
            showDescendantCount &&
            item.descendantCount !== undefined &&
            item.descendantCount !== null &&
            item.descendantCount > 0
          "
          class="descendant-count"
        >
          {{ item.descendantCount }}
        </span>
      </td>
      <td>
        {{
          (() => {
            const parts = [];
            if (item.salesLevel !== undefined && item.salesLevel !== null) {
              parts.push(`S${item.salesLevel}`);
            }
            if (item.inviteLevel !== null && item.inviteLevel !== undefined) {
              parts.push(`L${item.inviteLevel}`);
            }
            return parts.length > 0 ? parts.join(", ") : "-";
          })()
        }}
      </td>
      <td>
        {{ item.maskedPhone || "-" }}
      </td>
      <td>
        {{ item.maskedEmail || "-" }}
      </td>
      <td
        v-if="showTotalEarnings"
        class="earnings"
        :class="{
          'earnings-positive':
            item.totalEarnings !== undefined &&
            item.totalEarnings !== null &&
            item.totalEarnings > 0,
        }"
      >
        {{
          item.totalEarnings !== undefined &&
          item.totalEarnings !== null &&
          item.totalEarnings > 0
            ? "+"
            : ""
        }}{{
          item.totalEarnings !== undefined &&
          item.totalEarnings !== null &&
          item.totalEarnings > 0
            ? `${item.totalEarnings.toFixed(2)}${$t("settingsSidebar.yuan")}`
            : "-"
        }}
      </td>
    </tr>
    <!-- 递归渲染子行 -->
    <template
      v-if="
        item.children &&
        item.children.length > 0 &&
        expandedRows.has(item.userId)
      "
    >
      <InviteTreeChildRows
        :item="item"
        :level="level + 1"
        :expanded-rows="expandedRows"
        :loaded-children-nodes="loadedChildrenNodes"
        :target-node-children-user-ids="targetNodeChildrenUserIds"
        :render-single="false"
        :show-descendant-count="showDescendantCount"
        :show-total-earnings="showTotalEarnings"
        @toggle-expand="handleToggleExpand"
      />
    </template>
  </template>
  <!-- 渲染子节点列表(用于子行) -->
  <template v-else>
    <template
      v-for="(child, childIndex) in item.children"
      :key="child.userId || childIndex"
    >
      <tr
        :class="{
          'child-row': true,
          'target-node-row':
            child.startUserId !== undefined &&
            child.startUserId === child.userId,
        }"
        :data-level="level"
      >
        <td :style="{ paddingLeft: `${level * 10}px` }">
          <button
            v-if="
              !targetNodeChildrenUserIds.has(child.userId) &&
              child.descendantCount !== undefined &&
              child.descendantCount !== null &&
              child.descendantCount > 0
            "
            @click="handleToggleExpand(child.userId)"
            class="expand-btn"
            :class="{ expanded: expandedRows.has(child.userId) }"
          >
            <i
              class="expand-icon fas"
              :class="
                expandedRows.has(child.userId)
                  ? 'fa-chevron-down'
                  : 'fa-chevron-right'
              "
            ></i>
          </button>
        </td>
        <td :style="{ paddingLeft: `${level * 10}px` }">
          {{ formatDateTime(child.registeredAt || "") }}
        </td>
        <td :style="{ paddingLeft: `${level * 10}px` }">
          <span>{{ formatSourceUserName(child.username) || "-" }}</span>
          <span
            v-if="
              showDescendantCount &&
              child.descendantCount !== undefined &&
              child.descendantCount !== null &&
              child.descendantCount > 0
            "
            class="descendant-count"
          >
            {{ child.descendantCount }}
          </span>
        </td>
        <td :style="{ paddingLeft: `${level * 10}px` }">
          {{
            (() => {
              const parts = [];
              if (child.salesLevel !== undefined && child.salesLevel !== null) {
                parts.push(`S${child.salesLevel}`);
              }
              if (
                child.inviteLevel !== null &&
                child.inviteLevel !== undefined
              ) {
                parts.push(`L${child.inviteLevel}`);
              }
              return parts.length > 0 ? parts.join(", ") : "-";
            })()
          }}
        </td>
        <td :style="{ paddingLeft: `${level * 10}px` }">
          {{ child.maskedPhone || "-" }}
        </td>
        <td :style="{ paddingLeft: `${level * 10}px` }">
          {{ child.maskedEmail || "-" }}
        </td>
        <td
          v-if="showTotalEarnings"
          class="earnings"
          :class="{
            'earnings-positive':
              child.totalEarnings !== undefined &&
              child.totalEarnings !== null &&
              child.totalEarnings > 0,
          }"
          :style="{ paddingLeft: `${level * 10}px` }"
        >
          {{
            child.totalEarnings !== undefined &&
            child.totalEarnings !== null &&
            child.totalEarnings > 0
              ? "+"
              : ""
          }}{{
            child.totalEarnings !== undefined &&
            child.totalEarnings !== null &&
            child.totalEarnings > 0
              ? `${child.totalEarnings.toFixed(2)}${$t("settingsSidebar.yuan")}`
              : "-"
          }}
        </td>
      </tr>
      <!-- 递归渲染更深层的子行 -->
      <template
        v-if="
          child.children &&
          child.children.length > 0 &&
          expandedRows.has(child.userId)
        "
      >
        <InviteTreeChildRows
          :item="child"
          :level="level + 1"
          :expanded-rows="expandedRows"
          :loaded-children-nodes="loadedChildrenNodes"
          :target-node-children-user-ids="targetNodeChildrenUserIds"
          :render-single="false"
          :show-descendant-count="showDescendantCount"
          @toggle-expand="handleToggleExpand"
        />
      </template>
    </template>
  </template>
</template>

<script setup lang="ts">
import { useI18n } from "vue-i18n";
import type { InviteTreeItem } from "@/api/pay";

interface Props {
  item: InviteTreeItem;
  level: number;
  expandedRows: Set<number>;
  loadedChildrenNodes?: Set<number>; // 已加载子节点的节点ID集合
  targetNodeChildrenUserIds?: Set<number>; // 目标节点的子节点 userId 集合(用于隐藏展开/收起按钮)
  renderSingle?: boolean; // 是否渲染单个节点(用于父行),默认为 false(渲染子节点列表)
  showDescendantCount?: boolean; // 是否显示后代数量
  showTotalEarnings?: boolean; // 是否显示用户累计收益列
}

interface Emits {
  (e: "toggle-expand", userId: number): void;
}

const props = withDefaults(defineProps<Props>(), {
  renderSingle: false,
  loadedChildrenNodes: () => new Set<number>(),
  targetNodeChildrenUserIds: () => new Set<number>(),
  showDescendantCount: false,
  showTotalEarnings: true,
});
const emit = defineEmits<Emits>();
const { t } = useI18n();

// 格式化日期时间
const formatDateTime = (dateStr: string): string => {
  if (!dateStr) return "";

  // 如果日期字符串包含时间部分,直接格式化
  if (dateStr.includes("T") || dateStr.includes(" ")) {
    const date = new Date(dateStr);
    if (isNaN(date.getTime())) return dateStr;

    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    const hours = String(date.getHours()).padStart(2, "0");
    const minutes = String(date.getMinutes()).padStart(2, "0");
    const seconds = String(date.getSeconds()).padStart(2, "0");

    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  }

  // 如果只有日期部分,添加默认时间 00:00:00
  return `${dateStr} 00:00:00`;
};

// 格式化用户名
const formatSourceUserName = (userName?: string): string => {
  if (!userName) return "-";

  // 匹配"销售代表"(可能有后缀,也可能没有)
  if (userName.startsWith("销售代表")) {
    const suffix = userName.substring(4); // "销售代表"长度为4
    return suffix
      ? `${t("settingsSidebar.salesRepresentative")}${suffix}`
      : t("settingsSidebar.salesRepresentative");
  }

  return userName;
};

// 处理展开/收起
const handleToggleExpand = (userId: number) => {
  emit("toggle-expand", userId);
};
</script>

<style lang="scss">
// 不使用 scoped,因为 tr 元素是直接渲染在父组件的表格 tbody 中的
.child-row {
  background: #fafafa;

  &:hover {
    background: #f5f5f5;
  }
}

// 确保 td 的 padding 为 10px,与资金记录表格一致
// 但保留 paddingLeft 的缩进(通过内联样式设置)
tr td {
  padding-top: 12px !important;
  padding-right: 10px !important;
  padding-bottom: 12px !important;
  // padding-left 由内联样式控制,用于缩进
}

.expand-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: all 0.2s;
  color: #666;

  .expand-icon {
    font-size: 12px;
    transition: transform 0.2s;
  }
}

.earnings-positive {
  font-weight: 600;
  color: #00c853;
}

.empty-children-row {
  background: #fafafa;

  .empty-children-cell {
    text-align: center;
    color: #999;
    font-size: 14px;
    padding: 12px 10px !important;
  }
}

.descendant-count {
  margin-left: 8px;
  color: var(--color-primary);
  font-size: 12px;
  font-weight: 500;
}

// 目标节点高亮样式
.target-node-row {
  background-color: rgba(30, 112, 255, 0.2) !important;

  &:hover {
    background-color: rgba(30, 112, 255, 0.3) !important;
  }
}
</style>