<script setup lang="ts">
import { formatCurrencyAmount, type LedgerEntry } from "@cents/domain";
import { ArrowLeft, Check, ChevronDown, ChevronLeft, ChevronRight, CircleUserRound, Filter, Minus, ReceiptText, RotateCcw, WalletCards, X } from "@lucide/vue";
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import QuickEntryHost from "../components/QuickEntryHost.vue";
import { categories, entryTypes, findCategory, findCategoryPath } from "../data/categories";
import { ledgerIconComponent } from "../data/ledger-icons";
import { ledgerTheme } from "../data/ledgers";
import { isReimbursement } from "../data/reimbursements";
import { useEntryStore } from "../stores/entries";
import { useLedgerStore } from "../stores/ledgers";

type RangeMode = "month" | "year" | "custom" | "all";
const UNCATEGORIZED_CATEGORY_ID = "uncategorized";

const route = useRoute();
const router = useRouter();
const entryStore = useEntryStore();
const ledgerStore = useLedgerStore();
const now = new Date();
const queryOpen = ref(false);
const expandedCategoryBranches = ref(new Set<string>());

function queryString(value: unknown) {
  return typeof value === "string" ? value : "";
}

function queryDate(value: unknown, fallback: string) {
  const candidate = queryString(value);
  return /^\d{4}-\d{2}-\d{2}$/.test(candidate) ? candidate : fallback;
}

function parseList(value: unknown) {
  return [...new Set(queryString(value).split(",").filter(Boolean))];
}

const rangeMode = ref<RangeMode>(["year", "custom", "all"].includes(queryString(route.query.range)) ? queryString(route.query.range) as RangeMode : "month");
const selectedMonth = ref(/^(\d{4})-(0[1-9]|1[0-2])$/.test(queryString(route.query.month)) ? queryString(route.query.month) : `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`);
const selectedYear = ref(/^\d{4}$/.test(queryString(route.query.year)) ? Number(queryString(route.query.year)) : now.getFullYear());
const customStart = ref(queryDate(route.query.start, `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`));
const customEnd = ref(queryDate(route.query.end, toDateInput(now)));
const selectedLedgers = ref(parseList(route.query.ledgers));
const selectedCategories = ref(parseList(route.query.cat));
const keyword = ref(queryString(route.query.q));

const effectiveLedgerIds = computed(() => selectedLedgers.value.length ? selectedLedgers.value : (ledgerStore.currentLedgerId ? [ledgerStore.currentLedgerId] : []));
const categoryTrees = computed(() => entryTypes.map((type) => ({
  ...type,
  parents: categories[type.id].map((parent) => ({
    ...parent,
    children: [...(parent.children ?? []), { id: "other", label: "其他", color: parent.color, tint: parent.tint, icon: WalletCards }],
  })),
})));

const allRangeBounds = computed(() => {
  const dates = entryStore.entries
    .filter((entry) => entry.ledgerIds.some((ledgerId) => effectiveLedgerIds.value.includes(ledgerId)))
    .map((entry) => new Date(entry.occurredAt).getTime())
    .filter((value) => Number.isFinite(value));
  if (!dates.length) return null;
  const first = new Date(Math.min(...dates));
  const last = new Date(Math.max(...dates));
  return {
    start: new Date(first.getFullYear(), first.getMonth(), first.getDate()),
    end: new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1),
  };
});

const rangeBounds = computed(() => {
  if (rangeMode.value === "all") return allRangeBounds.value;
  if (rangeMode.value === "month") {
    const [year, month] = selectedMonth.value.split("-").map(Number);
    const start = new Date(year, month - 1, 1);
    return { start, end: new Date(year, month, 1) };
  }
  if (rangeMode.value === "year") {
    return { start: new Date(selectedYear.value, 0, 1), end: new Date(selectedYear.value + 1, 0, 1) };
  }
  const start = fromDateInput(customStart.value);
  const selectedEnd = fromDateInput(customEnd.value);
  if (!start || !selectedEnd || start > selectedEnd) return null;
  const end = new Date(selectedEnd);
  end.setDate(end.getDate() + 1);
  return { start, end };
});

function matchesCategory(entry: LedgerEntry, key: string) {
  const parts = key.split(":");
  if (parts.length < 2 || entry.type !== parts[0]) return false;
  const path = findCategoryPath(entry.type, entry.categoryId);
  if (parts.length === 2) {
    if (parts[1] === UNCATEGORIZED_CATEGORY_ID) return path.length === 0;
    return parts[1] === "*" || path.some((item) => item.id === parts[1]);
  }
  return path[0]?.id === parts[1] && (parts[2] === "other" ? entry.categoryId === "other" : entry.categoryId === parts[2]);
}

const categoryLabel = computed(() => {
  if (!selectedCategories.value.length) return "全部分类";
  const labels = selectedCategories.value.map((key) => {
    const separator = key.indexOf(":");
    if (separator < 1) return "";
    const parts = key.split(":");
    const type = parts[0] as LedgerEntry["type"];
    if (parts[1] === "*") return entryTypes.find((item) => item.id === type)?.label ?? "";
    if (parts[1] === UNCATEGORIZED_CATEGORY_ID) {
      const typeLabel = entryTypes.find((item) => item.id === type)?.label ?? "";
      return typeLabel ? `${typeLabel} · 未分类` : "未分类";
    }
    const path = findCategoryPath(type, parts[1]);
    if (parts.length === 2) return path.map((item) => item.label).join(" · ");
    return `${path[0]?.label ?? ""} · ${parts[2] === "other" ? "其他" : findCategoryPath(type, parts[2]).at(-1)?.label ?? parts[2]}`;
  }).filter(Boolean);
  return labels.length > 2 ? `${labels.slice(0, 2).join("、")}等 ${labels.length} 项` : labels.join("、");
});

const ledgerLabel = computed(() => {
  const names = effectiveLedgerIds.value.map((id) => ledgerStore.ledgers.find((ledger) => ledger.id === id)?.name).filter(Boolean) as string[];
  return names.length > 2 ? `${names.slice(0, 2).join("、")}等 ${names.length} 个` : names.join("、") || "全部账本";
});

const selectedCategorySummary = computed(() => {
  let parentCount = 0;
  let childCount = 0;
  for (const tree of categoryTrees.value) {
    for (const parent of tree.parents) {
      const selected = selectedChildCount(tree.id, parent.id);
      if (selected) parentCount += 1;
      childCount += selected;
    }
  }
  const uncategorized = entryTypes
    .filter((type) => isUncategorizedSelected(type.id))
    .map((type) => `${type.label}未分类`);
  return [`${parentCount} 个大类，${childCount} 个小类`, ...uncategorized].join("，");
});

const entries = computed(() => {
  const bounds = rangeBounds.value;
  if (!bounds) return [];
  return entryStore.entries
    .filter((entry) => {
      const occurredAt = new Date(entry.occurredAt);
      if (!entry.ledgerIds.some((ledgerId) => effectiveLedgerIds.value.includes(ledgerId)) || occurredAt < bounds.start || occurredAt >= bounds.end) return false;
      const tokens = keyword.value.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
      if (tokens.some((token) => !entry.note.toLocaleLowerCase().includes(token))) return false;
      return !selectedCategories.value.length || selectedCategories.value.some((key) => matchesCategory(entry, key));
    })
    .sort((left, right) => right.occurredAt.localeCompare(left.occurredAt));
});

const groupedEntries = computed(() => {
  const groups = new Map<string, LedgerEntry[]>();
  for (const entry of entries.value) {
    const date = localDateKey(entry.occurredAt);
    groups.set(date, [...(groups.get(date) ?? []), entry]);
  }
  return [...groups.entries()].map(([date, items]) => ({ date, entries: items }));
});

const rangeLabel = computed(() => {
  if (rangeMode.value === "month") {
    const [year, month] = selectedMonth.value.split("-");
    return `${year}年${Number(month)}月`;
  }
  if (rangeMode.value === "year") return `${selectedYear.value}年`;
  if (rangeMode.value === "all" && rangeBounds.value) {
    return `${formatFullDateLabel(toDateInput(rangeBounds.value.start))} 至 ${formatFullDateLabel(toDateInput(new Date(rangeBounds.value.end.getTime() - 86400000)))}`;
  }
  return `${customStart.value} 至 ${customEnd.value}`;
});
const currentTheme = computed(() => ledgerTheme(ledgerStore.currentLedger?.theme));
const yearOptions = computed(() => {
  const years = new Set([now.getFullYear(), ...entryStore.entries.map((entry) => new Date(entry.occurredAt).getFullYear())]);
  return [...years].sort((left, right) => right - left);
});

function toggleListValue(target: typeof selectedLedgers, value: string) {
  target.value = target.value.includes(value) ? target.value.filter((item) => item !== value) : [...target.value, value];
}

function toggleLedger(id: string) {
  if (selectedLedgers.value.length === 1 && selectedLedgers.value[0] === id) return;
  toggleListValue(selectedLedgers, id);
}

function parentKey(type: LedgerEntry["type"], parentId: string) {
  return `${type}:${parentId}`;
}

function childKey(type: LedgerEntry["type"], parentId: string, childId: string) {
  return `${type}:${parentId}:${childId}`;
}

function isParentFullySelected(type: LedgerEntry["type"], parentId: string) {
  const parent = categoryTrees.value.find((tree) => tree.id === type)?.parents.find((item) => item.id === parentId);
  if (!parent) return false;
  return selectedCategories.value.includes(parentKey(type, parentId))
    || parent.children.every((child) => selectedCategories.value.includes(childKey(type, parentId, child.id)));
}

function isChildSelected(type: LedgerEntry["type"], parentId: string, childId: string) {
  return selectedCategories.value.includes(parentKey(type, parentId))
    || selectedCategories.value.includes(childKey(type, parentId, childId));
}

function isParentPartiallySelected(type: LedgerEntry["type"], parentId: string) {
  return selectedChildCount(type, parentId) > 0 && !isParentFullySelected(type, parentId);
}

function selectedChildCount(type: LedgerEntry["type"], parentId: string) {
  const parent = categoryTrees.value.find((tree) => tree.id === type)?.parents.find((item) => item.id === parentId);
  if (!parent) return 0;
  if (selectedCategories.value.includes(parentKey(type, parentId))) return parent.children.length;
  return parent.children.filter((child) => selectedCategories.value.includes(childKey(type, parentId, child.id))).length;
}

function selectedTypeCount(type: LedgerEntry["type"]) {
  const categorized = categoryTrees.value.find((tree) => tree.id === type)?.parents.reduce((total, parent) => total + selectedChildCount(type, parent.id), 0) ?? 0;
  return categorized + (isUncategorizedSelected(type) ? 1 : 0);
}

function uncategorizedKey(type: LedgerEntry["type"]) {
  return `${type}:${UNCATEGORIZED_CATEGORY_ID}`;
}

function isUncategorizedSelected(type: LedgerEntry["type"]) {
  return selectedCategories.value.includes(uncategorizedKey(type));
}

function toggleUncategorized(type: LedgerEntry["type"]) {
  toggleListValue(selectedCategories, uncategorizedKey(type));
}

function isCategoryBranchExpanded(type: LedgerEntry["type"], parentId: string) {
  return expandedCategoryBranches.value.has(parentKey(type, parentId));
}

function toggleCategoryBranch(type: LedgerEntry["type"], parentId: string) {
  const key = parentKey(type, parentId);
  const next = new Set(expandedCategoryBranches.value);
  if (next.has(key)) next.delete(key);
  else next.add(key);
  expandedCategoryBranches.value = next;
}

function toggleAllCategory(type: LedgerEntry["type"], parentId: string) {
  const key = parentKey(type, parentId);
  const parent = categoryTrees.value.find((tree) => tree.id === type)?.parents.find((item) => item.id === parentId);
  if (!parent) return;
  const next = selectedCategories.value.filter((item) => item !== key && !item.startsWith(`${key}:`));
  if (!isParentFullySelected(type, parentId)) next.push(key);
  selectedCategories.value = next;
}

function toggleChildCategory(type: LedgerEntry["type"], parentId: string, childId: string) {
  const parent = categoryTrees.value.find((tree) => tree.id === type)?.parents.find((item) => item.id === parentId);
  if (!parent) return;
  const allChildren = parent.children.map((child) => childKey(type, parentId, child.id));
  let next = selectedCategories.value.filter((item) => item !== parentKey(type, parentId) && !item.startsWith(`${parentKey(type, parentId)}:`));
  const selected = isChildSelected(type, parentId, childId);
  if (selected && selectedCategories.value.includes(parentKey(type, parentId))) {
    next.push(...allChildren.filter((key) => key !== childKey(type, parentId, childId)));
  } else if (!selected) {
    next = [...selectedCategories.value, childKey(type, parentId, childId)];
  } else {
    next = selectedCategories.value.filter((key) => key !== childKey(type, parentId, childId));
  }
  selectedCategories.value = [...new Set(next)];
}

function chooseMonth(month: number) {
  selectedMonth.value = `${selectedYear.value}-${String(month + 1).padStart(2, "0")}`;
  rangeMode.value = "month";
}

function shiftYear(offset: -1 | 1) {
  const index = yearOptions.value.indexOf(selectedYear.value);
  const next = yearOptions.value[index + (offset < 0 ? 1 : -1)];
  if (next !== undefined) selectedYear.value = next;
}

function syncFromRoute() {
  const range = queryString(route.query.range);
  rangeMode.value = ["year", "custom", "all"].includes(range) ? range as RangeMode : "month";
  selectedMonth.value = /^(\d{4})-(0[1-9]|1[0-2])$/.test(queryString(route.query.month)) ? queryString(route.query.month) : selectedMonth.value;
  selectedYear.value = /^\d{4}$/.test(queryString(route.query.year)) ? Number(route.query.year) : selectedYear.value;
  customStart.value = queryDate(route.query.start, customStart.value);
  customEnd.value = queryDate(route.query.end, customEnd.value);
  selectedLedgers.value = parseList(route.query.ledgers);
  selectedCategories.value = parseList(route.query.cat);
  keyword.value = queryString(route.query.q);
}

watch(() => route.query, syncFromRoute, { deep: true });
watch([rangeMode, selectedMonth, selectedYear, customStart, customEnd, selectedLedgers, selectedCategories, keyword], () => {
  if (route.name !== "query") return;
  const query = { ...route.query };
  query.range = rangeMode.value;
  query.month = selectedMonth.value;
  query.year = String(selectedYear.value);
  query.start = customStart.value;
  query.end = customEnd.value;
  if (selectedLedgers.value.length) query.ledgers = selectedLedgers.value.join(",");
  else delete query.ledgers;
  if (selectedCategories.value.length) query.cat = selectedCategories.value.join(",");
  else delete query.cat;
  if (keyword.value.trim()) query.q = keyword.value.trim();
  else delete query.q;
  void router.replace({ query });
});

onMounted(async () => {
  await Promise.all([entryStore.loadEntries(), ledgerStore.loadLedgers()]);
  if (!selectedLedgers.value.length && ledgerStore.currentLedgerId) selectedLedgers.value = [ledgerStore.currentLedgerId];
});

function entryCategoryLabel(entry: LedgerEntry) {
  if (isReimbursement(entry)) {
    const source = entryStore.entries.find((item) => item.id === entry.reimbursementOfEntryId);
    const label = source ? findCategoryPath("expense", source.categoryId).at(-1)?.label : "";
    return `报销${label ? ` · ${label}` : ""}`;
  }
  return findCategoryPath(entry.type, entry.categoryId).map((item) => item.label).join(" · ")
    || (entry.type === "expense" ? "支出" : "收入");
}

function entryIcon(entry: LedgerEntry) {
  if (isReimbursement(entry)) return RotateCcw;
  return findCategory(entry.categoryId)?.icon ?? entryTypes.find((item) => item.id === entry.type)?.icon ?? ReceiptText;
}

function entryAmount(entry: LedgerEntry) {
  if (entry.baseAmount !== null) return `${entry.type === "expense" ? "−" : "+"}¥${money(entry.baseAmount)}`;
  return `${entry.type === "expense" ? "−" : "+"}${formatCurrencyAmount(entry.amount, entry.currency)}`;
}

function dayLabel(value: string) {
  const date = new Date(`${value}T00:00:00`);
  return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 · ${new Intl.DateTimeFormat("zh-CN", { weekday: "short" }).format(date)}`;
}

function money(value: number) {
  return (value / 100).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

function toDateInput(value: Date) {
  return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
}

function formatFullDateLabel(value: string) {
  const date = fromDateInput(value);
  return date ? `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` : value;
}

function fromDateInput(value: string) {
  const [year, month, day] = value.split("-").map(Number);
  if (!year || !month || !day) return null;
  return new Date(year, month - 1, day);
}

function localDateKey(value: string) {
  return toDateInput(new Date(value));
}
</script>

<template>
  <main class="app-shell stats-entries-shell">
    <header class="stats-entries-header" :style="{ '--ledger-gradient': currentTheme.gradient }">
      <button type="button" aria-label="返回" title="返回" @click="router.back()"><ArrowLeft :size="21" /></button>
      <div><strong>查询</strong><span>{{ rangeLabel }} · {{ ledgerLabel }} · {{ selectedCategorySummary }}</span></div>
      <button type="button" aria-label="打开查询条件" title="查询条件" @click="queryOpen = true"><Filter :size="20" /></button>
    </header>
    <section class="query-summary">
      <button type="button" @click="queryOpen = true"><span>账本</span><strong>{{ ledgerLabel }}</strong><ChevronDown :size="15" /></button>
      <button type="button" @click="queryOpen = true"><span>时间</span><strong>{{ rangeLabel }}</strong><ChevronDown :size="15" /></button>
      <button type="button" @click="queryOpen = true"><span>分类</span><strong>{{ selectedCategorySummary }}</strong><ChevronDown :size="15" /></button>
      <label class="query-keyword"><span>备注</span><input v-model="keyword" type="search" placeholder="支持模糊匹配" autocomplete="off" /></label>
    </section>
    <div class="stats-entries-scroll">
      <div v-if="!entries.length" class="stats-entries-empty"><ReceiptText :size="28" /><strong>没有符合条件的流水</strong><span>{{ rangeLabel }}暂无{{ categoryLabel }}记录</span></div>
      <section v-for="group in groupedEntries" :key="group.date" class="stats-entries-day">
        <header><strong>{{ dayLabel(group.date) }}</strong><span>{{ group.entries.length }} 笔</span></header>
        <RouterLink v-for="entry in group.entries" :key="entry.id" :to="`/entries/${entry.id}`" class="stats-entry-row">
          <span class="stats-entry-icon" :style="{ color: isReimbursement(entry) ? '#287c70' : findCategory(entry.categoryId)?.color ?? '#087f72', background: isReimbursement(entry) ? '#e8f6f2' : findCategory(entry.categoryId)?.tint ?? '#e9f7f3' }"><component :is="entryIcon(entry)" :size="19" /></span>
          <span class="stats-entry-copy"><strong>{{ entry.note || entryCategoryLabel(entry) }}</strong><small><CircleUserRound :size="12" />{{ entryCategoryLabel(entry) }}</small></span>
          <strong class="stats-entry-amount" :class="entry.type">{{ entryAmount(entry) }}</strong>
        </RouterLink>
      </section>
    </div>

    <Transition name="month-picker">
      <div v-if="queryOpen" class="month-picker-layer query-layer">
        <button class="month-picker-scrim" type="button" aria-label="关闭查询条件" @click="queryOpen = false"></button>
        <section class="month-picker-modal query-modal" role="dialog" aria-modal="true" aria-label="查询条件">
          <header><span></span><strong>查询条件</strong><button type="button" aria-label="关闭" title="关闭" @click="queryOpen = false"><X :size="20" /></button></header>

          <div class="query-modal-scroll">
          <section class="query-filter-section"><strong>账本</strong><div class="query-option-grid">
            <button v-for="ledger in ledgerStore.ledgers" :key="ledger.id" type="button" class="query-ledger-option" :class="{ selected: selectedLedgers.includes(ledger.id) }" @click="toggleLedger(ledger.id)"><span class="query-ledger-icon" :style="{ background: ledgerTheme(ledger.theme).gradient }"><component :is="ledgerIconComponent(ledger.icon)" :size="17" /></span><span>{{ ledger.name }}</span><Check v-if="selectedLedgers.includes(ledger.id)" :size="15" /></button>
          </div></section>

          <section class="query-filter-section"><strong>时间范围</strong><div class="stats-range-modes"><button type="button" :class="{ active: rangeMode === 'month' }" @click="rangeMode = 'month'">月</button><button type="button" :class="{ active: rangeMode === 'year' }" @click="rangeMode = 'year'">年</button><button type="button" :class="{ active: rangeMode === 'custom' }" @click="rangeMode = 'custom'">自定义</button><button type="button" :class="{ active: rangeMode === 'all' }" @click="rangeMode = 'all'">全部</button></div>
            <template v-if="rangeMode === 'month'"><div class="query-year-nav"><button type="button" @click="shiftYear(-1)"><ChevronLeft :size="18" /></button><strong>{{ selectedYear }}</strong><button type="button" @click="shiftYear(1)"><ChevronRight :size="18" /></button></div><div class="query-month-grid"><button v-for="month in 12" :key="month" type="button" :class="{ selected: selectedMonth === `${selectedYear}-${String(month).padStart(2, '0')}` }" @click="chooseMonth(month - 1)">{{ month }}月</button></div></template>
            <div v-else-if="rangeMode === 'year'" class="query-year-list"><button v-for="year in yearOptions" :key="year" type="button" :class="{ selected: selectedYear === year }" @click="selectedYear = year">{{ year }}年</button></div>
            <div v-else-if="rangeMode === 'all'" class="query-all-range">{{ rangeLabel }}</div>
            <div v-else class="query-custom-range"><label>开始<input v-model="customStart" type="date" /></label><label>结束<input v-model="customEnd" type="date" /></label></div>
          </section>

          <label class="query-modal-keyword"><strong>备注</strong><input v-model="keyword" type="search" placeholder="支持模糊匹配，关键字用空格分隔" autocomplete="off" /></label>

          <section class="query-filter-section"><strong>分类</strong>
            <div class="query-category-columns">
              <div v-for="tree in categoryTrees" :key="tree.id" class="query-category-tree">
                <header><span :style="{ color: tree.color }">{{ tree.label }}</span><b v-if="selectedTypeCount(tree.id)">{{ selectedTypeCount(tree.id) }}</b></header>
                <div v-for="parent in tree.parents" :key="parent.id" class="query-category-branch">
                  <div class="query-category-parent"><button class="query-category-check" type="button" :class="{ selected: isParentFullySelected(tree.id, parent.id), partial: isParentPartiallySelected(tree.id, parent.id) }" :aria-label="`${isParentFullySelected(tree.id, parent.id) ? '取消' : '选择'}全部${parent.label}`" :aria-checked="isParentPartiallySelected(tree.id, parent.id) ? 'mixed' : isParentFullySelected(tree.id, parent.id)" role="checkbox" @click.stop="toggleAllCategory(tree.id, parent.id)"><Check v-if="isParentFullySelected(tree.id, parent.id)" :size="14" /><Minus v-else-if="isParentPartiallySelected(tree.id, parent.id)" :size="14" /></button><button type="button" class="query-category-main" :class="{ selected: isParentFullySelected(tree.id, parent.id) }" :aria-label="`${isCategoryBranchExpanded(tree.id, parent.id) ? '收起' : '展开'}${parent.label}`" @click="toggleCategoryBranch(tree.id, parent.id)"><component :is="parent.icon" :size="17" /><span>{{ parent.label }}</span><b v-if="selectedChildCount(tree.id, parent.id)">{{ selectedChildCount(tree.id, parent.id) }}</b></button><button class="query-category-toggle" type="button" :aria-label="`${isCategoryBranchExpanded(tree.id, parent.id) ? '收起' : '展开'}${parent.label}`" @click="toggleCategoryBranch(tree.id, parent.id)"><ChevronDown v-if="isCategoryBranchExpanded(tree.id, parent.id)" :size="14" /><ChevronRight v-else :size="14" /></button></div>
                  <div v-if="isCategoryBranchExpanded(tree.id, parent.id)" class="query-category-children"><button type="button" class="query-category-all" :class="{ selected: isParentFullySelected(tree.id, parent.id) }" @click="toggleAllCategory(tree.id, parent.id)">全部<Check v-if="isParentFullySelected(tree.id, parent.id)" :size="13" /></button><button v-for="child in parent.children" :key="child.id" type="button" :class="{ selected: isChildSelected(tree.id, parent.id, child.id) }" @click="toggleChildCategory(tree.id, parent.id, child.id)">{{ child.label }}<Check v-if="isChildSelected(tree.id, parent.id, child.id)" :size="13" /></button></div>
                </div>
                <button type="button" class="query-uncategorized" :class="{ selected: isUncategorizedSelected(tree.id) }" :aria-label="`${isUncategorizedSelected(tree.id) ? '取消' : '选择'}${tree.label}未分类`" @click="toggleUncategorized(tree.id)"><ReceiptText :size="15" /><span>未分类</span><Check v-if="isUncategorizedSelected(tree.id)" :size="14" /></button>
              </div>
            </div>
          </section>
          </div>

          <button class="query-done" type="button" @click="queryOpen = false">完成</button>
        </section>
      </div>
    </Transition>
    <QuickEntryHost />
  </main>
</template>

<style scoped>
.stats-entries-shell { display:flex; flex-direction:column; background:#f5f8f7; }
.stats-entries-header { height:74px; display:grid; grid-template-columns:40px 1fr 40px; align-items:center; gap:8px; padding:14px 16px 8px; background:var(--ledger-gradient,#087f72); color:#fff; }
.stats-entries-header > button { width:38px; height:38px; display:grid; place-items:center; border:0; border-radius:8px; background:rgba(255,255,255,.14); color:#fff; }
.stats-entries-header > div { min-width:0; display:grid; gap:2px; text-align:center; }
.stats-entries-header strong { font-size:17px; }
.stats-entries-header span { overflow:hidden; color:rgba(255,255,255,.76); font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
.query-summary { position:sticky; top:0; z-index:2; display:grid; grid-template-columns:minmax(0,1fr); gap:7px; border-bottom:1px solid #dce7e4; padding:10px 16px; background:#f7faf9; box-shadow:0 2px 7px rgba(31,57,51,.06); }
.query-summary > button,.query-keyword { min-width:0; min-height:42px; display:grid; grid-template-columns:auto minmax(0,1fr) 15px; align-items:center; gap:5px; border:1px solid #d2dfdc; border-radius:8px; padding:0 9px; background:#fff; color:#52645e; text-align:left; }
.query-summary > button span,.query-keyword span { color:#84928e; font-size:10px; }
.query-summary > button strong { overflow:hidden; color:#344640; font-size:12px; text-overflow:ellipsis; white-space:nowrap; }
.query-keyword { grid-column:auto; grid-template-columns:auto minmax(0,1fr); }
.query-keyword input,.query-modal-keyword input { width:100%; min-width:0; border:0; outline:0; background:transparent; color:#26342f; font-size:13px; }
.query-layer { z-index:17; }
.query-modal { max-height:90%; display:flex; flex-direction:column; overflow:hidden; }
.query-modal > header,.query-done { flex:0 0 auto; }
.query-modal-scroll { min-height:0; flex:1; overflow-y:auto; overscroll-behavior:contain; }
.query-filter-section { border-bottom:1px solid #e1ebe8; padding:14px 0; }
.query-filter-section > strong,.query-modal-keyword > strong { display:block; margin-bottom:9px; color:#7b8884; font-size:11px; }
.query-filter-section .stats-range-modes { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; border-radius:8px; padding:4px; background:#e7eeec; }
.query-filter-section .stats-range-modes button { height:34px; border:0; border-radius:6px; background:transparent; color:#71807c; font-size:12px; font-weight:680; }
.query-filter-section .stats-range-modes button.active { background:#fff; color:#087f72; box-shadow:0 2px 7px rgba(28,52,47,.1); }
.query-option-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; }
.query-option-grid button { min-height:40px; display:flex; align-items:center; justify-content:space-between; gap:5px; border:1px solid #e1ebe8; border-radius:8px; padding:0 9px; background:#fff; color:#344640; text-align:left; }
.query-option-grid button.selected { border-color:#087f72; background:#eaf7f3; color:#087f72; box-shadow:inset 0 0 0 1px #087f72; font-weight:720; }
.query-ledger-option { justify-content:flex-start!important; }
.query-ledger-option > span:nth-child(2) { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.query-ledger-icon { width:26px; height:26px; display:grid; flex:0 0 auto; place-items:center; border-radius:6px; color:#fff; }
.query-year-nav { display:grid; grid-template-columns:38px 1fr 38px; align-items:center; gap:4px; margin:14px 0 10px; }
.query-year-nav button { min-height:34px; display:grid; place-items:center; border:0; border-radius:7px; background:transparent; color:#536762; }
.query-year-nav strong { text-align:center; color:#087f72; font-size:15px; }
.query-month-grid { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:7px; margin-top:8px; }
.query-month-grid button,.query-year-list button { min-height:42px; border:1px solid #e1ebe8; border-radius:8px; background:#fff; color:#344640; font-size:12px; }
.query-month-grid button.selected,.query-year-list button.selected { border-color:#087f72; background:#eaf7f3; color:#087f72; font-weight:720; }
.query-year-list { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px; margin-top:12px; }
.query-custom-range { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; margin-top:12px; }
.query-custom-range label { display:grid; gap:4px; color:#7b8884; font-size:10px; }
.query-custom-range input { width:100%; min-width:0; height:38px; border:1px solid #d2dfdc; border-radius:7px; padding:0 6px; background:#fff; color:#344640; font-size:12px; }
.query-all-range { margin-top:12px; border:1px solid #d2dfdc; border-radius:8px; padding:11px 10px; background:#f8fbfa; color:#536762; font-size:12px; text-align:center; }
.query-category-head { display:flex; align-items:center; justify-content:space-between; min-height:36px; margin-bottom:7px; color:#536762; font-size:12px; }
.query-category-head button { display:flex; align-items:center; gap:3px; border:0; background:transparent; color:#087f72; font-size:12px; }
.query-category-list { display:grid; gap:6px; }
.query-category-list > div { display:grid; grid-template-columns:minmax(0,1fr) 36px; gap:5px; }
.query-category-main,.query-category-next { min-height:40px; display:flex; align-items:center; gap:7px; border:1px solid #e1ebe8; background:#fff; color:#344640; }
.query-category-main { border-radius:8px 0 0 8px; padding:0 9px; text-align:left; }
.query-category-main.selected { border-color:#087f72; background:#eaf7f3; color:#087f72; font-weight:720; }
.query-category-next { justify-content:center; border-radius:0 8px 8px 0; color:#7b8884; }
.query-category-columns { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:9px; align-items:start; }
.query-category-tree { min-width:0; border:1px solid #e1ebe8; border-radius:9px; padding:7px; background:#fbfdfc; }
.query-category-tree > header { min-height:27px; display:flex; align-items:center; justify-content:space-between; padding:0 3px 6px; border-bottom:1px solid #e5edeb; font-size:12px; font-weight:760; }
.query-category-tree > header b,.query-category-parent b { min-width:17px; height:17px; display:inline-grid; place-items:center; border-radius:9px; background:#eaf7f3; color:#087f72; font-size:10px; font-weight:760; }
.query-category-branch { padding:6px 0 4px; border-bottom:1px solid #edf2f0; }
.query-category-branch:last-child { border-bottom:0; }
.query-category-parent { display:grid; grid-template-columns:22px minmax(0,1fr) 18px; gap:3px; }
.query-category-parent button { min-width:0; min-height:30px; display:flex; align-items:center; gap:4px; border:0; border-radius:6px; padding:0 4px; background:transparent; color:#344640; font-size:12px; text-align:left; }
.query-category-check { width:20px; height:20px; min-height:20px!important; justify-self:center; align-self:center; justify-content:center; border:1px solid #cbd9d5!important; border-radius:5px!important; padding:0!important; background:#fff!important; color:#fff!important; }
.query-category-check.selected { border-color:#087f72!important; background:#087f72!important; }
.query-category-check.partial { border-color:#087f72!important; background:#eaf7f3!important; color:#087f72!important; }
.query-category-main { border:0!important; background:transparent!important; }
.query-category-main.selected { background:#eaf7f3!important; color:#087f72; font-weight:720; }
.query-category-parent button span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.query-category-parent button b { margin-left:auto; }
.query-category-toggle { width:18px; min-height:30px; display:grid; place-items:center; border:0; border-radius:5px; background:transparent; color:#a1ada9; }
.query-category-children { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:3px; padding:4px 0 0 20px; }
.query-category-children button { min-width:0; min-height:27px; display:flex; align-items:center; justify-content:space-between; gap:2px; overflow:hidden; border:0; border-radius:5px; padding:0 3px; background:transparent; color:#71807c; font-size:11px; text-align:left; text-overflow:ellipsis; white-space:nowrap; }
.query-category-children button.selected { background:#eaf7f3; color:#087f72; font-weight:720; }
.query-category-children .query-category-all { color:#536762; font-weight:680; }
.query-uncategorized { width:100%; min-height:31px; display:flex; align-items:center; gap:5px; margin-top:6px; border:0; border-top:1px dashed #dbe5e2; padding:6px 4px 0; background:transparent; color:#7b8884; font-size:11px; text-align:left; }
.query-uncategorized svg:last-child { margin-left:auto; }
.query-uncategorized.selected { color:#087f72; font-weight:720; }
.query-modal-keyword { display:grid; gap:6px; border-bottom:1px solid #e1ebe8; padding:14px 0; }
.query-modal-keyword input { min-height:40px; border:1px solid #d2dfdc; border-radius:8px; padding:0 10px; background:#f8fbfa; }
.query-done { width:100%; min-height:44px; margin-top:14px; border:0; border-radius:8px; background:#087f72; color:#fff; font-weight:720; }
.stats-entries-scroll { min-height:0; flex:1; overflow-y:auto; padding:0 16px 104px; }
.stats-entries-day { margin-top:14px; border-top:1px solid #dce7e4; border-bottom:1px solid #dce7e4; background:#fff; }
.stats-entries-day > header { min-height:42px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid #e4ece9; padding:0 12px; }
.stats-entries-day > header strong { color:#64726f; font-size:12px; }
.stats-entries-day > header span { color:#8a9994; font-size:11px; }
.stats-entry-row { min-height:64px; display:grid; grid-template-columns:36px minmax(0,1fr) auto; align-items:center; gap:10px; border-bottom:1px solid #edf2f0; padding:8px 12px; color:#26342f; text-decoration:none; }
.stats-entry-row:last-child { border-bottom:0; }
.stats-entry-icon { width:34px; height:34px; display:grid; place-items:center; border-radius:8px; }
.stats-entry-copy { min-width:0; display:grid; gap:4px; }
.stats-entry-copy strong { overflow:hidden; font-size:13px; text-overflow:ellipsis; white-space:nowrap; }
.stats-entry-copy small { display:flex; align-items:center; gap:3px; overflow:hidden; color:#7d8986; font-size:10px; text-overflow:ellipsis; white-space:nowrap; }
.stats-entry-amount { font-size:12px; font-variant-numeric:tabular-nums; white-space:nowrap; }
.stats-entry-amount.income { color:#0d8b67; }
.stats-entry-amount.expense { color:#d84c36; }
.stats-entries-empty { min-height:260px; display:grid; place-items:center; align-content:center; gap:8px; color:#7b8884; font-size:13px; text-align:center; }
.stats-entries-empty strong { color:#536762; }
</style>
