<script lang="ts" setup>
/**
 * 挂号订单卡片看板（C 端紧凑排版 + Apple 毛玻璃）：与列表视图展示同一批字段，仅排版不同
 *
 * 与商品/处方订单卡片同一套设计语言：多列自适应网格、毛玻璃 14px 圆角 hairline 卡片、
 * 状态彩点、tinted 小胶囊、人员信息内联行、底部 hairline 操作栏。
 * 点击卡片主体打开挂号详情弹窗（就诊人/用户/诊所/医生/挂号方式/价格）。
 * 数据自取：props.formValues 变化时重置到第 1 页，触底自动加载下一页。
 */
import type { ActionItem } from '#/components/table-action';

import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';

import { Empty, Image, message, Spin } from 'ant-design-vue';

import { TableAction } from '#/components/table-action';
import { pillStyle, tagColorHex } from '#/utils/tagColor';

import { getRegisterListApi } from '../api';
import {
  REGISTER_REFUND_STATUS_MAP,
  REGISTER_STATUS_MAP,
  REGISTER_TYPE_MAP,
} from '../config/constants';

const props = defineProps<{
  /** 顶部统一搜索条件（含 patient_keyword / doctor_keyword / type / is_pay 等） */
  formValues?: Record<string, any>;
  /** 是否可退款（与列表行操作同一份判定，父级传入保证口径一致） */
  canRefund: (row: Record<string, any>) => boolean;
}>();

const emit = defineEmits<{
  /** 点卡片打开挂号详情弹窗 */
  openDetail: [row: Record<string, any>];
  /** 退款（复用父级退款弹窗） */
  refund: [registerId: number];
  /** 点处方号打开处方详情 */
  openPrescription: [prescriptionId: number];
  /** 点诊所名打开诊所卡片 */
  openStore: [storeId: number];
}>();

const items = ref<any[]>([]);
const page = ref(1);
const pageCount = ref(1);
const loading = ref(false);
const loadingMore = ref(false);
/** 请求序号：只应用最后一次响应，避免快速改筛选时旧结果覆盖新列表 */
let requestSeq = 0;

const scrollEl = ref<HTMLElement | null>(null);
const sentinelEl = ref<HTMLElement | null>(null);
let observer: IntersectionObserver | null = null;

const hasMore = computed(() => page.value < pageCount.value);

/** 挂号方式元数据 */
function typeMeta(row: Record<string, any>) {
  return REGISTER_TYPE_MAP[Number(row?.type)] ?? null;
}

/** 挂号状态元数据 */
function statusMeta(row: Record<string, any>) {
  return REGISTER_STATUS_MAP[Number(row?.status)] ?? null;
}

/** 退款状态元数据（未退款不显示胶囊，减少噪音） */
function refundMeta(row: Record<string, any>) {
  const meta = REGISTER_REFUND_STATUS_MAP[Number(row?.refund_status)];
  return meta && Number(row?.refund_status) !== 0 ? meta : null;
}

/** 挂号价格格式化（非法值显示占位符，¥ 符号由模板单独渲染） */
function money(value: unknown) {
  const n = Number(value);
  return Number.isFinite(n) ? n.toFixed(2) : '—';
}

/** 行操作：退款（显隐与列表一致），通过 emit 复用父级弹窗 */
function buildActions(row: Record<string, any>): ActionItem[] {
  return [
    {
      label: '详情',
      type: 'link',
      icon: 'marketeq:eye',
      onClick: () => emit('openDetail', row),
    },
    {
      label: '退款',
      type: 'link',
      icon: 'mingcute:refund-dollar-fill',
      ifShow: props.canRefund(row),
      onClick: () => emit('refund', Number(row.id)),
    },
  ];
}

/**
 * 拉取一页数据；reset 时清空列表并回到第 1 页
 * 用 requestSeq 丢弃过期响应，防止筛选连点时旧条件结果覆盖新条件
 */
async function fetchPage(targetPage: number, reset = false) {
  const seq = ++requestSeq;
  if (reset) {
    loading.value = true;
  } else {
    loadingMore.value = true;
  }
  try {
    const res: any = await getRegisterListApi({
      page: targetPage,
      pageSize: 12,
      ...(props.formValues || {}),
    });
    if (seq !== requestSeq) return;
    items.value = reset ? (res?.items ?? []) : [...items.value, ...(res?.items ?? [])];
    page.value = res?.page ?? targetPage;
    pageCount.value = res?.page_count ?? 1;
  } catch (error) {
    if (seq !== requestSeq) return;
    console.error(error);
    message.error('加载挂号列表失败');
  } finally {
    if (seq === requestSeq) {
      loading.value = false;
      loadingMore.value = false;
    }
  }
}

/** 筛选条件变化：重置回第 1 页 */
async function reload() {
  page.value = 1;
  items.value = [];
  await fetchPage(1, true);
}

/** 操作后原地刷新：按已加载条数一次性重拉，保持滚动位置 */
async function refreshKeepPage() {
  const loaded = Math.max(items.value.length, 12);
  const seq = ++requestSeq;
  loading.value = true;
  try {
    const res: any = await getRegisterListApi({
      page: 1,
      pageSize: loaded,
      ...(props.formValues || {}),
    });
    if (seq !== requestSeq) return;
    items.value = res?.items ?? [];
    page.value = Math.ceil((res?.items?.length ?? 0) / 12) || 1;
    pageCount.value = Math.ceil((res?.total ?? 0) / 12) || 1;
  } catch (error) {
    if (seq === requestSeq) console.error(error);
  } finally {
    if (seq === requestSeq) loading.value = false;
  }
}

defineExpose({ reload, refreshKeepPage });

async function loadMore() {
  if (!hasMore.value || loading.value || loadingMore.value) return;
  await fetchPage(page.value + 1, false);
}

/** 触底哨兵：滚动到底部附近自动加载下一页 */
function setupObserver() {
  observer?.disconnect();
  if (!sentinelEl.value) return;
  observer = new IntersectionObserver(
    (entries) => {
      if (entries.some((e) => e.isIntersecting)) {
        void loadMore();
      }
    },
    { root: scrollEl.value, rootMargin: '120px' },
  );
  observer.observe(sentinelEl.value);
}

watch(
  () => props.formValues,
  () => {
    void reload();
  },
  { deep: true },
);

onMounted(async () => {
  await reload();
  await nextTick();
  setupObserver();
});

onBeforeUnmount(() => {
  observer?.disconnect();
  observer = null;
});
</script>

<template>
  <div ref="scrollEl" class="rg-card-scroll">
    <Spin :spinning="loading">
      <div v-if="items.length" class="rg-card-grid">
        <div
          v-for="row in items"
          :key="row.id"
          class="rg-card"
          role="button"
          tabindex="0"
          @click="emit('openDetail', row)"
          @keydown.enter="emit('openDetail', row)"
        >
          <!-- 角落径向荧光（与提现管理统计岛同款质感装饰） -->
          <div class="rg-card__glow" />
          <!-- 头部：诊所名（可点）+ 状态彩点 -->
          <div class="rg-card__head">
            <button
              v-if="row.store?.id || row.store_id"
              type="button"
              class="rg-card__store"
              @click.stop="emit('openStore', Number(row.store?.id || row.store_id))"
            >
              <span class="rg-card__store-name">{{ row.store?.name || '—' }}</span>
              <span class="rg-card__store-arrow">›</span>
            </button>
            <span v-else class="rg-card__store-name">{{ row.store?.name || '—' }}</span>
            <span
              v-if="statusMeta(row)"
              class="rg-card__status"
              :style="{ color: tagColorHex(statusMeta(row)!.color) }"
            >
              <i
                class="rg-card__status-dot"
                :style="{ background: tagColorHex(statusMeta(row)!.color) }"
              ></i>
              {{ statusMeta(row)!.label }}
            </span>
          </div>
          <!-- 订单号 / 创建时间 -->
          <div class="rg-card__sub">
            <span class="truncate">{{ row.order_no || '—' }}</span>
            <span class="rg-card__dot">·</span>
            <span class="shrink-0">{{ row.created_at || '—' }}</span>
          </div>
          <!-- 语义胶囊行：挂号方式/支付/退款，右侧挂号费金额（C 端惯例右上金额） -->
          <div class="rg-card__pills">
            <span v-if="typeMeta(row)" class="rg-pill" :style="pillStyle(typeMeta(row)!.color)">
              {{ typeMeta(row)!.label }}
            </span>
            <span class="rg-pill" :style="pillStyle(row.is_pay === 1 ? 'green' : 'red')">
              {{ row.is_pay === 1 ? '已支付' : '未支付' }}
            </span>
            <span v-if="refundMeta(row)" class="rg-pill" :style="pillStyle(refundMeta(row)!.color)">
              {{ refundMeta(row)!.label }}
            </span>
            <span class="rg-card__amount-value"><i>¥</i>{{ money(row.price) }}</span>
          </div>
          <!-- 医生/科室/就诊人/推广员：内联小字行（省高度） -->
          <div class="rg-card__people">
            <span class="rg-kv">
              <i>医生</i>
              <em>{{ row.doctor_info?.name || '—' }}</em>
            </span>
            <span class="rg-kv">
              <i>科室</i>
              <em>{{ row.doctor_info?.depart?.name || '—' }}</em>
            </span>
            <span class="rg-kv">
              <i>就诊人</i>
              <em>{{ row.user_patient?.name || '—' }}</em>
            </span>
            <span class="rg-kv">
              <i>推广</i>
              <em class="rg-card__salesperson">
                <Image
                  v-if="row.salesperson?.id"
                  :src="row.salesperson.avatar || '/img/user-default-avatar.png'"
                  :width="16"
                  :height="16"
                  :preview="false"
                  class="shrink-0 rounded-full object-cover"
                  @click.stop
                />
                {{ row.salesperson?.nick_name || '无' }}
              </em>
            </span>
          </div>
          <!-- 关联处方（有才显示） -->
          <div v-if="Array.isArray(row.prescription) && row.prescription.length" class="rg-card__rx">
            <button
              v-for="item in row.prescription"
              :key="item.id"
              type="button"
              class="rg-card__rx-chip"
              @click.stop="emit('openPrescription', Number(item.id))"
            >
              {{ item.prescription_no }}
            </button>
          </div>
          <!-- 弹性占位：高度富余时把 footer 推到底（同行等高卡对齐），紧凑时保底 8px 间距 -->
          <div class="rg-card__spacer"></div>
          <!-- 底部操作：详情/退款（阻止冒泡避免触发卡片点击） -->
          <div class="rg-card__footer" @click.stop>
            <TableAction :actions="buildActions(row)" :drop-down-actions="[]" />
          </div>
        </div>
      </div>
      <Empty v-else-if="!loading" description="暂无挂号订单" />
    </Spin>
    <div ref="sentinelEl" class="h-8"></div>
    <div v-if="loadingMore" class="py-3 text-center text-xs text-[hsl(var(--muted-foreground))]">
      加载中…
    </div>
    <div
      v-else-if="items.length && !hasMore"
      class="py-3 text-center text-xs text-[hsl(var(--muted-foreground))]"
    >
      没有更多了
    </div>
  </div>
</template>

<style scoped>
.rg-card-scroll {
  height: 100%;
  /* 左右留 6px：通栏卡片 hover 阴影不被容器裁出切边 */
  padding: 4px 6px 20px;
  overflow: auto;
}

/* 多列自适应网格：单卡最窄 340px，宽屏自动 2-4 列（C 端紧凑卡片密度）；
   min(340px, 100%) 防止窄容器（分屏）时轨道小于容器导致横向滚动 */
.rg-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
  gap: 10px;
}

/* 玻璃质感紧凑卡片（与提现管理统计岛同款）：主色渐变玻璃底 + 12px 圆角 + 1px 边框；整卡可点看详情（禁止左侧竖色条） */
.rg-card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  cursor: pointer;
  background: linear-gradient(
    135deg,
    hsl(var(--primary) / 4%),
    hsl(var(--card, var(--background)))
  );
  backdrop-filter: blur(8px);
  border: 1px solid hsl(var(--border));
  border-radius: 12px;
  transition:
    transform 0.2s ease,
    box-shadow 0.2s ease,
    border-color 0.2s ease;
}

.rg-card:hover {
  border-color: hsl(var(--primary) / 40%);
  box-shadow:
    0 8px 20px hsl(var(--primary) / 14%),
    0 2px 6px rgb(0 0 0 / 6%);
  transform: translateY(-3px);
}

/* 角落径向荧光：右上角淡主色光斑，hover 增强 */
.rg-card__glow {
  position: absolute;
  top: -40px;
  right: -40px;
  width: 110px;
  height: 110px;
  pointer-events: none;
  background: radial-gradient(circle, hsl(var(--primary) / 18%) 0%, transparent 70%);
  opacity: 0.4;
  transition: opacity 0.2s ease;
}

.rg-card:hover .rg-card__glow {
  opacity: 1;
}

.rg-card__head {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 10px 12px 0;
}

.rg-card__store {
  display: inline-flex;
  min-width: 0;
  align-items: center;
  gap: 2px;
  padding: 0;
  cursor: pointer;
  background: none;
  border: none;
}

.rg-card__store-name {
  overflow: hidden;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.2px;
  color: hsl(var(--foreground));
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rg-card__store:hover .rg-card__store-name {
  color: hsl(var(--primary));
}

.rg-card__store-arrow {
  flex-shrink: 0;
  font-size: 14px;
  color: hsl(var(--muted-foreground));
}

.rg-card__status {
  display: inline-flex;
  flex-shrink: 0;
  gap: 4px;
  align-items: center;
  margin-left: auto;
  font-size: 12px;
  font-weight: 600;
}

.rg-card__status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}

.rg-card__sub {
  display: flex;
  gap: 4px;
  align-items: center;
  padding: 1px 12px 0;
  font-size: 11px;
  color: hsl(var(--muted-foreground));
}

.rg-card__dot {
  flex-shrink: 0;
}

/* 胶囊行：左胶囊右金额（金额 margin-left:auto 顶到行尾） */
.rg-card__pills {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  padding: 6px 12px 0;
}

.rg-pill {
  display: inline-flex;
  align-items: center;
  padding: 1px 7px;
  font-size: 10px;
  font-weight: 500;
  line-height: 16px;
  border: none;
  border-radius: 999px;
}

/* 医生/科室/就诊人/推广员内联行 */
.rg-card__people {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  padding: 7px 12px 0;
  font-size: 12px;
  line-height: 1.6;
}

.rg-kv {
  display: inline-flex;
  gap: 4px;
  align-items: baseline;
  min-width: 0;
}

.rg-kv > i {
  flex-shrink: 0;
  font-size: 11px;
  font-style: normal;
  color: hsl(var(--muted-foreground) / 75%);
}

.rg-kv > em {
  overflow: hidden;
  font-style: normal;
  font-weight: 500;
  color: hsl(var(--foreground));
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rg-card__salesperson {
  display: inline-flex;
  gap: 4px;
  align-items: center;
}

/* 关联处方 chips */
.rg-card__rx {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 6px 12px 0;
}

.rg-card__rx-chip {
  padding: 1px 8px;
  font-size: 11px;
  color: hsl(var(--primary));
  cursor: pointer;
  background: hsl(var(--primary) / 8%);
  border: 1px solid hsl(var(--primary) / 25%);
  border-radius: 999px;
  transition: box-shadow 0.15s ease;
}

.rg-card__rx-chip:hover {
  box-shadow: 0 0 6px hsl(var(--primary) / 18%);
}

/* 挂号费金额：胶囊行右侧（DIN 等宽数字，与提现管理统计岛同款字体质感） */
.rg-card__amount-value {
  margin-left: auto;
  font-family: 'DIN Alternate', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.3px;
  color: hsl(var(--foreground));
}

.rg-card__amount-value i {
  margin-right: 1px;
  font-size: 11px;
  font-style: normal;
  font-weight: 600;
}

/* 弹性占位：吃掉剩余高度让 footer 吸底（同行等高卡操作栏对齐） */
.rg-card__spacer {
  flex: 1 1 auto;
  min-height: 8px;
}

/* 底部操作栏 */
.rg-card__footer {
  display: flex;
  justify-content: flex-end;
  padding: 4px 6px;
  cursor: default;
  border-top: 1px solid hsl(var(--border) / 50%);
}
</style>
