from __future__ import annotations

import csv
import ctypes
from datetime import datetime, timedelta
import html as html_lib
import json
import math
import os
from pathlib import Path
import re
import threading
import tkinter as tk
import tkinter.font as tkfont
from tkinter import filedialog, messagebox, ttk
from tkinter.scrolledtext import ScrolledText
from typing import Callable, Sequence
import xml.etree.ElementTree as ET
from zipfile import ZipFile

from .core import (
    HydrologyError,
    DESIGN_FREQUENCIES_7,
    FrequencyRunoffInput,
    INSTANT_INFILTRATION_TABLE_LABELS,
    INSTANT_RAINFALL_ZONE_LABELS,
    REGION_FREQUENCIES,
    REGION_PARAMS,
    RATIONAL_FORMULA_REGION_LABELS,
    SMALL_RIVER_DURATION_RULE_TEXT,
    SMALL_RIVER_FREQUENCIES,
    SMALL_RIVER_RAINFALL_SCOPE_TEXT,
    SMALL_RIVER_REGION_NAMES,
    TIE_FREQUENCIES,
    TIE_SOIL_CLASS_PARAMS,
    FrequencyRunoffSetting,
    FloodSynthesisComponent,
    EnergyEquationInput,
    HecReachInput,
    HydraulicSectionInput,
    WeirFlowInput,
    WeirSectionRatingInput,
    INSTANT_UNIT_PARAM_FORMULA_LABELS,
    InstantRainfallDesignInput,
    ManningSectionInput,
    RegionalFormulaInput,
    SmallRiverInput,
    StationFlow,
    TieyuanInput,
    calculate_area_analogy,
    calculate_area_index,
    calculate_energy_equation_water_level,
    calculate_hec_profile,
    calculate_instant_unit_line_batch,
    calculate_instant_unit_line_atlas_batch,
    calculate_rational_formula_atlas_batch,
    calculate_regional_formula,
    calculate_small_river,
    calculate_tieyuan,
    calculate_weir_section_rating,
    calculate_weir_water_level,
    critical_water_level_for_section,
    double_log_interpolate_flows,
    format_fixed_table,
    calculate_manning_water_level,
    hydraulic_state_for_level,
    manning_flow_for_level,
    normal_water_level_for_section,
    default_instant_infiltration_table_id,
    instant_rainfall_distribution_table_text,
    instant_unit_line_audit_result_to_text,
    instant_unit_line_result_to_text,
    pearson_iii_design_values,
    rainfall_duration,
    rational_formula_audit_result_to_text,
    rational_formula_result_to_text,
    regional_formula_parameter_rows,
    select_instant_rainfall_distribution,
    small_river_parameter_rows,
    synthesize_flood_hydrographs,
    tieyuan_design_rainfall_values,
)
from .export import (
    export_calculation_book_docx,
    export_calculation_book_xlsx,
    export_calculation_report_html,
    export_rainfall_runoff_audit_xlsx,
    parse_calculation_blocks,
)
from .backwater import (
    BACKWATER_ETA_ROWS,
    BACKWATER_SOIL_TYPES,
    BackwaterInput,
    calculate_backwater,
    eta_from_blocked_flow_ratio,
    obstruction_area_ratio_percent,
)
from .wave import WaveHeightInput, WindSetupInput, calculate_wave_height, calculate_wind_setup
from .reservoir_routing import ReservoirRoutingInput, ReservoirRoutingResult, route_reservoir_flood
from .reservoir_storage import (
    DamSlopeProtectionInput,
    EarthDamFreeboardInput,
    EarthDamProfileInput,
    MountainFloodInput,
    PlaneGateForceInput,
    ReservoirCapacityInput,
    ReservoirCapacityLayer,
    ReservoirFloodInput,
    ReservoirSedimentationInput,
    calculate_dam_slope_protection,
    calculate_earth_dam_freeboard,
    calculate_earth_dam_profile,
    calculate_mountain_flood_peak,
    calculate_plane_gate_force,
    calculate_reservoir_annual_regulation,
    calculate_reservoir_capacity,
    calculate_reservoir_flood,
    calculate_reservoir_sedimentation,
)
from .river_levee import (
    BendScourInput,
    LockDamScourInput,
    ManningLeveeCrestInput,
    ObliqueBankScourInput,
    ParallelBankScourInput,
    RevetmentBlockSizeInput,
    RevetmentThicknessInput,
    RiverDischargeEstimateInput,
    ShortGroyneDesignInput,
    TrainingLineWidthInput,
    calculate_bend_scour,
    calculate_lock_dam_scour,
    calculate_manning_levee_crest,
    calculate_oblique_bank_scour,
    calculate_parallel_bank_scour,
    calculate_revetment_block_size,
    calculate_revetment_thickness,
    calculate_river_discharge_estimate,
    calculate_short_groyne_design,
    calculate_training_line_width,
)
from .hydropower import (
    CandidateEconomicInput,
    ExternalPressureInput,
    HydropowerOutputInput,
    InstalledCapacityInput,
    LoadForecastInput,
    PipeHeadLossInput,
    PipeSubmergenceInput,
    PressurePipeHydropowerInput,
    TransformerCapacityInput,
    TransmissionConductorInput,
    VentPipeInput,
    WaterHammerInput,
    calculate_candidate_economics,
    calculate_external_pressure,
    calculate_hydropower_output,
    calculate_installed_capacity,
    calculate_load_forecast,
    calculate_pipe_head_loss,
    calculate_pipe_minimum_submergence,
    calculate_pressure_pipe_hydropower,
    calculate_vent_pipe,
    calculate_water_hammer,
    calculate_transformer_capacity,
    calculate_transmission_conductor,
)
from .channel_hydraulics import (
    BestHydraulicSectionInput,
    ChannelSidewallStabilityInput,
    CompoundChannelInput,
    CriticalDepthInput,
    IntakeAngleInput,
    LinedSeepageInput,
    LongitudinalDesignInput,
    LongitudinalPointInput,
    NonScourVelocityInput,
    NonSiltingVelocityInput,
    OpenChannelInput,
    PracticalEconomicSectionInput,
    RebarGroupInput,
    ReinforcementInput,
    UChannelInput,
    UnlinedSeepageInput,
    calculate_best_hydraulic_section,
    calculate_channel_longitudinal_design,
    calculate_channel_sidewall_stability,
    calculate_compound_channel,
    calculate_critical_depth,
    calculate_intake_angle,
    calculate_lined_seepage_loss,
    calculate_non_scour_velocity,
    calculate_non_silting_velocity,
    calculate_open_channel,
    calculate_practical_economic_section,
    calculate_reinforcement,
    calculate_unlined_seepage_loss,
    solve_compound_total_depth_for_flow,
    solve_u_channel_by_width_depth_ratio,
)
from .hydraulic_structures_book import (
    AqueductInput,
    BridgeHydraulicInput,
    CulvertHydraulicInput,
    InvertedSiphonInput,
    SideWeirInput,
    SluiceHydraulicInput,
    SpillwayChuteInput,
    StillingBasinInput,
    SteepSlopeInput,
    TunnelHydraulicInput,
    VerticalDropInput,
    calculate_aqueduct,
    calculate_bridge_hydraulic,
    calculate_culvert_hydraulic,
    calculate_inverted_siphon,
    calculate_side_weir,
    calculate_sluice_hydraulic,
    calculate_spillway_chute,
    calculate_stilling_basin,
    calculate_steep_slope,
    calculate_tunnel_hydraulic,
    calculate_vertical_drop,
)
from .waterworks_irrigation import (
    CanalHeadwaterInput,
    CanalLossSegment,
    ContinuousDiversionInput,
    IrrigationModulusInput,
    PressurePipeInput,
    PumpDesignRateFlowInput,
    PumpEfficiencyInput,
    PumpHeadInput,
    PumpInstallationInput,
    PumpPowerInput,
    PumpSpecificSpeedInput,
    PumpTypicalYearFlowInput,
    RotationalDiversionInput,
    TransformerCapacityInput,
    TransformerMotorInput,
    WaterworksIrrigationError,
    calculate_canal_headwater,
    calculate_continuous_diversion,
    calculate_irrigation_modulus,
    calculate_pressure_pipe,
    calculate_pump_design_head,
    calculate_pump_efficiency,
    calculate_pump_flow_by_design_rate,
    calculate_pump_flow_by_typical_year,
    calculate_pump_installation_height,
    calculate_pump_specific_speed,
    calculate_pump_total_power,
    calculate_rotational_diversion,
    calculate_transformer_capacity,
)
from .waterworks_programs import (
    WATERWORKS_PROGRAMS,
    waterworks_program_markdown,
    waterworks_status_summary,
)


def enable_high_dpi_awareness() -> None:
    if os.name != "nt":
        return
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except Exception:
        try:
            ctypes.windll.user32.SetProcessDPIAware()
        except Exception:
            pass


def _load_app_icon_image(max_width: int = 256, max_height: int = 256) -> tk.PhotoImage | None:
    assets_dir = Path(__file__).resolve().parent / "assets"
    for name in ("app_icon.png", "wechat_webapp_hd_108_hydrology_v2.png", "huitongzhida_logo.png"):
        path = assets_dir / name
        if not path.exists():
            continue
        try:
            image = tk.PhotoImage(file=str(path))
        except tk.TclError:
            continue
        factor_w = max(1, (image.width() + max_width - 1) // max_width)
        factor_h = max(1, (image.height() + max_height - 1) // max_height)
        return image.subsample(max(factor_w, factor_h))
    return None


def _apply_window_icon(window: tk.Toplevel | tk.Tk) -> None:
    icon = _load_app_icon_image(256, 256)
    if icon is None:
        return
    try:
        window.iconphoto(True, icon)
    except tk.TclError:
        return
    window._hydrology_window_icon = icon  # type: ignore[attr-defined]


def _friendly_payment_error(message: str) -> str:
    if "微信支付未配置" in message:
        return "微信支付通道正在配置中，暂不能付款开通会员。管理员需完成微信支付商户号、证书序列号、商户私钥、API v3 Key 和平台公钥配置。"
    if "支付宝未配置" in message:
        return "支付宝支付通道正在配置中，暂不能付款开通会员。管理员需完成支付宝应用 APPID、应用私钥和支付宝公钥配置。"
    if "模拟支付未启用" in message:
        return "当前服务器未启用测试支付，正式付款通道配置完成后即可开通会员。"
    return message
from .scour import (
    SCOUR_BETA_BY_ANGLE,
    SCOUR_CLAY_EQUIVALENT_PARTICLE_TABLE,
    SCOUR_E_BY_SEDIMENT,
    SCOUR_METHODS,
    SCOUR_NONCOHESIVE_VH1_TABLE,
    ScourCalculationInput,
    calculate_scour,
)
from .bridge_opening import (
    BRIDGE_OPENING_METHODS,
    RIVER_SEGMENT_PARAMS,
    BridgeOpeningInput,
    calculate_bridge_opening,
)
from .river_evolution import (
    RIVER_EVOLUTION_METHODS,
    RiverEvolutionInput,
    calculate_river_evolution,
)
from .auth_client import (
    AuthClientConfig,
    AuthClientError,
    AuthServerRejectedError,
    OnlineAuthClient,
    auth_state_path,
    clear_auth_state,
    days_left as auth_days_left,
    is_offline_usable,
    is_state_active,
    load_auth_state,
    parse_datetime,
    save_auth_state,
)
from .versioning import load_version_info


def open_external_url(url: str) -> bool:
    try:
        os.startfile(url)  # type: ignore[attr-defined]
        return True
    except (AttributeError, OSError):
        return False


def _combo_label_values(options: Sequence[tuple[str, str]]) -> list[str]:
    return [label for _option_id, label in options]


def _combo_id_from_label(value: str, options: Sequence[tuple[str, str]]) -> str:
    text = (value or "").strip()
    for option_id, label in options:
        if text == label:
            return option_id
    for option_id, label in options:
        if text in {option_id, f"{option_id}. {label}"}:
            return option_id
    return text.split(".", 1)[0].strip()


def _set_combo_current_by_id(combo: ttk.Combobox, options: Sequence[tuple[str, str]], selected_id: str) -> None:
    for idx, (option_id, _label) in enumerate(options):
        if option_id == selected_id:
            combo.current(idx)
            return


DASHBOARD_CARD_WIDTH = 372
DASHBOARD_CARD_HEIGHT = 286
DASHBOARD_CARD_TEXT_WRAP = 236
from .font_policy import FONT_FAMILY, LATIN_FONT, configure_tk_fonts

DASHBOARD_CARD_FONT = FONT_FAMILY
DASHBOARD_MONO_FONT = LATIN_FONT
DASHBOARD_BG = "#f5f7fa"
DASHBOARD_BG_DEEP = "#eef3f7"
DASHBOARD_NEBULA = "#e7f1ff"
DASHBOARD_PANEL = "#ffffff"
DASHBOARD_PANEL_ALT = "#f2f6fa"
DASHBOARD_BORDER = "#d2dde8"
DASHBOARD_CYAN = "#0a5382"
DASHBOARD_BLUE = "#1565c0"
DASHBOARD_PURPLE = "#6a1b9a"
DASHBOARD_SILVER = "#102a43"
DASHBOARD_STAR = "#00838f"
DASHBOARD_TEXT = "#0f1e2e"
DASHBOARD_MUTED = "#44586b"
DASHBOARD_DEEP_MUTED = "#7c8ea0"
DASHBOARD_CATEGORY_COLORS = {
    "irrigation_channel": "#2e7d32",
    "peak": "#1565c0",
    "river_evolution": "#ef6c00",
    "water_level": "#6a1b9a",
    "reservoir": "#00838f",
    "hydropower_pipe": "#4527a0",
    "bridge_opening": "#0a5382",
}


def _initialize_dashboard_fonts() -> None:
    try:
        available = configure_tk_fonts(tk._default_root)
    except tk.TclError:
        available = False
    if not available:
        print("WARNING: KaiTi or Times New Roman is not installed; font rendering is not verified.")


def _blend_hex_color(color: str, background: str = "#ffffff", background_weight: float = 0.88) -> str:
    color = color.lstrip("#")
    background = background.lstrip("#")
    try:
        r1, g1, b1 = int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)
        r2, g2, b2 = int(background[0:2], 16), int(background[2:4], 16), int(background[4:6], 16)
    except (ValueError, IndexError):
        return "#eef6fb"
    foreground_weight = 1.0 - background_weight
    return f"#{int(r1 * foreground_weight + r2 * background_weight):02x}{int(g1 * foreground_weight + g2 * background_weight):02x}{int(b1 * foreground_weight + b2 * background_weight):02x}"


def _canvas_round_rect(
    canvas: tk.Canvas,
    x1: int,
    y1: int,
    x2: int,
    y2: int,
    radius: int,
    *,
    fill: str,
    outline: str = "",
    width: int = 1,
    tags: str | tuple[str, ...] = (),
) -> int:
    radius = max(1, min(radius, (x2 - x1) // 2, (y2 - y1) // 2))
    points = [
        x1 + radius, y1,
        x2 - radius, y1,
        x2, y1,
        x2, y1 + radius,
        x2, y2 - radius,
        x2, y2,
        x2 - radius, y2,
        x1 + radius, y2,
        x1, y2,
        x1, y2 - radius,
        x1, y1 + radius,
        x1, y1,
    ]
    return canvas.create_polygon(
        points,
        smooth=True,
        splinesteps=16,
        fill=fill,
        outline=outline,
        width=width,
        tags=tags,
    )


def _draw_deep_space(canvas: tk.Canvas, width: int, height: int) -> None:
    for idx in range(28):
        ratio = idx / 27
        r = int(245 - ratio * 5)
        g = int(247 - ratio * 3)
        b = int(250 - ratio * 2)
        canvas.create_rectangle(
            0,
            int(height * idx / 28),
            width,
            int(height * (idx + 1) / 28) + 2,
            fill=f"#{r:02x}{g:02x}{b:02x}",
            outline="",
        )
    # Keep the background crisp: thin hydrology contour lines instead of blurry translucent blobs.
    for y in (64, 118, 172, 226):
        canvas.create_line(24, y, width - 32, y - 24, fill="#d7e4ed", width=1, smooth=True)
    for y in (92, 196):
        canvas.create_line(84, y, 218, y - 13, 368, y - 4, 530, y - 26, fill="#e3edf3", width=1, smooth=True)
    for x in range(96, width, 196):
        canvas.create_line(x, 20, x - 64, height - 12, fill="#e5eef4", width=1)
    for x, y, size, color in (
        (48, 42, 2, "#b8c9dd"),
        (118, 118, 1, "#c5d7e4"),
        (214, 62, 1, "#93c5d8"),
        (336, 170, 2, "#c6d8e4"),
        (512, 86, 1, "#bfd6e2"),
        (690, 136, 1, "#93c5d8"),
        (824, 52, 2, "#b8c9dd"),
        (970, 188, 1, "#c5d7e4"),
        (1110, 92, 1, "#c6d8e4"),
    ):
        if x < width and y < height:
            canvas.create_oval(x, y, x + size, y + size, fill=color, outline="")


MEMBERSHIP_PLANS = {
    "月度会员": {"price": 128, "days": 30},
    "年度会员": {"price": 1288, "days": 365},
}
MEMBERSHIP_PLAN_IDS = {
    "月度会员": "month",
    "年度会员": "year",
}


def available_membership_plans(state: dict[str, str] | None = None) -> dict[str, dict[str, int]]:
    return dict(MEMBERSHIP_PLANS)


def monthly_membership_available(state: dict[str, str] | None = None) -> bool:
    state = state or {}
    return not (
        state.get("monthly_membership_available") == "0"
        or state.get("monthly_membership_used") == "1"
        or state.get("monthly_membership_pending") == "1"
    )


def default_membership_plan(state: dict[str, str] | None = None) -> str:
    plans = available_membership_plans(state)
    current = (state or {}).get("plan", "")
    if current in plans and (current != "月度会员" or monthly_membership_available(state)):
        return current
    return "月度会员" if monthly_membership_available(state) else "年度会员"


def membership_state_path() -> Path:
    return auth_state_path()


def load_membership_state() -> dict[str, str]:
    return load_auth_state()


def save_membership_state(state: dict[str, str]) -> None:
    save_auth_state(state)


def clear_membership_state() -> None:
    clear_auth_state()


def parse_membership_expire_at(state: dict[str, str]) -> datetime | None:
    return parse_datetime(state.get("expire_at", ""))


def is_membership_active(state: dict[str, str]) -> bool:
    return is_state_active(state)


def _trial_int(state: dict[str, str], key: str, default: int = 0) -> int:
    try:
        return max(0, int(str(state.get(key, "")).strip()))
    except (TypeError, ValueError):
        return default


def trial_limit(state: dict[str, str]) -> int:
    limit = _trial_int(state, "trial_limit", 3)
    return limit if limit > 0 else 3


def trial_used(state: dict[str, str]) -> int:
    return _trial_int(state, "trial_used", 0)


def trial_remaining(state: dict[str, str]) -> int:
    if not state.get("trial_remaining", ""):
        return max(trial_limit(state) - trial_used(state), 0)
    return _trial_int(state, "trial_remaining", 0)


def is_trial_available(state: dict[str, str]) -> bool:
    return bool(state.get("access_token")) and not is_membership_active(state) and trial_remaining(state) > 0


def trial_usage_text(state: dict[str, str]) -> str:
    limit = trial_limit(state)
    remaining = trial_remaining(state)
    used = max(limit - remaining, trial_used(state))
    return f"免费试用：剩余 {remaining}/{limit} 次，已用 {used} 次"


def membership_days_left(state: dict[str, str]) -> int:
    return auth_days_left(state)


def _is_wechat_temp_account(account: str) -> bool:
    return bool(re.fullmatch(r"wx_[0-9a-f]{16}", (account or "").strip()))


def _membership_account_display(state: dict[str, str]) -> str:
    account = (state.get("phone") or "").strip()
    if _is_wechat_temp_account(account):
        return "微信账号（未绑定手机号）"
    return account or "未登录"


class MembershipDialog(tk.Toplevel):
    def __init__(self, master: tk.Tk) -> None:
        super().__init__(master)
        self.title("会员登录 / 注册开通")
        self.resizable(False, False)
        self.result: dict[str, str] | None = None
        self.saved_state = load_membership_state()
        self.protocol("WM_DELETE_WINDOW", self._cancel)
        self.grab_set()

        self.phone_var = tk.StringVar(value=self.saved_state.get("phone", ""))
        self.plan_var = tk.StringVar(value=default_membership_plan(self.saved_state))
        self.status_var = tk.StringVar()

        body = tk.Frame(self, bg="#f3f7fb", padx=24, pady=22)
        body.grid(row=0, column=0, sticky="nsew")

        tk.Label(
            body,
            text="水文计算软件会员中心",
            bg="#f3f7fb",
            fg="#12324a",
            font=(DASHBOARD_CARD_FONT, 18, "bold"),
        ).grid(row=0, column=0, columnspan=3, sticky="w")
        tk.Label(
            body,
            text="使用计算模块前，请先用手机号注册并开通会员。本机版先保存本地会员状态，后续可接入联网支付和服务器校验。",
            bg="#f3f7fb",
            fg="#4a5b68",
            font=(DASHBOARD_CARD_FONT, 10),
            wraplength=620,
            justify=tk.LEFT,
        ).grid(row=1, column=0, columnspan=3, sticky="w", pady=(6, 18))

        current = self._current_status_text()
        if current:
            tk.Label(
                body,
                text=current,
                bg="#e8f5ee",
                fg="#14613c",
                anchor="w",
                padx=12,
                pady=8,
                font=(DASHBOARD_CARD_FONT, 10, "bold"),
            ).grid(row=2, column=0, columnspan=3, sticky="ew", pady=(0, 16))

        tk.Label(body, text="手机号", bg="#f3f7fb", font=(DASHBOARD_CARD_FONT, 11, "bold")).grid(row=3, column=0, sticky="w", pady=6)
        phone_entry = ttk.Entry(body, textvariable=self.phone_var, width=30, font=(DASHBOARD_CARD_FONT, 11))
        phone_entry.grid(row=3, column=1, columnspan=2, sticky="ew", pady=6)

        tk.Label(body, text="会员类型", bg="#f3f7fb", font=(DASHBOARD_CARD_FONT, 11, "bold")).grid(row=4, column=0, sticky="nw", pady=(12, 6))
        plan_frame = tk.Frame(body, bg="#f3f7fb")
        plan_frame.grid(row=4, column=1, columnspan=2, sticky="ew", pady=(8, 6))
        for idx, (name, info) in enumerate(available_membership_plans(self.saved_state).items()):
            card = tk.Radiobutton(
                plan_frame,
                text=f"{name}  ￥{info['price']} / {info['days']}天",
                variable=self.plan_var,
                value=name,
                indicatoron=False,
                width=22,
                padx=10,
                pady=10,
                bg="#ffffff",
                fg="#243746",
                selectcolor="#dcecff",
                activebackground="#edf5ff",
                font=(DASHBOARD_CARD_FONT, 10, "bold"),
            )
            card.grid(row=0, column=idx, padx=(0, 10), sticky="ew")
            plan_frame.columnconfigure(idx, weight=1)

        tk.Label(
            body,
            textvariable=self.status_var,
            bg="#f3f7fb",
            fg="#b0411f",
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=5, column=0, columnspan=3, sticky="w", pady=(10, 0))

        action_frame = tk.Frame(body, bg="#f3f7fb")
        action_frame.grid(row=6, column=0, columnspan=3, sticky="e", pady=(22, 0))
        if is_membership_active(self.saved_state):
            ttk.Button(action_frame, text="进入软件", command=self._enter_existing).grid(row=0, column=0, padx=6)
        ttk.Button(action_frame, text="注册并开通会员", command=self._register_membership).grid(row=0, column=1, padx=6)
        ttk.Button(action_frame, text="退出", command=self._cancel).grid(row=0, column=2, padx=6)

        body.columnconfigure(1, weight=1)
        phone_entry.focus_set()
        self.update_idletasks()
        x = master.winfo_screenwidth() // 2 - self.winfo_width() // 2
        y = master.winfo_screenheight() // 2 - self.winfo_height() // 2
        self.geometry(f"+{max(x, 0)}+{max(y, 0)}")
        self.deiconify()
        self.lift()
        self.focus_force()
        self.attributes("-topmost", True)
        self.after(800, lambda: self.attributes("-topmost", False))

    def _current_status_text(self) -> str:
        if not is_membership_active(self.saved_state):
            return ""
        expire_at = parse_membership_expire_at(self.saved_state)
        assert expire_at is not None
        return (
            f"已检测到有效会员：{self.saved_state.get('phone', '')}，"
            f"{self.saved_state.get('plan', '')}，到期时间 {expire_at.strftime('%Y-%m-%d %H:%M')}，"
            f"剩余 {membership_days_left(self.saved_state)} 天。"
        )

    def _enter_existing(self) -> None:
        self.result = self.saved_state
        self.destroy()

    def _register_membership(self) -> None:
        phone = self.phone_var.get().strip()
        if not re.fullmatch(r"1[3-9]\d{9}", phone):
            self.status_var.set("请输入有效的11位手机号。")
            return
        plan = self.plan_var.get()
        info = MEMBERSHIP_PLANS.get(plan)
        if info is None:
            self.status_var.set("请选择会员类型。")
            return
        now = datetime.now()
        expire_at = now + timedelta(days=int(info["days"]))
        state = {
            "phone": phone,
            "plan": plan,
            "price": str(info["price"]),
            "start_at": now.isoformat(timespec="seconds"),
            "expire_at": expire_at.isoformat(timespec="seconds"),
            "license_mode": "local",
        }
        save_membership_state(state)
        self.result = state
        self.destroy()

    def _cancel(self) -> None:
        self.result = None
        self.destroy()


class PersonalCenterDialog(tk.Toplevel):
    def __init__(self, master: tk.Tk, state: dict[str, str], server_url: str) -> None:
        super().__init__(master)
        self.title("个人中心")
        _apply_window_icon(self)
        self.resizable(False, False)
        self.result: dict[str, str] | None = None
        self.state = dict(state)
        self.server_url = server_url.strip().rstrip("/")
        self.phone_var = tk.StringVar()
        self.code_var = tk.StringVar()
        self.refund_order_var = tk.StringVar()
        self.refund_reason_var = tk.StringVar()
        self.refund_status_var = tk.StringVar(value="可在这里查看自己的支付订单，并提交退款申请。")
        self.refund_order_options: dict[str, str] = {}
        self.status_var = tk.StringVar(value="手机号仅用于账号归属、会员服务和后续通知。")
        self.protocol("WM_DELETE_WINDOW", self._close)
        self.grab_set()

        body = tk.Frame(self, bg="#f3f7fb", padx=24, pady=22)
        body.grid(row=0, column=0, sticky="nsew")
        body.columnconfigure(1, weight=1)

        tk.Label(
            body,
            text="个人中心",
            bg="#f3f7fb",
            fg="#102a43",
            font=(DASHBOARD_CARD_FONT, 20, "bold"),
        ).grid(row=0, column=0, columnspan=3, sticky="w")
        tk.Label(
            body,
            text="查看当前微信登录账号、会员状态，并在登录后绑定本人手机号。",
            bg="#f3f7fb",
            fg="#536676",
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=1, column=0, columnspan=3, sticky="w", pady=(6, 18))

        info = tk.Frame(body, bg="#ffffff", highlightthickness=1, highlightbackground="#d9e4ee", padx=16, pady=14)
        info.grid(row=2, column=0, columnspan=3, sticky="ew")
        info.columnconfigure(1, weight=1)

        account = (self.state.get("phone") or "").strip()
        expire_at = parse_membership_expire_at(self.state)
        expire_text = expire_at.strftime("%Y-%m-%d %H:%M") if expire_at else "未开通"
        phone_text = "未绑定" if _is_wechat_temp_account(account) else (account or "未登录")
        status_text = "已开通" if is_membership_active(self.state) else "未开通"
        rows = [
            ("登录账号", _membership_account_display(self.state)),
            ("绑定手机", phone_text),
            ("会员套餐", self.state.get("plan", "") or "未开通"),
            ("会员状态", f"{status_text}，到期：{expire_text}"),
        ]
        for row, (label, value) in enumerate(rows):
            tk.Label(info, text=label, bg="#ffffff", fg="#587083", width=10, anchor="w", font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=row, column=0, sticky="w", pady=4)
            tk.Label(info, text=value, bg="#ffffff", fg="#142f43", anchor="w", font=(DASHBOARD_CARD_FONT, 10)).grid(row=row, column=1, sticky="ew", pady=4, padx=(14, 0))

        bind_panel = tk.Frame(body, bg="#ffffff", highlightthickness=1, highlightbackground="#d9e4ee", padx=16, pady=14)
        bind_panel.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(14, 0))
        bind_panel.columnconfigure(1, weight=1)
        tk.Label(
            bind_panel,
            text="绑定手机号",
            bg="#ffffff",
            fg="#102a43",
            font=(DASHBOARD_CARD_FONT, 12, "bold"),
        ).grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 10))

        if _is_wechat_temp_account(account):
            tk.Label(bind_panel, text="手机号", bg="#ffffff", fg="#31495b", font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=1, column=0, sticky="w", pady=6)
            ttk.Entry(bind_panel, textvariable=self.phone_var, width=28, font=(DASHBOARD_CARD_FONT, 10)).grid(row=1, column=1, sticky="ew", pady=6, padx=(12, 8))
            ttk.Button(bind_panel, text="获取验证码", command=self._request_code).grid(row=1, column=2, sticky="e", pady=6)

            tk.Label(bind_panel, text="验证码", bg="#ffffff", fg="#31495b", font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=2, column=0, sticky="w", pady=6)
            ttk.Entry(bind_panel, textvariable=self.code_var, width=18, font=(DASHBOARD_CARD_FONT, 10)).grid(row=2, column=1, sticky="w", pady=6, padx=(12, 8))
            ttk.Button(bind_panel, text="确认绑定手机号", command=self._bind_phone).grid(row=2, column=2, sticky="e", pady=6)
        else:
            tk.Label(
                bind_panel,
                text="当前账号已经绑定手机号。更换手机号会影响会员订单和账号归属，如确需更换请在后台处理。",
                bg="#ffffff",
                fg="#536676",
                font=(DASHBOARD_CARD_FONT, 10),
                wraplength=560,
                justify=tk.LEFT,
            ).grid(row=1, column=0, columnspan=3, sticky="w")

        refund_panel = tk.Frame(body, bg="#ffffff", highlightthickness=1, highlightbackground="#d9e4ee", padx=16, pady=14)
        refund_panel.grid(row=4, column=0, columnspan=3, sticky="ew", pady=(14, 0))
        refund_panel.columnconfigure(1, weight=1)
        tk.Label(
            refund_panel,
            text="退款申请",
            bg="#ffffff",
            fg="#102a43",
            font=(DASHBOARD_CARD_FONT, 12, "bold"),
        ).grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 10))
        tk.Label(refund_panel, text="支付订单", bg="#ffffff", fg="#31495b", font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=1, column=0, sticky="w", pady=6)
        self.refund_order_combo = ttk.Combobox(refund_panel, textvariable=self.refund_order_var, width=42, state="readonly")
        self.refund_order_combo.grid(row=1, column=1, sticky="ew", pady=6, padx=(12, 8))
        ttk.Button(refund_panel, text="刷新订单", command=self._load_refund_orders).grid(row=1, column=2, sticky="e", pady=6)
        tk.Label(refund_panel, text="申请原因", bg="#ffffff", fg="#31495b", font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=2, column=0, sticky="w", pady=6)
        ttk.Entry(refund_panel, textvariable=self.refund_reason_var, width=42).grid(row=2, column=1, sticky="ew", pady=6, padx=(12, 8))
        ttk.Button(refund_panel, text="提交退款申请", command=self._request_refund).grid(row=2, column=2, sticky="e", pady=6)
        tk.Label(
            refund_panel,
            textvariable=self.refund_status_var,
            bg="#ffffff",
            fg="#536676",
            font=(DASHBOARD_CARD_FONT, 9),
            wraplength=560,
            justify=tk.LEFT,
        ).grid(row=3, column=0, columnspan=3, sticky="w", pady=(8, 0))

        tk.Label(
            body,
            textvariable=self.status_var,
            bg="#f3f7fb",
            fg="#9a3412",
            font=(DASHBOARD_CARD_FONT, 10),
            wraplength=620,
            justify=tk.LEFT,
        ).grid(row=5, column=0, columnspan=3, sticky="w", pady=(12, 0))

        action = tk.Frame(body, bg="#f3f7fb")
        action.grid(row=6, column=0, columnspan=3, sticky="e", pady=(20, 0))
        ttk.Button(action, text="关闭", command=self._close).pack(side=tk.RIGHT)
        self.after(200, self._load_refund_orders)

        self.update_idletasks()
        x = master.winfo_screenwidth() // 2 - self.winfo_width() // 2
        y = master.winfo_screenheight() // 2 - self.winfo_height() // 2
        self.geometry(f"+{max(x, 0)}+{max(y, 0)}")
        self.deiconify()
        self.lift()
        self.focus_force()
        self.attributes("-topmost", True)
        self.after(800, lambda: self.attributes("-topmost", False))

    def _normalized_phone(self) -> str:
        phone = re.sub(r"\D", "", self.phone_var.get().strip())
        if not re.fullmatch(r"1[3-9]\d{9}", phone):
            self.status_var.set("请输入有效的11位手机号。")
            return ""
        return phone

    def _auth_client(self) -> OnlineAuthClient | None:
        if not self.server_url.startswith(("http://", "https://")):
            self.status_var.set("登录服务地址异常，请重新登录后再绑定手机号。")
            return None
        return OnlineAuthClient(AuthClientConfig(server_url=self.server_url))

    def _load_refund_orders(self) -> None:
        client = self._auth_client()
        if client is None:
            return
        try:
            data = client.get_my_payment_orders(self.state)
        except AuthClientError as exc:
            self.refund_status_var.set(f"订单读取失败：{exc}")
            return
        orders = data.get("orders") if isinstance(data.get("orders"), list) else []
        values: list[str] = []
        self.refund_order_options.clear()
        for order in orders:
            if not isinstance(order, dict):
                continue
            order_no = str(order.get("order_no") or "")
            if not order_no:
                continue
            status = str(order.get("status") or "")
            refund_status = str(order.get("refund_status") or "")
            amount = str(order.get("amount_yuan") or "")
            plan = str(order.get("plan_name") or "")
            label = f"{order_no} | {plan} | {amount}元 | {status}"
            if refund_status:
                label += f" | 退款:{refund_status}"
            values.append(label)
            self.refund_order_options[label] = order_no
        self.refund_order_combo.configure(values=values)
        if values:
            if self.refund_order_var.get() not in self.refund_order_options:
                self.refund_order_var.set(values[0])
            self.refund_status_var.set("已读取支付订单。使用过计算功能的账号，提交退款时会被系统自动拦截。")
        else:
            self.refund_order_var.set("")
            self.refund_status_var.set("当前账号暂无支付订单。")

    def _request_refund(self) -> None:
        selected = self.refund_order_var.get().strip()
        order_no = self.refund_order_options.get(selected, selected.split("|", 1)[0].strip())
        if not order_no:
            self.refund_status_var.set("请先刷新并选择需要退款的支付订单。")
            return
        client = self._auth_client()
        if client is None:
            return
        self.refund_status_var.set("正在提交退款申请...")
        try:
            result = client.request_refund(self.state, order_no, self.refund_reason_var.get().strip())
        except AuthClientError as exc:
            self.refund_status_var.set(f"退款申请未提交：{exc}")
            return
        status_text = str(result.get("status_text") or result.get("status") or "已提交")
        self.refund_status_var.set(f"退款申请已提交：{status_text}。请等待后台审核。")
        submitted_message = self.refund_status_var.get()
        self._load_refund_orders()
        self.refund_status_var.set(submitted_message)

    def _request_code(self) -> None:
        phone = self._normalized_phone()
        if not phone:
            return
        client = self._auth_client()
        if client is None:
            return
        self.status_var.set("正在发送绑定验证码...")
        self.update_idletasks()
        try:
            data = client.request_bind_phone_code(self.state, phone)
        except AuthClientError as exc:
            self.status_var.set(str(exc))
            return
        if data.get("status") == "already_bound":
            self.status_var.set(str(data.get("message") or "当前账号已经绑定该手机号。"))
            return
        debug_code = str(data.get("debug_code") or "")
        if debug_code:
            self.code_var.set(debug_code)
            self.status_var.set("测试验证码已自动填入。正式短信配置后不会显示验证码。")
            return
        self.status_var.set(str(data.get("message") or "验证码已发送，请查看短信。"))

    def _bind_phone(self) -> None:
        phone = self._normalized_phone()
        if not phone:
            return
        code = self.code_var.get().strip()
        if not code:
            self.status_var.set("请输入短信验证码。")
            return
        client = self._auth_client()
        if client is None:
            return
        self.status_var.set("正在绑定手机号...")
        self.update_idletasks()
        try:
            new_state = client.bind_account_phone(self.state, phone, code)
        except AuthClientError as exc:
            self.status_var.set(str(exc))
            return
        save_membership_state(new_state)
        self.result = new_state
        messagebox.showinfo("绑定成功", "手机号已绑定到当前微信账号。", parent=self)
        self.destroy()

    def _close(self) -> None:
        self.destroy()


class OnlineLoginDialog(tk.Toplevel):
    def __init__(self, master: tk.Tk, version_info) -> None:
        super().__init__(master)
        self.title("微信扫码登录")
        _apply_window_icon(self)
        self.resizable(False, False)
        self.result: dict[str, str] | None = None
        self.version_info = version_info
        self.saved_state = load_membership_state()
        self.protocol("WM_DELETE_WINDOW", self._cancel)
        self.grab_set()

        self.phone_var = tk.StringVar(value="")
        self.code_var = tk.StringVar()
        default_server_url = (version_info.auth_server_url or "https://htzdwater.com").strip().rstrip("/")
        self.server_url_var = tk.StringVar(value=default_server_url)
        self.plan_var = tk.StringVar(value=default_membership_plan(self.saved_state))
        self.pay_provider_var = tk.StringVar(value="wechat")
        self.status_var = tk.StringVar()
        self.current_order_no = ""
        self.current_order_server_url = ""
        self.pay_poll_remaining = 0
        self.wechat_state = ""
        self.wechat_server_url = ""
        self.wechat_poll_remaining = 0
        self.wechat_bind_button: ttk.Button | None = None
        self.login_button: tk.Button | None = None
        self.status_var.set("请点击微信扫码登录，用微信确认后自动进入软件。")
        self._build_login_ui(version_info)
        self.update_idletasks()
        x = master.winfo_screenwidth() // 2 - self.winfo_width() // 2
        y = master.winfo_screenheight() // 2 - self.winfo_height() // 2
        self.geometry(f"+{max(x, 0)}+{max(y, 0)}")
        self.deiconify()
        self.lift()
        self.focus_force()
        self.attributes("-topmost", True)
        self.after(800, lambda: self.attributes("-topmost", False))

    def _build_login_ui(self, version_info) -> None:
        self.configure(bg="#edf4f7")
        body = tk.Frame(self, bg="#edf4f7", padx=18, pady=18)
        body.grid(row=0, column=0, sticky="nsew")

        card = tk.Frame(body, bg="white", highlightthickness=1, highlightbackground="#d7e5eb")
        card.grid(row=0, column=0, sticky="nsew")

        visual = tk.Frame(card, bg="#e9f8fb", width=232, height=336)
        visual.grid(row=0, column=0, sticky="nsw")
        visual.grid_propagate(False)
        canvas = tk.Canvas(visual, width=232, height=336, bg="#e9f8fb", highlightthickness=0)
        canvas.pack(fill=tk.BOTH, expand=True)
        canvas.create_rectangle(0, 0, 232, 336, fill="#e9f8fb", outline="")
        canvas.create_oval(-48, -36, 162, 152, fill="#d4f2f7", outline="")
        canvas.create_oval(92, 210, 276, 396, fill="#d6edf8", outline="")
        self.login_logo_image = _load_app_icon_image(122, 122)
        if self.login_logo_image is not None:
            canvas.create_image(116, 112, image=self.login_logo_image)
        else:
            canvas.create_oval(70, 62, 162, 154, fill="white", outline="#94cde0", width=2)
            canvas.create_line(84, 123, 104, 106, 124, 116, 148, 92, fill="#167db8", width=4, smooth=True)
        canvas.create_text(116, 217, text="慧通智达", fill="#12324a", font=(DASHBOARD_CARD_FONT, 18, "bold"))
        canvas.create_text(116, 247, text="水文计算软件", fill="#33586b", font=(DASHBOARD_CARD_FONT, 11, "bold"))
        canvas.create_line(34, 285, 72, 275, 108, 289, 146, 267, 198, 278, fill="#17a2c6", width=4, smooth=True)

        content = tk.Frame(card, bg="white", padx=32, pady=28)
        content.grid(row=0, column=1, sticky="nsew")
        content.columnconfigure(0, weight=1)

        tk.Label(
            content,
            text="微信扫码登录",
            bg="white",
            fg="#102a43",
            font=(DASHBOARD_CARD_FONT, 24, "bold"),
        ).grid(row=0, column=0, sticky="w")
        tk.Label(
            content,
            text="扫码确认后直接进入软件首页。未开通会员时，首页会显示套餐和扫码支付入口。",
            bg="white",
            fg="#526b7a",
            font=(DASHBOARD_CARD_FONT, 11),
            wraplength=390,
            justify=tk.LEFT,
        ).grid(row=1, column=0, sticky="w", pady=(8, 22))

        steps = tk.Frame(content, bg="white")
        steps.grid(row=2, column=0, sticky="ew", pady=(0, 20))
        for col, text in enumerate(("扫码授权", "自动建档", "首页开通会员")):
            item = tk.Frame(steps, bg="#f5fbfd", highlightthickness=1, highlightbackground="#d7eaf1", padx=10, pady=8)
            item.grid(row=0, column=col, sticky="ew", padx=(0, 8 if col < 2 else 0))
            steps.columnconfigure(col, weight=1)
            tk.Label(item, text=str(col + 1), bg="#0f7c90", fg="white", width=2, font=(DASHBOARD_CARD_FONT, 9, "bold")).pack(side=tk.LEFT)
            tk.Label(item, text=text, bg="#f5fbfd", fg="#204255", font=(DASHBOARD_CARD_FONT, 9, "bold")).pack(side=tk.LEFT, padx=(7, 0))

        current = self._current_status_text()
        if current:
            tk.Label(
                content,
                text=current,
                bg="#edf8f2",
                fg="#166534",
                anchor="w",
                padx=12,
                pady=8,
                font=(DASHBOARD_CARD_FONT, 10, "bold"),
                wraplength=390,
                justify=tk.LEFT,
            ).grid(row=3, column=0, sticky="ew", pady=(0, 14))

        status_box = tk.Frame(content, bg="#f8fafc", highlightthickness=1, highlightbackground="#e3ebf0", padx=12, pady=10)
        status_box.grid(row=4, column=0, sticky="ew", pady=(0, 22))
        tk.Label(
            status_box,
            textvariable=self.status_var,
            bg="#f8fafc",
            fg="#375467",
            font=(DASHBOARD_CARD_FONT, 10),
            wraplength=365,
            justify=tk.LEFT,
        ).pack(anchor="w", fill=tk.X)

        action_frame = tk.Frame(content, bg="white")
        action_frame.grid(row=5, column=0, sticky="ew")
        self.login_button = tk.Button(
            action_frame,
            text="微信扫码登录",
            command=self._start_wechat_login,
            bg="#0f7c60",
            fg="white",
            activebackground="#0b684f",
            activeforeground="white",
            relief=tk.FLAT,
            cursor="hand2",
            padx=24,
            pady=10,
            font=(DASHBOARD_CARD_FONT, 12, "bold"),
        )
        self.login_button.pack(side=tk.LEFT)
        tk.Button(
            action_frame,
            text="退出",
            command=self._cancel,
            bg="#eef4f7",
            fg="#294150",
            activebackground="#dde9ef",
            relief=tk.FLAT,
            cursor="hand2",
            padx=20,
            pady=10,
            font=(DASHBOARD_CARD_FONT, 11),
        ).pack(side=tk.LEFT, padx=(12, 0))

        if (
            not version_info.auth_required
            and self.saved_state.get("license_mode") == "online"
            and is_membership_active(self.saved_state)
        ):
            tk.Button(
                action_frame,
                text="使用已验证授权",
                command=self._enter_existing,
                bg="#e8f5ee",
                fg="#166534",
                activebackground="#d9f0e3",
                relief=tk.FLAT,
                cursor="hand2",
                padx=18,
                pady=10,
                font=(DASHBOARD_CARD_FONT, 11),
            ).pack(side=tk.LEFT, padx=(12, 0))

        body.columnconfigure(0, weight=1)
        body.rowconfigure(0, weight=1)

    def _current_status_text(self) -> str:
        if self.saved_state.get("license_mode") != "online" or not is_membership_active(self.saved_state):
            return ""
        expire_at = parse_membership_expire_at(self.saved_state)
        assert expire_at is not None
        return (
            f"已缓存在线授权：{self.saved_state.get('phone', '')}，"
            f"{self.saved_state.get('plan', '')}，到期时间 {expire_at.strftime('%Y-%m-%d %H:%M')}。"
        )

    def _enter_existing(self) -> None:
        self.result = self.saved_state
        self.destroy()

    def _normalized_server_url(self) -> str:
        server_url = self.server_url_var.get().strip().rstrip("/")
        if not server_url.startswith(("http://", "https://")):
            self.status_var.set("登录服务配置异常，请联系管理员处理。")
            return ""
        return server_url

    def _normalized_phone(self) -> str:
        phone = re.sub(r"\D", "", self.phone_var.get().strip())
        if not re.fullmatch(r"1[3-9]\d{9}", phone):
            self.status_var.set("请输入有效的11位手机号。")
            return ""
        return phone

    def _create_payment_order(self) -> None:
        self.status_var.set("请先微信扫码登录进入软件首页，再在首页扫码支付开通会员。")
        return
        phone = self._normalized_phone()
        server_url = self._normalized_server_url()
        if not phone or not server_url:
            return
        plan = self.plan_var.get().strip()
        provider = self.pay_provider_var.get().strip()
        if plan not in MEMBERSHIP_PLANS:
            self.status_var.set("请选择会员套餐。")
            return
        self.status_var.set("正在创建支付订单...")
        self.update_idletasks()
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            order = client.create_payment_order(phone, MEMBERSHIP_PLAN_IDS.get(plan, plan), provider)
        except AuthClientError as exc:
            self.status_var.set(_friendly_payment_error(str(exc)))
            return
        order_no = str(order.get("order_no") or "")
        pay_url = str(order.get("pay_page_url") or order.get("code_url") or "")
        if not order_no or not pay_url:
            self.status_var.set("服务器未返回有效支付订单。")
            return
        if pay_url.startswith("/"):
            pay_url = server_url + pay_url
        self.current_order_no = order_no
        self.current_order_server_url = server_url
        self.pay_poll_remaining = 200
        amount = str(order.get("amount_yuan") or "")
        if open_external_url(pay_url):
            self.status_var.set(f"已打开支付二维码，订单金额 {amount} 元。付款成功后会自动开通，请不要关闭本窗口。")
        else:
            self.status_var.set(f"未能自动打开支付二维码，请复制链接到浏览器：{pay_url}")
        self.after(3000, self._poll_payment_order)

    def _poll_payment_order(self) -> None:
        if not self.current_order_no or not self.current_order_server_url:
            return
        if self.pay_poll_remaining <= 0:
            self.status_var.set("支付状态查询已停止。如已付款但未开通，请重新登录或联系管理员。")
            return
        self.pay_poll_remaining -= 1
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=self.current_order_server_url))
            order = client.get_payment_order(self.current_order_no)
        except AuthClientError:
            self.after(3000, self._poll_payment_order)
            return
        status = str(order.get("status") or "")
        if status == "paid":
            self.current_order_no = ""
            expire_at = str(order.get("license_expire_at") or "")
            self.status_var.set(f"支付成功，会员已开通到 {expire_at}。请点击“微信扫码登录”。")
            return
        if status in {"expired", "closed", "failed"}:
            self.current_order_no = ""
            self.status_var.set(f"订单状态为 {status}，请重新创建订单。")
            return
        self.after(3000, self._poll_payment_order)

    def _start_wechat_login(self) -> None:
        server_url = self._normalized_server_url()
        if not server_url:
            return
        self.status_var.set("正在创建微信扫码登录二维码...")
        if self.login_button is not None:
            self.login_button.configure(state=tk.DISABLED, text="等待扫码确认")
        self.update_idletasks()
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            data = client.start_wechat_login(self.version_info.version)
        except AuthClientError as exc:
            self.status_var.set(str(exc))
            if self.login_button is not None:
                self.login_button.configure(state=tk.NORMAL, text="微信扫码登录")
            return
        state = str(data.get("state") or "")
        auth_url = str(data.get("auth_url") or "")
        if not state or not auth_url:
            self.status_var.set("服务器未返回有效的微信登录二维码。")
            if self.login_button is not None:
                self.login_button.configure(state=tk.NORMAL, text="微信扫码登录")
            return
        self.wechat_state = state
        self.wechat_server_url = server_url
        self.wechat_poll_remaining = max(20, int(data.get("expires_in") or 600) // 3)
        if self.wechat_bind_button is not None:
            self.wechat_bind_button.configure(state=tk.DISABLED)
        if open_external_url(auth_url):
            self.status_var.set("已打开微信扫码页面。扫码确认后系统会自动建档并进入软件首页。")
        else:
            self.status_var.set(f"未能自动打开微信扫码页面，请复制链接到浏览器：{auth_url}")
        self.after(3000, self._poll_wechat_login)

    def _poll_wechat_login(self) -> None:
        if not self.wechat_state or not self.wechat_server_url:
            return
        if self.wechat_poll_remaining <= 0:
            self.status_var.set("微信扫码登录已超时，请重新点击“微信扫码登录”。")
            if self.login_button is not None:
                self.login_button.configure(state=tk.NORMAL, text="微信扫码登录")
            return
        self.wechat_poll_remaining -= 1
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=self.wechat_server_url))
            data = client.get_wechat_login_status(self.wechat_state)
        except AuthClientError:
            self.after(3000, self._poll_wechat_login)
            return
        status = str(data.get("status") or "")
        message = str(data.get("message") or "")
        if status == "authenticated":
            try:
                state = client.auth_state_from_response(data)
            except AuthClientError as exc:
                self.status_var.set(str(exc))
                return
            save_membership_state(state)
            self.result = state
            self.destroy()
            return
        if status == "need_phone":
            self.status_var.set("当前版本不需要验证码。请重新点击“微信扫码登录”；如果仍出现该提示，请联系管理员检查服务器版本。")
            if self.login_button is not None:
                self.login_button.configure(state=tk.NORMAL, text="微信扫码登录")
            return
        if status == "need_membership":
            phone = str(data.get("phone") or "")
            if phone:
                self.phone_var.set(phone)
            account = phone or "当前微信账号"
            self.status_var.set(message or f"{account} 尚未开通会员，登录后请在软件首页扫码支付开通。")
            if self.login_button is not None:
                self.login_button.configure(state=tk.NORMAL, text="微信扫码登录")
            return
        if status in {"expired", "failed"}:
            self.status_var.set(message or "微信扫码登录失败，请重新扫码。")
            if self.login_button is not None:
                self.login_button.configure(state=tk.NORMAL, text="微信扫码登录")
            return
        self.after(3000, self._poll_wechat_login)

    def _bind_wechat_phone(self) -> None:
        self.status_var.set("当前版本不需要绑定手机号或输入验证码，请直接使用微信扫码登录。")
        return
        if not self.wechat_state:
            self.status_var.set("请先点击“微信扫码登录”并完成微信授权。")
            return
        phone = self._normalized_phone()
        code = self.code_var.get().strip()
        server_url = self._normalized_server_url()
        if not phone:
            return
        if not code:
            self.status_var.set("请先获取并输入验证码。")
            return
        if not server_url:
            return
        self.status_var.set("正在绑定微信和手机号...")
        self.update_idletasks()
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            data = client.bind_wechat_phone(self.wechat_state, phone, code, self.version_info.version)
        except AuthClientError as exc:
            self.status_var.set(str(exc))
            return
        status = str(data.get("status") or "")
        if status == "authenticated":
            try:
                state = client.auth_state_from_response(data)
            except AuthClientError as exc:
                self.status_var.set(str(exc))
                return
            save_membership_state(state)
            self.result = state
            self.destroy()
            return
        if status == "need_membership":
            self.status_var.set(str(data.get("message") or "微信已绑定手机号，但尚未开通会员。请先开通/续费会员。"))
            return
        self.status_var.set(str(data.get("message") or "微信手机号绑定未完成，请重试。"))

    def _request_code(self) -> None:
        self.status_var.set("当前版本不需要验证码，请直接使用微信扫码登录。")
        return
        if not self.wechat_state:
            self.status_var.set("验证码仅用于首次微信扫码后绑定手机号。请先点击“微信扫码登录”。")
            return
        phone = self._normalized_phone()
        server_url = self._normalized_server_url()
        if not phone:
            return
        if not server_url:
            return
        self.status_var.set("正在获取验证码...")
        self.update_idletasks()
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            data = client.request_login_code(phone)
        except AuthClientError as exc:
            self.status_var.set(str(exc))
            return
        debug_code = str(data.get("debug_code") or "")
        if debug_code:
            self.code_var.set(debug_code)
            self.status_var.set(f"测试验证码：{debug_code}。正式短信接入后这里不会显示验证码。")
            return
        self.status_var.set(str(data.get("message") or "验证码已发送，请查收短信。"))

    def _login_online(self) -> None:
        self.status_var.set("手机号验证码登录已关闭，请使用微信扫码登录。")
        return
        phone = self._normalized_phone()
        code = self.code_var.get().strip()
        server_url = self._normalized_server_url()
        if not phone:
            return
        if not code:
            self.status_var.set("请先获取并输入验证码。")
            return
        if not server_url:
            return
        self.status_var.set("正在连接登录服务...")
        self.update_idletasks()
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            state = client.login_with_code(phone, code, self.version_info.version)
        except AuthClientError as exc:
            self.status_var.set(str(exc))
            return
        save_membership_state(state)
        self.result = state
        self.destroy()

    def _cancel(self) -> None:
        self.result = None
        self.destroy()


METHOD_STEP_GUIDES = {
    "相关程序清单": """1. 先在左侧目录按当前大模块查看归并进来的《水利大算盘》程序。\n2. 选中任一程序，右侧会显示书内页码、PDF页码提示、接入状态和对应软件计算页。\n3. 已接入表示当前软件已有可计算工具；部分接入表示已有相近能力但还需按书中口径补齐。\n4. 待接入表示该程序会继续从扫描PDF提取公式、参数和例题后开发。\n5. 新公式接入时必须同步补参数解释、单位检查、结果表格和导出格式。\n6. 接入完成后运行对应测试和新人例题验收，再发布新版本。""",
    "灌溉引水与渠首水位": """1. 灌水模数：填写作物灌溉面积、灌水定额和灌水延续时间，计算q和该作物净流量。\n2. 续灌引水流量：填写总控制面积、灌水模数及干渠、支渠、斗渠、田间水利用系数。\n3. 轮灌引水流量：填写轮灌组数、每组平均面积、灌水模数和轮灌水利用系数。\n4. 渠首水位：填写最高田面高程、田面灌水深、渠道水深、建筑物水头损失；渠道水头损失可按“比降,长度”逐行输入。\n5. 开始计算后检查单位：面积按亩或万亩，时间按天，流量按m³/s，渠首水位按m。\n6. 结果用于渠首布置和渠道纵断面初步复核，正式采用前需结合工程等级、渠道控制点和水闸损失复核。""",
    "提水泵站与压力管道": """1. 提水设计流量可按典型年最大毛灌溉用水量法或设计灌水率法计算，作物比例按百分数输入。\n2. 压力管道填写设计流量和允许流速，程序计算管径、最小壁厚和通气孔面积。\n3. 水泵扬程填写出水池水位、吸水池水位和总水头损失，程序计算净扬程和设计扬程。\n4. 安装高程填写允许吸上真空高度、当地大气压、汽化压头、入口流速和吸水管损失。\n5. 功率效率、比速和主变容量按书中第8、9节公式计算；效率字段按界面标注的百分数或小数填写。\n6. 计算后重点检查流量单位、效率口径、压力单位和扬程量级，泵站选型仍需结合厂家曲线复核。""",
    "渠道水力计算": """1. 明渠水力：填写渠底宽b、水深h、边坡系数m、糙率n和渠底比降i，计算面积、湿周、水力半径、谢才系数、流速和流量。\n2. 临界水深：矩形渠道填m=0，梯形渠道填实际边坡系数m；程序按临界流条件求h。\n3. 水力最佳断面：矩形渠道m=0，梯形渠道填m；程序按书中最佳断面公式求水深和底宽。\n4. 比降如果资料写1/7000，则输入0.000142857；界面默认值已按书中例题换算。\n5. 计算后检查复算流量、b/h关系、面积和流速，确认量级合理后再采用。\n6. 导出前把参数来源、书内页码和采用公式写入计算书，便于报告复核。""",
    "渠道附属与安全校核": """1. 引水角：输入河道流速v0和渠道流速v1，v0/v1必须不大于1。\n2. 不冲/不淤：不冲流速用于校核设计流速是否过大；不淤流速用于校核小流量时是否易淤积。\n3. 最佳实用经济断面：输入面积偏离系数a，a通常取1.01~1.04，程序采用浅宽经济断面根。\n4. 渗漏损失：衬砌渠道和无衬砌渠道公式不同，长度单位分别按界面填写m和km。\n5. U形断面：用目标流量和h/B试算水深、水面宽和半圆半径，h/B必须大于0.5。\n6. 钢筋含筋率：当前按书中渠道例题的三类钢筋组合计算，可作为渠道钢筋总量快速核算模板。""",
    "渠道结构与纵断面": """1. 侧墙稳定：填写墙高、填土容重、内摩擦角、水深、混凝土强度和钢筋参数，程序计算土压力、弯矩和配筋。\n2. 复式断面：填写主槽、滩地和边坡参数，程序同时给出整体法、分块法及反求水深结果。\n3. 纵断面设计：逐桩填写桩号、地面高程和控制水深，程序计算渠底、最低水位、设计水位、加大水位和渠顶高程。\n4. 输入前确认桩号顺序、长度单位、糙率和比降均来自同一设计方案。\n5. 开始计算后检查挖填深度、渠顶超高、复式断面分块流量和配筋率是否异常。\n6. 导出时保留书中程序来源、参数表和逐桩成果表，便于报告直接引用。""",
    "堤防工程书中程序": """1. 先选择河道流量估算、治导线宽度、堤顶超高、护坡厚度、冲刷深度或短丁坝设计。\n2. 按当前方法说明填写流量、水深、坡降、糙率、块石参数、粒径或丁坝控制流量。\n3. 水面线推求使用“能量方程水位”或“多断面能量方程法（HEC标准步法）”页签，断面应按实际上下游顺序录入。\n4. 冲刷类方法要区分斜冲、平行、水流弯道和锁坝局部冲刷，不要混用参数。\n5. 开始计算后检查水深、流速、堤顶高程、护坡尺寸和冲刷深度量级。\n6. 结果应结合历史洪水、实测断面和规范安全加高复核后再采用。""",
    "水库蓄水书中程序": """1. 先选择洪峰洪量、淤积库容、年调节、土石坝超高、坝体外轮廓、护坡厚度、帷幕深度、库容或闸门启闭力。\n2. 按界面说明填写流域面积、雨强、水库面积、水位、坝高、风速、库容断面或闸门尺寸。\n3. 年调节和库容计算需要按表格逐行录入，不要把万m³和m³混用。\n4. 坝顶超高已包含风壅、波浪和安全加高，应与项目采用工况一致。\n5. 开始计算后检查库容、坝高、超高、护坡厚度和启闭力是否符合工程量级。\n6. 导出前保留书中例题校核、参数来源和采用工况说明。""",
    "水电站机电与压力管道": """1. 先选择水能出力、装机容量、压力管道、进口淹没、通气管、水锤、水头损失、临界外压、输电线、主变或负荷预测。\n2. 按当前方法填写流量、水头、效率、管径、壁厚、波速、电压、电流密度或负荷参数。\n3. 压力管道、水锤和水头损失应使用同一管径、流速和材料参数，避免前后不一致。\n4. 装机容量初选和经济比较要分别查看保证出力、年利用小时和投资回收期。\n5. 开始计算后检查功率、压力、损失、导线截面和主变容量量级。\n6. 机组选型、管材和电气设备最终应结合厂家资料和现行设计规范复核。""",
    "水工建筑物书中程序": """1. 先选择水闸、溢洪道泄槽、消力池、陡坡、跌水、渡槽、倒虹吸、桥孔、隧洞、涵洞或侧堰。\n2. 参数区按key=value填写，默认值为《水利大算盘》对应例题；切换方法后应重新核对全部参数。\n3. 流量统一用m³/s，高程和长度用m，管壁厚度输出为mm，闸门槽宽槽深输出为cm。\n4. 系数类参数必须记录来源，不能把不同建筑物的流量系数、淹没系数和损失系数混用。\n5. 开始计算后查看公式来源、输入参数表和结果表，重点复核宽度、水深、损失和高程闭合。\n6. 成果用于初步设计快速核算，重要工程需结合现行规范、结构布置和水工模型复核。""",
    "铁一院两所法": """1. 资料准备：填写流域面积、河长、比降、24小时设计雨量和地貌土壤条件。\n2. 参数选择：先核对土壤类别、前期湿润程度及汇流参数；没有依据时不要直接采用默认值。\n3. 频率雨量：逐频率填写H24，缺少某一频率时留空，不用其他频率替代。\n4. 开始计算：软件计算产流、汇流及洪峰流量。\n5. 结果复核：检查参数来源、单位和警告信息，并与邻近流域或既有成果比较。\n6. 成果导出：确认无警告后导出Excel或Word计算书。""",
    "中小河流经验公式法": """1. 划分区域：依据地貌、岩性和植被选择8个分区之一。\n2. 填写流域特征：F用km²、L用km、J用小数；软件自动计算形状系数f=F/L²。\n3. 确定暴雨历时：软件按F立即提示10分钟至24小时的对应历时。\n4. 填写H：推荐直接输入该历时、各频率的流域平均设计雨量；只有流域平均雨量均值时才用Cv和Kp换算。\n5. 开始计算：软件逐频率显示Kp、最终代入公式的H、分区参数和洪峰流量。\n6. 结果复核：检查H不是单站点雨量，分区和单位无误后再导出。""",
    "水文面积比拟法": """1. 选择与设计流域下垫面、气候和产汇流条件相似的参证站。\n2. 填写参证站及设计流域面积，单位均为km²。\n3. 填写参证站各频率洪峰流量，单位m³/s。\n4. 面积指数n可直接输入；有不少于3个站时，可先在右侧回归计算并应用。\n5. 开始计算并检查面积换算比例、n值及是否属于过度外推。\n6. 复核相似性条件后导出成果。""",
    "双对数内插法": """1. 选择两个频率成果可靠、控制面积不同的参证站。\n2. 填写两站面积和各频率洪峰流量。\n3. 填写设计流域面积。\n4. 开始计算：软件在log(F)-log(Q)坐标上逐频率内插。\n5. 检查结果标记：设计面积位于两站范围外时属于外推，应谨慎采用。\n6. 与面积比拟法或地区公式成果比较后导出。""",
    "洪水合成法": """1. 统一各支流过程线的时间步长和流量单位。\n2. 逐条粘贴或导入过程线，并填写汇流到控制断面的时间差。\n3. 检查各过程线起点和峰现时间是否采用同一时间基准。\n4. 开始计算：软件插值平移各过程线后逐时段叠加。\n5. 检查合成洪峰、峰现时间和过程总量是否合理。\n6. 打开过程线图复核后导出。""",
    "水库调洪演算": """1. 准备资料：入库洪水过程线、水位-库容曲线和同一调度工况下的水位-总泄量曲线。\n2. 填写控制条件：输入初始水位；汛限水位和校核洪水位可按项目需要填写。\n3. 录入三张表：时间用h、流量用m³/s、水位用m、库容用万m³；可直接从Excel粘贴或导入前两列。\n4. 开始演算：软件按等水面Modified Puls法逐时段求水位、库容和下泄流量。\n5. 结果复核：重点检查最高水位、最大下泄、削峰率、峰现延迟、曲线范围和水量平衡误差。\n6. 成果输出：打开入流/出流及水位过程图，确认退水段完整后导出Excel或Word计算书。""",
    "地区经验公式法": """1. 在分区表或项目资料中确定地区编号。\n2. 填写流域面积F(km²)。\n3. 核对所选地区的适用面积范围和各频率参数。\n4. 面积超限时优先更换方法；只有明确理由时才允许外推。\n5. 开始计算并与邻近流域实测或其他方法成果比较。\n6. 记录分区依据后导出。""",
    "瞬时单位线法": """1. 填写流域面积、坡降、雨型分区及各频率设计雨量。\n2. 选择入渗分区，核对自动查表的入渗率和产流历时。\n3. 核对单位线参数n、k及雨型时段分配；扫描图数字化参数必须结合原图复核。\n4. 开始计算净雨、单位线和洪水过程线。\n5. 检查净雨不为负、单位线体积闭合、洪峰和过程线连续。\n6. 打开图表复核后导出计算书。""",
    "推理公式法": """1. 填写流域面积、河长、坡降、设计雨量和损失参数。\n2. 选择雨型和入渗分区，核对自动查表值。\n3. 选择汇流参数来源；Ka、Kw、KT及基流由扫描图估算时须查看原图。\n4. 开始计算并查看主雨历时、净雨和汇流试算过程。\n5. 若提示试算点未跨越，不能直接采用，需人工复核汇流曲线。\n6. 比较经验公式或单位线成果后导出。""",
    "曼宁公式水位": """1. 按起点距从小到大准备断面x、z坐标，单位均为m。\n2. 填写设计流量Q(m³/s)、糙率n和水力坡降I(小数)。\n3. 导入或粘贴断面点，确认断面方向不影响高程但起点距不能重复。\n4. 开始计算：软件反算水位并逐级生成水位流量关系。\n5. 检查计算流量与设计流量、Fr、过水面积和水面宽是否合理。\n6. 查看曲线和断面图后导出。""",
    "能量方程水位": """1. 按实际上下游分别录入断面x、z和糙率n。\n2. 填写Q、河段长度、能量修正系数及局部损失系数。\n3. 亚临界流选择“下游已知求上游”；超临界流选择“上游已知求下游”。\n4. 已知水位必须填在对应控制断面，并与所选流态一致。\n5. 开始计算后检查Fr、临界水深、能量损失和方程残差。\n6. 近临界、混合流或有水跃时改用专门模型复核。""",
    "多断面能量方程法（HEC标准步法）": """1. 将全部断面严格按下游到上游顺序排列。\n2. 每个断面填写名称、糙率n、动能系数α和x,z坐标；除最上游外填写到下一上游断面的距离。\n3. 亚临界流填写下游控制水位；超临界流填写上游控制水位。\n4. 开始计算：软件采用平均流量模数摩阻坡降，并在临界水深正确一侧求解。\n5. 检查每个断面的Fr、临界状态、能量损失及纵断面连续性。\n6. 本工具只提供内置HEC标准步法计算；混合流、水跃、桥涵和堰闸应使用专门模型。""",
    "堰流公式水位": """1. 根据堰体结构选择宽顶堰、实用堰、薄壁堰或孔口式。\n2. 填写设计流量、堰顶高程、孔数、宽度和上下游水位。\n3. 按堰型及边界收缩条件核对流量系数、侧收缩系数和淹没条件。\n4. 开始计算反算上游水位或生成断面水位流量表。\n5. 检查δ/H等适用条件、出流状态和反算流量误差。\n6. 记录系数来源后导出。""",
    "波浪高度": """1. 填写设计风速、风区长度、平均水深和重力加速度。\n2. 确认风速重现期及风区方向与工程控制方向一致；有持续风时资料时一并填写。\n3. 选择设计波高换算方式：新人优先按表C.1.3选择累积频率P自动计算Kp；手动Kp必须记录来源。\n4. 开始计算平均波高、平均周期、最小风时、波长和设计波高。\n5. 检查实际风时是否满足、H/d是否在表格范围、波陡和输入单位是否合理。\n6. 记录风区、风速和频率来源后导出。""",
    "风壅水位": """1. 填写设计风速、风区长度、平均水深和风向夹角。\n2. 核对综合摩阻系数及水体方向，长度和水深均用m。\n3. 开始计算风壅增水。\n4. 检查夹角方向、正负号和量级。\n5. 将风壅增水与静水位、波浪爬高按采用规范组合。\n6. 记录参数来源后导出。""",
    "桥孔最小净长度": """1. 先选择一般河段、峡谷河段或其他对应公式。\n2. 按当前公式右侧说明填写设计流量、天然河宽、水深、粒径等参数。\n3. J用小数、粒径按界面标注单位填写；不要沿用其他公式的默认参数。\n4. 开始计算桥孔最小净长度。\n5. 检查河段类型、冲淤影响、墩台阻水和上下游控制条件。\n6. 与桥位布设及冲刷计算综合确定采用值。""",
    "河道演变计算": """1. 先选择河相、稳定河宽、稳定性或冲淤判别方法。\n2. 按动态参数说明逐项填写，切换方法后重新检查所有单位。\n3. 计算前确认参数来自同一河段和同一代表时段。\n4. 开始计算并查看中间量、分级判别和适用说明。\n5. 经验关系只反映趋势，必须结合历年断面、遥感和现场调查验证。\n6. 将参数来源和采用理由写入成果后导出。""",
    "壅水高度及长度": """1. 填写设计流量、天然过水面积、天然河槽流速、建后有效面积、水流交角和水面坡度。\n2. 调查河滩路堤阻挡的流量占设计流量百分比，用它查η；不要用面积阻水比代替。\n3. 选择土质类型，程序按表A.2.3-2计算桥下平均流速Vm；有审查取值时可手动填η。\n4. 开始计算壅水高度、影响长度、冲刷系数P和两种阻水比复核量。\n5. 检查有效过水面积、流速增幅、η来源及结果量级。\n6. 与桥孔、冲刷和河道边界条件联合复核后导出。""",
    "冲刷计算": """1. 根据对象选择一般冲刷、桥墩局部冲刷、桥台/堤岸冲刷及对应规范公式。\n2. 阅读当前公式、适用条件和参数解释，再填写key=value参数。\n3. 特别核对粒径单位、单宽流量、流速、水深和夹角。\n4. 开始计算并查看自动计算的中间参数。\n5. 检查公式适用范围；重要工程应采用多种方法、实测或数值模型复核。\n6. 明确采用公式、参数来源和最终采用冲刷深度后导出。""",
}


class FormMixin:
    def _entry(
        self,
        parent: tk.Widget,
        row: int,
        col: int,
        label: str,
        default: str = "",
        width: int = 13,
    ) -> tk.Entry:
        ttk.Label(parent, text=label).grid(row=row, column=col * 2, padx=(10, 4), pady=6, sticky="e")
        entry = ttk.Entry(parent, width=width)
        entry.insert(0, default)
        entry.grid(row=row, column=col * 2 + 1, padx=(0, 12), pady=6, sticky="w")
        return entry

    def _float(self, entry: tk.Entry, label: str) -> float:
        try:
            return float(entry.get().strip())
        except ValueError as exc:
            raise HydrologyError(f"{label} 必须是数字") from exc

    def _result_box(self, parent: tk.Widget) -> ScrolledText:
        box = ttk.LabelFrame(parent, text="计算结果")
        box.grid(row=1, column=0, sticky="nsew", padx=12, pady=(0, 12))
        box.rowconfigure(0, weight=1)
        box.columnconfigure(0, weight=1)
        text = RichTableText(box, height=18)
        setattr(text, "_hydrology_export_source", True)
        text.grid(row=0, column=0, sticky="nsew", padx=10, pady=(10, 6))
        export_bar = ttk.Frame(box)
        export_bar.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
        self._action_button(export_bar, "导出Excel", lambda: self._export_result("xlsx"), bg="#1688e8", fg="white").pack(side=tk.LEFT)
        self._action_button(export_bar, "导出Word", lambda: self._export_result("docx"), bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(10, 0))
        self._action_button(export_bar, "导出专项报告", lambda: self._export_result("html"), bg="#006b63", fg="white").pack(side=tk.LEFT, padx=(10, 0))
        return text

    def _action_button(self, parent: tk.Widget, text: str, command, bg: str = "#f0f0f0", fg: str = "#111111") -> tk.Button:
        button = tk.Button(
            parent,
            text=text,
            command=command,
            bg=bg,
            fg=fg,
            activebackground=bg,
            activeforeground=fg,
            relief=tk.RAISED,
            bd=1,
            padx=18,
            pady=6,
            font=(DASHBOARD_CARD_FONT, 9, "bold" if bg != "#f0f0f0" else "normal"),
        )
        return button

    def _is_export_source(self, text: ScrolledText) -> bool:
        return text is getattr(self, "output", None) or bool(getattr(text, "_hydrology_export_source", False))

    def _write(self, text: ScrolledText, content: str) -> None:
        if self._is_export_source(text):
            self.last_output = content
        text.configure(state="normal")
        text.delete("1.0", tk.END)
        text.insert(tk.END, content)
        text.configure(state="disabled")

    def _current_export_content(self) -> str:
        output = getattr(self, "output", None)
        if output is not None and hasattr(output, "get"):
            try:
                return output.get("1.0", "end-1c")
            except tk.TclError:
                pass
        return getattr(self, "last_output", "")

    def _report_title(self, content: str) -> str:
        for line in content.splitlines():
            text = line.strip()
            if text:
                return text[:60]
        return "水文计算专项报告"

    def _current_report_figures(self) -> list[tuple[str, str]]:
        return []

    def _line_svg_path(self, points: list[tuple[float, float]]) -> str:
        if not points:
            return ""
        clean = [(float(x), float(y)) for x, y in points if math.isfinite(float(x)) and math.isfinite(float(y))]
        if not clean:
            return ""
        path = [f"M {clean[0][0]:.1f} {clean[0][1]:.1f}"]
        path.extend(f"L {x:.1f} {y:.1f}" for x, y in clean[1:])
        return " ".join(path)

    def _smooth_svg_path(self, points: list[tuple[float, float]]) -> str:
        clean = [(float(x), float(y)) for x, y in points if math.isfinite(float(x)) and math.isfinite(float(y))]
        if not clean:
            return ""
        clean = sorted(clean, key=lambda item: item[0])
        deduped: list[tuple[float, float]] = []
        for x, y in clean:
            if deduped and abs(x - deduped[-1][0]) < 1e-9:
                deduped[-1] = (x, y)
            else:
                deduped.append((x, y))
        points = deduped
        if len(points) == 1:
            x, y = points[0]
            return f"M {x:.1f} {y:.1f}"
        if len(points) == 2:
            return self._line_svg_path(points)

        h = [points[i + 1][0] - points[i][0] for i in range(len(points) - 1)]
        if any(step <= 1e-9 for step in h):
            return self._line_svg_path(points)
        delta = [(points[i + 1][1] - points[i][1]) / h[i] for i in range(len(h))]
        slopes = [0.0] * len(points)
        slopes[0] = delta[0]
        slopes[-1] = delta[-1]
        for idx in range(1, len(points) - 1):
            left_delta = delta[idx - 1]
            right_delta = delta[idx]
            if left_delta == 0 or right_delta == 0 or left_delta * right_delta < 0:
                slopes[idx] = 0.0
                continue
            w1 = 2 * h[idx] + h[idx - 1]
            w2 = h[idx] + 2 * h[idx - 1]
            slopes[idx] = (w1 + w2) / ((w1 / left_delta) + (w2 / right_delta))

        path = [f"M {points[0][0]:.1f} {points[0][1]:.1f}"]
        for idx, ((x1, y1), (x2, y2)) in enumerate(zip(points, points[1:])):
            step = x2 - x1
            c1x = x1 + step / 3.0
            c2x = x2 - step / 3.0
            low_y, high_y = sorted((y1, y2))
            c1y = min(max(y1 + slopes[idx] * step / 3.0, low_y), high_y)
            c2y = min(max(y2 - slopes[idx + 1] * step / 3.0, low_y), high_y)
            path.append(f"C {c1x:.1f} {c1y:.1f}, {c2x:.1f} {c2y:.1f}, {x2:.1f} {y2:.1f}")
        return " ".join(path)

    def _line_chart_svg(
        self,
        title: str,
        series: list[tuple[str, list[tuple[float, float]], str]],
        y_axis_title: str,
        x_axis_title: str = "时间 t(h)",
    ) -> str:
        width, height = 1080, 430
        left, right, top, bottom = 78, 42, 52, 70
        plot_w = width - left - right
        plot_h = height - top - bottom
        points = [point for _label, values, _color in series for point in values]
        if not points:
            return ""
        x_values = [x for x, _y in points]
        y_values = [y for _x, y in points]
        min_x, max_x = min(x_values), max(x_values)
        min_y, max_y = min(y_values), max(y_values)
        if abs(max_x - min_x) < 1e-9:
            max_x = min_x + 1.0
        if min_y >= 0:
            min_y = 0.0
        span_y = max(max_y - min_y, max(abs(max_y), 1.0) * 0.08)
        max_y += span_y * 0.10
        if min_y < 0:
            min_y -= span_y * 0.05

        def sx(value: float) -> float:
            ratio = (value - min_x) / (max_x - min_x)
            return left + max(0.0, min(1.0, ratio)) * plot_w

        def sy(value: float) -> float:
            ratio = (max_y - value) / max(max_y - min_y, 1e-9)
            return top + max(0.0, min(1.0, ratio)) * plot_h

        grid: list[str] = []
        for idx in range(6):
            x_value = min_x + (max_x - min_x) * idx / 5
            x = sx(x_value)
            grid.append(f"<line x1='{x:.1f}' y1='{top}' x2='{x:.1f}' y2='{top + plot_h}' stroke='#e3e9ee'/>")
            grid.append(f"<text x='{x:.1f}' y='{top + plot_h + 24}' text-anchor='middle' font-size='12'>{x_value:.2f}</text>")
        for idx in range(6):
            y_value = min_y + (max_y - min_y) * idx / 5
            y = sy(y_value)
            grid.append(f"<line x1='{left}' y1='{y:.1f}' x2='{left + plot_w}' y2='{y:.1f}' stroke='#dce4ea'/>")
            grid.append(f"<text x='{left - 10}' y='{y + 4:.1f}' text-anchor='end' font-size='12'>{y_value:.2f}</text>")
        paths: list[str] = []
        legend: list[str] = []
        clip_id = f"plot_clip_{abs(hash((title, x_axis_title, y_axis_title))) % 1000000}"
        for index, (label, values, color) in enumerate(series):
            if not values:
                continue
            path = self._smooth_svg_path([(sx(x), sy(y)) for x, y in values])
            paths.append(f"<path d='{path}' fill='none' stroke='{color}' stroke-width='3' stroke-linejoin='round' stroke-linecap='round' clip-path='url(#{clip_id})'/>")
            legend.append(f"<text x='{left + 18 + index * 180}' y='{top + 20}' font-size='13' fill='{color}' font-weight='700'>{html_lib.escape(label)}</text>")
        return (
            "<div class='chart'>"
            f"<svg viewBox='0 0 {width} {height}' xmlns='http://www.w3.org/2000/svg'>"
            f"<defs><clipPath id='{clip_id}'><rect x='{left}' y='{top}' width='{plot_w}' height='{plot_h}'/></clipPath></defs>"
            f"<text x='{width / 2}' y='26' text-anchor='middle' font-size='20' font-weight='700'>{html_lib.escape(title)}</text>"
            + "".join(grid)
            + f"<line x1='{left}' y1='{top}' x2='{left}' y2='{top + plot_h}' stroke='#263847'/>"
            + f"<line x1='{left}' y1='{top + plot_h}' x2='{left + plot_w}' y2='{top + plot_h}' stroke='#263847'/>"
            + "".join(paths)
            + "".join(legend)
            + f"<text x='{left + plot_w / 2}' y='{height - 18}' text-anchor='middle' font-size='15'>{html_lib.escape(x_axis_title)}</text>"
            + f"<text x='22' y='{top + plot_h / 2}' transform='rotate(-90 22 {top + plot_h / 2})' text-anchor='middle' font-size='15'>{html_lib.escape(y_axis_title)}</text>"
            + "</svg></div>"
        )

    def _export_result(self, kind: str) -> None:
        content = self._current_export_content().strip()
        if not content:
            messagebox.showwarning("导出计算书", "当前没有可导出的计算结果，请先计算。")
            return
        if kind == "xlsx":
            path = filedialog.asksaveasfilename(
                title="导出Excel计算书",
                defaultextension=".xlsx",
                filetypes=[("Excel工作簿", "*.xlsx")],
                initialfile="水文计算书.xlsx",
            )
            if not path:
                return
            custom_xlsx_exporter = getattr(self, "_export_result_xlsx", None)
            if callable(custom_xlsx_exporter) and custom_xlsx_exporter(path):
                pass
            else:
                export_calculation_book_xlsx(content, path)
        elif kind == "docx":
            path = filedialog.asksaveasfilename(
                title="导出Word计算书",
                defaultextension=".docx",
                filetypes=[("Word文档", "*.docx")],
                initialfile="水文计算书.docx",
            )
            if not path:
                return
            export_calculation_book_docx(content, path)
        elif kind == "html":
            path = filedialog.asksaveasfilename(
                title="导出专项报告",
                defaultextension=".html",
                filetypes=[("HTML专项报告", "*.html")],
                initialfile="水文计算专项报告.html",
            )
            if not path:
                return
            export_calculation_report_html(
                content,
                path,
                title=self._report_title(content),
                figures=self._current_report_figures(),
            )
        else:
            raise HydrologyError(f"不支持的导出类型：{kind}")
        messagebox.showinfo("导出计算书", f"已导出：{path}")

    def _parse_section_points(self, value: str, section_name: str) -> tuple[tuple[float, float], ...]:
        pairs = [item.strip() for item in re.split(r"[;\n]+", value.strip()) if item.strip()]
        points: list[tuple[float, float]] = []
        for item in pairs:
            parts = [part for part in re.split(r"[,，\s]+", item) if part]
            if len(parts) != 2:
                raise HydrologyError(f"{section_name} 断面点格式错误：{item}")
            try:
                points.append((float(parts[0]), float(parts[1])))
            except ValueError as exc:
                raise HydrologyError(f"{section_name} 断面点必须为数字：{item}") from exc
        if len(points) < 2:
            raise HydrologyError(f"{section_name} 至少需要2个断面点")
        return tuple(points)

    def _points_to_text(self, points: tuple[tuple[float, float], ...]) -> str:
        return "; ".join(f"{x:g},{z:g}" for x, z in points)

    def _read_points_csv(self, path: Path) -> tuple[tuple[float, float], ...]:
        last_error: Exception | None = None
        for encoding in ("utf-8-sig", "gbk"):
            try:
                with path.open("r", encoding=encoding, newline="") as handle:
                    return self._rows_to_points(list(csv.reader(handle)), path.name)
            except Exception as exc:
                last_error = exc
        raise HydrologyError(f"CSV读取失败：{last_error}")

    def _read_points_xlsx(self, path: Path) -> tuple[tuple[float, float], ...]:
        ns = {
            "main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
            "rel": "http://schemas.openxmlformats.org/package/2006/relationships",
        }
        with ZipFile(path) as zf:
            shared: list[str] = []
            if "xl/sharedStrings.xml" in zf.namelist():
                root = ET.fromstring(zf.read("xl/sharedStrings.xml"))
                for si in root.findall("main:si", ns):
                    texts = [node.text or "" for node in si.findall(".//main:t", ns)]
                    shared.append("".join(texts))
            workbook = ET.fromstring(zf.read("xl/workbook.xml"))
            first_sheet = workbook.find("main:sheets/main:sheet", ns)
            if first_sheet is None:
                raise HydrologyError("Excel文件未找到工作表")
            rel_id = first_sheet.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
            rels = ET.fromstring(zf.read("xl/_rels/workbook.xml.rels"))
            target = None
            for rel in rels.findall("rel:Relationship", ns):
                if rel.attrib.get("Id") == rel_id:
                    target = rel.attrib.get("Target")
                    break
            if not target:
                raise HydrologyError("Excel文件未找到第一个工作表路径")
            sheet = ET.fromstring(zf.read("xl/" + target.lstrip("/")))
            rows: list[list[str]] = []
            for row in sheet.findall(".//main:sheetData/main:row", ns):
                values: list[str] = []
                for cell in row.findall("main:c", ns):
                    value_node = cell.find("main:v", ns)
                    value = value_node.text if value_node is not None else ""
                    if cell.attrib.get("t") == "s" and value:
                        value = shared[int(value)]
                    values.append(value)
                rows.append(values)
        return self._rows_to_points(rows, path.name)

    def _rows_to_points(self, rows: list[list[object]], source_name: str) -> tuple[tuple[float, float], ...]:
        points: list[tuple[float, float]] = []
        for row in rows:
            numeric: list[float] = []
            for cell in row:
                text = str(cell).strip()
                if not text:
                    continue
                try:
                    numeric.append(float(text))
                except ValueError:
                    continue
                if len(numeric) == 2:
                    break
            if len(numeric) == 2:
                points.append((numeric[0], numeric[1]))
        if len(points) < 2:
            raise HydrologyError(f"{source_name} 未读取到至少2行有效里程、高程数据")
        return tuple(points)

    def _import_points_from_file(self) -> tuple[tuple[float, float], ...] | None:
        path_text = filedialog.askopenfilename(
            title="导入断面点Excel/CSV",
            filetypes=[("Excel/CSV", "*.xlsx *.csv"), ("Excel工作簿", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")],
        )
        if not path_text:
            return None
        path = Path(path_text)
        if path.suffix.lower() == ".csv":
            return self._read_points_csv(path)
        if path.suffix.lower() == ".xlsx":
            return self._read_points_xlsx(path)
        raise HydrologyError("仅支持 .xlsx 或 .csv 文件")


def parse_hydraulic_points(value: str, section_name: str) -> tuple[tuple[float, float], ...]:
    pairs = [item.strip() for item in re.split(r"[;\n]+", value.strip()) if item.strip()]
    points: list[tuple[float, float]] = []
    for item in pairs:
        parts = [part for part in re.split(r"[,，\s]+", item) if part]
        if len(parts) != 2:
            raise HydrologyError(f"{section_name} 断面点格式错误：{item}")
        try:
            points.append((float(parts[0]), float(parts[1])))
        except ValueError as exc:
            raise HydrologyError(f"{section_name} 断面点必须为数字：{item}") from exc
    if len(points) < 2:
        raise HydrologyError(f"{section_name} 至少需要2个断面点")
    if len({x for x, _z in points}) != len(points):
        raise HydrologyError(f"{section_name} 断面点里程不能重复")
    return tuple(points)


class ScrollableFrame(ttk.Frame):
    def __init__(self, master: tk.Widget, background: str | None = None, show_horizontal: bool = True) -> None:
        super().__init__(master)
        self.show_horizontal = show_horizontal
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        canvas_options = {"highlightthickness": 0, "borderwidth": 0}
        if background is not None:
            canvas_options["background"] = background
        self.canvas = tk.Canvas(self, **canvas_options)
        self.v_scroll = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.canvas.yview)
        self.h_scroll = ttk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.canvas.xview)
        self.canvas.configure(yscrollcommand=self.v_scroll.set, xscrollcommand=self.h_scroll.set)
        self.canvas.grid(row=0, column=0, sticky="nsew")
        self.v_scroll.grid(row=0, column=1, sticky="ns")
        if show_horizontal:
            self.h_scroll.grid(row=1, column=0, sticky="ew")

        self.body = tk.Frame(self.canvas, bg=background) if background is not None else ttk.Frame(self.canvas)
        self.body.columnconfigure(0, weight=1)
        self.window_id = self.canvas.create_window((0, 0), window=self.body, anchor="nw")

        self.body.bind("<Configure>", self._on_body_configure)
        self.canvas.bind("<Configure>", self._on_canvas_configure)
        self.canvas.bind("<Enter>", self._bind_mousewheel)
        self.canvas.bind("<Leave>", self._unbind_mousewheel)

    def _on_body_configure(self, _event=None) -> None:
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def _on_canvas_configure(self, event) -> None:
        width = max(event.width, self.body.winfo_reqwidth()) if self.show_horizontal else event.width
        self.canvas.itemconfigure(self.window_id, width=width)

    def _bind_mousewheel(self, _event=None) -> None:
        self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
        self.canvas.bind_all("<Shift-MouseWheel>", self._on_shift_mousewheel)

    def _unbind_mousewheel(self, _event=None) -> None:
        self.canvas.unbind_all("<MouseWheel>")
        self.canvas.unbind_all("<Shift-MouseWheel>")

    def _on_mousewheel(self, event) -> str | None:
        if event.widget.winfo_class() in {"Text", "Treeview", "Listbox"}:
            return None
        self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"

    def _on_shift_mousewheel(self, event) -> str | None:
        if event.widget.winfo_class() in {"Text", "Treeview", "Listbox"}:
            return None
        self.canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"


class RichTableText(ttk.Frame):
    """Display exported calculation text with Excel-like table grids."""

    TABLE_HEADER_BG = "#d9ead3"
    TABLE_BODY_BG = "#eef7e9"
    TABLE_BORDER = "#222222"

    def __init__(
        self,
        master: tk.Widget,
        height: int = 18,
        font: tuple[str, int] | tuple[str, int, str] = (DASHBOARD_CARD_FONT, 10),
        table_font: tuple[str, int] | tuple[str, int, str] = (DASHBOARD_CARD_FONT, 10),
        wraplength: int = 1100,
        **kwargs,
    ) -> None:
        state = kwargs.pop("state", None)
        super().__init__(master, **kwargs)
        self._content = ""
        self._state = state or "normal"
        self._font = font
        self._table_font = table_font
        self._wraplength = wraplength
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)
        self.scroller = ScrollableFrame(self, background="white")
        self.scroller.grid(row=0, column=0, sticky="nsew")
        self.scroller.canvas.configure(height=max(120, height * 22), background="white")

    def configure(self, cnf=None, **kwargs):  # type: ignore[override]
        if cnf:
            kwargs.update(cnf)
        if "state" in kwargs:
            self._state = str(kwargs.pop("state"))
        if "font" in kwargs:
            self._font = kwargs.pop("font")
        if "wraplength" in kwargs:
            self._wraplength = int(kwargs.pop("wraplength"))
        if kwargs:
            return super().configure(**kwargs)
        return None

    config = configure

    def delete(self, *_args) -> None:
        self._content = ""
        self._render()

    def insert(self, _index, content: object) -> None:
        self._content += str(content)
        self._render()

    def get(self, *_args) -> str:
        return self._content

    def _clear_body(self) -> None:
        for child in self.scroller.body.winfo_children():
            child.destroy()

    def _render(self) -> None:
        self._clear_body()
        body = self.scroller.body
        body.configure(bg="white")
        row = 0
        for block_type, payload in parse_calculation_blocks(self._content):
            if block_type == "table":
                self._render_table(body, payload, row)  # type: ignore[arg-type]
                row += 1
            else:
                text = str(payload)
                self._render_paragraph(body, text, row)
                row += 1
        body.update_idletasks()
        self.scroller.canvas.configure(scrollregion=self.scroller.canvas.bbox("all"))

    def _render_paragraph(self, parent: tk.Widget, text: str, row: int) -> None:
        if not text.strip():
            spacer = tk.Frame(parent, height=8, bg="white")
            spacer.grid(row=row, column=0, sticky="ew")
            return
        is_heading = bool(re.match(r"^[一二三四五六七八九十]+[、.．]", text.strip()))
        label = tk.Label(
            parent,
            text=text,
            bg="white",
            fg="#111111",
            font=(DASHBOARD_CARD_FONT, 10, "bold") if is_heading else self._font,
            anchor="w",
            justify=tk.LEFT,
            wraplength=self._wraplength,
        )
        label.grid(row=row, column=0, sticky="ew", padx=4, pady=(8 if is_heading else 3, 2))

    def _render_table(self, parent: tk.Widget, rows: list[list[str]], row: int) -> None:
        if not rows:
            return
        max_cols = max((len(item) for item in rows), default=1)
        normalized = [[str(cell).strip() for cell in item] + [""] * (max_cols - len(item)) for item in rows]
        table = tk.Frame(parent, bg="white")
        table.grid(row=row, column=0, sticky="w", padx=4, pady=6)
        regular_font = tkfont.Font(font=self._table_font)
        header_font = tkfont.Font(font=(DASHBOARD_CARD_FONT, 10, "bold"))
        widths = []
        for col in range(max_cols):
            measured = max(
                (header_font if row_index == 0 else regular_font).measure(str(item[col]))
                for row_index, item in enumerate(normalized)
            )
            widths.append(min(480, max(96, measured + 28)))
        for col, width in enumerate(widths):
            table.columnconfigure(col, minsize=width)
        for row_index, values in enumerate(normalized):
            for col_index, value in enumerate(values):
                is_header = row_index == 0
                cell = tk.Label(
                    table,
                    text=str(value),
                    bg=self.TABLE_HEADER_BG if is_header else self.TABLE_BODY_BG,
                    fg="#000000",
                    font=(DASHBOARD_CARD_FONT, 10, "bold") if is_header else self._table_font,
                    anchor="center",
                    justify=tk.CENTER,
                    padx=8,
                    pady=3,
                    bd=1,
                    relief=tk.SOLID,
                    highlightthickness=0,
                    wraplength=max(76, widths[col_index] - 20),
                )
                cell.grid(row=row_index, column=col_index, sticky="nsew")


class WaterworksProgramLibraryTab(ttk.Frame):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)
        self.program_by_item: dict[str, object] = {}

        summary = waterworks_status_summary()
        header = ttk.LabelFrame(self, text="水利大算盘计算程序接入总览")
        header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 8))
        header.columnconfigure(0, weight=1)
        ttk.Label(
            header,
            text=(
                "按《水利水电工程设计常用计算Excel应用程序集（水利大算盘）》目录整理，"
                "用于统一管理书中全部计算程序的接入状态。"
            ),
            wraplength=1120,
            justify=tk.LEFT,
        ).grid(row=0, column=0, sticky="w", padx=10, pady=(8, 4))
        ttk.Label(
            header,
            text=(
                f"目录程序共 {len(WATERWORKS_PROGRAMS)} 项；"
                f"已接入 {summary['已接入']} 项，部分接入 {summary['部分接入']} 项，待接入 {summary['待接入']} 项。"
            ),
            foreground="#1f5f8b",
            font=(DASHBOARD_CARD_FONT, 10, "bold"),
        ).grid(row=1, column=0, sticky="w", padx=10, pady=(0, 8))

        actions = ttk.Frame(self)
        actions.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 8))
        ttk.Button(actions, text="打开水利大算盘PDF", command=self._open_pdf).pack(side=tk.LEFT)
        ttk.Button(actions, text="打开堰流计算程序Excel", command=self._open_weir_excel).pack(side=tk.LEFT, padx=(8, 0))

        split = ttk.Frame(self)
        split.grid(row=2, column=0, sticky="nsew", padx=10, pady=(0, 10))
        split.columnconfigure(0, weight=2)
        split.columnconfigure(1, weight=1)
        split.rowconfigure(0, weight=1)

        table_frame = ttk.LabelFrame(split, text="程序目录")
        table_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
        table_frame.columnconfigure(0, weight=1)
        table_frame.rowconfigure(0, weight=1)
        columns = ("section", "name", "book_page", "pdf_page", "status", "mapped")
        self.tree = ttk.Treeview(table_frame, columns=columns, show="tree headings", height=22)
        self.tree.heading("#0", text="章节")
        self.tree.heading("section", text="节")
        self.tree.heading("name", text="计算程序")
        self.tree.heading("book_page", text="书页")
        self.tree.heading("pdf_page", text="PDF页")
        self.tree.heading("status", text="状态")
        self.tree.heading("mapped", text="对应模块")
        self.tree.column("#0", width=220, anchor="w", stretch=False)
        self.tree.column("section", width=62, anchor="center", stretch=False)
        self.tree.column("name", width=260, anchor="w", stretch=True)
        self.tree.column("book_page", width=58, anchor="center", stretch=False)
        self.tree.column("pdf_page", width=58, anchor="center", stretch=False)
        self.tree.column("status", width=76, anchor="center", stretch=False)
        self.tree.column("mapped", width=170, anchor="w", stretch=False)
        yscroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=yscroll.set)
        self.tree.grid(row=0, column=0, sticky="nsew")
        yscroll.grid(row=0, column=1, sticky="ns")

        detail_frame = ttk.LabelFrame(split, text="接入说明")
        detail_frame.grid(row=0, column=1, sticky="nsew")
        detail_frame.columnconfigure(0, weight=1)
        detail_frame.rowconfigure(0, weight=1)
        self.detail = RichTableText(detail_frame, height=22, wraplength=390)
        self.detail.grid(row=0, column=0, sticky="nsew", padx=4, pady=4)

        self._populate_tree()
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        first_child = self.tree.get_children(self.tree.get_children()[0])[0]
        self.tree.selection_set(first_child)
        self.tree.focus(first_child)
        self._show_program(first_child)

    def _populate_tree(self) -> None:
        chapter_items: dict[str, str] = {}
        for program in WATERWORKS_PROGRAMS:
            parent = chapter_items.get(program.chapter)
            if parent is None:
                parent = self.tree.insert("", tk.END, text=program.chapter, open=True)
                chapter_items[program.chapter] = parent
            item = self.tree.insert(
                parent,
                tk.END,
                text="",
                values=(
                    program.section,
                    program.name,
                    program.book_page,
                    program.pdf_page_hint,
                    program.status,
                    program.mapped_module or "-",
                ),
            )
            self.program_by_item[item] = program

    def _on_select(self, _event=None) -> None:
        selected = self.tree.selection()
        if selected:
            self._show_program(selected[0])

    def _show_program(self, item: str) -> None:
        program = self.program_by_item.get(item)
        if program is None:
            children = self.tree.get_children(item)
            if children:
                self.tree.selection_set(children[0])
                self.tree.focus(children[0])
                self._show_program(children[0])
            return
        self.detail.delete("1.0", tk.END)
        self.detail.insert(tk.END, waterworks_program_markdown(program))

    def _open_pdf(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "堰流公式计算水位" / "水利大算盘.pdf"
        if not path.exists():
            messagebox.showerror("文件不存在", f"未找到：{path}")
            return
        os.startfile(path)

    def _open_weir_excel(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "堰流公式计算水位" / "堰流计算程序.xls"
        if not path.exists():
            messagebox.showerror("文件不存在", f"未找到：{path}")
            return
        os.startfile(path)


class WaterworksDomainProgramTab(ttk.Frame):
    title = "相关计算程序"
    description = ""
    chapter_prefixes: tuple[str, ...] = ()
    include_names: tuple[str, ...] = ()
    exclude_names: tuple[str, ...] = ()
    existing_methods: tuple[str, ...] = ()

    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)
        self.program_by_item: dict[str, object] = {}
        self.programs = self._filtered_programs()

        summary = self._status_summary(self.programs)
        header = ttk.LabelFrame(self, text=self.title)
        header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 8))
        header.columnconfigure(0, weight=1)
        ttk.Label(
            header,
            text=self.description,
            wraplength=1120,
            justify=tk.LEFT,
        ).grid(row=0, column=0, sticky="w", padx=10, pady=(8, 4))
        ttk.Label(
            header,
            text=(
                f"本类共归并《水利大算盘》程序 {len(self.programs)} 项；"
                f"已接入 {summary.get('已接入', 0)} 项，"
                f"部分接入 {summary.get('部分接入', 0)} 项，"
                f"待接入 {summary.get('待接入', 0)} 项。"
            ),
            foreground="#1f5f8b",
            font=(DASHBOARD_CARD_FONT, 10, "bold"),
        ).grid(row=1, column=0, sticky="w", padx=10, pady=(0, 8))

        if self.existing_methods:
            method_text = "本模块已接入软件计算页：" + "、".join(self.existing_methods)
            ttk.Label(header, text=method_text, foreground="#526577", wraplength=1120).grid(
                row=2, column=0, sticky="w", padx=10, pady=(0, 8)
            )

        actions = ttk.Frame(self)
        actions.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 8))
        ttk.Button(actions, text="打开水利大算盘PDF", command=self._open_pdf).pack(side=tk.LEFT)

        split = ttk.Frame(self)
        split.grid(row=2, column=0, sticky="nsew", padx=10, pady=(0, 10))
        split.columnconfigure(0, weight=2)
        split.columnconfigure(1, weight=1)
        split.rowconfigure(0, weight=1)

        table_frame = ttk.LabelFrame(split, text="归类后的程序清单")
        table_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
        table_frame.columnconfigure(0, weight=1)
        table_frame.rowconfigure(0, weight=1)
        columns = ("section", "name", "book_page", "pdf_page", "status", "mapped")
        self.tree = ttk.Treeview(table_frame, columns=columns, show="tree headings", height=18)
        self.tree.heading("#0", text="章节")
        self.tree.heading("section", text="节")
        self.tree.heading("name", text="计算程序")
        self.tree.heading("book_page", text="书页")
        self.tree.heading("pdf_page", text="PDF页")
        self.tree.heading("status", text="状态")
        self.tree.heading("mapped", text="对应计算页")
        self.tree.column("#0", width=220, anchor="w", stretch=False)
        self.tree.column("section", width=54, anchor="center", stretch=False)
        self.tree.column("name", width=260, anchor="w", stretch=True)
        self.tree.column("book_page", width=58, anchor="center", stretch=False)
        self.tree.column("pdf_page", width=58, anchor="center", stretch=False)
        self.tree.column("status", width=76, anchor="center", stretch=False)
        self.tree.column("mapped", width=170, anchor="w", stretch=False)
        yscroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=yscroll.set)
        self.tree.grid(row=0, column=0, sticky="nsew")
        yscroll.grid(row=0, column=1, sticky="ns")

        detail_frame = ttk.LabelFrame(split, text="接入说明")
        detail_frame.grid(row=0, column=1, sticky="nsew")
        detail_frame.columnconfigure(0, weight=1)
        detail_frame.rowconfigure(0, weight=1)
        self.detail = RichTableText(detail_frame, height=18, wraplength=390)
        self.detail.grid(row=0, column=0, sticky="nsew", padx=4, pady=4)

        self._populate_tree()
        self.tree.bind("<<TreeviewSelect>>", self._on_select)
        first = self._first_program_item()
        if first:
            self.tree.selection_set(first)
            self.tree.focus(first)
            self._show_program(first)
        else:
            self.detail.insert(tk.END, "本模块暂未分配《水利大算盘》条目，当前主要承载软件已有计算方法。")

    @classmethod
    def _matches(cls, program: object) -> bool:
        include = bool(cls.include_names and getattr(program, "name", "") in cls.include_names)
        chapter = getattr(program, "chapter", "")
        in_chapter = bool(cls.chapter_prefixes and any(chapter.startswith(prefix) for prefix in cls.chapter_prefixes))
        excluded = bool(cls.exclude_names and getattr(program, "name", "") in cls.exclude_names)
        return (include or in_chapter) and not excluded

    @classmethod
    def program_count(cls) -> int:
        return sum(1 for program in WATERWORKS_PROGRAMS if cls._matches(program))

    @classmethod
    def _filtered_programs(cls) -> list[object]:
        return [program for program in WATERWORKS_PROGRAMS if cls._matches(program)]

    @staticmethod
    def _status_summary(programs: list[object]) -> dict[str, int]:
        counts = {"已接入": 0, "部分接入": 0, "待接入": 0}
        for program in programs:
            status = getattr(program, "status", "")
            counts[status] = counts.get(status, 0) + 1
        return counts

    def _populate_tree(self) -> None:
        chapter_items: dict[str, str] = {}
        for program in self.programs:
            parent = chapter_items.get(program.chapter)
            if parent is None:
                parent = self.tree.insert("", tk.END, text=program.chapter, open=True)
                chapter_items[program.chapter] = parent
            item = self.tree.insert(
                parent,
                tk.END,
                text="",
                values=(
                    program.section,
                    program.name,
                    program.book_page,
                    program.pdf_page_hint,
                    program.status,
                    program.mapped_module or "-",
                ),
            )
            self.program_by_item[item] = program

    def _first_program_item(self) -> str | None:
        for chapter in self.tree.get_children():
            children = self.tree.get_children(chapter)
            if children:
                return children[0]
        return None

    def _on_select(self, _event=None) -> None:
        selected = self.tree.selection()
        if selected:
            self._show_program(selected[0])

    def _show_program(self, item: str) -> None:
        program = self.program_by_item.get(item)
        if program is None:
            children = self.tree.get_children(item)
            if children:
                self.tree.selection_set(children[0])
                self.tree.focus(children[0])
                self._show_program(children[0])
            return
        self.detail.delete("1.0", tk.END)
        self.detail.insert(tk.END, waterworks_program_markdown(program))

    def _open_pdf(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "堰流公式计算水位" / "水利大算盘.pdf"
        if not path.exists():
            messagebox.showerror("文件不存在", f"未找到：{path}")
            return
        os.startfile(path)


class FloodHydrologyProgramTab(WaterworksDomainProgramTab):
    title = "流域水文与设计洪水相关程序"
    description = "归并设计洪峰、洪水总量、经验公式、单位线和推理公式类入口。书中仅有洪峰流量和洪水总量程序，其余为软件已有水文计算方法。"
    include_names = ("洪峰流量和洪水总量计算",)
    existing_methods = ("铁一院两所法", "中小河流经验公式法", "水文面积比拟法", "地区经验公式法", "瞬时单位线法", "推理公式法", "洪水合成法")


class IrrigationChannelProgramTab(WaterworksDomainProgramTab):
    title = "灌溉泵站与渠道输水相关程序"
    description = "归并灌区需水、引水流量、泵站扬程、明渠水力、临界水深、经济断面、渠道稳定和纵断面等程序。"
    chapter_prefixes = ("第一章", "第二章")
    existing_methods = ("渠道水力计算",)


class RiverLeveeProgramTab(WaterworksDomainProgramTab):
    title = "河道堤防与防洪影响相关程序"
    description = "归并河道流量估算、治导线宽度、水面线、堤顶超高、护坡厚度、冲刷深度和丁坝设计等程序。"
    chapter_prefixes = ("第三章",)
    existing_methods = ("河道演变计算", "壅水高度及长度", "冲刷计算", "波浪高度", "风壅水位")


class ReservoirProgramTab(WaterworksDomainProgramTab):
    title = "水库蓄水与调洪相关程序"
    description = "归并水库淤积、年调节、坝顶超高、坝体外轮廓、库容、闸门启闭和调洪演算等程序。"
    chapter_prefixes = ("第五章",)
    exclude_names = ("洪峰流量和洪水总量计算",)
    existing_methods = ("水库调洪演算",)


class ReservoirStorageBookTab(ttk.Frame, FormMixin):
    METHOD_DEFS = {
        "洪峰流量和洪水总量": {
            "source": "《水利大算盘》第五章第1节，式(5-1)至式(5-6)，例题5-1和图5-2。",
            "formula": "Q=KSF^(2/3)；S=24^(n-1)H24P；W=1000hF；山洪Q=KF^n/CSF/CSF^(2/3)",
            "params": [
                ("F", "汇水面积F(km2)", "90"),
                ("n", "暴雨递减指数n", "0.62"),
                ("K", "洪峰流量参数K", "0.45"),
                ("H24", "设计频率24h降雨量H24P(mm)", "30"),
                ("h", "一次净雨量h(mm)", "20"),
                ("S_m", "山洪暴雨力S(mm/h)", "3"),
                ("K_m", "山洪径流模数K", "18"),
                ("C_m", "山洪洪峰流量参数C", "0.45"),
                ("n_m", "山洪面积参数n", "0.75"),
            ],
        },
        "水库淤积库容估算": {
            "source": "《水利大算盘》第五章第2节，式(5-7)、式(5-8)，例题5-2。",
            "formula": "W=MF；V=WT/γ",
            "params": [
                ("F", "流域面积F(km2)", "300"),
                ("M", "侵蚀模数M(t/km2)", "5"),
                ("T", "水库使用年限T(年)", "50"),
                ("gamma", "淤沙体干容重γ(t/m3)", "1.3"),
            ],
        },
        "水库年运用调节": {
            "source": "《水利大算盘》第五章第3节，图5-4，例题5-3。",
            "formula": "余水=来水-用水；亏水=用水-来水；蓄水量=上月蓄水+本月来水-本月用水-弃水",
            "params": [
                ("months", "月份，逗号分隔", "3,4,5,6,7,8,9,10,11,12,1,2"),
                ("inflows", "每月来水流量，逗号分隔", "31.1,40.4,68.2,85.8,58.2,30.6,13.4,6.5,3.2,4.4,9.2,15.5"),
                ("demands", "每月用水流量，逗号分隔", "20,20,20,20,20,20,20,20,20,20,20,20"),
                ("capacity", "水库有效库容(m3)", "300000000"),
            ],
        },
    }

    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        top = ttk.LabelFrame(self, text="水库蓄水书中程序")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(1, weight=1)
        ttk.Label(top, text="计算程序").grid(row=0, column=0, sticky="e", padx=(10, 4), pady=6)
        self.method = tk.StringVar(value="洪峰流量和洪水总量")
        method_box = ttk.Combobox(top, textvariable=self.method, values=tuple(self.METHOD_DEFS), state="readonly", width=28)
        method_box.grid(row=0, column=1, sticky="w", padx=(0, 12), pady=6)
        method_box.bind("<<ComboboxSelected>>", lambda _event: self._load_method())
        self.source_label = ttk.Label(top, text="", foreground="#555555", wraplength=980, justify=tk.LEFT)
        self.source_label.grid(row=1, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 4))
        self.formula_label = ttk.Label(top, text="", foreground="#333333", wraplength=980, justify=tk.LEFT)
        self.formula_label.grid(row=2, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 6))
        params_box = ttk.LabelFrame(top, text="输入参数：按 key=value 修改，默认值为书中例题")
        params_box.grid(row=3, column=0, columnspan=3, sticky="ew", padx=10, pady=6)
        params_box.columnconfigure(0, weight=1)
        self.params_text = ScrolledText(params_box, height=9, width=100, font=(DASHBOARD_MONO_FONT, 10))
        self.params_text.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.param_note = ttk.Label(params_box, text="", foreground="#666666", wraplength=1000, justify=tk.LEFT)
        self.param_note.grid(row=1, column=0, sticky="ew", padx=8, pady=(0, 8))
        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, columnspan=3, sticky="ew", padx=10, pady=(4, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "恢复本方法例题", self._load_method).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))
        self.output = self._result_box(self)
        self._load_method()

    def _load_method(self) -> None:
        info = self.METHOD_DEFS[self.method.get()]
        self.source_label.configure(text=f"来源：{info['source']}")
        self.formula_label.configure(text=f"公式：{info['formula']}")
        self.params_text.delete("1.0", tk.END)
        self.params_text.insert(tk.END, "\n".join(f"{key}={default}" for key, _label, default in info["params"]))
        self.param_note.configure(text="；".join(f"{key}：{label}" for key, label, _default in info["params"]))

    def _params(self) -> dict[str, str]:
        values: dict[str, str] = {}
        for raw_line in self.params_text.get("1.0", "end-1c").splitlines():
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                raise HydrologyError(f"参数行必须按 key=value 填写：{line}")
            key, value = line.split("=", 1)
            values[key.strip()] = value.strip()
        return values

    def _float_value(self, values: dict[str, str], key: str) -> float:
        if key not in values:
            raise HydrologyError(f"缺少参数：{key}")
        try:
            return float(values[key])
        except ValueError as exc:
            raise HydrologyError(f"{key} 必须是数字") from exc

    def _list(self, values: dict[str, str], key: str) -> list[str]:
        if key not in values:
            raise HydrologyError(f"缺少参数：{key}")
        return [item.strip() for item in values[key].split(",") if item.strip()]

    def _float_list(self, values: dict[str, str], key: str) -> list[float]:
        try:
            return [float(item) for item in self._list(values, key)]
        except ValueError as exc:
            raise HydrologyError(f"{key} 必须是逗号分隔的数字") from exc

    def _input_rows(self, values: dict[str, str]) -> list[list[str]]:
        rows = [["参数", "输入值", "说明"]]
        for key, label, _default in self.METHOD_DEFS[self.method.get()]["params"]:
            rows.append([key, values.get(key, ""), label])
        return rows

    def calculate(self) -> None:
        try:
            method = self.method.get()
            values = self._params()
            f = lambda key: self._float_value(values, key)
            if method == "洪峰流量和洪水总量":
                flood = calculate_reservoir_flood(ReservoirFloodInput(f("F"), f("n"), f("K"), f("H24"), f("h")))
                mountain = calculate_mountain_flood_peak(MountainFloodInput(f("F"), f("S_m"), f("n_m"), f("K_m"), f("C_m")))
                rows = [
                    ["项目", "数值", "单位"],
                    ["暴雨力S", f"{flood.rainfall_intensity_mm_h:.2f}", "mm/h"],
                    ["经验法洪峰流量Q", f"{flood.peak_flow_m3s:.2f}", "m3/s"],
                    ["一次洪水总量W", f"{flood.flood_volume_m3:.0f}", "m3"],
                    ["山洪-无降雨资料", f"{mountain.no_rainfall_data_peak_m3s:.2f}", "m3/s"],
                    ["山洪-F<3公式", f"{mountain.rainfall_small_area_peak_m3s:.2f}", "m3/s"],
                    ["山洪-F>=3公式", f"{mountain.rainfall_large_area_peak_m3s:.2f}", "m3/s"],
                ]
            elif method == "水库淤积库容估算":
                result = calculate_reservoir_sedimentation(ReservoirSedimentationInput(f("F"), f("M"), f("T"), f("gamma")))
                rows = [["项目", "数值", "单位"], ["多年平均年输沙量W", f"{result.annual_sediment_t:.2f}", "t/a"], ["水库淤积库容V", f"{result.sediment_capacity_m3:.2f}", "m3"]]
            elif method == "水库年运用调节":
                result = calculate_reservoir_annual_regulation(
                    self._list(values, "months"), self._float_list(values, "inflows"), self._float_list(values, "demands"), f("capacity")
                )
                rows = [["月份", "来水", "用水", "余水", "亏水", "蓄水量", "弃水量", "备注"]]
                for item in result.rows:
                    rows.append(
                        [
                            item.month,
                            f"{item.inflow:.1f}",
                            f"{item.demand:.1f}",
                            f"{item.surplus:.1f}",
                            f"{item.deficit:.1f}",
                            f"{item.storage:.1f}",
                            f"{item.abandoned:.1f}",
                            item.note,
                        ]
                    )
                rows.append(["合计", "", "", f"{result.total_surplus:.1f}", f"{result.total_deficit:.1f}", "", f"{result.total_abandoned:.1f}", ""])
            else:
                raise HydrologyError(f"暂未接入该方法：{method}")
            info = self.METHOD_DEFS[method]
            lines = [
                f"{method}计算书",
                "",
                "一、公式来源",
                format_fixed_table([["项目", "内容"], ["来源", info["source"]], ["公式", info["formula"]]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(self._input_rows(values), min_width=16),
                "",
                "三、计算结果",
                format_fixed_table(rows, min_width=16),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class ReservoirStorageBookTabV2(ReservoirStorageBookTab):
    METHOD_DEFS = {
        **ReservoirStorageBookTab.METHOD_DEFS,
        "土石坝坝顶超高及外轮廓": {
            "source": "《水利大算盘》第五章第4、5、7节，式(5-9)至式(5-15)、式(5-19)，例题5-4。",
            "formula": "Δh=Rm+e+A；H=max(水位+Δh)；B=sqrt(H)；b=Hm上游+B+Hm下游；S=H/3+C",
            "params": [
                ("depths", "四种工况坝前水深，逗号分隔", "30.5,30,30.8,30"),
                ("winds", "四种工况风速，逗号分隔", "4.5,4.5,8,8"),
                ("safeties", "四种工况安全加高，逗号分隔", "0.7,0.7,0.4,0.4"),
                ("fetch", "风区长度D(m)", "3000"),
                ("angle", "风向与坝轴线法线夹角β(°)", "18"),
                ("wave_period", "波浪平均周期Tm(s)", "3"),
                ("upstream_slope", "上游坝坡系数m", "2.5"),
                ("downstream_slope", "下游坝坡系数m下游", "2"),
                ("kd", "坝坡渗透性及糙率系数kΔ", "0.8"),
                ("kw", "经验系数kW", "1.1"),
                ("curtain_C", "悬挂帷幕常数C(m)", "18"),
            ],
        },
        "大坝护坡厚度": {
            "source": "《水利大算盘》第五章第6节，式(5-16)至式(5-18)，例题5-5。",
            "formula": "d=1.018Kt*ρw/(ρk-ρw)*sqrt(m²+1)/(m(m+2))*hp；Lm/hp≤15时t=1.67d/Kt，否则t=1.82d/Kt",
            "params": [
                ("m", "坝坡系数m", "2.5"),
                ("Kt", "随坡率变化系数Kt", "1.3"),
                ("rho_k", "块石密度ρk(t/m3)", "2.65"),
                ("rho_w", "水密度ρw(t/m3)", "1"),
                ("wind", "最大风速W(m/s)", "15"),
                ("fetch", "风区长度D(m)", "17000"),
                ("wave_period", "平均波周期Tm(s)", "3"),
            ],
        },
        "坝基悬挂帷幕深度": {
            "source": "《水利大算盘》第五章第7节，式(5-19)。",
            "formula": "S=H/3+C",
            "params": [
                ("dam_height", "坝高H(m)", "32.029"),
                ("C", "常数C(m)，一般取8-25", "18"),
            ],
        },
        "水库库容": {
            "source": "《水利大算盘》第五章第8节，式(5-20)至式(5-22)，例题5-6。",
            "formula": "Vi=1/2*bi(A1i+A2i)；Vz=1/3*bAz；V=ΣVi+Vz",
            "params": [
                ("layers", "分层参数：高度:上面积:下面积；多层用分号分隔", "20:200000:180000;15:180000:160000"),
                ("cone_height", "底部锥体高度b(m)", "10"),
                ("cone_area", "锥体上部水面面积Az(m2)", "160000"),
            ],
        },
        "平面闸门启闭力": {
            "source": "《水利大算盘》第五章第9节，式(5-23)至式(5-30)，例题5-7。",
            "formula": "FQ=nT(Tzd+Tzs)+ps+nG2G+Gj+Ws；FW=nT(Tzd+Tzs)-nG1G+pt",
            "params": [
                ("bearing_type", "支承类型：滑动支承/滑动轴承/滚动轴承", "滑动支承"),
                ("pressure", "作用在闸门上的总水压力p(kN)", "302.15"),
                ("water_head", "闸门上游水头(m)", "7"),
                ("stopwater_pressure", "作用在止水上的水压力pzs(kN)", "49.66"),
                ("stopwater_friction", "止水摩擦系数f3", "0.65"),
                ("gate_weight", "闸门自重G(kN)", "7.76"),
                ("water_column_weight", "作用在闸门上的水柱重量Ws(kN)", "22.06"),
                ("uplift", "上托力pt(kN)", "9.26"),
            ],
        },
    }

    def _parse_capacity_layers(self, values: dict[str, str]) -> list[ReservoirCapacityLayer]:
        layers: list[ReservoirCapacityLayer] = []
        for raw in self._list(values, "layers"):
            parts = [part.strip() for part in raw.split(":")]
            if len(parts) != 3:
                raise HydrologyError("layers每层必须按 高度:上面积:下面积 填写")
            try:
                height, upper, lower = (float(part) for part in parts)
            except ValueError as exc:
                raise HydrologyError("layers中的高度和面积必须是数字") from exc
            layers.append(ReservoirCapacityLayer(height, upper, lower))
        return layers

    def calculate(self) -> None:
        try:
            method = self.method.get()
            values = self._params()
            f = lambda key: self._float_value(values, key)
            rows: list[list[str]]
            if method in ReservoirStorageBookTab.METHOD_DEFS:
                return super().calculate()
            if method == "土石坝坝顶超高及外轮廓":
                depths = self._float_list(values, "depths")
                winds = self._float_list(values, "winds")
                safeties = self._float_list(values, "safeties")
                if not (len(depths) == len(winds) == len(safeties)):
                    raise HydrologyError("depths、winds、safeties数量必须一致")
                case_rows = [["工况", "水深(m)", "风速(m/s)", "安全加高(m)", "波浪爬高(m)", "风壅高度(m)", "坝顶超高(m)", "坝体高度(m)"]]
                elevations: list[float] = []
                for index, (depth, wind, safety) in enumerate(zip(depths, winds, safeties), start=1):
                    result = calculate_earth_dam_freeboard(
                        EarthDamFreeboardInput(
                            water_depth_m=depth,
                            wind_speed_m_s=wind,
                            fetch_m=f("fetch"),
                            wind_water_axis_angle_deg=f("angle"),
                            wave_period_s=f("wave_period"),
                            upstream_slope=f("upstream_slope"),
                            slope_permeability_coefficient=f("kd"),
                            empirical_coefficient=f("kw"),
                            safety_margin_m=safety,
                        )
                    )
                    elevations.append(result.crest_elevation_m)
                    case_rows.append(
                        [
                            f"工况{index}",
                            f"{depth:.3f}",
                            f"{wind:.3f}",
                            f"{safety:.3f}",
                            f"{result.wave_runup_m:.3f}",
                            f"{result.wind_setup_m:.4f}",
                            f"{result.freeboard_m:.3f}",
                            f"{result.crest_elevation_m:.3f}",
                        ]
                    )
                dam_height = max(elevations)
                profile = calculate_earth_dam_profile(
                    EarthDamProfileInput(
                        dam_body_height_m=dam_height,
                        upstream_slope=f("upstream_slope"),
                        downstream_slope=f("downstream_slope"),
                        curtain_constant_m=f("curtain_C"),
                    )
                )
                rows = case_rows + [
                    ["", "", "", "", "", "", "", ""],
                    ["控制成果", "坝体高度", f"{dam_height:.3f}", "m", "", "", "", ""],
                    ["控制成果", "坝顶宽度", f"{profile.crest_width_m:.3f}", "m", "", "", "", ""],
                    ["控制成果", "坝底宽度", f"{profile.bottom_width_m:.3f}", "m", "", "", "", ""],
                    ["控制成果", "悬挂帷幕深度", f"{profile.curtain_depth_m:.3f}", "m", "", "", "", ""],
                ]
            elif method == "大坝护坡厚度":
                result = calculate_dam_slope_protection(
                    DamSlopeProtectionInput(
                        slope_coefficient=f("m"),
                        slope_change_coefficient=f("Kt"),
                        block_density_t_m3=f("rho_k"),
                        water_density_t_m3=f("rho_w"),
                        wind_speed_m_s=f("wind"),
                        fetch_m=f("fetch"),
                        wave_period_s=f("wave_period"),
                    )
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["波高hp", f"{result.wave_height_m:.3f}", "m"],
                    ["平均波长Lm", f"{result.wave_length_m:.3f}", "m"],
                    ["Lm/hp", f"{result.wave_steepness_ratio:.2f}", "-"],
                    ["石块换算球形直径d", f"{result.equivalent_stone_diameter_m:.3f}", "m"],
                    ["大坝护坡厚度t", f"{result.protection_thickness_m:.3f}", "m"],
                ]
            elif method == "坝基悬挂帷幕深度":
                result = calculate_earth_dam_profile(EarthDamProfileInput(f("dam_height"), 1.0, 1.0, f("C")))
                rows = [["项目", "数值", "单位"], ["悬挂帷幕深度S", f"{result.curtain_depth_m:.3f}", "m"]]
            elif method == "水库库容":
                result = calculate_reservoir_capacity(
                    ReservoirCapacityInput(
                        layers=self._parse_capacity_layers(values),
                        cone_height_m=f("cone_height"),
                        cone_top_area_m2=f("cone_area"),
                    )
                )
                rows = [["项目", "数值", "单位"]]
                for index, volume in enumerate(result.layer_volumes_m3, start=1):
                    rows.append([f"第{index}层柱体库容", f"{volume:.2f}", "m3"])
                rows.extend(
                    [
                        ["底部锥体库容", f"{result.cone_volume_m3:.2f}", "m3"],
                        ["水库设计总库容", f"{result.total_capacity_m3:.2f}", "m3"],
                        ["水库设计总库容", f"{result.total_capacity_m3 / 10000.0:.3f}", "10^4 m3"],
                    ]
                )
            elif method == "平面闸门启闭力":
                result = calculate_plane_gate_force(
                    PlaneGateForceInput(
                        pressure_on_gate_kN=f("pressure"),
                        water_head_m=f("water_head"),
                        bearing_type=values.get("bearing_type", "滑动支承").strip(),
                        stopwater_pressure_kN=f("stopwater_pressure"),
                        stopwater_friction=f("stopwater_friction"),
                        gate_weight_kN=f("gate_weight"),
                        water_column_weight_kN=f("water_column_weight"),
                        uplift_kN=f("uplift"),
                    )
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["支承摩擦阻力Tzd", f"{result.support_friction_kN:.2f}", "kN"],
                    ["止水摩擦阻力Tzs", f"{result.stopwater_friction_kN:.2f}", "kN"],
                    ["闸门启门力FQ", f"{result.opening_force_kN:.2f}", "kN"],
                    ["闸门启门力", f"{result.opening_force_t:.2f}", "t"],
                    ["闸门闭门力FW", f"{result.closing_force_kN:.2f}", "kN"],
                    ["闸门闭门力", f"{result.closing_force_t:.2f}", "t"],
                    ["启闭机建议容量", f"{result.selected_capacity_t:.2f}", "t"],
                ]
            else:
                raise HydrologyError(f"暂未接入该方法：{method}")
            info = self.METHOD_DEFS[method]
            lines = [
                f"{method}计算书",
                "",
                "一、公式来源",
                format_fixed_table([["项目", "内容"], ["来源", info["source"]], ["公式", info["formula"]]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(self._input_rows(values), min_width=16),
                "",
                "三、计算结果",
                format_fixed_table(rows, min_width=16),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class HydropowerProgramTab(WaterworksDomainProgramTab):
    title = "水电站压力管道与机电相关程序"
    description = "归并水能、装机容量、压力管道直径壁厚、最小淹没水深、通气管、水锤、水头损失、输电线路和负荷预测等程序。"
    chapter_prefixes = ("第四章",)


class HydropowerBookTab(ttk.Frame, FormMixin):
    METHOD_DEFS = {
        "水电站水能出力": {
            "source": "《水利大算盘》第四章第1节，式(4-1)至式(4-4)，例题4-1。",
            "formula": "H=H总-h损；N=KQH；Nω=9.81QH；N=9.81QHη",
            "params": [
                ("Q", "引水流量Q(m3/s)", "4"),
                ("H_total", "总水头(m)", "60"),
                ("loss", "总水头损失(m)", "1.3"),
                ("eta", "水轮机效率η(%)", "85"),
                ("K", "水电站出力系数K", "7.5"),
            ],
        },
        "装机容量初选": {
            "source": "《水利大算盘》第四章第2节，式(4-5)至式(4-9)，例题4-2前半部分。",
            "formula": "Np=kQH；Ny=aNp；Ny=E年/T装",
            "params": [
                ("Q", "设计保证率流量Q(m3/s)", "1"),
                ("H", "工作水头H(m)", "60"),
                ("K", "机组出力系数k", "7.5"),
                ("a", "保证出力倍率法经验系数a", "1"),
                ("E", "多年平均发电量E年(万kWh)", "315.4"),
                ("T", "装机容量年利用小时数T装(h)", "6000"),
            ],
        },
        "装机容量经济比较": {
            "source": "《水利大算盘》第四章第2节，式(4-10)至式(4-12)，例题4-2后半部分。",
            "formula": "k1=m1E年；k2=m2M；k=k1-k2；还本年限=(M+k2)/k",
            "params": [
                ("capacities", "备选装机容量(kW)，逗号分隔", "380,450,600,750,900"),
                ("investments", "建站估算总投资(万元)，逗号分隔", "1200,1500,1800,2100,2500"),
                ("generations", "多年平均发电量(万kWh)，逗号分隔", "261.6,294.3,315.4,315.4,315.4"),
                ("price", "单位电价(元/kWh)", "0.5"),
                ("op_rate", "年运行费率(%)", "6"),
                ("load", "供电区域设计水平年负荷(kW)", "400"),
            ],
        },
        "压力管道直径和壁厚": {
            "source": "《水利大算盘》第四章第3节，式(4-13)至式(4-16)，例题4-3。",
            "formula": "D=(kQmax^3/H)^(1/7)；D=sqrt(4Q/(πv))；δ=100DH/(2φ[σ])；δ≥D/130；δ设=max(δ计+2,6)",
            "params": [
                ("Q", "管道设计流量Q(m3/s)", "4"),
                ("Qmax", "管道最大设计流量Qmax(m3/s)", "4"),
                ("v", "管道经济流速v(m/s)", "3.5"),
                ("H", "设计水头H(m)", "40"),
                ("Hmax", "最大设计水头(m)", "40"),
                ("K", "彭德舒公式系数k", "5.2"),
                ("phi", "钢管焊缝系数φ", "0.9"),
                ("sigma", "钢板允许应力[σ](kg/cm2)", "1300"),
            ],
        },
        "管道进口最小淹没水深": {
            "source": "《水利大算盘》第四章第4节，式(4-17)，例题4-4。",
            "formula": "Hmin=1.414D*sinδ*(Z/q^(2/3))^0.31；圆管q=Q/D",
            "params": [
                ("D", "管道内径D(m)", "1.206"),
                ("Q", "管中流量Q(m3/s)", "4"),
                ("Z_up", "管道上游水位(m)", "4000"),
                ("Z_down", "管道下游水位(m)", "3940"),
                ("angle", "管道轴线与水平线夹角δ(度)", "30"),
            ],
        },
        "通气管面积直径": {
            "source": "《水利大算盘》第四章第5节，式(4-18)、式(4-19)，图4-6。",
            "formula": "F=Q0/(1265C√ΔP)；ΔP=2E/K*(t/D0)^3；d=sqrt(4F/π)",
            "params": [
                ("D0", "管道内径D0(m)", "1.5"),
                ("Q0", "通气量Q0(m3/s)", "10.6"),
                ("C", "通气孔流量系数C", "0.7"),
                ("E", "管壁材料弹性模数E(MPa)", "210000"),
                ("t", "管壁厚度t(m)", "0.01"),
                ("K", "管道安全系数K", "10"),
            ],
        },
        "压力管道水锤压力": {
            "source": "《水利大算盘》第四章第6节，式(4-20)至式(4-26)，例题4-5。",
            "formula": "a=1435/sqrt(1+DK/(δE))；TR=2L/a；间接ζ=σ/2(√(σ²+4)+σ)",
            "params": [
                ("D", "管道内径D(m)", "0.6"),
                ("delta", "管壁厚度δ(m)", "0.02"),
                ("E", "管壁材料弹性模数E(MPa)", "210000"),
                ("K", "水的体积弹性模数K(MPa)", "2100"),
                ("L", "管道长度L(m)", "450"),
                ("Ts", "阀门或导叶启闭时间Ts(s)", "5"),
                ("V0", "启闭前流速V0(m/s)", "1.3"),
                ("V", "启闭后流速V(m/s)", "0"),
                ("H0", "恒定水头H0(m)", "36.5"),
            ],
        },
        "压力管道水头损失": {
            "source": "《水利大算盘》第四章第7节，式(4-27)，例题4-6。",
            "formula": "h=λLv²/(2gd)+Σζv²/(2g)，λ按雷诺数与粗糙度计算并按书中表格口径取三位小数。",
            "params": [
                ("v", "管中流速v(m/s)", "3"),
                ("d", "管道直径d(m)", "0.6"),
                ("L", "管道长度L(m)", "450"),
                ("roughness", "管壁粗糙度Δ(mm)", "0.1"),
                ("zeta", "局部损失系数Σζ", "2.661"),
            ],
        },
        "压力管道临界外压": {
            "source": "《水利大算盘》第四章第8节，式(4-28)、式(4-29)，图4-9。",
            "formula": "明管pcr=2E(δ/D0)^3；埋管pcr=3440(δ/D0)^1.7σs^0.25",
            "params": [
                ("delta", "管壁计算厚度δ(m)", "0.008"),
                ("D0", "钢管内径D0(m)", "1.7"),
                ("sigma_s", "钢材屈服强度σs(MPa)", "240"),
                ("E", "钢材弹模E(MPa)", "201000"),
            ],
        },
        "架空输电线路导线截面": {
            "source": "《水利大算盘》第四章第9节，式(4-30)、式(4-31)，例题4-7。",
            "formula": "S=P/(√3JUe cosφ)；I=P/(√3Ue cosφ)，并按表4-3校验导线长期允许载流量。",
            "params": [
                ("P", "最大输送电容量P(kW)", "1000"),
                ("U", "线路额定电压Ue(kV)", "35"),
                ("J", "经济电流密度J(A/mm2)", "0.9"),
                ("cos", "功率因数cosφ", "0.8"),
            ],
        },
        "主变压器总容量": {
            "source": "《水利大算盘》第四章第10节，式(4-32)，图4-11。",
            "formula": "S=(ΣP-Pmin)/cosφ≤Sn",
            "params": [
                ("P_total", "发电机组总有功功率ΣP(kW)", "450"),
                ("P_min", "发电机母线侧最小负荷Pmin(kW)", "20"),
                ("cos", "发电机额定功率因数cosφ", "0.8"),
                ("Sn", "主变压器标准容量总和Sn(kVA)", "1300"),
            ],
        },
        "负荷预测": {
            "source": "《水利大算盘》第四章第11节，式(4-33)至式(4-35)，图4-12。",
            "formula": "Pm=kzzΣPz；E=PmTm；An=A1(1+i)^n；供电最大负荷=用电最大负荷/(1-网损率)；发电最大负荷=供电最大负荷/(1-厂用电率)",
            "params": [
                ("Pz", "电网总负荷装置功率ΣPz(kW)", "1104"),
                ("kzz", "综合需用系数kzz", "0.3"),
                ("Tm", "年最大负荷利用小时数Tm(h)", "2800"),
                ("A1", "基准年负荷A1(kW)", "300"),
                ("i", "年平均增长率i(%)", "10"),
                ("n", "设计水平年n", "1"),
                ("grid_loss", "网损率(%)", "12"),
                ("plant_use", "厂用电率(%)", "1"),
            ],
        },
    }

    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        top = ttk.LabelFrame(self, text="水电站机电与压力管道书中程序")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(1, weight=1)
        ttk.Label(top, text="计算程序").grid(row=0, column=0, sticky="e", padx=(10, 4), pady=6)
        self.method = tk.StringVar(value="水电站水能出力")
        method_box = ttk.Combobox(top, textvariable=self.method, values=tuple(self.METHOD_DEFS), state="readonly", width=28)
        method_box.grid(row=0, column=1, sticky="w", padx=(0, 12), pady=6)
        method_box.bind("<<ComboboxSelected>>", lambda _event: self._load_method())
        self.source_label = ttk.Label(top, text="", foreground="#555555", wraplength=980, justify=tk.LEFT)
        self.source_label.grid(row=1, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 4))
        self.formula_label = ttk.Label(top, text="", foreground="#333333", wraplength=980, justify=tk.LEFT)
        self.formula_label.grid(row=2, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 6))
        params_box = ttk.LabelFrame(top, text="输入参数：按 key=value 修改，默认值为书中例题")
        params_box.grid(row=3, column=0, columnspan=3, sticky="ew", padx=10, pady=6)
        params_box.columnconfigure(0, weight=1)
        self.params_text = ScrolledText(params_box, height=9, width=100, font=(DASHBOARD_MONO_FONT, 10))
        self.params_text.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.param_note = ttk.Label(params_box, text="", foreground="#666666", wraplength=1000, justify=tk.LEFT)
        self.param_note.grid(row=1, column=0, sticky="ew", padx=8, pady=(0, 8))
        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, columnspan=3, sticky="ew", padx=10, pady=(4, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "恢复本方法例题", self._load_method).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))
        self.output = self._result_box(self)
        self._load_method()

    def _load_method(self) -> None:
        info = self.METHOD_DEFS[self.method.get()]
        self.source_label.configure(text=f"来源：{info['source']}")
        self.formula_label.configure(text=f"公式：{info['formula']}")
        self.params_text.delete("1.0", tk.END)
        self.params_text.insert(tk.END, "\n".join(f"{key}={default}" for key, _label, default in info["params"]))
        self.param_note.configure(text="；".join(f"{key}：{label}" for key, label, _default in info["params"]))

    def _params(self) -> dict[str, str]:
        values: dict[str, str] = {}
        for raw_line in self.params_text.get("1.0", "end-1c").splitlines():
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                raise HydrologyError(f"参数行必须按 key=value 填写：{line}")
            key, value = line.split("=", 1)
            values[key.strip()] = value.strip()
        return values

    def _float(self, values: dict[str, str], key: str) -> float:  # type: ignore[override]
        if key not in values:
            raise HydrologyError(f"缺少参数：{key}")
        try:
            return float(values[key])
        except ValueError as exc:
            raise HydrologyError(f"{key} 必须是数字") from exc

    def _float_list(self, values: dict[str, str], key: str) -> list[float]:
        if key not in values:
            raise HydrologyError(f"缺少参数：{key}")
        try:
            return [float(item.strip()) for item in values[key].split(",") if item.strip()]
        except ValueError as exc:
            raise HydrologyError(f"{key} 必须是逗号分隔的数字") from exc

    def _input_rows(self, values: dict[str, str]) -> list[list[str]]:
        rows = [["参数", "输入值", "说明"]]
        for key, label, _default in self.METHOD_DEFS[self.method.get()]["params"]:
            rows.append([key, values.get(key, ""), label])
        return rows

    def calculate(self) -> None:
        try:
            method = self.method.get()
            values = self._params()
            f = lambda key: self._float(values, key)
            if method == "水电站水能出力":
                result = calculate_hydropower_output(
                    HydropowerOutputInput(f("Q"), f("H_total"), f("loss"), f("eta"), f("K"))
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["工作水头H", f"{result.working_head:.2f}", "m"],
                    ["水电站出力N", f"{result.station_output_kw:.2f}", "kW"],
                    ["水轮机输入功率Nω", f"{result.turbine_input_kw:.2f}", "kW"],
                    ["水轮机输出功率N", f"{result.turbine_output_kw:.2f}", "kW"],
                ]
            elif method == "装机容量初选":
                result = calculate_installed_capacity(
                    InstalledCapacityInput(f("Q"), f("H"), f("K"), f("a"), f("E"), f("T"))
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["保证出力Np", f"{result.guarantee_output_kw:.2f}", "kW"],
                    ["保证出力倍率法初选Ny", f"{result.capacity_by_ratio_kw:.2f}", "kW"],
                    ["年利用小时数法初选Ny", f"{result.capacity_by_utilization_hours_kw:.2f}", "kW"],
                ]
            elif method == "装机容量经济比较":
                result = calculate_candidate_economics(
                    CandidateEconomicInput(
                        self._float_list(values, "capacities"),
                        self._float_list(values, "investments"),
                        self._float_list(values, "generations"),
                        f("price"),
                        f("op_rate"),
                        f("load"),
                    )
                )
                rows = [["装机(kW)", "投资(万元)", "年发电量(万kWh)", "年净收入(万元)", "还本年限(年)", "是否满足负荷"]]
                for item in result.rows:
                    rows.append(
                        [
                            f"{item.capacity_kw:.0f}",
                            f"{item.investment_10k_yuan:.2f}",
                            f"{item.annual_generation_10k_kwh:.2f}",
                            f"{item.net_revenue_10k_yuan:.2f}",
                            "不可还本" if not math.isfinite(item.payback_years) else f"{item.payback_years:.1f}",
                            "是" if item.meets_load else "否",
                        ]
                    )
                rows.append(["推荐装机", "-" if result.selected_capacity_kw is None else f"{result.selected_capacity_kw:.0f}", "kW", "", "", ""])
            elif method == "压力管道直径和壁厚":
                result = calculate_pressure_pipe_hydropower(
                    PressurePipeHydropowerInput(f("Q"), f("Qmax"), f("v"), f("H"), f("Hmax"), f("K"), f("phi"), f("sigma"))
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["彭德舒公式经济直径", f"{result.diameter_by_beyer_m:.3f}", "m"],
                    ["经济流速法直径", f"{result.diameter_by_velocity_m:.3f}", "m"],
                    ["强度计算厚度", f"{result.strength_thickness_mm:.3f}", "mm"],
                    ["最小厚度要求", f"{result.minimum_thickness_mm:.3f}", "mm"],
                    ["管壁计算厚度", f"{result.calculated_thickness_mm:.3f}", "mm"],
                    ["管壁设计厚度", f"{result.design_thickness_mm:.3f}", "mm"],
                ]
            elif method == "管道进口最小淹没水深":
                result = calculate_pipe_minimum_submergence(
                    PipeSubmergenceInput(f("D"), f("Q"), f("Z_up"), f("Z_down"), f("angle"))
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["管内单宽流量q", f"{result.unit_width_flow:.3f}", "m3/(s*m)"],
                    ["上下游水位差Z", f"{result.water_level_difference:.3f}", "m"],
                    ["最小淹没水深Hmin", f"{result.minimum_submergence_depth:.3f}", "m"],
                ]
            elif method == "通气管面积直径":
                result = calculate_vent_pipe(VentPipeInput(f("Q0"), f("C"), f("E"), f("K"), f("t"), f("D0")))
                rows = [
                    ["项目", "数值", "单位"],
                    ["管道内允许负压ΔP", f"{result.allowable_negative_pressure_mpa:.4f}", "MPa"],
                    ["通气孔面积F", f"{result.vent_area_m2:.4f}", "m2"],
                    ["通气管直径d", f"{result.vent_diameter_m * 100:.2f}", "cm"],
                ]
            elif method == "压力管道水锤压力":
                result = calculate_water_hammer(
                    WaterHammerInput(f("D"), f("delta"), f("E"), f("K"), f("L"), f("Ts"), f("V0"), f("V"), f("H0"))
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["水锤传播速度a", f"{result.wave_velocity:.1f}", "m/s"],
                    ["水锤波传播时间TR", f"{result.reflection_time:.3f}", "s"],
                    ["水锤类型", result.hammer_type, "-"],
                    ["直接水锤压力水头", f"{result.direct_head:.2f}", "m"],
                    ["间接水锤常数σ", f"{result.sigma:.3f}", "-"],
                    ["最大水击压强水头ΔH", f"{result.indirect_head:.2f}", "m"],
                    ["最大水击压强", f"{result.maximum_pressure_kn_m2:.1f}", "kN/m2"],
                    ["规范允许水锤压力值", f"{result.allowable_head:.1f}", "m"],
                ]
            elif method == "压力管道水头损失":
                result = calculate_pipe_head_loss(PipeHeadLossInput(f("v"), f("d"), f("L"), f("roughness"), f("zeta")))
                rows = [
                    ["项目", "数值", "单位"],
                    ["雷诺数Re", f"{result.reynolds_number:.1f}", "-"],
                    ["沿程水头损失系数λ", f"{result.friction_factor:.3f}", "-"],
                    ["沿程水头损失", f"{result.friction_loss:.3f}", "m"],
                    ["局部水头损失", f"{result.local_loss:.3f}", "m"],
                    ["压力管道总水头损失", f"{result.total_loss:.3f}", "m"],
                ]
            elif method == "压力管道临界外压":
                result = calculate_external_pressure(ExternalPressureInput(f("delta"), f("D0"), f("sigma_s"), f("E")))
                rows = [
                    ["项目", "数值", "单位"],
                    ["明钢管临界外压力", f"{result.exposed_pipe_kpa:.2f}", "kPa"],
                    ["埋钢管临界外压力", f"{result.buried_pipe_mpa:.2f}", "MPa"],
                ]
            elif method == "架空输电线路导线截面":
                result = calculate_transmission_conductor(TransmissionConductorInput(f("P"), f("U"), f("J"), f("cos")))
                rows = [
                    ["项目", "数值", "单位"],
                    ["经济导线截面S", f"{result.economic_section_mm2:.2f}", "mm2"],
                    ["推荐导线型号", result.recommended_conductor or "需人工放大选型", "-"],
                    ["导线持续允许电流校核值", f"{result.sustained_current_a:.2f}", "A"],
                    ["推荐型号80度长期允许载流量", "-" if result.selected_allowable_current_a is None else f"{result.selected_allowable_current_a:.0f}", "A"],
                    ["校验结论", "满足" if result.is_selected_sufficient else "不满足或需人工校验", "-"],
                ]
            elif method == "主变压器总容量":
                result = calculate_transformer_capacity(TransformerCapacityInput(f("P_total"), f("P_min"), f("cos"), f("Sn")))
                rows = [
                    ["项目", "数值", "单位"],
                    ["主变压器所需容量S", f"{result.required_capacity_kva:.2f}", "kVA"],
                    ["标准容量是否满足", "满足" if result.is_standard_sufficient else "不满足", "-"],
                ]
            elif method == "负荷预测":
                result = calculate_load_forecast(
                    LoadForecastInput(f("Pz"), f("kzz"), f("Tm"), f("A1"), f("i"), f("n"), f("grid_loss"), f("plant_use"))
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["综合需用系数法系统最大负荷Pm", f"{result.max_load_by_demand_kw:.1f}", "kW"],
                    ["综合需用系数法年用电量E", f"{result.annual_energy_by_demand_kwh:.1f}", "kWh"],
                    ["平均增长率法设计水平年负荷", f"{result.load_by_growth_kw:.1f}", "kW"],
                    ["用电最大负荷", f"{result.selected_user_max_load_kw:.1f}", "kW"],
                    ["系统供电最大负荷", f"{result.system_supply_max_load_kw:.1f}", "kW"],
                    ["系统发电最大负荷", f"{result.system_generation_max_load_kw:.1f}", "kW"],
                ]
            else:
                raise HydrologyError(f"暂未接入该方法：{method}")
            info = self.METHOD_DEFS[method]
            lines = [
                f"{method}计算书",
                "",
                "一、公式来源",
                format_fixed_table([["项目", "内容"], ["来源", info["source"]], ["公式", info["formula"]]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(self._input_rows(values), min_width=16),
                "",
                "三、计算结果",
                format_fixed_table(rows, min_width=16),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class HydraulicStructureProgramTab(WaterworksDomainProgramTab):
    title = "水工建筑物与桥涵泄流相关程序"
    description = "归并水闸、溢流堰、溢洪道泄槽、消力池、陡坡、跌水、渡槽、倒虹吸、桥孔、隧洞、涵洞和侧堰等程序。"
    chapter_prefixes = ("第六章",)
    existing_methods = ("水工建筑物书中程序", "堰流公式水位", "桥孔最小净长度")


class HydraulicStructureBookTab(ttk.Frame, FormMixin):
    METHOD_DEFS = {
        "水闸水力计算": {
            "source": "《水利大算盘》第六章第1节，式(6-1)至式(6-5)，例题6-1。",
            "formula": "Q=sigma*epsilon*m*n*b*sqrt(2g)*H0^(3/2); L=(H0+a)(1+0.1DeltaH)K; W=3.5Hsqrt(b0)+5; D=W/k",
            "params": [("Q", "过闸流量Q(m3/s)", "12"), ("H0", "上游总水头H0(m)", "2.3"), ("Hd", "下游水头(m)", "2.1"), ("Hg", "闸孔设计水头H(m)", "2.0"), ("a", "安全加高a(m)", "0.4"), ("m", "流量系数m", "0.4")],
        },
        "正槽式溢洪道泄槽": {
            "source": "《水利大算盘》第六章第3节，式(6-12)至式(6-22)，例题6-3。",
            "formula": "Q=A*C*sqrt(R*i); hb=(1+zeta*v/100)h; Delta=0.61+0.0372*v*h^(1/3); H=hb+Delta",
            "params": [("Q", "泄槽流量Q(m3/s)", "30"), ("B", "泄槽底宽B(m)", "5"), ("i", "泄槽底坡i", "0.002"), ("n", "糙率n", "0.015"), ("zeta", "掺气系数zeta", "1")],
        },
        "消力池和海漫": {
            "source": "《水利大算盘》第六章第4节，式(6-23)至式(6-36)，例题6-4。",
            "formula": "h2/h1=0.5*(sqrt(1+8q^2/(g*h1^3))-1); d=sigma0*h2-ht-DeltaZ; L=0.8*6.9(h2-h1)",
            "params": [("Q", "流量Q(m3/s)", "28"), ("B", "池宽B(m)", "3"), ("H", "上游水深H(m)", "3.3"), ("v", "行近流速v(m/s)", "0.67"), ("P", "跌坎高度P(m)", "4"), ("ht", "下游水深ht(m)", "2.9")],
        },
        "陡坡水力计算": {
            "source": "《水利大算盘》第六章第5节，式(6-37)至式(6-47)，例题6-5。",
            "formula": "入口宽度按堰流式反算，L渐=3h加；陡坡槽身按曼宁公式，消能段按水跃公式联算。",
            "params": [("Q", "流量Q(m3/s)", "5"), ("H0", "进口水头h0(m)", "1.2"), ("Hadd", "加大水深h加(m)", "1.3"), ("i", "陡坡坡降i", "0.02"), ("n", "糙率n", "0.015"), ("ht", "下游水深ht(m)", "1.2"), ("P", "落差P(m)", "20")],
        },
        "跌水水力计算": {
            "source": "《水利大算盘》第六章第6节，式(6-48)至式(6-63)，例题6-6。",
            "formula": "D=q^2/(gP^3); ld=4.3D^0.27P; hc0=1.66D^0.27P; hc=0.54D^0.425P; ls=ld+0.8(1.9hc0-hc)",
            "params": [("Q", "流量Q(m3/s)", "5"), ("Bdown", "下游渠道底宽(m)", "2.8"), ("H0", "进口加大水头(m)", "1.59"), ("Hadd", "加大水深h加(m)", "1.59"), ("P", "单级跌差P(m)", "4.8")],
        },
        "引水渡槽水力计算": {
            "source": "《水利大算盘》第六章第7节，式(6-64)至式(6-80)，例题6-8。",
            "formula": "DeltaZ1=(1+zeta)(v2^2-v1^2)/(2g); DeltaZ2=iL; DeltaZ3=DeltaZ1/3; DeltaZ=DeltaZ1+DeltaZ2-DeltaZ3",
            "params": [("Q", "渡槽流量Q(m3/s)", "10"), ("L", "槽身长度L(m)", "200"), ("i", "槽底比降i", "0.001"), ("n", "糙率n", "0.015"), ("B1", "上游渠道底宽B1(m)", "3.8"), ("h1", "上游渠道水深h1(m)", "1.845"), ("v1", "上游渠道流速v1(m/s)", "1.426"), ("Z1", "上游渠底高程(m)", "4500"), ("B2", "槽身底宽B2(m)", "3.2")],
        },
        "倒虹吸管水力计算": {
            "source": "《水利大算盘》第六章第8节，式(6-81)至式(6-99)，例题6-9。",
            "formula": "D=sqrt(4Q/(pi*v)); hw=(sum(zeta)+lambda*L/D)*v^2/(2g); delta=1000*Hp*D/(2[sigma]); Hd=Hu+hu-hd-hw",
            "params": [("Q", "倒虹吸流量Q(m3/s)", "5"), ("v", "管内设计流速v(m/s)", "1.3"), ("L", "管长L(m)", "450"), ("zeta", "局部损失系数和", "8.32"), ("lambda", "沿程阻力系数lambda", "0.006"), ("Zu", "上游渠底高程(m)", "4000"), ("hu", "上游水深(m)", "2"), ("hd", "下游水深(m)", "2"), ("sigma", "允许应力(kg/cm2)", "1300"), ("Hp", "设计水头Hp(m)", "13.1")],
        },
        "桥孔水力计算": {
            "source": "《水利大算盘》第六章第9节，式(6-100)，表6-3、表6-4，例题6-10。",
            "formula": "Q=mu*omega*sqrt(2g*DeltaZ0); omega=Bh0; H桥=h0+安全超高+桥面厚",
            "params": [("Q", "过桥流量Q(m3/s)", "20"), ("h0", "上游渠道正常水深h0(m)", "2.5"), ("a", "安全超高(m)", "0.3"), ("dz", "桥上游壅高水头(m)", "0.05"), ("mu1", "进口出口形式流量系数mu1", "0.9"), ("mu2", "桥墩形状流量系数mu2", "0.92")],
        },
        "隧洞水力计算": {
            "source": "《水利大算盘》第六章第10节，式(6-101)至式(6-104)，例题6-11。",
            "formula": "无压城门洞形：Q=m*sigma*b*sqrt(2g)*H0^(3/2)，r=0.5b/sin(alpha/2)；圆形：d=1.57(nQ/sqrt(i))^(3/8)。",
            "params": [("mode", "类型：无压城门洞形/无压圆形/有压矩形", "无压城门洞形"), ("Q", "过洞流量Q(m3/s)", "10"), ("H0", "上游总水头H0(m)", "3"), ("mu", "流量系数m或mu", "0.36"), ("sigma", "淹没系数sigma", "0.8"), ("ratio", "洞高宽比", "1.5"), ("angle", "拱中心角(度)", "120"), ("n", "糙率n", "0.015"), ("i", "底坡i", "0.001")],
        },
        "涵洞水力计算": {
            "source": "《水利大算盘》第六章第11节，式(6-105)至式(6-107)，例题6-12。",
            "formula": "无压箱涵：Q=m*sigma*b*sqrt(2g)*H0^(3/2)；有压箱涵：Q=mu*omega*sqrt(2g(H0+il-na))。",
            "params": [("mode", "类型：无压箱涵/有压箱涵", "无压箱涵"), ("Q", "过涵流量Q(m3/s)", "5"), ("H0", "涵洞上游总水头H0(m)", "2"), ("mu", "流量系数m或mu", "0.36"), ("sigma", "淹没系数sigma", "0.98"), ("ratio", "涵洞高宽比", "1.5")],
        },
        "侧堰水力计算": {
            "source": "《水利大算盘》第六章第12节，式(6-108)至式(6-110)，例题6-13。",
            "formula": "锐角分水：Q=m(1-v1/sqrt(g*h1)sin(alpha))*b*sqrt(2g)*H1^(3/2)；直角分水：Q=C*b*sqrt(2g)*H^(3/2)。",
            "params": [("mode", "类型：锐角分水/直角分水", "锐角分水"), ("Q", "侧堰流量Q(m3/s)", "10"), ("m", "流量系数m或C", "0.45"), ("v1", "侧堰首端平均流速v1(m/s)", "1.3"), ("h1", "侧堰首端水深h1(m)", "1.8"), ("angle", "分洪或引水角度(度)", "30"), ("H1", "侧堰首端堰顶水头H1(m)", "1"), ("H2", "侧堰末端堰顶水头H2(m)", "1")],
        },
    }

    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master)
        self.last_output = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        top = ttk.LabelFrame(self, text="水工建筑物书中程序")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(1, weight=1)
        ttk.Label(top, text="计算程序").grid(row=0, column=0, sticky="e", padx=(10, 4), pady=6)
        self.method = tk.StringVar(value="水闸水力计算")
        method_box = ttk.Combobox(top, textvariable=self.method, values=tuple(self.METHOD_DEFS), state="readonly", width=30)
        method_box.grid(row=0, column=1, sticky="w", padx=(0, 12), pady=6)
        method_box.bind("<<ComboboxSelected>>", lambda _event: self._load_method())
        self.source_label = ttk.Label(top, text="", foreground="#555555", wraplength=980, justify=tk.LEFT)
        self.source_label.grid(row=1, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 4))
        self.formula_label = ttk.Label(top, text="", foreground="#333333", wraplength=980, justify=tk.LEFT)
        self.formula_label.grid(row=2, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 6))
        params_box = ttk.LabelFrame(top, text="输入参数：按 key=value 修改，默认值为书中例题")
        params_box.grid(row=3, column=0, columnspan=3, sticky="ew", padx=10, pady=6)
        params_box.columnconfigure(0, weight=1)
        self.params_text = ScrolledText(params_box, height=9, width=100, font=(DASHBOARD_MONO_FONT, 10))
        self.params_text.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.param_note = ttk.Label(params_box, text="", foreground="#666666", wraplength=1000, justify=tk.LEFT)
        self.param_note.grid(row=1, column=0, sticky="ew", padx=8, pady=(0, 8))
        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, columnspan=3, sticky="ew", padx=10, pady=(4, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "恢复本方法例题", self._load_method).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))
        self.output = self._result_box(self)
        self._load_method()

    def _load_method(self) -> None:
        info = self.METHOD_DEFS[self.method.get()]
        self.source_label.configure(text=f"来源：{info['source']}")
        self.formula_label.configure(text=f"公式：{info['formula']}")
        self.params_text.delete("1.0", tk.END)
        self.params_text.insert(tk.END, "\n".join(f"{key}={default}" for key, _label, default in info["params"]))
        self.param_note.configure(text="；".join(f"{key}：{label}" for key, label, _default in info["params"]))

    def _params(self) -> dict[str, str]:
        values: dict[str, str] = {}
        for raw_line in self.params_text.get("1.0", "end-1c").splitlines():
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                raise HydrologyError(f"参数行必须按 key=value 填写：{line}")
            key, value = line.split("=", 1)
            values[key.strip()] = value.strip()
        return values

    def _float_value(self, values: dict[str, str], key: str) -> float:
        if key not in values:
            raise HydrologyError(f"缺少参数：{key}")
        try:
            return float(values[key])
        except ValueError as exc:
            raise HydrologyError(f"{key} 必须是数字") from exc

    def _input_rows(self, values: dict[str, str]) -> list[list[str]]:
        rows = [["参数", "输入值", "说明"]]
        for key, label, _default in self.METHOD_DEFS[self.method.get()]["params"]:
            rows.append([key, values.get(key, ""), label])
        return rows

    def calculate(self) -> None:
        try:
            method = self.method.get()
            values = self._params()
            f = lambda key: self._float_value(values, key)
            rows = [["项目", "数值", "单位"]]
            if method == "水闸水力计算":
                result = calculate_sluice_hydraulic(SluiceHydraulicInput(f("Q"), f("H0"), f("Hd"), f("Hg"), f("a"), f("m")))
                rows += [["闸孔总净宽", f"{result.total_clear_width_m:.3f}", "m"], ["闸室长度", f"{result.chamber_length_m:.3f}", "m"], ["门槽宽度", f"{result.groove_width_cm:.2f}", "cm"], ["门槽深度", f"{result.groove_depth_cm:.2f}", "cm"]]
            elif method == "正槽式溢洪道泄槽":
                result = calculate_spillway_chute(SpillwayChuteInput(f("Q"), f("B"), f("i"), f("n"), aeration_coefficient=f("zeta")))
                rows += [["正常水深", f"{result.normal_depth_m:.3f}", "m"], ["流速", f"{result.velocity_m_s:.3f}", "m/s"], ["掺气水深", f"{result.aerated_depth_m:.3f}", "m"], ["边墙高度", f"{result.sidewall_height_m:.3f}", "m"]]
            elif method == "消力池和海漫":
                result = calculate_stilling_basin(StillingBasinInput(f("Q"), f("B"), f("H"), f("v"), f("P"), f("ht")))
                rows += [["跃前水深", f"{result.first_conjugate_depth_m:.3f}", "m"], ["跃后水深", f"{result.second_conjugate_depth_m:.3f}", "m"], ["消力池深度", f"{result.basin_depth_m:.3f}", "m"], ["消力池长度", f"{result.basin_length_m:.3f}", "m"], ["边墙高度", f"{result.sidewall_height_m:.3f}", "m"], ["底板厚度", f"{result.slab_thickness_m:.3f}", "m"], ["海漫长度", f"{result.apron_length_m:.3f}", "m"], ["防冲槽深度", f"{result.trench_depth_m:.3f}", "m"]]
            elif method == "陡坡水力计算":
                result = calculate_steep_slope(SteepSlopeInput(f("Q"), f("H0"), f("Hadd"), f("i"), f("n"), f("ht"), f("P")))
                rows += [["入口底宽", f"{result.entrance_bottom_width_m:.3f}", "m"], ["渐变段长度", f"{result.transition_length_m:.3f}", "m"], ["陡坡水深", f"{result.chute_depth_m:.3f}", "m"], ["陡坡流速", f"{result.chute_velocity_m_s:.3f}", "m/s"], ["掺气水深", f"{result.aerated_depth_m:.3f}", "m"]]
            elif method == "跌水水力计算":
                result = calculate_vertical_drop(VerticalDropInput(f("Q"), f("Bdown"), f("H0"), f("Hadd"), f("P")))
                rows += [["入口底宽", f"{result.entrance_bottom_width_m:.3f}", "m"], ["渐变段长度", f"{result.transition_length_m:.3f}", "m"], ["单宽流量", f"{result.unit_discharge_m2s:.3f}", "m2/s"], ["消力池长度", f"{result.basin_length_m:.3f}", "m"]]
            elif method == "引水渡槽水力计算":
                result = calculate_aqueduct(AqueductInput(f("Q"), f("L"), f("i"), f("n"), f("B1"), f("h1"), f("v1"), f("Z1"), f("B2")))
                rows += [["槽身水深", f"{result.trough_depth_m:.3f}", "m"], ["槽身流速", f"{result.trough_velocity_m_s:.3f}", "m/s"], ["总水头损失", f"{result.total_head_loss_m:.3f}", "m"], ["进口渐变段", f"{result.inlet_transition_length_m:.3f}", "m"], ["出口渐变段", f"{result.outlet_transition_length_m:.3f}", "m"], ["槽身起点渠底", f"{result.trough_start_bed_m:.3f}", "m"], ["槽身终点渠底", f"{result.trough_end_bed_m:.3f}", "m"], ["下游渠底", f"{result.downstream_bed_m:.3f}", "m"], ["槽身超高", f"{result.freeboard_cm:.2f}", "cm"]]
            elif method == "倒虹吸管水力计算":
                result = calculate_inverted_siphon(InvertedSiphonInput(f("Q"), f("v"), f("L"), f("zeta"), f("lambda"), f("Zu"), f("hu"), f("hd"), f("sigma"), f("Hp")))
                rows += [["管径", f"{result.pipe_diameter_m:.3f}", "m"], ["总水头损失", f"{result.total_headloss_m:.3f}", "m"], ["管壁计算厚度", f"{result.wall_thickness_mm:.2f}", "mm"], ["最小厚度", f"{result.minimum_wall_thickness_mm:.2f}", "mm"], ["下游渠底高程", f"{result.downstream_bed_m:.3f}", "m"], ["弯管半径", f"{result.bend_radius_m:.3f}", "m"]]
            elif method == "桥孔水力计算":
                result = calculate_bridge_hydraulic(BridgeHydraulicInput(f("Q"), f("h0"), f("a"), f("dz"), f("mu1"), f("mu2")))
                rows += [["过流水面面积", f"{result.flow_area_m2:.3f}", "m2"], ["桥孔总净宽", f"{result.total_clear_width_m:.3f}", "m"], ["桥孔设计净高", f"{result.design_clear_height_m:.3f}", "m"]]
            elif method == "隧洞水力计算":
                result = calculate_tunnel_hydraulic(TunnelHydraulicInput(values.get("mode", "无压城门洞形"), f("Q"), f("H0"), f("mu"), f("sigma"), f("ratio"), f("angle"), roughness=f("n"), bed_slope=f("i")))
                rows += [["洞身净宽", f"{result.bottom_width_m:.3f}", "m"], ["洞身总高度", f"{result.tunnel_height_m:.3f}", "m"], ["城门洞拱半径", f"{result.arch_radius_m:.3f}", "m"], ["圆洞直径", f"{result.circular_diameter_m:.3f}", "m"]]
            elif method == "涵洞水力计算":
                result = calculate_culvert_hydraulic(CulvertHydraulicInput(f("Q"), f("H0"), f("mu"), f("sigma"), f("ratio"), values.get("mode", "无压箱涵")))
                rows += [["涵洞底宽", f"{result.bottom_width_m:.3f}", "m"], ["涵洞高度", f"{result.culvert_height_m:.3f}", "m"], ["涵内水深", f"{result.inner_depth_m:.3f}", "m"], ["自由出流复核", f"{result.free_flow_m3s:.3f}", "m3/s"], ["淹没出流复核", f"{result.submerged_flow_m3s:.3f}", "m3/s"]]
            elif method == "侧堰水力计算":
                result = calculate_side_weir(SideWeirInput(values.get("mode", "锐角分水"), f("Q"), None, f("m"), f("v1"), f("h1"), f("angle"), f("H1"), f("H2")))
                rows += [["平均堰顶水头", f"{result.mean_weir_head_m:.3f}", "m"], ["侧堰宽度", f"{result.side_weir_width_m:.3f}", "m"], ["侧堰流量复核", f"{result.side_weir_flow_m3s:.3f}", "m3/s"]]
            else:
                raise HydrologyError(f"暂未接入该方法：{method}")
            info = self.METHOD_DEFS[method]
            lines = [
                f"{method}计算书",
                "",
                "一、公式来源",
                format_fixed_table([["项目", "内容"], ["来源", info["source"]], ["公式", info["formula"]]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(self._input_rows(values), min_width=16),
                "",
                "三、计算结果",
                format_fixed_table(rows, min_width=16),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class IrrigationDiversionTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        modulus = ttk.LabelFrame(top, text="一、灌水模数计算（书第1章第1节）")
        modulus.grid(row=0, column=0, sticky="ew")
        self.modulus_inputs: dict[str, tk.Entry] = {}
        modulus_fields = [
            ("area", "作物灌溉面积(亩)", "10000"),
            ("quota", "灌水定额(m³/亩)", "60"),
            ("days", "延续时间T(d)", "10"),
        ]
        for idx, (key, label, default) in enumerate(modulus_fields):
            self.modulus_inputs[key] = self._entry(modulus, 0, idx, label, default, width=14)

        continuous = ttk.LabelFrame(top, text="二、续灌引水流量计算（书第1章第2节）")
        continuous.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        self.continuous_inputs: dict[str, tk.Entry] = {}
        continuous_fields = [
            ("area", "总控制面积S(万亩)", "8"),
            ("modulus", "灌水模数q", "0.35"),
            ("eta_main", "干渠η干", "0.86"),
            ("eta_branch", "支渠η支", "0.90"),
            ("eta_lateral", "斗渠η斗", "0.95"),
            ("eta_field", "田间η田", "0.90"),
        ]
        for idx, (key, label, default) in enumerate(continuous_fields):
            self.continuous_inputs[key] = self._entry(continuous, idx // 3, idx % 3, label, default, width=14)

        rotational = ttk.LabelFrame(top, text="三、轮灌引水流量计算（书第1章第2节）")
        rotational.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        self.rotational_inputs: dict[str, tk.Entry] = {}
        rotational_fields = [
            ("groups", "轮灌组数N", "3"),
            ("area", "每组平均面积An(万亩)", "2"),
            ("modulus", "灌水模数q", "0.35"),
            ("eta", "灌溉水利用系数ηn", "0.65"),
        ]
        for idx, (key, label, default) in enumerate(rotational_fields):
            self.rotational_inputs[key] = self._entry(rotational, idx // 3, idx % 3, label, default, width=14)

        headwater = ttk.LabelFrame(top, text="四、灌区渠首水位计算（书第1章第3节）")
        headwater.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        headwater.columnconfigure(7, weight=1)
        self.headwater_inputs: dict[str, tk.Entry] = {}
        headwater_fields = [
            ("field_elevation", "最高田面高程h田(m)", "4300"),
            ("field_depth", "田面灌水深∇h灌(m)", "0.03"),
            ("channel_depth", "渠道设计水深h水(m)", "1.20"),
            ("structure_loss", "建筑物损失∇h建(m)", "0.10"),
            ("extra_loss", "直接渠道损失(m)", "0"),
        ]
        for idx, (key, label, default) in enumerate(headwater_fields):
            self.headwater_inputs[key] = self._entry(headwater, idx // 3, idx % 3, label, default, width=14)
        ttk.Label(headwater, text="渠道比降和长度：每行输入 比降,长度m，例如 0.0005,1000").grid(
            row=2, column=0, columnspan=4, sticky="w", padx=10, pady=(6, 2)
        )
        self.segment_text = ScrolledText(headwater, height=4, width=46, font=(DASHBOARD_MONO_FONT, 10))
        self.segment_text.insert(tk.END, "0.0005,1000\n0.000333333,600")
        self.segment_text.grid(row=3, column=0, columnspan=6, sticky="ew", padx=10, pady=(0, 8))

        ttk.Button(top, text="开始计算本页全部内容", command=self.calculate).grid(row=4, column=0, sticky="e", pady=(10, 0))
        self.output = self._result_box(self)

    def _parse_segments(self) -> tuple[CanalLossSegment, ...]:
        segments: list[CanalLossSegment] = []
        text = self.segment_text.get("1.0", "end-1c").strip()
        if not text:
            return ()
        for line_no, line in enumerate(text.splitlines(), start=1):
            raw = line.strip()
            if not raw:
                continue
            parts = [part.strip() for part in raw.replace("，", ",").split(",")]
            if len(parts) != 2:
                raise HydrologyError(f"渠道损失第{line_no}行格式应为：比降,长度")
            try:
                slope = float(parts[0])
                length = float(parts[1])
            except ValueError as exc:
                raise HydrologyError(f"渠道损失第{line_no}行必须是数字") from exc
            segments.append(CanalLossSegment(slope=slope, length_m=length))
        return tuple(segments)

    def calculate(self) -> None:
        try:
            modulus_result = calculate_irrigation_modulus(
                IrrigationModulusInput(
                    irrigation_area_mu=self._float(self.modulus_inputs["area"], "作物灌溉面积"),
                    quota_m3_per_mu=self._float(self.modulus_inputs["quota"], "灌水定额"),
                    duration_days=self._float(self.modulus_inputs["days"], "延续时间"),
                )
            )
            continuous_result = calculate_continuous_diversion(
                ContinuousDiversionInput(
                    irrigation_area_10k_mu=self._float(self.continuous_inputs["area"], "总控制面积"),
                    irrigation_modulus=self._float(self.continuous_inputs["modulus"], "续灌灌水模数"),
                    main_canal_efficiency=self._float(self.continuous_inputs["eta_main"], "干渠水利用系数"),
                    branch_canal_efficiency=self._float(self.continuous_inputs["eta_branch"], "支渠水利用系数"),
                    lateral_canal_efficiency=self._float(self.continuous_inputs["eta_lateral"], "斗渠水利用系数"),
                    field_efficiency=self._float(self.continuous_inputs["eta_field"], "田间水利用系数"),
                )
            )
            rotational_result = calculate_rotational_diversion(
                RotationalDiversionInput(
                    group_count=int(self._float(self.rotational_inputs["groups"], "轮灌组数")),
                    average_group_area_10k_mu=self._float(self.rotational_inputs["area"], "每组平均面积"),
                    irrigation_modulus=self._float(self.rotational_inputs["modulus"], "轮灌灌水模数"),
                    irrigation_efficiency=self._float(self.rotational_inputs["eta"], "轮灌水利用系数"),
                )
            )
            headwater_result = calculate_canal_headwater(
                CanalHeadwaterInput(
                    field_elevation=self._float(self.headwater_inputs["field_elevation"], "最高田面高程"),
                    field_water_depth=self._float(self.headwater_inputs["field_depth"], "田面灌水深"),
                    channel_water_depth=self._float(self.headwater_inputs["channel_depth"], "渠道设计水深"),
                    structure_head_loss=self._float(self.headwater_inputs["structure_loss"], "建筑物损失"),
                    extra_canal_head_loss=self._float(self.headwater_inputs["extra_loss"], "直接渠道损失"),
                    canal_segments=self._parse_segments(),
                )
            )
        except (HydrologyError, WaterworksIrrigationError) as exc:
            messagebox.showerror("输入错误", str(exc))
            return

        rows = [
            "灌溉引水与渠首水位计算书",
            "",
            "一、采用公式",
            "| 项目 | 公式 |",
            "| 灌水模数 | q=m/(8.64*T)；Q净=q*A/10000 |",
            "| 续灌引水 | Q净=S*q；Q毛=Q净/η水；η水=η渠系*η田；η渠系=η干*η支*η斗 |",
            "| 轮灌引水 | Qn=N*q*An/ηn |",
            "| 渠首水位 | H首=h田+∇h灌+∇h渠+∇h建+h水 |",
            "",
            "二、计算结果",
            "| 计算项 | 数值 | 单位 |",
            f"| 灌水模数q | {modulus_result.irrigation_modulus:.4f} | m³/(s·10⁴亩) |",
            f"| 该作物净流量 | {modulus_result.net_flow:.3f} | m³/s |",
            f"| 续灌渠系水利用系数η渠系 | {continuous_result.canal_system_efficiency:.4f} | - |",
            f"| 续灌灌溉水利用系数η水 | {continuous_result.irrigation_efficiency:.4f} | - |",
            f"| 续灌干渠净流量 | {continuous_result.net_flow:.3f} | m³/s |",
            f"| 续灌干渠毛流量 | {continuous_result.gross_flow:.3f} | m³/s |",
            f"| 轮灌设计流量 | {rotational_result.design_flow:.3f} | m³/s |",
            f"| 渠道水头损失∇h渠 | {headwater_result.canal_head_loss:.3f} | m |",
            f"| 渠首设计水位H首 | {headwater_result.headwater_elevation:.3f} | m |",
            "",
            "三、参数说明",
            "| 参数 | 含义 |",
            "| m | 灌水定额，单位m³/亩 |",
            "| T | 灌水延续时间，单位d |",
            "| S | 总控制灌溉面积，单位万亩 |",
            "| η干、η支、η斗、η田 | 各级渠道及田间水利用系数，均为小数 |",
            "| i,L | 渠道比降和对应长度，∇h渠按Σ(iL)累加 |",
        ]
        self._write(self.output, "\n".join(rows))


class PumpStationPipeTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        flow = ttk.LabelFrame(top, text="一、提水灌溉设计流量（书第1章第4节）")
        flow.grid(row=0, column=0, sticky="ew")
        self.pump_flow_inputs: dict[str, tk.Entry] = {}
        flow_fields = [
            ("gross_depth", "最大毛灌水量M(mm)", "120"),
            ("area_hm2", "灌溉面积ω(hm²)", "1500"),
            ("pump_seconds", "提水时间t(s)", "72000"),
            ("crop_ratio", "作物比例a(%)", "15.63"),
            ("quota", "灌水定额m(m³/亩)", "40"),
            ("duration", "延续时间T(d)", "10"),
            ("daily_hours", "每天提水t(h)", "22"),
            ("area_10k", "总面积A(万亩)", "3.2"),
            ("eta_ir", "灌溉水利用系数η", "1"),
        ]
        for idx, (key, label, default) in enumerate(flow_fields):
            self.pump_flow_inputs[key] = self._entry(flow, idx // 3, idx % 3, label, default, width=13)

        pipe = ttk.LabelFrame(top, text="二、压力管道、通气孔与水泵扬程（书第1章第5、6节）")
        pipe.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        self.pipe_inputs: dict[str, tk.Entry] = {}
        pipe_fields = [
            ("pipe_flow", "管道流量Q(m³/s)", "1.0"),
            ("pipe_velocity", "允许流速v(m/s)", "1.0"),
            ("outlet_level", "出水池水位Z1(m)", "4000.789"),
            ("suction_level", "吸水池水位Z2(m)", "3944.354"),
            ("head_loss", "总水头损失ΔH(m)", "1.08"),
        ]
        for idx, (key, label, default) in enumerate(pipe_fields):
            self.pipe_inputs[key] = self._entry(pipe, idx // 3, idx % 3, label, default, width=13)

        install = ttk.LabelFrame(top, text="三、水泵安装高程（书第1章第7节）")
        install.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        self.install_inputs: dict[str, tk.Entry] = {}
        install_fields = [
            ("hs", "允许吸上真空高度[m](m)", "2.7"),
            ("pa", "当地大气压Pa(Pa)", "61300"),
            ("pv_head", "汽化压头(m)", "0.24"),
            ("rho", "水密度ρ(kg/m³)", "998.16"),
            ("velocity", "入口流速v(m/s)", "1.0"),
            ("loss", "吸水管损失hw(m)", "0.5"),
        ]
        for idx, (key, label, default) in enumerate(install_fields):
            self.install_inputs[key] = self._entry(install, idx // 3, idx % 3, label, default, width=13)

        perf = ttk.LabelFrame(top, text="四、功率效率、比速与主变容量（书第1章第8、9节）")
        perf.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        self.perf_inputs: dict[str, tk.Entry] = {}
        perf_fields = [
            ("density_t", "水密度(t/m³)", "1.0"),
            ("modulus", "设计毛灌溉模数", "0.001"),
            ("guaranteed_area", "保证面积(hm²)", "1500"),
            ("pump_head", "设计扬程Hp(m)", "30"),
            ("pump_eta", "装置效率η(%)", "85"),
            ("eff_flow", "效率流量Q(m³/s)", "0.12"),
            ("shaft_power", "轴功率P(kW)", "55"),
            ("rated_speed", "额定转速n(r/min)", "1450"),
            ("rated_flow", "额定流量Q(m³/s)", "0.12"),
            ("rated_head", "额定扬程H(m)", "40"),
        ]
        for idx, (key, label, default) in enumerate(perf_fields):
            self.perf_inputs[key] = self._entry(perf, idx // 3, idx % 3, label, default, width=13)
        self.double_suction_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(perf, text="双吸泵比速按Q/2", variable=self.double_suction_var).grid(
            row=4, column=0, columnspan=2, sticky="w", padx=10, pady=(2, 8)
        )
        ttk.Label(perf, text="主变电动机：每行 台数,额定功率kW,水泵轴功率kW,效率%,功率因数,修正系数").grid(
            row=5, column=0, columnspan=6, sticky="w", padx=10, pady=(6, 2)
        )
        self.motor_text = ScrolledText(perf, height=4, width=62, font=(DASHBOARD_MONO_FONT, 10))
        self.motor_text.insert(tk.END, "3,8,32,80,0.85,1.1\n2,10,64,80,0.85,1.1")
        self.motor_text.grid(row=6, column=0, columnspan=6, sticky="ew", padx=10, pady=(0, 8))
        self.transformer_inputs: dict[str, tk.Entry] = {}
        self.transformer_inputs["lighting"] = self._entry(perf, 7, 0, "照明负荷P2(kW)", "3", width=13)
        self.transformer_inputs["lighting_factor"] = self._entry(perf, 7, 1, "照明同时系数k2", "0.9", width=13)

        ttk.Button(top, text="开始计算本页全部内容", command=self.calculate).grid(row=4, column=0, sticky="e", pady=(10, 0))
        self.output = self._result_box(self)

    def _parse_motors(self) -> tuple[TransformerMotorInput, ...]:
        motors: list[TransformerMotorInput] = []
        text = self.motor_text.get("1.0", "end-1c").strip()
        if not text:
            return ()
        for line_no, line in enumerate(text.splitlines(), start=1):
            raw = line.strip()
            if not raw:
                continue
            parts = [part.strip() for part in raw.replace("，", ",").split(",")]
            if len(parts) != 6:
                raise HydrologyError(f"主变电动机第{line_no}行格式应为：台数,额定功率,轴功率,效率,功率因数,修正系数")
            try:
                count, rated, shaft, eta, factor, correction = [float(part) for part in parts]
            except ValueError as exc:
                raise HydrologyError(f"主变电动机第{line_no}行必须是数字") from exc
            motors.append(
                TransformerMotorInput(
                    count=int(count),
                    motor_rated_power_kw=rated,
                    pump_shaft_power_kw=shaft,
                    motor_efficiency_percent=eta,
                    power_factor=factor,
                    correction_factor=correction,
                )
            )
        return tuple(motors)

    def calculate(self) -> None:
        try:
            typical_flow = calculate_pump_flow_by_typical_year(
                PumpTypicalYearFlowInput(
                    max_gross_irrigation_depth_mm=self._float(self.pump_flow_inputs["gross_depth"], "最大毛灌水量"),
                    irrigation_area_hm2=self._float(self.pump_flow_inputs["area_hm2"], "灌溉面积"),
                    pumping_time_seconds=self._float(self.pump_flow_inputs["pump_seconds"], "提水时间"),
                )
            )
            design_rate_flow = calculate_pump_flow_by_design_rate(
                PumpDesignRateFlowInput(
                    crop_ratio_percent=self._float(self.pump_flow_inputs["crop_ratio"], "作物比例"),
                    quota_m3_per_mu=self._float(self.pump_flow_inputs["quota"], "灌水定额"),
                    duration_days=self._float(self.pump_flow_inputs["duration"], "延续时间"),
                    daily_pumping_hours=self._float(self.pump_flow_inputs["daily_hours"], "每天提水时间"),
                    total_area_10k_mu=self._float(self.pump_flow_inputs["area_10k"], "总面积"),
                    irrigation_efficiency=self._float(self.pump_flow_inputs["eta_ir"], "灌溉水利用系数"),
                )
            )
            pipe = calculate_pressure_pipe(
                PressurePipeInput(
                    design_flow=self._float(self.pipe_inputs["pipe_flow"], "管道流量"),
                    allowable_velocity=self._float(self.pipe_inputs["pipe_velocity"], "允许流速"),
                )
            )
            head = calculate_pump_design_head(
                PumpHeadInput(
                    outlet_water_level=self._float(self.pipe_inputs["outlet_level"], "出水池水位"),
                    suction_water_level=self._float(self.pipe_inputs["suction_level"], "吸水池水位"),
                    total_head_loss=self._float(self.pipe_inputs["head_loss"], "总水头损失"),
                )
            )
            install = calculate_pump_installation_height(
                PumpInstallationInput(
                    allowable_suction_vacuum_m=self._float(self.install_inputs["hs"], "允许吸上真空高度"),
                    atmospheric_pressure_pa=self._float(self.install_inputs["pa"], "当地大气压"),
                    vapor_pressure_head_m=self._float(self.install_inputs["pv_head"], "汽化压头"),
                    water_density_kg_m3=self._float(self.install_inputs["rho"], "水密度"),
                    suction_velocity_m_s=self._float(self.install_inputs["velocity"], "入口流速"),
                    suction_pipe_head_loss_m=self._float(self.install_inputs["loss"], "吸水管损失"),
                )
            )
            power = calculate_pump_total_power(
                PumpPowerInput(
                    density_t_m3=self._float(self.perf_inputs["density_t"], "水密度"),
                    irrigation_modulus_m3_s_hm2=self._float(self.perf_inputs["modulus"], "设计毛灌溉模数"),
                    guaranteed_irrigation_area_hm2=self._float(self.perf_inputs["guaranteed_area"], "保证灌溉面积"),
                    design_head_m=self._float(self.perf_inputs["pump_head"], "设计扬程"),
                    pump_efficiency_percent=self._float(self.perf_inputs["pump_eta"], "装置效率"),
                )
            )
            efficiency = calculate_pump_efficiency(
                PumpEfficiencyInput(
                    density_kg_m3=self._float(self.install_inputs["rho"], "水密度"),
                    design_flow_m3_s=self._float(self.perf_inputs["eff_flow"], "效率流量"),
                    design_head_m=self._float(self.perf_inputs["pump_head"], "设计扬程"),
                    shaft_power_kw=self._float(self.perf_inputs["shaft_power"], "轴功率"),
                )
            )
            speed = calculate_pump_specific_speed(
                PumpSpecificSpeedInput(
                    rated_speed_rpm=self._float(self.perf_inputs["rated_speed"], "额定转速"),
                    rated_flow_m3_s=self._float(self.perf_inputs["rated_flow"], "额定流量"),
                    rated_head_m=self._float(self.perf_inputs["rated_head"], "额定扬程"),
                    double_suction=self.double_suction_var.get(),
                )
            )
            transformer = calculate_transformer_capacity(
                TransformerCapacityInput(
                    motors=self._parse_motors(),
                    lighting_load_kw=self._float(self.transformer_inputs["lighting"], "照明负荷"),
                    lighting_coincidence_factor=self._float(self.transformer_inputs["lighting_factor"], "照明同时系数"),
                )
            )
        except (HydrologyError, WaterworksIrrigationError) as exc:
            messagebox.showerror("输入错误", str(exc))
            return

        rows = [
            "提水泵站与压力管道计算书",
            "",
            "一、采用公式",
            "| 项目 | 公式 |",
            "| 典型年法提水流量 | Q=10Mω/t |",
            "| 设计灌水率法 | q净=a*m/(0.36*T*t)；Q=q净*A/η灌 |",
            "| 压力管道 | d=(4Q/πv)^0.5；δ=d(mm)/800+4；S≥0.01Q |",
            "| 水泵设计扬程 | Hp=H净+ΔH；H净=Z1-Z2 |",
            "| 安装高程 | [Hg]=[Hs]-10.33+pa/(ρg)+0.24-pv/(ρg)-v²/(2g)-hw |",
            "| 功率/效率/比速 | N=ρqωHp/(102η)；η=Pe/P；ns=3.65n√Q/H^(3/4) |",
            "| 主变容量 | S=Σ(P1/η/cosφ*k1)+P2*k2；k1=k3*P3/P1 |",
            "",
            "二、计算结果",
            "| 计算项 | 数值 | 单位 |",
            f"| 典型年法提水设计流量 | {typical_flow.design_flow:.3f} | m³/s |",
            f"| 设计灌水率q净 | {design_rate_flow.irrigation_rate:.4f} | m³/(s·10⁴亩) |",
            f"| 设计灌水率法提水设计流量 | {design_rate_flow.design_flow:.3f} | m³/s |",
            f"| 压力管道内径 | {pipe.inner_diameter_m:.3f} | m |",
            f"| 管壁最小厚度 | {pipe.minimum_wall_thickness_mm:.2f} | mm |",
            f"| 通气孔面积 | {pipe.vent_area_m2:.4f} | m² |",
            f"| 水泵净扬程 | {head.static_head:.3f} | m |",
            f"| 水泵设计扬程 | {head.design_head:.3f} | m |",
            f"| 允许安装高程 | {install.allowable_installation_height_m:.3f} | m |",
            f"| 水泵总功率 | {power.total_power_kw:.2f} | kW |",
            f"| 水泵有效功率 | {efficiency.effective_power_kw:.2f} | kW |",
            f"| 水泵效率 | {efficiency.efficiency_percent:.2f} | % |",
            f"| 水泵比速 | {speed.specific_speed:.1f} | - |",
            f"| 主变电动机负荷 | {transformer.motor_load_kva:.2f} | kVA |",
            f"| 照明等负荷 | {transformer.lighting_load_kva:.2f} | kVA |",
            f"| 站用主变容量 | {transformer.total_capacity_kva:.2f} | kVA |",
            "",
            "三、使用提醒",
            "| 项目 | 提醒 |",
            "| 提水流量 | 典型年法和设计灌水率法口径不同，应结合灌区资料选择。 |",
            "| 压力管道 | 管壁厚度为理论最小值，书中提示设计时应考虑锈蚀、磨损等增加1~2mm。 |",
            "| 安装高程 | 负值表示水泵轴线应低于吸水池水面。 |",
            "| 主变容量 | 电动机效率按书中表格口径填百分数，例如80，不填0.80。 |",
        ]
        self._write(self.output, "\n".join(rows))


class ChannelHydraulicsTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        channel = ttk.LabelFrame(top, text="一、明渠均匀流水力计算（书第2章第2节）")
        channel.grid(row=0, column=0, sticky="ew")
        self.open_inputs: dict[str, tk.Entry] = {}
        open_fields = [
            ("bottom_width", "渠底宽b(m)", "1.5"),
            ("water_depth", "水深h(m)", "3.2"),
            ("side_slope", "边坡系数m", "2.5"),
            ("roughness", "糙率n", "0.025"),
            ("bed_slope", "渠底比降i", "0.000142857"),
            ("enlarge_coefficient", "加大流量系数", "1.15"),
        ]
        for idx, (key, label, default) in enumerate(open_fields):
            self.open_inputs[key] = self._entry(channel, idx // 3, idx % 3, label, default, width=14)
        ttk.Label(
            channel,
            text="提示：比降1/7000应填0.000142857；边坡系数m=水平距离/垂直高度，矩形渠道填0。",
            foreground="#667085",
        ).grid(row=2, column=0, columnspan=6, sticky="w", padx=10, pady=(0, 8))
        ttk.Button(channel, text="计算明渠水力", command=self._calculate_open_channel).grid(
            row=0, column=6, rowspan=2, sticky="ns", padx=(6, 12), pady=6
        )

        critical = ttk.LabelFrame(top, text="二、临界水深计算（书第2章第5节）")
        critical.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        self.critical_inputs: dict[str, tk.Entry] = {}
        critical_fields = [
            ("flow", "流量Q(m³/s)", "20.07"),
            ("bottom_width", "渠底宽b(m)", "1.5"),
            ("side_slope", "边坡系数m", "2.5"),
            ("alpha", "动能修正系数α", "1.0"),
            ("gravity", "重力加速度g", "9.81"),
        ]
        for idx, (key, label, default) in enumerate(critical_fields):
            self.critical_inputs[key] = self._entry(critical, idx // 3, idx % 3, label, default, width=14)
        ttk.Label(
            critical,
            text="提示：矩形渠道把m填0；书中矩形例题Q=20.07、b=1.5时，临界水深约2.63m。",
            foreground="#667085",
        ).grid(row=2, column=0, columnspan=6, sticky="w", padx=10, pady=(0, 8))
        ttk.Button(critical, text="计算临界水深", command=self._calculate_critical_depth).grid(
            row=0, column=6, rowspan=2, sticky="ns", padx=(6, 12), pady=6
        )

        best = ttk.LabelFrame(top, text="三、水力最佳断面计算（书第2章第6节）")
        best.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        self.best_inputs: dict[str, tk.Entry] = {}
        best_fields = [
            ("flow", "设计流量Q(m³/s)", "20.07"),
            ("roughness", "糙率n", "0.015"),
            ("bed_slope", "渠底比降i", "0.0005"),
            ("side_slope", "边坡系数m", "1.5"),
        ]
        for idx, (key, label, default) in enumerate(best_fields):
            self.best_inputs[key] = self._entry(best, idx // 3, idx % 3, label, default, width=14)
        ttk.Label(
            best,
            text="提示：矩形最佳断面填m=0；梯形例题Q=20.07、m=1.5、i=1/2000时，h约2.384m、b约1.444m。",
            foreground="#667085",
        ).grid(row=2, column=0, columnspan=6, sticky="w", padx=10, pady=(0, 8))
        ttk.Button(best, text="计算最佳断面", command=self._calculate_best_section).grid(
            row=0, column=6, rowspan=2, sticky="ns", padx=(6, 12), pady=6
        )

        self.output = self._result_box(self)

    def _render_result(self, text: str) -> None:
        self.output.delete("1.0", tk.END)
        self.output.insert(tk.END, text)

    def _calculate_open_channel(self) -> None:
        try:
            result = calculate_open_channel(
                OpenChannelInput(
                    bottom_width=self._float(self.open_inputs["bottom_width"], "渠底宽b"),
                    water_depth=self._float(self.open_inputs["water_depth"], "水深h"),
                    side_slope=self._float(self.open_inputs["side_slope"], "边坡系数m"),
                    roughness=self._float(self.open_inputs["roughness"], "糙率n"),
                    bed_slope=self._float(self.open_inputs["bed_slope"], "渠底比降i"),
                    enlarge_coefficient=self._float(self.open_inputs["enlarge_coefficient"], "加大流量系数"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        input_rows = [
            ["参数", "取值", "说明"],
            ["b", self.open_inputs["bottom_width"].get(), "渠底宽，m"],
            ["h", self.open_inputs["water_depth"].get(), "设计水深，m"],
            ["m", self.open_inputs["side_slope"].get(), "边坡系数，矩形渠为0"],
            ["n", self.open_inputs["roughness"].get(), "糙率"],
            ["i", self.open_inputs["bed_slope"].get(), "渠底比降，小数"],
            ["k", self.open_inputs["enlarge_coefficient"].get(), "加大流量系数"],
        ]
        result_rows = [
            ["项目", "结果", "单位/说明"],
            ["过水面积A", f"{result.area:.3f}", "m²"],
            ["湿周x", f"{result.wetted_perimeter:.3f}", "m"],
            ["水力半径R", f"{result.hydraulic_radius:.3f}", "m"],
            ["谢才系数C", f"{result.chezy:.2f}", ""],
            ["流速v", f"{result.velocity:.3f}", "m/s"],
            ["渠流量Q", f"{result.flow:.2f}", "m³/s"],
            ["加大流量", f"{result.enlarged_flow:.2f}", "m³/s"],
            ["加大流量对应水深", f"{result.enlarged_depth:.3f}", "m"],
            ["加大流量对应面积", f"{result.enlarged_area:.3f}", "m²"],
            ["渠岸超高Fb", f"{result.freeboard:.2f}", "m，Fb=1/4*h+0.2"],
            ["渠岸/堤顶宽D", f"{result.berm_width:.2f}", "m，D=h+0.3"],
            ["最小弯道半径R0", f"{result.bend_radius:.2f}", "m"],
        ]
        self._render_result(
            "\n".join(
                [
                    "明渠均匀流水力计算",
                    "",
                    "一、输入参数",
                    format_fixed_table(input_rows, min_width=14),
                    "",
                    "二、采用公式",
                    format_fixed_table([["来源", "《水利大算盘》第二章第二节"], ["公式", result.formula]], min_width=20),
                    "",
                    "三、计算结果",
                    format_fixed_table(result_rows, min_width=18),
                ]
            )
        )

    def _calculate_critical_depth(self) -> None:
        try:
            result = calculate_critical_depth(
                CriticalDepthInput(
                    flow=self._float(self.critical_inputs["flow"], "流量Q"),
                    bottom_width=self._float(self.critical_inputs["bottom_width"], "渠底宽b"),
                    side_slope=self._float(self.critical_inputs["side_slope"], "边坡系数m"),
                    alpha=self._float(self.critical_inputs["alpha"], "动能修正系数α"),
                    gravity=self._float(self.critical_inputs["gravity"], "重力加速度g"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        input_rows = [
            ["参数", "取值", "说明"],
            ["Q", self.critical_inputs["flow"].get(), "流量，m³/s"],
            ["b", self.critical_inputs["bottom_width"].get(), "渠底宽，m"],
            ["m", self.critical_inputs["side_slope"].get(), "边坡系数；矩形为0"],
            ["α", self.critical_inputs["alpha"].get(), "动能修正系数"],
            ["g", self.critical_inputs["gravity"].get(), "重力加速度，m/s²"],
        ]
        result_rows = [
            ["项目", "结果", "单位/说明"],
            ["单宽流量q", f"{result.unit_discharge:.3f}", "m²/s"],
            ["临界水深h", f"{result.critical_depth:.3f}", "m"],
            ["水面宽T", f"{result.top_width:.3f}", "m"],
            ["过水面积A", f"{result.area:.3f}", "m²"],
            ["临界判据", f"{result.criterion:.4f}", "接近1表示满足临界流"],
        ]
        self._render_result(
            "\n".join(
                [
                    "临界水深计算",
                    "",
                    "一、输入参数",
                    format_fixed_table(input_rows, min_width=14),
                    "",
                    "二、采用公式",
                    format_fixed_table([["来源", "《水利大算盘》第二章第五节"], ["公式", result.formula]], min_width=20),
                    "",
                    "三、计算结果",
                    format_fixed_table(result_rows, min_width=18),
                ]
            )
        )

    def _calculate_best_section(self) -> None:
        try:
            result = calculate_best_hydraulic_section(
                BestHydraulicSectionInput(
                    flow=self._float(self.best_inputs["flow"], "设计流量Q"),
                    roughness=self._float(self.best_inputs["roughness"], "糙率n"),
                    bed_slope=self._float(self.best_inputs["bed_slope"], "渠底比降i"),
                    side_slope=self._float(self.best_inputs["side_slope"], "边坡系数m"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        input_rows = [
            ["参数", "取值", "说明"],
            ["Q", self.best_inputs["flow"].get(), "设计流量，m³/s"],
            ["n", self.best_inputs["roughness"].get(), "糙率"],
            ["i", self.best_inputs["bed_slope"].get(), "渠底比降，小数"],
            ["m", self.best_inputs["side_slope"].get(), "边坡系数；矩形为0"],
        ]
        result_rows = [
            ["项目", "结果", "单位/说明"],
            ["断面类型", result.section_type, ""],
            ["最佳水深h0", f"{result.water_depth:.3f}", "m"],
            ["最佳底宽b0", f"{result.bottom_width:.3f}", "m"],
            ["b0/h0", f"{result.width_depth_ratio:.3f}", "最佳断面宽深比"],
            ["过水面积A", f"{result.area:.3f}", "m²"],
            ["湿周x", f"{result.wetted_perimeter:.3f}", "m"],
            ["水力半径R", f"{result.hydraulic_radius:.3f}", "m"],
            ["谢才系数C", f"{result.chezy:.2f}", ""],
            ["流速v", f"{result.velocity:.3f}", "m/s"],
            ["复算流量Q", f"{result.check_flow:.2f}", "m³/s"],
        ]
        self._render_result(
            "\n".join(
                [
                    "水力最佳断面计算",
                    "",
                    "一、输入参数",
                    format_fixed_table(input_rows, min_width=14),
                    "",
                    "二、采用公式",
                    format_fixed_table([["来源", "《水利大算盘》第二章第六节"], ["公式", result.formula]], min_width=20),
                    "",
                    "三、计算结果",
                    format_fixed_table(result_rows, min_width=18),
                ]
            )
        )


class ChannelSafetyAuxiliaryTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        for idx in range(2):
            top.columnconfigure(idx, weight=1)

        angle = ttk.LabelFrame(top, text="一、进水闸合理引水角（书第2章第1节）")
        angle.grid(row=0, column=0, sticky="ew", padx=(0, 6))
        self.angle_inputs: dict[str, tk.Entry] = {}
        for idx, (key, label, default) in enumerate([
            ("river_velocity", "河道流速v0(m/s)", "1.1"),
            ("canal_velocity", "渠道流速v1(m/s)", "1.2"),
        ]):
            self.angle_inputs[key] = self._entry(angle, idx, 0, label, default, width=14)
        ttk.Button(angle, text="计算引水角", command=self._calculate_intake_angle).grid(
            row=0, column=2, rowspan=2, padx=8, pady=6, sticky="ns"
        )

        velocity = ttk.LabelFrame(top, text="二、临界不冲与不淤流速（书第2章第3、4节）")
        velocity.grid(row=0, column=1, sticky="ew", padx=(6, 0))
        self.velocity_inputs: dict[str, tk.Entry] = {}
        velocity_fields = [
            ("bottom_width", "渠底宽b(m)", "1.5"),
            ("water_depth", "水深h(m)", "3.2"),
            ("side_slope", "边坡m", "2.5"),
            ("roughness", "糙率n", "0.025"),
            ("bed_slope", "比降i", "0.000142857"),
            ("enlarge_coefficient", "加大系数", "1.15"),
            ("soil_coefficient", "土渠A", "0.96"),
            ("rock_depth", "砌石加大水深(m)", "3.699"),
            ("block_length", "块石长a(m)", "0.40"),
            ("block_width", "块石宽b(m)", "0.30"),
            ("block_height", "块石高c(m)", "0.30"),
            ("silt_coefficient", "不淤C0", "0.20"),
            ("minimum_flow", "最小流量Q(m³/s)", "10.60"),
        ]
        for idx, (key, label, default) in enumerate(velocity_fields):
            self.velocity_inputs[key] = self._entry(velocity, idx // 3, idx % 3, label, default, width=12)
        ttk.Button(velocity, text="计算流速校核", command=self._calculate_velocity_checks).grid(
            row=5, column=0, columnspan=6, sticky="ew", padx=10, pady=6
        )

        economic = ttk.LabelFrame(top, text="三、最佳实用经济断面与U形断面（书第2章第7、11节）")
        economic.grid(row=1, column=0, sticky="ew", padx=(0, 6), pady=(10, 0))
        self.economic_inputs: dict[str, tk.Entry] = {}
        economic_fields = [
            ("flow", "梯形Q(m³/s)", "20.07"),
            ("roughness", "梯形n", "0.015"),
            ("bed_slope", "梯形i", "0.0005"),
            ("side_slope", "梯形m", "1.5"),
            ("area_ratio", "面积偏离系数a", "1.01"),
            ("u_flow", "U形Q(m³/s)", "3.0"),
            ("u_ratio", "U形h/B", "1.4"),
            ("u_roughness", "U形n", "0.015"),
            ("u_bed_slope", "U形i", "0.0005"),
        ]
        for idx, (key, label, default) in enumerate(economic_fields):
            self.economic_inputs[key] = self._entry(economic, idx // 3, idx % 3, label, default, width=12)
        ttk.Button(economic, text="计算断面尺寸", command=self._calculate_sections).grid(
            row=3, column=0, columnspan=6, sticky="ew", padx=10, pady=6
        )

        seepage = ttk.LabelFrame(top, text="四、渠道渗漏损失（书第2章第8节）")
        seepage.grid(row=1, column=1, sticky="ew", padx=(6, 0), pady=(10, 0))
        self.seepage_inputs: dict[str, tk.Entry] = {}
        seepage_fields = [
            ("lined_length", "衬砌渠长l(m)", "20000"),
            ("lining_coefficient", "衬砌系数c1", "1.0"),
            ("bottom_width", "渠底宽b(m)", "4.0"),
            ("water_depth", "水深h(m)", "2.0"),
            ("side_slope", "边坡m", "0.0"),
            ("roughness", "糙率n", "0.015"),
            ("bed_slope", "比降i", "0.0005"),
            ("unlined_length", "无衬砌渠长L(km)", "20"),
            ("permeability", "渗透系数kp", "0.04"),
            ("net_flow", "净流量Q净(m³/s)", "20.07"),
        ]
        for idx, (key, label, default) in enumerate(seepage_fields):
            self.seepage_inputs[key] = self._entry(seepage, idx // 3, idx % 3, label, default, width=12)
        ttk.Button(seepage, text="计算渗漏损失", command=self._calculate_seepage).grid(
            row=4, column=0, columnspan=6, sticky="ew", padx=10, pady=6
        )

        steel = ttk.LabelFrame(top, text="五、渠道钢筋总量及含筋率（书第2章第12节例题模板）")
        steel.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(10, 0))
        self.steel_inputs: dict[str, tk.Entry] = {}
        steel_fields = [
            ("channel_length", "渠道总长(m)", "20000"),
            ("joint_spacing", "伸缩缝间距(m)", "8"),
            ("joint_width", "伸缩缝宽(m)", "0.02"),
            ("section_perimeter", "分布筋结构长度(m)", "5.0"),
            ("side_main_length", "侧墙主筋结构长度(m)", "8.0"),
            ("bottom_main_length", "底板主筋结构长度(m)", "8.0"),
            ("cover_total", "保护层总厚(m)", "0.06"),
            ("distribution_straight", "分布筋直段(m)", "7.94"),
            ("side_straight", "侧墙主筋直段(m)", "1.69"),
            ("bottom_straight", "底板主筋直段(m)", "2.34"),
            ("distribution_spacing", "分布筋间距(m)", "0.25"),
            ("main_spacing", "主筋间距(m)", "0.2"),
            ("concrete_volume", "混凝土体积(m³)", "23940.16"),
            ("loss_rate", "损耗率(%)", "5"),
        ]
        for idx, (key, label, default) in enumerate(steel_fields):
            self.steel_inputs[key] = self._entry(steel, idx // 4, idx % 4, label, default, width=12)
        ttk.Button(steel, text="计算钢筋总量", command=self._calculate_reinforcement).grid(
            row=4, column=0, columnspan=8, sticky="ew", padx=10, pady=6
        )

        self.output = self._result_box(self)

    def _show_result(self, title: str, input_rows: list[list[str]], formula_rows: list[list[str]], result_rows: list[list[str]]) -> None:
        self._write(
            self.output,
            "\n".join(
                [
                    title,
                    "",
                    "一、输入参数",
                    format_fixed_table(input_rows, min_width=14),
                    "",
                    "二、采用公式",
                    format_fixed_table(formula_rows, min_width=20),
                    "",
                    "三、计算结果",
                    format_fixed_table(result_rows, min_width=18),
                    "",
                    "四、校核提醒",
                    format_fixed_table(
                        [["项目", "提醒"], ["适用性", "本页按《水利大算盘》扫描页例题口径实现，正式报告应记录参数来源和采用理由。"]],
                        min_width=18,
                    ),
                ]
            ),
        )

    def _calculate_intake_angle(self) -> None:
        try:
            result = calculate_intake_angle(
                IntakeAngleInput(
                    river_velocity=self._float(self.angle_inputs["river_velocity"], "河道流速v0"),
                    canal_velocity=self._float(self.angle_inputs["canal_velocity"], "渠道流速v1"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        self._show_result(
            "进水闸合理引水角计算",
            [["参数", "取值", "说明"], ["v0", self.angle_inputs["river_velocity"].get(), "闸外河道流速，m/s"], ["v1", self.angle_inputs["canal_velocity"].get(), "闸内渠道流速，m/s"]],
            [["来源", "《水利大算盘》第二章第一节"], ["公式", result.formula]],
            [["项目", "结果", "单位/说明"], ["流速比v0/v1", f"{result.velocity_ratio:.3f}", ""], ["引水角δ", f"{result.angle_degrees:.2f}", "°"]],
        )

    def _calculate_velocity_checks(self) -> None:
        try:
            scour = calculate_non_scour_velocity(
                NonScourVelocityInput(
                    bottom_width=self._float(self.velocity_inputs["bottom_width"], "渠底宽b"),
                    water_depth=self._float(self.velocity_inputs["water_depth"], "水深h"),
                    side_slope=self._float(self.velocity_inputs["side_slope"], "边坡系数m"),
                    roughness=self._float(self.velocity_inputs["roughness"], "糙率n"),
                    bed_slope=self._float(self.velocity_inputs["bed_slope"], "渠底比降i"),
                    enlarge_coefficient=self._float(self.velocity_inputs["enlarge_coefficient"], "加大流量系数"),
                    soil_coefficient=self._float(self.velocity_inputs["soil_coefficient"], "土渠不冲流速系数A"),
                    block_length=self._float(self.velocity_inputs["block_length"], "块石长度a"),
                    block_width=self._float(self.velocity_inputs["block_width"], "块石宽度b"),
                    block_height=self._float(self.velocity_inputs["block_height"], "块石高度c"),
                    rock_lining_water_depth=self._float(self.velocity_inputs["rock_depth"], "砌石加大流量水深"),
                )
            )
            silt = calculate_non_silting_velocity(
                NonSiltingVelocityInput(
                    coefficient=self._float(self.velocity_inputs["silt_coefficient"], "不淤流速系数C0"),
                    minimum_flow=self._float(self.velocity_inputs["minimum_flow"], "渠道最小流量Q"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        input_rows = [["参数", "取值", "说明"]]
        for key, label in [
            ("bottom_width", "渠底宽b"),
            ("water_depth", "水深h"),
            ("side_slope", "边坡m"),
            ("roughness", "糙率n"),
            ("bed_slope", "比降i"),
            ("soil_coefficient", "土渠A"),
            ("rock_depth", "砌石水深h"),
            ("silt_coefficient", "不淤C0"),
            ("minimum_flow", "最小流量Q"),
        ]:
            input_rows.append([label, self.velocity_inputs[key].get(), ""])
        self._show_result(
            "临界不冲流速与不淤流速计算",
            input_rows,
            [["来源", "《水利大算盘》第二章第三、四节"], ["不冲公式", scour.formula], ["不淤公式", silt.formula]],
            [
                ["项目", "结果", "单位/说明"],
                ["土渠水力半径R", f"{scour.design_hydraulic_radius:.3f}", "m"],
                ["土渠不冲流速", f"{scour.soil_velocity:.3f}", "m/s"],
                ["块石等效直径d", f"{scour.equivalent_block_diameter:.3f}", "m"],
                ["砌石护面不冲流速", f"{scour.rock_lining_velocity:.3f}", "m/s"],
                ["渠道不淤流速", f"{silt.velocity:.3f}", "m/s"],
            ],
        )

    def _calculate_sections(self) -> None:
        try:
            economic = calculate_practical_economic_section(
                PracticalEconomicSectionInput(
                    flow=self._float(self.economic_inputs["flow"], "梯形设计流量Q"),
                    roughness=self._float(self.economic_inputs["roughness"], "梯形糙率n"),
                    bed_slope=self._float(self.economic_inputs["bed_slope"], "梯形比降i"),
                    side_slope=self._float(self.economic_inputs["side_slope"], "梯形边坡m"),
                    area_ratio=self._float(self.economic_inputs["area_ratio"], "面积偏离系数a"),
                )
            )
            u_channel = solve_u_channel_by_width_depth_ratio(
                target_flow=self._float(self.economic_inputs["u_flow"], "U形设计流量Q"),
                width_depth_ratio=self._float(self.economic_inputs["u_ratio"], "U形h/B"),
                roughness=self._float(self.economic_inputs["u_roughness"], "U形糙率n"),
                bed_slope=self._float(self.economic_inputs["u_bed_slope"], "U形比降i"),
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        self._show_result(
            "最佳实用经济断面与U形断面计算",
            [
                ["参数", "取值", "说明"],
                ["梯形Q", self.economic_inputs["flow"].get(), "m³/s"],
                ["梯形m", self.economic_inputs["side_slope"].get(), ""],
                ["面积偏离系数a", self.economic_inputs["area_ratio"].get(), "通常1.01~1.04"],
                ["U形Q", self.economic_inputs["u_flow"].get(), "m³/s"],
                ["U形h/B", self.economic_inputs["u_ratio"].get(), "必须大于0.5"],
            ],
            [["来源", "《水利大算盘》第二章第七、十一节"], ["梯形公式", economic.formula], ["U形公式", u_channel.formula]],
            [
                ["项目", "结果", "单位/说明"],
                ["最佳断面水深hm", f"{economic.best_hydraulic_depth:.3f}", "m"],
                ["实用经济水深h", f"{economic.water_depth:.3f}", "m"],
                ["实用经济底宽b", f"{economic.bottom_width:.3f}", "m"],
                ["实用经济b/h", f"{economic.width_depth_ratio:.3f}", ""],
                ["实用经济复算Q", f"{economic.check_flow:.2f}", "m³/s"],
                ["U形水深h", f"{u_channel.water_depth:.3f}", "m"],
                ["U形水面宽B", f"{u_channel.water_width:.3f}", "m"],
                ["U形半圆半径", f"{u_channel.semicircle_radius:.3f}", "m"],
                ["U形流速v", f"{u_channel.velocity:.3f}", "m/s"],
                ["U形复算Q", f"{u_channel.flow:.2f}", "m³/s"],
            ],
        )

    def _calculate_seepage(self) -> None:
        try:
            lined = calculate_lined_seepage_loss(
                LinedSeepageInput(
                    channel_length=self._float(self.seepage_inputs["lined_length"], "衬砌渠道长度l"),
                    lining_coefficient=self._float(self.seepage_inputs["lining_coefficient"], "衬砌系数c1"),
                    bottom_width=self._float(self.seepage_inputs["bottom_width"], "渠底宽b"),
                    water_depth=self._float(self.seepage_inputs["water_depth"], "水深h"),
                    side_slope=self._float(self.seepage_inputs["side_slope"], "边坡系数m"),
                    roughness=self._float(self.seepage_inputs["roughness"], "糙率n"),
                    bed_slope=self._float(self.seepage_inputs["bed_slope"], "渠底比降i"),
                )
            )
            unlined = calculate_unlined_seepage_loss(
                UnlinedSeepageInput(
                    channel_length_km=self._float(self.seepage_inputs["unlined_length"], "无衬砌渠道长度L"),
                    permeability_coefficient=self._float(self.seepage_inputs["permeability"], "渗透系数kp"),
                    net_flow=self._float(self.seepage_inputs["net_flow"], "净流量Q净"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        self._show_result(
            "渠道渗漏损失量计算",
            [
                ["参数", "取值", "说明"],
                ["衬砌渠长l", self.seepage_inputs["lined_length"].get(), "m"],
                ["衬砌系数c1", self.seepage_inputs["lining_coefficient"].get(), ""],
                ["无衬砌渠长L", self.seepage_inputs["unlined_length"].get(), "km"],
                ["渗透系数kp", self.seepage_inputs["permeability"].get(), "m/d"],
                ["净流量Q净", self.seepage_inputs["net_flow"].get(), "m³/s"],
            ],
            [["来源", "《水利大算盘》第二章第八节"], ["衬砌公式", lined.formula], ["无衬砌公式", unlined.formula]],
            [
                ["项目", "结果", "单位/说明"],
                ["衬砌渠过水面积", f"{lined.area:.3f}", "m²"],
                ["衬砌渠流速", f"{lined.velocity:.3f}", "m/s"],
                ["衬砌渠渗漏损失", f"{lined.seepage_loss:.3f}", "书中单位m³/(s·d)"],
                ["无衬砌每km损失", f"{unlined.loss_per_km:.4f}", "m³/(s·km)"],
                ["无衬砌总损失", f"{unlined.total_loss:.3f}", "m³/s"],
            ],
        )

    def _calculate_reinforcement(self) -> None:
        try:
            channel_length = self._float(self.steel_inputs["channel_length"], "渠道总长")
            joint_spacing = self._float(self.steel_inputs["joint_spacing"], "伸缩缝间距")
            joint_width = self._float(self.steel_inputs["joint_width"], "伸缩缝宽")
            segment_count = channel_length / (joint_spacing + joint_width)
            result = calculate_reinforcement(
                ReinforcementInput(
                    concrete_volume_m3=self._float(self.steel_inputs["concrete_volume"], "混凝土体积"),
                    loss_rate_percent=self._float(self.steel_inputs["loss_rate"], "损耗率"),
                    groups=(
                        RebarGroupInput(
                            "分布筋φ8",
                            8,
                            0.395,
                            self._float(self.steel_inputs["section_perimeter"], "分布筋结构长度"),
                            self._float(self.steel_inputs["cover_total"], "保护层总厚"),
                            self._float(self.steel_inputs["distribution_spacing"], "分布筋间距"),
                            self._float(self.steel_inputs["distribution_straight"], "分布筋直段长度"),
                            2,
                            segment_count,
                            1,
                        ),
                        RebarGroupInput(
                            "侧墙主筋φ12",
                            12,
                            0.888,
                            self._float(self.steel_inputs["side_main_length"], "侧墙主筋结构长度"),
                            self._float(self.steel_inputs["cover_total"], "保护层总厚"),
                            self._float(self.steel_inputs["main_spacing"], "主筋间距"),
                            self._float(self.steel_inputs["side_straight"], "侧墙主筋直段长度"),
                            0,
                            segment_count,
                            2,
                        ),
                        RebarGroupInput(
                            "底板主筋φ12",
                            12,
                            0.888,
                            self._float(self.steel_inputs["bottom_main_length"], "底板主筋结构长度"),
                            self._float(self.steel_inputs["cover_total"], "保护层总厚"),
                            self._float(self.steel_inputs["main_spacing"], "主筋间距"),
                            self._float(self.steel_inputs["bottom_straight"], "底板主筋直段长度"),
                            0,
                            segment_count,
                            1,
                        ),
                    ),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        group_rows = [["钢筋组", "根数", "单根长(m)", "净重(t)"]]
        for group in result.groups:
            group_rows.append([
                group.name,
                f"{group.root_count:.0f}",
                f"{group.single_bar_length_m:.3f}",
                f"{group.net_weight_kg / 1000.0:.2f}",
            ])
        self._show_result(
            "钢筋总量及含筋率计算",
            [
                ["参数", "取值", "说明"],
                ["渠道总长", self.steel_inputs["channel_length"].get(), "m"],
                ["伸缩缝间距+宽度", f"{self.steel_inputs['joint_spacing'].get()}+{self.steel_inputs['joint_width'].get()}", "m"],
                ["分段数", f"{segment_count:.1f}", "段"],
                ["混凝土体积", self.steel_inputs["concrete_volume"].get(), "m³"],
                ["损耗率", self.steel_inputs["loss_rate"].get(), "%"],
            ],
            [["来源", "《水利大算盘》第二章第十二节"], ["公式", result.formula]],
            group_rows
            + [
                ["合计", "", "", ""],
                ["钢筋净重", f"{result.total_net_weight_kg / 1000.0:.2f}", "t", ""],
                ["含损耗钢筋总量", f"{result.total_gross_weight_t:.2f}", "t", ""],
                ["含筋率", f"{result.gross_ratio_kg_per_m3:.2f}", "kg/m³", ""],
            ],
        )


class ChannelStructureLongitudinalTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        for idx in range(3):
            top.columnconfigure(idx, weight=1)

        sidewall = ttk.LabelFrame(top, text="一、渠道侧墙稳定分析（书第2章第9节）")
        sidewall.grid(row=0, column=0, sticky="nsew", padx=(0, 6))
        self.sidewall_inputs: dict[str, tk.Entry] = {}
        sidewall_fields = [
            ("phi", "内摩擦角φ(°)", "20"),
            ("gamma", "填土容重γ(kN/m³)", "14"),
            ("height", "墙高H(m)", "2"),
            ("groundwater", "墙后水深Hw(m)", "0"),
            ("inner_depth", "渠内水深(m)", "0"),
            ("factor", "组合系数γ", "1.2"),
            ("fc", "混凝土fc(MPa)", "9.6"),
            ("thickness", "墙厚h(mm)", "200"),
            ("cover", "保护层c(mm)", "30"),
            ("diameter", "钢筋直径(mm)", "12"),
            ("fy", "钢筋fy(MPa)", "300"),
            ("spacing", "钢筋间距(mm)", "200"),
        ]
        for idx, (key, label, default) in enumerate(sidewall_fields):
            self.sidewall_inputs[key] = self._entry(sidewall, idx, 0, label, default, width=12)
        ttk.Button(sidewall, text="计算侧墙稳定", command=self._calculate_sidewall_stability).grid(
            row=len(sidewall_fields), column=0, columnspan=2, padx=10, pady=8, sticky="ew"
        )

        compound = ttk.LabelFrame(top, text="二、复式断面渠道水力（书第2章第10节）")
        compound.grid(row=0, column=1, sticky="nsew", padx=6)
        self.compound_inputs: dict[str, tk.Entry] = {}
        compound_fields = [
            ("target_flow", "设计流量Q(m³/s)", "20.01"),
            ("total_depth", "试算总水深h(m)", "3.41"),
            ("main_depth", "深槽水深h1(m)", "1.5"),
            ("main_width", "深槽底宽b1(m)", "1.5"),
            ("main_slope", "深槽边坡m1", "1.5"),
            ("flood_slope", "滩槽边坡m2", "2.0"),
            ("left_width", "左滩底宽(m)", "2.5"),
            ("right_width", "右滩底宽(m)", "2.5"),
            ("n1", "深槽糙率n1", "0.025"),
            ("n2", "滩槽糙率n2", "0.030"),
            ("bed_slope", "渠道比降i", "0.000142857"),
        ]
        for idx, (key, label, default) in enumerate(compound_fields):
            self.compound_inputs[key] = self._entry(compound, idx, 0, label, default, width=12)
        ttk.Button(compound, text="计算复式断面", command=self._calculate_compound_channel).grid(
            row=len(compound_fields), column=0, columnspan=2, padx=10, pady=8, sticky="ew"
        )

        longitudinal = ttk.LabelFrame(top, text="三、渠道纵断面设计（书第2章第13节）")
        longitudinal.grid(row=0, column=2, sticky="nsew", padx=(6, 0))
        self.longitudinal_inputs: dict[str, tk.Entry] = {}
        longitudinal_fields = [
            ("first_bed", "首桩渠底高程(m)", "100"),
            ("bed_slope", "渠底比降i", "0.001"),
            ("min_depth", "最小水深(m)", "1.0"),
            ("design_depth", "设计水深(m)", "2.0"),
            ("enlarged_depth", "加大水深(m)", "2.4"),
            ("freeboard", "渠堤超高(m)", "0.6"),
            ("ground1", "1号桩地面(m)", "103"),
            ("dist2", "2号桩距(m)", "100"),
            ("ground2", "2号桩地面(m)", "102.5"),
            ("dist3", "3号桩距(m)", "100"),
            ("ground3", "3号桩地面(m)", "102.2"),
        ]
        for idx, (key, label, default) in enumerate(longitudinal_fields):
            self.longitudinal_inputs[key] = self._entry(longitudinal, idx, 0, label, default, width=12)
        ttk.Button(longitudinal, text="计算纵断面", command=self._calculate_longitudinal_design).grid(
            row=len(longitudinal_fields), column=0, columnspan=2, padx=10, pady=8, sticky="ew"
        )

        self.output = self._result_box(self)

    def _show_result(self, title: str, input_rows: list[list[str]], formula_rows: list[list[str]], result_rows: list[list[str]]) -> None:
        self._write(
            self.output,
            "\n".join(
                [
                    title,
                    "",
                    "一、输入参数",
                    format_fixed_table(input_rows, min_width=14),
                    "",
                    "二、采用公式",
                    format_fixed_table(formula_rows, min_width=20),
                    "",
                    "三、计算结果",
                    format_fixed_table(result_rows, min_width=18),
                    "",
                    "四、说明",
                    "本页为《水利大算盘》第二章渠道结构与纵断面程序的本地接入版，正式成果应结合工程等级、材料指标和规范限值复核。",
                ]
            ),
        )

    def _calculate_sidewall_stability(self) -> None:
        try:
            result = calculate_channel_sidewall_stability(
                ChannelSidewallStabilityInput(
                    soil_friction_angle_deg=self._float(self.sidewall_inputs["phi"], "内摩擦角φ"),
                    soil_unit_weight=self._float(self.sidewall_inputs["gamma"], "填土容重γ"),
                    wall_height=self._float(self.sidewall_inputs["height"], "墙高H"),
                    groundwater_depth=self._float(self.sidewall_inputs["groundwater"], "墙后水深Hw"),
                    channel_inner_water_depth=self._float(self.sidewall_inputs["inner_depth"], "渠内水深"),
                    structure_factor=self._float(self.sidewall_inputs["factor"], "组合系数γ"),
                    concrete_fc=self._float(self.sidewall_inputs["fc"], "混凝土fc"),
                    wall_thickness_mm=self._float(self.sidewall_inputs["thickness"], "墙厚h"),
                    cover_thickness_mm=self._float(self.sidewall_inputs["cover"], "保护层c"),
                    steel_diameter_mm=self._float(self.sidewall_inputs["diameter"], "钢筋直径"),
                    steel_fy=self._float(self.sidewall_inputs["fy"], "钢筋fy"),
                    steel_spacing_mm=self._float(self.sidewall_inputs["spacing"], "钢筋间距"),
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        self._show_result(
            "渠道侧墙稳定分析计算",
            [
                ["参数", "取值", "说明"],
                ["φ", self.sidewall_inputs["phi"].get(), "填土有效内摩擦角"],
                ["γ", self.sidewall_inputs["gamma"].get(), "填土容重，kN/m³"],
                ["H", self.sidewall_inputs["height"].get(), "墙高，m"],
                ["Hw", self.sidewall_inputs["groundwater"].get(), "墙后地下水深，m"],
                ["渠内水深", self.sidewall_inputs["inner_depth"].get(), "抵消内水压力"],
                ["墙厚/保护层/钢筋", f"{self.sidewall_inputs['thickness'].get()}/{self.sidewall_inputs['cover'].get()}/{self.sidewall_inputs['diameter'].get()}", "mm"],
            ],
            [["来源", "《水利大算盘》第二章第九节"], ["公式", result.formula]],
            [
                ["项目", "结果", "单位/判断"],
                ["主动土压力系数K", f"{result.earth_pressure_coefficient:.3f}", ""],
                ["土压力E", f"{result.earth_pressure:.2f}", "kN/m"],
                ["地下水压力", f"{result.groundwater_pressure:.2f}", "kN/m"],
                ["渠内水压力", f"{result.inner_water_pressure:.2f}", "kN/m"],
                ["合力ΣE", f"{result.total_pressure:.2f}", "kN/m"],
                ["弯矩M", f"{result.bending_moment:.2f}", "kN·m"],
                ["有效高度h0", f"{result.effective_height_mm:.1f}", "mm"],
                ["截面抵抗系数as", f"{result.resistance_coefficient:.3f}", ""],
                ["相对受压区高度ε", f"{result.compression_zone_height_ratio:.4f}", ""],
                ["所需钢筋面积As", f"{result.required_steel_area_mm2:.1f}", "mm²/m"],
                ["实配钢筋面积", f"{result.provided_steel_area_mm2:.1f}", "mm²/m"],
                ["含筋率ρ", f"{result.steel_ratio_percent:.3f}", "%"],
                ["校核结论", "满足" if result.stable else "不满足", ""],
            ],
        )

    def _compound_data(self, total_depth: float | None = None) -> CompoundChannelInput:
        return CompoundChannelInput(
            total_depth=total_depth if total_depth is not None else self._float(self.compound_inputs["total_depth"], "试算总水深h"),
            main_depth=self._float(self.compound_inputs["main_depth"], "深槽水深h1"),
            main_bottom_width=self._float(self.compound_inputs["main_width"], "深槽底宽b1"),
            main_side_slope=self._float(self.compound_inputs["main_slope"], "深槽边坡m1"),
            floodplain_side_slope=self._float(self.compound_inputs["flood_slope"], "滩槽边坡m2"),
            left_floodplain_width=self._float(self.compound_inputs["left_width"], "左滩底宽"),
            right_floodplain_width=self._float(self.compound_inputs["right_width"], "右滩底宽"),
            main_roughness=self._float(self.compound_inputs["n1"], "深槽糙率n1"),
            floodplain_roughness=self._float(self.compound_inputs["n2"], "滩槽糙率n2"),
            bed_slope=self._float(self.compound_inputs["bed_slope"], "渠道比降i"),
        )

    def _calculate_compound_channel(self) -> None:
        try:
            data = self._compound_data()
            result = calculate_compound_channel(data)
            target_flow = self._float(self.compound_inputs["target_flow"], "设计流量Q")
            integral_depth = solve_compound_total_depth_for_flow(target_flow, data, method="integral")
            divided_depth = solve_compound_total_depth_for_flow(target_flow, data, method="divided")
            recommended_depth = (integral_depth + divided_depth) / 2.0
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        self._show_result(
            "复式断面渠道水力计算",
            [
                ["参数", "取值", "说明"],
                ["Q", self.compound_inputs["target_flow"].get(), "设计流量，m³/s"],
                ["h/h1", f"{self.compound_inputs['total_depth'].get()}/{self.compound_inputs['main_depth'].get()}", "总水深/深槽水深"],
                ["b1", self.compound_inputs["main_width"].get(), "深槽底宽"],
                ["b2左/b2右", f"{self.compound_inputs['left_width'].get()}/{self.compound_inputs['right_width'].get()}", "滩地底宽"],
                ["m1/m2", f"{self.compound_inputs['main_slope'].get()}/{self.compound_inputs['flood_slope'].get()}", "深槽/滩槽边坡"],
                ["n1/n2", f"{self.compound_inputs['n1'].get()}/{self.compound_inputs['n2'].get()}", "深槽/滩槽糙率"],
                ["i", self.compound_inputs["bed_slope"].get(), "渠道比降"],
            ],
            [["来源", "《水利大算盘》第二章第十节"], ["公式", result.formula]],
            [
                ["项目", "结果", "单位/说明"],
                ["整体断面面积ω", f"{result.total_area:.3f}", "m²"],
                ["整体湿周x", f"{result.total_wetted_perimeter:.3f}", "m"],
                ["综合糙率n综", f"{result.composite_roughness:.4f}", ""],
                ["整体法复算Q", f"{result.integral_flow:.2f}", "m³/s"],
                ["深槽面积ω1", f"{result.main_area:.3f}", "m²"],
                ["滩槽面积ω2", f"{result.floodplain_area:.3f}", "m²"],
                ["深槽流量Q1", f"{result.main_flow:.2f}", "m³/s"],
                ["滩槽流量Q2", f"{result.floodplain_flow:.2f}", "m³/s"],
                ["分块法复算Q", f"{result.divided_flow:.2f}", "m³/s"],
                ["整体法目标水深", f"{integral_depth:.3f}", "m"],
                ["分块法目标水深", f"{divided_depth:.3f}", "m"],
                ["平均建议水深", f"{recommended_depth:.3f}", "m"],
            ],
        )

    def _calculate_longitudinal_design(self) -> None:
        try:
            points = (
                LongitudinalPointInput(1, self._float(self.longitudinal_inputs["ground1"], "1号桩地面高程"), 0.0),
                LongitudinalPointInput(2, self._float(self.longitudinal_inputs["ground2"], "2号桩地面高程"), self._float(self.longitudinal_inputs["dist2"], "2号桩距")),
                LongitudinalPointInput(3, self._float(self.longitudinal_inputs["ground3"], "3号桩地面高程"), self._float(self.longitudinal_inputs["dist3"], "3号桩距")),
            )
            result = calculate_channel_longitudinal_design(
                LongitudinalDesignInput(
                    bed_slope=self._float(self.longitudinal_inputs["bed_slope"], "渠底比降i"),
                    design_depth=self._float(self.longitudinal_inputs["design_depth"], "设计水深"),
                    minimum_depth=self._float(self.longitudinal_inputs["min_depth"], "最小水深"),
                    enlarged_depth=self._float(self.longitudinal_inputs["enlarged_depth"], "加大水深"),
                    freeboard=self._float(self.longitudinal_inputs["freeboard"], "渠堤超高"),
                    first_bed_elevation=self._float(self.longitudinal_inputs["first_bed"], "首桩渠底高程"),
                    points=points,
                )
            )
        except Exception as exc:
            messagebox.showerror("输入错误", str(exc))
            return
        result_rows = [["桩号", "地面", "渠底", "最低水位", "设计水位", "加大水位", "渠顶", "挖深", "填高"]]
        for point in result.points:
            result_rows.append(
                [
                    str(point.station_no),
                    f"{point.ground_elevation:.3f}",
                    f"{point.bed_elevation:.3f}",
                    f"{point.minimum_water_level:.3f}",
                    f"{point.design_water_level:.3f}",
                    f"{point.enlarged_water_level:.3f}",
                    f"{point.top_elevation:.3f}",
                    f"{point.excavation_depth:.3f}",
                    f"{point.fill_height:.3f}",
                ]
            )
        self._show_result(
            "渠道纵断面设计计算",
            [
                ["参数", "取值", "说明"],
                ["首桩渠底", self.longitudinal_inputs["first_bed"].get(), "m"],
                ["渠底比降", self.longitudinal_inputs["bed_slope"].get(), ""],
                ["最小/设计/加大水深", f"{self.longitudinal_inputs['min_depth'].get()}/{self.longitudinal_inputs['design_depth'].get()}/{self.longitudinal_inputs['enlarged_depth'].get()}", "m"],
                ["渠堤超高", self.longitudinal_inputs["freeboard"].get(), "m"],
            ],
            [["来源", "《水利大算盘》第二章第十三节"], ["公式", result.formula]],
            result_rows,
        )


class TieyuanTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="流域特征参数")
        basic.grid(row=0, column=0, sticky="ew")
        self.inputs: dict[str, tk.Entry] = {}
        fields = [
            ("basin", "流域名称", "示例流域"),
            ("area", "面积F(km²)", "25.6"),
            ("l1", "河长L1(km)", "8.3"),
            ("j1", "河坡J1(‰)", "45"),
            ("l2", "坡面长度L2(km)", ""),
            ("i2", "山坡平均坡度I2(‰)", ""),
        ]
        for idx, (key, label, default) in enumerate(fields):
            self.inputs[key] = self._entry(basic, idx // 2, idx % 2, label, default, width=16)

        params = ttk.LabelFrame(top, text="可调参数（留空使用默认值）")
        params.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        parameter_layout = [
            ("a1", "A1:", "0.341", "（默认0.341）", 0, 0),
            ("a2", "A2:", "0.025", "（默认0.025）", 0, 4),
            ("n", "n:", "0.65", "（默认0.65）", 1, 0),
            ("r_param", "R:", "1.02", "（默认1.02）", 1, 4),
        ]
        for key, label, default, note, row, col in parameter_layout:
            ttk.Label(params, text=label).grid(row=row, column=col, padx=(10, 4), pady=6, sticky="e")
            entry = ttk.Entry(params, width=12)
            entry.insert(0, default)
            entry.grid(row=row, column=col + 1, padx=(0, 4), pady=6, sticky="w")
            self.inputs[key] = entry
            ttk.Label(params, text=note, foreground="#777777").grid(row=row, column=col + 2, padx=(0, 16), pady=6, sticky="w")
        self.gamma_var = tk.StringVar(value="（自动）")
        self.y_var = tk.StringVar(value="（自动）")
        ttk.Label(params, text="γ:").grid(row=0, column=8, padx=(10, 4), pady=6, sticky="e")
        ttk.Label(params, textvariable=self.gamma_var, foreground="blue", font=(DASHBOARD_CARD_FONT, 9, "bold")).grid(row=0, column=9, padx=(0, 16), pady=6, sticky="w")
        ttk.Label(params, text="y:").grid(row=1, column=8, padx=(10, 4), pady=6, sticky="e")
        ttk.Label(params, textvariable=self.y_var, foreground="blue", font=(DASHBOARD_CARD_FONT, 9, "bold")).grid(row=1, column=9, padx=(0, 16), pady=6, sticky="w")
        ttk.Label(params, text="土类:").grid(row=2, column=0, padx=(10, 4), pady=6, sticky="e")
        self.tie_soil_class = tk.StringVar(value="III")
        soil_combo = ttk.Combobox(params, textvariable=self.tie_soil_class, values=list(TIE_SOIL_CLASS_PARAMS), width=10, state="readonly")
        soil_combo.grid(row=2, column=1, padx=(0, 4), pady=6, sticky="w")
        soil_combo.bind("<<ComboboxSelected>>", lambda _event: self._update_tie_soil_params())
        ttk.Label(params, text="前期水分:").grid(row=2, column=2, padx=(10, 4), pady=6, sticky="e")
        self.tie_moisture = tk.StringVar(value="medium")
        moisture_combo = ttk.Combobox(params, textvariable=self.tie_moisture, values=["wet", "medium", "dry"], width=10, state="readonly")
        moisture_combo.grid(row=2, column=3, padx=(0, 4), pady=6, sticky="w")
        moisture_combo.bind("<<ComboboxSelected>>", lambda _event: self._update_tie_soil_params())
        self.r1_var = tk.StringVar(value="0.69（自动）")
        ttk.Label(params, text="r1:").grid(row=2, column=4, padx=(10, 4), pady=6, sticky="e")
        ttk.Label(params, textvariable=self.r1_var, foreground="blue", font=(DASHBOARD_CARD_FONT, 9, "bold")).grid(row=2, column=5, padx=(0, 16), pady=6, sticky="w")
        self.inputs["runoff_c"] = self._entry(params, 2, 3, "C(径流系数)", "0.86", width=10)
        ttk.Label(params, text="地貌:").grid(row=2, column=8, padx=(10, 4), pady=6, sticky="e")
        self.tie_landform = tk.StringVar(value="高低山")
        ttk.Combobox(params, textvariable=self.tie_landform, values=["高低山", "丘陵", "平坦"], width=10, state="readonly").grid(
            row=2, column=9, padx=(0, 16), pady=6, sticky="w"
        )
        self.tie_c_auto = tk.BooleanVar(value=True)
        ttk.Checkbutton(params, text="按表3-3自动查算C", variable=self.tie_c_auto, command=self._update_tie_c_mode).grid(row=3, column=0, columnspan=3, padx=10, pady=(0, 6), sticky="w")
        ttk.Label(params, text="自动查算时：按土类、前期水分、地貌、F、S选取C；取消勾选时使用手动C。", foreground="#777777").grid(
            row=3, column=3, columnspan=8, padx=(10, 16), pady=(0, 6), sticky="w"
        )
        self._update_tie_c_mode()

        rain_box = ttk.LabelFrame(top, text="24小时点雨量统计参数（自动换算各频率面雨量H₂₄）")
        rain_box.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        rain_fields = [
            ("h24_mean", "24小时点雨量均值(mm)", "70"),
            ("h24_cv", "Cv", "0.47"),
            ("h24_cs_ratio", "Cs/Cv", "3.5"),
            ("areal_reduction", "点面折减系数η", "1.00"),
            ("shape_correction", "流域形状修正系数φ", "1.00"),
        ]
        for idx, (key, label, default) in enumerate(rain_fields):
            self.inputs[key] = self._entry(rain_box, idx // 3, idx % 3, label, default, width=13)

        table = ttk.Frame(rain_box)
        table.grid(row=2, column=0, columnspan=10, sticky="ew", padx=10, pady=(6, 2))
        ttk.Label(table, text="频率").grid(row=0, column=0, padx=4, pady=3)
        ttk.Label(table, text="Kp").grid(row=1, column=0, padx=4, pady=3)
        ttk.Label(table, text="点雨量(mm)").grid(row=2, column=0, padx=4, pady=3)
        ttk.Label(table, text="面雨量H₂₄(mm)").grid(row=3, column=0, padx=4, pady=3)
        self.tie_kp_vars: dict[str, tk.StringVar] = {}
        self.tie_point_vars: dict[str, tk.StringVar] = {}
        self.tie_areal_vars: dict[str, tk.StringVar] = {}
        for idx, freq in enumerate(TIE_FREQUENCIES, 1):
            ttk.Label(table, text=f"{freq}年").grid(row=0, column=idx, padx=4, pady=3)
            self.tie_kp_vars[freq] = tk.StringVar(value="---")
            self.tie_point_vars[freq] = tk.StringVar(value="---")
            self.tie_areal_vars[freq] = tk.StringVar(value="---")
            ttk.Label(table, textvariable=self.tie_kp_vars[freq], width=9, anchor="center", relief=tk.SUNKEN).grid(row=1, column=idx, padx=2, pady=2)
            ttk.Label(table, textvariable=self.tie_point_vars[freq], width=9, anchor="center", relief=tk.SUNKEN).grid(row=2, column=idx, padx=2, pady=2)
            ttk.Label(table, textvariable=self.tie_areal_vars[freq], width=9, anchor="center", relief=tk.SUNKEN).grid(row=3, column=idx, padx=2, pady=2)
        ttk.Label(rain_box, text="H₂₄面雨量 = 24小时点雨量均值 × Kp × 点面折减系数η × 流域形状修正系数φ；若输入已为面雨量均值，η、φ均取1.00。", foreground="#666666").grid(
            row=3, column=0, columnspan=10, padx=10, pady=(2, 8), sticky="w"
        )

        actions = ttk.Frame(top)
        actions.grid(row=3, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#2fa84f", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空结果", self.clear_result).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def clear_result(self) -> None:
        self._write(self.output, "")

    def _update_tie_soil_params(self) -> None:
        params = TIE_SOIL_CLASS_PARAMS[self.tie_soil_class.get()][self.tie_moisture.get()]
        self.inputs["r_param"].delete(0, tk.END)
        self.inputs["r_param"].insert(0, str(params["r"]))
        self.r1_var.set(f"{params['r1']:.2f}（自动）")

    def _update_tie_c_mode(self) -> None:
        state = "disabled" if self.tie_c_auto.get() else "normal"
        self.inputs["runoff_c"].configure(state=state)

    def calculate(self) -> None:
        try:
            h24_mean = self._float(self.inputs["h24_mean"], "24小时点雨量均值")
            h24_cv = self._float(self.inputs["h24_cv"], "Cv")
            h24_cs_ratio = self._float(self.inputs["h24_cs_ratio"], "Cs/Cv")
            areal_reduction = self._float(self.inputs["areal_reduction"], "点面折减系数η")
            shape_correction = self._float(self.inputs["shape_correction"], "流域形状修正系数φ")
            soil_class = self.tie_soil_class.get()
            moisture_condition = self.tie_moisture.get()
            soil_params = TIE_SOIL_CLASS_PARAMS[soil_class][moisture_condition]
            runoff_c = None if self.tie_c_auto.get() else self._float(self.inputs["runoff_c"], "径流系数 C")
            kp_values, point_rainfall, areal_rainfall = tieyuan_design_rainfall_values(
                h24_mean,
                h24_cv,
                areal_reduction,
                TIE_FREQUENCIES,
                h24_cs_ratio,
                shape_correction,
            )
            for freq in TIE_FREQUENCIES:
                self.tie_kp_vars[freq].set(f"{kp_values[freq]:.3f}")
                self.tie_point_vars[freq].set(f"{point_rainfall[freq]:.2f}")
                self.tie_areal_vars[freq].set(f"{areal_rainfall[freq]:.2f}")

            result = calculate_tieyuan(
                TieyuanInput(
                    basin_name=self.inputs["basin"].get().strip() or "未命名流域",
                    area=self._float(self.inputs["area"], "面积 F"),
                    river_length_l1=self._float(self.inputs["l1"], "河长 L1"),
                    river_slope_j1=self._float(self.inputs["j1"], "河坡 J1"),
                    rainfall_24h=areal_rainfall,
                    slope_length_l2=float(self.inputs["l2"].get().strip()) if self.inputs["l2"].get().strip() else None,
                    slope_slope_i2=float(self.inputs["i2"].get().strip()) if self.inputs["i2"].get().strip() else None,
                    a1=self._float(self.inputs["a1"], "A1"),
                    a2=self._float(self.inputs["a2"], "A2"),
                    n=self._float(self.inputs["n"], "n"),
                    soil_class=soil_class,
                    moisture_condition=moisture_condition,
                    runoff_coefficient=runoff_c,
                    runoff_coefficient_auto=self.tie_c_auto.get(),
                    c_landform=self.tie_landform.get(),
                    r_param=self._float(self.inputs["r_param"], "R"),
                    r1_param=soil_params["r1"],
                    rainfall_24h_is_areal=True,
                )
            )
            self.gamma_var.set(f"{result.gamma:.4f}")
            self.y_var.set(f"{result.y:.4f}")

            input_rows = [
                ["项目", "数值"],
                ["流域名称", result.basin_name],
                ["面积F(km²)", f"{self._float(self.inputs['area'], '面积 F'):g}"],
                ["河长L1(km)", f"{self._float(self.inputs['l1'], '河长 L1'):g}"],
                ["河坡J1(‰)", f"{self._float(self.inputs['j1'], '河坡 J1'):g}"],
                ["24小时点雨量均值(mm)", f"{h24_mean:g}"],
                ["Cv", f"{h24_cv:g}"],
                ["Cs/Cv", f"{h24_cs_ratio:g}"],
                ["点面折减系数η", f"{areal_reduction:g}"],
                ["流域形状修正系数φ", f"{shape_correction:g}"],
                ["A1", f"{result.a1:g}"],
                ["A2", f"{result.a2:g}"],
                ["n", f"{result.n:g}"],
                ["土类", result.soil_class],
                ["前期土壤水分", result.moisture_condition],
                ["表3-3地貌类型", result.c_landform],
                ["R", f"{result.r_param:g}"],
                ["r1", f"{result.r1_param:g}"],
                ["C计算方式", "表3-3自动查算" if result.runoff_coefficient_auto else "手动输入"],
                ["C(手动输入值)", "" if result.runoff_coefficient is None else f"{result.runoff_coefficient:g}"],
                ["C(实际采用值)", "见洪峰流量成果表逐频率C列"],
                ["γ(自动计算)", f"{result.gamma}"],
                ["y(自动计算)", f"{result.y}"],
            ]
            probability_map = {"100": 1.0, "50": 2.0, "30": 100 / 30, "25": 4.0, "20": 5.0, "10": 10.0, "5": 20.0, "2": 50.0}
            rain_rows = [["频率", "设计频率P(%)", "Kp", "24小时点雨量(mm)", "点面折减系数η", "流域形状修正系数φ", "24小时面雨量H₂₄(mm)", "暴雨参数S"]]
            for freq in TIE_FREQUENCIES:
                sp = areal_rainfall[freq] * (24 ** (result.n - 1))
                rain_rows.append([
                    f"{freq}年",
                    f"{probability_map[freq]:.3g}",
                    f"{kp_values[freq]:.3f}",
                    f"{point_rainfall[freq]:.3f}",
                    f"{areal_reduction:g}",
                    f"{shape_correction:g}",
                    f"{areal_rainfall[freq]:.3f}",
                    f"{sp:.3f}",
                ])
            confluence_rows = [
                ["参数", "数值"],
                ["L2", result.l2],
                ["J2", result.j2],
                ["K1", result.k1],
                ["K₂", result.k2],
                ["X=K₁+0.95K₂", result.x],
                ["R(径流系数C值表)", result.r_param],
                ["r1(径流系数C值表)", result.r1_param],
                ["γ", result.gamma],
                ["y", result.y],
            ]
            flow_rows = [["频率", "面雨量H₂₄(mm)", "S", "C", "表3-3t(h)", "损失因子k₂", "Cₙ", "n′", "P₁", "P", "Q(m³/s)"]]
            for freq in TIE_FREQUENCIES:
                params = result.frequency_params.get(freq)
                if params is None:
                    flow_rows.append([f"{freq}年", "", "", "", "", "", "", "", "", "", ""])
                    continue
                c_table_t = "" if params.c_table_t is None else f"{params.c_table_t:.3f}"
                flow_rows.append([
                    f"{freq}年",
                    f"{params.rainfall_24h:.3f}",
                    f"{params.storm_parameter_s:.3f}",
                    f"{params.runoff_coefficient:.3f}",
                    c_table_t,
                    f"{params.loss_factor_k2:.3f}",
                    f"{params.cn:.3f}",
                    f"{params.n_prime:.3f}",
                    f"{params.p1:.3f}",
                    f"{params.p:.3f}",
                    "" if params.q is None else params.q,
                ])
            lines = [
                "一、输入参数表",
                format_fixed_table(input_rows, min_width=12),
                "",
                "二、暴雨参数表",
                format_fixed_table(rain_rows, min_width=12),
                "",
                "三、汇流及折减参数表",
                format_fixed_table(confluence_rows, min_width=12),
                "",
                "四、洪峰流量成果表",
                format_fixed_table(flow_rows, min_width=10),
            ]
            if result.warnings:
                lines.append("")
                lines.append("提示：")
                lines.extend(result.warnings)
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class SmallRiverTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="流域特征参数")
        basic.grid(row=0, column=0, sticky="ew")
        basic.columnconfigure(5, weight=1)
        self.inputs: dict[str, tk.Entry] = {}
        fields = [
            ("basin", "流域名称", "示例流域"),
            ("area", "面积F(km²)", "25.6"),
            ("length", "河长L(km)", "8.3"),
            ("slope", "比降J", "0.045"),
            ("cv", "Cv", "0.6"),
        ]
        for idx, (key, label, default) in enumerate(fields):
            self.inputs[key] = self._entry(basic, idx // 3, idx % 3, label, default, width=15)
        ttk.Label(basic, text="分区").grid(row=1, column=4, padx=(10, 4), pady=6, sticky="e")
        self.region = ttk.Combobox(
            basic,
            width=28,
            state="readonly",
            values=[f"{region_id}. {name}" for region_id, name in SMALL_RIVER_REGION_NAMES.items()],
        )
        self.region.current(0)
        self.region.grid(row=1, column=5, padx=(0, 12), pady=6, sticky="w")
        self.duration_hint = tk.StringVar(value="")
        ttk.Label(
            basic,
            textvariable=self.duration_hint,
            foreground="#0f5d8f",
            wraplength=980,
            justify=tk.LEFT,
            font=(DASHBOARD_CARD_FONT, 9, "bold"),
        ).grid(row=2, column=0, columnspan=6, padx=10, pady=(2, 8), sticky="w")

        rain_box = ttk.LabelFrame(top, text="H输入：面积对应历时的流域平均雨量(mm)")
        rain_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        for col in range(8):
            rain_box.columnconfigure(col, weight=1 if col == 7 else 0)

        ttk.Label(
            rain_box,
            text=SMALL_RIVER_RAINFALL_SCOPE_TEXT,
            foreground="#8a4b00",
            wraplength=980,
            justify=tk.LEFT,
        ).grid(row=0, column=0, columnspan=8, padx=10, pady=(8, 2), sticky="w")

        self.rainfall_mode = tk.StringVar(value="design")
        mode_row = ttk.Frame(rain_box)
        mode_row.grid(row=1, column=0, columnspan=8, padx=10, pady=(2, 2), sticky="w")
        ttk.Radiobutton(
            mode_row,
            text="直接输入各频率流域平均设计H（推荐）",
            variable=self.rainfall_mode,
            value="design",
            command=self._sync_rainfall_mode,
        ).pack(side=tk.LEFT)
        ttk.Radiobutton(
            mode_row,
            text="输入流域平均雨量均值H0，按Cv自动乘频率Kp",
            variable=self.rainfall_mode,
            value="mean",
            command=self._sync_rainfall_mode,
        ).pack(side=tk.LEFT, padx=(18, 0))

        self.mean_rainfall = self._entry(rain_box, 2, 0, "H0流域平均雨量均值(mm)", "70", width=10)
        ttk.Label(
            rain_box,
            text="均值模式：设计H=流域平均H0×频率模比系数Kp。单站点雨量须先做面积平均或点面折减。",
            foreground="#666666",
        ).grid(row=2, column=2, columnspan=6, padx=(0, 12), pady=6, sticky="w")

        defaults = {"10": "70", "5": "85", "2": "105", "1": "120"}
        self.rainfall: dict[str, tk.Entry] = {}
        for idx, freq in enumerate(SMALL_RIVER_FREQUENCIES):
            self.rainfall[freq] = self._entry(rain_box, 3, idx, f"H{freq}%流域平均H(mm)", defaults[freq], width=10)

        ttk.Label(
            rain_box,
            text=SMALL_RIVER_DURATION_RULE_TEXT,
            foreground="#666666",
            wraplength=980,
            justify=tk.LEFT,
        ).grid(row=4, column=0, columnspan=8, padx=10, pady=(2, 8), sticky="w")
        self.inputs["area"].bind("<KeyRelease>", self._update_duration_hint)
        self.inputs["area"].bind("<FocusOut>", self._update_duration_hint)
        self._update_duration_hint()
        self._sync_rainfall_mode()

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#2fa84f", fg="white").pack(side=tk.LEFT, padx=(16, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def _sync_rainfall_mode(self) -> None:
        mean_mode = self.rainfall_mode.get() == "mean"
        self.mean_rainfall.configure(state=tk.NORMAL if mean_mode else tk.DISABLED)
        self.inputs["cv"].configure(state=tk.NORMAL if mean_mode else tk.DISABLED)
        for entry in self.rainfall.values():
            entry.configure(state=tk.DISABLED if mean_mode else tk.NORMAL)

    def _update_duration_hint(self, _event=None) -> None:
        try:
            area = float(self.inputs["area"].get().strip())
            duration = rainfall_duration(area)
            self.duration_hint.set(
                f"当前面积F={area:g} km²：公式H应采用{duration}、同一频率的流域平均雨量。"
            )
        except Exception:
            self.duration_hint.set("先输入面积F，软件会立即提示H0应采用的暴雨历时。")

    def calculate(self) -> None:
        try:
            area = self._float(self.inputs["area"], "面积 F")
            mean_mode = self.rainfall_mode.get() == "mean"
            if mean_mode:
                mean_h = self._float(self.mean_rainfall, f"{rainfall_duration(area)}流域平均雨量均值H0")
                rainfall = {freq: mean_h for freq in SMALL_RIVER_FREQUENCIES}
                mode_text = "输入流域平均雨量均值H0，按Cv和Cs=3.5Cv计算频率Kp，设计H=H0×Kp"
            else:
                rainfall = {freq: self._float(entry, f"H{freq}%流域平均设计H") for freq, entry in self.rainfall.items()}
                mode_text = "直接输入各频率流域平均设计H，不再乘Kp"
            region_id = int(self.region.get().split(".", 1)[0])
            result = calculate_small_river(
                SmallRiverInput(
                    basin_name=self.inputs["basin"].get().strip() or "未命名流域",
                    region=region_id,
                    area=area,
                    river_length=self._float(self.inputs["length"], "河长 L"),
                    slope=self._float(self.inputs["slope"], "比降 J"),
                    cv=self._float(self.inputs["cv"], "Cv") if mean_mode else 0.0,
                    rainfall=rainfall,
                    rainfall_is_mean=mean_mode,
                )
            )
            lines = [
                f"流域：{result.basin_name}",
                f"分区：{region_id}. {SMALL_RIVER_REGION_NAMES[region_id]}",
                f"形状系数 f={result.shape_factor}",
                f"H采用历时：{result.duration}（由面积F={area:g} km²自动判定）",
                f"H输入口径：{SMALL_RIVER_RAINFALL_SCOPE_TEXT}",
                f"H输入模式：{mode_text}",
                "",
            ]
            result_rows = [["频率", "频率Kp", "输入H0或流域平均设计H(mm)", "代入公式的流域平均H(mm)", "Q(m³/s)"]]
            for freq in SMALL_RIVER_FREQUENCIES:
                kp_text = "不计算" if result.kp_values[freq] is None else result.kp_values[freq]
                result_rows.append([f"{freq}%", kp_text, rainfall[freq], result.design_rainfall[freq], result.flows[freq]])
            lines.append(format_fixed_table(result_rows, min_width=10))
            lines.extend(["", "所选分区参数表"])
            parameter_rows = [["频率", "K", "b", "n", "m", "α", "β", "公式形式"]]
            for freq, k, b, n, m, alpha, beta in small_river_parameter_rows(region_id):
                parameter_rows.append([
                    f"{freq}%",
                    f"{k:g}",
                    f"{b:g}",
                    f"{n:g}",
                    f"{m:g}",
                    f"{alpha:g}",
                    f"{beta:g}",
                    "Q=10^(K*log10(F^n*H^m*J^α*f^β)+b)",
                ])
            lines.append(format_fixed_table(parameter_rows, min_width=10))
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class AreaAnalogyTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.rowconfigure(0, weight=1)

        left = ttk.Frame(self)
        left.grid(row=0, column=0, sticky="nsew", padx=(12, 6), pady=12)
        left.columnconfigure(0, weight=1)
        left.rowconfigure(5, weight=1)

        right = ttk.Frame(self)
        right.grid(row=0, column=1, sticky="nsew", padx=(6, 12), pady=12)
        right.columnconfigure(0, weight=1)
        right.rowconfigure(0, weight=1)

        ref_box = ttk.LabelFrame(left, text="参证水文站信息")
        ref_box.grid(row=0, column=0, sticky="ew")
        self.ref_name = self._entry(ref_box, 0, 0, "参证站名称", "", width=16)
        self.ref_area = self._entry(ref_box, 0, 1, "参证站面积F参(km²)", "4580", width=16)

        flow_box = ttk.LabelFrame(left, text="参证站各频率洪峰流量 Q参(m³/s)")
        flow_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        defaults = {"100": "1850", "50": "1520", "30": "1280", "25": "1150", "20": "1050", "10": "780", "5": "560", "2": "380"}
        self.ref_flows: dict[str, tk.Entry] = {}
        for idx, freq in enumerate(TIE_FREQUENCIES):
            self.ref_flows[freq] = self._entry(flow_box, idx // 4, idx % 4, f"{freq}年", defaults[freq], width=10)

        design_box = ttk.LabelFrame(left, text="设计流域信息")
        design_box.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        self.design_name = self._entry(design_box, 0, 0, "设计流域名称", "", width=16)
        self.design_area = self._entry(design_box, 0, 1, "设计流域面积F设(km²)", "1250", width=16)

        n_box = ttk.LabelFrame(left, text="面积指数 n")
        n_box.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        self.unified_n = self._entry(n_box, 0, 0, "统一面积指数n", "0.667", width=12)
        ttk.Label(n_box, text="默认2/3=0.667；下方各频率n为空时使用统一n", foreground="#666666").grid(
            row=0, column=2, columnspan=6, padx=(0, 12), pady=6, sticky="w"
        )
        self.n_entries: dict[str, tk.Entry] = {}
        for idx, freq in enumerate(TIE_FREQUENCIES):
            ttk.Label(n_box, text=f"{freq}年n").grid(row=1 + idx // 4, column=(idx % 4) * 2, padx=(10, 4), pady=4, sticky="e")
            entry = ttk.Entry(n_box, width=9)
            entry.grid(row=1 + idx // 4, column=(idx % 4) * 2 + 1, padx=(0, 10), pady=4, sticky="w")
            self.n_entries[freq] = entry

        actions = ttk.Frame(left)
        actions.grid(row=4, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#183d63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空数据", self.clear_inputs).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "恢复默认", self.restore_defaults).pack(side=tk.LEFT, padx=(12, 0))

        result_box = ttk.LabelFrame(left, text="计算结果")
        result_box.grid(row=5, column=0, sticky="nsew", pady=(10, 0))
        result_box.rowconfigure(0, weight=1)
        result_box.columnconfigure(0, weight=1)
        self.output = RichTableText(result_box, height=14)
        setattr(self.output, "_hydrology_export_source", True)
        self.output.grid(row=0, column=0, sticky="nsew", padx=10, pady=(10, 6))
        export_bar = ttk.Frame(result_box)
        export_bar.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
        self._action_button(export_bar, "导出Excel", lambda: self._export_result("xlsx"), bg="#1688e8", fg="white").pack(side=tk.LEFT)
        self._action_button(export_bar, "导出Word", lambda: self._export_result("docx"), bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(10, 0))
        self._action_button(export_bar, "导出专项报告", lambda: self._export_result("html"), bg="#006b63", fg="white").pack(side=tk.LEFT, padx=(10, 0))

        station_box = ttk.LabelFrame(right, text="三个水文站数据（用于计算面积指数n）")
        station_box.grid(row=0, column=0, sticky="nsew")
        for idx, header in enumerate(["水文站名称", "面积(km²)", *[f"{freq}年Q(m³/s)" for freq in TIE_FREQUENCIES]]):
            ttk.Label(station_box, text=header).grid(row=0, column=idx, padx=3, pady=4)
        station_names = ["舒尔干", "格尔木(三)", "纳赤台(二)"]
        self.station_entries: list[dict[str, tk.Entry]] = []
        for row in range(3):
            row_entries: dict[str, tk.Entry] = {}
            name_entry = ttk.Entry(station_box, width=12)
            name_entry.insert(0, station_names[row])
            name_entry.grid(row=row + 1, column=0, padx=3, pady=4)
            row_entries["name"] = name_entry
            area_entry = ttk.Entry(station_box, width=10)
            area_entry.grid(row=row + 1, column=1, padx=3, pady=4)
            row_entries["area"] = area_entry
            for col, freq in enumerate(TIE_FREQUENCIES, 2):
                entry = ttk.Entry(station_box, width=9)
                entry.grid(row=row + 1, column=col, padx=3, pady=4)
                row_entries[freq] = entry
            self.station_entries.append(row_entries)

        n_result_box = ttk.LabelFrame(right, text="计算出的各频率面积指数 n")
        n_result_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        self.n_result_vars: dict[str, tk.StringVar] = {}
        for idx, freq in enumerate(TIE_FREQUENCIES):
            ttk.Label(n_result_box, text=f"{freq}年").grid(row=0, column=idx, padx=3, pady=(6, 2))
            var = tk.StringVar(value="---")
            self.n_result_vars[freq] = var
            ttk.Label(n_result_box, textvariable=var, background="#fffbc7", width=9, anchor="center").grid(row=1, column=idx, padx=3, pady=(0, 8))

        n_actions = ttk.Frame(right)
        n_actions.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        self.calculated_n: dict[str, float | None] = {}
        self._action_button(n_actions, "计算n值（线性回归）", self.calculate_n_values, bg="#2fa84f", fg="white").pack(side=tk.LEFT)
        self._action_button(n_actions, "应用n值到计算", self.apply_n_values, bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(12, 0))

    def calculate(self) -> None:
        try:
            flows = {freq: self._float(entry, f"{freq}年Q参(m³/s)") for freq, entry in self.ref_flows.items()}
            n_values: dict[str, float] = {}
            for freq, entry in self.n_entries.items():
                value = entry.get().strip()
                if value:
                    n_values[freq] = float(value)
            unified_n = self._float(self.unified_n, "统一 n")
            result = calculate_area_analogy(
                reference_area=self._float(self.ref_area, "参证站面积"),
                design_area=self._float(self.design_area, "设计流域面积"),
                reference_flows=flows,
                n_values=n_values,
                unified_n=unified_n,
            )
            ref_name = self.ref_name.get().strip() or "参证站"
            design_name = self.design_name.get().strip() or "设计流域"
            lines = [
                f"参证站：{ref_name}，F参={self.ref_area.get().strip()} km²",
                f"设计流域：{design_name}，F设={self.design_area.get().strip()} km²",
                "",
                "计算公式：Q设 = Q参 * (F设 / F参)^n",
                "",
            ]
            result_rows = [["频率", "Q参(m³/s)", "n", "Q设(m³/s)"]]
            for freq in TIE_FREQUENCIES:
                n_used = n_values.get(freq, unified_n)
                result_rows.append([f"{freq}年", f"{flows[freq]:g}", f"{n_used:g}", result[freq]])
            lines.append(format_fixed_table(result_rows, min_width=10))
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def _read_index_stations(self) -> list[StationFlow]:
        stations: list[StationFlow] = []
        for row, entries in enumerate(self.station_entries, 1):
            name = entries["name"].get().strip() or f"水文站{row}"
            area_text = entries["area"].get().strip()
            if not area_text:
                continue
            try:
                area = float(area_text)
            except ValueError as exc:
                raise HydrologyError(f"{name} 面积必须是数字") from exc
            flows: dict[str, float] = {}
            for freq in TIE_FREQUENCIES:
                value = entries[freq].get().strip()
                if value:
                    try:
                        flows[freq] = float(value)
                    except ValueError as exc:
                        raise HydrologyError(f"{name} {freq}年Q(m³/s)必须是数字") from exc
            stations.append(StationFlow(name, area, flows))
        return stations

    def calculate_n_values(self) -> None:
        try:
            self.calculated_n = calculate_area_index(self._read_index_stations(), TIE_FREQUENCIES)
            for freq in TIE_FREQUENCIES:
                value = self.calculated_n.get(freq)
                self.n_result_vars[freq].set("---" if value is None else f"{value:.3f}")
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def apply_n_values(self) -> None:
        if not self.calculated_n:
            self.calculate_n_values()
        for freq, entry in self.n_entries.items():
            value = self.calculated_n.get(freq)
            if value is not None:
                entry.delete(0, tk.END)
                entry.insert(0, f"{value:.3f}")

    def clear_inputs(self) -> None:
        for entry in [self.ref_name, self.ref_area, self.design_name, self.design_area, self.unified_n, *self.ref_flows.values(), *self.n_entries.values()]:
            entry.delete(0, tk.END)
        for row in self.station_entries:
            for entry in row.values():
                entry.delete(0, tk.END)
        for var in self.n_result_vars.values():
            var.set("---")
        self.calculated_n = {}
        self._write(self.output, "")

    def restore_defaults(self) -> None:
        self.unified_n.delete(0, tk.END)
        self.unified_n.insert(0, "0.667")
        for entry in self.n_entries.values():
            entry.delete(0, tk.END)


class DoubleLogInterpolationTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        design_box = ttk.LabelFrame(top, text="设计流域信息")
        design_box.grid(row=0, column=0, sticky="ew")
        self.design_name = self._entry(design_box, 0, 0, "设计流域名称", "设计流域", width=16)
        self.design_area = self._entry(design_box, 0, 1, "设计流域面积F设(km²)", "225", width=16)
        ttk.Label(
            design_box,
            text="软件自动选取面积包围F设的相邻两站；超出站点面积范围时按最近两站外推。",
            foreground="#666666",
        ).grid(row=0, column=4, columnspan=6, padx=(10, 12), pady=6, sticky="w")

        station_box = ttk.LabelFrame(top, text="参证水文站数据")
        station_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        headers = ["水文站名称", "面积(km²)", *[f"{freq}年Q(m³/s)" for freq in TIE_FREQUENCIES]]
        for idx, header in enumerate(headers):
            ttk.Label(station_box, text=header).grid(row=0, column=idx, padx=3, pady=4)

        defaults = [
            ("参证站A", "100", {"100": "50", "50": "45", "30": "40", "25": "38", "20": "35", "10": "28", "5": "22", "2": "15"}),
            ("参证站B", "400", {"100": "100", "50": "90", "30": "80", "25": "76", "20": "70", "10": "56", "5": "44", "2": "30"}),
            ("参证站C", "", {}),
            ("参证站D", "", {}),
        ]
        self.station_entries: list[dict[str, tk.Entry]] = []
        for row, (name, area, flows) in enumerate(defaults, 1):
            row_entries: dict[str, tk.Entry] = {}
            name_entry = ttk.Entry(station_box, width=12)
            name_entry.insert(0, name)
            name_entry.grid(row=row, column=0, padx=3, pady=4)
            row_entries["name"] = name_entry
            area_entry = ttk.Entry(station_box, width=10)
            area_entry.insert(0, area)
            area_entry.grid(row=row, column=1, padx=3, pady=4)
            row_entries["area"] = area_entry
            for col, freq in enumerate(TIE_FREQUENCIES, 2):
                entry = ttk.Entry(station_box, width=9)
                entry.insert(0, flows.get(freq, ""))
                entry.grid(row=row, column=col, padx=3, pady=4)
                row_entries[freq] = entry
            self.station_entries.append(row_entries)

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#7b61d1", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空数据", self.clear_inputs).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "恢复默认", self.restore_defaults).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def _read_stations(self) -> list[StationFlow]:
        stations: list[StationFlow] = []
        for row, entries in enumerate(self.station_entries, 1):
            name = entries["name"].get().strip() or f"水文站{row}"
            area_text = entries["area"].get().strip()
            if not area_text:
                continue
            try:
                area = float(area_text)
            except ValueError as exc:
                raise HydrologyError(f"{name} 面积必须是数字") from exc
            flows: dict[str, float] = {}
            for freq in TIE_FREQUENCIES:
                value = entries[freq].get().strip()
                if value:
                    try:
                        flows[freq] = float(value)
                    except ValueError as exc:
                        raise HydrologyError(f"{name} {freq}年Q(m³/s)必须是数字") from exc
            stations.append(StationFlow(name, area, flows))
        return stations

    def _select_station_pair(self, stations: list[StationFlow], design_area: float) -> tuple[StationFlow, StationFlow]:
        if len(stations) < 2:
            raise HydrologyError("双对数内插法至少需要填写 2 个水文站面积和流量")
        stations = sorted(stations, key=lambda item: item.area)
        for lower, upper in zip(stations, stations[1:]):
            if lower.area <= design_area <= upper.area:
                return lower, upper
        if design_area < stations[0].area:
            return stations[0], stations[1]
        return stations[-2], stations[-1]

    def calculate(self) -> None:
        try:
            design_area = self._float(self.design_area, "设计流域面积")
            stations = self._read_stations()
            station_a, station_b = self._select_station_pair(stations, design_area)
            result = double_log_interpolate_flows(station_a, station_b, design_area, TIE_FREQUENCIES)
            design_name = self.design_name.get().strip() or "设计流域"
            lines = [
                "双对数内插法",
                f"设计流域：{design_name}，F设={design_area:g} km²",
                f"采用站点：{station_a.name}(F={station_a.area:g}) 与 {station_b.name}(F={station_b.area:g})",
                "",
                "计算公式：lgQ设 = lgQ1 + (lgQ2-lgQ1) * (lgF设-lgF1) / (lgF2-lgF1)",
                "面积指数：n = (lgQ2-lgQ1) / (lgF2-lgF1)",
                "说明：位置为“外推”时，说明F设不在所填水文站面积范围内，应复核参证站代表性。",
                "",
            ]
            rows = [["频率", "站点1", "F₁", "Q₁", "站点2", "F₂", "Q₂", "n", "权重", "位置", "Q设(m³/s)"]]
            for freq in TIE_FREQUENCIES:
                item = result[freq]
                rows.append([
                    f"{freq}年",
                    item["station_a"],
                    item["area_a"],
                    item["q_a"] if item["q_a"] is not None else "---",
                    item["station_b"],
                    item["area_b"],
                    item["q_b"] if item["q_b"] is not None else "---",
                    item["n"] if item["n"] is not None else "---",
                    item["weight"],
                    item["position"],
                    item["q_design"] if item["q_design"] is not None else "---",
                ])
            lines.append(format_fixed_table(rows, min_width=10))
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def clear_inputs(self) -> None:
        self.design_name.delete(0, tk.END)
        self.design_area.delete(0, tk.END)
        for row in self.station_entries:
            for entry in row.values():
                entry.delete(0, tk.END)
        self._write(self.output, "")

    def restore_defaults(self) -> None:
        self.clear_inputs()
        self.design_name.insert(0, "设计流域")
        self.design_area.insert(0, "225")
        defaults = [
            ("参证站A", "100", {"100": "50", "50": "45", "30": "40", "25": "38", "20": "35", "10": "28", "5": "22", "2": "15"}),
            ("参证站B", "400", {"100": "100", "50": "90", "30": "80", "25": "76", "20": "70", "10": "56", "5": "44", "2": "30"}),
            ("参证站C", "", {}),
            ("参证站D", "", {}),
        ]
        for row_entries, (name, area, flows) in zip(self.station_entries, defaults):
            row_entries["name"].insert(0, name)
            row_entries["area"].insert(0, area)
            for freq in TIE_FREQUENCIES:
                row_entries[freq].insert(0, flows.get(freq, ""))


class FloodSynthesisTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.last_synthesis_result = None

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="基本参数")
        basic.grid(row=0, column=0, sticky="ew")
        self.project_name = self._entry(basic, 0, 0, "项目/断面名称", "合成断面", width=16)
        self.frequency = self._entry(basic, 0, 1, "频率/工况", "100年一遇", width=16)
        self.delta_t = self._entry(basic, 0, 2, "时段Δt(h)", "1", width=10)
        ttk.Label(
            basic,
            text="各过程线按同一Δt填写，流量用逗号、空格或换行分隔；时间滞后用于把支流过程线平移到合成断面。",
            foreground="#666666",
        ).grid(row=1, column=0, columnspan=8, padx=(10, 12), pady=(0, 6), sticky="w")

        line_box = ttk.LabelFrame(top, text="组成洪水过程线")
        line_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        line_box.columnconfigure(2, weight=1)
        headers = ["名称", "时间滞后(h)", "流量过程线Q(m³/s)"]
        for col, header in enumerate(headers):
            ttk.Label(line_box, text=header).grid(row=0, column=col, padx=4, pady=4)
        defaults = [
            ("干流上游", "0", "0, 80, 260, 420, 300, 120, 30, 0"),
            ("支流一", "1", "0, 50, 180, 240, 160, 60, 0"),
            ("支流二", "2", "0, 30, 120, 150, 90, 20, 0"),
            ("", "", ""),
            ("", "", ""),
        ]
        self.component_entries: list[dict[str, tk.Widget]] = []
        for row, (name, lag, flows) in enumerate(defaults, 1):
            row_entries: dict[str, tk.Widget] = {}
            name_entry = ttk.Entry(line_box, width=14)
            name_entry.insert(0, name)
            name_entry.grid(row=row, column=0, padx=4, pady=4, sticky="ew")
            row_entries["name"] = name_entry
            lag_entry = ttk.Entry(line_box, width=12)
            lag_entry.insert(0, lag)
            lag_entry.grid(row=row, column=1, padx=4, pady=4, sticky="ew")
            row_entries["lag"] = lag_entry
            flow_entry = ttk.Entry(line_box, width=90)
            flow_entry.insert(0, flows)
            flow_entry.grid(row=row, column=2, padx=4, pady=4, sticky="ew")
            row_entries["flows"] = flow_entry
            self.component_entries.append(row_entries)

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始合成", self.calculate, bg="#b36b00", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空数据", self.clear_inputs).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "恢复默认", self.restore_defaults).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def _parse_flows(self, value: str, label: str) -> list[float]:
        tokens = [item for item in re.split(r"[\s,，;；]+", value.strip()) if item]
        if not tokens:
            return []
        flows: list[float] = []
        for token in tokens:
            try:
                flows.append(float(token))
            except ValueError as exc:
                raise HydrologyError(f"{label} 流量过程线包含非数字：{token}") from exc
        return flows

    def _read_components(self) -> list[FloodSynthesisComponent]:
        components: list[FloodSynthesisComponent] = []
        for row, entries in enumerate(self.component_entries, 1):
            name_entry = entries["name"]
            lag_entry = entries["lag"]
            flows_entry = entries["flows"]
            flows = self._parse_flows(flows_entry.get(), f"第{row}行")
            if not flows:
                continue
            name = name_entry.get().strip() or f"过程线{row}"
            try:
                lag = float(lag_entry.get().strip() or "0")
            except ValueError as exc:
                raise HydrologyError(f"{name} 时间滞后必须是数字") from exc
            components.append(FloodSynthesisComponent(name, lag, flows))
        return components

    def calculate(self) -> None:
        try:
            delta_t = self._float(self.delta_t, "时段 Δt")
            components = self._read_components()
            result = synthesize_flood_hydrographs(components, delta_t)
            self.last_synthesis_result = result
            component_names = [item.name for item in result.components]
            lines = [
                "洪水合成法",
                "",
                "一、输入参数表",
                format_fixed_table([
                    ["项目", "数值"],
                    ["项目/断面名称", self.project_name.get().strip() or "合成断面"],
                    ["频率/工况", self.frequency.get().strip() or "未注明"],
                    ["时段Δt(h)", result.delta_t_h],
                    ["过程线条数", len(result.components)],
                ], min_width=12),
                "",
                "二、组成过程线参数表",
            ]
            param_rows = [["名称", "时间滞后(h)", "流量点数", "最大流量(m³/s)"]]
            for item in result.components:
                param_rows.append([item.name, item.time_offset_h, len(item.flows), f"{max(item.flows):g}"])
            lines.append(format_fixed_table(param_rows, min_width=12))
            lines.extend([
                "",
                "三、合成洪水过程线计算表",
            ])
            process_rows = [["时段", "t(h)", *component_names, "合成Q(m³/s)"]]
            for idx, point in enumerate(result.points):
                process_rows.append([
                    idx,
                    point.time_h,
                    *[point.component_flows[name] for name in component_names],
                    point.total_flow,
                ])
            lines.append(format_fixed_table(process_rows, min_width=12))
            lines.extend([
                "",
                "四、洪峰成果表",
                format_fixed_table([
                    ["项目", "数值"],
                    ["洪峰流量Qₘ(m³/s)", result.peak_flow_m3s],
                    ["峰现时间t(h)", result.peak_time_h],
                    ["洪量(万m³)", result.flood_volume_10000m3],
                ], min_width=14),
                "",
                "计算说明：Q合成(t)=ΣQi(t-滞后时间i)。当滞后时间不是Δt整数倍时，软件按相邻时段流量线性插值后叠加。",
            ])
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def _current_report_figures(self) -> list[tuple[str, str]]:
        result = self.last_synthesis_result
        if result is None:
            return []
        colors = ["#0b6fb3", "#d56a17", "#247a4a", "#7a32b4", "#8f4d00", "#006b68"]
        series: list[tuple[str, list[tuple[float, float]], str]] = []
        for index, component in enumerate(result.components):
            values = [(point.time_h, point.component_flows.get(component.name, 0.0)) for point in result.points]
            series.append((component.name, values, colors[index % len(colors)]))
        series.append(("合成Q", [(point.time_h, point.total_flow) for point in result.points], "#d62424"))
        chart = self._line_chart_svg("洪水合成过程线", series, "流量 Q(m³/s)")
        return [("洪水合成过程线", chart)]

    def clear_inputs(self) -> None:
        for entry in [self.project_name, self.frequency, self.delta_t]:
            entry.delete(0, tk.END)
        for row in self.component_entries:
            for entry in row.values():
                entry.delete(0, tk.END)
        self.last_synthesis_result = None
        self._write(self.output, "")

    def restore_defaults(self) -> None:
        self.clear_inputs()
        self.project_name.insert(0, "合成断面")
        self.frequency.insert(0, "100年一遇")
        self.delta_t.insert(0, "1")
        defaults = [
            ("干流上游", "0", "0, 80, 260, 420, 300, 120, 30, 0"),
            ("支流一", "1", "0, 50, 180, 240, 160, 60, 0"),
            ("支流二", "2", "0, 30, 120, 150, 90, 20, 0"),
            ("", "", ""),
            ("", "", ""),
        ]
        for row, (name, lag, flows) in zip(self.component_entries, defaults):
            row["name"].insert(0, name)
            row["lag"].insert(0, lag)
            row["flows"].insert(0, flows)


class ReservoirFloodRoutingTab(ttk.Frame, FormMixin):
    SAMPLE_INFLOW = """0 20
1 35
2 80
3 180
4 320
5 460
6 380
7 250
8 150
9 90
10 55
11 35
12 25
13 20
14 20
16 20
18 20
20 20
22 20
24 20
28 20
32 20"""
    SAMPLE_STORAGE = """100 0
101 12
102 30
103 55
104 90
105 140
106 205
107 285"""
    SAMPLE_DISCHARGE = """100 0
101 5
102 20
103 60
104 130
105 240
106 380
107 550"""

    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.last_result: ReservoirRoutingResult | None = None

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="步骤1  填写水库与控制水位")
        basic.grid(row=0, column=0, sticky="ew")
        self.reservoir_name = self._entry(basic, 0, 0, "水库名称", "示例水库", width=16)
        self.initial_level = self._entry(basic, 0, 1, "初始水位(m)", "102", width=12)
        self.flood_limit_level = self._entry(basic, 0, 2, "汛限水位(m，可空)", "106", width=12)
        self.check_flood_level = self._entry(basic, 0, 3, "校核洪水位(m，可空)", "106.8", width=12)
        ttk.Label(
            basic,
            text="初始水位决定起算库容和起算泄量；汛限、校核水位只用于超限判别，不参与曲线外推。",
            foreground="#5b6975",
        ).grid(row=1, column=0, columnspan=8, padx=10, pady=(0, 6), sticky="w")

        data_box = ttk.LabelFrame(top, text="步骤2  录入三组基础资料（每行两列，可从Excel直接粘贴）")
        data_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        for column in range(3):
            data_box.columnconfigure(column, weight=1)
        self.inflow_text = self._create_data_editor(
            data_box,
            0,
            "入库洪水过程线",
            "时间t(h)    入流I(m³/s)",
            self.SAMPLE_INFLOW,
            "入库洪水过程线",
        )
        self.storage_text = self._create_data_editor(
            data_box,
            1,
            "水位-库容曲线",
            "水位Z(m)    库容V(万m³)",
            self.SAMPLE_STORAGE,
            "水位-库容曲线",
        )
        self.discharge_text = self._create_data_editor(
            data_box,
            2,
            "水位-总泄量曲线",
            "水位Z(m)    总泄量O(m³/s)",
            self.SAMPLE_DISCHARGE,
            "水位-总泄量曲线",
        )
        ttk.Label(
            data_box,
            text="泄量曲线应为当前闸门、溢洪道及其他泄水建筑物在同一调度工况下的总泄量；库容单位为万m³，软件内部自动换算为m³。",
            foreground="#5b6975",
            wraplength=1160,
            justify=tk.LEFT,
        ).grid(row=1, column=0, columnspan=3, padx=10, pady=(2, 8), sticky="w")

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始调洪演算", self.calculate, bg="#2f6f9f", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "打开过程线图", self.open_routing_chart, bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(10, 0))
        self._action_button(actions, "恢复示例", self.restore_defaults).pack(side=tk.LEFT, padx=(10, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(10, 0))

        self.output = self._result_box(self)

    def _create_data_editor(
        self,
        parent: tk.Widget,
        column: int,
        title: str,
        columns: str,
        sample: str,
        import_label: str,
    ) -> ScrolledText:
        frame = ttk.Frame(parent)
        frame.grid(row=0, column=column, sticky="nsew", padx=6, pady=6)
        frame.columnconfigure(0, weight=1)
        ttk.Label(frame, text=title, font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=0, column=0, sticky="w")
        ttk.Label(frame, text=columns, foreground="#536676").grid(row=1, column=0, sticky="w", pady=(3, 4))
        editor = ScrolledText(frame, width=35, height=10, wrap=tk.NONE, font=(DASHBOARD_MONO_FONT, 9))
        editor.insert("1.0", sample)
        editor.grid(row=2, column=0, sticky="nsew")
        self._action_button(
            frame,
            "导入Excel/CSV前两列",
            lambda target=editor, label=import_label: self._import_pair_data(target, label),
        ).grid(row=3, column=0, sticky="w", pady=(6, 0))
        return editor

    def _import_pair_data(self, target: ScrolledText, label: str) -> None:
        try:
            path_text = filedialog.askopenfilename(
                title=f"导入{label}",
                filetypes=[("Excel/CSV", "*.xlsx *.csv"), ("Excel工作簿", "*.xlsx"), ("CSV文件", "*.csv")],
            )
            if not path_text:
                return
            path = Path(path_text)
            if path.suffix.lower() == ".csv":
                points = self._read_points_csv(path)
            elif path.suffix.lower() == ".xlsx":
                points = self._read_points_xlsx(path)
            else:
                raise HydrologyError("仅支持.xlsx或.csv文件")
            target.delete("1.0", tk.END)
            target.insert("1.0", "\n".join(f"{x:g}\t{y:g}" for x, y in points))
        except Exception as exc:
            messagebox.showerror("导入失败", str(exc))

    def _parse_pair_data(self, editor: ScrolledText, label: str) -> tuple[tuple[float, float], ...]:
        points: list[tuple[float, float]] = []
        rows = [row.strip() for row in re.split(r"[;；\n]+", editor.get("1.0", tk.END).strip()) if row.strip()]
        for row_number, row in enumerate(rows, 1):
            parts = [part for part in re.split(r"[,，\s]+", row) if part]
            if len(parts) != 2:
                raise HydrologyError(f"{label}第{row_number}行应只有两列：{row}")
            try:
                points.append((float(parts[0]), float(parts[1])))
            except ValueError as exc:
                if row_number == 1:
                    continue
                raise HydrologyError(f"{label}第{row_number}行必须是数字：{row}") from exc
        if len(points) < 2:
            raise HydrologyError(f"{label}至少需要2行有效数据")
        return tuple(points)

    def _optional_float(self, entry: tk.Entry, label: str) -> float | None:
        value = entry.get().strip()
        if not value:
            return None
        try:
            return float(value)
        except ValueError as exc:
            raise HydrologyError(f"{label}必须是数字或留空") from exc

    def calculate(self) -> None:
        try:
            storage_10000m3 = self._parse_pair_data(self.storage_text, "水位-库容曲线")
            data = ReservoirRoutingInput(
                reservoir_name=self.reservoir_name.get().strip(),
                inflow_hydrograph=self._parse_pair_data(self.inflow_text, "入库洪水过程线"),
                elevation_storage_curve=tuple((level, storage * 10000.0) for level, storage in storage_10000m3),
                elevation_discharge_curve=self._parse_pair_data(self.discharge_text, "水位-总泄量曲线"),
                initial_elevation_m=self._float(self.initial_level, "初始水位"),
                flood_limit_level_m=self._optional_float(self.flood_limit_level, "汛限水位"),
                check_flood_level_m=self._optional_float(self.check_flood_level, "校核洪水位"),
            )
            result = route_reservoir_flood(data)
            self.last_result = result
            lines = [
                "水库调洪演算（等水面Modified Puls法）",
                "",
                "一、计算条件表",
                format_fixed_table(
                    [
                        ["项目", "采用值", "说明"],
                        ["水库名称", result.reservoir_name, "本次演算对象"],
                        ["初始水位(m)", f"{data.initial_elevation_m:.3f}", "由曲线确定起算库容、泄量"],
                        ["汛限水位(m)", "未设置" if data.flood_limit_level_m is None else f"{data.flood_limit_level_m:.3f}", "仅作超限检查"],
                        ["校核洪水位(m)", "未设置" if data.check_flood_level_m is None else f"{data.check_flood_level_m:.3f}", "仅作超限检查"],
                        ["演算方法", "等水面调洪", "Modified Puls蓄量指示法"],
                    ],
                    min_width=15,
                ),
                "",
                "二、水位关系曲线表",
            ]
            storage_rows = [["水位Z(m)", "库容V(万m³)"]]
            storage_rows.extend([[f"{level:.3f}", f"{storage / 10000.0:.3f}"] for level, storage in data.elevation_storage_curve])
            discharge_rows = [["水位Z(m)", "总泄量O(m³/s)"]]
            discharge_rows.extend([[f"{level:.3f}", f"{flow:.3f}"] for level, flow in data.elevation_discharge_curve])
            lines.extend(
                [
                    "水位-库容曲线",
                    format_fixed_table(storage_rows, min_width=16),
                    "",
                    "水位-总泄量曲线",
                    format_fixed_table(discharge_rows, min_width=16),
                    "",
                    "三、调洪成果表",
                    format_fixed_table(
                        [
                            ["成果项目", "数值", "单位/说明"],
                            ["入库洪峰", f"{result.peak_inflow_m3s:.2f}", f"m³/s，t={result.peak_inflow_time_h:.2f}h"],
                            ["最大下泄流量", f"{result.peak_outflow_m3s:.2f}", f"m³/s，t={result.peak_outflow_time_h:.2f}h"],
                            ["削峰率", f"{result.peak_reduction_percent:.2f}", "%"],
                            ["出库峰现延迟", f"{result.peak_delay_h:.2f}", "h"],
                            ["最高水位", f"{result.max_elevation_m:.3f}", f"m，t={result.max_elevation_time_h:.2f}h"],
                            ["初始库容", f"{result.initial_storage_m3 / 10000.0:.3f}", "万m³"],
                            ["末时刻库容", f"{result.final_storage_m3 / 10000.0:.3f}", "万m³"],
                            ["总入库洪量", f"{result.total_inflow_volume_m3 / 10000.0:.3f}", "万m³"],
                            ["总出库洪量", f"{result.total_outflow_volume_m3 / 10000.0:.3f}", "万m³"],
                            ["库容变化量", f"{result.net_storage_change_m3 / 10000.0:.3f}", "万m³"],
                            ["水量平衡误差", f"{result.mass_balance_error_m3:.6f}", f"m³（{result.mass_balance_relative_percent:.9f}%）"],
                        ],
                        min_width=18,
                    ),
                    "",
                    "四、逐时段调洪过程表",
                ]
            )
            process_rows = [["序号", "t(h)", "Δt(h)", "入流I", "出流O", "水位Z(m)", "库容(万m³)", "入库量(万m³)", "出库量(万m³)", "Δ库容(万m³)", "平衡误差(m³)"]]
            for step in result.steps:
                process_rows.append(
                    [
                        step.index,
                        f"{step.time_h:.2f}",
                        "-" if step.index == 0 else f"{step.interval_h:.2f}",
                        f"{step.inflow_m3s:.2f}",
                        f"{step.outflow_m3s:.2f}",
                        f"{step.elevation_m:.3f}",
                        f"{step.storage_m3 / 10000.0:.3f}",
                        "-" if step.interval_inflow_volume_m3 is None else f"{step.interval_inflow_volume_m3 / 10000.0:.3f}",
                        "-" if step.interval_outflow_volume_m3 is None else f"{step.interval_outflow_volume_m3 / 10000.0:.3f}",
                        "-" if step.storage_change_m3 is None else f"{step.storage_change_m3 / 10000.0:.3f}",
                        "-" if step.balance_error_m3 is None else f"{step.balance_error_m3:.6f}",
                    ]
                )
            lines.append(format_fixed_table(process_rows, min_width=12))
            lines.extend(
                [
                    "",
                    "五、计算公式与复核说明",
                    "水量平衡：[(I₁+I₂)/2-(O₁+O₂)/2]Δt=S₂-S₁。",
                    "蓄量指示方程：2S₂/Δt+O₂=I₁+I₂+2S₁/Δt-O₁；软件在水位-库容、水位-泄量曲线共同范围内插值求解Z₂。",
                    "泄量曲线必须对应本次采用的闸门及泄洪建筑物运行工况；若调度规则随时间或预报动态改变，应分工况复核或采用专门调度模型。",
                    "",
                    "六、检查提示",
                ]
            )
            if result.warnings:
                lines.extend(f"{index}. {warning}" for index, warning in enumerate(result.warnings, 1))
            else:
                lines.append("未发现曲线越界、控制水位超限或退水段不完整问题；仍应由设计人员复核资料来源和调度工况。")
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("调洪演算错误", str(exc))

    def restore_defaults(self) -> None:
        for entry, value in (
            (self.reservoir_name, "示例水库"),
            (self.initial_level, "102"),
            (self.flood_limit_level, "106"),
            (self.check_flood_level, "106.8"),
        ):
            entry.delete(0, tk.END)
            entry.insert(0, value)
        for editor, value in (
            (self.inflow_text, self.SAMPLE_INFLOW),
            (self.storage_text, self.SAMPLE_STORAGE),
            (self.discharge_text, self.SAMPLE_DISCHARGE),
        ):
            editor.delete("1.0", tk.END)
            editor.insert("1.0", value)
        self.last_result = None
        self._write(self.output, "")

    def open_routing_chart(self) -> None:
        if self.last_result is None:
            messagebox.showwarning("过程线图", "请先完成调洪演算。")
            return
        out_dir = Path(__file__).resolve().parent.parent / "输出成果"
        out_dir.mkdir(parents=True, exist_ok=True)
        path = out_dir / f"水库调洪演算过程线_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        path.write_text(self._routing_chart_html(self.last_result), encoding="utf-8")
        os.startfile(path)

    def _routing_chart_html(self, result: ReservoirRoutingResult) -> str:
        width, height = 1080, 350
        left, right, top, bottom = 80, 35, 42, 58
        plot_width = width - left - right
        plot_height = height - top - bottom
        times = [step.time_h for step in result.steps]
        inflows = [step.inflow_m3s for step in result.steps]
        outflows = [step.outflow_m3s for step in result.steps]
        levels = [step.elevation_m for step in result.steps]
        time_min, time_max = min(times), max(times)

        def x_position(value: float) -> float:
            return left + (value - time_min) / max(time_max - time_min, 1e-9) * plot_width

        def chart_svg(values_and_colors: list[tuple[list[float], str]], title: str, axis_title: str, reference_levels: list[tuple[float, str, str]] | None = None) -> str:
            all_values = [value for values, _color in values_and_colors for value in values]
            if reference_levels:
                all_values.extend(value for value, _label, _color in reference_levels)
            value_min = min(all_values)
            value_max = max(all_values)
            if value_min >= 0 and axis_title.startswith("流量"):
                value_min = 0.0
            span = max(value_max - value_min, max(abs(value_max), 1.0) * 0.05)
            value_min -= span * (0.04 if value_min != 0 else 0.0)
            value_max += span * 0.08

            def y_position(value: float) -> float:
                return top + (value_max - value) / max(value_max - value_min, 1e-9) * plot_height

            elements = [
                f'<svg viewBox="0 0 {width} {height}" role="img" aria-label="{html_lib.escape(title)}">',
                f'<text x="{width / 2}" y="24" text-anchor="middle" class="title">{html_lib.escape(title)}</text>',
                f'<line x1="{left}" y1="{top}" x2="{left}" y2="{top + plot_height}" class="axis"/>',
                f'<line x1="{left}" y1="{top + plot_height}" x2="{left + plot_width}" y2="{top + plot_height}" class="axis"/>',
            ]
            for tick in range(6):
                ratio = tick / 5
                y = top + ratio * plot_height
                value = value_max - ratio * (value_max - value_min)
                elements.append(f'<line x1="{left}" y1="{y:.2f}" x2="{left + plot_width}" y2="{y:.2f}" class="grid"/>')
                elements.append(f'<text x="{left - 10}" y="{y + 5:.2f}" text-anchor="end" class="tick">{value:.2f}</text>')
            for tick in range(6):
                ratio = tick / 5
                x = left + ratio * plot_width
                value = time_min + ratio * (time_max - time_min)
                elements.append(f'<line x1="{x:.2f}" y1="{top}" x2="{x:.2f}" y2="{top + plot_height}" class="grid"/>')
                elements.append(f'<text x="{x:.2f}" y="{top + plot_height + 24}" text-anchor="middle" class="tick">{value:.2f}</text>')
            if reference_levels:
                for value, label, color in reference_levels:
                    y = y_position(value)
                    elements.append(f'<line x1="{left}" y1="{y:.2f}" x2="{left + plot_width}" y2="{y:.2f}" stroke="{color}" stroke-width="2" stroke-dasharray="8 5"/>')
                    elements.append(f'<text x="{left + plot_width - 4}" y="{y - 6:.2f}" text-anchor="end" fill="{color}" class="legend">{html_lib.escape(label)} {value:.3f}m</text>')
            for values, color in values_and_colors:
                points = " ".join(f"{x_position(time):.2f},{y_position(value):.2f}" for time, value in zip(times, values))
                elements.append(f'<polyline points="{points}" fill="none" stroke="{color}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>')
            elements.append(f'<text x="{left + plot_width / 2}" y="{height - 12}" text-anchor="middle" class="label">时间 t(h)</text>')
            elements.append(f'<text x="20" y="{top + plot_height / 2}" text-anchor="middle" transform="rotate(-90 20 {top + plot_height / 2})" class="label">{html_lib.escape(axis_title)}</text>')
            elements.append("</svg>")
            return "".join(elements)

        references: list[tuple[float, str, str]] = []
        flood_limit = self._optional_float(self.flood_limit_level, "汛限水位")
        check_level = self._optional_float(self.check_flood_level, "校核洪水位")
        if flood_limit is not None:
            references.append((flood_limit, "汛限水位", "#c17600"))
        if check_level is not None:
            references.append((check_level, "校核洪水位", "#b42318"))
        flow_svg = chart_svg([(inflows, "#1677b8"), (outflows, "#d56a17")], "入库与出库流量过程线", "流量 Q(m³/s)")
        level_svg = chart_svg([(levels, "#247a4a")], "水库水位过程线", "水位 Z(m)", references)
        safe_name = html_lib.escape(result.reservoir_name)
        return f"""<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><title>{safe_name}水库调洪演算过程线</title>
<style>body{{margin:0;background:#f3f6f9;color:#1d2d3a;font-family:'Times New Roman','KaiTi','楷体',serif}}main{{max-width:1140px;margin:22px auto;padding:0 18px}}h1{{font-size:24px;text-align:center;margin:0 0 16px}}.summary{{background:#fff;border:1px solid #d8e1e8;padding:14px 18px;margin-bottom:14px;text-align:center}}.chart{{background:#fff;border:1px solid #d8e1e8;margin:14px 0;padding:10px}}svg{{display:block;width:100%;height:auto}}.axis{{stroke:#263847;stroke-width:1.5}}.grid{{stroke:#dce4ea;stroke-width:1}}.title{{font-size:19px;font-weight:700}}.tick{{font-size:12px;fill:#4e6070}}.label{{font-size:14px;fill:#263847}}.legend{{font-size:12px;font-weight:700}}</style></head>
<body><main><h1>{safe_name}水库调洪演算过程线</h1><div class="summary">入库洪峰 {result.peak_inflow_m3s:.2f}m³/s　最大下泄 {result.peak_outflow_m3s:.2f}m³/s　最高水位 {result.max_elevation_m:.3f}m　削峰率 {result.peak_reduction_percent:.2f}%</div>
<div class="chart">{flow_svg}<div style="text-align:center;color:#526577"><span style="color:#1677b8">━━ 入库流量</span>　<span style="color:#d56a17">━━ 出库流量</span></div></div><div class="chart">{level_svg}</div></main></body></html>"""

    def _current_report_figures(self) -> list[tuple[str, str]]:
        if self.last_result is None:
            return []
        return [("水库调洪过程线", self._routing_chart_html(self.last_result))]


class ManningWaterLevelTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        section_box = ttk.LabelFrame(top, text="断面数据")
        section_box.grid(row=0, column=0, sticky="ew")
        section_box.columnconfigure(4, weight=1)
        headers = ["断面名称", "设计流量Q(m³/s)", "糙率n", "水力坡降I(小数)", "断面点x,z(m,m)"]
        for col, header in enumerate(headers):
            ttk.Label(section_box, text=header).grid(row=0, column=col, padx=4, pady=4)
        defaults = [
            ("断面1", "120", "0.035", "0.0015", "0,103; 8,101; 16,100.2; 25,100.4; 34,102; 42,103.5"),
            ("断面2", "", "0.035", "0.0015", ""),
            ("断面3", "", "0.035", "0.0015", ""),
            ("断面4", "", "0.035", "0.0015", ""),
            ("断面5", "", "0.035", "0.0015", ""),
        ]
        self.section_entries: list[dict[str, tk.Entry]] = []
        for row, (name, flow, roughness, slope, points) in enumerate(defaults, 1):
            entries: dict[str, tk.Entry] = {}
            for col, (key, value, width) in enumerate([
                ("name", name, 12),
                ("flow", flow, 12),
                ("roughness", roughness, 9),
                ("slope", slope, 10),
                ("points", points, 100),
            ]):
                entry = ttk.Entry(section_box, width=width)
                entry.insert(0, value)
                entry.grid(row=row, column=col, padx=4, pady=4, sticky="ew")
                entries[key] = entry
            self.section_entries.append(entries)
        ttk.Label(
            top,
            text="断面点格式：0,103; 8,101; 16,100.2。起点距单位m，高程单位m；水力坡降I按小数输入，如0.0015。",
            foreground="#666666",
        ).grid(row=1, column=0, sticky="w", padx=8, pady=(8, 0))

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空数据", self.clear_inputs).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "恢复默认", self.restore_defaults).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def _parse_section_points(self, value: str, section_name: str) -> tuple[tuple[float, float], ...]:
        pairs = [item.strip() for item in re.split(r"[;；\n]+", value.strip()) if item.strip()]
        points: list[tuple[float, float]] = []
        for item in pairs:
            parts = [part for part in re.split(r"[,，\s]+", item) if part]
            if len(parts) != 2:
                raise HydrologyError(f"{section_name} 断面点格式错误：{item}")
            try:
                points.append((float(parts[0]), float(parts[1])))
            except ValueError as exc:
                raise HydrologyError(f"{section_name} 断面点必须为数字：{item}") from exc
        return tuple(points)

    def _read_sections(self) -> list[ManningSectionInput]:
        sections: list[ManningSectionInput] = []
        for row, entries in enumerate(self.section_entries, 1):
            flow_text = entries["flow"].get().strip()
            points_text = entries["points"].get().strip()
            if not flow_text and not points_text:
                continue
            name = entries["name"].get().strip() or f"断面{row}"
            try:
                flow = float(flow_text)
                roughness = float(entries["roughness"].get().strip())
                slope = float(entries["slope"].get().strip())
            except ValueError as exc:
                raise HydrologyError(f"{name} 的Q、n、I必须为数字") from exc
            points = self._parse_section_points(points_text, name)
            sections.append(ManningSectionInput(name, roughness, slope, flow, points))
        if not sections:
            raise HydrologyError("至少需要填写1个断面")
        return sections

    def calculate(self) -> None:
        try:
            sections = self._read_sections()
            results = [calculate_manning_water_level(section) for section in sections]
            lines = [
                "曼宁公式水位计算",
                "",
                "计算公式：Q = (1/n) * A * R^(2/3) * I^(1/2)",
                "其中 A 为过水面积，R=A/P，P为湿周，I为水力坡降；软件按断面折线自动试算水位。",
                "",
                "一、计算成果表",
            ]
            roughness_table_rows, roughness_value = self._roughness_rows()
            lines[5:5] = [
                "河道综合糙率加权平均公式：n = Σ(nᵢχᵢ) / Σχᵢ",
                format_fixed_table(roughness_table_rows, min_width=12),
                "",
            ]
            self.composite_roughness.set(f"综合糙率 n = {roughness_value:.4f}")

            rows = [[
                "断面", "水位(m)", "设计Q(m³/s)", "复核Q(m³/s)", "面积A(m²)", "湿周P(m)",
                "水力半径R(m)", "水面宽B(m)", "最大水深(m)", "流速(m/s)", "弗汝德数",
            ]]
            for section, result in zip(sections, results):
                rows.append([
                    result.name,
                    f"{result.water_level:.2f}",
                    f"{section.design_flow_m3s:.2f}",
                    f"{result.calculated_flow_m3s:.2f}",
                    f"{result.area_m2:.2f}",
                    f"{result.wetted_perimeter_m:.2f}",
                    f"{result.hydraulic_radius_m:.2f}",
                    f"{result.top_width_m:.2f}",
                    f"{result.max_depth_m:.2f}",
                    f"{result.velocity_ms:.2f}",
                    f"{result.froude_number:.2f}",
                ])
            lines.append(format_fixed_table(rows, min_width=12))
            lines.extend(["", "二、输入断面点表"])
            for section in sections:
                point_rows = [["起点距x(m)", "高程z(m)"]]
                for x, z in section.points:
                    point_rows.append([f"{x:g}", f"{z:g}"])
                lines.append(section.name)
                lines.append(format_fixed_table(point_rows, min_width=12))
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def clear_inputs(self) -> None:
        for row in self.section_entries:
            for entry in row.values():
                entry.delete(0, tk.END)
        self._write(self.output, "")

    def restore_defaults(self) -> None:
        self.clear_inputs()
        defaults = [
            ("断面1", "120", "0.035", "0.0015", "0,103; 8,101; 16,100.2; 25,100.4; 34,102; 42,103.5"),
            ("断面2", "", "0.035", "0.0015", ""),
            ("断面3", "", "0.035", "0.0015", ""),
            ("断面4", "", "0.035", "0.0015", ""),
            ("断面5", "", "0.035", "0.0015", ""),
        ]
        for row, (name, flow, roughness, slope, points) in zip(self.section_entries, defaults):
            row["name"].insert(0, name)
            row["flow"].insert(0, flow)
            row["roughness"].insert(0, roughness)
            row["slope"].insert(0, slope)
            row["points"].insert(0, points)


class ManningWaterLevelAdvancedTab(ttk.Frame, FormMixin):
    FREQUENCIES = ("100", "50", "30", "25", "20", "10", "5", "2")

    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.last_manning_data: dict[str, object] | None = None

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        section_box = ttk.LabelFrame(top, text="断面基本数据")
        section_box.grid(row=0, column=0, sticky="ew")
        section_box.columnconfigure(4, weight=1)
        headers = ["断面名称", "设计流量Q(m³/s)", "糙率n", "水力坡降I(小数)", "断面点x,z(m,m)"]
        for col, header in enumerate(headers):
            ttk.Label(section_box, text=header).grid(row=0, column=col, padx=4, pady=4)
        defaults = [
            ("断面1", "120", "0.035", "0.0015", "0,103; 8,101; 16,100.2; 25,100.4; 34,102; 42,103.5"),
            ("断面2", "", "0.035", "0.0015", ""),
            ("断面3", "", "0.035", "0.0015", ""),
            ("断面4", "", "0.035", "0.0015", ""),
            ("断面5", "", "0.035", "0.0015", ""),
        ]
        self.section_entries: list[dict[str, tk.Entry]] = []
        for row, (name, flow, roughness, slope, points) in enumerate(defaults, 1):
            entries: dict[str, tk.Entry] = {}
            for col, (key, value, width) in enumerate([
                ("name", name, 12),
                ("flow", flow, 12),
                ("roughness", roughness, 9),
                ("slope", slope, 10),
                ("points", points, 96),
            ]):
                entry = ttk.Entry(section_box, width=width)
                entry.insert(0, value)
                entry.grid(row=row, column=col, padx=4, pady=4, sticky="ew")
                entries[key] = entry
            self.section_entries.append(entries)

        input_box = ttk.LabelFrame(top, text="断面点批量录入")
        input_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        input_box.columnconfigure(1, weight=1)
        ttk.Label(input_box, text="目标断面").grid(row=0, column=0, padx=(8, 4), pady=6, sticky="e")
        self.target_section = ttk.Combobox(input_box, width=12, state="readonly", values=[f"断面{i}" for i in range(1, 6)])
        self.target_section.current(0)
        self.target_section.grid(row=0, column=1, padx=(0, 8), pady=6, sticky="w")
        self._action_button(input_box, "导入Excel/CSV", self.import_points_file, bg="#1688e8", fg="white").grid(row=0, column=2, padx=6, pady=6)
        self._action_button(input_box, "应用粘贴数据", self.apply_pasted_points, bg="#006b63", fg="white").grid(row=0, column=3, padx=6, pady=6)
        self.paste_points = ScrolledText(input_box, height=4, wrap=tk.NONE)
        self.paste_points.grid(row=1, column=0, columnspan=4, padx=8, pady=(0, 6), sticky="ew")
        self.paste_points.insert(tk.END, "0\t103\n8\t101\n16\t100.2\n25\t100.4\n34\t102\n42\t103.5")

        ttk.Label(
            top,
            text="断面点支持导入Excel/CSV前两列，或直接从表格复制粘贴里程、高程两列。水力坡降I按小数输入，如0.0015。",
            foreground="#666666",
        ).grid(row=2, column=0, sticky="w", padx=8, pady=(8, 0))

        curve_box = ttk.LabelFrame(top, text="全断面水位流量关系与频率水位")
        curve_box.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        curve_box.columnconfigure(17, weight=1)
        self.level_step = self._entry(curve_box, 0, 0, "水位叠加高度(m)", "0.2", width=10)
        ttk.Label(curve_box, text="各频率流量Q(m³/s)").grid(row=0, column=2, padx=(14, 4), pady=6, sticky="e")
        self.frequency_entries: dict[str, tk.Entry] = {}
        for col, freq in enumerate(self.FREQUENCIES, 3):
            ttk.Label(curve_box, text=f"{freq}年").grid(row=0, column=col, padx=2, pady=(4, 0))
            entry = ttk.Entry(curve_box, width=8)
            entry.grid(row=1, column=col, padx=2, pady=(0, 6))
            self.frequency_entries[freq] = entry

        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "生成水位流量曲线", self.open_rating_curve_charts, bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空数据", self.clear_inputs).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "恢复默认", self.restore_defaults).pack(side=tk.LEFT, padx=(12, 0))

        self._build_roughness_box(top)
        self.output = self._result_box(self)

    def _build_roughness_box(self, parent: tk.Widget) -> None:
        parent.columnconfigure(1, weight=0)
        box = ttk.LabelFrame(parent, text="河道综合糙率计算")
        box.grid(row=0, column=1, rowspan=5, sticky="nsew", padx=(12, 0), pady=(0, 0))
        headers = ["部位", "糙率n", "湿周χ(m)"]
        for col, header in enumerate(headers):
            ttk.Label(box, text=header, anchor="center").grid(row=0, column=col, padx=4, pady=4, sticky="ew")
        defaults = [
            ("左岸岸坡", "0.022", "3.25"),
            ("左岸滩地", "0.036", "6.32"),
            ("河槽", "0.030", "9.51"),
            ("右岸滩地", "0.036", "6.47"),
            ("右岸岸坡", "0.022", "3.28"),
        ]
        self.roughness_entries: list[dict[str, tk.Entry]] = []
        for row, (name, n_value, chi) in enumerate(defaults, 1):
            entries: dict[str, tk.Entry] = {}
            for col, (key, value, width) in enumerate([
                ("name", name, 12),
                ("n", n_value, 8),
                ("chi", chi, 10),
            ]):
                entry = ttk.Entry(box, width=width)
                entry.insert(0, value)
                entry.grid(row=row, column=col, padx=4, pady=3, sticky="ew")
                entries[key] = entry
            self.roughness_entries.append(entries)
        self.composite_roughness = tk.StringVar(value="综合糙率 n = --")
        ttk.Label(box, textvariable=self.composite_roughness, foreground="#004a9f").grid(
            row=6, column=0, columnspan=3, padx=6, pady=(8, 4), sticky="w"
        )
        self._action_button(box, "计算糙率", self.calculate_composite_roughness, bg="#006b63", fg="white").grid(
            row=7, column=0, padx=4, pady=(4, 8), sticky="ew"
        )
        self._action_button(box, "应用到当前断面", self.apply_composite_roughness, bg="#1688e8", fg="white").grid(
            row=7, column=1, columnspan=2, padx=4, pady=(4, 8), sticky="ew"
        )
        ttk.Label(
            box,
            text="公式：n=Σ(nᵢχᵢ)/Σχᵢ",
            foreground="#666666",
        ).grid(row=8, column=0, columnspan=3, padx=6, pady=(0, 6), sticky="w")

    def _roughness_rows(self) -> tuple[list[list[str]], float]:
        rows = [["部位", "糙率nᵢ", "湿周χᵢ(m)", "nᵢχᵢ"]]
        numerator = 0.0
        denominator = 0.0
        for index, entries in enumerate(self.roughness_entries, 1):
            name = entries["name"].get().strip() or f"分段{index}"
            n_text = entries["n"].get().strip()
            chi_text = entries["chi"].get().strip()
            if not n_text and not chi_text:
                continue
            try:
                n_value = float(n_text)
                chi = float(chi_text)
            except ValueError as exc:
                raise HydrologyError(f"{name} 的糙率n和湿周χ必须为数字") from exc
            if n_value <= 0 or chi <= 0:
                raise HydrologyError(f"{name} 的糙率n和湿周χ必须大于0")
            product = n_value * chi
            numerator += product
            denominator += chi
            rows.append([name, f"{n_value:.4f}", f"{chi:.3f}", f"{product:.4f}"])
        if denominator <= 0:
            raise HydrologyError("至少需要填写一组有效的糙率n和湿周χ")
        composite = numerator / denominator
        rows.append(["合计", "", f"{denominator:.3f}", f"{numerator:.4f}"])
        rows.append(["综合糙率n", f"{composite:.4f}", "", ""])
        return rows, composite

    def calculate_composite_roughness(self) -> None:
        try:
            _rows, composite = self._roughness_rows()
            self.composite_roughness.set(f"综合糙率 n = {composite:.4f}")
        except Exception as exc:
            messagebox.showerror("糙率计算错误", str(exc))

    def apply_composite_roughness(self) -> None:
        try:
            _rows, composite = self._roughness_rows()
            row = self.section_entries[self._target_index()]
            row["roughness"].delete(0, tk.END)
            row["roughness"].insert(0, f"{composite:.4f}")
            self.composite_roughness.set(f"综合糙率 n = {composite:.4f}")
        except Exception as exc:
            messagebox.showerror("糙率应用错误", str(exc))

    def _parse_section_points(self, value: str, section_name: str) -> tuple[tuple[float, float], ...]:
        pairs = [item.strip() for item in re.split(r"[;\n]+", value.strip()) if item.strip()]
        points: list[tuple[float, float]] = []
        for item in pairs:
            parts = [part for part in re.split(r"[,，\s]+", item) if part]
            if len(parts) != 2:
                raise HydrologyError(f"{section_name} 断面点格式错误：{item}")
            try:
                points.append((float(parts[0]), float(parts[1])))
            except ValueError as exc:
                raise HydrologyError(f"{section_name} 断面点必须为数字：{item}") from exc
        if len(points) < 2:
            raise HydrologyError(f"{section_name} 至少需要2个断面点")
        return tuple(points)

    def _points_to_text(self, points: tuple[tuple[float, float], ...]) -> str:
        return "; ".join(f"{x:g},{z:g}" for x, z in points)

    def _target_index(self) -> int:
        match = re.search(r"(\d+)", self.target_section.get().strip())
        index = int(match.group(1)) - 1 if match else 0
        return max(0, min(index, len(self.section_entries) - 1))

    def _set_target_points(self, points: tuple[tuple[float, float], ...]) -> None:
        row = self.section_entries[self._target_index()]
        row["points"].delete(0, tk.END)
        row["points"].insert(0, self._points_to_text(points))

    def apply_pasted_points(self) -> None:
        try:
            points = self._parse_section_points(self.paste_points.get("1.0", tk.END), "粘贴数据")
            self._set_target_points(points)
            messagebox.showinfo("断面点录入", f"已写入 {len(points)} 个断面点。")
        except Exception as exc:
            messagebox.showerror("断面点录入错误", str(exc))

    def import_points_file(self) -> None:
        path_text = filedialog.askopenfilename(
            title="导入断面点Excel/CSV",
            filetypes=[("Excel/CSV", "*.xlsx *.csv"), ("Excel工作簿", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")],
        )
        if not path_text:
            return
        path = Path(path_text)
        try:
            if path.suffix.lower() == ".csv":
                points = self._read_points_csv(path)
            elif path.suffix.lower() == ".xlsx":
                points = self._read_points_xlsx(path)
            else:
                raise HydrologyError("仅支持 .xlsx 或 .csv 文件")
            self._set_target_points(points)
            messagebox.showinfo("导入断面点", f"已导入 {len(points)} 个断面点。")
        except Exception as exc:
            messagebox.showerror("导入断面点错误", str(exc))

    def _read_points_csv(self, path: Path) -> tuple[tuple[float, float], ...]:
        last_error: Exception | None = None
        for encoding in ("utf-8-sig", "gbk"):
            try:
                with path.open("r", encoding=encoding, newline="") as handle:
                    return self._rows_to_points(list(csv.reader(handle)), path.name)
            except Exception as exc:
                last_error = exc
        raise HydrologyError(f"CSV读取失败：{last_error}")

    def _read_points_xlsx(self, path: Path) -> tuple[tuple[float, float], ...]:
        ns = {
            "main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
            "rel": "http://schemas.openxmlformats.org/package/2006/relationships",
        }
        with ZipFile(path) as zf:
            shared: list[str] = []
            if "xl/sharedStrings.xml" in zf.namelist():
                root = ET.fromstring(zf.read("xl/sharedStrings.xml"))
                for si in root.findall("main:si", ns):
                    texts = [node.text or "" for node in si.findall(".//main:t", ns)]
                    shared.append("".join(texts))
            workbook = ET.fromstring(zf.read("xl/workbook.xml"))
            first_sheet = workbook.find("main:sheets/main:sheet", ns)
            if first_sheet is None:
                raise HydrologyError("Excel文件未找到工作表")
            rel_id = first_sheet.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
            rels = ET.fromstring(zf.read("xl/_rels/workbook.xml.rels"))
            target = None
            for rel in rels.findall("rel:Relationship", ns):
                if rel.attrib.get("Id") == rel_id:
                    target = rel.attrib.get("Target")
                    break
            if not target:
                raise HydrologyError("Excel文件未找到第一个工作表路径")
            sheet = ET.fromstring(zf.read("xl/" + target.lstrip("/")))
            rows: list[list[str]] = []
            for row in sheet.findall(".//main:sheetData/main:row", ns):
                values: list[str] = []
                for cell in row.findall("main:c", ns):
                    value_node = cell.find("main:v", ns)
                    value = value_node.text if value_node is not None else ""
                    if cell.attrib.get("t") == "s" and value:
                        value = shared[int(value)]
                    values.append(value)
                rows.append(values)
        return self._rows_to_points(rows, path.name)

    def _rows_to_points(self, rows: list[list[object]], source_name: str) -> tuple[tuple[float, float], ...]:
        points: list[tuple[float, float]] = []
        for row in rows:
            numeric: list[float] = []
            for cell in row:
                text = str(cell).strip()
                if not text:
                    continue
                try:
                    numeric.append(float(text))
                except ValueError:
                    continue
                if len(numeric) == 2:
                    break
            if len(numeric) == 2:
                points.append((numeric[0], numeric[1]))
        if len(points) < 2:
            raise HydrologyError(f"{source_name} 未读取到至少2行有效里程、高程数据")
        return tuple(points)

    def _read_sections(self) -> list[ManningSectionInput]:
        sections: list[ManningSectionInput] = []
        for row, entries in enumerate(self.section_entries, 1):
            flow_text = entries["flow"].get().strip()
            points_text = entries["points"].get().strip()
            if not flow_text and not points_text:
                continue
            name = entries["name"].get().strip() or f"断面{row}"
            try:
                flow = float(flow_text)
                roughness = float(entries["roughness"].get().strip())
                slope = float(entries["slope"].get().strip())
            except ValueError as exc:
                raise HydrologyError(f"{name} 的Q、n、I必须为数字") from exc
            points = self._parse_section_points(points_text, name)
            sections.append(ManningSectionInput(name, roughness, slope, flow, points))
        if not sections:
            raise HydrologyError("至少需要填写1个断面")
        return sections

    def _read_frequency_flows(self) -> dict[str, float]:
        flows: dict[str, float] = {}
        for freq, entry in self.frequency_entries.items():
            text = entry.get().strip()
            if not text:
                continue
            try:
                flows[freq] = float(text)
            except ValueError as exc:
                raise HydrologyError(f"{freq}年一遇流量必须为数字") from exc
        return flows

    def _rating_curve(
        self,
        section: ManningSectionInput,
        step: float,
        target_levels: list[float],
    ) -> list[tuple[float, float, float, float, float, float, float, float, float]]:
        points = tuple(sorted(section.points))
        min_z = min(z for _x, z in points)
        max_z = max(z for _x, z in points)
        thalweg_x, thalweg_z = min(points, key=lambda item: item[1])
        high = max([max_z, *target_levels]) if target_levels else max_z
        high += step
        level = min_z + step
        rows: list[tuple[float, float, float, float, float, float, float, float, float]] = []
        guard = 0
        while level <= high + 1e-9 and guard < 2000:
            q, area, wetted, radius, top_width, max_depth = manning_flow_for_level(
                points, level, section.roughness_n, section.slope
            )
            velocity = q / area if area > 0 else 0.0
            rows.append((
                round(level, 3),
                round(q, 3),
                round(area, 3),
                round(wetted, 3),
                round(radius, 3),
                round(top_width, 3),
                round(velocity, 3),
                round(thalweg_x, 3),
                round(thalweg_z, 3),
            ))
            level += step
            guard += 1
        return rows

    def calculate(self) -> None:
        try:
            sections = self._read_sections()
            step = float(self.level_step.get().strip())
            if step <= 0:
                raise HydrologyError("水位叠加高度必须大于0")
            frequency_flows = self._read_frequency_flows()
            results = [calculate_manning_water_level(section) for section in sections]
            frequency_results: dict[str, list[tuple[str, object]]] = {freq: [] for freq in frequency_flows}
            for freq, flow in frequency_flows.items():
                for section in sections:
                    frequency_results[freq].append((
                        section.name,
                        calculate_manning_water_level(ManningSectionInput(section.name, section.roughness_n, section.slope, flow, section.points)),
                    ))
            target_levels = [result.water_level for result in results]
            for values in frequency_results.values():
                target_levels.extend(result.water_level for _name, result in values)
            rating_tables = {section.name: self._rating_curve(section, step, target_levels) for section in sections}
            self.last_manning_data = {
                "sections": sections,
                "results": results,
                "frequency_flows": frequency_flows,
                "frequency_results": frequency_results,
                "rating_tables": rating_tables,
                "step": step,
            }

            lines = [
                "曼宁公式水位计算",
                "",
                "计算公式：Q = (1/n) * A * R^(2/3) * I^(1/2)",
                "其中 A 为过水面积，R=A/P，P为湿周，I为水力坡降；软件按断面折线自动试算水位。",
                "",
                "一、设计流量水位计算成果表",
            ]
            rows = [[
                "断面", "水位(m)", "设计Q(m³/s)", "复核Q(m³/s)", "面积A(m²)", "湿周P(m)",
                "水力半径R(m)", "水面宽B(m)", "深泓点高程(m)", "最大水深(m)", "流速(m/s)", "弗汝德数",
            ]]
            for section, result in zip(sections, results):
                rows.append([
                    result.name,
                    f"{result.water_level:.2f}",
                    f"{section.design_flow_m3s:.2f}",
                    f"{result.calculated_flow_m3s:.2f}",
                    f"{result.area_m2:.2f}",
                    f"{result.wetted_perimeter_m:.2f}",
                    f"{result.hydraulic_radius_m:.2f}",
                    f"{result.top_width_m:.2f}",
                    f"{result.thalweg_elevation_m:.2f}",
                    f"{result.max_depth_m:.2f}",
                    f"{result.velocity_ms:.2f}",
                    f"{result.froude_number:.2f}",
                ])
            lines.append(format_fixed_table(rows, min_width=12))
            if frequency_flows:
                lines.extend(["", "二、各频率水位流量计算成果表"])
                freq_rows = [["频率", "断面", "Q(m³/s)", "水位(m)", "深泓点高程(m)", "最大水深(m)", "复核Q(m³/s)"]]
                for freq in self.FREQUENCIES:
                    if freq not in frequency_results:
                        continue
                    for section_name, result in frequency_results[freq]:
                        freq_rows.append([
                            f"{freq}年一遇",
                            section_name,
                            f"{frequency_flows[freq]:.2f}",
                            f"{result.water_level:.2f}",
                            f"{result.thalweg_elevation_m:.2f}",
                            f"{result.max_depth_m:.2f}",
                            f"{result.calculated_flow_m3s:.2f}",
                        ])
                lines.append(format_fixed_table(freq_rows, min_width=12))
            lines.extend(["", "三、全断面水位流量关系计算表"])
            for section in sections:
                table = [[
                    "水位Z(m)", "流量Q(m³/s)", "面积A(m²)", "湿周P(m)", "水力半径R(m)",
                    "水面宽B(m)", "流速V(m/s)", "深泓点高程(m)", "水深H(m)",
                ]]
                for level, q, area, wetted, radius, top_width, velocity, thalweg_x, thalweg_z in rating_tables[section.name]:
                    table.append([
                        f"{level:.2f}",
                        f"{q:.2f}",
                        f"{area:.2f}",
                        f"{wetted:.2f}",
                        f"{radius:.2f}",
                        f"{top_width:.2f}",
                        f"{velocity:.2f}",
                        f"{thalweg_z:.2f}",
                        f"{level - thalweg_z:.2f}",
                    ])
                lines.append(section.name)
                lines.append(format_fixed_table(table, min_width=12))
            lines.extend(["", "四、输入断面点表"])
            for section in sections:
                point_rows = [["里程x(m)", "高程z(m)"]]
                for x, z in section.points:
                    point_rows.append([f"{x:g}", f"{z:g}"])
                lines.append(section.name)
                lines.append(format_fixed_table(point_rows, min_width=12))
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def open_rating_curve_charts(self) -> None:
        if self.last_manning_data is None:
            messagebox.showwarning("水位流量曲线", "请先完成曼宁公式计算。")
            return
        out_dir = Path(__file__).resolve().parent.parent / "输出成果"
        out_dir.mkdir(parents=True, exist_ok=True)
        path = out_dir / f"曼宁公式水位流量关系曲线_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        path.write_text(self._rating_curve_html(self.last_manning_data), encoding="utf-8")
        os.startfile(path)

    def _rating_curve_html(self, data: dict[str, object]) -> str:
        sections = data["sections"]
        frequency_results = data["frequency_results"]
        rating_tables = data["rating_tables"]
        parts = []
        for section in sections:
            section_name = section.name
            markers = []
            for freq in self.FREQUENCIES:
                for result_section_name, result in frequency_results.get(freq, []):
                    if result_section_name == section_name:
                        markers.append((f"{freq}年一遇", result.calculated_flow_m3s, result.water_level))
                        break
            parts.append(self._rating_curve_svg(section_name, rating_tables[section_name], markers, "全断面水位流量关系曲线"))
        return (
            "<!doctype html><html><head><meta charset='utf-8'>"
            "<title>曼宁公式水位流量关系曲线</title>"
            "<style>body{font-family:'Times New Roman','KaiTi','楷体',serif;margin:24px;background:#f7f7f7;color:#111;}"
            ".chart{background:#fff;border:1px solid #d0d0d0;margin:0 0 24px;padding:18px;}svg{width:100%;max-width:980px;height:auto;display:block;margin:auto;}"
            "</style></head><body><h1>曼宁公式水位流量关系曲线</h1>"
            + "\n".join(parts)
            + "</body></html>"
        )

    def _current_report_figures(self) -> list[tuple[str, str]]:
        if self.last_manning_data is None:
            return []
        return [("曼宁公式水位流量关系曲线", self._rating_curve_html(self.last_manning_data))]

    def _rating_curve_svg(
        self,
        section_name: str,
        rows: list[tuple[float, float, float, float, float, float, float, float, float]],
        markers: list[tuple[str, float, float]] | None,
        title: str,
    ) -> str:
        width, height = 920, 520
        left, right, top, bottom = 82, 48, 54, 72
        plot_w = width - left - right
        plot_h = height - top - bottom
        plot_rows = rows
        markers = list(markers or [])
        if markers:
            marker_q = max(q for _label, q, _z in markers)
            focused_max_q = max(marker_q * 1.6, 1.0)
            plot_rows = [row for row in rows if row[1] <= focused_max_q]
            if len(plot_rows) < 2:
                plot_rows = []
                for row in rows:
                    plot_rows.append(row)
                    if row[1] >= marker_q:
                        break
                if len(plot_rows) < 2:
                    plot_rows = rows[:2] or rows
                if plot_rows:
                    focused_max_q = max(focused_max_q, max(row[1] for row in plot_rows) * 1.08)
            max_q = focused_max_q
        else:
            q_values = [row[1] for row in rows]
            max_q = max(max(q_values, default=1.0), 1.0) * 1.08
        z_values = [row[0] for row in plot_rows]
        z_values.extend(z for _label, _q, z in markers)
        min_z = min(z_values, default=0.0)
        max_z = max(z_values, default=min_z + 1.0)
        if markers:
            z_span = max(max_z - min_z, 1.0)
            min_z -= z_span * 0.05
            max_z += z_span * 0.22
        if abs(max_z - min_z) < 1e-9:
            max_z = min_z + 1.0
        sx = lambda q: left + q / max_q * plot_w
        sy = lambda z: top + (max_z - z) / (max_z - min_z) * plot_h
        polyline = " ".join(f"{sx(q):.1f},{sy(z):.1f}" for z, q, *_rest in plot_rows)
        grid = []
        for idx in range(6):
            q = max_q * idx / 5
            x = sx(q)
            grid.append(f"<line x1='{x:.1f}' y1='{top}' x2='{x:.1f}' y2='{top + plot_h}' stroke='#ddd'/>")
            grid.append(f"<text x='{x:.1f}' y='{top + plot_h + 24}' text-anchor='middle' font-size='12'>{q:.2f}</text>")
        for idx in range(6):
            z = min_z + (max_z - min_z) * idx / 5
            y = sy(z)
            grid.append(f"<line x1='{left}' y1='{y:.1f}' x2='{left + plot_w}' y2='{y:.1f}' stroke='#ddd'/>")
            grid.append(f"<text x='{left - 12}' y='{y + 4:.1f}' text-anchor='end' font-size='12'>{z:.2f}</text>")
        marker_svg_parts = []
        if markers:
            marker_colors = ["#d62424", "#0b63ce", "#14823b", "#9a4b00", "#7a32b4", "#006b68", "#c2185b", "#5f5f5f"]
            placed_label_ys: list[float] = []
            for idx, (label, q, z) in enumerate(sorted(markers, key=lambda item: sy(item[2]))):
                x = sx(q)
                y = sy(z)
                label_x = min(x + 14, left + plot_w - 172)
                base_label_y = max(top + 18, min(y - 16, top + plot_h - 36))
                label_y = base_label_y
                for previous_y in placed_label_ys:
                    if abs(label_y - previous_y) < 32:
                        label_y = previous_y + 32
                if label_y > top + plot_h - 20:
                    label_y = top + plot_h - 20
                    for previous_y in reversed(placed_label_ys):
                        if abs(label_y - previous_y) < 32:
                            label_y = previous_y - 32
                label_y = max(top + 18, label_y)
                placed_label_ys.append(label_y)
                color = marker_colors[idx % len(marker_colors)]
                marker_svg_parts.append(
                    f"<line x1='{left}' y1='{y:.1f}' x2='{x:.1f}' y2='{y:.1f}' stroke='{color}' stroke-dasharray='5 4' opacity='0.8'/>"
                    f"<line x1='{x:.1f}' y1='{y:.1f}' x2='{x:.1f}' y2='{top + plot_h}' stroke='{color}' stroke-dasharray='5 4' opacity='0.8'/>"
                    f"<circle cx='{x:.1f}' cy='{y:.1f}' r='4.8' fill='{color}'/>"
                    f"<line x1='{x:.1f}' y1='{y:.1f}' x2='{label_x:.1f}' y2='{label_y - 5:.1f}' stroke='{color}' stroke-width='1' opacity='0.75'/>"
                    f"<text x='{label_x:.1f}' y='{label_y:.1f}' font-size='13' fill='{color}' font-weight='bold'>{html_lib.escape(label)} Z={z:.2f}m</text>"
                    f"<text x='{label_x:.1f}' y='{label_y + 17:.1f}' font-size='13' fill='{color}'>Q={q:.2f}m³/s</text>"
                )
        marker_svg = "".join(marker_svg_parts)
        return (
            "<div class='chart'>"
            f"<svg viewBox='0 0 {width} {height}' xmlns='http://www.w3.org/2000/svg'>"
            f"<text x='{width/2}' y='26' text-anchor='middle' font-size='20' font-weight='bold'>{html_lib.escape(section_name + ' ' + title)}</text>"
            + "".join(grid)
            + f"<line x1='{left}' y1='{top}' x2='{left}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<line x1='{left}' y1='{top + plot_h}' x2='{left + plot_w}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<polyline points='{polyline}' fill='none' stroke='#222' stroke-width='2.3'/>"
            + marker_svg
            + f"<text x='{left + plot_w / 2}' y='{height - 18}' text-anchor='middle' font-size='15'>流量Q(m³/s)</text>"
            + f"<text x='24' y='{top + plot_h / 2}' transform='rotate(-90 24 {top + plot_h / 2})' text-anchor='middle' font-size='15'>水位Z/m</text>"
            + "</svg></div>"
        )

    def clear_inputs(self) -> None:
        for row in self.section_entries:
            for entry in row.values():
                entry.delete(0, tk.END)
        for entry in self.frequency_entries.values():
            entry.delete(0, tk.END)
        for row in self.roughness_entries:
            for entry in row.values():
                entry.delete(0, tk.END)
        self.composite_roughness.set("综合糙率 n = --")
        self.paste_points.delete("1.0", tk.END)
        self._write(self.output, "")
        self.last_manning_data = None

    def restore_defaults(self) -> None:
        self.clear_inputs()
        defaults = [
            ("断面1", "120", "0.035", "0.0015", "0,103; 8,101; 16,100.2; 25,100.4; 34,102; 42,103.5"),
            ("断面2", "", "0.035", "0.0015", ""),
            ("断面3", "", "0.035", "0.0015", ""),
            ("断面4", "", "0.035", "0.0015", ""),
            ("断面5", "", "0.035", "0.0015", ""),
        ]
        for row, (name, flow, roughness, slope, points) in zip(self.section_entries, defaults):
            row["name"].insert(0, name)
            row["flow"].insert(0, flow)
            row["roughness"].insert(0, roughness)
            row["slope"].insert(0, slope)
            row["points"].insert(0, points)
        self.level_step.delete(0, tk.END)
        self.level_step.insert(0, "0.2")
        self.paste_points.insert(tk.END, "0\t103\n8\t101\n16\t100.2\n25\t100.4\n34\t102\n42\t103.5")
        roughness_defaults = [
            ("左岸岸坡", "0.022", "3.25"),
            ("左岸滩地", "0.036", "6.32"),
            ("河槽", "0.030", "9.51"),
            ("右岸滩地", "0.036", "6.47"),
            ("右岸岸坡", "0.022", "3.28"),
        ]
        for row, (name, n_value, chi) in zip(self.roughness_entries, roughness_defaults):
            row["name"].insert(0, name)
            row["n"].insert(0, n_value)
            row["chi"].insert(0, chi)

class RegionalFormulaTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="流域特征参数")
        basic.grid(row=0, column=0, sticky="ew")
        basic.columnconfigure(5, weight=1)
        self.basin = self._entry(basic, 0, 0, "流域名称", "示例流域", width=16)
        self.area = self._entry(basic, 0, 1, "面积F(km²)", "100", width=16)
        ttk.Label(basic, text="选择分区").grid(row=0, column=4, padx=(10, 4), pady=6, sticky="e")
        self.region = ttk.Combobox(
            basic,
            width=45,
            state="readonly",
            values=[f"{item.region_id}. {item.name}" for item in REGION_PARAMS],
        )
        self.region.current(0)
        self.region.grid(row=0, column=5, padx=(0, 12), pady=6, sticky="ew")
        self.region.bind("<<ComboboxSelected>>", lambda _event: self.refresh_region_params())

        param_box = ttk.LabelFrame(top, text="所选分区参数（自动关联）")
        param_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        headers = ["参数", *REGION_FREQUENCIES]
        for col, header in enumerate(headers):
            ttk.Label(param_box, text=header, width=12, anchor="center").grid(row=0, column=col, padx=3, pady=4)
        ttk.Label(param_box, text="K值", anchor="center").grid(row=1, column=0, padx=3, pady=4, sticky="ew")
        ttk.Label(param_box, text="n值", anchor="center").grid(row=2, column=0, padx=3, pady=4, sticky="ew")
        self.k_vars: dict[str, tk.StringVar] = {}
        self.n_vars: dict[str, tk.StringVar] = {}
        for col, freq in enumerate(REGION_FREQUENCIES, 1):
            k_var = tk.StringVar(value="")
            n_var = tk.StringVar(value="")
            self.k_vars[freq] = k_var
            self.n_vars[freq] = n_var
            ttk.Label(param_box, textvariable=k_var, background="#fffbc7", width=12, anchor="center").grid(row=1, column=col, padx=3, pady=4)
            ttk.Label(param_box, textvariable=n_var, background="#fffbc7", width=12, anchor="center").grid(row=2, column=col, padx=3, pady=4)
        self.region_note = tk.StringVar(value="")
        ttk.Label(param_box, textvariable=self.region_note, foreground="#666666").grid(
            row=3, column=0, columnspan=len(headers), padx=8, pady=(2, 6), sticky="w"
        )
        self.refresh_region_params()

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", pady=(12, 0))
        self.allow_out_of_range = tk.BooleanVar(value=False)
        ttk.Checkbutton(actions, text="允许超出分区面积范围", variable=self.allow_out_of_range).pack(side=tk.LEFT)
        self._action_button(actions, "开始计算", self.calculate, bg="#2fa84f", fg="white").pack(side=tk.LEFT, padx=(16, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def _selected_region_id(self) -> int:
        return int(self.region.get().split(".", 1)[0].strip())

    def refresh_region_params(self) -> None:
        try:
            region_id = self._selected_region_id()
            region = REGION_PARAMS[region_id - 1]
            for freq, n, k in regional_formula_parameter_rows(region_id):
                self.k_vars[freq].set(f"{k:g}")
                self.n_vars[freq].set(f"{n:g}")
            low, high = region.area_range
            if low is None and high is None:
                range_text = "适用面积范围：未限定"
            elif low is None:
                range_text = f"适用面积范围：F < {high:g} km²"
            elif high is None:
                range_text = f"适用面积范围：F > {low:g} km²"
            else:
                range_text = f"适用面积范围：{low:g} < F < {high:g} km²"
            self.region_note.set(f"分区：{region.region_id}. {region.name}    {range_text}")
        except Exception:
            for freq in REGION_FREQUENCIES:
                self.k_vars[freq].set("")
                self.n_vars[freq].set("")
            self.region_note.set("")

    def calculate(self) -> None:
        try:
            region_id = self._selected_region_id()
            result = calculate_regional_formula(
                RegionalFormulaInput(
                    basin_name=self.basin.get().strip() or "未命名流域",
                    area=self._float(self.area, "面积 F"),
                    region_id=region_id,
                    allow_out_of_range=self.allow_out_of_range.get(),
                )
            )
            lines = [
                "一、输入参数表",
                format_fixed_table([
                    ["项目", "数值"],
                    ["流域名称", result.basin_name],
                    ["流域面积F(km²)", self.area.get().strip()],
                    ["分区", f"{result.region_id}. {result.region_name}"],
                    ["允许超出分区面积范围", "是" if self.allow_out_of_range.get() else "否"],
                ], min_width=12),
                "",
                "二、地区经验公式参数表",
            ]
            rows = regional_formula_parameter_rows(region_id)
            parameter_rows = [["频率", "n", "K", "计算式"]]
            for freq, n, k in rows:
                parameter_rows.append([freq, f"{n:g}", f"{k:g}", "Q=K*F^(1-n)"])
            lines.append(format_fixed_table(parameter_rows, min_width=10))
            lines.extend([
                "",
                "三、洪峰流量成果表",
            ])
            result_rows = [["频率", "n", "K", "Q(m³/s)"]]
            for freq, n, k in rows:
                result_rows.append([freq, f"{n:g}", f"{k:g}", result.flows[freq]])
            lines.append(format_fixed_table(result_rows, min_width=10))
            if result.warnings:
                lines.extend(["", "四、数据复核警告", *[f"警告：{item}" for item in result.warnings]])
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


def show_text_window(master: tk.Tk, title: str, content: str) -> None:
    window = tk.Toplevel(master)
    window.title(title)
    window.geometry("820x520")
    window.minsize(640, 420)
    text = ScrolledText(window, font=(DASHBOARD_CARD_FONT, 10), wrap=tk.WORD)
    text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
    text.insert(tk.END, content)
    text.configure(state="disabled")


def show_image_gallery(master: tk.Tk, title: str, paths: list[Path]) -> None:
    missing = [str(path) for path in paths if not path.exists()]
    if missing:
        messagebox.showerror("资料缺失", "未找到资料文件：\n" + "\n".join(missing))
        return

    window = tk.Toplevel(master)
    window.title(title)
    window.geometry("1120x760")
    window.minsize(820, 520)
    canvas = tk.Canvas(window, background="white")
    y_scroll = ttk.Scrollbar(window, orient=tk.VERTICAL, command=canvas.yview)
    x_scroll = ttk.Scrollbar(window, orient=tk.HORIZONTAL, command=canvas.xview)
    canvas.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
    canvas.grid(row=0, column=0, sticky="nsew")
    y_scroll.grid(row=0, column=1, sticky="ns")
    x_scroll.grid(row=1, column=0, sticky="ew")
    window.rowconfigure(0, weight=1)
    window.columnconfigure(0, weight=1)

    frame = ttk.Frame(canvas)
    canvas.create_window((0, 0), window=frame, anchor="nw")
    window.images = []
    for path in paths:
        ttk.Label(frame, text=path.stem, font=(DASHBOARD_CARD_FONT, 10, "bold")).pack(anchor="w", padx=12, pady=(12, 4))
        image = tk.PhotoImage(file=str(path))
        window.images.append(image)
        ttk.Label(frame, image=image).pack(anchor="w", padx=12, pady=(0, 16))

    frame.update_idletasks()
    canvas.configure(scrollregion=canvas.bbox("all"))


class InstantUnitLineTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.last_instant_result = None

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="输入参数：流域特征、雨型分区")
        basic.grid(row=0, column=0, sticky="ew")
        basic.columnconfigure(7, weight=1)
        self.area = self._entry(basic, 0, 0, "流域面积F(km²)", "47.02", width=12)
        self.slope = self._entry(basic, 0, 1, "主河槽比降J(‰)", "95.724", width=12)
        self.delta_t = self._entry(basic, 0, 2, "计算步长Δt(h)", "1", width=10)
        self.delta_t.configure(state="disabled")
        self.cs_ratio = self._entry(basic, 0, 3, "Cs/Cv", "3.5", width=10)
        ttk.Label(basic, text="雨型分区").grid(row=1, column=0, padx=(10, 4), pady=6, sticky="e")
        self.rainfall_zone = ttk.Combobox(
            basic,
            width=24,
            state="readonly",
            values=_combo_label_values(INSTANT_RAINFALL_ZONE_LABELS),
        )
        self.rainfall_zone.current(3 if len(INSTANT_RAINFALL_ZONE_LABELS) > 3 else 0)
        self.rainfall_zone.grid(row=1, column=1, columnspan=3, padx=(0, 12), pady=6, sticky="w")
        self.rainfall_zone.bind("<<ComboboxSelected>>", lambda _event: self.on_rainfall_zone_changed())
        self.area.bind("<FocusOut>", lambda _event: self.update_distribution_preview())
        ttk.Label(basic, text="汇流参数公式").grid(row=1, column=4, padx=(10, 4), pady=6, sticky="e")
        self.unit_param_region = ttk.Combobox(
            basic,
            width=24,
            state="readonly",
            values=_combo_label_values(INSTANT_UNIT_PARAM_FORMULA_LABELS),
        )
        self.unit_param_region.current(0)
        self.unit_param_region.grid(row=1, column=5, columnspan=3, padx=(0, 12), pady=6, sticky="w")
        ttk.Label(basic, text="入渗率分区").grid(row=2, column=0, padx=(10, 4), pady=6, sticky="e")
        self.infiltration_region = ttk.Combobox(
            basic,
            width=36,
            state="readonly",
            values=_combo_label_values(INSTANT_INFILTRATION_TABLE_LABELS),
        )
        self.infiltration_region.grid(row=2, column=1, columnspan=4, padx=(0, 12), pady=6, sticky="w")
        self.auto_f = tk.BooleanVar(value=True)
        ttk.Checkbutton(
            basic,
            text="按附表12自动查算f",
            variable=self.auto_f,
            command=self.update_f_entry_state,
        ).grid(row=2, column=5, columnspan=3, padx=(10, 12), pady=6, sticky="w")
        ttk.Label(
            basic,
            text="Δt按图集附表10固定为1h；主雨峰自动判定：F<100选1h，100-300选3h，300-1000选6h；雨型系数按附表10分块读取",
            foreground="#666666",
        ).grid(row=3, column=0, columnspan=8, padx=10, pady=(2, 8), sticky="w")

        storm = ttk.LabelFrame(top, text="输入参数：暴雨统计、点面折减系数αt、流域形状修正系数γ")
        storm.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        storm_headers = ["控制时段", "均值(mm)", "Cv", "αt", "γ"]
        for col, header in enumerate(storm_headers):
            ttk.Label(storm, text=header).grid(row=0, column=col, padx=8, pady=4)
        storm_defaults = {
            "1h": ("25", "0.60", "0.966", "0.929"),
            "6h": ("35", "0.55", "0.969", "0.929"),
            "24h": ("50", "0.50", "0.985", "0.919"),
            "3h": ("自动H1/H6", "自动", "0.951", "0.929"),
        }
        self.storm_entries: dict[str, dict[str, tk.Entry]] = {}
        for row, duration in enumerate(["1h", "3h", "6h", "24h"], 1):
            ttk.Label(storm, text=duration).grid(row=row, column=0, padx=8, pady=3)
            self.storm_entries[duration] = {}
            for col, key in enumerate(["mean", "cv", "alpha", "gamma"], 1):
                entry = ttk.Entry(storm, width=12)
                entry.insert(0, storm_defaults[duration][col - 1])
                if duration == "3h" and key in {"mean", "cv"}:
                    entry.configure(state="disabled")
                entry.grid(row=row, column=col, padx=8, pady=3)
                self.storm_entries[duration][key] = entry

        preview_box = ttk.LabelFrame(top, text="当前雨型分配系数预览（按分区和流域面积自动关联）")
        preview_box.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        preview_box.columnconfigure(0, weight=1)
        self.distribution_preview = RichTableText(preview_box, height=8)
        self.distribution_preview.grid(row=0, column=0, sticky="ew", padx=10, pady=8)

        freq_box = ttk.LabelFrame(top, text="输入参数：各频率平均入渗率f（入渗量ΔF=f×Δt；tc、初损、n、k自动计算）")
        freq_box.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        headers = ["频率", "平均入渗率f(mm/h)"]
        for col, header in enumerate(headers):
            ttk.Label(freq_box, text=header).grid(row=0, column=col, padx=6, pady=4)
        defaults = [(freq, "自动查表") for freq in DESIGN_FREQUENCIES_7]
        self.freq_entries: dict[str, dict[str, tk.Entry]] = {}
        for row, values in enumerate(defaults, 1):
            freq = values[0]
            ttk.Label(freq_box, text=f"{freq}年").grid(row=row, column=0, padx=6, pady=3)
            self.freq_entries[freq] = {}
            for col, key in enumerate(["f"], 1):
                entry = ttk.Entry(freq_box, width=13)
                entry.insert(0, values[col])
                entry.grid(row=row, column=col, padx=6, pady=3)
                self.freq_entries[freq][key] = entry
        ttk.Label(
            freq_box,
            text="默认不用填写f：软件按附表12由Ptc和tc自动查算。取消自动查算后，才使用左侧手动f。",
            foreground="#666666",
        ).grid(row=1, column=2, rowspan=7, padx=(12, 8), pady=4, sticky="nw")

        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, sticky="ew", pady=(12, 0))
        self.keep_negative = tk.BooleanVar(value=False)
        ttk.Checkbutton(actions, text="净雨允许负值显示（洪水计算仍按0处理）", variable=self.keep_negative).pack(side=tk.LEFT)
        self._action_button(actions, "生成计算表", self.calculate, bg="#2fa84f", fg="white").pack(side=tk.LEFT, padx=(16, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)
        self.update_infiltration_region_default()
        self.update_f_entry_state()
        self.update_distribution_preview()

    def _selected_zone_id(self) -> str:
        return _combo_id_from_label(self.rainfall_zone.get(), INSTANT_RAINFALL_ZONE_LABELS)

    def _selected_unit_param_region(self) -> str:
        return _combo_id_from_label(self.unit_param_region.get(), INSTANT_UNIT_PARAM_FORMULA_LABELS)

    def _selected_infiltration_region(self) -> str:
        return _combo_id_from_label(self.infiltration_region.get(), INSTANT_INFILTRATION_TABLE_LABELS)

    def on_rainfall_zone_changed(self) -> None:
        self.update_infiltration_region_default()
        self.update_distribution_preview()

    def update_infiltration_region_default(self) -> None:
        table_id = default_instant_infiltration_table_id(self._selected_zone_id())
        _set_combo_current_by_id(self.infiltration_region, INSTANT_INFILTRATION_TABLE_LABELS, table_id)

    def update_f_entry_state(self) -> None:
        state = "disabled" if self.auto_f.get() else "normal"
        for entries in getattr(self, "freq_entries", {}).values():
            entry = entries["f"]
            entry.configure(state="normal")
            current = entry.get().strip()
            if self.auto_f.get():
                entry.delete(0, tk.END)
                entry.insert(0, "自动查表")
            elif current in {"", "自动", "自动查表"}:
                entry.delete(0, tk.END)
                entry.insert(0, "7.0")
            entry.configure(state=state)

    def update_distribution_preview(self) -> None:
        try:
            area = self._float(self.area, "流域面积F")
            distribution = select_instant_rainfall_distribution(self._selected_zone_id(), area)
            text = instant_rainfall_distribution_table_text(distribution)
        except Exception as exc:
            text = f"雨型预览暂不可用：{exc}"
        self._write(self.distribution_preview, text)

    def calculate(self) -> None:
        try:
            area = self._float(self.area, "流域面积F")
            slope = self._float(self.slope, "主河槽比降J")
            delta_t = self._float(self.delta_t, "时段Δt")
            cs_ratio = self._float(self.cs_ratio, "Cs/Cv")
            rainfall_distribution = select_instant_rainfall_distribution(self._selected_zone_id(), area)
            unit_param_region = self._selected_unit_param_region()
            rainfall_design = InstantRainfallDesignInput(
                h1_mean_mm=self._float(self.storm_entries["1h"]["mean"], "1h均值"),
                h1_cv=self._float(self.storm_entries["1h"]["cv"], "1h Cv"),
                h6_mean_mm=self._float(self.storm_entries["6h"]["mean"], "6h均值"),
                h6_cv=self._float(self.storm_entries["6h"]["cv"], "6h Cv"),
                h24_mean_mm=self._float(self.storm_entries["24h"]["mean"], "24h均值"),
                h24_cv=self._float(self.storm_entries["24h"]["cv"], "24h Cv"),
                alpha1=self._float(self.storm_entries["1h"]["alpha"], "1h αt"),
                alpha3=self._float(self.storm_entries["3h"]["alpha"], "3h αt"),
                alpha6=self._float(self.storm_entries["6h"]["alpha"], "6h αt"),
                alpha24=self._float(self.storm_entries["24h"]["alpha"], "24h αt"),
                gamma1=self._float(self.storm_entries["1h"]["gamma"], "1h γ"),
                gamma3=self._float(self.storm_entries["3h"]["gamma"], "3h γ"),
                gamma6=self._float(self.storm_entries["6h"]["gamma"], "6h γ"),
                gamma24=self._float(self.storm_entries["24h"]["gamma"], "24h γ"),
                cs_ratio=cs_ratio,
            )
            runoff_settings = []
            for freq in DESIGN_FREQUENCIES_7:
                entries = self.freq_entries[freq]
                manual_f = None if self.auto_f.get() else self._float(entries["f"], f"{freq} 入渗率f")
                runoff_settings.append(
                    FrequencyRunoffSetting(
                        frequency=freq,
                        infiltration_rate_mmh=manual_f,
                        unit_param_region=unit_param_region,
                        infiltration_table_id=self._selected_infiltration_region(),
                    )
                )
            result = calculate_instant_unit_line_atlas_batch(
                area_km2=area,
                river_slope_permille=slope,
                rainfall_distribution=rainfall_distribution,
                rainfall_design=rainfall_design,
                runoff_settings=runoff_settings,
                delta_t_h=delta_t,
                keep_negative_net_rain=self.keep_negative.get(),
            )
            self.last_instant_result = result
            self._write(self.output, instant_unit_line_audit_result_to_text(result, DESIGN_FREQUENCIES_7))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def _export_result_xlsx(self, path: str) -> bool:
        if self.last_instant_result is None:
            return False
        export_rainfall_runoff_audit_xlsx(
            self.last_instant_result,
            path,
            title="瞬时单位线法专项核验",
        )
        return True

    def _current_report_figures(self) -> list[tuple[str, str]]:
        result = self.last_instant_result
        if result is None:
            return []
        figures: list[tuple[str, str]] = []
        for freq in DESIGN_FREQUENCIES_7:
            flood = result.flood_results.get(freq)
            if flood is None:
                continue
            rain_series = [(float(row.hour), row.rainfall_mm.get(freq, 0.0)) for row in result.net_rain_table]
            infiltration_series = [(float(row.hour), row.infiltration_mm.get(freq, 0.0)) for row in result.net_rain_table]
            net_series = [(float(row.hour), max(0.0, row.net_rain_mm.get(freq, 0.0))) for row in result.net_rain_table]
            hydro_series = [(point.time_h, max(0.0, point.discharge_m3s)) for point in flood.hydrograph]
            unit_series = [(row.time_h, max(0.0, row.unit_depth_fraction)) for row in flood.unit_line]
            figures.append((
                f"瞬时单位线法 {freq}年逐时面雨量、入渗与净雨",
                self._line_chart_svg(
                    f"{freq}年逐时面雨量、入渗与净雨",
                    [
                        ("面雨量", rain_series, "#1f4e79"),
                        ("入渗量", infiltration_series, "#ed7d31"),
                        ("净雨", net_series, "#548235"),
                    ],
                    "雨量(mm)",
                ),
            ))
            figures.append((
                f"瞬时单位线法 {freq}年洪水过程线",
                self._line_chart_svg(
                    f"{freq}年洪水过程线",
                    [("洪水过程Q", hydro_series, "#1468b3")],
                    "流量Q(m³/s)",
                ),
            ))
            figures.append((
                f"瞬时单位线法 {freq}年单位线",
                self._line_chart_svg(
                    f"{freq}年单位线",
                    [("单位线比例", unit_series, "#247a4a")],
                    "单位净雨比例",
                ),
            ))
        return figures


class RationalFormulaTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="输入参数：流域特征、雨型分区、推理公式分区")
        basic.grid(row=0, column=0, sticky="ew")
        basic.columnconfigure(9, weight=1)
        self.area = self._entry(basic, 0, 0, "流域面积F(km²)", "47.02", width=12)
        self.length = self._entry(basic, 0, 1, "主河道长度L(km)", "13.2", width=12)
        self.slope = self._entry(basic, 0, 2, "主河槽比降J(‰)", "95.724", width=12)
        self.delta_t = self._entry(basic, 0, 3, "计算步长Δt(h)", "1", width=10)
        self.delta_t.configure(state="disabled")
        self.cs_ratio = self._entry(basic, 0, 4, "Cs/Cv", "3.5", width=10)
        ttk.Label(basic, text="雨型分区").grid(row=1, column=0, padx=(10, 4), pady=6, sticky="e")
        self.rainfall_zone = ttk.Combobox(
            basic,
            width=24,
            state="readonly",
            values=_combo_label_values(INSTANT_RAINFALL_ZONE_LABELS),
        )
        self.rainfall_zone.current(3 if len(INSTANT_RAINFALL_ZONE_LABELS) > 3 else 0)
        self.rainfall_zone.grid(row=1, column=1, columnspan=3, padx=(0, 12), pady=6, sticky="w")
        self.rainfall_zone.bind("<<ComboboxSelected>>", lambda _event: self.on_rainfall_zone_changed())
        self.area.bind("<FocusOut>", lambda _event: self.update_distribution_preview())

        ttk.Label(basic, text="推理公式分区").grid(row=1, column=4, padx=(10, 4), pady=6, sticky="e")
        self.rational_region = ttk.Combobox(
            basic,
            width=28,
            state="readonly",
            values=_combo_label_values(RATIONAL_FORMULA_REGION_LABELS),
        )
        self.rational_region.current(0)
        self.rational_region.grid(row=1, column=5, columnspan=4, padx=(0, 12), pady=6, sticky="w")

        ttk.Label(basic, text="入渗率分区").grid(row=2, column=0, padx=(10, 4), pady=6, sticky="e")
        self.infiltration_region = ttk.Combobox(
            basic,
            width=36,
            state="readonly",
            values=_combo_label_values(INSTANT_INFILTRATION_TABLE_LABELS),
        )
        self.infiltration_region.grid(row=2, column=1, columnspan=4, padx=(0, 12), pady=6, sticky="w")
        self.auto_f = tk.BooleanVar(value=True)
        ttk.Checkbutton(
            basic,
            text="按附表12自动查算f",
            variable=self.auto_f,
            command=self.update_f_entry_state,
        ).grid(row=2, column=5, columnspan=4, padx=(10, 12), pady=6, sticky="w")
        ttk.Label(
            basic,
            text="推理公式法前段采用图集暴雨、附表10雨型分配、附表12入渗率；汇流按表10 m关系和公式(14)-(20)计算。",
            foreground="#666666",
        ).grid(row=3, column=0, columnspan=10, padx=10, pady=(2, 8), sticky="w")

        storm = ttk.LabelFrame(top, text="输入参数：暴雨统计、点面折减系数αt、流域形状修正系数γ")
        storm.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        for col, header in enumerate(["控制时段", "均值(mm)", "Cv", "αt", "γ"]):
            ttk.Label(storm, text=header).grid(row=0, column=col, padx=8, pady=4)
        storm_defaults = {
            "1h": ("25", "0.60", "0.966", "0.929"),
            "6h": ("35", "0.55", "0.969", "0.929"),
            "24h": ("50", "0.50", "0.985", "0.919"),
            "3h": ("自动H1/H6", "自动", "0.951", "0.929"),
        }
        self.storm_entries: dict[str, dict[str, tk.Entry]] = {}
        for row, duration in enumerate(["1h", "3h", "6h", "24h"], 1):
            ttk.Label(storm, text=duration).grid(row=row, column=0, padx=8, pady=3)
            self.storm_entries[duration] = {}
            for col, key in enumerate(["mean", "cv", "alpha", "gamma"], 1):
                entry = ttk.Entry(storm, width=12)
                entry.insert(0, storm_defaults[duration][col - 1])
                if duration == "3h" and key in {"mean", "cv"}:
                    entry.configure(state="disabled")
                entry.grid(row=row, column=col, padx=8, pady=3)
                self.storm_entries[duration][key] = entry

        shape = ttk.LabelFrame(top, text="输入参数：推理公式汇流曲线")
        shape.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        self.shape_ka = self._entry(shape, 0, 0, "Ka(自动)", "自动查附图58/59", width=14)
        self.shape_kw = self._entry(shape, 0, 1, "Kw(自动)", "自动查附图58/59", width=14)
        self.shape_kt = self._entry(shape, 0, 2, "KT(自动)", "自动查附图58/59", width=14)
        self.baseflow = self._entry(shape, 0, 3, "基流量(自动)", "自动查附图66", width=14)
        for entry in (self.shape_ka, self.shape_kw, self.shape_kt, self.baseflow):
            entry.configure(state="disabled")
        ttk.Label(
            shape,
            text="Ka、Kw、KT按附图58/59自动查算，基流量按附图66自动查算；计算后会回填具体数值，可在计算书中查看来源。",
            foreground="#666666",
        ).grid(row=1, column=0, columnspan=8, padx=10, pady=(2, 8), sticky="w")

        preview_box = ttk.LabelFrame(top, text="当前雨型分配系数预览（按分区和流域面积自动关联）")
        preview_box.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        preview_box.columnconfigure(0, weight=1)
        self.distribution_preview = RichTableText(preview_box, height=7)
        self.distribution_preview.grid(row=0, column=0, sticky="ew", padx=10, pady=8)

        freq_box = ttk.LabelFrame(top, text="输入参数：各频率平均入渗率f（默认按附表12自动查算）")
        freq_box.grid(row=4, column=0, sticky="ew", pady=(10, 0))
        for col, header in enumerate(["频率", "平均入渗率f(mm/h)"]):
            ttk.Label(freq_box, text=header).grid(row=0, column=col, padx=6, pady=4)
        self.freq_entries: dict[str, dict[str, tk.Entry]] = {}
        for row, freq in enumerate(DESIGN_FREQUENCIES_7, 1):
            ttk.Label(freq_box, text=f"{freq}年").grid(row=row, column=0, padx=6, pady=3)
            self.freq_entries[freq] = {}
            entry = ttk.Entry(freq_box, width=13)
            entry.insert(0, "自动查表")
            entry.grid(row=row, column=1, padx=6, pady=3)
            self.freq_entries[freq]["f"] = entry
        ttk.Label(
            freq_box,
            text="设计洪水频率固定为100、50、30、25、20、10、5、2年一遇；tc由自动产流期判定得到，不固定为1h。",
            foreground="#666666",
        ).grid(row=1, column=2, rowspan=7, padx=(12, 8), pady=4, sticky="nw")

        actions = ttk.Frame(top)
        actions.grid(row=5, column=0, sticky="ew", pady=(12, 0))
        self.keep_negative = tk.BooleanVar(value=False)
        ttk.Checkbutton(actions, text="净雨允许负值显示（洪水计算仍按0处理）", variable=self.keep_negative).pack(side=tk.LEFT)
        self._action_button(actions, "生成计算表", self.calculate, bg="#2fa84f", fg="white").pack(side=tk.LEFT, padx=(16, 0))
        self._action_button(actions, "生成汇流曲线图", self.open_curve_charts, bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)
        self.update_infiltration_region_default()
        self.update_f_entry_state()
        self.update_distribution_preview()

    def _selected_zone_id(self) -> str:
        return _combo_id_from_label(self.rainfall_zone.get(), INSTANT_RAINFALL_ZONE_LABELS)

    def _selected_rational_region(self) -> str:
        return _combo_id_from_label(self.rational_region.get(), RATIONAL_FORMULA_REGION_LABELS)

    def _selected_infiltration_region(self) -> str:
        return _combo_id_from_label(self.infiltration_region.get(), INSTANT_INFILTRATION_TABLE_LABELS)

    def on_rainfall_zone_changed(self) -> None:
        self.update_infiltration_region_default()
        self.update_distribution_preview()

    def update_infiltration_region_default(self) -> None:
        table_id = default_instant_infiltration_table_id(self._selected_zone_id())
        _set_combo_current_by_id(self.infiltration_region, INSTANT_INFILTRATION_TABLE_LABELS, table_id)

    def update_f_entry_state(self) -> None:
        state = "disabled" if self.auto_f.get() else "normal"
        for entries in getattr(self, "freq_entries", {}).values():
            entry = entries["f"]
            entry.configure(state="normal")
            current = entry.get().strip()
            if self.auto_f.get():
                entry.delete(0, tk.END)
                entry.insert(0, "自动查表")
            elif current in {"", "自动", "自动查表"}:
                entry.delete(0, tk.END)
                entry.insert(0, "7.0")
            entry.configure(state=state)

    def update_distribution_preview(self) -> None:
        try:
            area = self._float(self.area, "流域面积F")
            distribution = select_instant_rainfall_distribution(self._selected_zone_id(), area)
            text = instant_rainfall_distribution_table_text(distribution)
        except Exception as exc:
            text = f"雨型预览暂不可用：{exc}"
        self._write(self.distribution_preview, text)

    def calculate(self) -> None:
        try:
            area = self._float(self.area, "流域面积F")
            length = self._float(self.length, "主河道长度L")
            slope = self._float(self.slope, "主河槽比降J")
            delta_t = self._float(self.delta_t, "时段Δt")
            cs_ratio = self._float(self.cs_ratio, "Cs/Cv")
            rainfall_distribution = select_instant_rainfall_distribution(self._selected_zone_id(), area)
            rainfall_design = InstantRainfallDesignInput(
                h1_mean_mm=self._float(self.storm_entries["1h"]["mean"], "1h均值"),
                h1_cv=self._float(self.storm_entries["1h"]["cv"], "1h Cv"),
                h6_mean_mm=self._float(self.storm_entries["6h"]["mean"], "6h均值"),
                h6_cv=self._float(self.storm_entries["6h"]["cv"], "6h Cv"),
                h24_mean_mm=self._float(self.storm_entries["24h"]["mean"], "24h均值"),
                h24_cv=self._float(self.storm_entries["24h"]["cv"], "24h Cv"),
                alpha1=self._float(self.storm_entries["1h"]["alpha"], "1h αt"),
                alpha3=self._float(self.storm_entries["3h"]["alpha"], "3h αt"),
                alpha6=self._float(self.storm_entries["6h"]["alpha"], "6h αt"),
                alpha24=self._float(self.storm_entries["24h"]["alpha"], "24h αt"),
                gamma1=self._float(self.storm_entries["1h"]["gamma"], "1h γ"),
                gamma3=self._float(self.storm_entries["3h"]["gamma"], "3h γ"),
                gamma6=self._float(self.storm_entries["6h"]["gamma"], "6h γ"),
                gamma24=self._float(self.storm_entries["24h"]["gamma"], "24h γ"),
                cs_ratio=cs_ratio,
            )
            runoff_settings = []
            for freq in DESIGN_FREQUENCIES_7:
                manual_f = None if self.auto_f.get() else self._float(self.freq_entries[freq]["f"], f"{freq} 入渗率f")
                runoff_settings.append(
                    FrequencyRunoffSetting(
                        frequency=freq,
                        infiltration_rate_mmh=manual_f,
                        unit_param_region="huangtu",
                        infiltration_table_id=self._selected_infiltration_region(),
                    )
                )
            result = calculate_rational_formula_atlas_batch(
                area_km2=area,
                river_length_km=length,
                river_slope_permille=slope,
                rainfall_distribution=rainfall_distribution,
                rainfall_design=rainfall_design,
                runoff_settings=runoff_settings,
                rational_region_id=self._selected_rational_region(),
                delta_t_h=delta_t,
                keep_negative_net_rain=self.keep_negative.get(),
            )
            for entry, value in (
                (self.shape_ka, result.shape_ka),
                (self.shape_kw, result.shape_kw),
                (self.shape_kt, result.shape_kt),
                (self.baseflow, result.baseflow_m3s),
            ):
                entry.configure(state="normal")
                entry.delete(0, tk.END)
                entry.insert(0, f"{value:.3f}")
                entry.configure(state="disabled")
            self.last_rational_result = result
            self._write(self.output, rational_formula_audit_result_to_text(result, DESIGN_FREQUENCIES_7))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def _export_result_xlsx(self, path: str) -> bool:
        result = getattr(self, "last_rational_result", None)
        if result is None:
            return False
        export_rainfall_runoff_audit_xlsx(
            result,
            path,
            title="推理公式法专项核验",
        )
        return True

    def open_curve_charts(self) -> None:
        result = getattr(self, "last_rational_result", None)
        if result is None:
            messagebox.showwarning("生成汇流曲线图", "请先生成推理公式法计算表。")
            return
        out_dir = Path(__file__).resolve().parent.parent / "输出" / "推理公式法汇流曲线"
        out_dir.mkdir(parents=True, exist_ok=True)
        path = out_dir / f"推理公式法汇流曲线_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        html = self._rational_curve_html(result)
        path.write_text(html, encoding="utf-8")
        os.startfile(path)

    def _rational_curve_html(self, result) -> str:
        sections = []
        for freq in DESIGN_FREQUENCIES_7:
            if freq not in result.frequency_results:
                continue
            net_values = [max(0.0, row.net_rain_mm[freq]) for row in result.shared.net_rain_table]
            sections.append(self._rational_confluence_svg(freq, result.frequency_results[freq]))
            sections.append(self._rational_curve_svg(freq, result.frequency_results[freq], net_values))
        body = "\n".join(sections)
        return (
            "<!doctype html><html><head><meta charset='utf-8'>"
            "<title>推理公式法汇流曲线</title>"
            "<style>body{font-family:'Times New Roman','KaiTi','楷体',serif;margin:24px;background:#f7f7f7;color:#111;}"
            ".chart{background:white;border:1px solid #bbb;margin:0 0 24px;padding:16px;}"
            "svg{width:100%;max-width:1180px;height:auto;display:block;margin:auto;}"
            "</style></head><body>"
            "<h1>推理公式法汇流曲线</h1>"
            "<p>每个频率包含两张图：一是图解法汇流曲线，显示 Qτ-t 与 Qτ-τ 两条曲线及交点 Qₘ、τ；二是净雨过程及地表洪水过程线，标注 A、Qa、Qₘ、Qb、B、Qₚ。</p>"
            f"{body}</body></html>"
        )

    def _current_report_figures(self) -> list[tuple[str, str]]:
        result = getattr(self, "last_rational_result", None)
        if result is None:
            return []
        figures: list[tuple[str, str]] = []
        for freq in DESIGN_FREQUENCIES_7:
            if freq not in result.frequency_results or freq not in result.shared.flood_results:
                continue
            rational = result.frequency_results[freq]
            rain_series = [(float(row.hour), row.rainfall_mm.get(freq, 0.0)) for row in result.shared.net_rain_table]
            infiltration_series = [(float(row.hour), row.infiltration_mm.get(freq, 0.0)) for row in result.shared.net_rain_table]
            net_series = [(float(row.hour), row.net_rain_mm.get(freq, 0.0)) for row in result.shared.net_rain_table]
            rational_hydro_series = [(point.time_h, point.discharge_m3s) for point in rational.hydrograph]
            figures.append((
                f"推理公式法 {freq}年逐时面雨量、入渗与净雨",
                self._line_chart_svg(
                    f"{freq}年逐时面雨量、入渗与净雨",
                    [
                        ("面雨量", rain_series, "#1f4e79"),
                        ("入渗量", infiltration_series, "#ed7d31"),
                        ("净雨", net_series, "#548235"),
                    ],
                    "雨量(mm)",
                ),
            ))
            figures.append((
                f"推理公式法 {freq}年洪水过程线",
                self._line_chart_svg(
                    f"{freq}年推理公式洪水过程线",
                    [("推理公式", rational_hydro_series, "#ed7d31")],
                    "流量Q(m³/s)",
                ),
            ))
            figures.append((f"推理公式法 {freq}年图解求Qm与τ", self._rational_confluence_svg(freq, rational)))
            net_values = [max(0.0, row.net_rain_mm[freq]) for row in result.shared.net_rain_table]
            figures.append((f"推理公式法 {freq}年概化洪水过程", self._rational_curve_svg(freq, rational, net_values)))
        return figures

    def _rational_confluence_svg(self, freq: str, flood) -> str:
        width, height = 1120, 520
        left, right, top, bottom = 78, 40, 58, 72
        plot_w = width - left - right
        plot_h = height - top - bottom
        dense_points = list(getattr(flood, "confluence_curve_points", ()) or ())
        if dense_points:
            rain_points = [(point[0], point[1]) for point in dense_points]
            travel_points = [
                (point[2], point[3] if len(point) >= 4 else point[1])
                for point in dense_points
            ]
        else:
            points = list(flood.confluence_curve)
            rain_points = [(point[0], point[3]) for point in points if len(point) >= 6]
            travel_points = [(point[4], point[3]) for point in points if len(point) >= 6]
            if not rain_points:
                rain_points = [(point[0], point[1]) for point in points]
                travel_points = [(point[0], point[2]) for point in points]
        adopted = (float(flood.tau_h), float(flood.surface_peak_m3s))

        tau_limit = float(flood.tau_h)

        def end_at_intersection(items: list[tuple[float, float]], *, horizontal_if_short: bool = False) -> list[tuple[float, float]]:
            filtered = [
                (float(time_h), float(flow))
                for time_h, flow in items
                if time_h >= 0 and flow >= 0 and time_h < tau_limit - 1e-9
            ]
            filtered.sort(key=lambda item: item[0])
            deduped: list[tuple[float, float]] = []
            for time_h, flow in filtered:
                rounded_time = round(time_h, 6)
                if deduped and round(deduped[-1][0], 6) == rounded_time:
                    deduped[-1] = (time_h, flow)
                else:
                    deduped.append((time_h, flow))
            if horizontal_if_short and len(deduped) < 1 and tau_limit > 0:
                deduped.append((max(0.0, tau_limit * 0.55), adopted[1]))
            deduped.append(adopted)
            return deduped

        rain_points = end_at_intersection(rain_points, horizontal_if_short=True)
        travel_points = end_at_intersection(travel_points)
        if len(travel_points) < 2 and tau_limit > 0 and adopted[1] > 0:
            # When the crossing falls inside the first calculation step, the
            # trial table has only the adopted point. Draw the theoretical
            # Q(τ) branch into the same point instead of leaving a tail past it.
            start_tau = max(tau_limit * 0.72, 1e-6)
            travel_points = [(start_tau, adopted[1] * (tau_limit / start_tau) ** 4), adopted]
        max_t_data = max([flood.tau_h, *(time_h for time_h, _q in rain_points), *(time_h for time_h, _q in travel_points)], default=1.0)
        max_t = max(max_t_data, tau_limit * 1.08, 1.0)
        max_q = max([flood.surface_peak_m3s, *(q for _time_h, q in rain_points), *(q for _time_h, q in travel_points)], default=1.0) * 1.08

        def sx(time_h: float) -> float:
            ratio = time_h / max(max_t, 1e-9)
            return left + max(0.0, min(1.0, ratio)) * plot_w

        def sy(flow: float) -> float:
            ratio = max(0.0, min(1.0, flow / max(max_q, 1e-9)))
            return top + plot_h - ratio * plot_h

        rain_path = self._smooth_svg_path([(sx(t), sy(q)) for t, q in rain_points])
        travel_path = self._smooth_svg_path([(sx(t), sy(q)) for t, q in travel_points])
        clip_id = f"confluence_clip_{abs(hash((freq, flood.tau_h, flood.surface_peak_m3s))) % 1000000}"
        grid = []
        for idx in range(6):
            q = max_q * idx / 5
            y = sy(q)
            grid.append(f"<line x1='{left}' y1='{y:.1f}' x2='{left + plot_w}' y2='{y:.1f}' stroke='#ddd'/>")
            grid.append(f"<text x='{left - 10}' y='{y + 4:.1f}' text-anchor='end' font-size='13'>{q:.0f}</text>")
        for idx in range(6):
            t = max_t * idx / 5
            x = sx(t)
            grid.append(f"<line x1='{x:.1f}' y1='{top}' x2='{x:.1f}' y2='{top + plot_h}' stroke='#eee'/>")
            grid.append(f"<text x='{x:.1f}' y='{top + plot_h + 24}' text-anchor='middle' font-size='13'>{t:.1f}</text>")

        x0, y0 = sx(flood.tau_h), sy(flood.surface_peak_m3s)
        title = html_lib.escape(f"{freq}年一遇 图解法汇流曲线")
        return (
            "<div class='chart'>"
            f"<svg viewBox='0 0 {width} {height}' xmlns='http://www.w3.org/2000/svg'>"
            f"<defs><clipPath id='{clip_id}'><rect x='{left}' y='{top}' width='{plot_w}' height='{plot_h}'/></clipPath></defs>"
            f"<text x='{width / 2}' y='28' text-anchor='middle' font-size='22' font-weight='bold'>{title}</text>"
            + "".join(grid)
            + f"<line x1='{left}' y1='{top}' x2='{left}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<line x1='{left}' y1='{top + plot_h}' x2='{left + plot_w}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<path d='{rain_path}' fill='none' stroke='#1c7c54' stroke-width='3' stroke-linejoin='round' stroke-linecap='round' clip-path='url(#{clip_id})'/>"
            + f"<path d='{travel_path}' fill='none' stroke='#b33f1d' stroke-width='3' stroke-linejoin='round' stroke-linecap='round' clip-path='url(#{clip_id})'/>"
            + f"<circle cx='{x0:.1f}' cy='{y0:.1f}' r='5' fill='#d62424'/>"
            + f"<line x1='{x0:.1f}' y1='{top + plot_h}' x2='{x0:.1f}' y2='{y0:.1f}' stroke='#d62424' stroke-dasharray='6 5'/>"
            + f"<line x1='{left}' y1='{y0:.1f}' x2='{x0:.1f}' y2='{y0:.1f}' stroke='#d62424' stroke-dasharray='6 5'/>"
            + f"<rect x='{width - 328}' y='{top + 8}' width='288' height='74' fill='white' stroke='#bbb' opacity='0.94'/>"
            + f"<text x='{width - 312}' y='{top + 31}' font-size='14' fill='#1c7c54'>绿色：按净雨强计算Q(t)</text>"
            + f"<text x='{width - 312}' y='{top + 53}' font-size='14' fill='#b33f1d'>红色：按汇流时间计算Q(τ)</text>"
            + f"<text x='{width - 312}' y='{top + 75}' font-size='14' fill='#a00000'>交点 τ={flood.tau_h:.3f}h, Qm={flood.surface_peak_m3s:.2f}</text>"
            + f"<text x='{left + plot_w / 2}' y='{height - 22}' text-anchor='middle' font-size='15'>τ 或 t(h)</text>"
            + f"<text x='20' y='{top + plot_h / 2}' transform='rotate(-90 20 {top + plot_h / 2})' text-anchor='middle' font-size='15'>Q(m³/s)</text>"
            "</svg></div>"
        )

    def _rational_curve_svg(self, freq: str, flood, net_values: list[float]) -> str:
        width, height = 1120, 560
        left, right, top, bottom = 78, 36, 58, 70
        plot_w = width - left - right
        rain_h = 130
        gap = 34
        hydro_top = top + rain_h + gap
        hydro_h = height - hydro_top - bottom
        points = flood.hydrograph
        max_t = max(point.time_h for point in points) or 1.0
        max_q = max(point.discharge_m3s for point in points) or 1.0
        max_q *= 1.12
        max_rain = max(net_values or [1.0]) or 1.0

        def sx(time_h: float) -> float:
            ratio = time_h / max(max_t, 1e-9)
            return left + max(0.0, min(1.0, ratio)) * plot_w

        def sy(flow: float) -> float:
            ratio = max(0.0, min(1.0, flow / max(max_q, 1e-9)))
            return hydro_top + hydro_h - ratio * hydro_h

        def ry(depth: float) -> float:
            ratio = max(0.0, min(1.0, depth / max(max_rain, 1e-9)))
            return top + ratio * rain_h

        hydro_path = self._smooth_svg_path([(sx(point.time_h), sy(point.discharge_m3s)) for point in points])
        component_svg = []
        component_colors = ["#8f3fb0", "#777777", "#999999", "#aaaaaa", "#bbbbbb"]
        hydro_clip_id = f"hydro_clip_{abs(hash((freq, flood.peak_flow_m3s, flood.tau_h))) % 1000000}"
        for idx, component in enumerate(getattr(flood, "hydrograph_components", ())):
            component_path = self._line_svg_path([(sx(time_h), sy(max(0.0, flow))) for time_h, flow in component])
            if not component_path:
                continue
            if idx == 0:
                component_svg.append(f"<path d='{component_path}' fill='none' stroke='{component_colors[0]}' stroke-width='2.4' stroke-dasharray='8 5' clip-path='url(#{hydro_clip_id})'/>")
            else:
                component_svg.append(f"<path d='{component_path}' fill='none' stroke='{component_colors[min(idx, len(component_colors)-1)]}' stroke-width='1.6' stroke-dasharray='5 5' opacity='0.85' clip-path='url(#{hydro_clip_id})'/>")
        axis_lines = [
            f"<line x1='{left}' y1='{top}' x2='{left}' y2='{top + rain_h}' stroke='#222'/>",
            f"<line x1='{left}' y1='{top}' x2='{left + plot_w}' y2='{top}' stroke='#222'/>",
            f"<line x1='{left}' y1='{hydro_top}' x2='{left}' y2='{hydro_top + hydro_h}' stroke='#222'/>",
            f"<line x1='{left}' y1='{hydro_top + hydro_h}' x2='{left + plot_w}' y2='{hydro_top + hydro_h}' stroke='#222'/>",
        ]
        grid = []
        for idx in range(6):
            q = max_q * idx / 5
            y = sy(q)
            grid.append(f"<line x1='{left}' y1='{y:.1f}' x2='{left + plot_w}' y2='{y:.1f}' stroke='#ddd'/>")
            grid.append(f"<text x='{left - 10}' y='{y + 4:.1f}' text-anchor='end' font-size='13'>{q:.0f}</text>")
        for idx in range(6):
            t = max_t * idx / 5
            x = sx(t)
            grid.append(f"<line x1='{x:.1f}' y1='{top}' x2='{x:.1f}' y2='{top + rain_h}' stroke='#eee'/>")
            grid.append(f"<line x1='{x:.1f}' y1='{hydro_top}' x2='{x:.1f}' y2='{hydro_top + hydro_h}' stroke='#eee'/>")
            grid.append(f"<text x='{x:.1f}' y='{hydro_top + hydro_h + 24}' text-anchor='middle' font-size='13'>{t:.1f}</text>")
        rain_bars = []
        bar_w = max(2.0, plot_w / max(len(net_values), 1) * 0.78)
        for idx, depth in enumerate(net_values, 1):
            if depth <= 0:
                continue
            x = sx(idx - 0.5) - bar_w / 2
            h = ry(depth) - top
            rain_bars.append(f"<rect x='{x:.1f}' y='{top:.1f}' width='{bar_w:.1f}' height='{h:.1f}' fill='#7fb6e6' opacity='0.75'/>")
        rain_labels = [
            f"<text x='{left - 10}' y='{top + 5}' text-anchor='end' font-size='13'>0</text>",
            f"<text x='{left - 10}' y='{top + rain_h:.1f}' text-anchor='end' font-size='13'>{max_rain:.1f}</text>",
            f"<text x='20' y='{top + rain_h / 2}' transform='rotate(-90 20 {top + rain_h / 2})' text-anchor='middle' font-size='15'>净雨(mm)</text>",
        ]

        marker_svg = []
        marker_rows = []
        for label, time_h, flow in flood.curve_markers:
            x = sx(time_h)
            y = sy(flow)
            escaped = html_lib.escape(label)
            marker_svg.append(f"<circle cx='{x:.1f}' cy='{y:.1f}' r='4.5' fill='#d62424'/>")
            marker_rows.append((escaped, time_h, flow))

        marker_box = []
        box_x = left + 16
        box_y = hydro_top + 8
        box_h = 24 + 18 * len(marker_rows)
        marker_box.append(f"<rect x='{box_x}' y='{box_y}' width='260' height='{box_h}' fill='white' stroke='#bbb' opacity='0.94'/>")
        marker_box.append(f"<text x='{box_x + 12}' y='{box_y + 19}' font-size='13' fill='#a00000'>特征点</text>")
        for row_idx, (label, time_h, flow) in enumerate(marker_rows, 1):
            y = box_y + 19 + row_idx * 18
            marker_box.append(f"<text x='{box_x + 12}' y='{y}' font-size='12' fill='#a00000'>{label}: t={time_h:g}h, Q={flow:.2f}</text>")

        title = html_lib.escape(f"{freq}年一遇 推理公式汇流曲线")
        return (
            "<div class='chart'>"
            f"<svg viewBox='0 0 {width} {height}' xmlns='http://www.w3.org/2000/svg'>"
            f"<defs><clipPath id='{hydro_clip_id}'><rect x='{left}' y='{hydro_top}' width='{plot_w}' height='{hydro_h}'/></clipPath></defs>"
            f"<text x='{width / 2}' y='28' text-anchor='middle' font-size='22' font-weight='bold'>{title}：净雨过程及地表洪水过程线</text>"
            + "".join(grid)
            + "".join(rain_bars)
            + "".join(rain_labels)
            + "".join(axis_lines)
            + "".join(component_svg)
            + f"<path d='{hydro_path}' fill='none' stroke='#1468b3' stroke-width='3' stroke-linejoin='round' stroke-linecap='round' clip-path='url(#{hydro_clip_id})'/>"
            + f"<rect x='{width - 330}' y='{hydro_top + 8}' width='294' height='76' fill='white' stroke='#bbb' opacity='0.94'/>"
            + f"<text x='{width - 314}' y='{hydro_top + 31}' font-size='13' fill='#1468b3'>蓝色：叠加地表洪水过程线</text>"
            + f"<text x='{width - 314}' y='{hydro_top + 53}' font-size='13' fill='#8f3fb0'>紫色虚线：主峰五点概化过程</text>"
            + f"<text x='{width - 314}' y='{hydro_top + 75}' font-size='13' fill='#666'>灰色虚线：其余净雨三角形过程</text>"
            + "".join(marker_svg)
            + "".join(marker_box)
            + f"<text x='{left + plot_w / 2}' y='{height - 22}' text-anchor='middle' font-size='15'>t(h)</text>"
            + f"<text x='20' y='{hydro_top + hydro_h / 2}' transform='rotate(-90 20 {hydro_top + hydro_h / 2})' text-anchor='middle' font-size='15'>Q(m³/s)</text>"
            + f"<text x='{left}' y='{height - 4}' font-size='12' fill='#555'>Qₚ={flood.peak_flow_m3s:.2f} m³/s, 峰现时间={flood.peak_time_h:g}h, τ={flood.tau_h:.3f}h</text>"
            "</svg></div>"
        )

class EnergyEquationWaterLevelTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="能量方程参数")
        basic.grid(row=0, column=0, sticky="ew")
        self.flow = self._entry(basic, 0, 0, "流量Q(m³/s)", "120", width=12)
        self.length = self._entry(basic, 0, 1, "河段长度L(m)", "120", width=12)
        self.known_level = self._entry(basic, 0, 2, "控制断面已知水位(m)", "103.0", width=12)
        ttk.Label(basic, text="流态与计算方向").grid(row=0, column=6, padx=(10, 4), pady=6, sticky="e")
        self.direction = ttk.Combobox(
            basic,
            width=28,
            state="readonly",
            values=["亚临界流：下游已知求上游", "超临界流：上游已知求下游"],
        )
        self.direction.current(0)
        self.direction.grid(row=0, column=7, padx=(0, 12), pady=6, sticky="w")
        self.contraction = self._entry(basic, 1, 0, "收缩系数(无量纲)", "0.10", width=12)
        self.expansion = self._entry(basic, 1, 1, "扩张系数(无量纲)", "0.30", width=12)
        self.side_slope = self._entry(basic, 1, 2, "边坡系数m(无量纲)", "1.5", width=12)

        section_box = ttk.LabelFrame(top, text="断面数据")
        section_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        section_box.columnconfigure(4, weight=1)
        headers = ["断面", "名称", "糙率n", "α(无量纲)", "断面点x,z(m,m)"]
        for col, header in enumerate(headers):
            ttk.Label(section_box, text=header).grid(row=0, column=col, padx=4, pady=4)
        defaults = [
            ("上游", "上游断面", "0.035", "1.0", "0,103.5; 8,101.5; 16,100.7; 25,100.9; 34,102.5; 42,104"),
            ("下游", "下游断面", "0.035", "1.0", "0,103; 8,101; 16,100.2; 25,100.4; 34,102; 42,103.5"),
        ]
        self.section_entries: dict[str, dict[str, tk.Entry]] = {}
        for row, (key, name, roughness, alpha, points) in enumerate(defaults, 1):
            ttk.Label(section_box, text=key).grid(row=row, column=0, padx=4, pady=4)
            entries: dict[str, tk.Entry] = {}
            for col, (field, value, width) in enumerate([
                ("name", name, 14),
                ("roughness", roughness, 10),
                ("alpha", alpha, 8),
                ("points", points, 100),
            ], 1):
                entry = ttk.Entry(section_box, width=width)
                entry.insert(0, value)
                entry.grid(row=row, column=col, padx=4, pady=4, sticky="ew")
                entries[field] = entry
            self.section_entries[key] = entries

        input_box = ttk.LabelFrame(top, text="断面点批量录入")
        input_box.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        input_box.columnconfigure(1, weight=1)
        ttk.Label(input_box, text="目标断面").grid(row=0, column=0, padx=(8, 4), pady=6, sticky="e")
        self.energy_target_section = ttk.Combobox(input_box, width=12, state="readonly", values=["上游", "下游"])
        self.energy_target_section.current(0)
        self.energy_target_section.grid(row=0, column=1, padx=(0, 8), pady=6, sticky="w")
        self._action_button(input_box, "导入Excel/CSV", self.import_energy_points_file, bg="#1688e8", fg="white").grid(
            row=0, column=2, padx=6, pady=6
        )
        self._action_button(input_box, "应用粘贴数据", self.apply_energy_pasted_points, bg="#006b63", fg="white").grid(
            row=0, column=3, padx=6, pady=6
        )
        self.energy_paste_points = ScrolledText(input_box, height=4, wrap=tk.NONE)
        self.energy_paste_points.grid(row=1, column=0, columnspan=4, padx=8, pady=(0, 6), sticky="ew")
        self.energy_paste_points.insert(tk.END, "0\t103\n8\t101\n16\t100.2\n25\t100.4\n34\t102\n42\t103.5")

        ttk.Label(
            top,
            text="能量方程：Z1+α1V1²/2g = Z2+α2V2²/2g + hf + hj；断面点支持导入Excel/CSV前两列，或直接粘贴里程、高程两列。",
            foreground="#666666",
        ).grid(row=3, column=0, sticky="w", padx=8, pady=(8, 0))

        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))
        self.output = self._result_box(self)

    def _section(self, key: str) -> HydraulicSectionInput:
        entries = self.section_entries[key]
        name = entries["name"].get().strip() or f"{key}断面"
        return HydraulicSectionInput(
            name=name,
            points=parse_hydraulic_points(entries["points"].get(), name),
            roughness_n=float(entries["roughness"].get().strip()),
            alpha=float(entries["alpha"].get().strip()),
        )

    def _set_energy_target_points(self, points: tuple[tuple[float, float], ...]) -> None:
        key = self.energy_target_section.get().strip() or "上游"
        entries = self.section_entries[key]
        entries["points"].delete(0, tk.END)
        entries["points"].insert(0, self._points_to_text(points))

    def apply_energy_pasted_points(self) -> None:
        try:
            points = self._parse_section_points(self.energy_paste_points.get("1.0", tk.END), "粘贴数据")
            self._set_energy_target_points(points)
            messagebox.showinfo("断面点录入", f"已写入 {len(points)} 个断面点。")
        except Exception as exc:
            messagebox.showerror("断面点录入错误", str(exc))

    def import_energy_points_file(self) -> None:
        try:
            points = self._import_points_from_file()
            if points is None:
                return
            self._set_energy_target_points(points)
            messagebox.showinfo("导入断面点", f"已导入 {len(points)} 个断面点。")
        except Exception as exc:
            messagebox.showerror("导入断面点错误", str(exc))

    def calculate(self) -> None:
        try:
            upstream = self._section("上游")
            downstream = self._section("下游")
            flow = float(self.flow.get().strip())
            known_level = float(self.known_level.get().strip())
            selected_direction = self.direction.get()
            known_is_upstream = selected_direction.startswith("超临界")
            regime_code = "supercritical" if known_is_upstream else "subcritical"
            control_section = upstream if known_is_upstream else downstream
            control_state = hydraulic_state_for_level(control_section, flow, known_level)
            regime_note = (
                f"控制断面Fr={control_state.froude_number:.3f}；"
                f"按{('超临界流、上游控制、自上而下' if known_is_upstream else '亚临界流、下游控制、自下而上')}计算。"
            )
            known_section = upstream if known_is_upstream else downstream
            target_section = downstream if known_is_upstream else upstream
            result = calculate_energy_equation_water_level(
                EnergyEquationInput(
                    flow_m3s=flow,
                    known_section=known_section,
                    target_section=target_section,
                    known_water_level_m=known_level,
                    reach_length_m=float(self.length.get().strip()),
                    known_is_upstream=known_is_upstream,
                    contraction_coeff=float(self.contraction.get().strip()),
                    expansion_coeff=float(self.expansion.get().strip()),
                    flow_regime=regime_code,
                )
            )
            direction_text = "上游已知，求下游" if result.known_is_upstream else "下游已知，求上游"
            up_thalweg = min(z for _x, z in upstream.points)
            down_thalweg = min(z for _x, z in downstream.points)
            reach_length = float(self.length.get().strip())
            bed_slope = (up_thalweg - down_thalweg) / reach_length if reach_length > 0 else 0.0
            side_slope = float(self.side_slope.get().strip())
            if result.known_is_upstream:
                upstream_state = result.known_state
                downstream_state = result.target_state
            else:
                upstream_state = result.target_state
                downstream_state = result.known_state

            def regime_row(label: str, section: HydraulicSectionInput, state) -> list[str]:
                try:
                    _critical_level, critical_depth, _critical_area, _critical_width = critical_water_level_for_section(
                        section.points, flow
                    )
                    critical_text = f"{critical_depth:.3f}"
                except Exception:
                    critical_text = "无法计算"
                if bed_slope > 0:
                    try:
                        _normal_level, normal_depth, _normal_fr = normal_water_level_for_section(section, flow, bed_slope)
                        normal_text = f"{normal_depth:.3f}"
                    except Exception:
                        normal_text = "无法计算"
                else:
                    normal_text = "无法计算"
                return [
                    f"{label}({section.name})",
                    f"{flow:.3f}",
                    f"{state.top_width_m:.3f}",
                    f"{section.roughness_n:.3f}",
                    f"{side_slope:.3f}",
                    critical_text,
                    normal_text,
                    f"{state.froude_number:.3f}",
                ]

            regime_rows = [
                ["控制断面", "流量Q(m³/s)", "水面宽度b(m)", "糙率n", "边坡系数m", "临界水深h꜀(m)", "正常水深h₀(m)", "弗汝德数Fr"],
                regime_row("上游断面", upstream, upstream_state),
                regime_row("下游断面", downstream, downstream_state),
            ]
            rows = [[
                "断面", "水位(m)", "水深(m)", "能量线(m)", "面积(m²)", "湿周(m)", "R(m)", "水面宽(m)", "流速(m/s)",
                "速度水头(m)", "流量模数K", "Fr", "流态", "摩阻坡降", "深泓点高程(m)",
            ]]
            for state in (result.known_state, result.target_state):
                state_regime = "亚临界" if state.froude_number < 0.95 else ("超临界" if state.froude_number > 1.05 else "近临界")
                rows.append([
                    state.name,
                    f"{state.water_level_m:.3f}",
                    f"{state.max_depth_m:.3f}",
                    f"{state.energy_grade_m:.3f}",
                    f"{state.area_m2:.3f}",
                    f"{state.wetted_perimeter_m:.3f}",
                    f"{state.hydraulic_radius_m:.3f}",
                    f"{state.top_width_m:.3f}",
                    f"{state.velocity_ms:.3f}",
                    f"{state.velocity_head_m:.4f}",
                    f"{state.flow_modulus_k:.3f}",
                    f"{state.froude_number:.3f}",
                    state_regime,
                    f"{state.friction_slope:.7f}",
                    f"{state.thalweg_elevation_m:.3f}",
                ])
            loss_rows = [
                ["项目", "数值"],
                ["计算方向", direction_text],
                ["流态判别", regime_note],
                ["流量Q(m³/s)", f"{result.flow_m3s:.3f}"],
                ["河段长度L(m)", f"{result.reach_length_m:.3f}"],
                ["沿程损失hf(m)", f"{result.friction_loss_m:.4f}"],
                ["局部损失hj(m)", f"{result.local_loss_m:.4f}"],
                ["总损失h(m)", f"{result.total_loss_m:.4f}"],
                ["能量方程残差(m)", f"{result.energy_residual_m:.5f}"],
            ]
            lines = [
                "能量方程计算水位",
                "",
                "计算公式：Z1 + α1V1²/2g = Z2 + α2V2²/2g + hf + hj",
                "",
                "一、流态判别及正常水深计算成果表",
                format_fixed_table(regime_rows, min_width=14),
                "",
                "二、计算参数及损失",
                format_fixed_table(loss_rows, min_width=14),
                "",
                "三、水位成果及断面水力要素表",
                format_fixed_table(rows, min_width=12),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class HecWaterLevelTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.last_hec_data: dict[str, object] | None = None
        self.last_hec_chart_path: Path | None = None

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="多断面能量方程法（HEC标准步法）参数")
        basic.grid(row=0, column=0, sticky="ew")
        self.flow = self._entry(basic, 0, 0, "流量Q(m³/s)", "120", width=12)
        self.downstream_level = self._entry(basic, 0, 1, "下游控制水位(m)", "103.0", width=12)
        self.upstream_level = self._entry(basic, 0, 2, "上游控制水位(m)", "103.5", width=12)
        ttk.Label(basic, text="计算流态").grid(row=0, column=6, padx=(10, 4), pady=6, sticky="e")
        self.flow_regime = ttk.Combobox(
            basic,
            width=24,
            state="readonly",
            values=["亚临界流（下游控制）", "超临界流（上游控制）"],
        )
        self.flow_regime.current(0)
        self.flow_regime.grid(row=0, column=7, padx=(0, 12), pady=6, sticky="w")
        self.flow_regime.bind("<<ComboboxSelected>>", self._sync_hec_boundary_fields)
        self.contraction = self._entry(basic, 1, 0, "收缩系数(无量纲)", "0.10", width=12)
        self.expansion = self._entry(basic, 1, 1, "扩张系数(无量纲)", "0.30", width=12)
        self._sync_hec_boundary_fields()

        section_box = ttk.LabelFrame(top, text="断面数据（按下游到上游顺序填写）")
        section_box.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        section_box.columnconfigure(5, weight=1)
        self.profile_section_box = section_box
        headers = ["序号", "断面名称", "到上游下一断面距离L(m)", "糙率n", "α(无量纲)", "断面点x,z(m,m)"]
        for col, header in enumerate(headers):
            ttk.Label(section_box, text=header).grid(row=0, column=col, padx=4, pady=4)
        defaults = [
            ("下游断面", "120", "0.035", "1.0", "0,103; 8,101; 16,100.2; 25,100.4; 34,102; 42,103.5"),
            ("中间断面", "100", "0.035", "1.0", "0,103.2; 8,101.2; 16,100.4; 25,100.6; 34,102.2; 42,103.7"),
            ("上游断面", "", "0.035", "1.0", "0,103.5; 8,101.5; 16,100.7; 25,100.9; 34,102.5; 42,104"),
            ("", "", "0.035", "1.0", ""),
            ("", "", "0.035", "1.0", ""),
        ]
        self.section_entries: list[dict[str, tk.Entry]] = []
        for name, length, roughness, alpha, points in defaults:
            self._add_profile_row(name, length, roughness, alpha, points)

        input_box = ttk.LabelFrame(top, text="断面点批量录入")
        input_box.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        input_box.columnconfigure(1, weight=1)
        ttk.Label(input_box, text="目标断面").grid(row=0, column=0, padx=(8, 4), pady=6, sticky="e")
        self.hec_target_section = ttk.Combobox(input_box, width=12, state="readonly", values=[])
        self.hec_target_section.grid(row=0, column=1, padx=(0, 8), pady=6, sticky="w")
        self._refresh_hec_target_sections()
        self._action_button(input_box, "导入Excel/CSV", self.import_hec_points_file, bg="#1688e8", fg="white").grid(
            row=0, column=2, padx=6, pady=6
        )
        self._action_button(input_box, "应用粘贴数据", self.apply_hec_pasted_points, bg="#006b63", fg="white").grid(
            row=0, column=3, padx=6, pady=6
        )
        self.hec_paste_points = ScrolledText(input_box, height=4, wrap=tk.NONE)
        self.hec_paste_points.grid(row=1, column=0, columnspan=4, padx=8, pady=(0, 6), sticky="ew")
        self.hec_paste_points.insert(tk.END, "0\t103\n8\t101\n16\t100.2\n25\t100.4\n34\t102\n42\t103.5")

        ttk.Label(
            top,
            text="步骤：1按下游到上游录入断面；2选择流态；3填写对应控制水位；4计算；5检查每个断面的Fr和临界水深。混合流、水跃及建筑物请用专门模型复核。",
            foreground="#666666",
        ).grid(row=3, column=0, sticky="w", padx=8, pady=(8, 0))

        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "打开断面/纵断面图", self.open_hec_charts, bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "新增断面行", lambda: self._add_profile_row("", "", "0.035", "1.0", "")).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "新增5行", self._add_five_profile_rows).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))
        self.output = self._result_box(self)

    def _add_profile_row(
        self,
        name: str = "",
        length: str = "",
        roughness: str = "0.035",
        alpha: str = "1.0",
        points: str = "",
    ) -> None:
        row = len(self.section_entries) + 1
        ttk.Label(self.profile_section_box, text=str(row)).grid(row=row, column=0, padx=4, pady=4)
        entries: dict[str, tk.Entry] = {}
        for col, (key, value, width) in enumerate([
            ("name", name, 14),
            ("length", length, 12),
            ("roughness", roughness, 9),
            ("alpha", alpha, 8),
            ("points", points, 100),
        ], 1):
            entry = ttk.Entry(self.profile_section_box, width=width)
            entry.insert(0, value)
            entry.grid(row=row, column=col, padx=4, pady=4, sticky="ew")
            entries[key] = entry
        self.section_entries.append(entries)
        if hasattr(self, "hec_target_section"):
            self._refresh_hec_target_sections()

    def _add_five_profile_rows(self) -> None:
        for _ in range(5):
            self._add_profile_row("", "", "0.035", "1.0", "")

    def _refresh_hec_target_sections(self) -> None:
        values = [f"断面{i}" for i in range(1, len(self.section_entries) + 1)]
        current = self.hec_target_section.get().strip()
        self.hec_target_section.configure(values=values)
        if current in values:
            self.hec_target_section.set(current)
        elif values:
            self.hec_target_section.current(0)

    def _hec_target_index(self) -> int:
        match = re.search(r"(\d+)", self.hec_target_section.get().strip())
        index = int(match.group(1)) - 1 if match else 0
        return max(0, min(index, len(self.section_entries) - 1))

    def _set_hec_target_points(self, points: tuple[tuple[float, float], ...]) -> None:
        entries = self.section_entries[self._hec_target_index()]
        entries["points"].delete(0, tk.END)
        entries["points"].insert(0, self._points_to_text(points))

    def apply_hec_pasted_points(self) -> None:
        try:
            points = self._parse_section_points(self.hec_paste_points.get("1.0", tk.END), "粘贴数据")
            self._set_hec_target_points(points)
            messagebox.showinfo("断面点录入", f"已写入 {len(points)} 个断面点。")
        except Exception as exc:
            messagebox.showerror("断面点录入错误", str(exc))

    def import_hec_points_file(self) -> None:
        try:
            points = self._import_points_from_file()
            if points is None:
                return
            self._set_hec_target_points(points)
            messagebox.showinfo("导入断面点", f"已导入 {len(points)} 个断面点。")
        except Exception as exc:
            messagebox.showerror("导入断面点错误", str(exc))

    def _hec_regime_code(self) -> str:
        text = self.flow_regime.get()
        if text.startswith("超临界"):
            return "supercritical"
        return "subcritical"

    def _sync_hec_boundary_fields(self, _event=None) -> None:
        supercritical = self.flow_regime.get().startswith("超临界")
        self.downstream_level.configure(state=tk.DISABLED if supercritical else tk.NORMAL)
        self.upstream_level.configure(state=tk.NORMAL if supercritical else tk.DISABLED)

    def _read_profile(self) -> tuple[list[HydraulicSectionInput], list[float]]:
        sections: list[HydraulicSectionInput] = []
        lengths: list[float] = []
        for idx, entries in enumerate(self.section_entries, 1):
            name_text = entries["name"].get().strip()
            points_text = entries["points"].get().strip()
            if not name_text and not points_text:
                continue
            name = name_text or f"断面{idx}"
            sections.append(
                HydraulicSectionInput(
                    name=name,
                    points=parse_hydraulic_points(points_text, name),
                    roughness_n=float(entries["roughness"].get().strip()),
                    alpha=float(entries["alpha"].get().strip()),
                )
            )
            length_text = entries["length"].get().strip()
            if length_text:
                lengths.append(float(length_text))
        if len(sections) < 2:
            raise HydrologyError("HEC水位计算至少需要2个断面")
        if len(lengths) < len(sections) - 1:
            raise HydrologyError("除最后一个上游断面外，每个断面都需要填写到上游下一断面的距离L")
        return sections, lengths[: len(sections) - 1]

    def calculate(self) -> None:
        try:
            sections, lengths = self._read_profile()
            result = calculate_hec_profile(
                HecReachInput(
                    flow_m3s=float(self.flow.get().strip()),
                    sections_downstream_to_upstream=tuple(sections),
                    reach_lengths_m=tuple(lengths),
                    downstream_water_level_m=float(self.downstream_level.get().strip()),
                    upstream_water_level_m=float(self.upstream_level.get().strip()),
                    flow_regime=self._hec_regime_code(),
                    contraction_coeff=float(self.contraction.get().strip()),
                    expansion_coeff=float(self.expansion.get().strip()),
                )
            )
            self.last_hec_data = {"sections": sections, "lengths": lengths, "result": result}
            self.last_hec_chart_path = self._write_hec_charts(open_after_write=False)
            state_rows = [[
                "序号", "断面", "水位(m)", "临界水位(m)", "与临界水位关系", "水深(m)", "能量线(m)", "面积(m²)", "湿周(m)", "R(m)", "水面宽(m)",
                "流速(m/s)", "速度水头(m)", "流量模数K", "Fr", "流态", "摩阻坡降", "深泓点高程(m)",
            ]]
            for idx, (section, state) in enumerate(zip(sections, result.states_downstream_to_upstream), 1):
                state_regime = "亚临界" if state.froude_number < 0.95 else ("超临界" if state.froude_number > 1.05 else "近临界")
                critical_level = critical_water_level_for_section(section.points, result.flow_m3s)[0]
                critical_relation = "临界水位以上" if state.water_level_m > critical_level else ("临界水位以下" if state.water_level_m < critical_level else "临界水位")
                state_rows.append([
                    idx,
                    state.name,
                    f"{state.water_level_m:.3f}",
                    f"{critical_level:.3f}",
                    critical_relation,
                    f"{state.max_depth_m:.3f}",
                    f"{state.energy_grade_m:.3f}",
                    f"{state.area_m2:.3f}",
                    f"{state.wetted_perimeter_m:.3f}",
                    f"{state.hydraulic_radius_m:.3f}",
                    f"{state.top_width_m:.3f}",
                    f"{state.velocity_ms:.3f}",
                    f"{state.velocity_head_m:.4f}",
                    f"{state.flow_modulus_k:.3f}",
                    f"{state.froude_number:.3f}",
                    state_regime,
                    f"{state.friction_slope:.7f}",
                    f"{state.thalweg_elevation_m:.3f}",
                ])
            step_rows = [["步号", "下游断面", "上游断面", "L(m)", "下游水位(m)", "上游水位(m)", "hf(m)", "hj(m)", "h合计(m)"]]
            for step in result.steps:
                step_rows.append([
                    step.index,
                    step.downstream_section,
                    step.upstream_section,
                    f"{step.reach_length_m:.3f}",
                    f"{step.downstream_water_level_m:.3f}",
                    f"{step.upstream_water_level_m:.3f}",
                    f"{step.friction_loss_m:.4f}",
                    f"{step.local_loss_m:.4f}",
                    f"{step.total_loss_m:.4f}",
                ])
            setting_rows = [
                ["项目", "数值"],
                ["计算方法", "多断面能量方程法（HEC标准步法）"],
                ["摩阻坡降", "HEC默认平均流量模数法：Sf=((Q1+Q2)/(K1+K2))²"],
                ["计算流态", result.flow_regime],
                ["计算方向", result.calculation_direction],
                ["边界条件说明", result.boundary_note],
                ["下游控制水位(m)", f"{float(self.downstream_level.get().strip()):.3f}"],
                ["上游控制水位(m)", f"{float(self.upstream_level.get().strip()):.3f}"],
                ["收缩系数(无量纲)", f"{float(self.contraction.get().strip()):.3f}"],
                ["扩张系数(无量纲)", f"{float(self.expansion.get().strip()):.3f}"],
            ]
            lines = [
                "多断面能量方程法（HEC标准步法）计算水位",
                "",
                "说明：按所选流态在临界水深正确一侧逐断面求解；本工具是内置HEC标准步法计算器。",
                "",
                "一、计算设置与边界条件",
                format_fixed_table(setting_rows, min_width=14),
                "",
                "二、水面线成果表",
                format_fixed_table(state_rows, min_width=12),
                "",
                "三、逐河段能量损失表",
                format_fixed_table(step_rows, min_width=12),
                "",
                "四、断面图及纵断面图",
                f"已生成图件文件：{self.last_hec_chart_path}",
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))

    def open_hec_charts(self) -> None:
        if self.last_hec_data is None:
            messagebox.showwarning("多断面能量方程图件", "请先完成多断面能量方程法计算。")
            return
        path = self._write_hec_charts(open_after_write=False)
        self.last_hec_chart_path = path
        os.startfile(path)

    def _write_hec_charts(self, open_after_write: bool = False) -> Path:
        if self.last_hec_data is None:
            raise HydrologyError("没有可生成图件的标准步法计算结果")
        out_dir = Path(__file__).resolve().parent.parent / "输出成果"
        out_dir.mkdir(parents=True, exist_ok=True)
        path = out_dir / f"多断面能量方程法_标准步法断面与纵断面_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        path.write_text(self._hec_charts_html(self.last_hec_data), encoding="utf-8")
        if open_after_write:
            os.startfile(path)
        return path

    def _hec_charts_html(self, data: dict[str, object]) -> str:
        sections = data["sections"]
        lengths = data["lengths"]
        result = data["result"]
        states = result.states_downstream_to_upstream
        parts = [self._hec_profile_svg(states, lengths)]
        for section, state in zip(sections, states):
            parts.append(self._hec_section_svg(section, state))
        return (
            "<!doctype html><html><head><meta charset='utf-8'>"
            "<title>多断面能量方程法（HEC标准步法）断面与纵断面图</title>"
            "<style>body{font-family:'Times New Roman','KaiTi','楷体',serif;margin:24px;background:#f7f7f7;color:#111;}"
            ".chart{background:white;border:1px solid #cfcfcf;margin:0 0 24px;padding:18px;}"
            "svg{width:100%;max-width:1120px;height:auto;display:block;margin:auto;}"
            "h1{font-size:22px;margin:0 0 18px;text-align:center;}</style></head><body>"
            "<h1>多断面能量方程法（HEC标准步法）断面图及纵断面图</h1>"
            + "\n".join(parts)
            + "</body></html>"
        )

    def _current_report_figures(self) -> list[tuple[str, str]]:
        if self.last_hec_data is None:
            return []
        return [("多断面HEC标准步法断面与纵断面图", self._hec_charts_html(self.last_hec_data))]

    def _hec_wet_span(self, points: tuple[tuple[float, float], ...], level: float) -> tuple[float, float] | None:
        wet_x: list[float] = []
        for (x1, z1), (x2, z2) in zip(points, points[1:]):
            if z1 <= level:
                wet_x.append(x1)
            if z2 <= level:
                wet_x.append(x2)
            if (z1 - level) * (z2 - level) < 0:
                ratio = (level - z1) / (z2 - z1)
                wet_x.append(x1 + ratio * (x2 - x1))
        if not wet_x:
            return None
        return min(wet_x), max(wet_x)

    def _hec_section_svg(self, section: HydraulicSectionInput, state) -> str:
        points = tuple(sorted(section.points))
        width, height = 1040, 520
        left, right, top, bottom = 78, 42, 58, 72
        plot_w = width - left - right
        plot_h = height - top - bottom
        xs = [x for x, _z in points]
        zs = [z for _x, z in points] + [state.water_level_m]
        min_x, max_x = min(xs), max(xs)
        min_z, max_z = min(zs), max(zs)
        if abs(max_x - min_x) < 1e-9:
            max_x = min_x + 1.0
        z_pad = max(0.5, (max_z - min_z) * 0.12)
        min_z -= z_pad
        max_z += z_pad

        def sx(x: float) -> float:
            return left + (x - min_x) / (max_x - min_x) * plot_w

        def sy(z: float) -> float:
            return top + (max_z - z) / (max_z - min_z) * plot_h

        bed_polyline = " ".join(f"{sx(x):.1f},{sy(z):.1f}" for x, z in points)
        grid = []
        for idx in range(6):
            x = min_x + (max_x - min_x) * idx / 5
            px = sx(x)
            grid.append(f"<line x1='{px:.1f}' y1='{top}' x2='{px:.1f}' y2='{top + plot_h}' stroke='#eee'/>")
            grid.append(f"<text x='{px:.1f}' y='{top + plot_h + 24}' text-anchor='middle' font-size='12'>{x:.0f}</text>")
        for idx in range(6):
            z = min_z + (max_z - min_z) * idx / 5
            py = sy(z)
            grid.append(f"<line x1='{left}' y1='{py:.1f}' x2='{left + plot_w}' y2='{py:.1f}' stroke='#eee'/>")
            grid.append(f"<text x='{left - 10}' y='{py + 4:.1f}' text-anchor='end' font-size='12'>{z:.2f}</text>")
        span = self._hec_wet_span(points, state.water_level_m)
        water_svg = ""
        if span is not None:
            x1, x2 = span
            water_y = sy(state.water_level_m)
            water_svg = (
                f"<line x1='{sx(x1):.1f}' y1='{water_y:.1f}' x2='{sx(x2):.1f}' y2='{water_y:.1f}' stroke='#1468b3' stroke-width='2.6'/>"
                f"<text x='{sx(x2) + 10:.1f}' y='{water_y - 8:.1f}' font-size='13' fill='#1468b3'>Z={state.water_level_m:.3f}m</text>"
            )
        thalweg_x = sx(state.thalweg_x_m)
        thalweg_y = sy(state.thalweg_elevation_m)
        return (
            "<div class='chart'>"
            f"<svg viewBox='0 0 {width} {height}' xmlns='http://www.w3.org/2000/svg'>"
            f"<text x='{width / 2}' y='28' text-anchor='middle' font-size='20' font-weight='bold'>{html_lib.escape(section.name)} 横断面图</text>"
            + "".join(grid)
            + f"<line x1='{left}' y1='{top}' x2='{left}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<line x1='{left}' y1='{top + plot_h}' x2='{left + plot_w}' y2='{top + plot_h}' stroke='#222'/>"
            + water_svg
            + f"<polyline points='{bed_polyline}' fill='none' stroke='#333' stroke-width='2.4'/>"
            + f"<circle cx='{thalweg_x:.1f}' cy='{thalweg_y:.1f}' r='4.5' fill='#d62424'/>"
            + f"<text x='{thalweg_x + 10:.1f}' y='{thalweg_y + 18:.1f}' font-size='13' fill='#a00000'>深泓点高程 {state.thalweg_elevation_m:.2f}m</text>"
            + f"<text x='{left + plot_w / 2}' y='{height - 18}' text-anchor='middle' font-size='15'>起点距 x(m)</text>"
            + f"<text x='22' y='{top + plot_h / 2}' transform='rotate(-90 22 {top + plot_h / 2})' text-anchor='middle' font-size='15'>高程 Z(m)</text>"
            + "</svg></div>"
        )

    def _hec_profile_svg(self, states, lengths: list[float]) -> str:
        width, height = 1100, 520
        left, right, top, bottom = 82, 42, 58, 76
        plot_w = width - left - right
        plot_h = height - top - bottom
        distances = [0.0]
        for length in lengths:
            distances.append(distances[-1] + length)
        distances = distances[: len(states)]
        water = [state.water_level_m for state in states]
        bed = [state.thalweg_elevation_m for state in states]
        max_x = max(distances) if distances else 1.0
        if max_x <= 0:
            max_x = 1.0
        min_z = min(bed + water)
        max_z = max(bed + water)
        z_pad = max(0.5, (max_z - min_z) * 0.15)
        min_z -= z_pad
        max_z += z_pad

        def sx(distance: float) -> float:
            return left + distance / max_x * plot_w

        def sy(z: float) -> float:
            return top + (max_z - z) / (max_z - min_z) * plot_h

        bed_polyline = " ".join(f"{sx(x):.1f},{sy(z):.1f}" for x, z in zip(distances, bed))
        water_polyline = " ".join(f"{sx(x):.1f},{sy(z):.1f}" for x, z in zip(distances, water))
        grid = []
        for idx in range(6):
            x_value = max_x * idx / 5
            x = sx(x_value)
            grid.append(f"<line x1='{x:.1f}' y1='{top}' x2='{x:.1f}' y2='{top + plot_h}' stroke='#eee'/>")
            grid.append(f"<text x='{x:.1f}' y='{top + plot_h + 24}' text-anchor='middle' font-size='12'>{x_value:.0f}</text>")
        for idx in range(6):
            z = min_z + (max_z - min_z) * idx / 5
            y = sy(z)
            grid.append(f"<line x1='{left}' y1='{y:.1f}' x2='{left + plot_w}' y2='{y:.1f}' stroke='#eee'/>")
            grid.append(f"<text x='{left - 10}' y='{y + 4:.1f}' text-anchor='end' font-size='12'>{z:.2f}</text>")
        labels = []
        for distance, state in zip(distances, states):
            x = sx(distance)
            labels.append(f"<circle cx='{x:.1f}' cy='{sy(state.water_level_m):.1f}' r='4' fill='#1468b3'/>")
            labels.append(f"<circle cx='{x:.1f}' cy='{sy(state.thalweg_elevation_m):.1f}' r='3.5' fill='#8a5a2b'/>")
            labels.append(
                f"<text x='{x:.1f}' y='{top + plot_h + 44}' text-anchor='middle' font-size='12'>{html_lib.escape(state.name)}</text>"
            )
        return (
            "<div class='chart'>"
            f"<svg viewBox='0 0 {width} {height}' xmlns='http://www.w3.org/2000/svg'>"
            f"<text x='{width / 2}' y='28' text-anchor='middle' font-size='20' font-weight='bold'>多断面能量方程法（HEC标准步法）纵断面图</text>"
            + "".join(grid)
            + f"<line x1='{left}' y1='{top}' x2='{left}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<line x1='{left}' y1='{top + plot_h}' x2='{left + plot_w}' y2='{top + plot_h}' stroke='#222'/>"
            + f"<polyline points='{bed_polyline}' fill='none' stroke='#8a5a2b' stroke-width='2.4'/>"
            + f"<polyline points='{water_polyline}' fill='none' stroke='#1468b3' stroke-width='2.8'/>"
            + "".join(labels)
            + f"<rect x='{width - 230}' y='{top + 10}' width='180' height='56' fill='white' stroke='#bbb'/>"
            + f"<line x1='{width - 210}' y1='{top + 30}' x2='{width - 166}' y2='{top + 30}' stroke='#1468b3' stroke-width='2.8'/>"
            + f"<text x='{width - 156}' y='{top + 35}' font-size='13'>计算水面线</text>"
            + f"<line x1='{width - 210}' y1='{top + 52}' x2='{width - 166}' y2='{top + 52}' stroke='#8a5a2b' stroke-width='2.4'/>"
            + f"<text x='{width - 156}' y='{top + 57}' font-size='13'>深泓线</text>"
            + f"<text x='{left + plot_w / 2}' y='{height - 18}' text-anchor='middle' font-size='15'>沿程距离(m，自下游起算)</text>"
            + f"<text x='22' y='{top + plot_h / 2}' transform='rotate(-90 22 {top + plot_h / 2})' text-anchor='middle' font-size='15'>高程 Z(m)</text>"
            + "</svg></div>"
        )


class WeirFlowWaterLevelTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget) -> None:
        super().__init__(master, padding=0)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.Frame(self)
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="堰流公式计算水位")
        basic.grid(row=0, column=0, sticky="ew")
        ttk.Label(basic, text="堰型").grid(row=0, column=0, padx=(10, 4), pady=6, sticky="e")
        self.weir_type = ttk.Combobox(
            basic,
            width=26,
            state="readonly",
            values=["宽顶堰", "实用堰-敞式", "实用堰-胸墙孔口式", "实用堰-胸墙孔口式平底闸堰", "薄壁堰"],
        )
        self.weir_type.current(0)
        self.weir_type.grid(row=0, column=1, padx=(0, 12), pady=6, sticky="w")
        self.design_flow = self._entry(basic, 0, 1, "设计流量Q(m³/s)", "1479", width=12)
        self.crest_elevation = self._entry(basic, 0, 2, "堰顶/堰底高程(m)", "1481.02", width=12)
        self.downstream_level = self._entry(basic, 0, 3, "下游水位高程(m)", "1481.02", width=12)
        self.opening_count = self._entry(basic, 1, 0, "孔数n", "16", width=12)
        self.single_width = self._entry(basic, 1, 1, "单孔宽/溢流宽B(m)", "6", width=12)
        self.sill_height = self._entry(basic, 1, 2, "坎高P/堰高(m)", "2.18", width=12)
        self.practical_m = self._entry(basic, 1, 3, "流量系数m/μ", "0.48", width=12)
        self.upstream_weir_height = self._entry(basic, 2, 0, "薄壁堰上游堰高a(m)", "0.5", width=12)
        self.orifice_height = self._entry(basic, 2, 1, "孔口高度D(m)", "1.0", width=12)
        self.basic_grid_widgets = {
            "sill_height": self._grid_widgets(basic, 1, 4, 5),
            "practical_m": self._grid_widgets(basic, 1, 6, 7),
            "upstream_weir_height": self._grid_widgets(basic, 2, 0, 1),
            "orifice_height": self._grid_widgets(basic, 2, 2, 3),
        }
        self.default_coefficients = tk.BooleanVar(value=True)
        ttk.Checkbutton(
            basic,
            text="系数采用默认值",
            variable=self.default_coefficients,
            command=self._toggle_default_coefficients,
        ).grid(row=2, column=2, columnspan=2, padx=(10, 12), pady=6, sticky="w")
        ttk.Label(
            basic,
            text="下游水位高程为尾水条件；仅宽顶堰用于判断自由/淹没溢流，无明显顶托时可填小于或等于堰顶/堰底高程。",
            foreground="#666666",
        ).grid(row=3, column=0, columnspan=8, padx=10, pady=(0, 6), sticky="w")

        shape = ttk.LabelFrame(top, text="边墩、闸墩和底坎类型")
        self.shape_frame = shape
        shape.grid(row=1, column=0, sticky="ew", pady=(10, 0))
        ttk.Label(shape, text="边墩形状").grid(row=0, column=0, padx=(10, 4), pady=6, sticky="e")
        self.abutment_type = ttk.Combobox(shape, width=18, state="readonly", values=[
            "1-直角形", "2-八字形", "3-圆弧形", "4-流线形",
        ])
        self.abutment_type.current(3)
        self.abutment_type.grid(row=0, column=1, padx=(0, 12), pady=6, sticky="w")
        ttk.Label(shape, text="闸墩形状").grid(row=0, column=2, padx=(10, 4), pady=6, sticky="e")
        self.pier_type = ttk.Combobox(shape, width=18, state="readonly", values=[
            "1-矩形", "2-尖角形", "3-半圆形", "4-尖圆形", "5-流线型",
        ])
        self.pier_type.current(2)
        self.pier_type.grid(row=0, column=3, padx=(0, 12), pady=6, sticky="w")
        ttk.Label(shape, text="底坎型").grid(row=0, column=4, padx=(10, 4), pady=6, sticky="e")
        self.sill_type = ttk.Combobox(shape, width=12, state="readonly", values=["1-直坎", "2-圆坎"])
        self.sill_type.current(0)
        self.sill_type.grid(row=0, column=5, padx=(0, 12), pady=6, sticky="w")
        self.shape_grid_widgets = {
            "sill_type": self._grid_widgets(shape, 0, 4, 5),
        }

        rating = ttk.LabelFrame(top, text="断面水位流量计算表")
        rating.grid(row=2, column=0, sticky="ew", pady=(10, 0))
        rating.columnconfigure(7, weight=1)
        self.section_name = self._entry(rating, 0, 0, "断面名称", "计算断面（0+000）", width=24)
        self.thalweg_elevation = self._entry(rating, 0, 1, "深泓点高程(m)", "1481.02", width=12)
        self.depth_step = self._entry(rating, 0, 2, "水深步长(m)", "0.20", width=12)
        self.max_depth = self._entry(rating, 0, 3, "最大水深(m)", "3.20", width=12)
        self.rating_contraction = self._entry(rating, 1, 0, "侧收缩系数ε", "1.00", width=12)
        self.rating_m = self._entry(rating, 1, 1, "流量系数m", "0.36", width=12)
        self.rating_g = self._entry(rating, 1, 2, "重力加速度g(m/s²)", "9.81", width=12)
        ttk.Label(
            rating,
            text="系数参考：只有宽顶堰按hₛ/H判断自由溢流或淹没溢流并自动取δ，δ约0.40~1.00；实用堰、薄壁堰不区分自由/淹没溢流。侧收缩系数ε一般可取1.00，WES实用堰常取0.90~0.95；流量系数m宽顶堰约0.32~0.385、敞式实用堰约0.47~0.49、孔口式μ约0.60~0.80、薄壁堰m₀约0.40~0.45。",
            foreground="#666666",
            wraplength=1040,
            justify=tk.LEFT,
        ).grid(row=2, column=0, columnspan=8, padx=10, pady=(4, 4), sticky="ew")
        data_box = ttk.LabelFrame(rating, text="断面点批量录入")
        data_box.grid(row=3, column=0, columnspan=8, sticky="ew", padx=10, pady=(6, 8))
        data_box.columnconfigure(2, weight=1)
        ttk.Label(data_box, text="目标断面").grid(
            row=0, column=0, padx=(10, 4), pady=6, sticky="e"
        )
        self.target_section = ttk.Combobox(data_box, width=14, state="readonly", values=["断面1"])
        self.target_section.current(0)
        self.target_section.grid(row=0, column=1, padx=(0, 12), pady=6, sticky="w")
        point_bar = ttk.Frame(data_box)
        point_bar.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="e")
        self._action_button(point_bar, "导入Excel/CSV", self._import_rating_points, bg="#1688e8", fg="white").pack(side=tk.LEFT)
        self._action_button(point_bar, "整理粘贴数据", self._normalize_rating_points).pack(side=tk.LEFT, padx=(10, 0))
        self.rating_points = ScrolledText(data_box, height=4, font=(DASHBOARD_MONO_FONT, 9))
        self.rating_points.grid(row=1, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 8))
        self.rating_points.insert(
            tk.END,
            "0 1484.50\n2 1483.00\n5 1481.02\n18 1481.02\n30 1483.00\n34 1484.50",
        )

        note = ttk.LabelFrame(top, text="参数填写说明")
        note.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        ttk.Label(
            note,
            text=(
                "必须手动输入：堰型、设计流量Q、堰顶/堰底高程、孔数n、单孔宽/溢流宽B、断面深泓点高程、最大水深、断面点。"
                "下游水位是下游水面高程，不是深泓点高程；深泓点高程用于断面表中把水深h₀换算为设计洪水位。"
                "坎高P/堰高用于宽顶堰流量系数计算，可理解为堰顶或坎顶相对上游底板/河床的高度。"
                "可默认参数：重力加速度g=9.81；宽顶堰淹没系数δ由下游水位和逐行水位自动判断；侧收缩系数ε宽顶堰默认1.00、敞式实用堰默认0.95；流量系数m宽顶堰默认0.36、敞式实用堰默认0.48、孔口式μ默认0.70、薄壁堰m₀默认0.42；取消“系数采用默认值”后可手动修改。"
                "δ、ε、m需按堰型和边界收缩情况复核；无资料时先用默认值试算。"
                "宽顶堰需要坎高P和底坎型；敞式实用堰需要m及边墩/闸墩形状；胸墙孔口式需要μ和D；薄壁堰只保留薄壁堰相关堰高参数。"
            ),
            foreground="#555555",
            wraplength=1060,
            justify=tk.LEFT,
        ).grid(row=0, column=0, sticky="ew", padx=10, pady=8)

        ttk.Label(
            top,
            text="根据堰流计算程序.xls整理：宽顶堰按 Q=δ·m·ε·n·B·sqrt(2g)·H^(3/2)；敞式实用堰按 Q=m·ε·n·B·sqrt(2g)·H^(3/2)；胸墙孔口式按 Q=μ·A·sqrt(2g(H-D))；薄壁堰按 Q=m₀·b·sqrt(2g)·H^(3/2)。",
            foreground="#666666",
        ).grid(row=4, column=0, sticky="w", padx=8, pady=(8, 0))

        actions = ttk.Frame(top)
        actions.grid(row=5, column=0, sticky="ew", pady=(12, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))
        self.output = self._result_box(self)
        self.weir_type.bind("<<ComboboxSelected>>", lambda _event: self._refresh_weir_type_fields())
        self._toggle_default_coefficients()
        self._refresh_weir_type_fields()

    def _combo_code(self, combo: ttk.Combobox) -> int:
        text = combo.get().strip()
        match = re.match(r"(\d+)", text)
        return int(match.group(1)) if match else 1

    def _grid_widgets(self, parent: tk.Widget, row: int, *columns: int) -> list[tk.Widget]:
        widgets: list[tk.Widget] = []
        for column in columns:
            widgets.extend(parent.grid_slaves(row=row, column=column))
        return widgets

    def _set_entry_text(self, entry: tk.Entry, value: str) -> None:
        entry.configure(state="normal")
        entry.delete(0, tk.END)
        entry.insert(0, value)

    def _default_weir_coefficients(self) -> dict[tk.Entry, str]:
        weir_type = self.weir_type.get().strip()
        if weir_type == "实用堰-敞式":
            rating_m = "0.48"
            contraction = "0.95"
            practical_m = "0.48"
        elif weir_type in {"实用堰-胸墙孔口式", "实用堰-胸墙孔口式平底闸堰"}:
            rating_m = "0.70"
            contraction = "1.00"
            practical_m = "0.70"
        elif weir_type == "薄壁堰":
            rating_m = "0.42"
            contraction = "1.00"
            practical_m = "0.48"
        else:
            rating_m = "0.36"
            contraction = "1.00"
            practical_m = "0.48"
        return {
            self.practical_m: practical_m,
            self.upstream_weir_height: "0.5",
            self.orifice_height: "1.0",
            self.rating_contraction: contraction,
            self.rating_m: rating_m,
            self.rating_g: "9.81",
        }

    def _apply_default_coefficients(self) -> None:
        for entry, value in self._default_weir_coefficients().items():
            self._set_entry_text(entry, value)
            entry.configure(state="disabled")

    def _toggle_default_coefficients(self) -> None:
        defaulting = self.default_coefficients.get()
        defaults = self._default_weir_coefficients()
        for entry, value in defaults.items():
            if defaulting:
                self._apply_default_coefficients()
                break
            else:
                entry.configure(state="normal")
        self._refresh_weir_type_fields()

    def _show_widgets(self, widgets: list[tk.Widget], show: bool) -> None:
        for widget in widgets:
            if show:
                widget.grid()
            else:
                widget.grid_remove()

    def _refresh_weir_type_fields(self) -> None:
        weir_type = self.weir_type.get().strip()
        is_broad = weir_type == "宽顶堰"
        is_open_practical = weir_type == "实用堰-敞式"
        is_orifice = weir_type in {"实用堰-胸墙孔口式", "实用堰-胸墙孔口式平底闸堰"}
        is_thin = weir_type == "薄壁堰"
        self._show_widgets(self.basic_grid_widgets["sill_height"], is_broad)
        self._show_widgets(self.basic_grid_widgets["practical_m"], is_open_practical or is_orifice)
        self._show_widgets(self.basic_grid_widgets["upstream_weir_height"], is_thin)
        self._show_widgets(self.basic_grid_widgets["orifice_height"], is_orifice)
        if self.default_coefficients.get():
            self._apply_default_coefficients()
        if is_thin or is_orifice:
            self.shape_frame.grid_remove()
        else:
            self.shape_frame.grid()
            self._show_widgets(self.shape_grid_widgets["sill_type"], is_broad)

    def _rating_points_text(self) -> str:
        return self.rating_points.get("1.0", tk.END).strip()

    def _set_rating_points_text(self, points: tuple[tuple[float, float], ...]) -> None:
        self.rating_points.delete("1.0", tk.END)
        self.rating_points.insert(tk.END, "\n".join(f"{x:g} {z:g}" for x, z in points))

    def _normalize_rating_points(self) -> None:
        try:
            points = parse_hydraulic_points(self._rating_points_text(), "堰流断面")
            self._set_rating_points_text(points)
        except Exception as exc:
            messagebox.showerror("断面点错误", str(exc))

    def _import_rating_points(self) -> None:
        try:
            points = self._import_points_from_file()
            if points:
                self._set_rating_points_text(points)
        except Exception as exc:
            messagebox.showerror("导入断面点", str(exc))

    def calculate(self) -> None:
        try:
            result = calculate_weir_water_level(
                WeirFlowInput(
                    weir_type=self.weir_type.get(),
                    design_flow_m3s=float(self.design_flow.get().strip()),
                    crest_elevation_m=float(self.crest_elevation.get().strip()),
                    downstream_water_level_m=float(self.downstream_level.get().strip()),
                    opening_count=int(float(self.opening_count.get().strip())),
                    single_width_m=float(self.single_width.get().strip()),
                    sill_height_m=float(self.sill_height.get().strip()),
                    abutment_type=self._combo_code(self.abutment_type),
                    pier_type=self._combo_code(self.pier_type),
                    sill_type=self._combo_code(self.sill_type),
                    practical_m=float(self.practical_m.get().strip()),
                    upstream_weir_height_m=float(self.upstream_weir_height.get().strip()),
                    orifice_height_m=float(self.orifice_height.get().strip()),
                )
            )
            rating_result = calculate_weir_section_rating(
                WeirSectionRatingInput(
                    section_name=self.section_name.get().strip() or self.target_section.get().strip(),
                    points=parse_hydraulic_points(self._rating_points_text(), "堰流断面"),
                    thalweg_elevation_m=float(self.thalweg_elevation.get().strip()),
                    max_depth_m=float(self.max_depth.get().strip()),
                    weir_type=self.weir_type.get(),
                    crest_elevation_m=float(self.crest_elevation.get().strip()),
                    downstream_water_level_m=float(self.downstream_level.get().strip()),
                    depth_step_m=float(self.depth_step.get().strip()),
                    contraction_coeff=float(self.rating_contraction.get().strip()),
                    discharge_coeff=float(self.rating_m.get().strip()),
                    opening_count=int(float(self.opening_count.get().strip())),
                    gravity=float(self.rating_g.get().strip()),
                    orifice_height_m=float(self.orifice_height.get().strip()),
                )
            )
            input_rows = [
                ["项目", "数值"],
                ["堰型", result.weir_type],
                ["设计流量Q(m³/s)", f"{result.design_flow_m3s:.3f}"],
                ["堰顶/堰底高程(m)", f"{result.crest_elevation_m:.3f}"],
                ["下游水位高程(m)", f"{result.downstream_water_level_m:.3f}"],
                ["孔数n", self.opening_count.get().strip()],
                ["单孔宽/溢流宽B(m)", self.single_width.get().strip()],
            ]
            if result.weir_type == "宽顶堰":
                input_rows.extend([
                    ["坎高P/堰高(m)", self.sill_height.get().strip()],
                    ["边墩形状", self.abutment_type.get()],
                    ["闸墩形状", self.pier_type.get()],
                    ["底坎型", self.sill_type.get()],
                ])
            elif result.weir_type == "实用堰-敞式":
                input_rows.extend([
                    ["实用堰m", self.practical_m.get().strip()],
                    ["边墩形状", self.abutment_type.get()],
                    ["闸墩形状", self.pier_type.get()],
                ])
            elif result.weir_type in {"实用堰-胸墙孔口式", "实用堰-胸墙孔口式平底闸堰"}:
                input_rows.extend([
                    ["孔口流量系数μ", self.practical_m.get().strip()],
                    ["孔口高度D(m)", self.orifice_height.get().strip()],
                ])
            else:
                input_rows.append(["薄壁堰上游堰高a(m)", self.upstream_weir_height.get().strip()])
            parameter_note_rows = [
                ["参数", "是否必须手动", "含义与建议"],
                ["堰型", "是", "宽顶堰、实用堰-敞式、实用堰-胸墙孔口式、实用堰-胸墙孔口式平底闸堰、薄壁堰。"],
                ["设计流量Q", "是", "需要计算对应水位的洪峰流量，单位m³/s。"],
                ["堰顶/堰底高程", "是", "计算水头H和上游水位的基准高程。"],
                ["下游水位高程", "通常需填", "是下游水面高程，不是深泓点高程；仅宽顶堰用于判别淹没影响。"],
                ["孔数n、单孔宽B", "是", "用于反算设计流量对应水位；断面表中的河宽B由断面点自动计算。"],
                ["坎高P/堰高、底坎型", "宽顶堰需填", "P为堰顶或坎顶相对上游底板/河床的高度，用于宽顶堰流量系数计算。"],
                ["实用堰-敞式m", "可默认", "默认0.48；图示资料一般取0.47~0.49，有设计手册或试验资料时应改为项目取值。"],
                ["胸墙孔口μ、D", "孔口式需填", "μ默认0.70，一般约0.6~0.8；D为孔口高度，按孔口出流公式计算。"],
                ["薄壁堰上游堰高a", "薄壁堰需填", "默认0.50m；用于薄壁堰m₀计算。"],
                ["宽顶堰淹没系数δ", "自动计算", "仅宽顶堰按每行H、hₛ和hₛ/H判断；hₛ/H<=0.80为自由溢流，超过后按淹没溢流折减。"],
                ["实用堰、薄壁堰状态", "自动标注", "不区分自由溢流和淹没溢流；表中分别标为实用堰出流、孔口出流或薄壁堰出流。"],
                ["侧收缩系数ε", "可默认", "宽顶堰一般可不考虑平面收缩取1.00；WES实用堰常取0.90~0.95。"],
                ["流量系数m/μ", "可默认", "宽顶堰m默认0.36；敞式实用堰m默认0.48；孔口式μ默认0.70；薄壁堰m₀默认0.42。"],
                ["重力加速度g(m/s²)", "可默认", "通常取9.81m/s²。"],
                ["深泓点高程、断面点", "是", "用于逐级计算设计洪水位和对应河宽B。"],
                ["水深步长、最大水深", "是", "控制水位流量关系表行数；步长常用0.1至0.5m。"],
            ]
            rating_rows = [
                ["流量Q(m³/s)", "出流状态", "宽顶堰δ", "侧收缩系数ε", "流量系数m/μ", "H(m)", "hₛ(m)", "hₛ/H", "断面h₀(m)", "深泓点高程(m)", "设计洪水位(m)", "河宽B(m)"]
            ]
            for row in rating_result.rows:
                rating_rows.append([
                    f"{row.flow_m3s:.2f}",
                    row.overflow_state,
                    f"{row.submergence_coeff:.2f}",
                    f"{row.contraction_coeff:.2f}",
                    f"{row.discharge_coeff:.2f}",
                    f"{row.head_m:.2f}",
                    f"{row.downstream_head_m:.2f}",
                    f"{row.submergence_ratio:.3f}",
                    f"{row.depth_m:.2f}",
                    f"{row.thalweg_elevation_m:.2f}",
                    f"{row.water_level_m:.2f}",
                    f"{row.top_width_m:.2f}",
                ])
            result_rows = [
                ["项目", "数值"],
                ["计算上游水位(m)", f"{result.upstream_water_level_m:.3f}"],
                ["堰上作用水头H(m)", f"{result.head_m:.3f}"],
                ["下游堰上水深hₛ(m)", f"{result.downstream_head_m:.3f}"],
                ["hₛ/H", f"{result.submergence_ratio:.3f}"],
                ["宽顶堰淹没系数δ", f"{result.submergence_coeff:.3f}"],
                ["流量系数m/m₀", f"{result.discharge_coeff:.6f}"],
                ["边墩系数", f"{result.abutment_coeff:.3f}"],
                ["闸墩系数", f"{result.pier_coeff:.3f}"],
                ["侧收缩系数ε", f"{result.contraction_coeff:.3f}"],
                ["复核流量Q(m³/s)", f"{result.calculated_flow_m3s:.3f}"],
                ["公式", result.formula_note],
                ["适用提示", result.classification_note],
            ]
            lines = [
                "5.1.10  堰流公式法",
                "",
                "当控制断面对水流产生束窄、顶托或溢流控制时，可按相应堰流或孔口出流公式计算断面水位。",
                "",
                "计算公式：",
                "宽顶堰：Q = δ·ε·m·n·B·sqrt(2g)·H^(3/2)；敞式实用堰：Q = ε·m·n·B·sqrt(2g)·H^(3/2)；孔口出流：Q = μ·A·sqrt(2g(H-D))",
                "",
                "式中：Q为流量(m³/s)；δ为宽顶堰淹没系数；ε为侧收缩系数；m或μ为流量系数；n为孔数；B为对应水位下过流宽度(m)；H为堰上水头；D为孔口高度；g为重力加速度。",
                "",
                "一、输入参数",
                format_fixed_table(input_rows, min_width=16),
                "",
                "二、参数备注与取值说明",
                format_fixed_table(parameter_note_rows, min_width=14),
                "",
                f"表 5.1-73  {rating_result.section_name} 水位流量计算表",
                format_fixed_table(rating_rows, min_width=12),
                "",
                "三、设计流量反算成果",
                format_fixed_table(result_rows, min_width=16),
                "",
                "说明：默认系数按所附资料设置；项目有专门设计手册、模型试验或审查取值时，应取消默认后采用项目取值。",
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class BackwaterCalculationTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.LabelFrame(self, text="壅水高度及壅水曲线长度计算")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="输入参数")
        basic.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.design_flow = self._entry(basic, 0, 0, "设计流量Qₚ(m³/s)", "500", width=14)
        self.full_area = self._entry(basic, 0, 1, "全河天然过水面积ω(m²)", "300", width=14)
        self.vp = self._entry(basic, 0, 2, "设计流速VP(m/s)", "2.0", width=14)
        self.bridge_area = self._entry(basic, 0, 3, "建后有效过水面积ωg(m²)", "220", width=14)
        self.alpha = self._entry(basic, 1, 0, "水流交角α(°)", "10", width=14)
        self.slope = self._entry(basic, 1, 1, "天然水面坡度I₀(小数)", "0.001", width=14)
        self.eta_manual = self._entry(basic, 1, 2, "η手动(可空)", "", width=14)
        ttk.Label(basic, text="面积阻水比(自动)").grid(row=1, column=6, padx=(10, 4), pady=6, sticky="e")
        self.obstruction_ratio_var = tk.StringVar(value="--")
        ttk.Label(
            basic,
            textvariable=self.obstruction_ratio_var,
            background="#fffbc7",
            width=14,
            anchor="center",
        ).grid(row=1, column=7, padx=(0, 12), pady=6, sticky="w")
        ttk.Label(basic, text="土质类型").grid(row=2, column=0, padx=(10, 4), pady=6, sticky="e")
        self.soil_type = ttk.Combobox(basic, values=list(BACKWATER_SOIL_TYPES), state="readonly", width=12)
        self.soil_type.current(1)
        self.soil_type.grid(row=2, column=1, padx=(0, 12), pady=6, sticky="w")
        self.blocked_flow_ratio = self._entry(basic, 2, 1, "阻挡流量/设计流量(%)", "20", width=14)
        ttk.Label(basic, text="η取值依据").grid(row=2, column=4, padx=(10, 4), pady=6, sticky="e")
        self.eta_choice_values = [f"{eta:.2f}  {river}" for river, _upper, eta in BACKWATER_ETA_ROWS]
        self.eta_choice = ttk.Combobox(basic, values=self.eta_choice_values, state="readonly", width=28)
        self.eta_choice.current(1)
        self.eta_choice.grid(row=2, column=5, padx=(0, 12), pady=6, sticky="w")

        eta_rows = [["河流特征", "河滩路堤阻挡流量/设计流量", "η"]]
        labels = ["≤10%", "11~30%", "31~50%", ">50%"]
        for (river, _upper, eta), label in zip(BACKWATER_ETA_ROWS, labels):
            eta_rows.append([river, label, f"{eta:.2f}"])
        self.eta_reference_table = RichTableText(
            top,
            height=7,
            font=(DASHBOARD_CARD_FONT, 9),
            table_font=(DASHBOARD_CARD_FONT, 9),
            wraplength=980,
        )
        self.eta_reference_table.grid(row=1, column=0, padx=8, pady=(0, 8), sticky="ew")
        self.eta_reference_table.insert(
            "1.0",
            "表A.2.3-1 η系数取值参考\n" + format_fixed_table(eta_rows, min_width=14),
        )

        note = ttk.Label(
            top,
            text=(
                "公式来源：SLT 808-2025 A.2.3。V=Qₚ/全河天然过水面积ω；ωx=Qₚ/(Vₚ·cosα)；P=ωx/ωg；ωg=ω-建筑物阻水面积；"
                "Vₘ按土质取值：松软土Vₘ=Vₚ，中等土Vₘ=Vₚ·2P/(P+1)，密实土Vₘ=P·Vₚ；"
                "ΔZₘ=η(Vₘ²-V²)，Lᵧ=2ΔZₘ/I₀。η按河滩路堤阻挡流量/设计流量查表；"
                "面积阻水比=Δω/ω×100%只作断面复核，不用于查η。η手动输入时优先采用手动值。"
            ),
            foreground="#666666",
            wraplength=1060,
            justify=tk.LEFT,
        )
        note.grid(row=2, column=0, padx=8, pady=(0, 8), sticky="ew")

        actions = ttk.Frame(top)
        actions.grid(row=3, column=0, sticky="ew", padx=8, pady=(4, 0))
        self._action_button(actions, "计算面积阻水比", self.apply_blocked_ratio_eta).pack(side=tk.LEFT)
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def _set_eta_choice_by_value(self, eta: float) -> None:
        target = f"{eta:.2f}"
        for idx, text in enumerate(self.eta_choice_values):
            if text.startswith(target):
                self.eta_choice.current(idx)
                return

    def _obstruction_area_ratio(self) -> tuple[float, float]:
        full_area = float(self.full_area.get().strip())
        effective_area = float(self.bridge_area.get().strip())
        obstruction_area = max(0.0, full_area - effective_area)
        ratio = obstruction_area_ratio_percent(full_area, effective_area)
        self.obstruction_ratio_var.set(f"{ratio:.2f}%")
        return obstruction_area, ratio

    def apply_blocked_ratio_eta(self) -> None:
        try:
            obstruction_area, ratio = self._obstruction_area_ratio()
            self._write(
                self.output,
                "\n".join(
                    [
                        "面积阻水比计算",
                        "",
                        format_fixed_table(
                            [
                                ["项目", "数值"],
                                ["全河天然过水面积ω(m²)", self.full_area.get().strip()],
                                ["建后有效过水面积ωg(m²)", self.bridge_area.get().strip()],
                                ["阻水面积Δω=ω-ωg(m²)", f"{obstruction_area:.3f}"],
                                ["面积阻水比Δω/ω(%)", f"{ratio:.2f}"],
                            ],
                            min_width=16,
                        ),
                        "",
                        "面积阻水比仅用于断面几何复核。η应按“河滩路堤阻挡流量/设计流量”查表。",
                    ]
                ),
            )
        except Exception as exc:
            messagebox.showerror("阻水比计算错误", str(exc))

    def calculate(self) -> None:
        try:
            eta_text = self.eta_manual.get().strip()
            obstruction_area, obstruction_ratio = self._obstruction_area_ratio()
            blocked_flow_ratio = float(self.blocked_flow_ratio.get().strip())
            recommended_eta, recommended_source = eta_from_blocked_flow_ratio(blocked_flow_ratio)
            if eta_text:
                eta_value = float(eta_text)
                eta_source = "手动输入"
            else:
                eta_value = recommended_eta
                eta_source = recommended_source
                self._set_eta_choice_by_value(recommended_eta)
            result = calculate_backwater(
                BackwaterInput(
                    design_flow_m3s=float(self.design_flow.get().strip()),
                    full_section_area_m2=float(self.full_area.get().strip()),
                    natural_channel_velocity_ms=float(self.vp.get().strip()),
                    bridge_actual_area_m2=float(self.bridge_area.get().strip()),
                    flow_angle_deg=float(self.alpha.get().strip()),
                    soil_type=self.soil_type.get(),
                    obstruction_area_ratio_percent=obstruction_ratio,
                    natural_water_surface_slope=float(self.slope.get().strip()),
                    blocked_flow_ratio_percent=blocked_flow_ratio,
                    eta_manual=eta_value,
                    eta_source_label=eta_source,
                )
            )
            input_rows = [
                ["项目", "数值"],
                ["设计流量Qₚ(m³/s)", f"{result.design_flow_m3s:.3f}"],
                ["全河平均流速V(m/s)", f"{result.average_velocity_v_ms:.4f}"],
                ["天然河槽设计流速VP(m/s)", f"{result.natural_channel_velocity_vp_ms:.4f}"],
                ["设计所需过水面积ωx(m²)", f"{result.design_bridge_area_wx_m2:.4f}"],
                ["建后有效过水面积ωg(m²)", f"{result.actual_bridge_area_wg_m2:.4f}"],
                ["阻水面积Δω=ω-ωg(m²)", f"{result.obstruction_area_m2:.4f}"],
                ["面积阻水比Δω/ω(%)", f"{result.obstruction_area_ratio_percent:.2f}"],
                ["冲刷系数P", f"{result.scour_coefficient_p:.4f}"],
                ["建后断面平均流速Vₘ(m/s)", f"{result.bridge_velocity_vm_ms:.4f}"],
                ["河滩路堤阻挡流量比(%)", f"{result.blocked_flow_ratio_percent:.2f}"],
                ["按阻挡流量比推荐η", f"{recommended_eta:.4f}"],
                ["推荐η依据", recommended_source],
                ["η", f"{result.eta:.4f}"],
                ["η来源", result.eta_source],
                ["土质类型", result.soil_type],
            ]
            result_rows = [
                ["成果项目", "数值", "单位"],
                ["桥前最大壅水高度ΔZₘ", f"{result.max_backwater_height_m:.4f}", "m"],
                ["壅水曲线全长Lᵧ", f"{result.backwater_curve_length_m:.2f}", "m"],
            ]
            lines = [
                "壅水高度及壅水曲线长度计算",
                "",
                "一、计算公式",
                format_fixed_table([["来源", "SLT 808-2025 A.2.3"], ["计算式", result.formula_note]], min_width=16),
                "",
                "二、参数与中间量",
                format_fixed_table(input_rows, min_width=16),
                "",
                "三、计算成果",
                format_fixed_table(result_rows, min_width=16),
                "",
                "说明：壅水影响范围较大或影响严重时，导则建议采用一维/二维数学模型或河工模型试验复核。",
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class WaveHeightCalculationTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.LabelFrame(self, text="波浪高度计算")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="输入参数")
        basic.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.wind_speed = self._entry(basic, 0, 0, "计算风速V(m/s)", "20", width=14)
        self.fetch_length = self._entry(basic, 0, 1, "风区长度F(m)", "1000", width=14)
        self.average_depth = self._entry(basic, 0, 2, "平均水深d(m)", "3.0", width=14)
        self.factor_mode = tk.StringVar(value="按规范表C.1.3自动计算")
        ttk.Label(basic, text="设计波高换算方式").grid(row=0, column=6, sticky="e", padx=(10, 4), pady=6)
        ttk.Combobox(
            basic,
            textvariable=self.factor_mode,
            values=("按规范表C.1.3自动计算", "手动输入Kp"),
            state="readonly",
            width=24,
        ).grid(row=0, column=7, sticky="w", padx=(0, 12), pady=6)
        self.frequency = tk.StringVar(value="5")
        ttk.Label(basic, text="累积频率P(%)").grid(row=1, column=0, sticky="e", padx=(10, 4), pady=6)
        ttk.Combobox(
            basic,
            textvariable=self.frequency,
            values=("0.1", "1", "2", "3", "4", "5", "10", "13", "20", "50"),
            state="readonly",
            width=12,
        ).grid(row=1, column=1, sticky="w", padx=(0, 12), pady=6)
        self.height_factor = self._entry(basic, 1, 1, "手动波高换算Kp", "1.0", width=14)
        self.wind_duration = self._entry(basic, 1, 2, "实际持续风时(h，可空)", "", width=14)
        self.gravity = self._entry(basic, 1, 3, "重力加速度g(m/s²)", "9.81", width=14)

        note = ttk.Label(
            top,
            text=(
                "按《堤防工程设计规范》常用莆田试验站公式计算平均波高H和平均波周期T；"
                "自动模式按平均波高/水深比和累积频率P，从表C.1.3线性插值Kp；手动模式需注明Kp来源。"
                "填写实际持续风时后，程序会与公式C.1.2-3所需最小风时比较。"
            ),
            foreground="#666666",
            wraplength=1060,
            justify=tk.LEFT,
        )
        note.grid(row=1, column=0, padx=8, pady=(0, 8), sticky="ew")

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", padx=8, pady=(4, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def calculate(self) -> None:
        try:
            automatic_factor = self.factor_mode.get() == "按规范表C.1.3自动计算"
            wind_duration_text = self.wind_duration.get().strip()
            result = calculate_wave_height(
                WaveHeightInput(
                    wind_speed_ms=float(self.wind_speed.get().strip()),
                    fetch_length_m=float(self.fetch_length.get().strip()),
                    average_depth_m=float(self.average_depth.get().strip()),
                    gravity=float(self.gravity.get().strip()),
                    height_factor=float(self.height_factor.get().strip()),
                    cumulative_frequency_percent=float(self.frequency.get()) if automatic_factor else None,
                    wind_duration_h=float(wind_duration_text) if wind_duration_text else None,
                )
            )
            input_rows = [
                ["项目", "数值"],
                ["计算风速V(m/s)", f"{result.wind_speed_ms:.3f}"],
                ["风区长度F(m)", f"{result.fetch_length_m:.3f}"],
                ["平均水深d(m)", f"{result.average_depth_m:.3f}"],
                ["重力加速度g(m/s²)", f"{float(self.gravity.get().strip()):.3f}"],
                ["波高换算方式", result.height_factor_source],
                ["累积频率P(%)", "手动模式" if result.cumulative_frequency_percent is None else f"{result.cumulative_frequency_percent:g}"],
                ["波高换算系数Kp", f"{result.height_factor:.4f}"],
                ["实际持续风时(h)", "未填写" if result.wind_duration_h is None else f"{result.wind_duration_h:.4f}"],
            ]
            middle_rows = [
                ["项目", "数值"],
                ["gd/V^2", f"{result.dimensionless_depth:.6f}"],
                ["gF/V^2", f"{result.dimensionless_fetch:.6f}"],
                ["水深项", f"{result.depth_term:.6f}"],
                ["风区项", f"{result.fetch_term:.6f}"],
                ["平均波高/水深H/d", f"{result.height_depth_ratio:.6f}"],
                ["达到稳定状态最小风时(h)", f"{result.minimum_wind_duration_h:.4f}"],
            ]
            result_rows = [
                ["成果项目", "数值", "单位"],
                ["平均波高H", f"{result.mean_wave_height_m:.4f}", "m"],
                ["平均波周期T", f"{result.mean_wave_period_s:.4f}", "s"],
                ["波长L", f"{result.wavelength_m:.4f}", "m"],
                ["设计波高Hp=Kp*H", f"{result.design_wave_height_m:.4f}", "m"],
                ["波陡Hp/L", f"{result.wave_steepness:.6f}", "-"],
            ]
            if result.duration_sufficient is not None:
                result_rows.append(
                    ["持续风时校核", "满足" if result.duration_sufficient else "不满足，风浪可能未达到稳定状态", "-"]
                )
            lines = [
                "波浪高度计算",
                "",
                "一、计算公式",
                format_fixed_table([["来源", "《堤防工程设计规范》莆田试验站公式"], ["计算式", result.formula_note]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(input_rows, min_width=16),
                "",
                "三、中间量",
                format_fixed_table(middle_rows, min_width=16),
                "",
                "四、计算成果",
                format_fixed_table(result_rows, min_width=16),
                "",
                "说明：自动Kp仅在表C.1.3的H/d=0~0.5范围内计算，不作外推；若用于堤顶超高、护坡稳定或风浪爬高，应继续按相应规范进行爬高和安全加高计算。",
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class WindSetupCalculationTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.LabelFrame(self, text="风壅水位计算")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        basic = ttk.LabelFrame(top, text="输入参数")
        basic.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.base_level = self._entry(basic, 0, 0, "静水位Z0(m)", "100.00", width=14)
        self.wind_speed = self._entry(basic, 0, 1, "计算风速V(m/s)", "20", width=14)
        self.fetch_length = self._entry(basic, 0, 2, "风区长度D(m)", "1000", width=14)
        self.average_depth = self._entry(basic, 0, 3, "平均水深Hm(m)", "3.0", width=14)
        self.wind_angle = self._entry(basic, 1, 0, "风向夹角β(°)", "0", width=14)
        self.wind_coefficient = self._entry(basic, 1, 1, "综合摩阻系数k", "0.0000036", width=14)
        self.gravity = self._entry(basic, 1, 2, "重力加速度g(m/s²)", "9.81", width=14)

        note = ttk.Label(
            top,
            text=(
                "风向夹角β按风向与计算岸段法线夹角填写：0°为正向吹向计算岸段，风壅最大；"
                "90°为沿岸风，风壅为0。默认k=3.6E-6，正式成果应按采用规范或专题资料复核。"
            ),
            foreground="#666666",
            wraplength=1060,
            justify=tk.LEFT,
        )
        note.grid(row=1, column=0, padx=8, pady=(0, 8), sticky="ew")

        actions = ttk.Frame(top)
        actions.grid(row=2, column=0, sticky="ew", padx=8, pady=(4, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)

    def calculate(self) -> None:
        try:
            result = calculate_wind_setup(
                WindSetupInput(
                    base_water_level_m=float(self.base_level.get().strip()),
                    wind_speed_ms=float(self.wind_speed.get().strip()),
                    fetch_length_m=float(self.fetch_length.get().strip()),
                    average_depth_m=float(self.average_depth.get().strip()),
                    wind_angle_deg=float(self.wind_angle.get().strip()),
                    wind_stress_coefficient=float(self.wind_coefficient.get().strip()),
                    gravity=float(self.gravity.get().strip()),
                )
            )
            input_rows = [
                ["项目", "数值"],
                ["静水位Z0(m)", f"{result.base_water_level_m:.3f}"],
                ["计算风速V(m/s)", f"{result.wind_speed_ms:.3f}"],
                ["风区长度D(m)", f"{result.fetch_length_m:.3f}"],
                ["平均水深Hm(m)", f"{result.average_depth_m:.3f}"],
                ["风向夹角β(°)", f"{result.wind_angle_deg:.3f}"],
                ["综合摩阻系数k", f"{result.wind_stress_coefficient:.9f}"],
                ["重力加速度g(m/s²)", f"{result.gravity:.3f}"],
            ]
            middle_rows = [
                ["项目", "数值"],
                ["方向系数cosβ", f"{result.direction_factor:.6f}"],
                ["V²", f"{result.wind_speed_ms * result.wind_speed_ms:.3f}"],
                ["2*g*Hm", f"{2 * result.gravity * result.average_depth_m:.3f}"],
            ]
            result_rows = [
                ["成果项目", "数值", "单位"],
                ["风壅增水hz", f"{result.wind_setup_m:.4f}", "m"],
                ["叠加后水位Z=Z0+hz", f"{result.adjusted_water_level_m:.3f}", "m"],
            ]
            lines = [
                "风壅水位计算",
                "",
                "一、计算公式",
                format_fixed_table([["来源", "常用风壅水面高度公式"], ["计算式", result.formula_note]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(input_rows, min_width=16),
                "",
                "三、中间量",
                format_fixed_table(middle_rows, min_width=16),
                "",
                "四、计算成果",
                format_fixed_table(result_rows, min_width=16),
                "",
                "说明：风壅计算结果对风速、风区长度和平均水深较敏感；用于防洪评价或堤顶高程复核时，应与设计洪水位、波浪爬高和安全加高统一口径。",
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class RiverLeveeBookTab(ttk.Frame, FormMixin):
    METHOD_DEFS = {
        "河道流量估算": {
            "source": "《水利大算盘》第三章第1节，式(3-1)、式(3-2)。",
            "formula": "v=H^(2/3)*sqrt(J)/n；Q=B*H*v",
            "params": [
                ("H", "平均水深H(m)", "3.5"),
                ("B", "河道宽度B(m)", "30"),
                ("J", "水面比降J", "0.0005"),
                ("n", "糙率n", "0.025"),
            ],
        },
        "河道治导线宽度": {
            "source": "《水利大算盘》第三章第2节，式(3-3)。",
            "formula": "B=Q*n/(H^(5/3)*sqrt(J))",
            "params": [
                ("Q", "设计流量Q(m3/s)", "239.63"),
                ("H", "平均水深H(m)", "3.5"),
                ("J", "水面比降J", "0.0005"),
                ("n", "糙率n", "0.025"),
            ],
        },
        "超高和堤顶高程": {
            "source": "《水利大算盘》第三章第4节，式(3-4)至式(3-21)。",
            "formula": "反算梯形断面水深h；e=0.0208*v^(5/4)*D^(1/3)；堤身高度H=h+e+A",
            "params": [
                ("Q", "设计流量Q(m3/s)", "45.82"),
                ("B", "底宽B(m)", "30"),
                ("m", "边坡系数m", "1.5"),
                ("J", "比降J", "0.0005"),
                ("n", "糙率n", "0.033"),
                ("D", "风区长度D(km)", "30"),
                ("A", "安全加高A(m)", "0.6"),
            ],
        },
        "堤身护坡厚度": {
            "source": "《水利大算盘》第三章第5节，式(3-22)。",
            "formula": "t=n*c*(W/(0.1*γb))^(1/3)",
            "params": [
                ("N", "砌块层数n", "1"),
                ("c", "系数c", "1.4"),
                ("W", "单块重量W(t)", "0.025"),
                ("gamma_b", "块石重度γb(kN/m3)", "23.5"),
            ],
        },
        "堤身护坡块石尺寸": {
            "source": "《水利大算盘》第三章第5节，式(3-23)、式(3-24)。",
            "formula": "d=v^2/[2*c^2*g*((γs-γ)/γ)]；d0=1.24*(a*b*h)^(1/3)",
            "params": [
                ("v", "岸坡流速v(m/s)", "3"),
                ("gamma_w", "水重度γ(t/m3)", "1"),
                ("gamma_s", "块石重度γs(t/m3)", "2.65"),
                ("c", "稳定系数c", "0.9"),
                ("a", "块石长度a(m)", "0.30"),
                ("b", "块石宽度b(m)", "0.23"),
                ("h", "块石高度h(m)", "0.30"),
            ],
        },
        "堤基冲刷-水流斜冲护岸": {
            "source": "《水利大算盘》第三章第6节，式(3-25)、式(3-27)。",
            "formula": "vj=Q/(ω-ωp)；hp=23*tan(α/2)*vj^2/(sqrt(1+m^2)*g)-30d",
            "params": [
                ("alpha", "水流与岸坡交角α(度)", "30"),
                ("m", "岸坡边坡系数m", "1.5"),
                ("d", "坡脚土壤粒径d(m)", "0.005"),
                ("Q", "设计流量Q(m3/s)", "45.82"),
                ("omega", "原河道过水面积ω(m2)", "47.25"),
                ("omega_p", "缩窄部分面积ωp(m2)", "20"),
            ],
        },
        "堤基冲刷-水流平行护岸": {
            "source": "《水利大算盘》第三章第6节，式(3-28)。",
            "formula": "hB=p*h+[(vp/vH)^n-1]；冲深=hB-h",
            "params": [
                ("h", "冲刷前水深h(m)", "2"),
                ("vp", "河槽平均流速vp(m/s)", "2"),
                ("vH", "河床允许不冲流速vH(m/s)", "1.2"),
                ("n", "指数n", "0.5"),
                ("p", "冲刷系数p", "1.5"),
            ],
        },
        "堤基冲刷-弯道最大冲深": {
            "source": "《水利大算盘》第三章第6节，式(3-30)。",
            "formula": "Hmax=[λQ/(B*d^(1/3)*sqrt(g*((γs-γ)/γ)))]^(6/7)；λ=0.64e^(3.61d/H0)",
            "params": [
                ("Q", "河道流量Q(m3/s)", "216.49"),
                ("B", "弯道水面宽B(m)", "30"),
                ("H0", "直段平均水深H0(m)", "2"),
                ("d", "河床砂平均粒径d(m)", "0.08"),
                ("gamma_w", "水重度γ(t/m3)", "1"),
                ("gamma_s", "床砂重度γs(t/m3)", "2"),
            ],
        },
        "堤基冲刷-锁坝局部冲深": {
            "source": "《水利大算盘》第三章第6节，式(3-31)。",
            "formula": "hp=0.332*q/[sqrt(d)*(h/d)^(1/6)]",
            "params": [
                ("q", "单宽流量q(m3/s/m)", "3"),
                ("d", "河床砂平均粒径d(m)", "0.08"),
                ("h", "上游水深h(m)", "2"),
            ],
        },
        "短丁坝设计": {
            "source": "《水利大算盘》第三章第7节，图3-8例题。短丁坝L=(0.2~0.3)B，间距S=(1~2)L。",
            "formula": "B=Q*n/(H^(5/3)*sqrt(J))；L=kL*B；S=kS*L",
            "params": [
                ("Q", "设计流量Q(m3/s)", "90"),
                ("H", "设计水深H(m)", "3.5"),
                ("J", "水面比降J", "0.0005"),
                ("n", "糙率n", "0.025"),
                ("length_factor", "丁坝长度系数kL", "0.3"),
                ("spacing_factor", "丁坝间距系数kS", "2"),
            ],
        },
    }

    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.LabelFrame(self, text="堤防工程书中程序")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(1, weight=1)
        ttk.Label(top, text="计算程序").grid(row=0, column=0, sticky="e", padx=(10, 4), pady=6)
        self.method = tk.StringVar(value="河道流量估算")
        method_box = ttk.Combobox(top, textvariable=self.method, values=tuple(self.METHOD_DEFS), state="readonly", width=28)
        method_box.grid(row=0, column=1, sticky="w", padx=(0, 12), pady=6)
        method_box.bind("<<ComboboxSelected>>", lambda _event: self._load_method())

        self.source_label = ttk.Label(top, text="", foreground="#555555", wraplength=980, justify=tk.LEFT)
        self.source_label.grid(row=1, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 4))
        self.formula_label = ttk.Label(top, text="", foreground="#333333", wraplength=980, justify=tk.LEFT)
        self.formula_label.grid(row=2, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 6))

        params_box = ttk.LabelFrame(top, text="输入参数：按 key=value 修改，默认值为书中例题")
        params_box.grid(row=3, column=0, columnspan=3, sticky="ew", padx=10, pady=6)
        params_box.columnconfigure(0, weight=1)
        self.params_text = ScrolledText(params_box, height=9, width=100, font=(DASHBOARD_MONO_FONT, 10))
        self.params_text.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
        self.param_note = ttk.Label(params_box, text="", foreground="#666666", wraplength=1000, justify=tk.LEFT)
        self.param_note.grid(row=1, column=0, sticky="ew", padx=8, pady=(0, 8))

        actions = ttk.Frame(top)
        actions.grid(row=4, column=0, columnspan=3, sticky="ew", padx=10, pady=(4, 0))
        self._action_button(actions, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT)
        self._action_button(actions, "恢复本方法例题", self._load_method).pack(side=tk.LEFT, padx=(12, 0))
        self._action_button(actions, "清空结果", lambda: self._write(self.output, "")).pack(side=tk.LEFT, padx=(12, 0))

        self.output = self._result_box(self)
        self._load_method()

    def _load_method(self) -> None:
        info = self.METHOD_DEFS[self.method.get()]
        self.source_label.configure(text=f"来源：{info['source']}")
        self.formula_label.configure(text=f"公式：{info['formula']}")
        lines = [f"{key}={default}" for key, _label, default in info["params"]]
        self.params_text.delete("1.0", tk.END)
        self.params_text.insert(tk.END, "\n".join(lines))
        note_lines = [f"{key}：{label}" for key, label, _default in info["params"]]
        self.param_note.configure(text="；".join(note_lines))

    def _params(self) -> dict[str, float]:
        values: dict[str, float] = {}
        for raw_line in self.params_text.get("1.0", "end-1c").splitlines():
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                raise HydrologyError(f"参数行必须按 key=value 填写：{line}")
            key, value = line.split("=", 1)
            key = key.strip()
            try:
                values[key] = float(value.strip())
            except ValueError as exc:
                raise HydrologyError(f"{key} 必须是数字") from exc
        return values

    def _require(self, values: dict[str, float], key: str) -> float:
        if key not in values:
            raise HydrologyError(f"缺少参数：{key}")
        return values[key]

    def _input_rows(self, values: dict[str, float]) -> list[list[str]]:
        info = self.METHOD_DEFS[self.method.get()]
        rows = [["参数", "输入值", "说明"]]
        for key, label, _default in info["params"]:
            value = values.get(key)
            rows.append([key, "" if value is None else f"{value:g}", label])
        return rows

    def calculate(self) -> None:
        try:
            method = self.method.get()
            values = self._params()
            p = lambda key: self._require(values, key)
            if method == "河道流量估算":
                result = calculate_river_discharge_estimate(
                    RiverDischargeEstimateInput(mean_depth=p("H"), river_width=p("B"), slope=p("J"), roughness=p("n"))
                )
                rows = [["项目", "数值", "单位"], ["河道流速v", f"{result.velocity:.2f}", "m/s"], ["河道流量Q", f"{result.flow:.2f}", "m3/s"]]
            elif method == "河道治导线宽度":
                result = calculate_training_line_width(
                    TrainingLineWidthInput(flow=p("Q"), mean_depth=p("H"), slope=p("J"), roughness=p("n"))
                )
                rows = [["项目", "数值", "单位"], ["治导线宽度B", f"{result.width:.2f}", "m"]]
            elif method == "超高和堤顶高程":
                result = calculate_manning_levee_crest(
                    ManningLeveeCrestInput(
                        flow=p("Q"),
                        bottom_width=p("B"),
                        side_slope=p("m"),
                        slope=p("J"),
                        roughness=p("n"),
                        wind_fetch_km=p("D"),
                        safety_freeboard=p("A"),
                    )
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["设计水深h", f"{result.water_depth:.3f}", "m"],
                    ["平均流速v", f"{result.velocity:.3f}", "m/s"],
                    ["波浪爬高e", f"{result.wave_runup:.3f}", "m"],
                    ["堤身高度H", f"{result.levee_height:.3f}", "m"],
                ]
            elif method == "堤身护坡厚度":
                result = calculate_revetment_thickness(
                    RevetmentThicknessInput(layers=p("N"), coefficient=p("c"), block_weight_t=p("W"), block_unit_weight_kn_m3=p("gamma_b"))
                )
                rows = [["项目", "数值", "单位"], ["护坡厚度t", f"{result.thickness:.2f}", "m"]]
            elif method == "堤身护坡块石尺寸":
                result = calculate_revetment_block_size(
                    RevetmentBlockSizeInput(
                        velocity=p("v"),
                        water_density_t_m3=p("gamma_w"),
                        stone_density_t_m3=p("gamma_s"),
                        stability_coefficient=p("c"),
                        block_length=p("a"),
                        block_width=p("b"),
                        block_height=p("h"),
                    )
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["稳定所需折算粒径d", f"{result.stable_diameter:.3f}", "m"],
                    ["实际块石折算粒径d0", f"{result.actual_equivalent_diameter:.3f}", "m"],
                    ["稳定判断", "满足" if result.is_stable else "不满足", "-"],
                ]
            elif method == "堤基冲刷-水流斜冲护岸":
                result = calculate_oblique_bank_scour(
                    ObliqueBankScourInput(
                        angle_deg=p("alpha"),
                        side_slope=p("m"),
                        soil_particle_diameter=p("d"),
                        flow=p("Q"),
                        section_area=p("omega"),
                        contracted_area=p("omega_p"),
                    )
                )
                rows = [["项目", "数值", "单位"], ["局部流速vj", f"{result.local_velocity:.2f}", "m/s"], ["局部冲深hp", f"{result.scour_depth:.2f}", "m"]]
            elif method == "堤基冲刷-水流平行护岸":
                result = calculate_parallel_bank_scour(
                    ParallelBankScourInput(
                        pre_scour_depth=p("h"),
                        average_velocity=p("vp"),
                        allowable_velocity=p("vH"),
                        exponent=p("n"),
                        scour_coefficient=p("p"),
                    )
                )
                rows = [["项目", "数值", "单位"], ["冲刷后水深hB", f"{result.post_scour_depth:.2f}", "m"], ["局部冲深hB-h", f"{result.scour_depth:.2f}", "m"]]
            elif method == "堤基冲刷-弯道最大冲深":
                result = calculate_bend_scour(
                    BendScourInput(
                        flow=p("Q"),
                        water_width=p("B"),
                        straight_average_depth=p("H0"),
                        bed_particle_diameter=p("d"),
                        water_density_t_m3=p("gamma_w"),
                        sediment_density_t_m3=p("gamma_s"),
                    )
                )
                rows = [["项目", "数值", "单位"], ["λ", f"{result.lambda_coefficient:.4f}", "-"], ["弯道最大冲深Hmax", f"{result.max_depth_from_water_surface:.2f}", "m"]]
            elif method == "堤基冲刷-锁坝局部冲深":
                result = calculate_lock_dam_scour(LockDamScourInput(unit_discharge=p("q"), bed_particle_diameter=p("d"), water_depth=p("h")))
                rows = [["项目", "数值", "单位"], ["锁坝局部冲深hp", f"{result.post_scour_depth:.3f}", "m"]]
            elif method == "短丁坝设计":
                result = calculate_short_groyne_design(
                    ShortGroyneDesignInput(
                        flow=p("Q"),
                        mean_depth=p("H"),
                        slope=p("J"),
                        roughness=p("n"),
                        length_factor=p("length_factor"),
                        spacing_factor=p("spacing_factor"),
                    )
                )
                rows = [
                    ["项目", "数值", "单位"],
                    ["主流宽度B", f"{result.training_line_width:.2f}", "m"],
                    ["短丁坝长度L", f"{result.groyne_length:.2f}", "m"],
                    ["短丁坝间距S", f"{result.groyne_spacing:.2f}", "m"],
                ]
            else:
                raise HydrologyError(f"暂未接入该方法：{method}")

            info = self.METHOD_DEFS[method]
            lines = [
                f"{method}计算书",
                "",
                "一、公式来源",
                format_fixed_table([["项目", "内容"], ["来源", info["source"]], ["公式", info["formula"]]], min_width=16),
                "",
                "二、输入参数",
                format_fixed_table(self._input_rows(values), min_width=16),
                "",
                "三、计算结果",
                format_fixed_table(rows, min_width=16),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class RiverEvolutionCalculationTab(ttk.Frame, FormMixin):
    PARAM_DEFS = {
        "bankfull_width_b": ("平滩水面/河宽B(m)", "144.83"),
        "average_depth_h": ("平均水深h(m)", "3.93"),
        "sediment_d": ("床沙平均粒径d(mm)", "34.5"),
        "formative_flow_q": ("造床/平滩流量Q(m³/s)", "418"),
        "slope_j": ("比降J(小数)", "0.00111"),
        "stable_width_a": ("稳定河宽系数A(按右侧选取)", "1.0"),
        "low_water_width_b": ("枯水河宽b(m)", "98.56"),
        "longitudinal_phi_h1": ("纵向稳定系数φh₁", "1.69"),
        "lateral_phi_b1": ("横向稳定系数φb₁", "0.40"),
        "inlet_velocity_v0": ("进口断面流速v₀(m/s)", "1.0"),
        "outlet_velocity_vi": ("出口断面流速vᵢ(m/s)", "1.0"),
        "inlet_depth_h0": ("进口断面水深h₀", "3.93"),
        "outlet_depth_hi": ("出口断面水深hᵢ", "3.82"),
    }
    PARAM_NOTES = {
        "bankfull_width_b": "B：平滩流量或相当造床流量下的水面宽/河宽，单位m。",
        "average_depth_h": "h：平滩流量或相当造床流量下的平均水深，单位m。",
        "sediment_d": "d：床沙平均粒径，单位mm。本模块按报告第3章口径录入，不再自动换算。",
        "formative_flow_q": "Q：造床流量或平滩流量，单位m³/s。",
        "slope_j": "J：河道比降，按小数录入；1‰填0.001，1.11‰填0.00111，不直接填1.11。",
        "stable_width_a": "A：稳定河宽系数，按河段类型选取：山区0.75~0.90，山麓0.90~1.00，中游1.00~1.10，下游1.10~1.70。",
        "low_water_width_b": "b：枯水河宽，单位m。",
        "longitudinal_phi_h1": "φh₁：纵向稳定系数，可由希尔兹数倒数法先算得后填入。",
        "lateral_phi_b1": "φb₁：横向稳定系数，可由横向稳定方法一先算得后填入。",
        "inlet_velocity_v0": "v₀：进口断面平均流速，单位m/s。",
        "outlet_velocity_vi": "vᵢ：出口断面平均流速，单位m/s。",
        "inlet_depth_h0": "h₀：进口断面水深，单位m。",
        "outlet_depth_hi": "hᵢ：出口断面水深，单位m。",
    }
    METHOD_UI = {
        "河相关系-格鲁什科夫宽深公式": {
            "params": ("bankfull_width_b", "average_depth_h"),
            "formula": "ζ = sqrt(B) / h",
            "target": "河相系数ζ",
            "note": "用于按平滩水面宽和平均水深判断河相关系。河相系数参考：蜿蜒型约2~4，顺直过渡型约8~12，游荡型约20~30。",
        },
        "河相关系-量纲分析公式": {
            "params": ("bankfull_width_b", "sediment_d", "average_depth_h"),
            "formula": "ζ = sqrt(B·d) / h",
            "target": "河相系数ζ",
            "note": "用于结合床沙粒径进行河相定性分析。床沙平均粒径d按mm输入。",
        },
        "稳定河宽-阿尔图宁公式": {
            "params": ("formative_flow_q", "slope_j", "stable_width_a"),
            "formula": "B = A·Q^0.5 / J^0.2",
            "target": "稳定河宽B",
            "note": "用于估算稳定河宽。J按小数输入，例如1.11‰输入0.00111；A按河段类型选取：山区0.75~0.90，山麓0.90~1.00，中游1.00~1.10，下游1.10~1.70。",
        },
        "纵向稳定系数-希尔兹数倒数法": {
            "params": ("sediment_d", "average_depth_h", "slope_j"),
            "formula": "φh₁ = d / (h·J)",
            "target": "纵向稳定系数φh₁",
            "note": "用于河床纵向稳定性分析。d按mm输入，h按m输入，J按小数输入；正式成果应按报告公式口径复核。",
        },
        "纵向稳定系数-洛赫庆系数法": {
            "params": ("sediment_d", "slope_j"),
            "formula": "φh₂ = d / J",
            "target": "纵向稳定系数φh₂",
            "note": "用于河床纵向稳定性分析。d按mm输入，J按小数输入；正式成果应按报告公式口径复核。",
        },
        "横向稳定系数-方法一": {
            "params": ("formative_flow_q", "slope_j", "bankfull_width_b"),
            "formula": "φb₁ = Q^0.5 / (J^0.2·B)",
            "target": "横向稳定系数φb₁",
            "note": "用于按平滩流量、比降和平滩河宽判断横向稳定性。J按小数输入；横向稳定系数越大，河岸相对越稳定。",
        },
        "横向稳定系数-方法二": {
            "params": ("low_water_width_b", "bankfull_width_b"),
            "formula": "φb₂ = b / B",
            "target": "横向稳定系数φb₂",
            "note": "用于按枯水河宽和平滩河宽比值判断横向稳定性。",
        },
        "综合稳定系数": {
            "params": ("longitudinal_phi_h1", "lateral_phi_b1"),
            "formula": "φ = φh₁·(φb₁)^2",
            "target": "综合稳定系数φ",
            "note": "用于将纵向稳定和横向稳定综合为一个稳定性指标。",
        },
        "冲淤判别系数": {
            "params": ("inlet_velocity_v0", "outlet_velocity_vi", "inlet_depth_h0", "outlet_depth_hi"),
            "formula": "ψ = (v₀^(3/2)·hᵢ) / (vᵢ^(3/2)·h₀)",
            "target": "冲淤判别系数ψ",
            "note": "用于进出口断面流速和水深变化下的冲淤趋势判断。ψ<0.95为冲刷趋势，ψ>1.05为淤积趋势。",
        },
    }

    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.entries: dict[str, tk.Entry] = {}
        self.param_values = {key: default for key, (_label, default) in self.PARAM_DEFS.items()}
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.LabelFrame(self, text="河道演变计算")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        selector = ttk.Frame(top)
        selector.grid(row=0, column=0, sticky="ew", padx=10, pady=(8, 4))
        selector.columnconfigure(1, weight=1)
        ttk.Label(selector, text="计算方法").grid(row=0, column=0, padx=(0, 6), pady=6, sticky="w")
        self.method_var = tk.StringVar(value=RIVER_EVOLUTION_METHODS[0])
        self.method_combo = ttk.Combobox(selector, textvariable=self.method_var, values=RIVER_EVOLUTION_METHODS, state="readonly", width=42)
        self.method_combo.grid(row=0, column=1, padx=(0, 12), pady=6, sticky="w")
        self.method_combo.bind("<<ComboboxSelected>>", lambda _event: self._refresh_note())
        self._action_button(selector, "开始计算", self.calculate, bg="#2b788a", fg="white").grid(row=0, column=2, padx=(8, 0), pady=6, sticky="e")

        body = ttk.Frame(top)
        body.grid(row=1, column=0, sticky="ew", padx=10, pady=(2, 10))
        body.columnconfigure(0, weight=1)
        body.columnconfigure(1, weight=1)

        self.param_box = ttk.LabelFrame(body, text="当前方法所需参数")
        self.param_box.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
        for col in range(4):
            self.param_box.columnconfigure(col, weight=1 if col % 2 == 1 else 0)

        formula_box = ttk.LabelFrame(body, text="当前方法公式与选用说明")
        formula_box.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
        formula_box.columnconfigure(0, weight=1)
        self.formula_title_var = tk.StringVar()
        ttk.Label(formula_box, textvariable=self.formula_title_var, font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(
            row=0, column=0, padx=10, pady=(10, 4), sticky="w"
        )
        self.formula_text = RichTableText(formula_box, height=9)
        self.formula_text.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="ew")
        self.formula_text.configure(state="disabled")

        self.output = self._result_box(self)
        self._refresh_note()

    def _refresh_note(self) -> None:
        for key, entry in self.entries.items():
            self.param_values[key] = entry.get().strip()
        for child in self.param_box.winfo_children():
            child.destroy()
        self.entries.clear()

        method = self.method_var.get()
        meta = self.METHOD_UI.get(method, {})
        params = meta.get("params", ())
        for idx, key in enumerate(params):
            label, default = self.PARAM_DEFS[key]
            row, col = divmod(idx, 2)
            ttk.Label(self.param_box, text=label).grid(row=row, column=col * 2, padx=(10, 4), pady=7, sticky="e")
            entry = ttk.Entry(self.param_box, width=16)
            entry.insert(0, self.param_values.get(key, default))
            entry.grid(row=row, column=col * 2 + 1, padx=(0, 12), pady=7, sticky="w")
            self.entries[key] = entry

        self.formula_title_var.set(f"{method}：{meta.get('target', '')}")
        param_notes = "\n".join(f"- {self.PARAM_NOTES[key]}" for key in params)
        formula_lines = [
            f"计算公式：{meta.get('formula', '')}",
            "",
            f"计算成果：{meta.get('target', '')}",
            "",
            "参数口径：",
            param_notes,
            "",
            f"选用说明：{meta.get('note', '')}",
        ]
        self.formula_text.configure(state="normal")
        self.formula_text.delete("1.0", tk.END)
        self.formula_text.insert(tk.END, "\n".join(formula_lines))
        self.formula_text.configure(state="disabled")

    def _selected_input(self) -> RiverEvolutionInput:
        values = {key: 0.0 for key in self.PARAM_DEFS}
        for key, entry in self.entries.items():
            label, _default = self.PARAM_DEFS[key]
            values[key] = self._float(entry, label)
            self.param_values[key] = entry.get().strip()
        return RiverEvolutionInput(method=self.method_var.get(), **values)

    def _selected_input_rows(self) -> list[list[str]]:
        rows = [["参数", "数值"]]
        for key in self.METHOD_UI.get(self.method_var.get(), {}).get("params", ()):
            label, _default = self.PARAM_DEFS[key]
            rows.append([label, self.entries[key].get().strip()])
        return rows

    def _selected_param_note_rows(self) -> list[list[str]]:
        rows = [["参数", "单位/输入口径"]]
        for key in self.METHOD_UI.get(self.method_var.get(), {}).get("params", ()):
            label, _default = self.PARAM_DEFS[key]
            rows.append([label, self.PARAM_NOTES[key]])
        return rows

    def calculate(self) -> None:
        try:
            result = calculate_river_evolution(self._selected_input())
            input_rows = self._selected_input_rows()
            result_rows = [
                ["项目", "数值", "单位"],
                [result.value_name, f"{result.value:.4f}", result.unit],
                ["判断/说明", result.judgement or result.note or "见公式说明", "-"],
            ]
            lines = [
                "河道演变计算",
                "",
                "一、采用公式",
                format_fixed_table([["计算方法", result.method], ["公式", result.formula]], min_width=20),
                "",
                "二、输入参数",
                format_fixed_table(input_rows, min_width=20),
                "",
                "三、参数口径与选取",
                format_fixed_table(self._selected_param_note_rows(), min_width=24),
                "",
                "四、计算成果",
                format_fixed_table(result_rows, min_width=20),
                "",
                "五、使用说明",
                "本模块按报告第3章河道演变相关公式整理。床沙平均粒径d按mm输入；比降J按小数输入，‰值需除以1000。"
                + (f" {result.note}" if result.note else ""),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class BridgeOpeningCalculationTab(ttk.Frame, FormMixin):
    GENERAL_SEGMENT_METHOD = "一般河段公式（按表7.2.1取Kq、n₃）"
    UI_METHODS = [
        "峡谷河段",
        GENERAL_SEGMENT_METHOD,
        "宽滩河段",
        "滩、槽难分的不稳定河段",
    ]
    PARAM_DEFS = {
        "general_segment_type": ("河段分类（表7.2.1）", "开阔、顺直微弯河段"),
        "design_flow_qp": ("设计流量Qₚ(m³/s)", "1000"),
        "channel_flow_qc": ("河槽流量Qc(m³/s)", "800"),
        "channel_width_bc": ("河槽宽度Bc(m)", "120"),
        "average_unit_discharge_qc": ("河槽平均单宽流量qc(m³/(s·m))", "8.0"),
        "floodplain_flow_qt": ("河滩流量Qt(m³/s)", "300"),
        "annual_mean_max_flow_qbar": ("年最大流量均值Q̄(m³/s)", "500"),
        "bed_sediment_dbar_mm": ("泥沙平均粒径d̄(mm)", "2.0"),
        "flood_flow_q2_percent": ("2%洪水流量Q₂%(m³/s)", "1200"),
    }
    PARAM_NOTES = {
        "general_segment_type": "按表7.2.1选择河段分类，程序自动带入Kq和n₃。",
        "design_flow_qp": "Qₚ：设计流量，单位m³/s。",
        "channel_flow_qc": "Qc：河槽流量，单位m³/s。",
        "channel_width_bc": "Bc：河槽宽度，单位m。",
        "average_unit_discharge_qc": "qc：河槽平均单宽流量，单位m³/(s·m)。",
        "floodplain_flow_qt": "Qt：河滩流量，单位m³/s。",
        "annual_mean_max_flow_qbar": "Q̄：年最大流量均值，单位m³/s。",
        "bed_sediment_dbar_mm": "d̄：河床泥沙平均粒径，界面按mm输入，程序换算为m后代入原式。",
        "flood_flow_q2_percent": "Q₂%：频率为2%的洪水流量，单位m³/s。",
    }
    METHOD_UI = {
        "峡谷河段": {
            "params": (),
            "formula": "按河床地形布孔，不宜压缩河槽，可不作桥孔最小净长度计算。",
            "target": "桥孔布设控制说明",
            "note": "峡谷河段优先结合河床地形、两岸地形、路线条件、通航和漂浮物通过要求确定桥孔布设。",
        },
        GENERAL_SEGMENT_METHOD: {
            "params": ("general_segment_type", "design_flow_qp", "channel_flow_qc", "channel_width_bc"),
            "formula": "Lj = Kq·(Qₚ/Qc)^n₃·Bc",
            "target": "桥孔最小净长度Lj",
            "note": "开阔、顺直微弯河段，分汊、弯曲河段，滩、槽可分的不稳定河段公式一致，仅Kq和n₃按表7.2.1取值。",
        },
        "宽滩河段": {
            "params": ("design_flow_qp", "channel_flow_qc", "floodplain_flow_qt", "average_unit_discharge_qc"),
            "formula": "Lj = Qₚ/(β·qc)，β = 1.19·(Qc/Qt)^0.10",
            "target": "桥孔最小净长度Lj",
            "note": "适用于宽滩河段。qc为河槽平均单宽流量，单位m³/(s·m)。",
        },
        "滩、槽难分的不稳定河段": {
            "params": ("design_flow_qp", "annual_mean_max_flow_qbar", "bed_sediment_dbar_mm", "flood_flow_q2_percent"),
            "formula": "Lj = Cp·B₀；B₀ = 16.07·(Q̄/d̄^0.3)^0.24；Cp = (Qₚ/Q₂%)^0.33",
            "target": "桥孔最小净长度Lj",
            "note": "界面d̄按mm输入，程序自动换算为m后代入B₀原式。",
        },
    }

    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.entries: dict[str, tk.Entry] = {}
        self.param_values = {key: default for key, (_label, default) in self.PARAM_DEFS.items()}
        self.segment_type_var = tk.StringVar(value="开阔、顺直微弯河段")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        top = ttk.LabelFrame(self, text="桥孔最小净长度计算")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
        top.columnconfigure(0, weight=1)

        selector = ttk.Frame(top)
        selector.grid(row=0, column=0, sticky="ew", padx=10, pady=(8, 4))
        selector.columnconfigure(1, weight=1)
        ttk.Label(selector, text="河段类型").grid(row=0, column=0, padx=(0, 6), pady=6, sticky="w")
        self.method_var = tk.StringVar(value=self.GENERAL_SEGMENT_METHOD)
        self.method_combo = ttk.Combobox(selector, textvariable=self.method_var, values=self.UI_METHODS, state="readonly", width=38)
        self.method_combo.grid(row=0, column=1, padx=(0, 12), pady=6, sticky="w")
        self.method_combo.bind("<<ComboboxSelected>>", lambda _event: self._refresh_note())
        self._action_button(selector, "开始计算", self.calculate, bg="#1d7fc1", fg="white").grid(row=0, column=2, padx=(8, 0), pady=6, sticky="e")

        body = ttk.Frame(top)
        body.grid(row=1, column=0, sticky="ew", padx=10, pady=(2, 10))
        body.columnconfigure(0, weight=1)
        body.columnconfigure(1, weight=1)

        self.param_box = ttk.LabelFrame(body, text="当前河段类型所需参数")
        self.param_box.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
        for col in range(4):
            self.param_box.columnconfigure(col, weight=1 if col % 2 == 1 else 0)

        formula_box = ttk.LabelFrame(body, text="当前河段公式与选用说明")
        formula_box.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
        formula_box.columnconfigure(0, weight=1)
        self.formula_title_var = tk.StringVar()
        ttk.Label(formula_box, textvariable=self.formula_title_var, font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(
            row=0, column=0, padx=10, pady=(10, 4), sticky="w"
        )
        self.formula_text = RichTableText(formula_box, height=12)
        self.formula_text.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="ew")
        self.formula_text.configure(state="disabled")

        self.output = self._result_box(self)
        self._refresh_note()

    def _refresh_note(self) -> None:
        for key, entry in self.entries.items():
            self.param_values[key] = entry.get().strip()
        for child in self.param_box.winfo_children():
            child.destroy()
        self.entries.clear()

        method = self.method_var.get()
        meta = self.METHOD_UI.get(method, {})
        params = meta.get("params", ())
        if not params:
            ttk.Label(
                self.param_box,
                text="该河段按河床地形布孔，不要求填写桥孔最小净长度计算参数。",
                foreground="#666666",
                wraplength=420,
                justify=tk.LEFT,
            ).grid(row=0, column=0, columnspan=4, padx=10, pady=12, sticky="w")
        for idx, key in enumerate(params):
            label, default = self.PARAM_DEFS[key]
            row, col = divmod(idx, 2)
            ttk.Label(self.param_box, text=label).grid(row=row, column=col * 2, padx=(10, 4), pady=7, sticky="e")
            if key == "general_segment_type":
                combo = ttk.Combobox(
                    self.param_box,
                    textvariable=self.segment_type_var,
                    values=list(RIVER_SEGMENT_PARAMS),
                    state="readonly",
                    width=24,
                )
                combo.grid(row=row, column=col * 2 + 1, padx=(0, 12), pady=7, sticky="w")
                combo.bind("<<ComboboxSelected>>", lambda _event: self._refresh_note())
                continue
            entry = ttk.Entry(self.param_box, width=16)
            entry.insert(0, self.param_values.get(key, default))
            entry.grid(row=row, column=col * 2 + 1, padx=(0, 12), pady=7, sticky="w")
            self.entries[key] = entry

        self.formula_title_var.set(f"{method}：{meta.get('target', '')}")
        param_notes = "\n".join(f"- {self.PARAM_NOTES[key]}" for key in params)
        if not param_notes:
            param_notes = "- 无固定计算参数；按河床地形和控制条件综合布孔。"
        value_note = ""
        table_text = ""
        if method == self.GENERAL_SEGMENT_METHOD:
            selected_segment = self.segment_type_var.get()
            selected_params = RIVER_SEGMENT_PARAMS[selected_segment]
            value_note = f"\n当前选择：{selected_segment}，Kq={selected_params['Kq']:.2f}，n₃={selected_params['n3']:.2f}"
            table_rows = [["河段分类", "Kq", "n₃"]]
            for segment, params_row in RIVER_SEGMENT_PARAMS.items():
                marker = "→ " if segment == selected_segment else "  "
                table_rows.append([marker + segment, f"{params_row['Kq']:.2f}", f"{params_row['n3']:.2f}"])
            table_text = "\n表7.2.1  Kq、n₃值\n" + format_fixed_table(table_rows, min_width=16)
        formula_lines = [
            f"计算公式：{meta.get('formula', '')}",
            "",
            f"计算成果：{meta.get('target', '')}",
            value_note.strip(),
            table_text,
            "",
            "参数口径：",
            param_notes,
            "",
            f"选用说明：{meta.get('note', '')}",
        ]
        self.formula_text.configure(state="normal")
        self.formula_text.delete("1.0", tk.END)
        self.formula_text.insert(tk.END, "\n".join(line for line in formula_lines if line is not None))
        self.formula_text.configure(state="disabled")

    def _selected_input(self) -> BridgeOpeningInput:
        values = {
            "design_flow_qp": 0.0,
            "channel_flow_qc": 0.0,
            "channel_width_bc": 0.0,
            "average_unit_discharge_qc": 0.0,
            "floodplain_flow_qt": 0.0,
            "annual_mean_max_flow_qbar": 0.0,
            "bed_sediment_dbar": 0.0,
            "flood_flow_q2_percent": 0.0,
        }
        for key, entry in self.entries.items():
            label, _default = self.PARAM_DEFS[key]
            raw = self._float(entry, label)
            self.param_values[key] = entry.get().strip()
            if key == "bed_sediment_dbar_mm":
                values["bed_sediment_dbar"] = raw / 1000.0
            else:
                values[key] = raw
        method = self.segment_type_var.get() if self.method_var.get() == self.GENERAL_SEGMENT_METHOD else self.method_var.get()
        return BridgeOpeningInput(method=method, **values)

    def _selected_input_rows(self) -> list[list[str]]:
        rows = [["参数", "数值"]]
        rows.append(["公式类型", self.method_var.get()])
        if self.method_var.get() == self.GENERAL_SEGMENT_METHOD:
            rows.append(["河段分类", self.segment_type_var.get()])
        for key in self.METHOD_UI.get(self.method_var.get(), {}).get("params", ()):
            if key == "general_segment_type":
                continue
            label, _default = self.PARAM_DEFS[key]
            rows.append([label, self.entries[key].get().strip()])
            if key == "bed_sediment_dbar_mm":
                rows.append(["泥沙平均粒径d̄换算值(m)", f"{self._float(self.entries[key], label) / 1000.0:.6f}"])
        return rows

    def _selected_param_note_rows(self) -> list[list[str]]:
        rows = [["参数", "单位/输入口径"]]
        params = self.METHOD_UI.get(self.method_var.get(), {}).get("params", ())
        if not params:
            rows.append(["河段类型", "峡谷河段可不作桥孔最小净长度公式计算，应结合河床地形布孔。"])
        for key in params:
            label, _default = self.PARAM_DEFS[key]
            if key == "general_segment_type":
                rows.append([label, self.PARAM_NOTES[key]])
                selected_segment = self.segment_type_var.get()
                selected_params = RIVER_SEGMENT_PARAMS[selected_segment]
                rows.append(["当前Kq、n₃", f"{selected_segment}：Kq={selected_params['Kq']:.2f}，n₃={selected_params['n3']:.2f}"])
                continue
            rows.append([label, self.PARAM_NOTES[key]])
        return rows

    def calculate(self) -> None:
        try:
            result = calculate_bridge_opening(self._selected_input())
            input_rows = self._selected_input_rows()
            middle_rows = [["项目", "数值"]]
            if result.kq is not None:
                middle_rows.append(["Kq", f"{result.kq:.4f}"])
            if result.n3 is not None:
                middle_rows.append(["n₃", f"{result.n3:.4f}"])
            if result.beta is not None:
                middle_rows.append(["β", f"{result.beta:.4f}"])
            if result.basic_width_b0 is not None:
                middle_rows.append(["B₀(m)", f"{result.basic_width_b0:.3f}"])
            if result.frequency_factor_cp is not None:
                middle_rows.append(["Cp", f"{result.frequency_factor_cp:.4f}"])
            result_rows = [["成果", "数值", "单位"]]
            if result.min_clear_length_lj is None:
                result_rows.append(["桥孔最小净长度Lj", "按河床地形布孔", "-"])
            else:
                result_rows.append(["桥孔最小净长度Lj", f"{result.min_clear_length_lj:.3f}", "m"])
            rule_rows = [
                ["条文", "要点"],
                ["7.1.1", "桥孔设计应保证设计洪水以内各级洪水和泥沙安全通过，并满足通航、流冰及漂浮物通过要求。"],
                ["7.1.2", "桥孔布设应适应河段特性及演变特点，避免河床不利变形，并做到经济合理。"],
                ["7.1.3", "建桥后桥前壅水高度、流势变化和河床变形应在安全允许范围内。"],
                ["7.3", "桥孔布设应结合河段稳定性、河床变形、分流比例、防堤、通航净空和地质条件综合确定。"],
            ]
            lines = [
                "桥孔设计 - 桥孔最小净长度计算",
                "",
                "一、计算公式",
                format_fixed_table([["河段类型", result.method], ["采用公式", result.formula]], min_width=18),
                "",
                "二、输入参数",
                format_fixed_table(input_rows, min_width=18),
                "",
                "三、参数口径与选取",
                format_fixed_table(self._selected_param_note_rows(), min_width=24),
                "",
                "四、中间参数",
                format_fixed_table(middle_rows, min_width=18),
                "",
                "五、计算成果",
                format_fixed_table(result_rows, min_width=18),
                "",
                "六、桥孔布设控制要点",
                format_fixed_table(rule_rows, min_width=18),
                "",
                f"说明：{result.note}" + (" 界面中泥沙平均粒径d̄按mm输入，计算时已换算为m代入原式。" if result.method == "滩、槽难分的不稳定河段" else ""),
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class ScourCalculationTab(ttk.Frame, FormMixin):
    def __init__(self, master: tk.Widget):
        super().__init__(master)
        self.last_output = ""
        self.param_entries: dict[str, tk.Entry] = {}
        self.param_values: dict[str, dict[str, str]] = {}
        self.active_method_key = ""
        self.columnconfigure(0, weight=1)
        self.rowconfigure(2, weight=1)

        top = ttk.LabelFrame(self, text="冲刷计算公式")
        top.grid(row=0, column=0, sticky="ew", padx=12, pady=(12, 6))
        top.columnconfigure(1, weight=1)
        ttk.Label(top, text="公式分类").grid(row=0, column=0, padx=(10, 4), pady=6, sticky="e")
        self.ui_methods = [item for item in SCOUR_METHODS if item.key not in {"slt_wetland_vj", "slt_no_floodplain_vj"}]
        self.method_names = [f"{item.category} - {item.name}" for item in self.ui_methods]
        self.method_by_name = dict(zip(self.method_names, self.ui_methods))
        self.method_combo = ttk.Combobox(top, values=self.method_names, state="readonly", width=74)
        self.method_combo.current(0)
        self.active_method_key = self.method_by_name[self.method_combo.get()].key
        self.method_combo.grid(row=0, column=1, padx=(0, 10), pady=6, sticky="ew")
        self.method_combo.bind("<<ComboboxSelected>>", lambda _event: self._on_method_changed())

        helper = ttk.Frame(top)
        helper.grid(row=0, column=2, padx=(0, 10), pady=6, sticky="e")
        self._action_button(helper, "套用默认参数", self._reset_method_defaults).pack(side=tk.LEFT)
        self._action_button(helper, "开始计算", self.calculate, bg="#006b63", fg="white").pack(side=tk.LEFT, padx=(10, 0))

        body = ttk.Frame(self)
        body.grid(row=1, column=0, sticky="nsew", padx=12, pady=(0, 8))
        body.columnconfigure(0, weight=1)
        body.columnconfigure(1, weight=1)

        self.param_box = ttk.LabelFrame(body, text="本公式需填写参数")
        self.param_box.grid(row=0, column=0, sticky="nsew", padx=(0, 6))
        self.param_box.columnconfigure(1, weight=1)

        info_box = ttk.LabelFrame(body, text="公式、适用条件及参数查找表")
        info_box.grid(row=0, column=1, sticky="nsew", padx=(6, 0))
        info_box.rowconfigure(0, weight=1)
        info_box.columnconfigure(0, weight=1)
        self.info_text = RichTableText(info_box, height=18, font=(DASHBOARD_CARD_FONT, 9), table_font=(DASHBOARD_CARD_FONT, 9), wraplength=620)
        self.info_text.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)

        note = ttk.Label(
            self,
            text=(
                "说明：冲刷模块按 SLT 808-2025 与 JTG C30-2015 常用公式分类。"
                "右侧同步显示当前公式、适用条件、需要查表的参数和重复性审查结论；正式成果仍应结合规范原表、地勘资料和审查意见复核。"
            ),
            foreground="#666666",
            wraplength=1180,
            justify=tk.LEFT,
        )
        note.grid(row=3, column=0, sticky="ew", padx=12, pady=(0, 8))

        result_box = ttk.LabelFrame(self, text="计算结果")
        result_box.grid(row=2, column=0, sticky="nsew", padx=12, pady=(0, 8))
        result_box.rowconfigure(0, weight=1)
        result_box.columnconfigure(0, weight=1)
        self.output = RichTableText(result_box, height=18)
        setattr(self.output, "_hydrology_export_source", True)
        self.output.grid(row=0, column=0, sticky="nsew", padx=10, pady=(10, 6))
        export_bar = ttk.Frame(result_box)
        export_bar.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
        self._action_button(export_bar, "导出Excel", lambda: self._export_result("xlsx"), bg="#1688e8", fg="white").pack(side=tk.LEFT)
        self._action_button(export_bar, "导出Word", lambda: self._export_result("docx"), bg="#1688e8", fg="white").pack(side=tk.LEFT, padx=(10, 0))
        self._action_button(export_bar, "导出专项报告", lambda: self._export_result("html"), bg="#006b63", fg="white").pack(side=tk.LEFT, padx=(10, 0))
        self._refresh_method()

    def _current_method(self):
        return self.method_by_name[self.method_combo.get()]

    def _store_current_values(self) -> None:
        if not self.param_entries:
            return
        values = {key: entry.get() for key, entry in self.param_entries.items()}
        if "vj_mode" in values:
            raw = values["vj_mode"]
            values["vj_mode"] = "2" if raw.startswith("无滩地") else "0" if raw.startswith("手动") else "1"
        self.param_values[self.active_method_key] = values

    def _on_method_changed(self) -> None:
        self._store_current_values()
        self._refresh_method()

    def _reset_method_defaults(self) -> None:
        method = self._current_method()
        self.param_values.pop(method.key, None)
        self._refresh_method()

    def _refresh_method(self) -> None:
        method = self._current_method()
        self.active_method_key = method.key
        for child in self.param_box.winfo_children():
            child.destroy()
        self.param_entries.clear()

        saved = self.param_values.get(method.key, {})
        parameter_rows: list[list[str]] = []
        row_index = 0
        if method.key == "slt_oblique_bank":
            mode_value = saved.get("vj_mode", "1")
            mode_text = {"0": "手动输入Vj", "1": "滩地河床(A.3.4-3)", "2": "无滩地河床(A.3.4-4)"}.get(str(mode_value), "滩地河床(A.3.4-3)")
            ttk.Label(self.param_box, text="Vj计算方式").grid(row=row_index, column=0, padx=(10, 6), pady=5, sticky="e")
            mode_combo = ttk.Combobox(
                self.param_box,
                values=["滩地河床(A.3.4-3)", "无滩地河床(A.3.4-4)", "手动输入Vj"],
                state="readonly",
                width=20,
            )
            mode_combo.set(mode_text)
            mode_combo.grid(row=row_index, column=1, padx=(0, 8), pady=5, sticky="w")
            ttk.Label(self.param_box, text="选择后自动切换所需参数", foreground="#666666").grid(
                row=row_index, column=2, padx=(0, 10), pady=5, sticky="w"
            )
            mode_combo.bind("<<ComboboxSelected>>", lambda _event: self._on_scour_vj_mode_changed())
            self.param_entries["vj_mode"] = mode_combo
            parameter_rows.append(["vj_mode", "1", "1=滩地；2=无滩地；0=手动"])
            row_index += 1

        for key, desc, default in self._visible_scour_parameters(method):
            value = saved.get(key, default)
            ttk.Label(self.param_box, text=key).grid(row=row_index, column=0, padx=(10, 6), pady=5, sticky="e")
            entry = ttk.Entry(self.param_box, width=18)
            entry.insert(0, value)
            entry.grid(row=row_index, column=1, padx=(0, 8), pady=5, sticky="w")
            ttk.Label(self.param_box, text=desc, foreground="#666666", wraplength=420).grid(
                row=row_index, column=2, padx=(0, 10), pady=5, sticky="w"
            )
            self.param_entries[key] = entry
            parameter_rows.append([key, default, desc])
            row_index += 1

        self.info_text.configure(state=tk.NORMAL)
        self.info_text.delete("1.0", tk.END)
        self.info_text.insert(tk.END, self._method_info_text(method, parameter_rows))
        self.info_text.configure(state=tk.DISABLED)

    def _visible_scour_parameters(self, method):
        if method.key != "slt_oblique_bank":
            return method.parameters
        saved = self.param_values.get(method.key, {})
        mode = str(saved.get("vj_mode", "1"))
        if mode not in {"0", "1", "2"}:
            mode = "1"
        common = {"alpha_deg", "m", "d", "g"}
        if mode == "0":
            keys = common | {"Vj"}
        elif mode == "2":
            keys = common | {"Q", "W", "Wp"}
        else:
            keys = common | {"Q1", "B1", "H1"}
        return tuple(item for item in method.parameters if item[0] in keys)

    def _on_scour_vj_mode_changed(self) -> None:
        combo = self.param_entries.get("vj_mode")
        if combo is None:
            return
        text = combo.get()
        code = "1"
        if text.startswith("无滩地"):
            code = "2"
        elif text.startswith("手动"):
            code = "0"
        current = {key: entry.get() for key, entry in self.param_entries.items() if key != "vj_mode"}
        current["vj_mode"] = code
        self.param_values[self.active_method_key] = current
        self._refresh_method()

    def _method_info_text(self, method, parameter_rows: list[list[str]]) -> str:
        lines = [
            "一、当前公式",
            format_fixed_table(
                [
                    ["项目", "内容"],
                    ["分类", method.category],
                    ["公式名称", method.name],
                    ["来源", method.source],
                    ["计算式", method.formula],
                    ["备注", method.note or "-"],
                ],
                min_width=14,
            ),
            "",
            "二、需填写参数",
            format_fixed_table([["参数", "默认值", "说明"], *parameter_rows], min_width=14),
            "",
            "三、参数查找表及取值说明",
            self._method_lookup_text(method),
            "",
            "四、重复性审查",
            self._duplicate_audit_text(method.key),
        ]
        return "\n".join(lines)

    def _method_lookup_text(self, method) -> str:
        key = method.key
        blocks: list[str] = []
        if key == "slt_parallel_bank":
            blocks.extend(
                [
                    "vH 为河床允许不冲流速。它不是本公式直接算出的结果，一般按河床土质、粒径、水流挟沙特性和相关水力计算资料选取；资料不足时可先参考不冲刷流速表，再按项目审查意见复核。",
                    "JTG C30-2015 表8.3.1-3：水深1m时非黏性土不冲刷流速 VH1（可作为vH取值参考，正式取值仍按河床条件折算复核）",
                    format_fixed_table(
                        [["河床泥沙", "平均粒径d(mm)", "VH1(m/s)"], *[list(row) for row in SCOUR_NONCOHESIVE_VH1_TABLE]],
                        min_width=14,
                    ),
                ]
            )
        if key == "slt_oblique_bank":
            blocks.extend(
                [
                    "A.3.4-2 的 Vj 已合并在本公式内计算：选择滩地河床时用 A.3.4-3；选择无滩地河床时用 A.3.4-4；也可手动输入 Vj。",
                    format_fixed_table(
                        [
                            ["Vj方式", "计算式", "所需参数"],
                            ["滩地河床", "Vj=Q1/(B1*H1)*2β/(1+β)", "Q1、B1、H1、α"],
                            ["无滩地河床", "Vj=Q/(W-Wp)", "Q、W、Wp"],
                            ["手动输入", "Vj=输入值", "Vj"],
                        ],
                        min_width=18,
                    ),
                    "表A.3.4-2 β 值表（按水流方向与岸坡夹角线性插值）",
                    format_fixed_table(
                        [["夹角α(°)", "β"], *[[f"{angle:g}", f"{beta:g}"] for angle, beta in SCOUR_BETA_BY_ANGLE]],
                        min_width=12,
                    ),
                    "表A.3.4-1 黏性土当量粒径d取值（单位：cm；用于坡脚土壤计算粒径换算，输入框d单位为m）",
                    format_fixed_table(
                        [["土性", "空隙比", "干容重(kN/m3)", "黏土/重黏土", "轻黏壤土", "黄土"], *[list(row) for row in SCOUR_CLAY_EQUIVALENT_PARTICLE_TABLE]],
                        min_width=14,
                    ),
                ]
            )
        if key in {"jtg_64_1_nonc_channel", "jtg_64_2_nonc_simplified"}:
            blocks.extend(
                [
                    "E 系数按汛期含沙量 S 自动选取。规范表注：S 为历年汛期6、7、8、9月中选出的年最大月平均含沙量的算术平均值，单位 kg/m3。",
                    format_fixed_table(
                        [["汛期含沙量S(kg/m3)", "E"], ["S < 1.0", "0.46"], ["1.0 ≤ S ≤ 10", "0.66"], ["S > 10", "0.86"]],
                        min_width=18,
                    ),
                    "含沙量不是冲刷公式内部推导量，通常来自水文泥沙资料统计。软件已按S自动选E；如以后有逐年月平均含沙量资料，可再扩展批量统计工具。",
                    "A=(sqrt(Bd)/H)^0.15。64-1、64-2均按该式自动计算A；当河槽宽浅、游荡或变迁且Bd/H不易确定时，按规范说明可控制A≤1.8。",
                ]
            )
        if key == "slt_bridge_nonc_floodplain":
            blocks.extend(
                [
                    "JTG C30-2015 表8.3.1-3：水深1m时非黏性土不冲刷流速 VH1",
                    format_fixed_table(
                        [["河床泥沙", "平均粒径d(mm)", "VH1(m/s)"], *[list(row) for row in SCOUR_NONCOHESIVE_VH1_TABLE]],
                        min_width=14,
                    ),
                ]
            )
        if key == "jtg_65_nonc_pier_auto":
            blocks.extend(
                [
                    "该项为65-1修正式/SLT A.3.5-8~A.3.5-12口径，程序自动计算v0、Kη、v0'、n，并按 v≤v0 或 v>v0 采用不同分支。",
                    format_fixed_table(
                        [
                            ["判别条件", "采用公式"],
                            ["v ≤ v0", "hb = Kξ*Kη*B1^0.6*(v-v0')"],
                            ["v > v0", "hb = Kξ*Kη*B1^0.6*(v-v0')*((v-v0')/(v0-v0'))^n"],
                        ],
                        min_width=18,
                    ),
                    "Kξ为墩形系数，应按墩形查规范附录表或审查取值。",
                ]
            )
        if key == "jtg_65_2_nonc_pier_original":
            blocks.extend(
                [
                    "JTG C30 65-2 原式已单独列出，避免与65-1修正式混用。程序自动计算Kη2、v0、v0'、n2。",
                    format_fixed_table(
                        [
                            ["判别条件", "采用公式"],
                            ["v ≤ v0", "hb=Kξ*Kη2*B1^0.6*hp^0.15*((v-v0')/v0)"],
                            ["v > v0", "hb=Kξ*Kη2*B1^0.6*hp^0.15*((v-v0')/v0)^n2"],
                        ],
                        min_width=18,
                    ),
                ]
            )
        if key == "slt_pier_cohesive_auto":
            blocks.extend(
                [
                    "粘性土桥墩局部冲刷自动判别",
                    format_fixed_table(
                        [
                            ["判别条件", "采用公式"],
                            ["hp/B1 ≥ 2.5", "A.3.5-13：hb=0.83*Kξ*B1^0.6*IL^1.25*v"],
                            ["hp/B1 < 2.5", "A.3.5-14：hb=0.55*Kξ*B1^0.6*hp^0.1*IL*v"],
                        ],
                        min_width=18,
                    ),
                ]
            )
        if key == "slt_groyne_non_submerged":
            blocks.extend(
                [
                    "A.3.4-5 参数说明：第一个参数为 α（水流轴线与丁坝轴线交角），不是英文字母a。上挑丁坝α>90°时取tan(α/2)=1。",
                    "丁坝修正系数",
                    format_fixed_table(
                        [
                            ["系数", "计算式", "说明"],
                            ["K1", "exp(-5.1*sqrt(v²/(g*l)))", "与丁坝在水流法线上的投影长度l有关"],
                            ["K2", "exp(-0.2*m)", "与丁坝边坡坡率m有关"],
                        ],
                        min_width=18,
                    ),
                ]
            )
        if key == "slt_groyne_fine_bed":
            blocks.append("A.3.4-8 中 α 为水流与丁坝交角；本界面已统一标注为 alpha_deg/α，输入单位为度。")
        if key in {"slt_bridge_cohesive_channel", "slt_bridge_cohesive_floodplain"}:
            blocks.append("粘性土一般冲刷公式中的 IL 为冲刷范围内粘性土样液性指数，应由地勘试验或审查取值确定。")
        if not blocks:
            blocks.append("本公式暂无独立查表项，按左侧参数说明输入并结合规范条文复核。")
        return "\n".join(blocks)

    def _duplicate_audit_text(self, method_key: str) -> str:
        common = "已核查当前冲刷模块公式清单：未发现完全相同计算对象、完全相同计算式、完全相同适用条件的重复入口。"
        method_notes = {
            "jtg_64_1_nonc_channel": "64-1修正式使用桥下流量、桥孔净宽、桥下最大/平均水深、粒径和含沙量系数，适合具备桥下断面资料时采用。",
            "jtg_64_2_nonc_simplified": "64-2简化式使用天然河槽/河滩流量、桥孔压缩与墩阻水面积；程序自动计算Q2和Ad。lambda须由设计水位下桥墩阻水面积计算。",
            "slt_oblique_bank": "A.3.4-3与A.3.4-4已合并为A.3.4-2内部的Vj计算方式，不再作为独立公式入口显示。",
            "slt_wetland_vj": "滩地 Vj 现为A.3.4-2内部辅助计算，不再单独显示。",
            "slt_no_floodplain_vj": "无滩地 Vj 现为A.3.4-2内部辅助计算，不再单独显示。",
            "jtg_65_nonc_pier_auto": "该项为65-1修正式/SLT A.3.5-8~12口径；65-2原式已另列，不是重复项。",
            "jtg_65_2_nonc_pier_original": "该项为65-2原式；与65-1修正式公式结构不同，保留为独立入口便于复核。",
            "slt_pier_cohesive_auto": "A.3.5-13 与 A.3.5-14 已按 hp/B1 自动判别，不作为重复入口。",
        }
        return common + "\n" + method_notes.get(method_key, "同类公式按计算对象或适用条件分开保留，便于使用者按资料条件选择。")

    def _read_scour_params(self) -> dict[str, float]:
        params: dict[str, float] = {}
        for key, entry in self.param_entries.items():
            raw = entry.get().strip()
            if not raw:
                raise HydrologyError(f"参数 {key} 不能为空")
            if key == "vj_mode":
                if raw.startswith("无滩地"):
                    params[key] = 2.0
                elif raw.startswith("手动"):
                    params[key] = 0.0
                else:
                    params[key] = 1.0
                continue
            try:
                params[key] = float(raw)
            except ValueError as exc:
                raise HydrologyError(f"参数 {key} 必须是数字：{raw}") from exc
        return params

    def calculate(self) -> None:
        try:
            method = self._current_method()
            params = self._read_scour_params()
            self.param_values[method.key] = {key: entry.get() for key, entry in self.param_entries.items()}
            result = calculate_scour(ScourCalculationInput(method.key, params))
            formula_rows = [
                ["项目", "内容"],
                ["分类", result.category],
                ["公式名称", result.method_name],
                ["来源", result.source],
                ["计算式", result.formula],
                ["备注", result.note or "-"],
            ]
            input_rows = [["参数", "输入值", "说明"]]
            method_param_desc = {key: desc for key, desc, _default in method.parameters}
            for param_key, value in params.items():
                input_rows.append([param_key, f"{value:g}", method_param_desc.get(param_key, "")])
            parameter_rows = [["参数", "数值", "说明"], *result.rows]
            result_rows = [
                ["成果项目", "数值", "单位"],
                [result.result_name, f"{result.result_value:.6g}", result.unit],
            ]
            lines = [
                "冲刷计算",
                "",
                "一、公式分类与重复性审查",
                format_fixed_table(formula_rows, min_width=16),
                self._duplicate_audit_text(method.key),
                "",
                "二、输入参数",
                format_fixed_table(input_rows, min_width=14),
                "",
                "三、参数查找表及取值说明",
                self._method_lookup_text(method),
                "",
                "四、计算中间量",
                format_fixed_table(parameter_rows, min_width=14),
                "",
                "五、计算成果",
                format_fixed_table(result_rows, min_width=16),
                "",
                "说明：本窗口用于按规范经验式快速计算冲刷深度或局部冲刷流速；重要工程、敏感区域或冲刷影响较大时，应按导则要求结合数值模拟、河工模型或实测资料复核。",
            ]
            self._write(self.output, "\n".join(lines))
        except Exception as exc:
            messagebox.showerror("计算错误", str(exc))


class HydrologyApp(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.withdraw()
        _initialize_dashboard_fonts()
        self.title("水文计算软件 V6.0（Python转换版）")
        _apply_window_icon(self)
        self.geometry("1320x820")
        self.minsize(1180, 720)
        self.version_info = load_version_info()
        self.membership_state = self._authenticate_membership()
        if self.membership_state is None:
            self.destroy()
            return
        default_plan = default_membership_plan(self.membership_state)
        self.member_plan_var = tk.StringVar(value=default_plan)
        self.member_pay_provider_var = tk.StringVar(value="wechat")
        self.member_payment_status_var = tk.StringVar()
        self.member_current_order_no = ""
        self.member_current_order_server_url = ""
        self.member_pay_poll_remaining = 0
        self._calculation_usage_reported: set[str] = set()
        menu = tk.Menu(self)
        file_menu = tk.Menu(menu, tearoff=0)
        file_menu.add_command(label="退出登录/切换账号", command=self._logout_membership)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.destroy)
        menu.add_cascade(label="文件", menu=file_menu)
        data_menu = tk.Menu(menu, tearoff=0)
        data_menu.add_command(label="打开资料目录", command=self.open_data_folder)
        data_menu.add_command(label="打开全公式新人例题验收报告", command=self.open_formula_audit_report)
        data_menu.add_command(label="打开皮尔逊Ⅲ型曲线模比系数表", command=self.open_pearson_table)
        data_menu.add_command(label="P-III计算公式说明", command=self.show_pearson_help)
        data_menu.add_separator()
        self._add_method_data_menu(
            data_menu,
            "铁一院两所法",
            "铁一院法",
            [("公式、参数选取表", self.open_tieyuan_formula_tables)],
        )
        self._add_method_data_menu(
            data_menu,
            "中小河流经验公式法",
            "中小河流经验公式法",
            [("分区公式表", self.open_small_river_formula_tables)],
        )
        self._add_method_data_menu(data_menu, "水文面积比拟法", "水文面积比拟法", [])
        self._add_method_data_menu(data_menu, "双对数内插法", "双对数内插法", [])
        self._add_method_data_menu(data_menu, "洪水合成法", "洪水合成法", [])
        self._add_method_data_menu(data_menu, "水库调洪演算", "水库调洪演算", [])
        self._add_method_data_menu(
            data_menu,
            "曼宁公式水位计算",
            "曼宁公式水位计算",
            [("糙率表", self.open_manning_roughness_tables)],
        )
        self._add_method_data_menu(data_menu, "能量方程水位计算", "能量方程水位计算", [])
        self._add_method_data_menu(data_menu, "多断面能量方程法（HEC标准步法）", "HEC水位计算", [])
        self._add_method_data_menu(data_menu, "河道演变计算", "河道演变计算", [])
        self._add_method_data_menu(data_menu, "桥孔计算", "桥孔计算", [])
        self._add_method_data_menu(
            data_menu,
            "堰流公式计算水位",
            "堰流公式计算水位",
            [
                ("堰流计算程序Excel", self.open_weir_excel),
                ("水利大算盘PDF", self.open_weir_pdf),
            ],
        )
        self._add_method_data_menu(
            data_menu,
            "地区经验公式法",
            "地区经验公式法",
            [("地区公式Excel", self.open_regional_formula_excel)],
        )
        self._add_method_data_menu(
            data_menu,
            "瞬时单位线法",
            "瞬时单位线法",
            [
                ("雨型分配表、入渗率表", self.open_instant_unit_line_tables),
                ("图集公式提取PDF", self.open_instant_formula_pdf),
            ],
        )
        self._add_method_data_menu(
            data_menu,
            "推理公式法",
            "推理公式法",
            [("公式、参数表、汇流图", self.open_rational_formula_tables)],
        )
        self._add_method_data_menu(data_menu, "壅水计算", "壅水计算", [])
        self._add_method_data_menu(data_menu, "波浪高度计算", "波浪高度计算", [])
        self._add_method_data_menu(data_menu, "风壅水位计算", "风壅水位计算", [])
        self._add_method_data_menu(data_menu, "冲刷计算", "冲刷计算", [])
        menu.add_cascade(label="资料", menu=data_menu)
        help_menu = tk.Menu(menu, tearoff=0)
        tieyuan_help = tk.Menu(help_menu, tearoff=0)
        tieyuan_help.add_command(label="方法说明", command=lambda: self.show_method_help("铁一院两所法"))
        tieyuan_help.add_command(label="公式图片", command=self.open_tieyuan_formula_tables)
        help_menu.add_cascade(label="铁一院两所法", menu=tieyuan_help)
        small_river_help = tk.Menu(help_menu, tearoff=0)
        small_river_help.add_command(label="方法说明", command=lambda: self.show_method_help("中小河流经验公式法"))
        small_river_help.add_command(label="分区公式表", command=self.open_small_river_formula_tables)
        help_menu.add_cascade(label="中小河流经验公式法", menu=small_river_help)
        area_help = tk.Menu(help_menu, tearoff=0)
        area_help.add_command(label="方法说明", command=lambda: self.show_method_help("水文面积比拟法"))
        help_menu.add_cascade(label="水文面积比拟法", menu=area_help)
        double_log_help = tk.Menu(help_menu, tearoff=0)
        double_log_help.add_command(label="方法说明", command=lambda: self.show_method_help("双对数内插法"))
        help_menu.add_cascade(label="双对数内插法", menu=double_log_help)
        flood_synthesis_help = tk.Menu(help_menu, tearoff=0)
        flood_synthesis_help.add_command(label="方法说明", command=lambda: self.show_method_help("洪水合成法"))
        help_menu.add_cascade(label="洪水合成法", menu=flood_synthesis_help)
        reservoir_help = tk.Menu(help_menu, tearoff=0)
        reservoir_help.add_command(label="方法说明", command=lambda: self.show_method_help("水库调洪演算"))
        help_menu.add_cascade(label="水库调洪演算", menu=reservoir_help)
        river_evolution_help = tk.Menu(help_menu, tearoff=0)
        river_evolution_help.add_command(label="方法说明", command=lambda: self.show_method_help("河道演变计算"))
        help_menu.add_cascade(label="河道演变计算", menu=river_evolution_help)
        bridge_help = tk.Menu(help_menu, tearoff=0)
        bridge_help.add_command(label="方法说明", command=lambda: self.show_method_help("桥孔计算"))
        help_menu.add_cascade(label="桥孔计算", menu=bridge_help)
        weir_help = tk.Menu(help_menu, tearoff=0)
        weir_help.add_command(label="方法说明", command=lambda: self.show_method_help("堰流公式计算水位"))
        weir_help.add_command(label="堰流计算程序Excel", command=self.open_weir_excel)
        weir_help.add_command(label="水利大算盘PDF", command=self.open_weir_pdf)
        help_menu.add_cascade(label="堰流公式计算水位", menu=weir_help)
        region_help = tk.Menu(help_menu, tearoff=0)
        region_help.add_command(label="方法说明", command=lambda: self.show_method_help("地区经验公式法"))
        help_menu.add_cascade(label="地区经验公式法", menu=region_help)
        unit_help = tk.Menu(help_menu, tearoff=0)
        unit_help.add_command(label="方法说明", command=lambda: self.show_method_help("瞬时单位线法"))
        unit_help.add_command(label="附表10雨型分配表/附表12入渗率表", command=self.open_instant_unit_line_tables)
        unit_help.add_command(label="图集公式提取PDF", command=self.open_instant_formula_pdf)
        help_menu.add_cascade(label="瞬时单位线法", menu=unit_help)
        rational_help = tk.Menu(help_menu, tearoff=0)
        rational_help.add_command(label="方法说明", command=lambda: self.show_method_help("推理公式法"))
        rational_help.add_command(label="公式与汇流曲线图集页", command=self.open_rational_formula_tables)
        help_menu.add_cascade(label="推理公式法", menu=rational_help)
        backwater_help = tk.Menu(help_menu, tearoff=0)
        backwater_help.add_command(label="方法说明", command=lambda: self.show_method_help("壅水计算"))
        help_menu.add_cascade(label="壅水计算", menu=backwater_help)
        wave_help = tk.Menu(help_menu, tearoff=0)
        wave_help.add_command(label="方法说明", command=lambda: self.show_method_help("波浪高度计算"))
        help_menu.add_cascade(label="波浪高度计算", menu=wave_help)
        wind_setup_help = tk.Menu(help_menu, tearoff=0)
        wind_setup_help.add_command(label="方法说明", command=lambda: self.show_method_help("风壅水位计算"))
        help_menu.add_cascade(label="风壅水位计算", menu=wind_setup_help)
        scour_help = tk.Menu(help_menu, tearoff=0)
        scour_help.add_command(label="方法说明", command=lambda: self.show_method_help("冲刷计算"))
        help_menu.add_cascade(label="冲刷计算", menu=scour_help)
        help_menu.add_separator()
        help_menu.add_command(label="关于", command=lambda: messagebox.showinfo("关于", "水文计算软件 V6.0（Python转换版）"))
        menu.add_cascade(label="帮助", menu=help_menu)
        self.config(menu=menu)

        style = ttk.Style(self)
        style.configure("TLabelframe", padding=8)
        style.configure("TButton", padding=(12, 5))
        style.configure("TNotebook.Tab", padding=(18, 8), font=(DASHBOARD_CARD_FONT, 10))
        style.configure("Main.TNotebook.Tab", padding=(34, 14), font=(DASHBOARD_CARD_FONT, 13, "bold"))
        style.configure("Inner.TNotebook.Tab", padding=(18, 8), font=(DASHBOARD_CARD_FONT, 10))
        style.configure("Cosmic.TNotebook", background=DASHBOARD_BG, borderwidth=0)
        style.configure(
            "Cosmic.TNotebook.Tab",
            padding=(18, 9),
            font=(DASHBOARD_CARD_FONT, 10),
            background="#f8fcfd",
            foreground=DASHBOARD_TEXT,
        )
        style.map(
            "Cosmic.TNotebook.Tab",
            background=[("selected", "#e8f4f8"), ("active", "#eef9f9")],
            foreground=[("selected", DASHBOARD_TEXT), ("active", DASHBOARD_TEXT)],
        )

        self.configure(bg=DASHBOARD_BG)
        self.logo_image = self._load_logo_image(150, 150)
        self.logo_small_image = self._load_logo_image(62, 62)
        self._add_member_header()
        self.content = tk.Frame(self, bg=DASHBOARD_BG)
        self.content.pack(fill=tk.BOTH, expand=True)
        self.module_defs = [
            {
                "key": "peak",
                "category": "流域水文",
                "title": "流域水文与设计洪水",
                "subtitle": "洪峰、洪量、过程线、经验公式与汇流",
                "description": "整合软件已有设计洪水方法，并归并《水利大算盘》中洪峰流量和洪水总量程序；用于流域来水、频率洪水和成果对比。",
                "accent": DASHBOARD_CATEGORY_COLORS["peak"],
                "bg": "#e7f3ff",
                "count_label": f"{FloodHydrologyProgramTab.program_count()} 项程序 · 8 个计算页",
                "tabs": [
                    ("相关程序清单", FloodHydrologyProgramTab),
                    ("铁一院两所法", TieyuanTab),
                    ("中小河流经验公式法", SmallRiverTab),
                    ("水文面积比拟法", AreaAnalogyTab),
                    ("双对数内插法", DoubleLogInterpolationTab),
                    ("洪水合成法", FloodSynthesisTab),
                    ("地区经验公式法", RegionalFormulaTab),
                    ("瞬时单位线法", InstantUnitLineTab),
                    ("推理公式法", RationalFormulaTab),
                ],
            },
            {
                "key": "irrigation_channel",
                "category": "灌溉输水",
                "title": "灌溉泵站与渠道输水",
                "subtitle": "灌区需水、引水流量、渠首水位、明渠水力和泵站",
                "description": "归并《水利大算盘》第一章、第二章程序，当前已接入灌水模数、灌溉引水、渠首水位、提水泵站、压力管道、明渠水力、流速校核、渗漏、U形渠和钢筋总量。",
                "accent": DASHBOARD_CATEGORY_COLORS["irrigation_channel"],
                "bg": "#e8f7f5",
                "count_label": f"{IrrigationChannelProgramTab.program_count()} 项程序 · 5 个计算页",
                "tabs": [
                    ("相关程序清单", IrrigationChannelProgramTab),
                    ("灌溉引水与渠首水位", IrrigationDiversionTab),
                    ("提水泵站与压力管道", PumpStationPipeTab),
                    ("渠道水力计算", ChannelHydraulicsTab),
                    ("渠道附属与安全校核", ChannelSafetyAuxiliaryTab),
                    ("渠道结构与纵断面", ChannelStructureLongitudinalTab),
                ],
            },
            {
                "key": "river_evolution",
                "category": "河道堤防",
                "title": "河道堤防与防洪影响",
                "subtitle": "河道流量、治导线、水面线、堤顶超高、冲刷和丁坝",
                "description": "归并《水利大算盘》第三章程序，并整合现有河道演变、壅水、波浪、风壅和冲刷计算。",
                "accent": DASHBOARD_CATEGORY_COLORS["river_evolution"],
                "bg": "#eef7df",
                "count_label": f"{RiverLeveeProgramTab.program_count()} 项程序 · 6 个计算页",
                "tabs": [
                    ("相关程序清单", RiverLeveeProgramTab),
                    ("堤防工程书中程序", RiverLeveeBookTab),
                    ("河道演变计算", RiverEvolutionCalculationTab),
                    ("壅水高度及长度", BackwaterCalculationTab),
                    ("冲刷计算", ScourCalculationTab),
                    ("波浪高度", WaveHeightCalculationTab),
                    ("风壅水位", WindSetupCalculationTab),
                ],
            },
            {
                "key": "water_level",
                "category": "水位断面",
                "title": "水位水面线与断面复核",
                "subtitle": "曼宁、能量方程、多断面HEC标准步法",
                "description": "集中管理断面水位、能量损失、水面线推求和水位流量关系；用于桥涵、堤防和涉河工程水位复核。",
                "accent": DASHBOARD_CATEGORY_COLORS["water_level"],
                "bg": "#e9f8ef",
                "count_label": "0 项程序 · 3 个计算页",
                "tabs": [
                    ("曼宁公式水位", ManningWaterLevelAdvancedTab),
                    ("能量方程水位", EnergyEquationWaterLevelTab),
                    ("多断面能量方程法（HEC标准步法）", HecWaterLevelTab),
                ],
            },
            {
                "key": "reservoir",
                "category": "水库大坝",
                "title": "水库蓄水与调洪",
                "subtitle": "库容、淤积、调节、坝顶超高和调洪演算",
                "description": "归并《水利大算盘》第五章水库蓄水工程程序，并整合现有水库调洪演算。",
                "accent": DASHBOARD_CATEGORY_COLORS["reservoir"],
                "bg": "#eaf2f8",
                "count_label": f"{ReservoirProgramTab.program_count()} 项程序 · 2 个计算页",
                "tabs": [
                    ("相关程序清单", ReservoirProgramTab),
                    ("水库蓄水书中程序", ReservoirStorageBookTabV2),
                    ("水库调洪演算", ReservoirFloodRoutingTab),
                ],
            },
            {
                "key": "hydropower_pipe",
                "category": "水电机电",
                "title": "水电站压力管道与机电",
                "subtitle": "水能、装机、压力管道、水锤、输电和负荷",
                "description": "归并《水利大算盘》第四章水力发电工程程序；后续按书页逐项接入水能、管道和机电计算。",
                "accent": DASHBOARD_CATEGORY_COLORS["hydropower_pipe"],
                "bg": "#fff6df",
                "count_label": f"{HydropowerProgramTab.program_count()} 项程序 · 2 个计算页",
                "tabs": [("相关程序清单", HydropowerProgramTab), ("水电站机电与压力管道", HydropowerBookTab)],
            },
            {
                "key": "bridge_opening",
                "category": "水工建筑物",
                "title": "水工建筑物与桥涵泄流",
                "subtitle": "水闸、溢流堰、溢洪道、消能、桥孔、涵洞和侧堰",
                "description": "归并《水利大算盘》第六章水工建筑物程序，并整合现有堰流公式水位和桥孔最小净长度计算。",
                "accent": DASHBOARD_CATEGORY_COLORS["bridge_opening"],
                "bg": "#e6f6f7",
                "count_label": f"{HydraulicStructureProgramTab.program_count()} 项程序 · 3 个计算页",
                "tabs": [
                    ("相关程序清单", HydraulicStructureProgramTab),
                    ("水工建筑物书中程序", HydraulicStructureBookTab),
                    ("堰流公式水位", WeirFlowWaterLevelTab),
                    ("桥孔最小净长度", BridgeOpeningCalculationTab),
                ],
            },
        ]
        self._show_dashboard()
        self.deiconify()

    def _clear_content(self) -> None:
        for child in self.content.winfo_children():
            child.destroy()

    def _show_dashboard(self) -> None:
        self._clear_content()
        dashboard = tk.Frame(self.content, bg=DASHBOARD_BG)
        dashboard.pack(fill=tk.BOTH, expand=True)
        dashboard.columnconfigure(0, weight=1)

        top_canvas = tk.Canvas(dashboard, height=340, bg=DASHBOARD_BG, highlightthickness=0)
        top_canvas.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
        top_canvas.bind(
            "<Configure>",
            lambda event, canvas=top_canvas: self._draw_dashboard_header(canvas, event.width, 340),
        )

        intro = tk.Frame(dashboard, bg=DASHBOARD_BG)
        intro.grid(row=1, column=0, sticky="ew", padx=34, pady=(8, 4))
        tk.Label(
            intro,
            text="按工程任务选择计算入口",
            bg=DASHBOARD_BG,
            fg=DASHBOARD_TEXT,
            font=(DASHBOARD_CARD_FONT, 18, "bold"),
        ).pack(anchor="center")
        tk.Label(
            intro,
            text="核心洪水计算放大居中，河道、断面、水库和水工建筑物按业务属性有序组织。",
            bg=DASHBOARD_BG,
            fg=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 13),
        ).pack(anchor="center", pady=(4, 0))

        card_row = 2
        if not is_membership_active(self.membership_state):
            if not self.member_payment_status_var.get():
                self.member_payment_status_var.set(f"已完成微信登录。{trial_usage_text(self.membership_state)}。试用用完后需扫码开通会员。")
            paywall = self._create_membership_paywall(dashboard)
            paywall.grid(row=2, column=0, sticky="ew", padx=34, pady=(4, 10))
            card_row = 3

        summary_row = card_row
        total_tools = sum(len(module["tabs"]) for module in self.module_defs)
        summary_bar = tk.Frame(dashboard, bg=DASHBOARD_PANEL, highlightthickness=1, highlightbackground=DASHBOARD_BORDER)
        summary_bar.grid(row=summary_row, column=0, sticky="ew", padx=92, pady=(8, 14))
        summary_bar.columnconfigure(0, weight=1)
        summary_items = [
            ("主入口", f"{len(self.module_defs)}", "▣", DASHBOARD_BLUE),
            ("计算程序", f"{len(WATERWORKS_PROGRAMS)}", "∑", DASHBOARD_CYAN),
            ("计算页", f"{total_tools}", "▦", DASHBOARD_STAR),
            ("在线授权 · 自动更新", "LIVE", "●", "#2e7d32"),
        ]
        summary_inner = tk.Frame(summary_bar, bg=DASHBOARD_PANEL)
        summary_inner.grid(row=0, column=0, sticky="n", pady=12)
        for idx, (label, value, icon, color) in enumerate(summary_items):
            chip_width = 180 if idx == 3 else 150
            chip = tk.Canvas(summary_inner, width=chip_width, height=54, bg=DASHBOARD_PANEL, highlightthickness=0)
            chip.grid(row=0, column=idx, sticky="n", padx=(0 if idx == 0 else 14, 0), pady=(0, 0))
            _canvas_round_rect(chip, 4, 4, chip_width - 4, 50, 13, fill=DASHBOARD_PANEL_ALT, outline=DASHBOARD_BORDER)
            chip.create_text(24, 27, text=icon, fill=color, font=(DASHBOARD_MONO_FONT, 13, "bold"), anchor="center")
            chip.create_text(54, 22, text=value, fill=color, font=(DASHBOARD_MONO_FONT, 15, "bold"), anchor="w")
            chip.create_text(54, 38, text=label, fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 9), anchor="w")

        card_row = summary_row + 1
        dashboard.rowconfigure(card_row, weight=1)
        card_scroller = ScrollableFrame(dashboard, background=DASHBOARD_BG, show_horizontal=False)
        card_scroller.grid(row=card_row, column=0, sticky="nsew", padx=28, pady=(8, 24))
        card_scroller.body.columnconfigure(0, weight=1)
        card_area = tk.Frame(card_scroller.body, bg=DASHBOARD_BG)
        card_area.grid(row=0, column=0, sticky="n")
        for col in range(3):
            card_area.columnconfigure(col, minsize=372, weight=1)

        staggered_layout = {
            "peak": (0, 0, 372, 286, "support", 1),
            "irrigation_channel": (0, 1, 372, 286, "support", 1),
            "river_evolution": (0, 2, 372, 286, "support", 1),
            "water_level": (1, 0, 372, 286, "support", 1),
            "reservoir": (1, 1, 372, 286, "support", 1),
            "hydropower_pipe": (1, 2, 372, 286, "support", 1),
            "bridge_opening": (2, 0, 760, 286, "support", 2),
        }
        main_modules = self.module_defs
        for module in main_modules:
            row, col, width, height, variant, columnspan = staggered_layout[module["key"]]
            card = self._create_module_card(card_area, module, width=width, height=height, variant=variant)
            card.grid(
                row=row,
                column=col,
                columnspan=columnspan,
                padx=6,
                pady=14,
                sticky="nsew",
            )
        more_card = self._create_more_functions_card(card_area, width=372, height=286)
        more_card.grid(row=2, column=2, padx=6, pady=14, sticky="nsew")

    def _create_more_functions_card(self, parent: tk.Frame, *, width: int, height: int) -> tk.Canvas:
        card = tk.Canvas(parent, bg=DASHBOARD_BG, width=width, height=height, highlightthickness=0, cursor="hand2")

        def draw(active: bool = False) -> None:
            card.delete("all")
            card.create_rectangle(0, 0, width, height, fill=DASHBOARD_BG, outline="")
            _canvas_round_rect(card, 4, 5, width - 7, height - 8, 18, fill="#ffffff", outline="#d2dde8", width=1)
            card.create_rectangle(
                18,
                20,
                width - 18,
                height - 22,
                outline=DASHBOARD_CYAN if active else "#9bb6ca",
                width=2,
                dash=() if active else (7, 5),
            )
            card.create_text(
                width // 2,
                92,
                text="+",
                fill=DASHBOARD_CYAN,
                font=(DASHBOARD_MONO_FONT, 30, "bold"),
                anchor="center",
            )
            card.create_text(
                width // 2,
                136,
                text="查看更多功能",
                fill=DASHBOARD_TEXT,
                font=(DASHBOARD_CARD_FONT, 16, "bold"),
                anchor="center",
            )
            card.create_text(
                width // 2,
                170,
                text="7 个主入口已全部展示；后续扩展模块将在这里接入。",
                fill=DASHBOARD_MUTED,
                font=(DASHBOARD_CARD_FONT, 12),
                anchor="center",
                width=270,
            )
            _canvas_round_rect(card, 98, height - 70, width - 98, height - 38, 9, fill="#f6f9fc", outline="#dce7f0")
            card.create_text(
                width // 2,
                height - 54,
                text="全部入口可见",
                fill=DASHBOARD_CYAN,
                font=(DASHBOARD_CARD_FONT, 10, "bold"),
                anchor="center",
            )

        draw(False)
        card.bind("<Enter>", lambda _event: draw(True))
        card.bind("<Leave>", lambda _event: draw(False))
        return card

    def _create_more_functions_bar(self, parent: tk.Frame, modules: list[dict]) -> tk.Frame:
        bar = tk.Frame(parent, bg=DASHBOARD_PANEL, highlightthickness=1, highlightbackground=DASHBOARD_BORDER)
        bar.columnconfigure(1, weight=1)
        tk.Label(
            bar,
            text="查看更多功能",
            bg=DASHBOARD_PANEL,
            fg=DASHBOARD_TEXT,
            font=(DASHBOARD_CARD_FONT, 13, "bold"),
        ).grid(row=0, column=0, sticky="w", padx=(22, 12), pady=14)
        tk.Label(
            bar,
            text="水工建筑物、桥涵泄流、堰流公式等低频入口已收口，避免主工作区第三行裸露截断。",
            bg=DASHBOARD_PANEL,
            fg=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 12),
        ).grid(row=0, column=1, sticky="w", pady=14)
        actions = tk.Frame(bar, bg=DASHBOARD_PANEL)
        actions.grid(row=0, column=2, sticky="e", padx=(10, 18), pady=10)
        for idx, module in enumerate(modules):
            tk.Button(
                actions,
                text=f"{module['title']}  →",
                command=lambda item=module: self._open_module(item),
                bg=DASHBOARD_PANEL_ALT,
                fg=DASHBOARD_CYAN,
                activebackground="#eef3f7",
                activeforeground=DASHBOARD_CYAN,
                relief=tk.FLAT,
                bd=1,
                padx=14,
                pady=7,
                cursor="hand2",
                font=(DASHBOARD_CARD_FONT, 10, "bold"),
            ).grid(row=0, column=idx, sticky="e", padx=(8, 0))
        return bar

    def _draw_dashboard_header(self, canvas: tk.Canvas, width: int, height: int) -> None:
        for child in canvas.winfo_children():
            child.destroy()
        canvas.delete("all")
        _draw_deep_space(canvas, max(width, 980), height)
        panel_right = max(width - 28, 980)
        panel_left = 24
        panel_top = 18
        panel_bottom = height - 18
        _canvas_round_rect(canvas, panel_left, panel_top, panel_right, panel_bottom, 22, fill=DASHBOARD_PANEL, outline=DASHBOARD_BORDER, width=1)
        _canvas_round_rect(canvas, panel_left + 10, panel_top + 10, panel_right - 10, panel_bottom - 10, 18, fill="#eef7fb", outline="#e4edf5", width=1)
        canvas.create_rectangle(panel_left + 18, panel_top + 18, panel_right - 18, panel_top + 86, fill="#e9f2ff", outline="")
        for x in range(panel_left + 62, panel_right - 36, 82):
            canvas.create_line(x, panel_top + 24, x - 34, panel_bottom - 38, fill="#dfeaf3", width=1)
        for y in (212, 238, 264):
            canvas.create_line(panel_left + 54, y, 210, y - 12, 374, y + 6, 574, y - 20, fill="#cfdee9", width=1, smooth=True)

        logo_x = 76
        logo_y = 66
        _canvas_round_rect(canvas, logo_x - 26, logo_y - 27, logo_x + 28, logo_y + 27, 14, fill=DASHBOARD_CYAN, outline="")
        canvas.create_text(logo_x + 1, logo_y, text="HT", fill="#ffffff", font=(DASHBOARD_MONO_FONT, 16, "bold"), anchor="center")
        canvas.create_text(
            122,
            52,
            text="慧通智达水文计算云平台",
            fill=DASHBOARD_TEXT,
            font=(DASHBOARD_CARD_FONT, 30, "bold"),
            anchor="w",
        )
        canvas.create_text(
            122,
            86,
            text="智算云平台 V6.0 · 专业洪评与水文复核工作台",
            fill=DASHBOARD_CYAN,
            font=(DASHBOARD_CARD_FONT, 12, "bold"),
            anchor="w",
        )
        canvas.create_line(122, 108, min(654, width - 580), 108, fill=DASHBOARD_CYAN, width=3)
        canvas.create_text(
            122,
            136,
            text="设计暴雨 · 产汇流 · 水位水面线 · 水库调洪 · 水工建筑物",
            fill=DASHBOARD_TEXT,
            font=(DASHBOARD_CARD_FONT, 16, "bold"),
            anchor="w",
            width=560,
        )
        canvas.create_text(
            122,
            176,
            text="把资料、公式、过程线、参数表和计算书导出组织到同一套专业工作台，服务长时间看表、录参和成果复核。",
            fill=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 13),
            anchor="w",
            width=min(610, max(500, int(width * 0.48))),
        )
        canvas.create_text(
            122,
            236,
            text="数据底板：流域 DEM · 河道断面 · 测站 · 建筑物",
            fill=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
            anchor="w",
            width=560,
        )
        canvas.create_text(
            122,
            258,
            text="计算方法：推理公式 · 瞬时单位线 · 地区经验公式 · HEC 标准步法",
            fill=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
            anchor="w",
            width=560,
        )
        canvas.create_text(
            122,
            280,
            text="计算方法均采用软件内置公式与规范参数；未接入外部模型不在界面展示",
            fill=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
            anchor="w",
            width=560,
        )

        visual_x = max(int(width * 0.56), 690)
        panel = tk.Canvas(canvas, width=548, height=268, bg="#eef7fb", highlightthickness=0)
        canvas.create_window(visual_x, 42, window=panel, anchor="nw")
        self._draw_dashboard_illustration(panel)
        for idx, (text, color) in enumerate((("✓ 资料可追溯", DASHBOARD_CYAN), ("✓ 公式可复核", DASHBOARD_STAR), ("✓ 成果可导出", "#0d47a1"))):
            x = 122 + idx * 156
            _canvas_round_rect(canvas, x, 304, x + 140, 330, 9, fill="#ffffff", outline="#cbd8e4")
            canvas.create_text(x + 70, 317, text=text, fill=color, font=(DASHBOARD_CARD_FONT, 10, "bold"))

    def _create_membership_paywall(self, parent: tk.Widget) -> tk.Frame:
        panel = tk.Frame(parent, bg="#ffffff", highlightthickness=1, highlightbackground=DASHBOARD_BORDER, padx=22, pady=16)
        panel.columnconfigure(1, weight=1)
        tk.Label(
            panel,
            text="会员未开通",
            bg="#ffffff",
            fg=DASHBOARD_STAR,
            font=(DASHBOARD_CARD_FONT, 15, "bold"),
        ).grid(row=0, column=0, sticky="w")
        tk.Label(
            panel,
            text="未开通会员可免费试用 3 次，所有模块和计算公式累计计数。试用用完后，请选择套餐并扫码支付开通会员。",
            bg="#ffffff",
            fg=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=1, column=0, columnspan=4, sticky="w", pady=(6, 12))
        tk.Label(panel, text="套餐", bg="#ffffff", fg=DASHBOARD_TEXT, font=(DASHBOARD_CARD_FONT, 10, "bold")).grid(row=2, column=0, sticky="w")
        available_plans = available_membership_plans(self.membership_state)
        if self.member_plan_var.get() not in available_plans or (
            self.member_plan_var.get() == "月度会员" and not monthly_membership_available(self.membership_state)
        ):
            self.member_plan_var.set(default_membership_plan(self.membership_state))
        plan_box = ttk.Combobox(
            panel,
            textvariable=self.member_plan_var,
            values=list(available_plans.keys()),
            state="readonly",
            width=16,
        )
        plan_box.grid(row=2, column=1, sticky="w", padx=(8, 20))
        tk.Radiobutton(
            panel,
            text="微信",
            variable=self.member_pay_provider_var,
            value="wechat",
            bg="#ffffff",
            fg=DASHBOARD_TEXT,
            activebackground="#ffffff",
            activeforeground=DASHBOARD_TEXT,
            selectcolor="#e8f4f8",
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=2, column=2, sticky="w", padx=(0, 10))
        tk.Radiobutton(
            panel,
            text="支付宝",
            variable=self.member_pay_provider_var,
            value="alipay",
            bg="#ffffff",
            fg=DASHBOARD_TEXT,
            activebackground="#ffffff",
            activeforeground=DASHBOARD_TEXT,
            selectcolor="#e8f4f8",
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=2, column=3, sticky="w", padx=(0, 14))
        ttk.Button(panel, text="扫码支付开通会员", command=self._create_membership_payment_order).grid(row=2, column=4, sticky="e")
        tk.Label(
            panel,
            textvariable=self.member_payment_status_var,
            bg="#ffffff",
            fg=DASHBOARD_STAR,
            font=(DASHBOARD_CARD_FONT, 10),
            wraplength=900,
            justify=tk.LEFT,
        ).grid(row=3, column=0, columnspan=5, sticky="w", pady=(10, 0))
        if not monthly_membership_available(self.membership_state) and not self.member_payment_status_var.get():
            self.member_payment_status_var.set(
                self.membership_state.get("membership_purchase_rule")
                or "该账号已使用过月度会员，继续使用请选择年度会员。"
            )
        return panel

    def _membership_server_url(self) -> str:
        return (
            self.membership_state.get("server_url")
            or self.version_info.auth_server_url
            or "https://htzdwater.com"
        ).strip().rstrip("/")

    def _create_membership_payment_order(self) -> None:
        account = (self.membership_state.get("phone") or "").strip()
        if not account:
            self.member_payment_status_var.set("当前没有有效登录账号，请退出后重新微信扫码登录。")
            return
        plan = self.member_plan_var.get().strip()
        available_plans = available_membership_plans(self.membership_state)
        if plan not in available_plans:
            self.member_payment_status_var.set(
                self.membership_state.get("membership_purchase_rule")
                or "该账号已订阅过月度会员，继续使用只能订购年度会员。"
            )
            return
        if plan == "月度会员" and not monthly_membership_available(self.membership_state):
            self.member_payment_status_var.set(
                self.membership_state.get("membership_purchase_rule")
                or "该账号已使用过月度会员，继续使用请选择年度会员。"
            )
            return
        provider = self.member_pay_provider_var.get().strip()
        server_url = self._membership_server_url()
        if not server_url.startswith(("http://", "https://")):
            self.member_payment_status_var.set("登录服务地址异常，请退出后重新登录。")
            return
        self.member_payment_status_var.set("正在创建支付订单...")
        self.update_idletasks()
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            order = client.create_payment_order(account, MEMBERSHIP_PLAN_IDS.get(plan, plan), provider)
        except AuthClientError as exc:
            self.member_payment_status_var.set(_friendly_payment_error(str(exc)))
            return
        order_no = str(order.get("order_no") or "")
        pay_url = str(order.get("pay_page_url") or order.get("code_url") or "")
        if not order_no or not pay_url:
            self.member_payment_status_var.set("服务器未返回有效支付订单，请稍后重试。")
            return
        if pay_url.startswith("/"):
            pay_url = server_url + pay_url
        self.member_current_order_no = order_no
        self.member_current_order_server_url = server_url
        self.member_pay_poll_remaining = 200
        amount = str(order.get("amount_yuan") or "")
        if open_external_url(pay_url):
            self.member_payment_status_var.set(f"已打开支付二维码，订单金额 {amount} 元。付款成功后软件会自动开通会员。")
        else:
            self.member_payment_status_var.set(f"未能自动打开支付二维码，请复制链接到浏览器：{pay_url}")
        self.after(3000, self._poll_membership_payment_order)

    def _poll_membership_payment_order(self) -> None:
        if not self.member_current_order_no or not self.member_current_order_server_url:
            return
        if self.member_pay_poll_remaining <= 0:
            self.member_payment_status_var.set("支付状态查询已停止。如已付款但未开通，请重新打开软件让系统刷新会员状态。")
            return
        self.member_pay_poll_remaining -= 1
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=self.member_current_order_server_url))
            order = client.get_payment_order(self.member_current_order_no)
        except AuthClientError:
            self.after(3000, self._poll_membership_payment_order)
            return
        status = str(order.get("status") or "")
        if status == "paid":
            self.member_current_order_no = ""
            self._apply_paid_membership(order)
            return
        if status in {"expired", "closed", "failed"}:
            self.member_current_order_no = ""
            self.member_payment_status_var.set(f"订单状态为 {status}，请重新创建支付订单。")
            return
        self.after(3000, self._poll_membership_payment_order)

    def _apply_paid_membership(self, order: dict[str, object]) -> None:
        new_state = dict(self.membership_state)
        expire_at = str(order.get("license_expire_at") or "")
        plan = str(order.get("plan_name") or self.member_plan_var.get() or "")
        if expire_at:
            new_state["expire_at"] = expire_at
        if plan:
            new_state["plan"] = plan
        new_state["license_active"] = "1"
        new_state["last_verified_at"] = datetime.now().isoformat(timespec="seconds")
        server_url = self.member_current_order_server_url or self._membership_server_url()
        if server_url:
            new_state["server_url"] = server_url
        try:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            new_state = client.heartbeat(new_state, self.version_info.version)
        except AuthClientError:
            pass
        self.membership_state = new_state
        save_membership_state(new_state)
        self._refresh_member_header()
        self.member_payment_status_var.set("支付成功，会员已开通。")
        messagebox.showinfo("会员已开通", "支付成功，会员已自动开通，现在可以使用计算模块。")
        self._show_dashboard()

    def _create_module_card(self, parent: tk.Frame, module: dict, *, width: int | None = None, height: int | None = None, variant: str = "support") -> tk.Canvas:
        accent = module["accent"]
        width = width or DASHBOARD_CARD_WIDTH
        height = height or DASHBOARD_CARD_HEIGHT
        card = tk.Canvas(
            parent,
            bg=DASHBOARD_BG,
            width=width,
            height=height,
            highlightthickness=0,
            cursor="hand2",
        )
        compact = width < 340
        category_y = 34
        icon_x = 28 if compact else 30
        icon_y = 75 if compact else 70
        icon_size = 50 if compact else 58
        icon_active_size = 52 if compact else 61
        text_x = 92 if compact else 108
        title_y = 77 if compact else 78
        subtitle_y = 122 if compact else 110
        desc_y = 150 if compact else 138
        action_y = height - 52
        text_width = width - text_x - 26
        title_size = 14 if compact else 16
        subtitle_size = 9 if compact else 10
        desc_size = 11 if compact else 13
        desc_limit = 24 if compact else 58
        short_desc = str(module["description"])
        if len(short_desc) > desc_limit:
            short_desc = short_desc[: desc_limit - 2].rstrip("，；。") + "..."

        def draw_icon(x: int, y: int, size: int) -> None:
            icon_fill = _blend_hex_color(accent, "#ffffff", 0.88)
            icon_border = _blend_hex_color(accent, "#ffffff", 0.68)
            _canvas_round_rect(card, x, y, x + size, y + size, 12, fill=icon_fill, outline=icon_border, width=1)
            cx = x + size // 2
            cy = y + size // 2
            if module["key"] == "peak":
                card.create_line(x + 12, y + size - 18, x + 24, y + size - 28, x + 38, y + size - 24, x + size - 14, y + 18, fill=accent, width=2, smooth=True)
                card.create_line(x + 12, y + size - 12, x + size - 12, y + size - 12, fill="#829ab1", width=1)
            elif module["key"] == "irrigation_channel":
                card.create_line(x + 10, y + size - 14, x + size - 10, y + size - 14, fill=accent, width=2, smooth=True)
                card.create_line(x + 18, y + 20, x + 18, y + size - 14, fill=accent, width=2)
                card.create_arc(x + 18, y + 18, x + size - 12, y + size - 10, start=0, extent=180, style=tk.ARC, outline="#829ab1", width=2)
            elif module["key"] == "river_evolution":
                card.create_line(x + 10, y + size - 18, x + 24, y + size - 28, x + 40, y + size - 24, x + size - 12, y + 24, fill=accent, width=2, smooth=True)
                card.create_line(x + 10, y + size - 10, x + 28, y + size - 14, x + 46, y + size - 8, x + size - 10, y + size - 14, fill="#829ab1", width=2, smooth=True)
            elif module["key"] == "water_level":
                card.create_line(x + 14, y + 16, x + 14, y + size - 14, fill=accent, width=2)
                card.create_line(x + 10, y + size - 14, x + size - 10, y + size - 14, fill=accent, width=2)
                card.create_line(x + 18, y + size - 24, x + 32, y + size - 38, x + 44, y + size - 32, x + size - 10, y + 22, fill="#829ab1", width=2, smooth=True)
            elif module["key"] == "reservoir":
                card.create_polygon(x + 16, y + size - 16, x + 28, y + 20, x + 44, y + size - 16, fill="", outline=accent, width=2)
                for offset in (22, 32, 42):
                    card.create_line(x + 42, y + offset, x + size - 10, y + offset, fill=accent, width=2)
            elif module["key"] == "hydropower_pipe":
                card.create_polygon(x + 20, y + 14, x + 32, y + 14, x + 27, y + 34, x + 40, y + 34, x + 22, y + size - 10, x + 28, y + 42, x + 16, y + 42, fill="", outline=accent, width=2)
                card.create_line(x + 44, y + 24, x + size - 10, y + 24, fill=accent, width=2)
                card.create_line(x + 44, y + 42, x + size - 10, y + 42, fill="#829ab1", width=2)
            else:
                card.create_polygon(x + 14, y + size - 18, cx, y + 18, x + size - 12, y + size - 18, fill="", outline=accent, width=2)
                card.create_line(x + 14, y + size - 12, x + size - 12, y + size - 12, fill=accent, width=3)

        def draw(active: bool = False) -> None:
            card.delete("all")
            card.create_rectangle(0, 0, width, height, fill=DASHBOARD_BG, outline="")
            if active:
                _canvas_round_rect(card, 8, 10, width - 4, height - 4, 16, fill="#dfe6ee", outline="")
            _canvas_round_rect(card, 4, 5, width - 7, height - 8, 16, fill=DASHBOARD_PANEL, outline=accent if active else DASHBOARD_BORDER, width=1)
            card.create_rectangle(4, 18, 10, height - 24, fill=accent, outline="")
            card.create_line(26, 56, width - 28, 56, fill="#e7eff6", width=1)
            draw_icon(icon_x, icon_y, icon_size if not active else icon_active_size)
            _canvas_round_rect(card, 28, 25, 98, 45, 7, fill="#ffffff", outline="#cbd8e4")
            card.create_text(63, 35, text=str(module["category"]), fill=accent, font=(DASHBOARD_CARD_FONT, 9, "bold"), anchor="center")
            card.create_text(text_x, title_y, text=module["title"], fill=DASHBOARD_TEXT, font=(DASHBOARD_CARD_FONT, title_size, "bold"), anchor="w", width=text_width)
            card.create_text(text_x, subtitle_y, text=module["subtitle"], fill=DASHBOARD_CYAN, font=(DASHBOARD_CARD_FONT, subtitle_size, "bold"), anchor="w", width=text_width)
            card.create_text(text_x, desc_y, text=short_desc, fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, desc_size), anchor="nw", width=text_width)
            card.create_line(28, action_y - 12, width - 28, action_y - 12, fill="#e7eff6", width=1)
            action_text = module.get("count_label") or f"{len(module['tabs'])} 个计算工具"
            button_fill = "#0D47A1" if active else DASHBOARD_CYAN
            _canvas_round_rect(card, 28, action_y, 102, action_y + 32, 9, fill=button_fill, outline=button_fill)
            card.create_text(65, action_y + 16, text="进入", fill="#ffffff", font=(DASHBOARD_CARD_FONT, 10, "bold"), anchor="center")
            _canvas_round_rect(card, width - 176, action_y, width - 28, action_y + 32, 9, fill="#f6f9fc", outline="#dce7f0")
            card.create_text(width - 102, action_y + 16, text=action_text, fill=DASHBOARD_TEXT, font=(DASHBOARD_MONO_FONT, 9, "bold"), anchor="center")

        def open_module(_event=None, item=module):
            self._open_module(item)

        def set_hover(_event=None):
            draw(True)

        def unset_hover(_event=None):
            draw(False)

        draw(False)
        card.bind("<Button-1>", open_module)
        card.bind("<Enter>", set_hover)
        card.bind("<Leave>", unset_hover)
        return card

    def _ensure_calculation_usage_hook(self, tab_class: type) -> None:
        if getattr(tab_class, "_hydrology_usage_hooked", False):
            return
        original = getattr(tab_class, "calculate", None)
        if not callable(original):
            return

        def wrapped_calculate(instance, *args, **kwargs):
            top = instance.winfo_toplevel()
            module_key = getattr(instance, "_usage_module_key", "")
            module_title = getattr(instance, "_usage_module_title", "")
            method_title = getattr(instance, "_usage_method_title", instance.__class__.__name__)
            gate = getattr(top, "_can_start_calculation", None)
            if callable(gate) and not gate(module_key, module_title, method_title):
                return None
            result = original(instance, *args, **kwargs)
            try:
                checker = getattr(top, "_tab_has_calculation_output", None)
                successful = checker(instance) if callable(checker) else True
                reporter = getattr(top, "_report_calculation_usage", None)
                if successful and callable(reporter):
                    reporter(module_key, module_title, method_title)
            except Exception:
                pass
            return result

        setattr(tab_class, "calculate", wrapped_calculate)
        setattr(tab_class, "_hydrology_usage_hooked", True)

    def _tab_has_calculation_output(self, tab_body: tk.Widget) -> bool:
        output = getattr(tab_body, "output", None)
        if output is None:
            return True
        getter = getattr(output, "get", None)
        if not callable(getter):
            return True
        try:
            try:
                text = getter("1.0", tk.END)
            except TypeError:
                text = getter()
        except Exception:
            return True
        return bool(str(text).strip())

    def _apply_trial_status_from_response(self, data: dict) -> None:
        trial = data.get("trial") if isinstance(data.get("trial"), dict) else {}
        license_info = data.get("license") if isinstance(data.get("license"), dict) else {}
        if not trial and isinstance(license_info.get("trial"), dict):
            trial = license_info["trial"]
        if not trial:
            return
        self.membership_state["trial_enabled"] = "1" if bool(trial.get("enabled")) else "0"
        self.membership_state["trial_limit"] = str(trial.get("limit") if trial.get("limit") is not None else "")
        self.membership_state["trial_used"] = str(trial.get("used") if trial.get("used") is not None else "")
        self.membership_state["trial_remaining"] = str(trial.get("remaining") if trial.get("remaining") is not None else "")
        self.membership_state["trial_exhausted"] = "1" if bool(trial.get("exhausted")) else "0"
        save_membership_state(self.membership_state)
        self._refresh_member_header()
        if not is_membership_active(self.membership_state):
            self.member_payment_status_var.set(f"{trial_usage_text(self.membership_state)}。试用用完后需扫码开通会员。")

    def _refresh_membership_state_from_server(self) -> None:
        token = self.membership_state.get("access_token", "")
        if not token:
            raise AuthClientError("本地未保存登录令牌，请重新登录")
        client = OnlineAuthClient(AuthClientConfig(server_url=self._membership_server_url()))
        self.membership_state = client.heartbeat(self.membership_state, self.version_info.version)
        save_membership_state(self.membership_state)
        self._refresh_member_header()

    def _can_start_calculation(self, module_key: str, module_title: str, method_title: str) -> bool:
        if is_membership_active(self.membership_state):
            return True
        try:
            self._refresh_membership_state_from_server()
        except AuthClientError as exc:
            self.member_payment_status_var.set(f"联网校验试用次数失败：{exc}")
            messagebox.showwarning("无法校验试用次数", f"需要连接授权服务器校验剩余试用次数后才能计算。\n\n{exc}")
            return False
        if is_membership_active(self.membership_state):
            return True
        if is_trial_available(self.membership_state):
            self.member_payment_status_var.set(f"{trial_usage_text(self.membership_state)}。本次计算成功后将扣减 1 次。")
            return True
        self.member_payment_status_var.set("3次免费试用已用完，请扫码支付开通会员后继续使用。")
        messagebox.showinfo("试用已结束", "3次免费试用已用完，请在首页扫码支付开通会员后继续使用。")
        return False

    def _report_calculation_usage(self, module_key: str, module_title: str, method_title: str) -> None:
        token = self.membership_state.get("access_token", "")
        if not token:
            return
        account = self.membership_state.get("phone", "")
        cache_key = f"{account}|{module_key}|{method_title}"
        active_member = is_membership_active(self.membership_state)
        if active_member and cache_key in self._calculation_usage_reported:
            return
        if active_member:
            self._calculation_usage_reported.add(cache_key)
        server_url = self._membership_server_url()
        app_version = self.version_info.version

        def send_usage() -> dict:
            client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
            return client.report_calculation_usage(self.membership_state, app_version, module_title or module_key, method_title)

        if not active_member:
            try:
                data = send_usage()
                self._apply_trial_status_from_response(data)
                if trial_remaining(self.membership_state) <= 0:
                    messagebox.showinfo("试用已用完", "本次计算已完成，3次免费试用已用完。继续使用请在首页扫码开通会员。")
            except AuthServerRejectedError as exc:
                self.membership_state["trial_remaining"] = "0"
                self.membership_state["trial_exhausted"] = "1"
                save_membership_state(self.membership_state)
                self._refresh_member_header()
                self.member_payment_status_var.set(str(exc))
                messagebox.showinfo("试用已结束", f"{exc}\n请在首页扫码支付开通会员后继续使用。")
            except AuthClientError as exc:
                self.member_payment_status_var.set(f"试用次数记录失败：{exc}")
                messagebox.showwarning("试用次数记录失败", f"本次计算已完成，但服务器未能记录试用次数。请检查网络后重新打开软件。\n\n{exc}")
            return

        def worker() -> None:
            try:
                send_usage()
            except AuthClientError:
                self._calculation_usage_reported.discard(cache_key)

        threading.Thread(target=worker, daemon=True).start()

    def _open_module(self, module: dict) -> None:
        if not is_membership_active(self.membership_state) and not is_trial_available(self.membership_state):
            self.member_payment_status_var.set("3次免费试用已用完，请扫码支付开通会员后继续使用。")
            messagebox.showinfo("试用已结束", "3次免费试用已用完，请先在首页扫码支付开通会员。")
            return
        if not is_membership_active(self.membership_state):
            self.member_payment_status_var.set(f"{trial_usage_text(self.membership_state)}。进入模块后每次成功计算扣减 1 次。")
        self._clear_content()
        page = tk.Frame(self.content, bg=DASHBOARD_BG)
        page.pack(fill=tk.BOTH, expand=True)
        page.rowconfigure(1, weight=1)
        page.columnconfigure(0, weight=1)

        top = tk.Frame(page, bg=DASHBOARD_PANEL, padx=20, pady=15, highlightthickness=1, highlightbackground=DASHBOARD_BORDER)
        top.grid(row=0, column=0, sticky="ew", padx=18, pady=(16, 10))
        top.columnconfigure(3, weight=1)
        ttk.Button(top, text="← 返回首页", command=self._show_dashboard).grid(row=0, column=0, sticky="w")
        if self.logo_small_image is not None:
            tk.Label(top, image=self.logo_small_image, bg=DASHBOARD_PANEL).grid(row=0, column=1, rowspan=2, padx=(20, 12))
        tk.Label(
            top,
            text=module["title"],
            bg=DASHBOARD_PANEL,
            fg=DASHBOARD_TEXT,
            font=(DASHBOARD_CARD_FONT, 19, "bold"),
        ).grid(row=0, column=2, sticky="w")
        tk.Label(
            top,
            text=module["description"],
            bg=DASHBOARD_PANEL,
            fg=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=1, column=2, columnspan=2, sticky="w", pady=(4, 0))

        module_frame = tk.Frame(page, bg="#ffffff", highlightthickness=1, highlightbackground=DASHBOARD_BORDER)
        module_frame.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
        module_frame.rowconfigure(0, weight=1)
        module_frame.columnconfigure(0, weight=1)
        notebook = ttk.Notebook(module_frame, style="Cosmic.TNotebook")
        notebook.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
        for tab_title, tab_class in module["tabs"]:
            self._ensure_calculation_usage_hook(tab_class)
            tab_container = ScrollableFrame(notebook)
            tab_body = tab_class(tab_container.body)
            setattr(tab_body, "_usage_module_key", module["key"])
            setattr(tab_body, "_usage_module_title", module["title"])
            setattr(tab_body, "_usage_method_title", tab_title)
            tab_body.grid(row=0, column=0, sticky="nsew")
            tab_container.body.rowconfigure(0, weight=1)
            notebook.add(tab_container, text=tab_title)

        def show_current_steps() -> None:
            selected = notebook.select()
            title = notebook.tab(selected, "text") if selected else module["tabs"][0][0]
            guide = METHOD_STEP_GUIDES.get(title, "请按资料准备、参数输入、开始计算、结果复核、成果导出的顺序操作。")
            show_text_window(self, f"{title} - 操作步骤", guide)

        tk.Button(
            top,
            text="当前方法操作步骤",
            command=show_current_steps,
            bg="#e8f4f8",
            fg=DASHBOARD_TEXT,
            activebackground="#eef9f9",
            activeforeground=DASHBOARD_TEXT,
            relief=tk.FLAT,
            bd=1,
            padx=16,
            pady=7,
            font=(DASHBOARD_CARD_FONT, 9, "bold"),
        ).grid(row=0, column=4, rowspan=2, sticky="e", padx=(18, 0))
        tk.Button(
            top,
            text="全公式新人例题",
            command=self.open_formula_audit_report,
            bg="#f8fcfd",
            fg=DASHBOARD_STAR,
            activebackground="#eef9f9",
            activeforeground=DASHBOARD_STAR,
            relief=tk.FLAT,
            bd=1,
            padx=14,
            pady=7,
            font=(DASHBOARD_CARD_FONT, 9, "bold"),
        ).grid(row=0, column=5, rowspan=2, sticky="e", padx=(10, 0))

    def _load_logo_image(self, max_width: int, max_height: int) -> tk.PhotoImage | None:
        return _load_app_icon_image(max_width, max_height)

    def _cancel_dashboard_model_animation(self) -> None:
        for after_id in getattr(self, "_dashboard_model_after_ids", []):
            try:
                self.after_cancel(after_id)
            except tk.TclError:
                pass
        self._dashboard_model_after_ids = []

    def _schedule_dashboard_model_frame(self, callback: Callable[[], None], delay_ms: int) -> None:
        self._dashboard_model_after_ids.append(self.after(delay_ms, callback))

    def _draw_dashboard_illustration(self, canvas: tk.Canvas) -> None:
        self._cancel_dashboard_model_animation()
        active_view = getattr(self, "_dashboard_model_view", "flow")
        if active_view not in {"flow", "routing", "section"}:
            active_view = "flow"
            self._dashboard_model_view = active_view
        canvas.delete("all")
        canvas.create_rectangle(0, 0, 548, 268, fill="#eef7fb", outline="")
        _canvas_round_rect(canvas, 4, 4, 544, 264, 18, fill="#ffffff", outline=DASHBOARD_BORDER)
        _canvas_round_rect(canvas, 14, 14, 534, 254, 14, fill="#f7fbfe", outline="#bfd0df")
        for x in range(34, 522, 54):
            canvas.create_line(x, 60, x - 20, 236, fill="#e1ebf3", width=1)

        canvas.create_text(28, 34, text="实时水文模型", fill=DASHBOARD_TEXT, font=(DASHBOARD_CARD_FONT, 12, "bold"), anchor="w")
        canvas.create_oval(122, 29, 132, 39, fill="#2e7d32", outline="", tags=("live_dot",))
        canvas.create_text(140, 34, text="LIVE", fill="#2e7d32", font=(DASHBOARD_MONO_FONT, 9, "bold"), anchor="w")
        pre_items = (
            ("预报", "#1565c0", True),
            ("预警", "#ef6c00", False),
            ("预演", "#00838f", False),
            ("预案", "#6a1b9a", False),
        )
        for idx, (text, color, active) in enumerate(pre_items):
            x = 178 + idx * 44
            _canvas_round_rect(canvas, x, 21, x + 38, 47, 8, fill=color if active else "#ffffff", outline="#cbd8e4")
            canvas.create_text(
                x + 19,
                34,
                text=text,
                fill="#ffffff" if active else color,
                font=(DASHBOARD_CARD_FONT, 10, "bold"),
            )
        canvas.create_text(512, 34, text=f"更新于 {datetime.now().strftime('%H:%M:%S')}", fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 8), anchor="e")

        tab_specs = (("flow", "流量过程"), ("routing", "洪水演进"), ("section", "断面分析"))
        for idx, (view_key, label) in enumerate(tab_specs):
            x = 28 + idx * 82
            tag = f"dashboard_model_tab_{view_key}"
            is_active = active_view == view_key
            _canvas_round_rect(canvas, x, 52, x + 72, 74, 7, fill="#e5f0f8" if is_active else "#ffffff", outline="#d3e0eb", tags=(tag,))
            canvas.create_text(
                x + 36,
                63,
                text=label,
                fill=DASHBOARD_CYAN if is_active else DASHBOARD_MUTED,
                font=(DASHBOARD_CARD_FONT, 8, "bold"),
                anchor="center",
                tags=(tag,),
            )
            canvas.tag_bind(tag, "<Button-1>", lambda _event, key=view_key, target=canvas: self._switch_dashboard_model_view(target, key))

        if active_view == "routing":
            self._draw_dashboard_routing_view(canvas)
        elif active_view == "section":
            self._draw_dashboard_section_view(canvas)
        else:
            self._draw_dashboard_flow_view(canvas)
        self._draw_dashboard_metric_cards(canvas)

    def _switch_dashboard_model_view(self, canvas: tk.Canvas, view_key: str) -> None:
        self._dashboard_model_view = view_key
        self._draw_dashboard_illustration(canvas)

    def _draw_dashboard_flow_view(self, canvas: tk.Canvas) -> None:
        chart_x1, chart_y1, chart_x2, chart_y2 = 38, 90, 344, 196
        canvas.create_line(chart_x1, chart_y2, chart_x2, chart_y2, fill="#829ab1", width=1)
        canvas.create_line(chart_x1, chart_y1, chart_x1, chart_y2, fill="#829ab1", width=1)
        canvas.create_line(chart_x2, chart_y1, chart_x2, chart_y2, fill="#b8c9dd", width=1)
        canvas.create_text(chart_x1 - 8, chart_y1, text="Q m³/s", fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 7), anchor="e")
        canvas.create_text(chart_x2 + 8, chart_y1, text="P mm", fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 7), anchor="w")
        for y in (112, 140, 168):
            canvas.create_line(chart_x1, y, chart_x2, y, fill="#e6eef5", width=1)
        for x in range(chart_x1 + 42, chart_x2, 42):
            canvas.create_line(x, chart_y1, x, chart_y2, fill="#e6eef5", width=1)
        canvas.create_line(chart_x1, 138, chart_x2, 138, fill="#DC2626", width=1, dash=(5, 4))
        canvas.create_text(chart_x2 - 6, 133, text="警戒 1000 m³/s", fill="#DC2626", font=(DASHBOARD_CARD_FONT, 7), anchor="e")

        rain_values = (18, 30, 46, 62, 42, 24)
        for idx, value in enumerate(rain_values):
            x = chart_x1 + 198 + idx * 16
            canvas.create_rectangle(x, chart_y2 - value, x + 9, chart_y2, fill="#93c5d8", outline="")
        hydro_points = (
            (chart_x1, 190), (chart_x1 + 34, 186), (chart_x1 + 68, 174), (chart_x1 + 102, 152),
            (chart_x1 + 136, 112), (chart_x1 + 172, 96), (chart_x1 + 208, 128), (chart_x1 + 244, 154),
            (chart_x1 + 282, 174), (chart_x2, 188),
        )
        forecast_points = (
            (chart_x1 + 170, 100), (chart_x1 + 204, 120), (chart_x1 + 240, 144),
            (chart_x1 + 282, 164), (chart_x2, 178),
        )
        confidence_upper = [(x, y - 12) for x, y in forecast_points]
        confidence_lower = [(x, y + 13) for x, y in reversed(forecast_points)]
        canvas.create_polygon(
            *[coord for point in [*confidence_upper, *confidence_lower] for coord in point],
            fill="#dbe7f5",
            outline="",
        )
        canvas.create_line(*[coord for point in hydro_points for coord in point], fill=DASHBOARD_CYAN, width=3, smooth=True)
        canvas.create_line(*[coord for point in forecast_points for coord in point], fill="#1b7fc4", width=2, dash=(6, 4), smooth=True)
        now_x = chart_x1 + 170
        canvas.create_line(now_x, chart_y1, now_x, chart_y2, fill="#64748B", width=1, dash=(4, 4))
        canvas.create_text(now_x + 4, chart_y1 + 8, text="现在", fill="#64748B", font=(DASHBOARD_CARD_FONT, 7), anchor="w")
        canvas.create_oval(chart_x1 + 130, 106, chart_x1 + 142, 118, fill="#ffffff", outline=DASHBOARD_CYAN, width=2)
        canvas.create_text(chart_x1, 214, text="00:00", fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 7), anchor="w")
        canvas.create_text((chart_x1 + chart_x2) // 2, 214, text="12:00", fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 7), anchor="center")
        canvas.create_text(chart_x2, 214, text="24:00", fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 7), anchor="e")
        canvas.create_line(40, 232, 58, 232, fill=DASHBOARD_CYAN, width=3)
        canvas.create_text(64, 232, text="计算流量", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8), anchor="w")
        canvas.create_line(136, 232, 154, 232, fill="#1b7fc4", width=2, dash=(5, 3))
        canvas.create_text(160, 232, text="预报流量", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8), anchor="w")
        canvas.create_rectangle(234, 226, 252, 238, fill="#dbe7f5", outline="")
        canvas.create_text(258, 232, text="置信区间", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8), anchor="w")

    def _draw_dashboard_routing_view(self, canvas: tk.Canvas) -> None:
        river = [(42, 166), (86, 148), (130, 156), (176, 128), (222, 134), (272, 110), (338, 122)]
        canvas.create_text(42, 92, text="上游", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8, "bold"), anchor="w")
        canvas.create_text(336, 214, text="下游 · 出口", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8, "bold"), anchor="e")
        flat_river = [coord for point in river for coord in point]
        canvas.create_line(*flat_river, fill="#d9edf7", width=16, smooth=True, capstyle=tk.ROUND)
        canvas.create_line(*flat_river, fill="#86cce3", width=10, smooth=True, capstyle=tk.ROUND)
        canvas.create_line(*flat_river, fill=DASHBOARD_CYAN, width=4, smooth=True, capstyle=tk.ROUND)
        canvas.create_line(306, 118, 336, 122, 324, 110, fill=DASHBOARD_CYAN, width=2, arrow=tk.LAST, arrowshape=(10, 12, 5), smooth=True)
        for x, y in ((80, 118), (124, 104), (186, 96), (246, 88)):
            canvas.create_line(x - 22, y - 26, x, y, fill="#8bb5d2", width=2, arrow=tk.LAST, dash=(3, 3))
        for x in (82, 144, 230):
            canvas.create_line(x, 84, x + 10, 116, fill="#93c5d8", width=2, dash=(2, 5), tags=("routing_rain",))
        for label, x, y, q in (("S1", 106, 148, "385"), ("S2", 208, 128, "870"), ("S3", 306, 118, "1280")):
            canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill="#ffffff", outline="#1565c0", width=2)
            canvas.create_text(x, y - 18, text=f"{label}  Q={q}", fill=DASHBOARD_TEXT, font=(DASHBOARD_MONO_FONT, 8, "bold"), anchor="center")
        wave = canvas.create_oval(38, 158, 54, 174, fill="#ffffff", outline="#38bdf8", width=3, tags=("routing_wave",))

        def animate(step: int = 0) -> None:
            if not canvas.winfo_exists() or getattr(self, "_dashboard_model_view", "flow") != "routing":
                return
            phase = (step % 39) / 38
            segment = min(int(phase * (len(river) - 1)), len(river) - 2)
            local = phase * (len(river) - 1) - segment
            x1, y1 = river[segment]
            x2, y2 = river[segment + 1]
            x = x1 + (x2 - x1) * local
            y = y1 + (y2 - y1) * local
            canvas.coords(wave, x - 8, y - 8, x + 8, y + 8)
            canvas.itemconfigure("routing_rain", fill="#93c5d8" if step % 4 < 2 else "#c7e6f1")
            self._schedule_dashboard_model_frame(lambda: animate(step + 1), 180)

        animate()

    def _draw_dashboard_section_view(self, canvas: tk.Canvas) -> None:
        base = [(54, 196), (110, 118), (260, 118), (326, 196)]
        canvas.create_line(*[coord for point in base for coord in point], fill="#566b7d", width=2, smooth=True)
        canvas.create_line(54, 196, 326, 196, fill="#566b7d", width=2)
        canvas.create_polygon(84, 162, 124, 130, 248, 130, 290, 162, 290, 196, 84, 196, fill="#dbeafe", outline="", tags=("section_water",))
        water_line = canvas.create_line(84, 162, 290, 162, fill="#1565c0", width=3, tags=("section_water_line",))
        canvas.create_line(72, 106, 320, 106, fill="#DC2626", width=1, dash=(5, 4))
        canvas.create_text(318, 101, text="警戒水位", fill="#DC2626", font=(DASHBOARD_CARD_FONT, 8), anchor="e")
        canvas.create_line(92, 144, 304, 144, fill="#F59E0B", width=1, dash=(5, 4))
        canvas.create_text(302, 139, text="壅水线", fill="#F59E0B", font=(DASHBOARD_CARD_FONT, 8), anchor="e")
        canvas.create_arc(142, 82, 248, 176, start=0, extent=180, style=tk.ARC, outline="#94a3b8", width=3)
        canvas.create_line(142, 128, 142, 196, fill="#94a3b8", width=3)
        canvas.create_line(248, 128, 248, 196, fill="#94a3b8", width=3)
        canvas.create_text(194, 78, text="水工建筑物", fill="#64748B", font=(DASHBOARD_CARD_FONT, 8, "bold"), anchor="center")
        canvas.create_text(78, 218, text="左岸", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8), anchor="center")
        canvas.create_text(304, 218, text="右岸", fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8), anchor="center")

        def animate(step: int = 0) -> None:
            if not canvas.winfo_exists() or getattr(self, "_dashboard_model_view", "flow") != "section":
                return
            offset = int(math.sin(step / 31 * math.tau) * 6)
            y = 162 + offset
            canvas.coords("section_water", 84, y, 124, 130 + offset, 248, 130 + offset, 290, y, 290, 196, 84, 196)
            canvas.coords(water_line, 84, y, 290, y)
            self._schedule_dashboard_model_frame(lambda: animate(step + 1), 160)

        animate()

    def _draw_dashboard_metric_cards(self, canvas: tk.Canvas) -> None:
        metric_data = (
            ("洪峰流量", "—", "m³/s", DASHBOARD_CYAN, 366, 78),
            ("断面水位", "—", "m", DASHBOARD_PURPLE, 390, 134),
            ("降雨雨型", "待计算", "", "#2e7d32", 366, 190),
        )
        for label, value, unit, color, x, y in metric_data:
            _canvas_round_rect(canvas, x, y, x + 146, y + 48, 10, fill="#ffffff", outline=DASHBOARD_BORDER)
            canvas.create_text(x + 14, y + 14, text=label, fill=DASHBOARD_MUTED, font=(DASHBOARD_CARD_FONT, 8), anchor="w")
            canvas.create_text(x + 14, y + 34, text=value, fill=color, font=(DASHBOARD_MONO_FONT, 16, "bold"), anchor="w")
            if unit:
                canvas.create_text(x + 96, y + 34, text=unit, fill=DASHBOARD_DEEP_MUTED, font=(DASHBOARD_MONO_FONT, 8), anchor="w")

    def _draw_module_icon(self, canvas: tk.Canvas, key: str, accent: str) -> None:
        canvas.create_oval(6, 6, 72, 72, fill="#eef9f9", outline="#d6e7f0", width=1)
        canvas.create_oval(17, 17, 61, 61, outline="#cce6ef", width=1)
        if key == "peak":
            canvas.create_line(16, 54, 27, 42, 38, 49, 50, 24, 64, 34, fill=DASHBOARD_CYAN, width=4, smooth=True)
            canvas.create_line(16, 58, 64, 58, fill="#5a9bb8", width=2)
        elif key == "irrigation_channel":
            canvas.create_line(12, 58, 66, 58, fill=DASHBOARD_CYAN, width=4, smooth=True)
            canvas.create_line(14, 45, 64, 45, fill=accent, width=3)
            canvas.create_line(22, 28, 22, 58, fill=DASHBOARD_CYAN, width=4)
            canvas.create_arc(21, 22, 57, 58, start=0, extent=180, style=tk.ARC, outline=accent, width=3)
            canvas.create_line(35, 33, 51, 49, fill="#66e8ff", width=3)
        elif key == "water_level":
            canvas.create_line(14, 50, 24, 38, 36, 44, 50, 31, 64, 40, fill="#8de8ff", width=3, smooth=True)
            canvas.create_line(12, 56, 66, 56, fill=accent, width=4)
            canvas.create_rectangle(24, 22, 31, 56, fill=DASHBOARD_CYAN, outline="")
        elif key == "backwater":
            canvas.create_line(13, 54, 66, 54, fill="#b8c9dd", width=3)
            canvas.create_polygon(22, 54, 45, 20, 68, 54, fill="#e8f4f8", outline=accent)
            canvas.create_line(22, 43, 58, 43, fill=DASHBOARD_CYAN, width=3)
        elif key == "bridge_opening":
            canvas.create_line(12, 56, 66, 56, fill="#b8c9dd", width=3)
            canvas.create_rectangle(18, 26, 27, 56, fill=accent, outline="")
            canvas.create_rectangle(51, 26, 60, 56, fill=accent, outline="")
            canvas.create_arc(22, 18, 56, 66, start=0, extent=180, style=tk.ARC, outline=DASHBOARD_CYAN, width=4)
            canvas.create_line(17, 44, 61, 44, fill="#8ee7ff", width=2)
        elif key == "river_evolution":
            canvas.create_line(12, 55, 66, 55, fill="#b8c9dd", width=3)
            canvas.create_line(14, 45, 25, 35, 36, 40, 47, 25, 63, 31, fill=DASHBOARD_CYAN, width=4, smooth=True)
            canvas.create_line(16, 62, 28, 58, 42, 63, 60, 58, fill="#8ee36f", width=3, smooth=True)
            canvas.create_oval(26, 19, 35, 28, fill=accent, outline="")
        elif key == "reservoir":
            canvas.create_line(12, 58, 66, 58, fill="#b8c9dd", width=3)
            canvas.create_polygon(18, 55, 26, 25, 35, 55, fill=accent, outline=accent)
            canvas.create_line(35, 42, 64, 42, fill=DASHBOARD_CYAN, width=3)
            canvas.create_line(35, 49, 64, 49, fill="#75d4e8", width=3)
            canvas.create_line(35, 56, 64, 56, fill="#9beeff", width=3)
        elif key == "hydropower_pipe":
            canvas.create_line(12, 58, 66, 58, fill="#b8c9dd", width=3)
            canvas.create_polygon(18, 25, 29, 25, 24, 45, 36, 45, 22, 68, 26, 51, 16, 51, fill=DASHBOARD_CYAN, outline=DASHBOARD_CYAN)
            canvas.create_line(42, 26, 66, 26, fill=accent, width=4)
            canvas.create_line(42, 40, 66, 40, fill="#9de9ff", width=4)
            canvas.create_line(42, 54, 66, 54, fill="#73c8e0", width=4)
        else:
            canvas.create_polygon(15, 48, 36, 24, 62, 48, fill="#e8f4f8", outline=DASHBOARD_CYAN, width=2)
            canvas.create_line(17, 56, 64, 56, fill=accent, width=3)
            canvas.create_line(25, 59, 18, 68, fill="#b99bff", width=2)

    def _authenticate_membership(self) -> dict[str, str] | None:
        version_info = getattr(self, "version_info", load_version_info())
        state = load_membership_state()
        if not version_info.auth_required:
            if state.get("license_mode") == "local" and is_membership_active(state):
                local_state = dict(state)
            else:
                now = datetime.now()
                local_state = {
                    "phone": "local-debug",
                    "plan": "本机测试授权",
                    "price": "0",
                    "start_at": now.isoformat(timespec="seconds"),
                    "expire_at": (now + timedelta(days=3650)).isoformat(timespec="seconds"),
                    "license_mode": "local",
                    "server_url": "local-debug",
                }
            local_state["phone"] = local_state.get("phone") or "local-debug"
            local_state["plan"] = "本机测试授权"
            local_state["license_mode"] = "local"
            save_membership_state(local_state)
            return local_state
        if state.get("license_mode") == "online" and state.get("phone") and state.get("access_token"):
            try:
                server_url = version_info.auth_server_url if version_info.auth_required else (state.get("server_url") or version_info.auth_server_url)
                client = OnlineAuthClient(AuthClientConfig(server_url=server_url))
                verified_state = client.heartbeat(state, version_info.version)
                save_membership_state(verified_state)
                return verified_state
            except AuthServerRejectedError:
                pass
            except AuthClientError as exc:
                if is_offline_usable(state, version_info.offline_grace_days):
                    offline_state = dict(state)
                    offline_state["auth_warning"] = f"登录服务暂不可用，已按离线宽限继续使用：{exc}"
                    return offline_state
                if not is_membership_active(state):
                    inactive_state = dict(state)
                    inactive_state["auth_warning"] = f"登录服务暂不可用，暂按本机已登录账号显示开通入口：{exc}"
                    return inactive_state
        if (
            not version_info.auth_required
            and state.get("license_mode") == "local"
            and is_membership_active(state)
        ):
            return state
        if (
            os.environ.get("HYDROLOGY_ALLOW_LOCAL_LICENSE") == "1"
            and state.get("license_mode") == "local"
            and is_membership_active(state)
        ):
            return state
        dialog = OnlineLoginDialog(self, version_info)
        self.wait_window(dialog)
        return dialog.result if dialog.result else None

    def _logout_membership(self) -> None:
        phone = self.membership_state.get("phone", "") if self.membership_state else ""
        if not messagebox.askyesno("退出登录", f"确定退出当前账号 {phone} 吗？\n退出后重新打开软件即可切换账号。"):
            return
        clear_membership_state()
        messagebox.showinfo("已退出登录", "本机登录状态已清除。请重新打开软件并登录需要使用的账号。")
        self.destroy()

    def _open_personal_center(self) -> None:
        dialog = PersonalCenterDialog(self, self.membership_state, self._membership_server_url())
        self.wait_window(dialog)
        if dialog.result:
            self.membership_state = dialog.result
            save_membership_state(self.membership_state)
            self._refresh_member_header()

    def _add_member_header(self) -> None:
        expire_at = parse_membership_expire_at(self.membership_state)
        expire_text = expire_at.strftime("%Y-%m-%d") if expire_at else "未知"
        account_text = _membership_account_display(self.membership_state)
        header = tk.Frame(self, bg="#ffffff", padx=16, pady=10, highlightthickness=1, highlightbackground=DASHBOARD_BORDER)
        header.pack(fill=tk.X)
        logo = tk.Canvas(header, width=42, height=42, bg="#ffffff", highlightthickness=0)
        logo.grid(row=0, column=0, rowspan=2, sticky="w", padx=(0, 12))
        _canvas_round_rect(logo, 2, 2, 40, 40, 11, fill=DASHBOARD_CYAN, outline="")
        logo.create_text(21, 21, text="HT", fill="#ffffff", font=(DASHBOARD_MONO_FONT, 13, "bold"), anchor="center")
        tk.Label(
            header,
            text="慧通智达水文计算云平台",
            bg="#ffffff",
            fg=DASHBOARD_TEXT,
            font=(DASHBOARD_CARD_FONT, 17, "bold"),
        ).grid(row=0, column=1, sticky="w")
        tk.Label(
            header,
            text="智算云平台 V6.0  |  洪峰流量 · 水位 · 壅水 · 冲刷 · 水利大算盘",
            bg="#ffffff",
            fg=DASHBOARD_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=1, column=1, sticky="w", pady=(2, 0))
        search = tk.Frame(header, bg="#f4f8fb", highlightthickness=1, highlightbackground="#d7e3ed")
        search.grid(row=0, column=2, rowspan=2, sticky="ew", padx=(26, 18), pady=3)
        search.columnconfigure(0, weight=1)
        tk.Label(
            search,
            text="搜索程序、公式或规范",
            bg="#f4f8fb",
            fg=DASHBOARD_DEEP_MUTED,
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=0, column=0, sticky="w", padx=(14, 8), pady=8)
        tk.Label(
            search,
            text="智能",
            bg="#e5f1f8",
            fg=DASHBOARD_CYAN,
            font=(DASHBOARD_CARD_FONT, 9, "bold"),
            padx=8,
            pady=3,
        ).grid(row=0, column=1, sticky="e", padx=(0, 8), pady=6)
        if is_membership_active(self.membership_state):
            plan = str(self.membership_state.get("plan", "") or "会员")
            online_status = "离线授权" if self.membership_state.get("auth_warning") else "在线授权"
            if "终身" in plan or (expire_at and expire_at.year >= 2099):
                license_text = "● 终身会员 · 永久授权"
            else:
                license_text = f"● {plan} · 到期 {expire_text}"
            member_text = (
                f"账号：{account_text}    "
                f"{license_text}    "
                f"● {online_status}"
            )
        else:
            member_text = f"账号：{account_text}    会员未开通    {trial_usage_text(self.membership_state)}"
        self.member_label = tk.Label(
            header,
            text=member_text,
            bg="#ffffff",
            fg=DASHBOARD_SILVER,
            padx=14,
            pady=8,
            font=(DASHBOARD_CARD_FONT, 11, "bold"),
            highlightthickness=1,
            highlightbackground=DASHBOARD_BORDER,
            anchor="e",
            justify=tk.RIGHT,
            wraplength=420,
        )
        self.member_label.grid(row=0, column=3, rowspan=2, sticky="ew", padx=(8, 0))
        tk.Button(
            header,
            text="个人中心",
            command=self._open_personal_center,
            bg="#ffffff",
            fg=DASHBOARD_STAR,
            activebackground="#eef9f9",
            activeforeground=DASHBOARD_STAR,
            relief=tk.FLAT,
            cursor="hand2",
            padx=12,
            pady=8,
            font=(DASHBOARD_CARD_FONT, 11, "bold"),
        ).grid(row=0, column=4, rowspan=2, sticky="e", padx=(10, 0))
        tk.Button(
            header,
            text="退出登录",
            command=self._logout_membership,
            bg="#fff7f7",
            fg="#b91c1c",
            activebackground="#fee2e2",
            activeforeground="#991b1b",
            relief=tk.FLAT,
            cursor="hand2",
            padx=12,
            pady=8,
            font=(DASHBOARD_CARD_FONT, 11, "bold"),
        ).grid(row=0, column=5, rowspan=2, sticky="e", padx=(8, 0))
        header.columnconfigure(2, weight=1, minsize=260)
        header.columnconfigure(3, weight=0, minsize=300)

    def _refresh_member_header(self) -> None:
        label = getattr(self, "member_label", None)
        if label is None:
            return
        expire_at = parse_membership_expire_at(self.membership_state)
        expire_text = expire_at.strftime("%Y-%m-%d") if expire_at else "未知"
        account_text = _membership_account_display(self.membership_state)
        if is_membership_active(self.membership_state):
            plan = str(self.membership_state.get("plan", "") or "会员")
            online_status = "离线授权" if self.membership_state.get("auth_warning") else "在线授权"
            if "终身" in plan or (expire_at and expire_at.year >= 2099):
                license_text = "● 终身会员 · 永久授权"
            else:
                license_text = f"● {plan} · 到期 {expire_text}"
            text = (
                f"账号：{account_text}    "
                f"{license_text}    "
                f"● {online_status}"
            )
        else:
            text = f"账号：{account_text}    会员未开通    {trial_usage_text(self.membership_state)}"
        label.configure(text=text)

    def _add_category_banner(self, parent: ttk.Frame, title: str, desc: str, bg: str, fg: str) -> None:
        banner = tk.Frame(parent, bg=bg, padx=18, pady=12)
        banner.grid(row=0, column=0, sticky="ew", padx=8, pady=(8, 4))
        tk.Label(
            banner,
            text=title,
            bg=bg,
            fg=fg,
            font=(DASHBOARD_CARD_FONT, 16, "bold"),
        ).grid(row=0, column=0, sticky="w")
        tk.Label(
            banner,
            text=desc,
            bg=bg,
            fg="#394a58",
            font=(DASHBOARD_CARD_FONT, 10),
        ).grid(row=1, column=0, sticky="w", pady=(4, 0))
        banner.columnconfigure(0, weight=1)

    def _add_method_data_menu(
        self,
        parent: tk.Menu,
        method_name: str,
        folder_name: str,
        extra_commands: list[tuple[str, Callable[[], None]]],
    ) -> None:
        submenu = tk.Menu(parent, tearoff=0)
        submenu.add_command(label="打开资料文件夹", command=lambda folder=folder_name: self.open_method_data_folder(folder))
        submenu.add_command(label="方法说明", command=lambda name=method_name: self.show_method_help(name))
        for label, command in extra_commands:
            submenu.add_command(label=label, command=command)
        parent.add_cascade(label=method_name, menu=submenu)

    def open_data_folder(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料"
        os.startfile(path)

    def open_formula_audit_report(self) -> None:
        root = Path(__file__).resolve().parent.parent
        candidates = [
            root / "validation" / "水文计算软件全公式新人例题验收报告.md",
            root / "资料" / "软件验收报告" / "水文计算软件全公式新人例题验收报告.md",
        ]
        for path in candidates:
            if path.exists():
                os.startfile(path)
                return
        messagebox.showerror("例题验收报告缺失", "未找到验收报告，请重新启动软件完成资料更新。")

    def open_method_data_folder(self, folder_name: str) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / folder_name
        if not path.exists():
            messagebox.showerror("资料缺失", f"未找到资料文件夹：{path}")
            return
        os.startfile(path)

    def open_pearson_table(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "皮尔逊Ⅲ型曲线模比系数计算.xls"
        if not path.exists():
            messagebox.showerror("资料缺失", f"未找到资料文件：{path}")
            return
        os.startfile(path)

    def open_regional_formula_excel(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "地区经验公式法" / "洪峰流量经验公式法(3).xlsx"
        if not path.exists():
            messagebox.showerror("资料缺失", f"未找到资料文件：{path}")
            return
        os.startfile(path)

    def open_tieyuan_formula_tables(self) -> None:
        base = Path(__file__).resolve().parent.parent / "资料" / "铁一院法"
        show_image_gallery(
            self,
            "铁一院法 - 公式与参数表",
            [
                base / "公式页_3-1_洪峰流量公式.png",
                base / "示例页_计算步骤与Qm.png",
                base / "表3-3_径流系数C值表.png",
                base / "表3-4_A2与表3-5_y和表3-6_gamma.png",
                base / "表3-7_Cn值表.png",
                base / "表3-8_R_r1值表.png",
                base / "表3-9_P1值表.png",
                base / "表3-10_P值表.png",
                base / "表3-11_设计洪水过程线alpha值.png",
            ],
        )

    def show_pearson_help(self) -> None:
        messagebox.showinfo(
            "P-III计算公式说明",
            "输入：均值、Cv、Cs/Cv。\n"
            "Cs = Cv * (Cs/Cv)\n"
            "tp = GAMMAINV(1 - P/100, 4/Cs^2, 1)\n"
            "Φ = Cs/2 * tp - 2/Cs\n"
            "Kp = 1 + Cv * Φ\n"
            "设计值 = 均值 * Kp\n\n"
            "公式来源：资料菜单中的《皮尔逊Ⅲ型曲线模比系数计算.xls》。",
        )

    def open_small_river_formula_tables(self) -> None:
        base = Path(__file__).resolve().parent.parent / "资料" / "中小河流经验公式法"
        show_image_gallery(
            self,
            "中小河流经验公式法 - 分区公式表",
            [
                base / "分区公式表_1_2.png",
                base / "分区公式表_3_4.png",
                base / "分区公式表_5_6.png",
                base / "分区公式表_7_8.png",
            ],
        )

    def open_instant_unit_line_tables(self) -> None:
        base = Path(__file__).resolve().parent.parent / "资料" / "瞬时单位线法"
        show_image_gallery(
            self,
            "瞬时单位线法 - 附表10雨型分配表与附表12入渗率表",
            [
                base / "附表10_河西区.png",
                base / "附表10_黄河干支流区.png",
                base / "附表10_渭河区.png",
                base / "附表10_泾河区.png",
                base / "附表10_嘉陵江区.png",
                base / "附表12_河西石山森林草原区f值表.png",
                base / "附表12_陇南石山林区f值表.png",
                base / "附表12_黄土区f值表.png",
                base / "附表12_黄土山林区f值表.png",
                base / "附表12_六盘山土石山区f值表.png",
            ],
        )

    def open_instant_formula_pdf(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "瞬时单位线法" / "提取自甘肃省暴雨洪水图集.pdf"
        if not path.exists():
            messagebox.showerror("资料缺失", f"未找到资料文件：{path}")
            return
        os.startfile(path)

    def open_manning_roughness_tables(self) -> None:
        base = Path(__file__).resolve().parent.parent / "资料" / "曼宁公式水位计算"
        show_image_gallery(
            self,
            "曼宁公式水位计算 - 河道与滩地糙率表",
            [
                base / "天然河道糙率表.png",
                base / "滩地糙率表.png",
                base / "综合糙率加权平均示意.png",
            ],
        )

    def open_weir_excel(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "堰流公式计算水位" / "堰流计算程序.xls"
        if not path.exists():
            messagebox.showerror("资料缺失", f"未找到资料文件：{path}")
            return
        os.startfile(path)

    def open_weir_pdf(self) -> None:
        path = Path(__file__).resolve().parent.parent / "资料" / "堰流公式计算水位" / "水利大算盘.pdf"
        if not path.exists():
            messagebox.showerror("资料缺失", f"未找到资料文件：{path}")
            return
        os.startfile(path)

    def open_rational_formula_tables(self) -> None:
        base = Path(__file__).resolve().parent.parent / "资料" / "推理公式法"
        show_image_gallery(
            self,
            "推理公式法 - 图集公式与汇流曲线",
            [
                base / "01_产流与入渗公式.png",
                base / "02_推理公式基本公式.png",
                base / "03_汇流参数表10.png",
                base / "04_设计洪水过程线公式.png",
                base / "05_附图58_黄土区Ka_Kw_KT综合关系图.png",
                base / "06_附图59_六盘山区Ka_Kw_KT综合关系图.png",
                base / "07_附图66_各分区基流量与流域面积关系图.png",
            ],
        )

    def show_method_help(self, method_name: str) -> None:
        content = {
            "铁一院两所法": (
                "铁一院两所法\n\n"
                "主要输入：流域面积F、河长L₁、河坡J₁、各频率24小时暴雨量H₂₄，以及A₁、A₂、n、β₁。\n"
                "主要输出：L₂、J₂、η、γ、β、各频率洪峰流量Q。\n"
                "注意：Python版不再用H₂₄_5代替H₂₄_2，缺少H₂₄_2时不计算Q2。"
            ),
            "中小河流经验公式法": (
                "中小河流经验公式法\n\n"
                "主要输入：分区、流域面积F(km²)、河长L(km)、比降J(小数)、Cv、面积控制历时雨量H(mm)。\n"
                "分区：I 黄土沟壑区，II 石山林区，III 土山林区，IV 草原区，V 土石山沟壑混合区，VI 祁连山区，VII 荒漠区，VIII 河西走廊区。\n"
                f"雨量历时：{SMALL_RIVER_DURATION_RULE_TEXT}\n"
                f"H含义：{SMALL_RIVER_RAINFALL_SCOPE_TEXT}\n"
                "输入方式：推荐直接填写10%、5%、2%、1%流域平均设计H；只有流域平均雨量均值H0时，才按P-III计算频率Kp。单站点雨量还须先进行面积平均或点面折减。\n"
                "结果区会列出所选分区的K、b、n、m、α、β参数表。\n\n"
                "原始分区公式表可在：帮助 -> 中小河流经验公式法 -> 分区公式表 查看。"
            ),
            "水文面积比拟法": (
                "水文面积比拟法\n\n"
                "主要输入：参证站面积(km²)、设计流域面积(km²)、参证站各频率洪峰流量(m³/s)、面积指数n。\n"
                "主要输出：按面积比拟换算后的设计流域洪峰流量。\n\n"
                "右侧水文站表用于线性回归计算各频率面积指数n，并应用到面积比拟公式。"
            ),
            "双对数内插法": (
                "双对数内插法\n\n"
                "主要输入：设计流域面积F设(km²)、多个参证水文站面积(km²)及各频率洪峰流量(m³/s)。\n"
                "计算方法：软件按设计流域面积自动选择相邻两个水文站，在lgF-lgQ坐标上内插各频率Q设。\n"
                "公式：lgQ设 = lgQ1 + (lgQ2-lgQ1) * (lgF设-lgF1) / (lgF2-lgF1)。\n"
                "若F设不在已填水文站面积范围内，结果表会标为外推，正式成果应复核参证站代表性。"
            ),
            "洪水合成法": (
                "洪水合成法\n\n"
                "主要输入：统一计算时段Δt、各组成流域或断面的洪水过程线、各过程线到合成断面的时间滞后。\n"
                "计算方法：先按时间滞后将各过程线平移到合成断面；若滞后时间不是Δt整数倍，则按相邻时段线性插值；"
                "然后逐时段叠加得到Q合成(t)=ΣQi(t-滞后时间i)。\n"
                "主要输出：组成过程线参数表、合成洪水过程线计算表、洪峰流量、峰现时间和洪量。"
            ),
            "水库调洪演算": (
                "水库调洪演算\n\n"
                "用途：根据入库洪水过程线、水位-库容曲线和水位-总泄量曲线，计算水库水位、库容与下泄流量随时间的变化。\n"
                "方法：等水面Modified Puls蓄量指示法。逐时段采用[(I₁+I₂)/2-(O₁+O₂)/2]Δt=S₂-S₁，并联立S(Z)、O(Z)关系求下一时刻水位。\n"
                "主要输入：初始水位、入库过程线t-I、水位库容Z-V和水位泄量Z-O；V按万m³输入，其他单位按界面标注。\n"
                "主要输出：入库洪峰、最大下泄、削峰率、峰现延迟、最高水位、逐时段水量平衡表和过程线图。\n"
                "边界：当前泄量曲线应代表同一闸门/溢洪道运行工况。软件只在两条关系曲线的共同水位范围内插值，不做曲线外推。\n"
                "复核：调洪结果必须检查控制水位、曲线覆盖范围、退水段完整性及总水量平衡误差。"
            ),
            "桥孔计算": (
                "桥孔计算\n\n"
                "依据图片中桥孔设计第7.2节整理桥孔最小净长度计算。\n"
                "一般河段公式：开阔、顺直微弯河段，分汊、弯曲河段，滩、槽可分的不稳定河段公式一致，均采用 Lj=Kq*(Qₚ/Qc)^n₃*Bc，Kq、n₃按表7.2.1自动取值。\n"
                "宽滩河段：采用 Lj=Qₚ/(β*qc)，β=1.19*(Qc/Qt)^0.10，qc单位为m³/(s·m)。\n"
                "滩、槽难分的不稳定河段：采用 Lj=Cp*B₀，B₀=16.07*(Q̄/d̄^0.3)^0.24，Cp=(Qₚ/Q₂%)^0.33；界面d̄按mm输入，计算时换算为m代入原式。\n"
                "峡谷河段：按条文可按河床地形布孔，不宜压缩河槽，可不作桥孔最小净长度计算。\n"
                "桥孔布设还应结合桥位地形、河床地质、桥前壅水、冲刷深度、桥梁及引道纵坡、台后填土高度、通航和漂浮物通过要求综合确定。"
            ),
            "河道演变计算": (
                "河道演变计算\n\n"
                "依据防洪评价报告第3章整理，包含河道造床流量、河相关系、稳定河宽、纵向稳定、横向稳定、综合稳定和冲淤判别。\n"
                "河相关系：格鲁什科夫宽深公式 ζ=sqrt(B)/h；量纲分析公式 ζ=sqrt(B*d)/h。\n"
                "床沙平均粒径：d按mm输入，不自动换算。\n"
                "比降：J按小数输入，1‰填0.001，1.11‰填0.00111，不能直接填1或1.11。\n"
                "稳定河宽：阿尔图宁公式 B=A*Q^0.5/J^0.2。稳定河宽系数A按河段类型选取：山区0.75~0.90，山麓0.90~1.00，中游1.00~1.10，下游1.10~1.70。\n"
                "纵向稳定：希尔兹数倒数法 φh₁=d/(h*J)，洛赫庆系数法 φh₂=d/J。\n"
                "横向稳定：方法一 φb₁=Q^0.5/(J^0.2*B)，方法二 φb₂=b/B。\n"
                "综合稳定：φ=φh₁*(φb₁)^2。\n"
                "冲淤判别：ψ=(v₀^(3/2)*hᵢ)/(vᵢ^(3/2)*h₀)，ψ<1为冲刷趋势，ψ>1为淤积趋势。\n"
                "注意：正式成果应按原报告、规范或审查意见复核参数口径。"
            ),
            "堰流公式计算水位": (
                "堰流公式计算水位\n\n"
                "堰型：宽顶堰、实用堰-敞式、实用堰-胸墙孔口式、实用堰-胸墙孔口式平底闸堰、薄壁堰。\n"
                "主要输入：设计流量Q(m³/s)、堰顶或堰底高程(m)、下游水位(m)、孔数、单孔宽或溢流宽(m)及对应堰型参数。\n"
                "主要输出：反算上游水位、堰上作用水头H、hₛ/H、宽顶堰淹没系数或出流状态、流量系数和复核流量。\n"
                "宽顶堰采用 Q=δ·m·ε·n·B·sqrt(2g)·H^(3/2)，仅宽顶堰按hₛ/H区分自由溢流和淹没溢流。\n"
                "敞式实用堰采用 Q=m·ε·n·B·sqrt(2g)·H^(3/2)，不区分自由溢流和淹没溢流。\n"
                "胸墙孔口式采用 Q=μ·A·sqrt(2g(H-D))，A=n·B·D。\n"
                "薄壁堰采用 Q=m₀·b·sqrt(2g)·H^(3/2)。\n"
                "默认值按资料图示设置：宽顶堰m默认0.36，敞式实用堰m默认0.48，孔口式μ默认0.70，薄壁堰m₀默认0.42；正式成果应按项目资料复核。"
            ),
            "冲刷计算": (
                "冲刷计算\n\n"
                "公式分类：堤岸/护岸冲刷、丁坝/挑流建筑物冲刷、桥下一般冲刷、桥墩局部冲刷。\n"
                "SLT 808-2025已整理页码33~39的A.3.4、A.3.5公式，包括平行岸坡、斜冲岸坡、丁坝冲刷、桥下一般冲刷和桥墩局部冲刷。\n"
                "补充JTG C30常用64-1修正式、64-2简化式、65-1/65-2非粘性土桥墩局部冲刷公式。\n"
                "参数输入方式：在文本框按key=value填写；切换公式后自动给出该公式所需参数、默认试算值和参数解释。\n"
                "输出内容：公式来源、计算式、输入参数、中间量和最终冲刷深度或局部冲刷流速。\n"
                "注意：重要工程、敏感区域或冲刷影响较大的基础设施，应结合河床土质、粒径试验、实测水沙资料、数值模拟或河工模型复核。"
            ),
            "壅水计算": (
                "壅水计算\n\n"
                "公式来源：SLT 808-2025 A.2.3，参照TB 10017计算桥梁等阻水建筑物壅水高度及壅水曲线长度。\n"
                "壅水高度：ΔZₘ = η(Vₘ² - V²)。\n"
                "设计所需过水面积：ωx = Qₚ / (Vₚ cosα)。\n"
                "冲刷系数：P = ωx / ωg。\n"
                "面积阻水比：Δω/ω × 100%，其中Δω=ω-ωg，ω为全河天然过水面积，ωg为建后有效过水面积。\n"
                "建后断面平均流速Vₘ按土质选择：松软土Vₘ=Vₚ；中等土Vₘ=Vₚ·2P/(P+1)；密实土Vₘ=P·Vₚ。\n"
                "壅水曲线全长：Lᵧ = 2ΔZₘ / I₀。\n"
                "η按河滩路堤阻挡流量占设计流量百分比取值：≤10%取0.05，11~30%取0.07，31~50%取0.10，>50%取0.15；手动η优先。\n"
                "面积阻水比由天然与建后过水面积计算，仅作几何复核，不能代替阻挡流量比。"
            ),
            "波浪高度计算": (
                "波浪高度计算\n\n"
                "公式来源：《堤防工程设计规范》常用莆田试验站公式。\n"
                "主要输入：计算风速V、风区长度F、水域平均水深d、设计波高累积频率P或手动换算系数Kp、实际持续风时。\n"
                "主要输出：平均波高H、平均波周期T、最小风时tmin、波长L、设计波高Hp=Kp*H、波陡Hp/L。\n"
                "平均波高公式：gH/V² = 0.13*tanh[0.7*(gd/V²)^0.7]*tanh{0.0018*(gF/V²)^0.45/tanh[0.7*(gd/V²)^0.7]}。\n"
                "平均波周期公式：gT/V = 13.9*(gH/V²)^0.5；最小风时公式：gtmin/V=168*(gT/V)^3.45。波长L按有限水深色散关系迭代。\n"
                "设计波高：自动模式按GB 50286-2013表C.1.3由H/d和累积频率P线性插值Kp；超出表格范围时程序拒绝外推。\n"
                "说明：本窗口只计算波浪要素；若用于堤顶超高或护坡稳定，还需继续计算风浪爬高和安全加高。"
            ),
            "风壅水位计算": (
                "风壅水位计算\n\n"
                "公式：hz = k*V²*D*cosβ/(2*g*Hm)。\n"
                "主要输入：静水位Z0、计算风速V、风区长度D、平均水深Hm、风向夹角β、综合摩阻系数k和重力加速度g。\n"
                "主要输出：风壅增水hz、叠加后水位Z=Z0+hz。\n"
                "β为风向与计算岸段法线夹角：0°为正向吹向计算岸段，风壅最大；90°为沿岸风，风壅为0。\n"
                "默认k=3.6E-6。正式成果应按项目采用规范、库区或河段形态、设计风速和风区长度取值复核。"
            ),
            "曼宁公式水位计算": (
                "曼宁公式水位计算\n\n"
                "主要输入：断面名称、设计流量Q、糙率n、水力坡降I、断面点坐标(x,z)。\n"
                "断面点格式：0,103; 8,101; 16,100.2。x为起点距，z为高程，单位均为m。\n"
                "断面点可通过Excel/CSV导入，也可直接从表格复制粘贴里程、高程两列。\n"
                "计算公式：Q=(1/n)*A*R^(2/3)*I^(1/2)，R=A/P。\n"
                "软件按断面折线自动计算不同水位下的过水面积A、湿周P、水力半径R，并用二分法求满足设计Q的水位。\n"
                "主要输出：水位、复核流量、过水面积、湿周、水力半径、水面宽、深泓点、最大水深、平均流速和弗汝德数。\n"
                "可按默认0.2m或自定义步长生成全断面水位流量关系表；输入各频率流量后，可自动反算各频率水位并生成单独标注曲线。\n"
                "右侧可按湿周加权平均计算河道综合糙率，公式为n=Σ(nᵢχᵢ)/Σχᵢ；天然河道和滩地糙率表可在资料栏查看。"
            ),
            "能量方程水位计算": (
                "能量方程水位计算\n\n"
                "主要输入：流量Q、河段长度L、已知水位、计算方向、收缩/扩张损失系数，以及上游和下游断面资料。\n"
                "计算公式：Z1+α1V1²/2g = Z2+α2V2²/2g + hf + hj。\n"
                "沿程损失默认采用平均流量模数法；局部损失hj按收缩或扩张系数乘速度水头差计算。\n"
                "亚临界流只取临界水深以上的解，超临界流只取临界水深以下的解；近临界和混合流不自动代算。"
            ),
            "HEC水位计算": (
                "HEC水位计算\n\n"
                "主要输入：流量Q、下游控制水位、收缩/扩张损失系数，以及按下游到上游顺序排列的多个断面。\n"
                "计算方法：采用HEC标准步法逐河段求解能量方程；亚临界从下游向上游，超临界从上游向下游。\n"
                "断面表中的L为当前断面到上游下一断面的距离；最后一个上游断面不需要填写L。\n"
                "本工具为软件内置标准步法，不自动计算混合流、水跃、桥涵、堰闸和急变流。"
            ),
            "地区经验公式法": (
                "地区经验公式法\n\n"
                "主要输入：流域面积F、地区经验公式分区。\n"
                "主要输出：7个频率洪峰流量。\n"
                "软件会校验分区适用面积范围，确需超范围计算时可勾选允许超出。"
            ),
            "瞬时单位线法": (
                "瞬时单位线法\n\n"
                "主要输入：雨型分区、入渗率分区、汇流参数公式、流域面积F、河道比降J、1h/6h/24h均值与Cv、alpha/gamma。\n"
                "自动计算：按面积选择主雨峰，按P-III计算控制时段点雨量，按H3=H1^0.387*H6^0.613计算3h点雨量，"
                "按附表10分配面雨量过程，按附表12由Ptc和tc自动查算平均入渗率f，自动判定产流期tc和初损I0，"
                "再按图集公式计算m1、m2、n、k并生成净雨过程、洪水过程线和洪峰成果表。"
            ),
            "推理公式法": (
                "推理公式法\n\n"
                "主要输入：雨型分区、入渗率分区、推理公式汇流分区、流域面积F、主河道长度L、主河槽比降J、1h/6h/24h均值与Cv、alpha/gamma。\n"
                "前段计算：与瞬时单位线法一致，按P-III计算控制时段暴雨，按附表10分配时程，按附表12由Ptc和tc查平均入渗率f并生成净雨过程。\n"
                "汇流参数分区：图集表10共3个分区，分别为黄土区、六盘山土石山林区、六盘山土石山植被一般区；按项目所在水文地貌区选择。\n"
                "临界净雨深：来自图集表10，黄土区35mm、六盘山土石山林区20mm、六盘山土石山植被一般区30mm；黄土区m公式中h超过临界值时按临界值控制。\n"
                "过程线形状参数：Ka、Kw、KT由软件按附图58/59自动查算，黄土区查附图58，六盘山区查附图59；基流量按附图66自动估算，正式成果可结合实测资料复核。\n"
                "推理公式汇流：按图集表10计算m，从最大净雨强时段向前后相邻时段连续累积得到h_t，"
                "逐个假设t计算Q_t=0.278Fh_t/t和τ_t=0.278L/(mJ^(1/3)Q_t^(1/4))，再按Q_t-t与Q_t-τ_t交点求Qm、τ。"
                "随后按图集五点概化过程线与三角形过程线叠加生成洪水过程线。计算完成后可点击“生成汇流曲线图”。"
            ),
        }.get(method_name, method_name)
        show_text_window(self, method_name, content)


def main() -> None:
    enable_high_dpi_awareness()
    app = HydrologyApp()
    app.mainloop()


if __name__ == "__main__":
    main()

