import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { format } from "date-fns";
import { zhCN } from "date-fns/locale";
import { motion } from "motion/react";
import { AtSign, Camera, CheckCircle2, FileText, ListTodo, MapPin, NotebookPen, Plus, RefreshCw, Search, Send, Trash2, X } from "lucide-react";
import { flushSync } from "react-dom";
import { Link, Navigate, useLocation } from "react-router-dom";
import {
  getCurrentUser,
  getExpansionOverview,
  getOpportunityOverview,
  getProfileOverview,
  getWorkHistory,
  getWorkOverview,
  reverseWorkGeocode,
  saveWorkCheckIn,
  saveWorkDailyReport,
  uploadWorkCheckInPhoto,
  type ChannelExpansionItem,
  type CreateWorkCheckInPayload,
  type CreateWorkDailyReportPayload,
  type OpportunityItem,
  type ProfileOverview,
  type SalesExpansionItem,
  type UserProfile,
  type WorkHistoryItem,
  type WorkReportLineItem,
  type WorkTomorrowPlanItem,
} from "@/lib/auth";
import { ProtectedImage } from "@/components/ProtectedImage";
import { resolveTencentMapLocation } from "@/lib/tencentMap";
import { loadTencentMapGlApi } from "@/lib/tencentMapGl";
import { cn } from "@/lib/utils";

const reportFieldLabels = {
  sales: ["沟通内容", "后续规划"],
  channel: ["沟通内容", "后续规划"],
  opportunity: ["项目最新进展", "后续规划"],
} as const;

const workSectionItems = [
  {
    key: "checkin",
    label: "打卡",
    title: "外勤打卡",
    description: "定位、拍照并提交现场记录",
    path: "/work/checkin",
    accent: "bg-emerald-500",
    icon: MapPin,
  },
  {
    key: "report",
    label: "日报",
    title: "销售日报",
    description: "记录跟进内容与明日计划",
    path: "/work/report",
    accent: "bg-violet-600",
    icon: NotebookPen,
  },
] as const;

const defaultCheckInForm: CreateWorkCheckInPayload = {
  locationText: "",
  remark: "",
  photoUrls: [],
};

const defaultReportForm: CreateWorkDailyReportPayload = {
  workContent: "",
  lineItems: [],
  planItems: [],
  tomorrowPlan: "",
  sourceType: "manual",
};

const LOCATION_ADJUST_RADIUS_METERS = 120;
const CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS = 80;
const locationDisplayCache = new Map<string, string>();

type WorkRelationOption = {
  id: number;
  label: string;
};

type BizType = WorkReportLineItem["bizType"];
type PickerMode = "report" | "checkin";
type WorkSection = (typeof workSectionItems)[number]["key"];
type LocationPoint = {
  latitude: number;
  longitude: number;
};

type ObjectPickerState = {
  mode: PickerMode;
  lineIndex?: number;
  bizType: BizType;
  query: string;
};

function createEmptyReportLine(): WorkReportLineItem {
  return {
    workDate: format(new Date(), "yyyy-MM-dd"),
    bizType: "sales",
    bizId: 0,
    bizName: "",
    editorText: "",
    content: "",
    visitStartTime: "",
    evaluationContent: "",
    nextPlan: "",
    latestProgress: "",
    communicationTime: "",
    communicationContent: "",
  };
}

function createEmptyPlanItem(): WorkTomorrowPlanItem {
  return { content: "" };
}

function getReportStatus(status?: string) {
  if (!status) {
    return "待提交";
  }
  return status === "reviewed" || status === "已点评" ? "已点评" : "已提交";
}

export default function Work() {
  const routerLocation = useLocation();
  const currentWorkDate = format(new Date(), "yyyy-MM-dd");
  const hasAutoRefreshedLocation = useRef(false);
  const photoInputRef = useRef<HTMLInputElement | null>(null);
  const historyLoadMoreRef = useRef<HTMLDivElement | null>(null);
  const [loading, setLoading] = useState(true);
  const [refreshingLocation, setRefreshingLocation] = useState(false);
  const [submittingCheckIn, setSubmittingCheckIn] = useState(false);
  const [submittingReport, setSubmittingReport] = useState(false);
  const [uploadingPhoto, setUploadingPhoto] = useState(false);
  const [mobilePanel, setMobilePanel] = useState<"entry" | "history">("entry");
  const [locationHint, setLocationHint] = useState("");
  const [locationAdjustOpen, setLocationAdjustOpen] = useState(false);
  const [locationAdjustOrigin, setLocationAdjustOrigin] = useState<LocationPoint | null>(null);
  const [locationAccuracyMeters, setLocationAccuracyMeters] = useState<number | undefined>();
  const [locationAdjustmentConfirmed, setLocationAdjustmentConfirmed] = useState(false);
  const [pageError, setPageError] = useState("");
  const [checkInError, setCheckInError] = useState("");
  const [reportError, setReportError] = useState("");
  const [checkInSuccess, setCheckInSuccess] = useState("");
  const [reportSuccess, setReportSuccess] = useState("");
  const [historyData, setHistoryData] = useState<WorkHistoryItem[]>([]);
  const [historyLoading, setHistoryLoading] = useState(true);
  const [historyLoadingMore, setHistoryLoadingMore] = useState(false);
  const [historyHasMore, setHistoryHasMore] = useState(false);
  const [historyPage, setHistoryPage] = useState(1);
  const [reportStatus, setReportStatus] = useState<string>();
  const [currentUser, setCurrentUser] = useState<UserProfile | null>(null);
  const [profileOverview, setProfileOverview] = useState<ProfileOverview | null>(null);
  const [checkInPhotoUrls, setCheckInPhotoUrls] = useState<string[]>([]);
  const [salesOptions, setSalesOptions] = useState<WorkRelationOption[]>([]);
  const [channelOptions, setChannelOptions] = useState<WorkRelationOption[]>([]);
  const [opportunityOptions, setOpportunityOptions] = useState<WorkRelationOption[]>([]);
  const [reportTargetsLoaded, setReportTargetsLoaded] = useState(false);
  const [reportTargetsLoading, setReportTargetsLoading] = useState(false);
  const [checkInForm, setCheckInForm] = useState<CreateWorkCheckInPayload>(defaultCheckInForm);
  const [reportForm, setReportForm] = useState<CreateWorkDailyReportPayload>(defaultReportForm);
  const [objectPicker, setObjectPicker] = useState<ObjectPickerState | null>(null);
  const [historyDetailItem, setHistoryDetailItem] = useState<WorkHistoryItem | null>(null);
  const [previewPhoto, setPreviewPhoto] = useState<{ url: string; alt: string } | null>(null);
  const activeWorkSection = getWorkSection(routerLocation.pathname);
  const historySection = activeWorkSection ?? "checkin";
  const activeWorkMeta = activeWorkSection
    ? workSectionItems.find((item) => item.key === activeWorkSection)
    : null;

  const pickerOptions = useMemo(() => {
    if (!objectPicker) {
      return [];
    }
    const options = getOptionsByBizType(objectPicker.bizType, salesOptions, channelOptions, opportunityOptions);
    const keyword = normalizeObjectPickerQuery(objectPicker.query).toLowerCase();
    if (!keyword) {
      return options;
    }
    return options.filter((option) => option.label.toLowerCase().includes(keyword));
  }, [objectPicker, salesOptions, channelOptions, opportunityOptions]);

  const loadReportTargets = useCallback(async () => {
    if (reportTargetsLoaded || reportTargetsLoading) {
      return;
    }
    setReportTargetsLoading(true);
    const [expansionResult, opportunityResult] = await Promise.allSettled([
      getExpansionOverview(""),
      getOpportunityOverview(),
    ]);

    if (expansionResult.status === "fulfilled") {
      setSalesOptions(buildSalesOptions(expansionResult.value.salesItems ?? []));
      setChannelOptions(buildChannelOptions(expansionResult.value.channelItems ?? []));
    }

    if (opportunityResult.status === "fulfilled") {
      setOpportunityOptions(buildOpportunityOptions(opportunityResult.value.items ?? []));
    }
    if (expansionResult.status === "fulfilled" && opportunityResult.status === "fulfilled") {
      setReportTargetsLoaded(true);
    }
    setReportTargetsLoading(false);
  }, [reportTargetsLoaded, reportTargetsLoading]);

  useEffect(() => {
    void loadOverview();
  }, []);

  useEffect(() => {
    void loadHistory(historySection, 1, true);
  }, [historySection]);

  useEffect(() => {
    const sentinel = historyLoadMoreRef.current;
    if (!sentinel || !historyHasMore || historyLoading || historyLoadingMore) {
      return;
    }

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries.some((entry) => entry.isIntersecting)) {
          void loadHistory(historySection, historyPage + 1, false);
        }
      },
      {
        rootMargin: "120px 0px",
      },
    );

    observer.observe(sentinel);
    return () => observer.disconnect();
  }, [historyHasMore, historyLoading, historyLoadingMore, historyPage, historySection]);

  useEffect(() => {
    if (!historyDetailItem) {
      return;
    }
    const nextItem = historyData.find((item) => item.id === historyDetailItem.id && item.type === historyDetailItem.type) ?? null;
    setHistoryDetailItem(nextItem);
  }, [historyData, historyDetailItem]);

  useEffect(() => {
    let cancelled = false;

    async function loadUserContext() {
      try {
        const [userData, overviewData] = await Promise.all([
          getCurrentUser().catch(() => null),
          getProfileOverview().catch(() => null),
        ]);
        if (cancelled) {
          return;
        }
        setCurrentUser(userData);
        setProfileOverview(overviewData);
      } catch {
        if (!cancelled) {
          setCurrentUser(null);
          setProfileOverview(null);
        }
      }
    }

    void loadUserContext();
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    if (loading || hasAutoRefreshedLocation.current) {
      return;
    }
    hasAutoRefreshedLocation.current = true;
    void handleRefreshLocation();
  }, [loading]);

  async function loadOverview() {
    setLoading(true);
    setPageError("");

    try {
      const data = await getWorkOverview();
      setReportStatus(data.todayReport?.status);
      setCheckInForm({
        ...defaultCheckInForm,
        locationText: "",
        remark: "",
        bizType: data.todayCheckIn?.bizType,
        bizId: data.todayCheckIn?.bizId,
        bizName: data.todayCheckIn?.bizName,
        userName: data.todayCheckIn?.userName,
        deptName: data.todayCheckIn?.deptName,
      });
      setCheckInPhotoUrls([]);
      setLocationHint("");
      setLocationAccuracyMeters(undefined);
      setLocationAdjustmentConfirmed(false);
      setReportForm({
        workContent: data.todayReport?.workContent || "",
        lineItems: data.todayReport?.lineItems?.length
          ? data.todayReport.lineItems.map(normalizeLoadedLineItem)
          : [createEmptyReportLine()],
        planItems: data.todayReport?.planItems?.length
          ? data.todayReport.planItems.map((item) => ({ content: item.content || "" }))
          : [createEmptyPlanItem()],
        tomorrowPlan: data.todayReport?.tomorrowPlan ?? "",
        sourceType: data.todayReport?.sourceType ?? "manual",
      });
    } catch (error) {
      setPageError(error instanceof Error ? error.message : "工作台数据加载失败");
      setHistoryData([]);
      setReportStatus(undefined);
    } finally {
      setLoading(false);
    }
  }

  async function loadHistory(section: WorkSection, page = 1, replace = false) {
    if (replace) {
      setPageError("");
      setHistoryData([]);
      setHistoryLoading(true);
    } else {
      setHistoryLoadingMore(true);
    }

    try {
      const data = await getWorkHistory(section, page, 8);
      const nextItems = data.items ?? [];
      setPageError("");
      setHistoryData((current) => replace ? nextItems : [...current, ...nextItems]);
      setHistoryHasMore(Boolean(data.hasMore));
      setHistoryPage(data.page ?? page);
    } catch (error) {
      if (replace) {
        setHistoryData([]);
        setPageError(error instanceof Error ? error.message : "历史记录加载失败");
      }
      setHistoryHasMore(false);
    } finally {
      setHistoryLoading(false);
      setHistoryLoadingMore(false);
    }
  }

  const handleRefreshLocation = async () => {
    setCheckInError("");
    setRefreshingLocation(true);
    if (!window.isSecureContext && location.hostname !== "localhost" && location.hostname !== "127.0.0.1") {
      setLocationHint("当前地址不是 HTTPS，手机浏览器会拦截定位。请使用 HTTPS 地址打开。");
      setRefreshingLocation(false);
      return;
    }

    setLocationHint("正在获取当前位置...");
    try {
      const position = await resolveTencentMapLocation();
      const latitude = position.latitude;
      const longitude = position.longitude;
      const displayName = await resolveLocationDisplayName(latitude, longitude, position.address);
      setCheckInForm((prev) => ({
        ...prev,
        locationText: displayName || formatLocationFallback(),
        latitude,
        longitude,
      }));
      setLocationAdjustOrigin({ latitude, longitude });
      setLocationAccuracyMeters(position.accuracy);
      setLocationAdjustmentConfirmed(false);
      const accuracyText = position.accuracy ? `当前精度约 ${Math.round(position.accuracy)} 米` : "定位已刷新";
      const sourceText = position.sourceType === "tencent" ? "腾讯定位" : "浏览器定位";
      setLocationHint(
        position.accuracy && position.accuracy > CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS
          ? `${sourceText} ${accuracyText}，当前精度超过 ${CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS} 米，不能直接打卡，请先重新定位或调整位置确认。`
          : `${sourceText} ${accuracyText}，已通过腾讯地图解析当前位置；地图可在附近 ${LOCATION_ADJUST_RADIUS_METERS} 米内精调。`,
      );
    } catch (error) {
      setCheckInForm((prev) => ({
        ...prev,
        locationText: "",
        latitude: undefined,
        longitude: undefined,
      }));
      setLocationAdjustOrigin(null);
      setLocationAccuracyMeters(undefined);
      setLocationAdjustmentConfirmed(false);
      setLocationHint(error instanceof Error ? error.message : "定位获取失败，请开启定位权限后重试。");
    } finally {
      setRefreshingLocation(false);
    }
  };

  const handleOpenLocationAdjust = () => {
    if (!checkInForm.latitude || !checkInForm.longitude) {
      setCheckInError("请先刷新定位，再调整打卡位置。");
      return;
    }
    setCheckInError("");
    setLocationAdjustOpen(true);
  };

  const handleApplyLocationAdjust = async (point: LocationPoint, resolvedAddress?: string) => {
    const displayName = isReadableLocationText(resolvedAddress)
      ? normalizeLocationText(resolvedAddress)
      : await resolveLocationDisplayName(point.latitude, point.longitude);

    const origin = locationAdjustOrigin ?? {
      latitude: checkInForm.latitude ?? point.latitude,
      longitude: checkInForm.longitude ?? point.longitude,
    };
    const movedDistance = calculateDistanceMeters(origin, point);

    setCheckInForm((prev) => ({
      ...prev,
      latitude: point.latitude,
      longitude: point.longitude,
      locationText: displayName || formatLocationFallback(),
    }));
    setLocationAdjustmentConfirmed(true);
    setLocationAdjustOpen(false);
    setLocationHint(`已调整约 ${Math.round(movedDistance)} 米，位置已确认，可继续打卡。`);
  };

  const handlePickPhoto = () => {
    setCheckInError("");
    if (!supportsMobileCameraCapture()) {
      setCheckInError("现场照片仅支持手机端直接拍照，请使用手机打开当前页面。");
      return;
    }
    if (!checkInForm.locationText.trim()) {
      setCheckInError("请先完成定位，再进行现场拍照。");
      return;
    }
    photoInputRef.current?.click();
  };

  const handlePhotoChange = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) {
      return;
    }

    setCheckInError("");
    setCheckInSuccess("");
    setUploadingPhoto(true);

    try {
      if (!supportsMobileCameraCapture()) {
        throw new Error("现场照片仅支持手机端直接拍照，请使用手机打开当前页面。");
      }
      if (!checkInForm.locationText.trim()) {
        throw new Error("请先完成定位，再进行现场拍照。");
      }
      const stampedFile = await stampPhotoWithTimeAndLocation(file, checkInForm.locationText);
      const uploadedUrl = await uploadWorkCheckInPhoto(stampedFile);
      setCheckInPhotoUrls([uploadedUrl]);
    } catch (error) {
      setCheckInError(error instanceof Error ? error.message : "现场照片上传失败");
    } finally {
      setUploadingPhoto(false);
      event.target.value = "";
    }
  };

  const handleRemovePhoto = () => {
    setCheckInPhotoUrls([]);
  };

  const handleOpenObjectPicker = (mode: PickerMode, lineIndex?: number, bizType: BizType = "sales") => {
    const currentOptions = getOptionsByBizType(bizType, salesOptions, channelOptions, opportunityOptions);
    if (!currentOptions.length && !reportTargetsLoading) {
      void loadReportTargets();
    }
    setObjectPicker({ mode, lineIndex, bizType, query: "" });
  };

  const handleSelectObject = (option: WorkRelationOption) => {
    if (!objectPicker) {
      return;
    }

    if (objectPicker.mode === "checkin") {
      setCheckInForm((current) => ({
        ...current,
        bizType: objectPicker.bizType,
        bizId: option.id,
        bizName: option.label,
        userName: currentUser?.displayName || profileOverview?.realName || currentUser?.username || "",
        deptName: profileOverview?.deptName || "",
      }));
      setObjectPicker(null);
      return;
    }

    const lineIndex = objectPicker.lineIndex ?? 0;
    setReportForm((current) => ({
      ...current,
      lineItems: current.lineItems.map((item, index) => {
        if (index !== lineIndex) {
          return item;
        }
        return {
          ...createEmptyReportLine(),
          bizType: objectPicker.bizType,
          bizId: option.id,
          bizName: option.label,
          workDate: currentWorkDate,
          editorText: buildEditorTemplate(objectPicker.bizType, option.label),
          content: "",
        };
      }),
    }));
    setObjectPicker(null);
  };

  const handleReportLineKeyDown = (index: number, event: KeyboardEvent<HTMLTextAreaElement>) => {
    if (event.key !== "@") {
      return;
    }
    event.preventDefault();
    const line = reportForm.lineItems[index];
    handleOpenObjectPicker("report", index, line?.bizType || "sales");
  };

  const handleReportLineChange = (index: number, value: string) => {
    setReportForm((current) => ({
      ...current,
      lineItems: current.lineItems.map((item, itemIndex) => {
        if (itemIndex !== index) {
          return item;
        }
        if (!item.bizId || !item.bizName) {
          return { ...item, editorText: "" };
        }
        const firstLine = value.replace(/\r/g, "").split("\n")[0]?.trim() || "";
        if (firstLine !== buildEditorMentionLine(item.bizType, item.bizName)) {
          return clearReportLineSelection(item);
        }
        return {
          ...item,
          editorText: sanitizeEditorText(item.bizType, item.bizName, value),
        };
      }),
    }));
  };

  const handleAddReportLine = () => {
    setReportForm((current) => ({
      ...current,
      lineItems: [...current.lineItems, createEmptyReportLine()],
    }));
  };

  const handleRemoveReportLine = (index: number) => {
    setReportForm((current) => ({
      ...current,
      lineItems: current.lineItems.length === 1
        ? [createEmptyReportLine()]
        : current.lineItems.filter((_, itemIndex) => itemIndex !== index),
    }));
    setObjectPicker((current) => (
      current && current.mode === "report" && current.lineIndex === index
        ? null
        : current
    ));
  };

  const handlePlanItemChange = (index: number, value: string) => {
    setReportForm((current) => ({
      ...current,
      planItems: current.planItems.map((item, itemIndex) => itemIndex === index ? { ...item, content: value } : item),
    }));
  };

  const handleAddPlanItem = () => {
    setReportForm((current) => ({
      ...current,
      planItems: [...current.planItems, createEmptyPlanItem()],
    }));
  };

  const handleRemovePlanItem = (index: number) => {
    setReportForm((current) => ({
      ...current,
      planItems: current.planItems.length === 1
        ? [createEmptyPlanItem()]
        : current.planItems.filter((_, itemIndex) => itemIndex !== index),
    }));
  };

  const handleCheckInSubmit = async () => {
    if (submittingCheckIn) {
      return;
    }

    setCheckInError("");
    setCheckInSuccess("");
    setSubmittingCheckIn(true);

    try {
      if (!checkInForm.latitude || !checkInForm.longitude || !checkInForm.locationText.trim()) {
        throw new Error("请先完成定位后再提交打卡");
      }
      if (
        locationAccuracyMeters
        && locationAccuracyMeters > CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS
        && !locationAdjustmentConfirmed
      ) {
        throw new Error(`当前定位精度约 ${Math.round(locationAccuracyMeters)} 米，超过 ${CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS} 米；请先重新定位或完成位置确认后再打卡`);
      }
      if (!checkInForm.bizType || !checkInForm.bizId) {
        throw new Error("请先关联本次打卡对象");
      }
      if (!checkInPhotoUrls.length) {
        throw new Error("请先上传现场照片");
      }
      await saveWorkCheckIn({
        locationText: checkInForm.locationText.trim(),
        remark: checkInForm.remark?.trim() || undefined,
        longitude: checkInForm.longitude,
        latitude: checkInForm.latitude,
        photoUrls: checkInPhotoUrls,
        bizType: checkInForm.bizType,
        bizId: checkInForm.bizId,
        bizName: checkInForm.bizName,
        userName: currentUser?.displayName || profileOverview?.realName || currentUser?.username || "",
        deptName: profileOverview?.deptName || "",
      });
      await Promise.all([
        loadOverview(),
        loadHistory(historySection, 1, true),
      ]);
      setCheckInPhotoUrls([]);
      setCheckInSuccess("打卡已记录。");
    } catch (error) {
      setCheckInError(error instanceof Error ? error.message : "打卡提交失败");
    } finally {
      setSubmittingCheckIn(false);
    }
  };

  const handleReportSubmit = async () => {
    if (submittingReport) {
      return;
    }

    setReportError("");
    setReportSuccess("");
    setSubmittingReport(true);

    try {
      const normalizedLineItems = reportForm.lineItems.map((item) => normalizeReportLineItem(item, currentWorkDate));
      const normalizedPlanItems = reportForm.planItems
        .map((item) => ({ content: item.content.trim() }))
        .filter((item) => item.content);

      if (!normalizedLineItems.length) {
        throw new Error("请至少填写一条今日工作内容");
      }
      validateReportLineItems(normalizedLineItems);
      if (!normalizedPlanItems.length) {
        throw new Error("请至少填写一条明日工作计划");
      }

      await saveWorkDailyReport({
        workContent: buildReportSummary(normalizedLineItems),
        lineItems: normalizedLineItems,
        planItems: normalizedPlanItems,
        tomorrowPlan: buildPlanSummary(normalizedPlanItems),
        sourceType: reportForm.sourceType || "manual",
      });
      await Promise.all([
        loadOverview(),
        loadHistory(historySection, 1, true),
      ]);
      setReportSuccess("日报已保存，今日再次提交会覆盖当天内容。");
    } catch (error) {
      setReportError(error instanceof Error ? error.message : "日报提交失败");
    } finally {
      setSubmittingReport(false);
    }
  };

  if (!activeWorkSection || !activeWorkMeta) {
    return <Navigate to="/work/checkin" replace />;
  }

  return (
    <div className="crm-page-stack">
      <header className="flex flex-wrap items-center justify-between gap-3">
        <div>
          <h1 className="text-xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-2xl">销售工作台</h1>
          <p className="mt-1 text-xs leading-5 text-slate-500 dark:text-slate-400 sm:text-sm">今天是 {format(new Date(), "yyyy年MM月dd日 EEEE", { locale: zhCN })}</p>
        </div>
      </header>

      <WorkSectionNav activeWorkSection={activeWorkSection} />

      <div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-12 lg:gap-6">
        <div className={`min-w-0 crm-page-stack lg:col-span-7 xl:col-span-8 ${mobilePanel === "entry" ? "block lg:flex" : "hidden lg:flex"}`}>
          <div className="mb-2 flex items-center justify-between gap-3">
            <SectionTitle title={activeWorkMeta.title} accent={activeWorkMeta.accent} compact />
            <MobilePanelToggle mobilePanel={mobilePanel} onChange={setMobilePanel} />
          </div>

          {loading ? (
            <WorkEntrySkeleton section={activeWorkSection} />
          ) : activeWorkSection === "checkin" ? (
            <CheckInPanel
              loading={loading}
              checkInForm={checkInForm}
              refreshingLocation={refreshingLocation}
              onOpenObjectPicker={() => handleOpenObjectPicker("checkin", undefined, checkInForm.bizType || "sales")}
              onRefreshLocation={() => void handleRefreshLocation()}
              onOpenLocationAdjust={handleOpenLocationAdjust}
              locationAccuracyMeters={locationAccuracyMeters}
              locationAdjustmentConfirmed={locationAdjustmentConfirmed}
              requiresLocationConfirmation={Boolean(
                locationAccuracyMeters
                  && locationAccuracyMeters > CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS
                  && !locationAdjustmentConfirmed,
              )}
              locationHint={locationHint}
              onRemarkChange={(remark) => setCheckInForm((prev) => ({ ...prev, remark }))}
              photoInputRef={photoInputRef}
              onPhotoChange={(event) => void handlePhotoChange(event)}
              onPickPhoto={handlePickPhoto}
              uploadingPhoto={uploadingPhoto}
              checkInPhotoUrls={checkInPhotoUrls}
              onRemovePhoto={handleRemovePhoto}
              checkInError={checkInError}
              checkInSuccess={checkInSuccess}
              pageError={pageError}
              submittingCheckIn={submittingCheckIn}
              onSubmit={() => void handleCheckInSubmit()}
            />
          ) : (
            <ReportPanel
              loading={loading}
              reportStatus={reportStatus}
              currentWorkDate={currentWorkDate}
              reportForm={reportForm}
              onAddReportLine={handleAddReportLine}
              onRemoveReportLine={handleRemoveReportLine}
              onOpenObjectPicker={handleOpenObjectPicker}
              onReportLineKeyDown={handleReportLineKeyDown}
              onReportLineChange={handleReportLineChange}
              onAddPlanItem={handleAddPlanItem}
              onPlanItemChange={handlePlanItemChange}
              onRemovePlanItem={handleRemovePlanItem}
              reportError={reportError}
              reportSuccess={reportSuccess}
              pageError={pageError}
              submittingReport={submittingReport}
              onSubmit={() => void handleReportSubmit()}
            />
          )}
        </div>

        <div className={`min-w-0 crm-page-stack lg:col-span-5 lg:sticky lg:top-6 xl:col-span-4 ${mobilePanel === "history" ? "block lg:flex" : "hidden lg:flex"}`}>
          <div className="mb-2 flex items-center justify-between gap-3">
            <SectionTitle title="历史记录" accent="bg-slate-300 dark:bg-slate-700" compact />
            <MobilePanelToggle mobilePanel={mobilePanel} onChange={setMobilePanel} />
          </div>

          <div className="max-h-[calc(100vh-12rem)] space-y-4 overflow-y-auto pr-2 scrollbar-hide">
            {historyLoading && historyData.length === 0 ? <WorkHistorySkeleton /> : null}

            {historyData.map((item, index) => (
              <div key={`${item.type}-${item.id}-${index}`}>
                <HistoryCard
                  item={item}
                  index={index}
                  onOpen={() => setHistoryDetailItem(item)}
                  onPreviewPhoto={(url, alt) => setPreviewPhoto({ url, alt })}
                />
              </div>
            ))}

            {!historyLoading && historyData.length === 0 ? (
              <div className="crm-empty-state rounded-2xl border border-slate-100 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900/50">
                当前没有{getHistoryLabelBySection(historySection)}记录
              </div>
            ) : null}

            {historyHasMore ? (
              <div
                ref={historyLoadMoreRef}
                className="flex items-center justify-center rounded-2xl border border-dashed border-slate-200 bg-white px-3 py-3 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-900/50 dark:text-slate-400"
              >
                {historyLoadingMore ? "加载更多记录中..." : "下拉到底自动加载更多"}
              </div>
            ) : null}
          </div>
        </div>
      </div>

      {objectPicker ? (
        <div className="fixed inset-0 z-[90]">
          <button
            type="button"
            onClick={() => setObjectPicker(null)}
            className="absolute inset-0 bg-slate-900/35 backdrop-blur-sm"
            aria-label="关闭选择对象"
          />
          <div className="absolute inset-x-0 bottom-0 max-h-[82dvh] overflow-hidden rounded-t-3xl border border-slate-200 bg-white shadow-2xl dark:border-slate-800 dark:bg-slate-900 md:inset-x-auto md:left-1/2 md:top-1/2 md:bottom-auto md:w-[min(720px,88vw)] md:max-h-[72vh] md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-3xl">
            <div className="border-b border-slate-100 px-5 py-4 dark:border-slate-800 md:px-6">
              <div className="mb-3 flex items-center justify-between">
                <div>
                  <h3 className="text-base font-semibold text-slate-900 dark:text-white">选择关联对象</h3>
                  <p className="crm-field-note mt-1">先选类型，再搜索具体对象</p>
                </div>
                <button
                  type="button"
                  onClick={() => setObjectPicker(null)}
                  className="inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
                >
                  <X className="h-4 w-4" />
                </button>
              </div>

              <div className="mb-3 flex flex-wrap gap-2">
                {(["sales", "channel", "opportunity"] as BizType[]).map((bizType) => (
                  <button
                    key={bizType}
                    type="button"
                    onClick={() => setObjectPicker((current) => current ? { ...current, bizType, query: "" } : current)}
                    className={`rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${
                      objectPicker.bizType === bizType
                        ? "bg-violet-600 text-white"
                        : "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
                    }`}
                  >
                    {getBizTypeLabel(bizType)}
                  </button>
                ))}
              </div>

              <div className="relative">
                <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
                <input
                  autoFocus
                  value={objectPicker.query}
                  onChange={(event) => setObjectPicker((current) => current ? { ...current, query: normalizeObjectPickerQuery(event.target.value) } : current)}
                  placeholder={`搜索${getBizTypeLabel(objectPicker.bizType)}名称`}
                  className="crm-input-text min-h-11 w-full rounded-xl border border-slate-200 bg-white py-2.5 pl-10 pr-3 text-base text-slate-900 outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50 dark:text-white md:text-sm"
                />
              </div>
            </div>

            <div className="max-h-[calc(82dvh-180px)] overflow-y-auto px-3 py-3 pb-[calc(env(safe-area-inset-bottom)+16px)] md:max-h-[calc(72vh-180px)] md:px-4 md:py-4">
              {reportTargetsLoading && !pickerOptions.length ? (
                <div className="crm-empty-state rounded-2xl border border-dashed border-slate-200 px-4 py-8 dark:border-slate-800">
                  正在加载可选对象...
                </div>
              ) : pickerOptions.length ? (
                <div className="space-y-2">
                  {pickerOptions.map((option) => (
                    <button
                      key={`${objectPicker.bizType}-${option.id}`}
                      type="button"
                      onClick={() => handleSelectObject(option)}
                      className="flex w-full items-center justify-between rounded-2xl border border-slate-200 bg-white px-4 py-3 text-left transition-colors hover:border-violet-200 hover:bg-violet-50/70 dark:border-slate-800 dark:bg-slate-900/40 dark:hover:border-violet-500/30 dark:hover:bg-slate-800"
                    >
                      <span className="text-sm font-medium text-slate-900 dark:text-white">{option.label}</span>
                      <span className="text-xs text-slate-400">选择</span>
                    </button>
                  ))}
                </div>
              ) : (
                <div className="crm-empty-state rounded-2xl border border-dashed border-slate-200 px-4 py-8 dark:border-slate-800">
                  没有找到匹配对象，请换个关键词试试
                </div>
              )}
            </div>
          </div>
        </div>
      ) : null}

      {locationAdjustOpen && checkInForm.latitude && checkInForm.longitude ? (
        <LocationAdjustModal
          origin={locationAdjustOrigin ?? { latitude: checkInForm.latitude, longitude: checkInForm.longitude }}
          currentPoint={{ latitude: checkInForm.latitude, longitude: checkInForm.longitude }}
          radius={LOCATION_ADJUST_RADIUS_METERS}
          currentAddress={checkInForm.locationText}
          onClose={() => setLocationAdjustOpen(false)}
          onConfirm={(point, address) => void handleApplyLocationAdjust(point, address)}
        />
      ) : null}

      {historyDetailItem ? (
        <HistoryDetailModal
          item={historyDetailItem}
          onClose={() => setHistoryDetailItem(null)}
          onPreviewPhoto={(url, alt) => setPreviewPhoto({ url, alt })}
        />
      ) : null}

      {previewPhoto ? (
        <PhotoPreviewModal
          url={previewPhoto.url}
          alt={previewPhoto.alt}
          onClose={() => setPreviewPhoto(null)}
        />
      ) : null}
    </div>
  );
}

function LocationAdjustModal({
  origin,
  currentPoint,
  radius,
  currentAddress,
  onClose,
  onConfirm,
}: {
  origin: LocationPoint;
  currentPoint: LocationPoint;
  radius: number;
  currentAddress: string;
  onClose: () => void;
  onConfirm: (point: LocationPoint, address: string) => void;
}) {
  const mapContainerRef = useRef<HTMLDivElement | null>(null);
  const mapRef = useRef<{
    map?: {
      on: (event: string, handler: (event: { latLng?: { getLat?: () => number; getLng?: () => number; lat?: number; lng?: number } }) => void) => void;
      off?: (event: string, handler: (event: { latLng?: { getLat?: () => number; getLng?: () => number; lat?: number; lng?: number } }) => void) => void;
      setCenter?: (latLng: unknown) => void;
      panTo?: (latLng: unknown) => void;
      getCenter?: () => { getLat?: () => number; getLng?: () => number; lat?: number; lng?: number };
      destroy?: () => void;
    };
    originMarker?: { setGeometries: (geometries: unknown[]) => void };
    createLatLng?: (latitude: number, longitude: number) => unknown;
    moveHandler?: () => void;
  }>({});
  const [selectedPoint, setSelectedPoint] = useState<LocationPoint>(currentPoint);
  const [selectedAddress, setSelectedAddress] = useState(currentAddress);
  const [mapLoading, setMapLoading] = useState(true);
  const [mapError, setMapError] = useState("");
  const [selectionHint, setSelectionHint] = useState("");
  const [resolvingAddress, setResolvingAddress] = useState(false);
  const [confirming, setConfirming] = useState(false);

  useEffect(() => {
    let cancelled = false;

    async function initMap() {
      if (!mapContainerRef.current) {
        return;
      }

      setMapLoading(true);
      setMapError("");

      try {
        const TMap = await loadTencentMapGlApi();
        if (cancelled || !mapContainerRef.current) {
          return;
        }

        const createLatLng = (latitude: number, longitude: number) => new TMap.LatLng(latitude, longitude);
        const map = new TMap.Map(mapContainerRef.current, {
          center: createLatLng(currentPoint.latitude, currentPoint.longitude),
          zoom: 18,
          pitch: 0,
          rotation: 0,
          mapStyleId: "style1",
        });

        const originMarker = new TMap.MultiMarker({
          map,
          styles: {
            origin: new TMap.MarkerStyle({
              width: 18,
              height: 18,
              anchor: { x: 9, y: 9 },
              src: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'%3e%3ccircle cx='9' cy='9' r='7' fill='%23ffffff' fill-opacity='0.95' stroke='%237c3aed' stroke-width='2'/%3e%3ccircle cx='9' cy='9' r='2.5' fill='%237c3aed'/%3e%3c/svg%3e",
            }),
          },
          geometries: [
            {
              id: "origin",
              styleId: "origin",
              position: createLatLng(origin.latitude, origin.longitude),
            },
          ],
        });

        new TMap.MultiCircle({
          map,
          styles: {
            allowed: new TMap.CircleStyle({
              color: "rgba(124, 58, 237, 0.14)",
              showBorder: true,
              borderColor: "rgba(124, 58, 237, 0.55)",
              borderWidth: 2,
            }),
          },
          geometries: [
            {
              id: "range",
              styleId: "allowed",
              center: createLatLng(origin.latitude, origin.longitude),
              radius,
            },
          ],
        });

        const syncCenterSelection = () => {
          const center = map.getCenter?.();
          const latitude = center?.getLat?.() ?? center?.lat;
          const longitude = center?.getLng?.() ?? center?.lng;
          if (typeof latitude !== "number" || typeof longitude !== "number") {
            return;
          }

          const nextPoint = {
            latitude: Number(latitude.toFixed(6)),
            longitude: Number(longitude.toFixed(6)),
          };
          const distance = calculateDistanceMeters(origin, nextPoint);
          if (distance > radius) {
            setSelectionHint(`仅可在原始定位附近 ${radius} 米内微调，当前超出约 ${Math.round(distance - radius)} 米。`);
          } else {
            setSelectionHint(`当前准星距原始定位约 ${Math.round(distance)} 米。`);
          }
          setSelectedPoint(nextPoint);
        };

        map.on("moveend", syncCenterSelection);
        map.on("dragend", syncCenterSelection);
        syncCenterSelection();
        mapRef.current = {
          map,
          originMarker,
          createLatLng,
          moveHandler: syncCenterSelection,
        };
      } catch (error) {
        if (!cancelled) {
          setMapError(error instanceof Error ? error.message : "地图加载失败，请稍后重试。");
        }
      } finally {
        if (!cancelled) {
          setMapLoading(false);
        }
      }
    }

    void initMap();

    return () => {
      cancelled = true;
      mapRef.current.map?.off?.("moveend", mapRef.current.moveHandler ?? (() => undefined));
      mapRef.current.map?.off?.("dragend", mapRef.current.moveHandler ?? (() => undefined));
      mapRef.current.map?.destroy?.();
      mapRef.current = {};
    };
  }, [currentPoint.latitude, currentPoint.longitude, origin.latitude, origin.longitude]);

  useEffect(() => {
    let cancelled = false;
    const timer = window.setTimeout(async () => {
      setResolvingAddress(true);
      try {
        const address = await resolveLocationDisplayName(selectedPoint.latitude, selectedPoint.longitude);
        if (!cancelled) {
          setSelectedAddress(address || formatLocationFallback());
        }
      } catch {
        if (!cancelled) {
          setSelectedAddress(formatLocationFallback());
        }
      } finally {
        if (!cancelled) {
          setResolvingAddress(false);
        }
      }
    }, 350);

    return () => {
      cancelled = true;
      window.clearTimeout(timer);
    };
  }, [selectedPoint.latitude, selectedPoint.longitude]);

  const movedDistance = Math.round(calculateDistanceMeters(origin, selectedPoint));

  return (
    <div className="fixed inset-0 z-[95]">
      <button
        type="button"
        onClick={onClose}
        className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
        aria-label="关闭调整位置"
      />
      <div className="absolute inset-x-0 bottom-0 flex max-h-[92dvh] flex-col overflow-hidden rounded-t-3xl border border-slate-200 bg-white shadow-2xl dark:border-slate-800 dark:bg-slate-900 md:inset-x-auto md:left-1/2 md:top-1/2 md:bottom-auto md:w-[min(860px,92vw)] md:max-h-[84vh] md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-3xl">
        <div className="shrink-0 border-b border-slate-100 px-5 py-4 dark:border-slate-800 md:px-6">
          <div className="mb-3 flex items-center justify-between">
            <div>
              <h3 className="text-base font-semibold text-slate-900 dark:text-white">调整打卡位置</h3>
              <p className="crm-field-note mt-1">拖动地图，让中间准星对准目标位置；仅允许在当前定位附近 {radius} 米范围内精调。</p>
            </div>
            <button
              type="button"
              onClick={onClose}
              className="inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
            >
              <X className="h-4 w-4" />
            </button>
          </div>
          <div className="rounded-2xl bg-slate-50 px-4 py-3 text-xs text-slate-500 dark:bg-slate-800/70 dark:text-slate-400">
            <p>原始定位：{currentAddress || formatLocationFallback()}</p>
            <p className="mt-1">当前位置：距原点约 {movedDistance} 米</p>
          </div>
        </div>

        <div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4 md:px-6">
          <div className="overflow-hidden rounded-2xl border border-slate-200 bg-slate-100 dark:border-slate-800 dark:bg-slate-950">
            <div className="relative h-[260px] w-full md:h-[420px]">
              <div ref={mapContainerRef} className="h-full w-full" />
              {!mapLoading ? (
                <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
                  <div className="relative h-12 w-12">
                    <div className="absolute inset-0 rounded-full border-2 border-violet-500/65 bg-violet-500/10 shadow-[0_0_0_6px_rgba(139,92,246,0.12)]" />
                    <div className="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-violet-600 shadow-lg" />
                    <div className="absolute left-1/2 top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white" />
                  </div>
                </div>
              ) : null}
              {mapLoading ? <div className="absolute inset-0 flex items-center justify-center text-sm text-slate-500">地图加载中...</div> : null}
            </div>
          </div>
          {mapError ? <p className="text-sm text-rose-500">{mapError}</p> : null}
          {selectionHint ? <p className="text-sm text-violet-600 dark:text-violet-300">{selectionHint}</p> : null}
          <div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-900/60">
            <p className="crm-field-note">调整后地址</p>
            <p className="mt-1 text-sm text-slate-900 dark:text-white">{resolvingAddress ? "正在解析地址..." : selectedAddress}</p>
          </div>
        </div>
        <div className="shrink-0 border-t border-slate-100 bg-white px-4 py-4 pb-[calc(env(safe-area-inset-bottom)+16px)] dark:border-slate-800 dark:bg-slate-900 md:px-6">
          <div className="flex items-center gap-3">
            <button
              type="button"
              onClick={() => {
                setSelectedPoint(origin);
                setSelectionHint("已恢复到原始定位。");
                if (mapRef.current.createLatLng) {
                  mapRef.current.map?.panTo?.(mapRef.current.createLatLng(origin.latitude, origin.longitude));
                }
              }}
              className="crm-btn min-w-0 flex-1 rounded-xl border border-slate-200 bg-white text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
            >
              恢复原点
            </button>
            <button
              type="button"
              onClick={async () => {
                setConfirming(true);
                try {
                  await onConfirm(selectedPoint, selectedAddress);
                } finally {
                  setConfirming(false);
                }
              }}
              disabled={confirming || resolvingAddress || movedDistance > radius || Boolean(mapError)}
              className="crm-btn min-w-0 flex-1 rounded-xl bg-violet-600 text-white transition-colors hover:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
            >
              {confirming ? "保存中..." : resolvingAddress ? "解析中..." : "确认使用此位置"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function WorkSectionNav({
  activeWorkSection,
}: {
  activeWorkSection: WorkSection;
}) {
  return (
    <div className="flex rounded-xl border border-slate-200/50 bg-slate-100 p-1 backdrop-blur-sm dark:border-slate-800/50 dark:bg-slate-900/50">
      {workSectionItems.map((item) => {
        const isActive = item.key === activeWorkSection;
        return (
          <Link
            key={item.path}
            to={item.path}
            className={cn(
              "flex-1 rounded-lg px-4 py-2 text-center text-sm font-medium transition-all duration-200",
              isActive
                ? "bg-white text-violet-600 shadow-sm dark:bg-slate-800 dark:text-violet-400"
                : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
            )}
          >
            {item.label}
          </Link>
        );
      })}
    </div>
  );
}

function MobilePanelToggle({
  mobilePanel,
  onChange,
}: {
  mobilePanel: "entry" | "history";
  onChange: (panel: "entry" | "history") => void;
}) {
  return (
    <div className="flex rounded-xl border border-slate-200/70 bg-slate-100 p-1 dark:border-slate-800/70 dark:bg-slate-900/50 lg:hidden">
      <button
        type="button"
        onClick={() => onChange("entry")}
        className={cn(
          "rounded-lg px-4 py-1.5 text-sm font-medium transition-all duration-200",
          mobilePanel === "entry"
            ? "bg-white text-violet-600 shadow-sm dark:bg-slate-800 dark:text-violet-400"
            : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
        )}
      >
        填写
      </button>
      <button
        type="button"
        onClick={() => onChange("history")}
        className={cn(
          "rounded-lg px-4 py-1.5 text-sm font-medium transition-all duration-200",
          mobilePanel === "history"
            ? "bg-white text-violet-600 shadow-sm dark:bg-slate-800 dark:text-violet-400"
            : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
        )}
      >
        历史
      </button>
    </div>
  );
}

function WorkEntrySkeleton({
  section,
}: {
  section: WorkSection;
}) {
  return (
    <div className="crm-card crm-card-pad rounded-2xl">
      <div className="mb-4 flex items-center justify-between border-b border-slate-100 pb-4 dark:border-slate-800/60">
        <div className="flex items-center gap-2">
          <div className={cn("h-5 w-5 rounded-full", section === "checkin" ? "bg-emerald-100" : "bg-violet-100")} />
          <div className="h-5 w-24 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
        </div>
        <div className="h-6 w-14 animate-pulse rounded-full bg-slate-100 dark:bg-slate-800" />
      </div>

      <div className="space-y-4">
        <div className="rounded-2xl border border-slate-200 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-900/40">
          <div className="h-4 w-20 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
          <div className="mt-3 h-14 animate-pulse rounded-xl bg-slate-100 dark:bg-slate-800" />
        </div>
        <div className="rounded-2xl border border-slate-200 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-900/40">
          <div className="mb-3 flex items-center justify-between gap-3">
            <div className="h-4 w-16 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
            <div className="flex gap-2">
              <div className="h-8 w-20 animate-pulse rounded-full bg-slate-100 dark:bg-slate-800" />
              <div className="h-8 w-16 animate-pulse rounded-full bg-slate-100 dark:bg-slate-800" />
            </div>
          </div>
          <div className="h-24 animate-pulse rounded-2xl bg-slate-100 dark:bg-slate-800" />
        </div>
        <div className="h-11 animate-pulse rounded-xl bg-slate-100 dark:bg-slate-800" />
      </div>
    </div>
  );
}

function WorkHistorySkeleton() {
  return (
    <div className="space-y-4">
      {[0, 1, 2].map((item) => (
        <div
          key={`history-skeleton-${item}`}
          className="crm-card crm-card-pad rounded-2xl"
        >
          <div className="mb-3 flex items-start justify-between">
            <div className="flex items-center gap-3">
              <div className="h-8 w-8 animate-pulse rounded-full bg-slate-100 dark:bg-slate-800" />
              <div className="space-y-2">
                <div className="h-4 w-20 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
                <div className="h-3 w-24 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
              </div>
            </div>
            <div className="h-5 w-12 animate-pulse rounded-full bg-slate-100 dark:bg-slate-800" />
          </div>
          <div className="pl-11">
            <div className="space-y-2">
              <div className="h-3 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
              <div className="h-3 w-11/12 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
              <div className="h-3 w-3/4 animate-pulse rounded bg-slate-100 dark:bg-slate-800" />
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

function HistoryCard({
  item,
  index,
  onOpen,
  onPreviewPhoto,
}: {
  item: WorkHistoryItem;
  index: number;
  onOpen: () => void;
  onPreviewPhoto: (url: string, alt: string) => void;
}) {
  return (
    <motion.div
      initial={{ opacity: 0, x: 20 }}
      animate={{ opacity: 1, x: 0 }}
      transition={{ delay: index * 0.06 }}
      role="button"
      tabIndex={0}
      onClick={onOpen}
      onKeyDown={(event) => {
        if (event.key === "Enter" || event.key === " ") {
          event.preventDefault();
          onOpen();
        }
      }}
      className="crm-card crm-card-pad cursor-pointer rounded-2xl transition-all hover:border-violet-100 hover:shadow-md dark:hover:border-violet-900/50"
    >
      <div className="mb-3 flex items-start justify-between">
        <div className="flex items-center gap-3">
          <div className={`flex h-8 w-8 items-center justify-center rounded-full ${item.type === "日报" ? "bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400" : "bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400"}`}>
            {item.type === "日报" ? <FileText className="h-4 w-4" /> : <MapPin className="h-4 w-4" />}
          </div>
          <div>
            <h3 className="text-sm font-semibold text-slate-900 dark:text-white">{item.type}</h3>
            <p className="text-[10px] text-slate-500 dark:text-slate-400">{[item.date, item.time].filter(Boolean).join(" ")}</p>
          </div>
        </div>
        <div className="flex flex-col items-end gap-1">
          <span className={`crm-pill px-2 py-0.5 text-[10px] ${
            item.status === "已点评"
              ? "crm-pill-violet"
              : item.status === "已阅" || item.status === "已提交"
                ? "crm-pill-neutral"
                : "crm-pill-emerald"
          }`}
          >
            {item.status}
          </span>
          {item.score ? <span className="text-[10px] font-bold text-rose-600 dark:text-rose-400">{item.score}分</span> : null}
        </div>
      </div>

      <div className="pl-11">
        <p className="whitespace-pre-line text-xs leading-relaxed text-slate-700 dark:text-slate-300">{item.content}</p>
        {item.photoUrls?.length ? (
          <div className="mt-3 flex gap-2 overflow-x-auto pb-1">
            {item.photoUrls.map((photoUrl, photoIndex) => (
              <button
                key={`${item.id}-photo-${photoIndex}`}
                type="button"
                onClick={(event) => {
                  event.stopPropagation();
                  onPreviewPhoto(photoUrl, `打卡照片${photoIndex + 1}`);
                }}
                className="shrink-0"
              >
                <ProtectedImage
                  src={photoUrl}
                  alt={`打卡照片${photoIndex + 1}`}
                  className="h-16 w-16 rounded-lg border border-slate-200 object-cover dark:border-slate-700"
                />
              </button>
            ))}
          </div>
        ) : null}
        {item.comment ? (
          <div className="mt-2 rounded-lg border border-slate-100 bg-slate-50 p-2.5 dark:border-slate-800/50 dark:bg-slate-800/50">
            <p className="mb-0.5 text-[10px] font-medium text-slate-900 dark:text-white">主管点评:</p>
            <p className="text-[10px] text-slate-600 dark:text-slate-400">{item.comment}</p>
          </div>
        ) : null}
        <p className="mt-3 text-[11px] font-medium text-violet-600 dark:text-violet-400">点击查看详情</p>
      </div>
    </motion.div>
  );
}

function CheckInPanel({
  loading,
  checkInForm,
  refreshingLocation,
  onOpenObjectPicker,
  onRefreshLocation,
  onOpenLocationAdjust,
  locationAccuracyMeters,
  locationAdjustmentConfirmed,
  requiresLocationConfirmation,
  locationHint,
  onRemarkChange,
  photoInputRef,
  onPhotoChange,
  onPickPhoto,
  uploadingPhoto,
  checkInPhotoUrls,
  onRemovePhoto,
  checkInError,
  checkInSuccess,
  pageError,
  submittingCheckIn,
  onSubmit,
}: {
  loading: boolean;
  checkInForm: CreateWorkCheckInPayload;
  refreshingLocation: boolean;
  onOpenObjectPicker: () => void;
  onRefreshLocation: () => void;
  onOpenLocationAdjust: () => void;
  locationAccuracyMeters?: number;
  locationAdjustmentConfirmed: boolean;
  requiresLocationConfirmation: boolean;
  locationHint: string;
  onRemarkChange: (remark: string) => void;
  photoInputRef: { current: HTMLInputElement | null };
  onPhotoChange: (event: ChangeEvent<HTMLInputElement>) => void;
  onPickPhoto: () => void;
  uploadingPhoto: boolean;
  checkInPhotoUrls: string[];
  onRemovePhoto: () => void;
  checkInError: string;
  checkInSuccess: string;
  pageError: string;
  submittingCheckIn: boolean;
  onSubmit: () => void;
}) {
  const mobileCameraOnly = supportsMobileCameraCapture();

  return (
    <motion.div
      key="checkin"
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      className="crm-card crm-card-pad rounded-2xl"
    >
      <div className="mb-4 flex items-center justify-between border-b border-slate-100 pb-4 dark:border-slate-800/60">
        <div className="flex items-center gap-2">
          <MapPin className="h-5 w-5 text-emerald-500 dark:text-emerald-400" />
          <h3 className="text-base font-semibold text-slate-900 dark:text-white">外勤打卡</h3>
        </div>
      </div>

      <div>
        <div>
          <p className="mb-1 text-sm font-medium text-slate-900 dark:text-white">关联对象</p>
          <button
            type="button"
            onClick={onOpenObjectPicker}
            className="crm-btn crm-input-text flex w-full items-center justify-between rounded-xl border border-slate-200 bg-slate-50 text-left transition-colors hover:border-violet-300 hover:bg-violet-50 dark:border-slate-800 dark:bg-slate-900/60 dark:hover:border-violet-500/30"
          >
            <div>
              <p className="crm-field-note">人员拓展 / 渠道拓展 / 商机</p>
              <p className={`mt-1 text-sm font-medium ${checkInForm.bizName ? "text-slate-900 dark:text-white" : "text-slate-400 dark:text-slate-500"}`}>
                {checkInForm.bizName || "点击选择本次打卡关联对象"}
              </p>
            </div>
            <AtSign className="h-4 w-4 text-violet-500" />
          </button>
        </div>
      </div>

      <div className="mt-4">
        <div className="mb-2 flex items-center justify-between gap-3">
          <p className="text-sm font-medium text-slate-900 dark:text-white">当前位置</p>
          <div className="flex items-center gap-2">
            <button
              type="button"
              onClick={onOpenLocationAdjust}
              disabled={!checkInForm.latitude || !checkInForm.longitude || refreshingLocation}
              className="crm-pill crm-pill-neutral inline-flex h-8 shrink-0 items-center justify-center border border-slate-200 px-3 transition-colors hover:border-violet-300 hover:text-violet-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-200 dark:hover:border-violet-500/40 dark:hover:text-violet-300"
            >
              调整位置
            </button>
            <button
              type="button"
              onClick={onRefreshLocation}
              disabled={refreshingLocation}
              className="crm-pill crm-pill-violet inline-flex h-8 shrink-0 items-center justify-center gap-1.5 px-3 transition-colors hover:bg-violet-100 disabled:cursor-not-allowed disabled:opacity-60 dark:hover:bg-violet-500/20"
            >
              <RefreshCw className={`h-3.5 w-3.5 ${refreshingLocation ? "animate-spin" : ""}`} />
              {refreshingLocation ? "定位中" : "刷新"}
            </button>
          </div>
        </div>
        <textarea
          rows={3}
          value={checkInForm.locationText}
          readOnly
          placeholder="系统会自动定位当前位置"
          className="crm-input-box-readonly crm-input-text w-full border border-slate-200 bg-slate-50 text-slate-900 outline-none dark:border-slate-800 dark:bg-slate-900/60 dark:text-white"
        />
        {locationHint ? <p className="crm-field-note mt-2">{locationHint}</p> : null}
        {locationAccuracyMeters && locationAccuracyMeters > CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS ? (
          <div className="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300">
            {locationAdjustmentConfirmed
              ? `当前原始定位精度约 ${Math.round(locationAccuracyMeters)} 米，已完成位置确认，可以提交打卡。`
              : `当前原始定位精度约 ${Math.round(locationAccuracyMeters)} 米，超过 ${CHECK_IN_ACCURACY_CONFIRM_THRESHOLD_METERS} 米，暂不允许直接打卡。请先重新定位或完成位置确认。`}
          </div>
        ) : null}
        <div className="mt-3 flex flex-wrap items-center gap-2">
          <span className="text-[11px] text-slate-400 dark:text-slate-500">仅支持在当前位置附近 {LOCATION_ADJUST_RADIUS_METERS} 米内精调</span>
        </div>
      </div>

      <div className="mt-4 rounded-2xl border border-slate-200 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-900/40">
        <div className="mb-3">
          <p className="text-sm font-medium text-slate-900 dark:text-white">现场照片</p>
        </div>
        {mobileCameraOnly ? (
          <input
            ref={photoInputRef}
            type="file"
            accept="image/*"
            capture="environment"
            className="hidden"
            onChange={onPhotoChange}
          />
        ) : null}

        {checkInPhotoUrls.length ? (
          <div className="space-y-3">
            <div className="relative overflow-hidden rounded-xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900/50">
              <ProtectedImage src={checkInPhotoUrls[0]} alt="打卡现场照片" className="h-52 w-full object-cover" />
              <button
                type="button"
                onClick={onRemovePhoto}
                className="absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-full bg-slate-900/75 text-white transition-colors hover:bg-slate-900"
              >
                <X className="h-4 w-4" />
              </button>
            </div>
            <button
              type="button"
              onClick={onPickPhoto}
              disabled={uploadingPhoto || !mobileCameraOnly}
              className="crm-btn flex w-full items-center justify-center gap-2 rounded-xl border border-slate-200 bg-white text-slate-700 transition-colors hover:border-violet-300 hover:text-violet-600 disabled:cursor-not-allowed disabled:opacity-60 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-200 dark:hover:border-violet-500/40 dark:hover:text-violet-300"
            >
              <Camera className="h-4 w-4" />
              {uploadingPhoto ? "处理中..." : "重新拍照"}
            </button>
          </div>
        ) : (
          <button
            type="button"
            onClick={onPickPhoto}
            disabled={uploadingPhoto || !mobileCameraOnly}
            className="flex min-h-36 w-full flex-col items-center justify-center rounded-xl border border-dashed border-slate-300 bg-white px-4 py-6 text-center transition-colors hover:border-violet-300 hover:bg-violet-50/50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-slate-700 dark:bg-slate-900/50 dark:hover:border-violet-500/40 dark:hover:bg-slate-900"
          >
            <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-300">
              <Camera className="h-5 w-5" />
            </div>
            <p className="text-sm font-medium text-slate-900 dark:text-white">
              {uploadingPhoto ? "照片处理中..." : mobileCameraOnly ? "点击拍摄现场照片" : "请使用手机端拍照打卡"}
            </p>
            <p className="crm-field-note mt-1">
              {mobileCameraOnly
                ? "仅支持手机端直接拍照，系统会自动叠加拍摄时间和当前位置水印。"
                : "当前设备不支持拍照打卡，请使用手机浏览器打开后现场拍照。"}
            </p>
          </button>
        )}
      </div>

      <div className="mt-4">
        <p className="mb-1 text-sm font-medium text-slate-900 dark:text-white">打卡备注 (选填)</p>
        <textarea
          rows={2}
          value={checkInForm.remark ?? ""}
          onChange={(event) => onRemarkChange(event.target.value)}
          placeholder="补充说明现场情况..."
          className="crm-input-box crm-input-text w-full border border-slate-200 bg-white text-slate-900 outline-none transition-all focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/60 dark:text-white"
        />
      </div>

      {checkInError ? <p className="mt-4 text-xs text-rose-500">{checkInError}</p> : null}
      {checkInSuccess ? <p className="mt-4 text-xs text-emerald-500">{checkInSuccess}</p> : null}
      {pageError ? <p className="mt-4 text-xs text-rose-500">{pageError}</p> : null}

      <button
        type="button"
        onClick={onSubmit}
        disabled={submittingCheckIn || loading || requiresLocationConfirmation}
        className="crm-btn mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-emerald-600 text-white transition-all hover:bg-emerald-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
      >
        <CheckCircle2 className="h-4 w-4" />
        {submittingCheckIn ? "提交中..." : requiresLocationConfirmation ? "请先确认位置" : "确认打卡"}
      </button>
    </motion.div>
  );
}

function ReportPanel({
  loading,
  reportStatus,
  currentWorkDate,
  reportForm,
  onAddReportLine,
  onRemoveReportLine,
  onOpenObjectPicker,
  onReportLineKeyDown,
  onReportLineChange,
  onAddPlanItem,
  onPlanItemChange,
  onRemovePlanItem,
  reportError,
  reportSuccess,
  pageError,
  submittingReport,
  onSubmit,
}: {
  loading: boolean;
  reportStatus?: string;
  currentWorkDate: string;
  reportForm: CreateWorkDailyReportPayload;
  onAddReportLine: () => void;
  onRemoveReportLine: (index: number) => void;
  onOpenObjectPicker: (mode: PickerMode, lineIndex?: number, bizType?: BizType) => void;
  onReportLineKeyDown: (index: number, event: KeyboardEvent<HTMLTextAreaElement>) => void;
  onReportLineChange: (index: number, value: string) => void;
  onAddPlanItem: () => void;
  onPlanItemChange: (index: number, value: string) => void;
  onRemovePlanItem: (index: number) => void;
  reportError: string;
  reportSuccess: string;
  pageError: string;
  submittingReport: boolean;
  onSubmit: () => void;
}) {
  const [editingReportLineIndex, setEditingReportLineIndex] = useState<number | null>(null);
  const [editingPlanItemIndex, setEditingPlanItemIndex] = useState<number | null>(null);
  const reportLineTextareaRefs = useRef<(HTMLTextAreaElement | null)[]>([]);
  const planItemInputRefs = useRef<(HTMLInputElement | null)[]>([]);
  const previousReportLineCountRef = useRef(reportForm.lineItems.length);
  const previousPlanItemCountRef = useRef(reportForm.planItems.length);
  const pendingNewReportLineFocusRef = useRef<number | null>(null);
  const pendingNewPlanItemFocusRef = useRef<number | null>(null);

  const focusTextControl = useCallback((element: HTMLTextAreaElement | HTMLInputElement | null) => {
    if (!element) {
      return;
    }
    if (element instanceof HTMLTextAreaElement) {
      syncAutoHeightTextarea(element);
    }
    element.focus();
    const textLength = element.value.length;
    element.setSelectionRange(textLength, textLength);
  }, []);

  const activateReportLineEditor = useCallback((index: number) => {
    flushSync(() => {
      setEditingPlanItemIndex(null);
      setEditingReportLineIndex(index);
    });
    const target = reportLineTextareaRefs.current[index];
    if (target) {
      focusTextControl(target);
      return;
    }
    requestAnimationFrame(() => focusTextControl(reportLineTextareaRefs.current[index]));
  }, [focusTextControl]);

  const activatePlanItemEditor = useCallback((index: number) => {
    flushSync(() => {
      setEditingReportLineIndex(null);
      setEditingPlanItemIndex(index);
    });
    const target = planItemInputRefs.current[index];
    if (target) {
      focusTextControl(target);
      return;
    }
    requestAnimationFrame(() => focusTextControl(planItemInputRefs.current[index]));
  }, [focusTextControl]);

  const handleAddReportLineAndFocus = useCallback(() => {
    pendingNewReportLineFocusRef.current = reportForm.lineItems.length;
    onAddReportLine();
  }, [onAddReportLine, reportForm.lineItems.length]);

  const handleAddPlanItemAndFocus = useCallback(() => {
    pendingNewPlanItemFocusRef.current = reportForm.planItems.length;
    onAddPlanItem();
  }, [onAddPlanItem, reportForm.planItems.length]);

  useEffect(() => {
    if (reportForm.lineItems.length > previousReportLineCountRef.current) {
      if (pendingNewReportLineFocusRef.current !== null) {
        const targetIndex = Math.min(pendingNewReportLineFocusRef.current, reportForm.lineItems.length - 1);
        setEditingReportLineIndex(targetIndex);
        setEditingPlanItemIndex(null);
        pendingNewReportLineFocusRef.current = null;
      }
    } else if (editingReportLineIndex !== null && editingReportLineIndex >= reportForm.lineItems.length) {
      setEditingReportLineIndex(reportForm.lineItems.length ? reportForm.lineItems.length - 1 : null);
    }
    previousReportLineCountRef.current = reportForm.lineItems.length;
  }, [editingReportLineIndex, reportForm.lineItems.length]);

  useEffect(() => {
    if (reportForm.planItems.length > previousPlanItemCountRef.current) {
      if (pendingNewPlanItemFocusRef.current !== null) {
        const targetIndex = Math.min(pendingNewPlanItemFocusRef.current, reportForm.planItems.length - 1);
        setEditingPlanItemIndex(targetIndex);
        setEditingReportLineIndex(null);
        pendingNewPlanItemFocusRef.current = null;
      }
    } else if (editingPlanItemIndex !== null && editingPlanItemIndex >= reportForm.planItems.length) {
      setEditingPlanItemIndex(reportForm.planItems.length ? reportForm.planItems.length - 1 : null);
    }
    previousPlanItemCountRef.current = reportForm.planItems.length;
  }, [editingPlanItemIndex, reportForm.planItems.length]);

  useEffect(() => {
    reportLineTextareaRefs.current.forEach(syncAutoHeightTextarea);
  }, [reportForm.lineItems]);

  useEffect(() => {
    if (editingReportLineIndex === null) {
      return;
    }
    requestAnimationFrame(() => focusTextControl(reportLineTextareaRefs.current[editingReportLineIndex]));
  }, [editingReportLineIndex, focusTextControl]);

  useEffect(() => {
    if (editingPlanItemIndex === null) {
      return;
    }
    requestAnimationFrame(() => focusTextControl(planItemInputRefs.current[editingPlanItemIndex]));
  }, [editingPlanItemIndex, focusTextControl]);

  return (
    <motion.div
      key="report"
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      className="crm-card crm-card-pad rounded-2xl"
    >
      <div className="mb-4 flex items-center justify-between border-b border-slate-100 pb-4 dark:border-slate-800/60">
        <div className="flex items-center gap-2">
          <NotebookPen className="h-5 w-5 text-violet-600 dark:text-violet-400" />
          <h3 className="text-base font-semibold text-slate-900 dark:text-white">销售日报</h3>
        </div>
        <span className="crm-pill crm-pill-amber">
          {loading ? "加载中..." : getReportStatus(reportStatus)}
        </span>
      </div>

      <div>
        <div className="mb-3 flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
          <div>
            <label className="flex items-center gap-2 text-sm font-medium text-slate-900 dark:text-white">
              <FileText className="h-4 w-4 text-slate-400 dark:text-slate-500" />
              今日工作内容
            </label>
            <p className="crm-field-note mt-1">
              输入 <span className="font-semibold text-violet-600">@</span> 选择对象，系统会自动补齐 <span className="font-semibold text-violet-600">#字段</span>。
            </p>
          </div>
          <div className="flex w-full items-center justify-between gap-2 lg:w-auto lg:justify-end">
            <span className="crm-pill crm-pill-neutral text-[11px]">
              日期 {currentWorkDate}
            </span>
            <button
              type="button"
              onClick={handleAddReportLineAndFocus}
              aria-label="添加一行今日工作内容"
              className="crm-pill crm-pill-violet inline-flex h-8 w-8 items-center justify-center rounded-full p-0 transition-colors hover:bg-violet-100 dark:hover:bg-violet-500/20"
            >
              <Plus className="h-4 w-4" />
            </button>
          </div>
        </div>

        <div className="space-y-3">
          {reportForm.lineItems.map((item, index) => {
            const isEditing = editingReportLineIndex === index;
            const collapsedPreviewLines = buildCollapsedPreviewLines(item.editorText, "先输入 @ 选择对象，系统会自动生成固定字段。");
            return (
              <div key={`report-line-${index}`} className="flex items-start gap-3">
                {isEditing ? (
                  <textarea
                    rows={3}
                    ref={(element) => {
                      reportLineTextareaRefs.current[index] = element;
                      syncAutoHeightTextarea(element);
                    }}
                    value={item.editorText || ""}
                    onKeyDown={(event) => onReportLineKeyDown(index, event)}
                    onChange={(event) => {
                      syncAutoHeightTextarea(event.currentTarget);
                      onReportLineChange(index, event.target.value);
                    }}
                    onBlur={() => setEditingReportLineIndex((current) => (current === index ? null : current))}
                    placeholder="先输入 @ 选择对象，系统会自动生成固定字段。"
                    className="crm-input-box crm-input-text min-h-[96px] flex-1 resize-none overflow-hidden rounded-2xl border border-slate-200 bg-slate-50 leading-7 text-slate-900 outline-none transition-all focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/60 dark:text-white"
                  />
                ) : (
                  <button
                    type="button"
                    onClick={() => activateReportLineEditor(index)}
                    className="min-h-[72px] flex-1 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-left transition-all hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900/60 dark:hover:border-violet-500"
                  >
                    <div className="space-y-1">
                      {collapsedPreviewLines.map((line, lineIndex) => (
                        <p
                          key={`report-line-preview-${index}-${lineIndex}`}
                          className={cn(
                            "truncate text-sm leading-6",
                            item.editorText ? "text-slate-900 dark:text-white" : "text-slate-400 dark:text-slate-500",
                          )}
                        >
                          {line}
                        </p>
                      ))}
                    </div>
                  </button>
                )}
                <div className="flex shrink-0 items-center self-center">
                  <button
                    type="button"
                    onClick={() => onRemoveReportLine(index)}
                    className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-rose-200 bg-rose-50 text-rose-500 shadow-sm transition-colors hover:bg-rose-100 hover:text-rose-600 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-300 dark:hover:bg-rose-500/20"
                    title="删除本行"
                  >
                    <Trash2 className="h-4.5 w-4.5" />
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      <div className="mt-6">
        <div className="mb-3 flex items-center justify-between">
          <label className="flex items-center gap-2 text-sm font-medium text-slate-900 dark:text-white">
            <ListTodo className="h-4 w-4 text-slate-400 dark:text-slate-500" />
            明日工作计划
          </label>
          <button
            type="button"
            onClick={handleAddPlanItemAndFocus}
            aria-label="添加一行明日工作计划"
            className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-violet-50 text-violet-600 transition-colors hover:bg-violet-100 dark:bg-violet-500/10 dark:text-violet-400 dark:hover:bg-violet-500/20"
          >
            <Plus className="h-4 w-4" />
          </button>
        </div>
        <div className="space-y-3">
          {reportForm.planItems.map((item, index) => {
            const isEditing = editingPlanItemIndex === index;
            const collapsedPreview = item.content.trim() || "输入明日工作计划";
            return (
              <div key={`plan-${index}`} className="flex items-center gap-3">
                {isEditing ? (
                  <input
                    type="text"
                    ref={(element) => {
                      planItemInputRefs.current[index] = element;
                    }}
                    value={item.content}
                    onChange={(event) => onPlanItemChange(index, event.target.value)}
                    onBlur={() => setEditingPlanItemIndex((current) => (current === index ? null : current))}
                    placeholder="输入明日工作计划"
                    className="crm-input-box crm-input-text h-10 flex-1 rounded-xl border border-slate-200 bg-slate-50 text-slate-900 outline-none transition-all focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/60 dark:text-white"
                  />
                ) : (
                  <button
                    type="button"
                    onClick={() => activatePlanItemEditor(index)}
                    className="crm-btn-sm crm-input-text flex h-10 flex-1 items-center rounded-xl border border-slate-200 bg-slate-50 text-left transition-all hover:border-violet-300 dark:border-slate-800 dark:bg-slate-900/60 dark:hover:border-violet-500"
                  >
                    <p
                      className={cn(
                        "w-full truncate text-sm leading-6",
                        item.content ? "text-slate-900 dark:text-white" : "text-slate-400 dark:text-slate-500",
                      )}
                    >
                      {collapsedPreview}
                    </p>
                  </button>
                )}
                <div className="flex shrink-0 items-center">
                  <button
                    type="button"
                    onClick={() => onRemovePlanItem(index)}
                    className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-rose-200 bg-rose-50 text-rose-500 shadow-sm transition-colors hover:bg-rose-100 hover:text-rose-600 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-300 dark:hover:bg-rose-500/20"
                    title="删除本行"
                  >
                    <Trash2 className="h-4.5 w-4.5" />
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {reportError ? <p className="mt-4 text-xs text-rose-500">{reportError}</p> : null}
      {reportSuccess ? <p className="mt-4 text-xs text-emerald-500">{reportSuccess}</p> : null}
      {pageError ? <p className="mt-4 text-xs text-rose-500">{pageError}</p> : null}

      <button
        type="button"
        onClick={onSubmit}
        disabled={submittingReport || loading}
        className="crm-btn mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-violet-600 text-white shadow-sm transition-all hover:bg-violet-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
      >
        <Send className="h-4 w-4" />
        {submittingReport ? "提交中..." : "提交日报"}
      </button>
    </motion.div>
  );
}

function HistoryDetailModal({
  item,
  onClose,
  onPreviewPhoto,
}: {
  item: WorkHistoryItem;
  onClose: () => void;
  onPreviewPhoto: (url: string, alt: string) => void;
}) {
  return (
    <div className="fixed inset-0 z-[95]">
      <button
        type="button"
        onClick={onClose}
        className="absolute inset-0 bg-slate-900/45 backdrop-blur-sm"
        aria-label="关闭历史详情"
      />
      <div className="absolute inset-x-0 bottom-0 max-h-[86dvh] overflow-hidden rounded-t-3xl border border-slate-200 bg-white shadow-2xl dark:border-slate-800 dark:bg-slate-900 md:inset-x-auto md:left-1/2 md:top-1/2 md:bottom-auto md:w-[min(760px,88vw)] md:max-h-[80vh] md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-3xl">
        <div className="flex items-start justify-between border-b border-slate-100 px-5 py-4 dark:border-slate-800 md:px-6">
          <div>
            <h3 className="text-base font-semibold text-slate-900 dark:text-white">{item.type || "历史记录详情"}</h3>
            <p className="crm-field-note mt-1">{[item.date, item.time].filter(Boolean).join(" ") || "无时间信息"}</p>
          </div>
          <button
            type="button"
            onClick={onClose}
            className="inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
            aria-label="关闭"
          >
            <X className="h-4 w-4" />
          </button>
        </div>

        <div className="max-h-[calc(86dvh-84px)] overflow-y-auto px-5 py-4 pb-[calc(env(safe-area-inset-bottom)+20px)] md:max-h-[calc(80vh-84px)] md:px-6">
          <div className="flex flex-wrap items-center gap-2">
            {item.status ? (
              <span className={cn(
                "rounded-full px-2.5 py-1 text-xs font-medium",
                item.status === "已点评"
                  ? "bg-violet-50 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400"
                  : item.status === "已阅" || item.status === "已提交"
                    ? "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400"
                    : "bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400",
              )}
              >
                {item.status}
              </span>
            ) : null}
            {item.score ? <span className="text-xs font-bold text-rose-600 dark:text-rose-400">{item.score}分</span> : null}
            {item.photoUrls?.length ? (
              <span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs text-slate-500 dark:bg-slate-800 dark:text-slate-400">
                {item.photoUrls.length} 张照片
              </span>
            ) : null}
          </div>

          <section className="mt-4 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-900/40">
            <p className="text-xs font-medium text-slate-500 dark:text-slate-400">记录内容</p>
            <p className="mt-2 whitespace-pre-line text-sm leading-7 text-slate-800 dark:text-slate-200">
              {item.content || "无"}
            </p>
          </section>

          {item.photoUrls?.length ? (
            <section className="mt-4">
              <p className="mb-3 text-sm font-medium text-slate-900 dark:text-white">现场照片</p>
              <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
                {item.photoUrls.map((photoUrl, photoIndex) => (
                  <button
                    key={`${item.id}-detail-photo-${photoIndex}`}
                    type="button"
                    onClick={() => onPreviewPhoto(photoUrl, `${item.type || "历史"}照片${photoIndex + 1}`)}
                    className="overflow-hidden rounded-2xl border border-slate-200 bg-white text-left transition-transform hover:scale-[1.01] dark:border-slate-700 dark:bg-slate-900/50"
                  >
                    <ProtectedImage
                      src={photoUrl}
                      alt={`${item.type || "历史"}照片${photoIndex + 1}`}
                      className="h-28 w-full object-cover"
                    />
                    <div className="crm-field-note px-3 py-2">点击查看大图</div>
                  </button>
                ))}
              </div>
            </section>
          ) : null}

          {item.comment ? (
            <section className="mt-4 rounded-2xl border border-slate-100 bg-white p-4 dark:border-slate-800 dark:bg-slate-900/40">
              <p className="text-xs font-medium text-slate-500 dark:text-slate-400">主管点评</p>
              <p className="mt-2 whitespace-pre-line text-sm leading-7 text-slate-800 dark:text-slate-200">{item.comment}</p>
            </section>
          ) : null}
        </div>
      </div>
    </div>
  );
}

function PhotoPreviewModal({
  url,
  alt,
  onClose,
}: {
  url: string;
  alt: string;
  onClose: () => void;
}) {
  return (
    <div className="fixed inset-0 z-[100]">
      <button
        type="button"
        onClick={onClose}
        className="absolute inset-0 bg-slate-950/82 backdrop-blur-sm"
        aria-label="关闭照片预览"
      />
      <div className="absolute inset-0 flex items-center justify-center px-4 py-6">
        <button
          type="button"
          onClick={onClose}
          className="absolute right-4 top-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
          aria-label="关闭"
        >
          <X className="h-5 w-5" />
        </button>
        <ProtectedImage
          src={url}
          alt={alt}
          className="max-h-full max-w-full rounded-2xl object-contain shadow-2xl"
        />
      </div>
    </div>
  );
}

function syncAutoHeightTextarea(textarea: HTMLTextAreaElement | null) {
  if (!textarea) {
    return;
  }
  textarea.style.height = "auto";
  textarea.style.height = `${Math.max(textarea.scrollHeight, 96)}px`;
}

function buildCollapsedPreviewLines(content: string | undefined, placeholder: string) {
  if (!content?.trim()) {
    return [placeholder];
  }

  const lines = content
    .replace(/\r/g, "")
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean);

  if (!lines.length) {
    return [placeholder];
  }

  const previewLines = lines.slice(0, 3);
  if (lines.length > previewLines.length) {
    const lastIndex = previewLines.length - 1;
    previewLines[lastIndex] = `${previewLines[lastIndex]} …`;
  }
  return previewLines;
}

function SummaryCell({ label, value }: { label: string; value: string }) {
  return (
    <div className="bg-white px-4 py-3 dark:bg-slate-900/50">
      <p className="text-xs text-slate-500">{label}</p>
      <p className="break-anywhere mt-1 text-sm font-semibold text-slate-900 dark:text-white">{value}</p>
    </div>
  );
}

function SectionTitle({ title, accent, compact = false }: { title: string; accent: string; compact?: boolean }) {
  return (
    <div className={`flex items-center gap-2 ${compact ? "mb-0" : "mb-2"}`}>
      <div className={`h-6 w-1 rounded-full ${accent}`} />
      <h2 className="text-base font-semibold text-slate-900 dark:text-white sm:text-lg">{title}</h2>
    </div>
  );
}

function normalizeLocationText(value?: string) {
  return typeof value === "string" ? value.trim() : "";
}

function isCoordinateLikeLocation(value: string) {
  return /[-+]?\d+\.\d+\s*,\s*[-+]?\d+\.\d+/.test(value);
}

function isReadableLocationText(value?: string) {
  const normalizedValue = normalizeLocationText(value);
  if (!normalizedValue) {
    return false;
  }
  if (normalizedValue.includes("坐标")) {
    return false;
  }
  if (isCoordinateLikeLocation(normalizedValue)) {
    return false;
  }
  return normalizedValue.length >= 6;
}

function buildLocationCacheKey(latitude: number, longitude: number) {
  return `${latitude.toFixed(4)},${longitude.toFixed(4)}`;
}

async function resolveLocationDisplayName(latitude: number, longitude: number, preferredText?: string) {
  const normalizedPreferredText = normalizeLocationText(preferredText);
  if (isReadableLocationText(normalizedPreferredText)) {
    locationDisplayCache.set(buildLocationCacheKey(latitude, longitude), normalizedPreferredText);
    return normalizedPreferredText;
  }

  const cacheKey = buildLocationCacheKey(latitude, longitude);
  const cachedLocation = locationDisplayCache.get(cacheKey);
  if (isReadableLocationText(cachedLocation)) {
    return cachedLocation;
  }

  for (let attempt = 0; attempt < 2; attempt += 1) {
    try {
      const address = await reverseWorkGeocode(latitude, longitude);
      if (isReadableLocationText(address)) {
        const normalizedAddress = normalizeLocationText(address);
        locationDisplayCache.set(cacheKey, normalizedAddress);
        return normalizedAddress;
      }
    } catch {
      // ignore and continue retrying
    }
  }

  return "";
}

function formatLocationFallback() {
  return "当前位置待补充，请点击“刷新定位”重新解析地点名称";
}

function calculateDistanceMeters(origin: LocationPoint, target: LocationPoint) {
  const toRadians = (value: number) => (value * Math.PI) / 180;
  const earthRadius = 6371000;
  const deltaLatitude = toRadians(target.latitude - origin.latitude);
  const deltaLongitude = toRadians(target.longitude - origin.longitude);
  const originLatitude = toRadians(origin.latitude);
  const targetLatitude = toRadians(target.latitude);
  const haversine = Math.sin(deltaLatitude / 2) ** 2
    + Math.cos(originLatitude) * Math.cos(targetLatitude) * Math.sin(deltaLongitude / 2) ** 2;
  return 2 * earthRadius * Math.asin(Math.sqrt(haversine));
}

function getWorkSection(pathname: string): WorkSection | null {
  if (pathname === "/work" || pathname === "/work/") {
    return null;
  }
  if (pathname.startsWith("/work/checkin")) {
    return "checkin";
  }
  if (pathname.startsWith("/work/report")) {
    return "report";
  }
  return null;
}

function getHistoryLabelBySection(section: WorkSection) {
  return section === "checkin" ? "打卡" : "日报";
}

function buildSalesOptions(items: SalesExpansionItem[]): WorkRelationOption[] {
  return items.filter((item) => item.id).map((item) => ({ id: item.id, label: item.name || `人员拓展#${item.id}` }));
}

function buildChannelOptions(items: ChannelExpansionItem[]): WorkRelationOption[] {
  return items.filter((item) => item.id).map((item) => ({ id: item.id, label: item.name || `拓展渠道#${item.id}` }));
}

function buildOpportunityOptions(items: OpportunityItem[]): WorkRelationOption[] {
  return items.filter((item) => item.id).map((item) => ({ id: item.id, label: item.name || `商机#${item.id}` }));
}

function getBizTypeLabel(bizType: BizType) {
  if (bizType === "sales") {
    return "人员拓展";
  }
  if (bizType === "channel") {
    return "渠道拓展";
  }
  return "商机";
}

function getOptionsByBizType(
  bizType: BizType,
  salesOptions: WorkRelationOption[],
  channelOptions: WorkRelationOption[],
  opportunityOptions: WorkRelationOption[],
) {
  if (bizType === "sales") {
    return salesOptions;
  }
  if (bizType === "channel") {
    return channelOptions;
  }
  return opportunityOptions;
}

function normalizeObjectPickerQuery(query: string) {
  return query.replace(/@+/g, "").trimStart();
}

function getTemplateFields(bizType: BizType) {
  return [...reportFieldLabels[bizType]];
}

function buildEditorMentionLine(bizType: BizType, bizName: string) {
  return `@${getBizTypeLabel(bizType)} ${bizName}`;
}

function buildEditorTemplate(bizType: BizType, bizName: string, fieldValues?: Record<string, string>) {
  const lines = [buildEditorMentionLine(bizType, bizName)];
  for (const field of getTemplateFields(bizType)) {
    const value = fieldValues?.[field] || "";
    lines.push(`# ${field}：${value}`);
  }
  return lines.join("\n");
}

function clearReportLineSelection(item: WorkReportLineItem): WorkReportLineItem {
  return {
    ...createEmptyReportLine(),
    bizType: item.bizType,
    workDate: item.workDate,
  };
}

function parseTemplateValues(bizType: BizType, editorText: string) {
  const values: Record<string, string> = {};
  const fieldSet = getTemplateFields(bizType);
  let currentField: string | null = null;

  for (const rawLine of editorText.replace(/\r/g, "").split("\n")) {
    if (rawLine.startsWith("@")) {
      currentField = null;
      continue;
    }
    const parsedFieldLine = parseTemplateFieldLine(rawLine, fieldSet);
    if (parsedFieldLine) {
      currentField = parsedFieldLine.field;
      if (parsedFieldLine.field) {
        values[parsedFieldLine.field] = parsedFieldLine.value;
      }
      continue;
    }
    if (currentField) {
      const line = rawLine.trim();
      if (!line) {
        continue;
      }
      values[currentField] = values[currentField] ? `${values[currentField]}\n${line}` : line;
    }
  }

  return values;
}

function parseTemplateFieldLine(rawLine: string, fieldSet: string[]) {
  const match = rawLine.match(/^#\s*(.*)$/);
  if (!match) {
    return null;
  }

  const content = match[1].trim();
  for (const field of fieldSet) {
    if (!content.startsWith(field)) {
      continue;
    }

    const suffix = content.slice(field.length).trimStart();
    if (!suffix) {
      return { field, value: "" };
    }
    if (suffix.startsWith("：") || suffix.startsWith(":")) {
      return { field, value: suffix.slice(1).trim() };
    }
    return { field: null, value: "" };
  }

  return { field: null, value: "" };
}

function sanitizeEditorText(bizType: BizType, bizName: string, input: string) {
  return buildEditorTemplate(bizType, bizName, parseTemplateValues(bizType, input));
}

function normalizeLoadedLineItem(item: WorkReportLineItem): WorkReportLineItem {
  const normalized: WorkReportLineItem = { ...createEmptyReportLine(), ...item };
  const fieldValues: Record<string, string> = {};
  if (normalized.bizType === "sales" || normalized.bizType === "channel") {
    fieldValues["沟通内容"] = normalized.evaluationContent || extractContentByLabel(normalized.content, "沟通内容") || "";
    fieldValues["后续规划"] = normalized.nextPlan || extractContentByLabel(normalized.content, "后续规划") || "";
  } else {
    fieldValues["项目最新进展"] = normalized.latestProgress || extractContentByLabel(normalized.content, "项目最新进展") || "";
    fieldValues["后续规划"] = normalized.nextPlan || extractContentByLabel(normalized.content, "后续规划") || "";
  }
  const bizName = normalized.bizName || "未命名对象";
  normalized.editorText = sanitizeEditorText(
    normalized.bizType,
    bizName,
    normalized.editorText || buildEditorTemplate(normalized.bizType, bizName, fieldValues),
  );
  return normalized;
}

function normalizeReportLineItem(item: WorkReportLineItem, currentWorkDate: string): WorkReportLineItem {
  const bizName = item.bizName?.trim() || "";
  const editorText = bizName ? sanitizeEditorText(item.bizType, bizName, item.editorText || "") : "";
  return {
    ...item,
    workDate: currentWorkDate,
    bizName,
    editorText,
    content: buildLinePreview(item.bizType, parseTemplateValues(item.bizType, editorText)),
  };
}

function validateReportLineItems(lineItems: WorkReportLineItem[]) {
  for (const item of lineItems) {
    if (!item.bizId || !item.bizName || !item.editorText) {
      throw new Error("每一条日报都需要先通过 @ 选择对象");
    }
    const values = parseTemplateValues(item.bizType, item.editorText);
    if (item.bizType === "opportunity") {
      if (!values["项目最新进展"]?.trim()) {
        throw new Error(`商机“${item.bizName}”请填写项目最新进展`);
      }
      continue;
    }
    if (!values["沟通内容"]?.trim()) {
      throw new Error(`${getBizTypeLabel(item.bizType)}“${item.bizName}”请填写沟通内容`);
    }
  }
}

function buildLinePreview(bizType: BizType, values: Record<string, string>) {
  if (bizType === "opportunity") {
    return [
      values["项目最新进展"] ? `项目最新进展：${values["项目最新进展"]}` : "",
      values["后续规划"] ? `后续规划：${values["后续规划"]}` : "",
    ].filter(Boolean).join("\n");
  }
  return [
    values["沟通内容"] ? `沟通内容：${values["沟通内容"]}` : "",
    values["后续规划"] ? `后续规划：${values["后续规划"]}` : "",
  ].filter(Boolean).join("\n");
}

function buildReportSummary(lineItems: WorkReportLineItem[]) {
  return lineItems.map((item, index) => {
    const values = parseTemplateValues(item.bizType, item.editorText || "");
    const detail = buildLinePreview(item.bizType, values).replace(/\n/g, "；");
    return `${index + 1}. ${item.workDate} 跟进${getBizTypeLabel(item.bizType)}“${item.bizName || ""}”：${detail}`;
  }).join("\n");
}

function buildPlanSummary(planItems: WorkTomorrowPlanItem[]) {
  return planItems.map((item, index) => `${index + 1}. ${item.content.trim()}`).join("\n");
}

function extractContentByLabel(content: string | undefined, label: string) {
  if (!content) {
    return "";
  }
  const match = content.match(new RegExp(`${label}：([^\n]+)`));
  return match?.[1]?.trim() || "";
}

async function stampPhotoWithTimeAndLocation(file: File, locationText: string) {
  const imageUrl = URL.createObjectURL(file);
  try {
    const image = await loadImageElement(imageUrl);
    const canvas = document.createElement("canvas");
    canvas.width = image.naturalWidth;
    canvas.height = image.naturalHeight;
    const context = canvas.getContext("2d");
    if (!context) {
      throw new Error("图片处理失败");
    }

    context.drawImage(image, 0, 0);
    const timeText = format(new Date(), "yyyy-MM-dd HH:mm:ss");
    const locationLine = compactLocationWatermark(locationText);
    const stampLines = [timeText, locationLine];
    const fontSize = Math.max(28, Math.round(canvas.width / 28));
    context.font = `${fontSize}px sans-serif`;
    context.textBaseline = "bottom";
    const maxLineWidth = canvas.width * 0.58;
    const wrappedLines = stampLines.flatMap((line) => wrapCanvasText(context, line, maxLineWidth));
    const paddingX = Math.round(fontSize * 0.6);
    const paddingY = Math.round(fontSize * 0.45);
    const lineHeight = Math.round(fontSize * 1.35);
    const maxTextWidth = wrappedLines.reduce((max, line) => Math.max(max, context.measureText(line).width), 0);
    const boxWidth = Math.ceil(maxTextWidth + paddingX * 2);
    const boxHeight = Math.ceil(lineHeight * wrappedLines.length + paddingY * 2);
    const x = canvas.width - boxWidth - paddingX;
    const y = canvas.height - paddingY;

    context.fillStyle = "rgba(15, 23, 42, 0.68)";
    context.fillRect(x - paddingX, y - boxHeight + paddingY, boxWidth, boxHeight);
    context.fillStyle = "#ffffff";
    wrappedLines.forEach((line, index) => {
      const baselineY = y - lineHeight * (wrappedLines.length - 1 - index);
      context.fillText(line, x, baselineY);
    });

    const blob = await new Promise<Blob>((resolve, reject) => {
      canvas.toBlob((value) => {
        if (!value) {
          reject(new Error("图片处理失败"));
          return;
        }
        resolve(value);
      }, "image/jpeg", 0.92);
    });

    return new File([blob], file.name.replace(/\.[^.]+$/, "") + "-stamped.jpg", { type: "image/jpeg" });
  } finally {
    URL.revokeObjectURL(imageUrl);
  }
}

function compactLocationWatermark(locationText: string) {
  const normalized = locationText.replace(/\s+/g, " ").trim();
  if (!normalized) {
    return "位置待补充";
  }
  return normalized.length > 48 ? `${normalized.slice(0, 48)}...` : normalized;
}

function wrapCanvasText(context: CanvasRenderingContext2D, text: string, maxWidth: number) {
  if (!text) {
    return [""];
  }
  if (context.measureText(text).width <= maxWidth) {
    return [text];
  }

  const lines: string[] = [];
  let currentLine = "";
  for (const char of text) {
    const nextLine = `${currentLine}${char}`;
    if (currentLine && context.measureText(nextLine).width > maxWidth) {
      lines.push(currentLine);
      currentLine = char;
    } else {
      currentLine = nextLine;
    }
  }
  if (currentLine) {
    lines.push(currentLine);
  }
  return lines;
}

function supportsMobileCameraCapture() {
  if (typeof navigator === "undefined") {
    return false;
  }
  const userAgent = navigator.userAgent || "";
  return /Android|iPhone|iPod|Mobile|HarmonyOS/i.test(userAgent);
}

function loadImageElement(src: string) {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image();
    image.onload = () => resolve(image);
    image.onerror = () => reject(new Error("图片读取失败"));
    image.src = src;
  });
}
